diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index cfb29ebdfa9..69e13850258 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -1,6 +1,8 @@ ARG VARIANT="12" FROM mcr.microsoft.com/vscode/devcontainers/javascript-node:${VARIANT} +RUN curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | gpg --dearmor > /usr/share/keyrings/yarn-archive-keyring.gpg + # [Optional] Uncomment this section to install additional OS packages. # RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ # && apt-get -y install --no-install-recommends diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 0176b8317b3..104d9a38132 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -16,7 +16,8 @@ // Add the IDs of extensions you want installed when the container is created. "extensions": [ - "nickdodd79.gulptasks" + "nickdodd79.gulptasks", + "dbaeumer.vscode-eslint" ], // 9999 is web server, 9876 is karma diff --git a/.eslintrc.js b/.eslintrc.js index 06a5e81d9f5..fc3ad3afe66 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -19,7 +19,6 @@ module.exports = { 'import' ], globals: { - '$$PREBID_GLOBAL$$': false, 'BROWSERSTACK_USERNAME': false, 'BROWSERSTACK_KEY': false, 'FEATURES': 'readonly', diff --git a/.github/workflows/issue_tracker.yml b/.github/workflows/issue_tracker.yml index 4a9502e38c1..69cf4c5fc7f 100644 --- a/.github/workflows/issue_tracker.yml +++ b/.github/workflows/issue_tracker.yml @@ -14,7 +14,7 @@ jobs: steps: - name: Generate token id: generate_token - uses: tibdex/github-app-token@021a2405c7f990db57f5eae5397423dcc554159c + uses: tibdex/github-app-token@b62528385c34dbc9f38e5f4225ac829252d1ea92 with: app_id: ${{ secrets.ISSUE_APP_ID }} private_key: ${{ secrets.ISSUE_APP_PEM }} @@ -29,21 +29,30 @@ jobs: gh api graphql -f query=' query($org: String!, $number: Int!) { organization(login: $org){ - projectNext(number: $number) { + projectV2(number: $number) { id fields(first:100) { nodes { - id - name - settings + ... on ProjectV2Field { + id + name + } + ... on ProjectV2SingleSelectField { + id + name + options { + id + name + } + } } } } } }' -f org=$ORGANIZATION -F number=$PROJECT_NUMBER > project_data.json - echo 'PROJECT_ID='$(jq '.data.organization.projectNext.id' project_data.json) >> $GITHUB_ENV - echo 'DATE_FIELD_ID='$(jq '.data.organization.projectNext.fields.nodes[] | select(.name== "'"$DATE_FIELD"'") | .id' project_data.json) >> $GITHUB_ENV + echo 'PROJECT_ID='$(jq '.data.organization.projectV2.id' project_data.json) >> $GITHUB_ENV + echo 'DATE_FIELD_ID='$(jq '.data.organization.projectV2.fields.nodes[] | select(.name=="'"$DATE_FIELD"'") | .id' project_data.json) >> $GITHUB_ENV - name: Add issue to project env: @@ -52,9 +61,9 @@ jobs: run: | gh api graphql -f query=' mutation($project:ID!, $issue:ID!) { - addProjectNextItem(input: {projectId: $project, contentId: $issue}) { - projectNextItem { - id, + addProjectV2ItemById(input: {projectId: $project, contentId: $issue}) { + item { + id content { ... on Issue { createdAt @@ -67,8 +76,8 @@ jobs: } }' -f project=$PROJECT_ID -f issue=$ISSUE_ID > issue_data.json - echo 'ITEM_ID='$(jq '.data.addProjectNextItem.projectNextItem.id' issue_data.json) >> $GITHUB_ENV - echo 'ITEM_CREATION_DATE='$(jq '.data.addProjectNextItem.projectNextItem.content.createdAt' issue_data.json) >> $GITHUB_ENV + echo 'ITEM_ID='$(jq '.data.addProjectV2ItemById.item.id' issue_data.json) >> $GITHUB_ENV + echo 'ITEM_CREATION_DATE='$(jq '.data.addProjectV2ItemById.item.content.createdAt' issue_data.json | cut -c 2-11) >> $GITHUB_ENV - name: Set fields env: @@ -79,15 +88,17 @@ jobs: $project: ID! $item: ID! $date_field: ID! - $date_value: String! + $date_value: Date! ) { - set_creation_date: updateProjectNextItemField(input: { + set_creation_date: updateProjectV2ItemFieldValue(input: { projectId: $project itemId: $item fieldId: $date_field - value: $date_value + value: { + date: $date_value + } }) { - projectNextItem { + projectV2Item { id } } diff --git a/README.md b/README.md index bbc0d79ab41..fdbb5482ebf 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,6 @@ [![Build Status](https://circleci.com/gh/prebid/Prebid.js.svg?style=svg)](https://circleci.com/gh/prebid/Prebid.js) [![Percentage of issues still open](http://isitmaintained.com/badge/open/prebid/Prebid.js.svg)](http://isitmaintained.com/project/prebid/Prebid.js "Percentage of issues still open") [![Coverage Status](https://coveralls.io/repos/github/prebid/Prebid.js/badge.svg)](https://coveralls.io/github/prebid/Prebid.js) -[![Total Alerts](https://img.shields.io/lgtm/alerts/g/prebid/Prebid.js.svg?logo=lgtm&logoWidth=18)](https://lgtm.com/projects/g/prebid/Prebid.js/alerts/) # Prebid.js diff --git a/features.json b/features.json index c0f7e4d75a9..ccb2166a05f 100644 --- a/features.json +++ b/features.json @@ -1,3 +1,4 @@ [ - "NATIVE" + "NATIVE", + "VIDEO" ] diff --git a/integrationExamples/gpt/adloox.html b/integrationExamples/gpt/adloox.html index fd61267479d..78fba71f774 100644 --- a/integrationExamples/gpt/adloox.html +++ b/integrationExamples/gpt/adloox.html @@ -147,8 +147,13 @@ realTimeData: { auctionDelay: AUCTION_DELAY, dataProviders: [ + { + name: 'intersection', + waitForIt: true + }, { name: 'adloox', + waitForIt: true, params: { // optional, defaults shown thresholds: [ 50, 60, 70, 80, 90 ], slotinpath: false diff --git a/integrationExamples/gpt/gpp_us_hello_world.html b/integrationExamples/gpt/gpp_us_hello_world.html new file mode 100644 index 00000000000..28be86127fc --- /dev/null +++ b/integrationExamples/gpt/gpp_us_hello_world.html @@ -0,0 +1,124 @@ + + + + + + + + + + + + + + + + + + +

Prebid.js Test

+
Div-1
+ +
+ +
+ + + \ No newline at end of file diff --git a/integrationExamples/gpt/gpp_us_hello_world_iframe.html b/integrationExamples/gpt/gpp_us_hello_world_iframe.html new file mode 100644 index 00000000000..c0a62f9d72e --- /dev/null +++ b/integrationExamples/gpt/gpp_us_hello_world_iframe.html @@ -0,0 +1,12 @@ + + + + + + + + + + diff --git a/integrationExamples/gpt/gpp_us_hello_world_iframe_subpage.html b/integrationExamples/gpt/gpp_us_hello_world_iframe_subpage.html new file mode 100644 index 00000000000..8c2096d614d --- /dev/null +++ b/integrationExamples/gpt/gpp_us_hello_world_iframe_subpage.html @@ -0,0 +1,140 @@ + + + + + + + + + + + + + + + +

Prebid.js Test

+
Div-1
+ +
+ +
+ + + \ No newline at end of file diff --git a/integrationExamples/gpt/hello_world.html b/integrationExamples/gpt/hello_world.html old mode 100755 new mode 100644 diff --git a/integrationExamples/gpt/lemma_sample.html b/integrationExamples/gpt/lemma_sample.html new file mode 100755 index 00000000000..bdf72eeb484 --- /dev/null +++ b/integrationExamples/gpt/lemma_sample.html @@ -0,0 +1,129 @@ + + + + + + + + + + + + +
+ + +
+ + + \ No newline at end of file diff --git a/integrationExamples/gpt/neuwoRtdProvider_example.html b/integrationExamples/gpt/neuwoRtdProvider_example.html new file mode 100644 index 00000000000..142a7c39613 --- /dev/null +++ b/integrationExamples/gpt/neuwoRtdProvider_example.html @@ -0,0 +1,201 @@ + + + + + + + + + + +

Basic Prebid.js Example using neuwoRtdProvider

+
+ Looks like you're not following the testing environment setup, try accessing http://localhost:9999/integrationExamples/gpt/neuwoRtdProvider_example.html + after running commands in the prebid.js source folder that includes libraries/modules/neuwoRtdProvider.js + + npm ci + npm i -g gulp-cli + gulp serve --modules=rtdModule,neuwoRtdProvider,appnexusBidAdapter + +
+
+

Add token and url to use for Neuwo extension configuration

+ + + + +
+ +
Div-1
+
+ Ad spot div-1: This content will be replaced by prebid.js and/or related components once you click "Update" +
+ +
+ +
Div-2
+
+ Ad spot div-2: Replaces this text as well, if everything goes to plan + + +
+ + + + \ No newline at end of file diff --git a/integrationExamples/gpt/permutiveRtdProvider_example.html b/integrationExamples/gpt/permutiveRtdProvider_example.html index 118cc678726..554f2081c6d 100644 --- a/integrationExamples/gpt/permutiveRtdProvider_example.html +++ b/integrationExamples/gpt/permutiveRtdProvider_example.html @@ -15,7 +15,8 @@ _papns: ['appnexus1', 'appnexus2'], _psegs: ['1234', '1000001', '1000002'], _ppam: ['ppam1', 'ppam2'], - _pcrprs: ['pcrprs1', 'pcrprs2'] + _pcrprs: ['pcrprs1', 'pcrprs2'], + _pssps: { ssps: ['appnexus', 'some other'], cohorts: ['abcd', 'efgh', 'ijkl'] }, } for (let key in data) { diff --git a/integrationExamples/gpt/relevadRtdProvider_example.html b/integrationExamples/gpt/relevadRtdProvider_example.html new file mode 100644 index 00000000000..daa6d27cf33 --- /dev/null +++ b/integrationExamples/gpt/relevadRtdProvider_example.html @@ -0,0 +1,138 @@ + + + + + + + + + + + + + + + +

Basic Prebid.js Example

+
Div-1
+
+ +
+ +
Div-2
+
+ +
+ + \ No newline at end of file diff --git a/integrationExamples/gpt/topics_frame.html b/integrationExamples/gpt/topics_frame.html new file mode 100644 index 00000000000..7a12b030a6a --- /dev/null +++ b/integrationExamples/gpt/topics_frame.html @@ -0,0 +1,43 @@ + + + + Topics demo + + + + + + + + + \ No newline at end of file diff --git a/integrationExamples/gpt/userId_example.html b/integrationExamples/gpt/userId_example.html index 5a6c80c4adf..4f94c73c281 100644 --- a/integrationExamples/gpt/userId_example.html +++ b/integrationExamples/gpt/userId_example.html @@ -249,7 +249,17 @@ } }, { - "name": "uid2" + "name": "uid2", + "params": { + "uid2Token": { + "advertising_token": "example token", + "refresh_token": "aslkdjaslkjdaslkhj", + "identity_expires": Date.now() + 60*1000, + "refresh_from": Date.now() - 10*1000, + "refresh_expires": Date.now() + 12*60*60*1000, + "refresh_response_key": null + } + } } , { diff --git a/integrationExamples/gpt/x-domain/creative.html b/integrationExamples/gpt/x-domain/creative.html index bea8b70b4fe..2216d0ed6ae 100644 --- a/integrationExamples/gpt/x-domain/creative.html +++ b/integrationExamples/gpt/x-domain/creative.html @@ -1,6 +1,6 @@ diff --git a/integrationExamples/videoModule/jwplayer/eventListeners.html b/integrationExamples/videoModule/jwplayer/eventListeners.html index b4f5aaa5443..0b43ff44d6c 100644 --- a/integrationExamples/videoModule/jwplayer/eventListeners.html +++ b/integrationExamples/videoModule/jwplayer/eventListeners.html @@ -87,7 +87,6 @@ pbjs.onEvent('videoPlaybackRequest', (e) => { console.log('videos pb playbackRequest: ', e); - pbjs.requestBids(adUnits); }); pbjs.onEvent('videoAutostartBlocked', (e) => { @@ -222,6 +221,10 @@ console.log('videos pb auction ad load attempt: ', e); }); + pbjs.onEvent('videoAuctionAdLoadQueued', (e) => { + console.log('videos pb auction ad load queued: ', e); + }); + pbjs.onEvent('videoAuctionAdLoadAttempt', event => { console.log('The Video Module is attempting to load an ad into the player! \n', event); }); @@ -233,6 +236,8 @@ pbjs.onEvent('videoBidError', event => { console.log('The ad error resulted from a bid! \n', event); }); + + pbjs.requestBids(adUnits); }); diff --git a/integrationExamples/videoModule/jwplayer/gamAdServerMediation.html b/integrationExamples/videoModule/jwplayer/gamAdServerMediation.html index 4612d03f336..296a3265bd1 100644 --- a/integrationExamples/videoModule/jwplayer/gamAdServerMediation.html +++ b/integrationExamples/videoModule/jwplayer/gamAdServerMediation.html @@ -94,10 +94,6 @@ console.log('player setup failed: ', e); }); - pbjs.onEvent('videoPlaybackRequest', (e) => { - pbjs.requestBids(adUnits); - }); - pbjs.onEvent('videoAdRequest', (e) => { console.log('videos pb ad request: ', e); }); @@ -109,6 +105,8 @@ pbjs.onEvent('videoBidImpression', e => { console.log('An Ad Impression came from a Bid: ', e); }); + + pbjs.requestBids(adUnits); }); diff --git a/integrationExamples/videoModule/jwplayer/mediaMetadata.html b/integrationExamples/videoModule/jwplayer/mediaMetadata.html index 97e35c4de0d..815f5c4d6d7 100644 --- a/integrationExamples/videoModule/jwplayer/mediaMetadata.html +++ b/integrationExamples/videoModule/jwplayer/mediaMetadata.html @@ -56,7 +56,6 @@ pbjs.onEvent('videoSetupComplete', e => { console.log('player setup complete: ', e); - pbjs.requestBids(adUnits); }); pbjs.onEvent('videoSetupFailed', e => { @@ -66,6 +65,8 @@ pbjs.onEvent('videoContentLoaded', (e) => { console.log('videos pb contentLoaded: ', e); }); + + pbjs.requestBids(adUnits); }); diff --git a/integrationExamples/videoModule/jwplayer/playlist.html b/integrationExamples/videoModule/jwplayer/playlist.html index eb9a9b12700..b77a1ec05fc 100644 --- a/integrationExamples/videoModule/jwplayer/playlist.html +++ b/integrationExamples/videoModule/jwplayer/playlist.html @@ -97,7 +97,6 @@ // request a bid when media is loaded pbjs.onEvent('videoContentLoaded', (e) => { console.log('videos pb contentLoaded: ', e); - pbjs.requestBids(adUnits); }); pbjs.onEvent('videoComplete', (e) => { @@ -107,6 +106,8 @@ pbjs.onEvent('videoPlaylistComplete', (e) => { console.log('videos pb playlistComplete: ', e); }); + + pbjs.requestBids(adUnits); }); diff --git a/integrationExamples/videoModule/videojs/bidMarkedAsUsed.html b/integrationExamples/videoModule/videojs/bidMarkedAsUsed.html index 3c21130e8f5..b645a58a4fc 100644 --- a/integrationExamples/videoModule/videojs/bidMarkedAsUsed.html +++ b/integrationExamples/videoModule/videojs/bidMarkedAsUsed.html @@ -101,10 +101,6 @@ console.log('player setup failed: ', e); }); - pbjs.onEvent('videoContentLoaded', (e) => { - pbjs.requestBids(adUnits); - }); - pbjs.onEvent('videoBidError', e => { console.log('An Ad Error came from a Bid: ', e); }); @@ -113,6 +109,7 @@ console.log('An Ad Impression came from a Bid: ', e); }); + pbjs.requestBids(adUnits); }); diff --git a/integrationExamples/videoModule/videojs/eventListeners.html b/integrationExamples/videoModule/videojs/eventListeners.html index 63de878715f..16edaf4da41 100644 --- a/integrationExamples/videoModule/videojs/eventListeners.html +++ b/integrationExamples/videoModule/videojs/eventListeners.html @@ -209,6 +209,10 @@ console.log('videos pb auction ad load attempt: ', e); }); + pbjs.onEvent('videoAuctionAdLoadQueued', (e) => { + console.log('videos pb auction ad load queued: ', e); + }); + pbjs.onEvent('videoAuctionAdLoadAbort', (e) => { console.log('videos pb auction ad load attempt: ', e); }); diff --git a/integrationExamples/videoModule/videojs/gamAdServerMediation.html b/integrationExamples/videoModule/videojs/gamAdServerMediation.html index 72deba861bf..9efd77e9681 100644 --- a/integrationExamples/videoModule/videojs/gamAdServerMediation.html +++ b/integrationExamples/videoModule/videojs/gamAdServerMediation.html @@ -96,8 +96,6 @@ pbjs.addAdUnits(adUnits); - pbjs.requestBids(adUnits); - pbjs.onEvent('videoSetupComplete', e => { // Load media with its Metadata when the video player is done instantiating. videojs('player').loadMedia({ @@ -125,6 +123,7 @@ console.log('An Ad Impression came from a Bid: ', e); }); + pbjs.requestBids(adUnits); }); diff --git a/integrationExamples/videoModule/videojs/playlist.html b/integrationExamples/videoModule/videojs/playlist.html index a9354e06c83..a9e3d1bc99b 100644 --- a/integrationExamples/videoModule/videojs/playlist.html +++ b/integrationExamples/videoModule/videojs/playlist.html @@ -126,15 +126,15 @@ console.log('videos pb playlist: ', e); }); - // request a bid when media is loaded pbjs.onEvent('videoContentLoaded', (e) => { console.log('videos pb contentLoaded: ', e); - pbjs.requestBids(adUnits); }); pbjs.onEvent('videoComplete', (e) => { console.log('videos pb complete: ', e); }); + + pbjs.requestBids(adUnits); }); diff --git a/karma.conf.maker.js b/karma.conf.maker.js index b20f73d74bc..e05d5b08afd 100644 --- a/karma.conf.maker.js +++ b/karma.conf.maker.js @@ -110,7 +110,7 @@ module.exports = function(codeCoverage, browserstack, watchMode, file, disableFe var webpackConfig = newWebpackConfig(codeCoverage, disableFeatures); var plugins = newPluginsArray(browserstack); - var files = file ? ['test/test_deps.js', file] : ['test/test_index.js']; + var files = file ? ['test/test_deps.js', file, 'test/helpers/hookSetup.js'].flatMap(f => f) : ['test/test_index.js']; // This file opens the /debug.html tab automatically. // It has no real value unless you're running --watch, and intend to do some debugging in the browser. if (watchMode) { diff --git a/libraries/domainOverrideToRootDomain/index.js b/libraries/domainOverrideToRootDomain/index.js new file mode 100644 index 00000000000..95a334755d1 --- /dev/null +++ b/libraries/domainOverrideToRootDomain/index.js @@ -0,0 +1,39 @@ +/** + * Create a domainOverride callback for an ID module, closing over + * an instance of StorageManager. + * + * The domainOverride function, given document.domain, will return + * the topmost domain we are able to set a cookie on. For example, + * given subdomain.example.com, it would return example.com. + * + * @param {StorageManager} storage e.g. from getStorageManager() + * @param {string} moduleName the name of the module using this function + * @returns {function(): string} + */ +export function domainOverrideToRootDomain(storage, moduleName) { + return function() { + const domainElements = document.domain.split('.'); + const cookieName = `_gd${Date.now()}_${moduleName}`; + + for (let i = 0, topDomain, testCookie; i < domainElements.length; i++) { + const nextDomain = domainElements.slice(i).join('.'); + + // write test cookie + storage.setCookie(cookieName, '1', undefined, undefined, nextDomain); + + // read test cookie to verify domain was valid + testCookie = storage.getCookie(cookieName); + + // delete test cookie + storage.setCookie(cookieName, '', 'Thu, 01 Jan 1970 00:00:01 GMT', undefined, nextDomain); + + if (testCookie === '1') { + // cookie was written successfully using test domain so the topDomain is updated + topDomain = nextDomain; + } else { + // cookie failed to write using test domain so exit by returning the topDomain + return topDomain; + } + } + } +} diff --git a/libraries/ortbConverter/converter.js b/libraries/ortbConverter/converter.js index 2057af8b6d3..c367aec268a 100644 --- a/libraries/ortbConverter/converter.js +++ b/libraries/ortbConverter/converter.js @@ -90,12 +90,13 @@ export function ortbConverter({ req: Object.assign({bidRequests}, defaultContext, context), imp: {} } + ctx.req.impContext = ctx.imp; const imps = bidRequests.map(bidRequest => { const impContext = Object.assign({bidderRequest, reqContext: ctx.req}, defaultContext, context); const result = buildImp(bidRequest, impContext); if (result != null) { if (result.hasOwnProperty('id')) { - impContext.bidRequest = bidRequest; + Object.assign(impContext, {bidRequest, imp: result}); ctx.imp[result.id] = impContext; return result; } @@ -116,7 +117,7 @@ export function ortbConverter({ throw new Error('ortbRequest passed to `fromORTB` must be the same object returned by `toORTB`') } function augmentContext(ctx, extraParams = {}) { - return Object.assign({ortbRequest: request}, extraParams, ctx); + return Object.assign(ctx, {ortbRequest: request}, extraParams, ctx); } const impsById = Object.fromEntries((request.imp || []).map(imp => [imp.id, imp])); const bidResponses = (response.seatbid || []).flatMap(seatbid => diff --git a/libraries/ortbConverter/processors/default.js b/libraries/ortbConverter/processors/default.js index 70acb7c9953..00922608707 100644 --- a/libraries/ortbConverter/processors/default.js +++ b/libraries/ortbConverter/processors/default.js @@ -1,10 +1,10 @@ -import {deepSetValue, getDefinedParams, getDNT, mergeDeep} from '../../../src/utils.js'; +import {deepSetValue, mergeDeep} from '../../../src/utils.js'; import {bannerResponseProcessor, fillBannerImp} from './banner.js'; import {fillVideoImp, fillVideoResponse} from './video.js'; import {setResponseMediaType} from './mediaType.js'; import {fillNativeImp, fillNativeResponse} from './native.js'; import {BID_RESPONSE, IMP, REQUEST} from '../../../src/pbjsORTB.js'; -import {config} from '../../../src/config.js'; +import {clientSectionChecker} from '../../../src/fpd/oneClient.js'; export const DEFAULT_PROCESSORS = { [REQUEST]: { @@ -15,18 +15,10 @@ export const DEFAULT_PROCESSORS = { mergeDeep(ortbRequest, bidderRequest.ortb2) } }, - // override FPD app, site, and device with getConfig('app'), etc if defined - // TODO: these should be deprecated for v8 - appFpd: fpdFromTopLevelConfig('app'), - siteFpd: fpdFromTopLevelConfig('site'), - deviceFpd: fpdFromTopLevelConfig('device'), - device: { - // sets device w / h / ua / language - fn: setDevice - }, - site: { - // sets site.domain, page, and ref from refererInfo - fn: setSite + onlyOneClient: { + // make sure only one of 'dooh', 'app', 'site' is set in request + priority: -99, + fn: clientSectionChecker('ORTB request') }, props: { // sets request properties id, tmax, test, source.tid @@ -41,15 +33,7 @@ export const DEFAULT_PROCESSORS = { } deepSetValue(ortbRequest, 'source.tid', ortbRequest.source?.tid || bidderRequest.auctionId); } - }, - coppa: { - fn(ortbRequest) { - const coppa = config.getConfig('coppa'); - if (typeof coppa === 'boolean') { - deepSetValue(ortbRequest, 'regs.coppa', coppa ? 1 : 0); - } - } - }, + } }, [IMP]: { fpd: { @@ -69,10 +53,6 @@ export const DEFAULT_PROCESSORS = { // populates imp.banner fn: fillBannerImp }, - video: { - // populates imp.video - fn: fillVideoImp - }, pbadslot: { // removes imp.ext.data.pbaslot if it's not a string // TODO: is this needed? @@ -94,10 +74,6 @@ export const DEFAULT_PROCESSORS = { // sets banner response attributes if bidResponse.mediaType === BANNER fn: bannerResponseProcessor(), }, - video: { - // sets video response attributes if bidResponse.mediaType === VIDEO - fn: fillVideoResponse - }, props: { // sets base bidResponse properties common to all types of bids fn(bidResponse, bid, context) { @@ -138,30 +114,13 @@ if (FEATURES.NATIVE) { } } -function fpdFromTopLevelConfig(prop) { - return { - priority: 90, // after FPD from 'ortb2', before the rest - fn(ortbRequest) { - const data = config.getConfig(prop); - if (typeof data === 'object') { - ortbRequest[prop] = data; - } - } +if (FEATURES.VIDEO) { + DEFAULT_PROCESSORS[IMP].video = { + // populates imp.video + fn: fillVideoImp } -} - -export function setDevice(ortbRequest) { - ortbRequest.device = Object.assign({ - w: window.innerWidth, - h: window.innerHeight, - dnt: getDNT() ? 1 : 0, - ua: window.navigator.userAgent, - language: window.navigator.language.split('-').shift() - }, ortbRequest.device); -} - -export function setSite(ortbRequest, bidderRequest) { - if (bidderRequest.refererInfo) { - ortbRequest.site = Object.assign(getDefinedParams(bidderRequest.refererInfo, ['page', 'domain', 'ref']), ortbRequest.site); + DEFAULT_PROCESSORS[BID_RESPONSE].video = { + // sets video response attributes if bidResponse.mediaType === VIDEO + fn: fillVideoResponse } } diff --git a/libraries/pbsExtensions/processors/adUnitCode.js b/libraries/pbsExtensions/processors/adUnitCode.js new file mode 100644 index 00000000000..f936e0f662f --- /dev/null +++ b/libraries/pbsExtensions/processors/adUnitCode.js @@ -0,0 +1,13 @@ +import {deepSetValue} from '../../../src/utils.js'; + +export function setImpAdUnitCode(imp, bidRequest) { + const adUnitCode = bidRequest.adUnitCode; + + if (adUnitCode) { + deepSetValue( + imp, + `ext.prebid.adunitcode`, + adUnitCode + ); + } +} diff --git a/libraries/pbsExtensions/processors/pbs.js b/libraries/pbsExtensions/processors/pbs.js index 56a2a391c76..0ed2d12fad8 100644 --- a/libraries/pbsExtensions/processors/pbs.js +++ b/libraries/pbsExtensions/processors/pbs.js @@ -3,6 +3,7 @@ import {deepAccess, isPlainObject, isStr, mergeDeep} from '../../../src/utils.js import {extPrebidMediaType} from './mediaType.js'; import {setRequestExtPrebidAliases} from './aliases.js'; import {setImpBidParams} from './params.js'; +import {setImpAdUnitCode} from './adUnitCode.js'; import {setRequestExtPrebid, setRequestExtPrebidChannel} from './requestExtPrebid.js'; import {setBidResponseVideoCache} from './video.js'; @@ -26,6 +27,10 @@ export const PBS_PROCESSORS = { // sets bid ext.prebid.bidder.[bidderCode] with bidRequest.params, passed through transformBidParams if necessary fn: setImpBidParams }, + adUnitCode: { + // sets bid ext.prebid.adunitcode + fn: setImpAdUnitCode + } }, [BID_RESPONSE]: { mediaType: { diff --git a/libraries/video/constants/enums.js b/libraries/video/constants/constants.js similarity index 65% rename from libraries/video/constants/enums.js rename to libraries/video/constants/constants.js index b0755020580..55e3785ccc2 100644 --- a/libraries/video/constants/enums.js +++ b/libraries/video/constants/constants.js @@ -1,3 +1,5 @@ +export const videoKey = 'video'; + export const PLAYBACK_MODE = { VOD: 0, LIVE: 1, diff --git a/libraries/video/constants/events.js b/libraries/video/constants/events.js index 5be594b1b48..b7932adf621 100644 --- a/libraries/video/constants/events.js +++ b/libraries/video/constants/events.js @@ -52,6 +52,7 @@ export const allVideoEvents = [ ]; export const AUCTION_AD_LOAD_ATTEMPT = 'auctionAdLoadAttempt'; +export const AUCTION_AD_LOAD_QUEUED = 'auctionAdLoadQueued'; export const AUCTION_AD_LOAD_ABORT = 'auctionAdLoadAbort'; export const BID_IMPRESSION = 'bidImpression'; export const BID_ERROR = 'bidError'; diff --git a/libraries/video/shared/helpers.js b/libraries/video/shared/helpers.js new file mode 100644 index 00000000000..1b87f3bf1f3 --- /dev/null +++ b/libraries/video/shared/helpers.js @@ -0,0 +1,5 @@ +import { videoKey } from '../constants/constants.js' + +export function getExternalVideoEventName(eventName) { + return videoKey + eventName.replace(/^./, eventName[0].toUpperCase()); +} diff --git a/libraries/video/shared/parentModule.js b/libraries/video/shared/parentModule.js index e96113fd894..06c71ebd75b 100644 --- a/libraries/video/shared/parentModule.js +++ b/libraries/video/shared/parentModule.js @@ -72,7 +72,6 @@ export function SubmoduleBuilder(submoduleDirectory_, sharedUtils_) { } const submodule = submoduleFactory(config, sharedUtils); - submodule && submodule.init && submodule.init(); return submodule; } diff --git a/modules/.submodules.json b/modules/.submodules.json index f8c30459e1e..226673488cf 100644 --- a/modules/.submodules.json +++ b/modules/.submodules.json @@ -7,7 +7,7 @@ "amxIdSystem", "britepoolIdSystem", "connectIdSystem", - "cpexIdSystem", + "czechAdIdSystem", "criteoIdSystem", "dacIdSystem", "deepintentDpesIdSystem", @@ -55,9 +55,11 @@ "aaxBlockmeterRtdProvider", "airgridRtdProvider", "akamaiDapRtdProvider", + "arcspanRtdProvider", "blueconicRtdProvider", "browsiRtdProvider", "captifyRtdProvider", + "confiantRtdProvider", "dgkeywordRtdProvider", "geoedgeRtdProvider", "hadronRtdProvider", @@ -76,7 +78,6 @@ "zeusPrimeRtdProvider" ], "fpdModule": [ - "enrichmentFpdModule", "validationFpdModule", "topicsFpdModule" ], diff --git a/modules/33acrossBidAdapter.js b/modules/33acrossBidAdapter.js index d9524a281f8..e9901794ff9 100644 --- a/modules/33acrossBidAdapter.js +++ b/modules/33acrossBidAdapter.js @@ -167,7 +167,8 @@ function buildRequests(bidRequests, bidderRequest) { ttxSettings, gdprConsent, uspConsent, - pageUrl + pageUrl, + referer } = _buildRequestParams(bidRequests, bidderRequest); const groupedRequests = _buildRequestGroups(ttxSettings, bidRequests); @@ -181,6 +182,7 @@ function buildRequests(bidRequests, bidderRequest) { gdprConsent, uspConsent, pageUrl, + referer, ttxSettings }) ) @@ -199,7 +201,9 @@ function _buildRequestParams(bidRequests, bidderRequest) { const uspConsent = bidderRequest && bidderRequest.uspConsent; - const pageUrl = bidderRequest?.refererInfo?.page + const pageUrl = bidderRequest?.refererInfo?.page; + + const referer = bidderRequest?.refererInfo?.ref; adapterState.uniqueSiteIds = bidRequests.map(req => req.params.siteId).filter(uniques); @@ -207,7 +211,8 @@ function _buildRequestParams(bidRequests, bidderRequest) { ttxSettings, gdprConsent, uspConsent, - pageUrl + pageUrl, + referer } } @@ -241,7 +246,7 @@ function _getMRAKey(bidRequest) { } // Infer the necessary data from valid bid for a minimal ttxRequest and create HTTP request -function _createServerRequest({ bidRequests, gdprConsent = {}, uspConsent, pageUrl, ttxSettings }) { +function _createServerRequest({ bidRequests, gdprConsent = {}, uspConsent, pageUrl, referer, ttxSettings }) { const ttxRequest = {}; const firstBidRequest = bidRequests[0]; const { siteId, test } = firstBidRequest.params; @@ -262,6 +267,10 @@ function _createServerRequest({ bidRequests, gdprConsent = {}, uspConsent, pageU ttxRequest.site.page = pageUrl; } + if (referer) { + ttxRequest.site.ref = referer; + } + ttxRequest.id = firstBidRequest.auctionId; if (gdprConsent.consentString) { diff --git a/modules/33acrossIdSystem.js b/modules/33acrossIdSystem.js index 5e07fe9523d..be81b26b110 100644 --- a/modules/33acrossIdSystem.js +++ b/modules/33acrossIdSystem.js @@ -17,8 +17,11 @@ const CALLER_NAME = 'pbjs'; function getEnvelope(response) { if (!response.succeeded) { - logError(`${MODULE_NAME}: Unsuccessful response`); - + if (response.error == 'Cookied User') { + logMessage(`${MODULE_NAME}: Unsuccessful response`.concat(' ', response.error)); + } else { + logError(`${MODULE_NAME}: Unsuccessful response`.concat(' ', response.error)); + } return; } diff --git a/modules/acuityAdsBidAdapter.js b/modules/acuityAdsBidAdapter.js index f469fe48c60..b0bb132ddae 100644 --- a/modules/acuityAdsBidAdapter.js +++ b/modules/acuityAdsBidAdapter.js @@ -150,7 +150,7 @@ export const spec = { coppa: config.getConfig('coppa') === true ? 1 : 0, ccpa: bidderRequest.uspConsent || undefined, gdpr: bidderRequest.gdprConsent || undefined, - tmax: config.getConfig('bidderTimeout') + tmax: bidderRequest.timeout }; const len = validBidRequests.length; diff --git a/modules/adagioBidAdapter.js b/modules/adagioBidAdapter.js index 8dcd95f933b..86021d2a90c 100644 --- a/modules/adagioBidAdapter.js +++ b/modules/adagioBidAdapter.js @@ -15,10 +15,12 @@ import { isFn, isInteger, isNumber, + isArrayOfNums, logError, logInfo, logWarn, mergeDeep, + isStr, } from '../src/utils.js'; import {config} from '../src/config.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; @@ -26,12 +28,12 @@ import {loadExternalScript} from '../src/adloader.js'; import {verify} from 'criteo-direct-rsa-validate/build/verify.js'; import {getStorageManager} from '../src/storageManager.js'; import {getRefererInfo, parseDomain} from '../src/refererDetection.js'; -import {createEidsArray} from './userId/eids.js'; import {BANNER, NATIVE, VIDEO} from '../src/mediaTypes.js'; import {Renderer} from '../src/Renderer.js'; import {OUTSTREAM} from '../src/video.js'; import { getGlobal } from '../src/prebidGlobal.js'; import { convertOrtbRequestToProprietaryNative } from '../src/native.js'; +import { userSync } from '../src/userSync.js'; const BIDDER_CODE = 'adagio'; const LOG_PREFIX = 'Adagio:'; @@ -41,40 +43,39 @@ const SUPPORTED_MEDIA_TYPES = [BANNER, NATIVE, VIDEO]; const ADAGIO_TAG_URL = 'https://script.4dex.io/localstore.js'; const ADAGIO_LOCALSTORAGE_KEY = 'adagioScript'; const GVLID = 617; -export const storage = getStorageManager({gvlid: GVLID, bidderCode: BIDDER_CODE}); +export const storage = getStorageManager({bidderCode: BIDDER_CODE}); export const RENDERER_URL = 'https://script.4dex.io/outstream-player.js'; const MAX_SESS_DURATION = 30 * 60 * 1000; const ADAGIO_PUBKEY = 'AL16XT44Sfp+8SHVF1UdC7hydPSMVLMhsYknKDdwqq+0ToDSJrP0+Qh0ki9JJI2uYm/6VEYo8TJED9WfMkiJ4vf02CW3RvSWwc35bif2SK1L8Nn/GfFYr/2/GG/Rm0vUsv+vBHky6nuuYls20Og0HDhMgaOlXoQ/cxMuiy5QSktp'; const ADAGIO_PUBKEY_E = 65537; const CURRENCY = 'USD'; -// This provide a whitelist and a basic validation -// of OpenRTB 2.5 options used by the Adagio SSP. -// https://www.iab.com/wp-content/uploads/2016/03/OpenRTB-API-Specification-Version-2-5-FINAL.pdf +// This provide a whitelist and a basic validation of OpenRTB 2.6 options used by the Adagio SSP. +// https://iabtechlab.com/wp-content/uploads/2022/04/OpenRTB-2-6_FINAL.pdf export const ORTB_VIDEO_PARAMS = { 'mimes': (value) => Array.isArray(value) && value.length > 0 && value.every(v => typeof v === 'string'), 'minduration': (value) => isInteger(value), 'maxduration': (value) => isInteger(value), - 'protocols': (value) => Array.isArray(value) && value.every(v => [1, 2, 3, 4, 5, 6, 7, 8, 9, 10].indexOf(v) !== -1), + 'protocols': (value) => isArrayOfNums(value), 'w': (value) => isInteger(value), 'h': (value) => isInteger(value), 'startdelay': (value) => isInteger(value), - 'placement': (value) => [1, 2, 3, 4, 5].indexOf(value) !== -1, - 'linearity': (value) => [1, 2].indexOf(value) !== -1, - 'skip': (value) => [0, 1].indexOf(value) !== -1, + 'placement': (value) => isInteger(value), + 'linearity': (value) => isInteger(value), + 'skip': (value) => isInteger(value), 'skipmin': (value) => isInteger(value), 'skipafter': (value) => isInteger(value), 'sequence': (value) => isInteger(value), - 'battr': (value) => Array.isArray(value) && value.every(v => Array.from({length: 17}, (_, i) => i + 1).indexOf(v) !== -1), + 'battr': (value) => isArrayOfNums(value), 'maxextended': (value) => isInteger(value), 'minbitrate': (value) => isInteger(value), 'maxbitrate': (value) => isInteger(value), - 'boxingallowed': (value) => [0, 1].indexOf(value) !== -1, - 'playbackmethod': (value) => Array.isArray(value) && value.every(v => [1, 2, 3, 4, 5, 6].indexOf(v) !== -1), - 'playbackend': (value) => [1, 2, 3].indexOf(value) !== -1, - 'delivery': (value) => [1, 2, 3].indexOf(value) !== -1, - 'pos': (value) => [0, 1, 2, 3, 4, 5, 6, 7].indexOf(value) !== -1, - 'api': (value) => Array.isArray(value) && value.every(v => [1, 2, 3, 4, 5, 6].indexOf(v) !== -1) + 'boxingallowed': (value) => isInteger(value), + 'playbackmethod': (value) => isArrayOfNums(value), + 'playbackend': (value) => isInteger(value), + 'delivery': (value) => isInteger(value), + 'pos': (value) => isInteger(value), + 'api': (value) => isArrayOfNums(value) }; let currentWindow; @@ -399,8 +400,8 @@ function _getSchain(bidRequest) { } function _getEids(bidRequest) { - if (deepAccess(bidRequest, 'userId')) { - return createEidsArray(bidRequest.userId); + if (deepAccess(bidRequest, 'userIdAsEids')) { + return bidRequest.userIdAsEids; } } @@ -669,10 +670,8 @@ function autoFillParams(bid) { } // extra params - setExtraParam(bid, 'environment'); setExtraParam(bid, 'pagetype'); setExtraParam(bid, 'category'); - setExtraParam(bid, 'subcategory'); } function getPageDimensions() { @@ -761,46 +760,51 @@ function getSlotPosition(adUnitElementId) { position.x = Math.round(sfGeom.t); position.y = Math.round(sfGeom.l); } else if (canAccessTopWindow()) { - // window.top based computing - const wt = getWindowTop(); - const d = wt.document; + try { + // window.top based computing + const wt = getWindowTop(); + const d = wt.document; - let domElement; + let domElement; - if (inIframe() === true) { - const ws = getWindowSelf(); - const currentElement = ws.document.getElementById(adUnitElementId); - domElement = internal.getElementFromTopWindow(currentElement, ws); - } else { - domElement = wt.document.getElementById(adUnitElementId); - } + if (inIframe() === true) { + const ws = getWindowSelf(); + const currentElement = ws.document.getElementById(adUnitElementId); + domElement = internal.getElementFromTopWindow(currentElement, ws); + } else { + domElement = wt.document.getElementById(adUnitElementId); + } - if (!domElement) { - return ''; - } + if (!domElement) { + return ''; + } - let box = domElement.getBoundingClientRect(); - - const docEl = d.documentElement; - const body = d.body; - const clientTop = d.clientTop || body.clientTop || 0; - const clientLeft = d.clientLeft || body.clientLeft || 0; - const scrollTop = wt.pageYOffset || docEl.scrollTop || body.scrollTop; - const scrollLeft = wt.pageXOffset || docEl.scrollLeft || body.scrollLeft; - - const elComputedStyle = wt.getComputedStyle(domElement, null); - const elComputedDisplay = elComputedStyle.display || 'block'; - const mustDisplayElement = elComputedDisplay === 'none'; - - if (mustDisplayElement) { - domElement.style = domElement.style || {}; - const originalDisplay = domElement.style.display; - domElement.style.display = 'block'; - box = domElement.getBoundingClientRect(); - domElement.style.display = originalDisplay || null; + let box = domElement.getBoundingClientRect(); + + const docEl = d.documentElement; + const body = d.body; + const clientTop = d.clientTop || body.clientTop || 0; + const clientLeft = d.clientLeft || body.clientLeft || 0; + const scrollTop = wt.pageYOffset || docEl.scrollTop || body.scrollTop; + const scrollLeft = wt.pageXOffset || docEl.scrollLeft || body.scrollLeft; + + const elComputedStyle = wt.getComputedStyle(domElement, null); + const elComputedDisplay = elComputedStyle.display || 'block'; + const mustDisplayElement = elComputedDisplay === 'none'; + + if (mustDisplayElement) { + domElement.style = domElement.style || {}; + const originalDisplay = domElement.style.display; + domElement.style.display = 'block'; + box = domElement.getBoundingClientRect(); + domElement.style.display = originalDisplay || null; + } + position.x = Math.round(box.left + scrollLeft - clientLeft); + position.y = Math.round(box.top + scrollTop - clientTop); + } catch (err) { + logError(LOG_PREFIX, err); + return ''; } - position.x = Math.round(box.left + scrollLeft - clientLeft); - position.y = Math.round(box.top + scrollTop - clientTop); } else { return ''; } @@ -909,6 +913,8 @@ export const spec = { const coppa = _getCoppa(); const schain = _getSchain(validBidRequests[0]); const eids = _getEids(validBidRequests[0]) || []; + const syncEnabled = deepAccess(config.getConfig('userSync'), 'syncEnabled') + const usIfr = syncEnabled && userSync.canBidderRegisterSync('iframe', 'adagio') const adUnits = _map(validBidRequests, (bidRequest) => { const globalFeatures = GlobalExchange.getOrSetGlobalFeatures(); @@ -918,6 +924,46 @@ export const spec = { adunit_position: getSlotPosition(bidRequest.params.adUnitElementId) // adUnitElementId à déplacer ??? }; + // Force the Split Keyword to be a String + if (bidRequest.params.splitKeyword) { + if (isStr(bidRequest.params.splitKeyword) || isNumber(bidRequest.params.splitKeyword)) { + bidRequest.params.splitKeyword = bidRequest.params.splitKeyword.toString(); + } else { + delete bidRequest.params.splitKeyword; + + logWarn(LOG_PREFIX, 'The splitKeyword param have been removed because the type is invalid, accepted type: number or string.'); + } + } + + // Force the Data Layer key and value to be a String + if (bidRequest.params.dataLayer) { + if (isStr(bidRequest.params.dataLayer) || isNumber(bidRequest.params.dataLayer) || isArray(bidRequest.params.dataLayer) || isFn(bidRequest.params.dataLayer)) { + logWarn(LOG_PREFIX, 'The dataLayer param is invalid, only object is accepted as a type.'); + delete bidRequest.params.dataLayer; + } else { + let invalidDlParam = false; + + bidRequest.params.dl = bidRequest.params.dataLayer + // Remove the dataLayer from the BidRequest to send the `dl` instead of the `dataLayer` + delete bidRequest.params.dataLayer + + Object.keys(bidRequest.params.dl).forEach((key) => { + if (bidRequest.params.dl[key]) { + if (isStr(bidRequest.params.dl[key]) || isNumber(bidRequest.params.dl[key])) { + bidRequest.params.dl[key] = bidRequest.params.dl[key].toString(); + } else { + invalidDlParam = true; + delete bidRequest.params.dl[key]; + } + } + }); + + if (invalidDlParam) { + logWarn(LOG_PREFIX, 'Some parameters of the dataLayer property have been removed because the type is invalid, accepted type: number or string.'); + } + } + } + Object.keys(features).forEach((prop) => { if (features[prop] === '') { delete features[prop]; @@ -1027,7 +1073,8 @@ export const spec = { eids: eids }, prebidVersion: '$prebid.version$', - featuresVersion: FEATURES_VERSION + featuresVersion: FEATURES_VERSION, + usIfr: usIfr }, options: { contentType: 'text/plain' @@ -1089,8 +1136,6 @@ export const spec = { bidObj.placement = bidReq.params.placement; bidObj.pagetype = bidReq.params.pagetype; bidObj.category = bidReq.params.category; - bidObj.subcategory = bidReq.params.subcategory; - bidObj.environment = bidReq.params.environment; } bidResponses.push(bidObj); }); diff --git a/modules/adagioBidAdapter.md b/modules/adagioBidAdapter.md index 9b48caa8233..45f39fc6f2d 100644 --- a/modules/adagioBidAdapter.md +++ b/modules/adagioBidAdapter.md @@ -10,30 +10,65 @@ Connects to Adagio demand source to fetch bids. ## Configuration +### User Sync + +Add the following code to enable user sync. Adagio strongly recommends enabling user syncing through iframes. This functionality improves DSP user match rates and increases the bid rate and bid price. Be sure to call `pbjs.setConfig()` only once. + +```javascript +pbjs.setConfig({ + userSync: { + iframeEnabled: true, + filterSettings: { + iframe: { + bidders: ['adagio'], + filter: 'include' + } + } + } +}); +``` + +### Bidder Settings + +The Adagio bid adapter uses browser local storage. Since Prebid.js 7.x, the access to it must be explicitly set. + +```js +// https://docs.prebid.org/dev-docs/publisher-api-reference/bidderSettings.html +pbjs.bidderSettings = { + adagio: { + storageAllowed: true + } +} +``` + +### Params configuration + Adagio require several params. These params must be set at Prebid.js BidderConfig config level or at adUnit level. Below, the list of Adagio params and where they can be set. -| Param name | Global config | AdUnit config | -| ---------- | ------------- | ------------- | -| siteId | x | -| organizationId (obsolete) | | x -| site (obsolete) | | x -| pagetype | x | x -| environment | x | x -| category | x | x -| subcategory | x | x -| useAdUnitCodeAsAdUnitElementId | x | x -| useAdUnitCodeAsPlacement | x | x -| placement | | x -| adUnitElementId | | x -| debug | | x -| video | | x -| native | | x - -### Global configuration - -The global configuration is used to store params once instead of duplicate them to each adUnit. The values will be used as "params" in the ad-request. +| Param name | Global config | AdUnit config | +| ------------------------------ | ------------- | ------------- | +| siteId | x | | +| organizationId * | | x | +| site * | | x | +| pagetype | x | x | +| category | x | x | +| useAdUnitCodeAsAdUnitElementId | x | x | +| useAdUnitCodeAsPlacement | x | x | +| placement | | x | +| adUnitElementId | | x | +| debug | | x | +| video | | x | +| native | | x | +| splitKeyword | | x | +| dataLayer | | x | + +_* These params are deprecated in favor the Global configuration setup, see below._ + +#### Global Adagio configuration + +The global Adagio configuration is used to store params once instead of duplicate them to each adUnit. The values will be used as "params" in the ad-request. Be sure to call `pbjs.setConfig()` only once. ```javascript pbjs.setConfig({ @@ -49,27 +84,14 @@ pbjs.setConfig({ // - underscores `_` // Also, each param can have at most 50 unique active values (case-insensitive). pagetype: 'article', // Highly recommended. The pagetype describes what kind of content will be present in the page. - environment: 'mobile', // Recommended. Environment where the page is displayed. category: 'sport', // Recommended. Category of the content displayed in the page. - subcategory: 'handball', // Optional. Subcategory of the content displayed in the page. useAdUnitCodeAsAdUnitElementId: false, // Optional. Use it by-pass adUnitElementId and use the adUnit code as value useAdUnitCodeAsPlacement: false, // Optional. Use it to by-pass placement and use the adUnit code as value - }, + } }); ``` -#### Note on FPD support - -Adagio will use FPD data as fallback for the params below: -- pagetype -- environment -- category -- subcategory - -If the FPD value is an array, the 1st value of this array will be used. - -### adUnit configuration - +#### Ad-unit configuration ```javascript var adUnits = [ { @@ -85,20 +107,34 @@ var adUnits = [ cpm: 3.00 // default to 1.00 }, video: { + api: [2, 7], // Required - Your video player must at least support the value 2 and/or 7. + playbackMethod: [6], // Highly recommended skip: 0 - // OpenRTB 2.5 video options defined here override ones defined in mediaTypes. + // OpenRTB video options defined here override ones defined in mediaTypes. }, native: { // Optional OpenRTB Native 1.2 request object. Only `context`, `plcmttype` fields are supported. context: 1, plcmttype: 2 }, + splitKeyword: 'splitrule-one', + dl: { + placement: '1234' + } } }] } ]; ``` +#### Note on FPD support + +Adagio will use FPD data as fallback for the params below: +- pagetype +- category + +If the FPD value is an array, the 1st value of this array will be used. + ## Test Parameters ```javascript @@ -107,11 +143,17 @@ var adUnits = [ debug: true, adagio: { pagetype: 'article', - environment: 'mobile', category: 'sport', - subcategory: 'handball', useAdUnitCodeAsAdUnitElementId: false, useAdUnitCodeAsPlacement: false, + }, + userSync: { + filterSettings: { + iframe: { + bidders: ['adagio'], + filter: 'include' + } + } } }); @@ -208,12 +250,6 @@ var adUnits = [ return bidResponse.site; } }, - { - key: "environment", - val: function (bidResponse) { - return bidResponse.environment; - } - }, { key: "placement", val: function (bidResponse) { @@ -231,12 +267,6 @@ var adUnits = [ val: function (bidResponse) { return bidResponse.category; } - }, - { - key: "subcategory", - val: function (bidResponse) { - return bidResponse.subcategory; - } } ] } diff --git a/modules/addefendBidAdapter.js b/modules/addefendBidAdapter.js index f0a6852b084..d73c25935ee 100644 --- a/modules/addefendBidAdapter.js +++ b/modules/addefendBidAdapter.js @@ -1,4 +1,5 @@ import {registerBidder} from '../src/adapters/bidderFactory.js'; +import {getGlobal} from '../src/prebidGlobal.js'; const BIDDER_CODE = 'addefend'; @@ -16,7 +17,7 @@ export const spec = { }, buildRequests: function(validBidRequests, bidderRequest) { let bid = { - v: $$PREBID_GLOBAL$$.version, + v: getGlobal().version, auctionId: false, pageId: false, gdpr_applies: bidderRequest.gdprConsent && bidderRequest.gdprConsent.gdprApplies ? bidderRequest.gdprConsent.gdprApplies : 'true', diff --git a/modules/adfBidAdapter.js b/modules/adfBidAdapter.js index d8e598deb8f..82bd7f03ff0 100644 --- a/modules/adfBidAdapter.js +++ b/modules/adfBidAdapter.js @@ -3,10 +3,9 @@ import {registerBidder} from '../src/adapters/bidderFactory.js'; import {BANNER, NATIVE, VIDEO} from '../src/mediaTypes.js'; -import {_map, deepAccess, deepSetValue, mergeDeep, parseSizesInput} from '../src/utils.js'; +import {deepAccess, deepSetValue, mergeDeep, parseSizesInput, deepClone} from '../src/utils.js'; import {config} from '../src/config.js'; import {Renderer} from '../src/Renderer.js'; -import { convertOrtbRequestToProprietaryNative } from '../src/native.js'; const { getConfig } = config; @@ -16,38 +15,7 @@ const BIDDER_ALIAS = [ { code: 'adformOpenRTB', gvlid: GVLID }, { code: 'adform', gvlid: GVLID } ]; -const NATIVE_ASSET_IDS = { 0: 'title', 2: 'icon', 3: 'image', 5: 'sponsoredBy', 4: 'body', 1: 'cta' }; -const NATIVE_PARAMS = { - title: { - id: 0, - name: 'title' - }, - icon: { - id: 2, - type: 1, - name: 'img' - }, - image: { - id: 3, - type: 3, - name: 'img' - }, - sponsoredBy: { - id: 5, - name: 'data', - type: 1 - }, - body: { - id: 4, - name: 'data', - type: 2 - }, - cta: { - id: 1, - type: 12, - name: 'data' - } -}; + const OUTSTREAM_RENDERER_URL = 'https://s2.adform.net/banners/scripts/video/outstream/render.js'; export const spec = { @@ -61,9 +29,6 @@ export const spec = { return !!(mid || (inv && mname)); }, buildRequests: (validBidRequests, bidderRequest) => { - // convert Native ORTB definition to old-style prebid native definition - validBidRequests = convertOrtbRequestToProprietaryNative(validBidRequests); - let app, site; const commonFpd = bidderRequest.ortb2 || {}; @@ -123,45 +88,28 @@ export const spec = { } }; - const assets = _map(bid.nativeParams, (bidParams, key) => { - const props = NATIVE_PARAMS[key]; - const asset = { - required: bidParams.required & 1, - }; - if (props) { - asset.id = props.id; - let wmin, hmin, w, h; - let aRatios = bidParams.aspect_ratios; - - if (aRatios && aRatios[0]) { - aRatios = aRatios[0]; - wmin = aRatios.min_width || 0; - hmin = aRatios.ratio_height * wmin / aRatios.ratio_width | 0; + if (bid.nativeOrtbRequest && bid.nativeOrtbRequest.assets) { + let assets = bid.nativeOrtbRequest.assets; + let requestAssets = []; + for (let i = 0; i < assets.length; i++) { + let asset = deepClone(assets[i]); + let img = asset.img; + if (img) { + let aspectratios = img.ext && img.ext.aspectratios; + + if (aspectratios) { + let ratioWidth = parseInt(aspectratios[0].split(':')[0], 10); + let ratioHeight = parseInt(aspectratios[0].split(':')[1], 10); + img.wmin = img.wmin || 0; + img.hmin = ratioHeight * img.wmin / ratioWidth | 0; + } } - - if (bidParams.sizes) { - const sizes = flatten(bidParams.sizes); - w = sizes[0]; - h = sizes[1]; - } - - asset[props.name] = { - len: bidParams.len, - type: props.type, - wmin, - hmin, - w, - h - }; - - return asset; + requestAssets.push(asset); } - }).filter(Boolean); - if (assets.length) { imp.native = { request: { - assets + assets: requestAssets } }; } @@ -268,7 +216,9 @@ export const spec = { }; if (bidResponse.native) { - result.native = parseNative(bidResponse); + result.native = { + ortb: bidResponse.native + }; } else { result[ mediaType === VIDEO ? 'vastXml' : 'ad' ] = bidResponse.adm; } @@ -286,25 +236,6 @@ export const spec = { registerBidder(spec); -function parseNative(bid) { - const { assets, link, imptrackers, jstracker } = bid.native; - const result = { - clickUrl: link.url, - clickTrackers: link.clicktrackers || undefined, - impressionTrackers: imptrackers || undefined, - javascriptTrackers: jstracker ? [ jstracker ] : undefined - }; - assets.forEach(asset => { - const kind = NATIVE_ASSET_IDS[asset.id]; - const content = kind && asset[NATIVE_PARAMS[kind].name]; - if (content) { - result[kind] = content.text || content.value || { url: content.url, width: content.w, height: content.h }; - } - }); - - return result; -} - function setOnAny(collection, key) { for (let i = 0, result; i < collection.length; i++) { result = deepAccess(collection[i], key); diff --git a/modules/adkernelAdnAnalyticsAdapter.js b/modules/adkernelAdnAnalyticsAdapter.js index 901c0d2fd98..48897f8516b 100644 --- a/modules/adkernelAdnAnalyticsAdapter.js +++ b/modules/adkernelAdnAnalyticsAdapter.js @@ -5,12 +5,14 @@ import { logError, parseUrl, _each } from '../src/utils.js'; import {ajax} from '../src/ajax.js'; import {getStorageManager} from '../src/storageManager.js'; import {config} from '../src/config.js'; +import {MODULE_TYPE_ANALYTICS} from '../src/activities/modules.js'; +const MODULE_CODE = 'adkernelAdn'; const GVLID = 14; const ANALYTICS_VERSION = '1.0.2'; const DEFAULT_QUEUE_TIMEOUT = 4000; const DEFAULT_HOST = 'tag.adkernel.com'; -const storageObj = getStorageManager({gvlid: GVLID}); +const storageObj = getStorageManager({moduleType: MODULE_TYPE_ANALYTICS, moduleName: MODULE_CODE}); const ADK_HB_EVENTS = { AUCTION_INIT: 'auctionInit', @@ -104,7 +106,7 @@ analyticsAdapter.enableAnalytics = (config) => { adapterManager.registerAnalyticsAdapter({ adapter: analyticsAdapter, - code: 'adkernelAdn', + code: MODULE_CODE, gvlid: GVLID }); diff --git a/modules/adkernelBidAdapter.js b/modules/adkernelBidAdapter.js index b0449418a87..d5639f57c10 100644 --- a/modules/adkernelBidAdapter.js +++ b/modules/adkernelBidAdapter.js @@ -97,7 +97,9 @@ export const spec = { {code: 'motionspots'}, {code: 'sonic_twist'}, {code: 'displayioads'}, - {code: 'rtbdemand_com'} + {code: 'rtbdemand_com'}, + {code: 'bidbuddy'}, + {code: 'adliveconnect'} ], supportedMediaTypes: [BANNER, VIDEO, NATIVE], diff --git a/modules/adlooxAnalyticsAdapter.md b/modules/adlooxAnalyticsAdapter.md index 203b118652e..d77ee25ab5f 100644 --- a/modules/adlooxAnalyticsAdapter.md +++ b/modules/adlooxAnalyticsAdapter.md @@ -34,19 +34,21 @@ When tracking video you have two options: To view an [example of an Adloox integration](../integrationExamples/gpt/adloox.html): - gulp serve --nolint --notest --modules=gptPreAuction,categoryTranslation,dfpAdServerVideo,rtdModule,instreamTracking,rubiconBidAdapter,spotxBidAdapter,adlooxAnalyticsAdapter,adlooxAdServerVideo,adlooxRtdProvider + gulp serve --nolint --notest --modules=gptPreAuction,categoryTranslation,dfpAdServerVideo,intersectionRtdProvider,rtdModule,instreamTracking,rubiconBidAdapter,spotxBidAdapter,adlooxAnalyticsAdapter,adlooxAdServerVideo,adlooxRtdProvider **N.B.** `categoryTranslation` is required by `dfpAdServerVideo` that otherwise causes a JavaScript console warning +**N.B.** `intersectionRtdProvider` is used by `adlooxRtdProvider` to provide (above-the-fold) ATF measurement, if not enabled the `atf` segment will not be available + Now point your browser at: http://localhost:9999/integrationExamples/gpt/adloox.html?pbjs_debug=true ### Public Example -The example is published publically at: https://storage.googleapis.com/adloox-ads-js-test/prebid.html?pbjs_debug=true +The example is published publicly at: https://storage.googleapis.com/adloox-ads-js-test/prebid.html?pbjs_debug=true **N.B.** this will show a [CORS error](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS/Errors) for the request `https://p.adlooxtracking.com/q?...` that is safe to ignore on the public example page; it is related to the [RTD integration](./adlooxRtdProvider.md) which requires pre-registration of your sites -It is recommended you use [Google Chrome's 'Local Overrides' located in the Developer Tools panel](https://www.trysmudford.com/blog/chrome-local-overrides/) to explore the example without the inconvience of having to run your own web server. +It is recommended you use [Google Chrome's 'Local Overrides' located in the Developer Tools panel](https://www.trysmudford.com/blog/chrome-local-overrides/) to explore the example without the inconvenience of having to run your own web server. #### Pre-built `prebid.js` diff --git a/modules/adlooxRtdProvider.js b/modules/adlooxRtdProvider.js index 8862ac8ac47..c2037429185 100644 --- a/modules/adlooxRtdProvider.js +++ b/modules/adlooxRtdProvider.js @@ -6,11 +6,13 @@ * @module modules/adlooxRtdProvider * @requires module:modules/realTimeData * @requires module:modules/adlooxAnalyticsAdapter + * @optional module:modules/intersectionRtdProvider */ /* eslint standard/no-callback-literal: "off" */ /* eslint prebid/validate-imports: "off" */ +import {auctionManager} from '../src/auctionManager.js'; import {command as analyticsCommand, COMMAND} from './adlooxAnalyticsAdapter.js'; import {submodule} from '../src/hook.js'; import {ajax} from '../src/ajax.js'; @@ -18,142 +20,31 @@ import {getGlobal} from '../src/prebidGlobal.js'; import {getRefererInfo} from '../src/refererDetection.js'; import { _each, + _map, + buildUrl, deepAccess, + deepClone, deepSetValue, - getAdUnitSizes, getGptSlotInfoForAdUnitCode, isArray, isBoolean, isInteger, isPlainObject, - isStr, logError, logInfo, logWarn, - mergeDeep + mergeDeep, + parseUrl, + safeJSONParse } from '../src/utils.js'; -import {includes} from '../src/polyfill.js'; const MODULE_NAME = 'adloox'; const MODULE = `${MODULE_NAME}RtdProvider`; const API_ORIGIN = 'https://p.adlooxtracking.com'; const SEGMENT_HISTORIC = { 'a': 'aud', 'd': 'dis', 'v': 'vid' }; -const SEGMENT_HISTORIC_VALUES = Object.keys(SEGMENT_HISTORIC).map(k => SEGMENT_HISTORIC[k]); -const ADSERVER_TARGETING_PREFIX = 'adl_'; - -const CREATIVE_WIDTH_MIN = 20; -const CREATIVE_HEIGHT_MIN = 20; -const CREATIVE_AREA_MIN = CREATIVE_WIDTH_MIN * CREATIVE_HEIGHT_MIN; -// try to avoid using IntersectionObserver as it has unbounded (observed multi-second) latency -let intersectionObserver = window == top ? false : undefined; -const intersectionObserverElements = []; -// .map/.findIndex are safe here as they are only used for intersectionObserver -function atf(adUnit, cb) { - analyticsCommand(COMMAND.TOSELECTOR, { bid: { adUnitCode: adUnit.code } }, function(selector) { - const element = document.querySelector(selector); - if (!element) return cb(null); - - if (window.getComputedStyle(element)['display'] == 'none') return cb(NaN); - - try { - // short circuit for cross-origin - if (intersectionObserver) throw false; - - // Google's advice is to collapse slots on no fill but - // we have to cater to clients that grow slots on fill - const rect = (function(rect) { - const sizes = getAdUnitSizes(adUnit); - - if (sizes.length == 0) return false; - // interstitial (0x0, 1x1) - if (sizes.length == 1 && (sizes[0][0] * sizes[0][1]) <= 1) return true; - // try to catch premium slots (coord=0,0) as they will likely bounce into view - if (rect.top <= -window.pageYOffset && rect.left <= -window.pageXOffset && rect.top == rect.bottom) return true; - - // pick the smallest creative size as many publishers will just leave the element unbounded in the vertical - let width = Infinity; - let height = Infinity; - for (let i = 0; i < sizes.length; i++) { - const area = sizes[i][0] * sizes[i][1]; - if (area < CREATIVE_AREA_MIN) continue; - if (area < (width * height)) { - width = sizes[i][0]; - height = sizes[i][1]; - } - } - // we also scale the smallest size to the size of the slot as publishers resize units depending on viewport - const scale = Math.min(1, (rect.right - rect.left) / width); - - return { - left: rect.left, - right: rect.left + Math.max(CREATIVE_WIDTH_MIN, scale * width), - top: rect.top, - bottom: rect.top + Math.max(CREATIVE_HEIGHT_MIN, scale * height) - }; - })(element.getBoundingClientRect()); - - if (rect === false) return cb(NaN); - if (rect === true) return cb(1); - - const W = rect.right - rect.left; - const H = rect.bottom - rect.top; - - if (W * H < CREATIVE_AREA_MIN) return cb(NaN); - - let el; - let win = window; - while (1) { - // https://stackoverflow.com/a/8876069 - const vw = Math.max(win.document.documentElement.clientWidth || 0, win.innerWidth || 0); - const vh = Math.max(win.document.documentElement.clientHeight || 0, win.innerHeight || 0); - - // cut to viewport - rect.left = Math.min(Math.max(rect.left, 0), vw); - rect.right = Math.min(Math.max(rect.right, 0), vw); - rect.top = Math.min(Math.max(rect.top, 0), vh); - rect.bottom = Math.min(Math.max(rect.bottom, 0), vh); - - if (win == top) return cb(((rect.right - rect.left) * (rect.bottom - rect.top)) / (W * H)); - el = win.frameElement; - if (!el) throw false; // cross-origin - win = win.parent; - - // transpose to frame element - const frameElementRect = el.getBoundingClientRect(); - rect.left += frameElementRect.left; - rect.right = Math.min(rect.right + frameElementRect.left, frameElementRect.right); - rect.top += frameElementRect.top; - rect.bottom = Math.min(rect.bottom + frameElementRect.top, frameElementRect.bottom); - } - } catch (_) { - if (intersectionObserver === undefined) { - try { - intersectionObserver = new IntersectionObserver(function(entries) { - entries.forEach(entry => { - const ratio = entry.intersectionRect.width * entry.intersectionRect.height < CREATIVE_AREA_MIN ? NaN : entry.intersectionRatio; - const idx = intersectionObserverElements.findIndex(x => x.element == entry.target); - intersectionObserverElements[idx].cb.forEach(cb => cb(ratio)); - intersectionObserverElements.splice(idx, 1); - intersectionObserver.unobserve(entry.target); - }); - }); - } catch (_) { - intersectionObserver = false; - } - } - if (!intersectionObserver) return cb(null); - const idx = intersectionObserverElements.findIndex(x => x.element == element); - if (idx == -1) { - intersectionObserverElements.push({ element, cb: [ cb ] }); - intersectionObserver.observe(element); - } else { - intersectionObserverElements[idx].cb.push(cb); - } - } - }); -} +const ADSERVER_TARGETING_PREFIX = 'adl'; function init(config, userConsent) { logInfo(MODULE, 'init', config, userConsent); @@ -167,10 +58,6 @@ function init(config, userConsent) { logError(MODULE, 'invalid params'); return false; } - if (!(config.params.api_origin === undefined || isStr(config.params.api_origin))) { - logError(MODULE, 'invalid api_origin params value'); - return false; - } if (!(config.params.imps === undefined || (isInteger(config.params.imps) && config.params.imps > 0))) { logError(MODULE, 'invalid imps params value'); return false; @@ -213,193 +100,110 @@ function init(config, userConsent) { } function getBidRequestData(reqBidsConfigObj, callback, config, userConsent) { - // gptPreAuction runs *after* RTD so pbadslot may not be populated... (╯°□°)╯ ┻━┻ - const adUnits = (reqBidsConfigObj.adUnits || getGlobal().adUnits).map(adUnit => { - return { - gpid: deepAccess(adUnit, 'ortb2Imp.ext.gpid') || deepAccess(adUnit, 'ortb2Imp.ext.data.pbadslot') || getGptSlotInfoForAdUnitCode(adUnit.code).gptSlot || adUnit.code, - unit: adUnit - }; - }).filter(adUnit => !!adUnit.gpid); - - let response = {}; - function setSegments() { - function val(v, k) { - if (!((SEGMENT_HISTORIC[k] || k == 'atf') && v >= 0)) return v; - return config.params.thresholds.filter(t => t <= v); - } - - const ortb2 = reqBidsConfigObj.ortb2Fragments?.global || {}; - const dataSite = deepAccess(ortb2, 'site.ext.data') || {}; - const dataUser = deepAccess(ortb2, 'user.ext.data') || {}; - - _each(response, (v0, k0) => { - if (k0 == '_') return; - const k = SEGMENT_HISTORIC[k0] || k0; - const v = val(v0, k0); - deepSetValue(k == k0 ? dataUser : dataSite, `${MODULE_NAME}_rtd.${k}`, v); - }); - deepSetValue(dataSite, `${MODULE_NAME}_rtd.ok`, true); + const adUnits0 = reqBidsConfigObj.adUnits || getGlobal().adUnits; + // adUnits must be ordered according to adUnitCodes for stable 's' param usage and handling the response below + const adUnits = reqBidsConfigObj.adUnitCodes.map(code => adUnits0.find(unit => unit.code == code)); + + // buildUrl creates PHP style multi-parameters and includes undefined... (╯°□°)╯ ┻━┻ + const url = buildUrl(mergeDeep(parseUrl(`${API_ORIGIN}/q`), { search: { + 'v': `pbjs-${getGlobal().version}`, + 'c': config.params.clientid, + 'p': config.params.platformid, + 't': config.params.tagid, + 'imp': config.params.imps, + 'fc_ip': config.params.freqcap_ip, + 'fc_ipua': config.params.freqcap_ipua, + 'pn': (getRefererInfo().page || '').substr(0, 300).split(/[?#]/)[0], + 's': _map(adUnits, function(unit) { + // gptPreAuction runs *after* RTD so pbadslot may not be populated... (╯°□°)╯ ┻━┻ + const gpid = deepAccess(unit, 'ortb2Imp.ext.gpid') || + deepAccess(unit, 'ortb2Imp.ext.data.pbadslot') || + getGptSlotInfoForAdUnitCode(unit.code).gptSlot || + unit.code; + const ref = [ gpid ]; + if (!config.params.slotinpath) ref.push(unit.code); + return ref.join('\t'); + }) + } })).replace(/\[\]|[^?&]+=undefined/g, '').replace(/([?&])&+/g, '$1'); + + ajax(url, + function(responseText, q) { + function val(v, k) { + if (!(SEGMENT_HISTORIC[k] && v >= 0)) return v; + return config.params.thresholds.filter(t => t <= v); + } - deepSetValue(ortb2, 'site.ext.data', dataSite); - deepSetValue(ortb2, 'user.ext.data', dataUser); - deepSetValue(reqBidsConfigObj, 'ortb2Fragments.global', ortb2); + const response = safeJSONParse(responseText); + if (!response) { + logError(MODULE, 'unexpected response'); + return callback(); + } - adUnits.forEach((adUnit, i) => { - _each(response['_'][i], (v0, k0) => { + const { site: ortb2site, user: ortb2user } = reqBidsConfigObj.ortb2Fragments.global; + _each(response, function(v0, k0) { + if (k0 == '_') return; const k = SEGMENT_HISTORIC[k0] || k0; const v = val(v0, k0); - deepSetValue(adUnit.unit, `ortb2Imp.ext.data.${MODULE_NAME}_rtd.${k}`, v); + deepSetValue(k == k0 ? ortb2user : ortb2site, `ext.data.${MODULE_NAME}_rtd.${k}`, v); }); - }); - }; - - // mergeDeep does not handle merging deep arrays... (╯°□°)╯ ┻━┻ - function mergeDeep(target, ...sources) { - function emptyValue(v) { - if (isPlainObject(v)) { - return {}; - } else if (isArray(v)) { - return []; - } else { - return undefined; - } - } - if (!sources.length) return target; - const source = sources.shift(); - - if (isPlainObject(target) && isPlainObject(source)) { - Object.keys(source).forEach(key => { - if (!(key in target)) target[key] = emptyValue(source[key]); - target[key] = target[key] !== undefined ? mergeDeep(target[key], source[key]) : source[key]; - }); - } else if (isArray(target) && isArray(source)) { - source.forEach((v, i) => { - if (!(i in target)) target[i] = emptyValue(v); - target[i] = target[i] !== undefined ? mergeDeep(target[i], v) : v; + _each(response._, function(segments, i) { + _each(segments, function(v0, k0) { + const k = SEGMENT_HISTORIC[k0] || k0; + const v = val(v0, k0); + deepSetValue(adUnits[i], `ortb2Imp.ext.data.${MODULE_NAME}_rtd.${k}`, v); + }); }); - } else { - target = source; - } - return mergeDeep(target, ...sources); - } + deepSetValue(ortb2site, `ext.data.${MODULE_NAME}_rtd.ok`, true); - let semaphore = 1; - function semaphoreInc(inc) { - if (semaphore == 0) return; - semaphore += inc; - if (semaphore == 0) { - setSegments() callback(); } - } - - const refererInfo = getRefererInfo(); - const args = [ - [ 'v', `pbjs-${getGlobal().version}` ], - [ 'c', config.params.clientid ], - [ 'p', config.params.platformid ], - [ 't', config.params.tagid ], - [ 'imp', config.params.imps ], - [ 'fc_ip', config.params.freqcap_ip ], - [ 'fc_ipua', config.params.freqcap_ipua ], - [ 'pn', (refererInfo.page || '').substr(0, 300).split(/[?#]/)[0] ] - ]; - - if (!adUnits.length) { - logWarn(MODULE, 'no suitable adUnits (missing pbadslot?)'); - } - const atfQueue = []; - adUnits.map((adUnit, i) => { - const ref = [ adUnit.gpid ]; - if (!config.params.slotinpath) ref.push(adUnit.unit.code); - args.push(['s', ref.join('\t')]); - - semaphoreInc(1); - atfQueue.push(function() { - atf(adUnit.unit, function(x) { - let viewable = document.visibilityState === undefined || document.visibilityState == 'visible'; - try { viewable = viewable && top.document.hasFocus() } catch (_) {} - logInfo(MODULE, `atf code=${adUnit.unit.code} has area=${x}, viewable=${viewable}`); - const atfList = []; atfList[i] = { atf: parseInt(x * 100) }; - response = mergeDeep(response, { _: atfList }); - semaphoreInc(-1); - }); - }); - }); - function atfCb() { - atfQueue.forEach(x => x()); - } - if (document.readyState == 'loading') { - document.addEventListener('DOMContentLoaded', atfCb, false); - } else { - atfCb(); - } - - analyticsCommand(COMMAND.URL, { - url: (config.params.api_origin || API_ORIGIN) + '/q?', - args: args - }, function(url) { - ajax(url, { - success: function(responseText, q) { - try { - if (q.getResponseHeader('content-type') == 'application/json') { - response = mergeDeep(response, JSON.parse(responseText)); - } else { - throw false; - } - } catch (_) { - logError(MODULE, 'unexpected response'); - } - semaphoreInc(-1); - }, - error: function(statusText, q) { - logError(MODULE, 'request failed'); - semaphoreInc(-1); - } - }); - }); + ); } function getTargetingData(adUnitArray, config, userConsent, auction) { - function targetingNormalise(v) { + function val(v) { if (isArray(v) && v.length == 0) return undefined; if (isBoolean(v)) v = ~~v; if (!v) return undefined; // empty string and zero return v; } - const ortb2 = auction.getFPD().global || {}; - const dataSite = deepAccess(ortb2, `site.ext.data.${MODULE_NAME}_rtd`) || {}; - if (!dataSite.ok) return {}; + const { site: ortb2site, user: ortb2user } = auctionManager.index.getAuction(auction).getFPD().global; - const dataUser = deepAccess(ortb2, `user.ext.data.${MODULE_NAME}_rtd`) || {}; - return getGlobal().adUnits.filter(adUnit => includes(adUnitArray, adUnit.code)).reduce((a, adUnit) => { - a[adUnit.code] = {}; + const ortb2base = {}; + _each(deepAccess(mergeDeep(ortb2site, ortb2user), `ext.data.${MODULE_NAME}_rtd`), function(v0, k) { + const v = val(v0); + if (v) ortb2base[`${ADSERVER_TARGETING_PREFIX}_${k}`] = v; + }); - _each(dataSite, (v0, k) => { - if (includes(SEGMENT_HISTORIC_VALUES, k)) return; // ignore site average viewability - const v = targetingNormalise(v0); - if (v) a[adUnit.code][ADSERVER_TARGETING_PREFIX + k] = v; - }); + const targeting = {}; + _each(auction.adUnits.filter(unit => adUnitArray.includes(unit.code)), function(unit) { + targeting[unit.code] = deepClone(ortb2base); - const adUnitSegments = deepAccess(adUnit, `ortb2Imp.ext.data.${MODULE_NAME}_rtd`, {}); - _each(Object.assign({}, dataUser, adUnitSegments), (v0, k) => { - const v = targetingNormalise(v0); - if (v) a[adUnit.code][ADSERVER_TARGETING_PREFIX + k] = v; + const ortb2imp = deepAccess(unit, `ortb2Imp.ext.data.${MODULE_NAME}_rtd`); + _each(ortb2imp, function(v0, k) { + const v = val(v0); + if (v) targeting[unit.code][`${ADSERVER_TARGETING_PREFIX}_${k}`] = v; }); - return a; - }, {}); + // ATF results shamelessly exfiltrated from intersectionRtdProvider + const bid = unit.bids.find(bid => !!bid.intersection); + if (bid) { + const v = val(config.params.thresholds.filter(t => t <= (bid.intersection.intersectionRatio * 100))); + if (v) targeting[unit.code][`${ADSERVER_TARGETING_PREFIX}_atf`] = v; + } + }); + + return targeting; } export const subModuleObj = { name: MODULE_NAME, init, getBidRequestData, - getTargetingData, - atf // used by adlooxRtdProvider_spec.js + getTargetingData }; submodule('realTimeData', subModuleObj); diff --git a/modules/adlooxRtdProvider.md b/modules/adlooxRtdProvider.md index 466f8ed1ba2..9d6a20a01a7 100644 --- a/modules/adlooxRtdProvider.md +++ b/modules/adlooxRtdProvider.md @@ -19,12 +19,7 @@ This provider fetches segments and populates the [First Party Data](https://docs * AdUnit segments are placed into `AdUnit.ortb2Imp.ext.data.adloox_rtd`: * **`{dis,vid,aud}`:** an list of integers describing the likelihood the AdUnit will be visible * **`atf`:** an list of integers describing the percentage of pixels visible at auction - * measured only once at pre-auction - * usable when the publisher uses the strategy of collapsing ad slots on no-fill - * using the reverse strategy, growing ad slots on fill, invalidates the measurement the position of all content (including the slots) changes post-auction - * works best when your page loads your ad slots have their actual size rendered (ie. not zero height) - * uses the smallest ad unit (above a threshold area of 20x20) supplied by the [publisher to Prebid.js](https://docs.prebid.org/dev-docs/examples/basic-example.html) and measures viewability as if that size to be used - * when used in cross-origin (unfriendly) IFRAME environments the ad slot is directly measured as is (ignoring publisher provided sizes) due to limitations in using [IntersectionObserver](https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserver) + * measured at pre-auction time using the [Intersection Module](https://docs.prebid.org/dev-docs/modules/intersectionRtdProvider.html); if not enabled then this measurement is not available **N.B.** this provider does not offer or utilise any user orientated data @@ -57,8 +52,13 @@ To use this, you *must* also integrate the [Adloox Analytics Adapter](./adlooxAn realTimeData: { auctionDelay: 100, // see below for guidance dataProviders: [ + { + name: 'intersection', + waitForIt: true + }, { name: 'adloox', + waitForIt: true, params: { // optional, defaults shown thresholds: [ 50, 60, 70, 80, 90 ], slotinpath: false diff --git a/modules/admaruBidAdapter.js b/modules/admaruBidAdapter.js index 65f62c77e26..f681a9a4191 100644 --- a/modules/admaruBidAdapter.js +++ b/modules/admaruBidAdapter.js @@ -5,6 +5,7 @@ const ADMARU_ENDPOINT = 'https://p1.admaru.net/AdCall'; const BIDDER_CODE = 'admaru'; const DEFAULT_BID_TTL = 360; +const SYNC_URL = 'https://p2.admaru.net/UserSync/sync' function parseBid(rawBid, currency) { const bid = {}; @@ -75,7 +76,24 @@ export const spec = { } return bidResponses; - } + }, + + getUserSyncs: function (syncOptions, responses) { + if (syncOptions.iframeEnabled) { + return [{ + type: 'iframe', + url: SYNC_URL + }]; + } + if (syncOptions.pixelEnabled) { + return [{ + type: 'image', + url: SYNC_URL + }]; + } + + return []; + }, } registerBidder(spec); diff --git a/modules/admaticBidAdapter.js b/modules/admaticBidAdapter.js index 148424c0a98..808c788fcb9 100644 --- a/modules/admaticBidAdapter.js +++ b/modules/admaticBidAdapter.js @@ -1,5 +1,6 @@ import { getValue, logError, deepAccess, getBidIdParameter, isArray } from '../src/utils.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { config } from '../src/config.js'; import { BANNER, VIDEO } from '../src/mediaTypes.js'; let SYNC_URL = ''; const BIDDER_CODE = 'admatic'; @@ -35,7 +36,7 @@ export const spec = { const bids = validBidRequests.map(buildRequestObject); const networkId = getValue(validBidRequests[0].params, 'networkId'); const host = getValue(validBidRequests[0].params, 'host'); - const currency = getValue(validBidRequests[0].params, 'currency') || 'TRY'; + const currency = config.getConfig('currency.adServerCurrency') || 'TRY'; const bidderName = validBidRequests[0].bidder; const payload = { diff --git a/modules/admediaBidAdapter.js b/modules/admediaBidAdapter.js new file mode 100644 index 00000000000..42593a36159 --- /dev/null +++ b/modules/admediaBidAdapter.js @@ -0,0 +1,98 @@ +import {registerBidder} from '../src/adapters/bidderFactory.js'; +import {BANNER} from '../src/mediaTypes.js'; + +const BIDDER_CODE = 'admedia'; +const ENDPOINT_URL = 'https://prebid.admedia.com/bidder/'; + +export const spec = { + code: BIDDER_CODE, + supportedMediaTypes: [BANNER], + + /** + * Determines whether or not the given bid request is valid. + * + * @param {BidRequest} bid The bid params to validate. + * @return boolean True if this is a valid bid, and false otherwise. + */ + isBidRequestValid: function (bid) { + return !!(bid.params.placementId); + }, + + /** + * Make a server request from the list of BidRequests. + * + * @return Array Info describing the request to the server. + * @param validBidRequests + * @param bidderRequest + */ + buildRequests: function (validBidRequests, bidderRequest) { + if (validBidRequests.length === 0) { + return []; + } + return validBidRequests.map(bidRequest => { + let sizes = [] + if (bidRequest.mediaTypes && bidRequest.mediaTypes[BANNER] && bidRequest.mediaTypes[BANNER].sizes) { + sizes = bidRequest.mediaTypes[BANNER].sizes; + } + + var tagData = []; + for (var i = 0, j = sizes.length; i < j; i++) { + let tag = {}; + tag.sizes = []; + tag.id = bidRequest.params.placementId; + tag.aid = bidRequest.params.aid; + tag.sizes.push(sizes[i].toString().replace(',', 'x')); + tagData.push(tag); + } + + const payload = { + id: bidRequest.params.placementId, + aid: bidRequest.params.aid, + tags: tagData, + bidId: bidRequest.bidId, + referer: encodeURIComponent(bidderRequest.refererInfo.page) + }; + + return { + method: 'POST', + url: ENDPOINT_URL, + data: payload + }; + }); + }, + + /** + * Unpack the response from the server into a list of bids. + * + * @param {ServerResponse} serverResponse A successful response from the server. + * @param bidRequest + * @return {Bid[]} An array of bids which were nested inside the server. + */ + interpretResponse: function (serverResponse, bidRequest) { + const requiredKeys = ['requestId', 'cpm', 'width', 'height', 'ad', 'ttl', 'creativeId', 'netRevenue', 'currency', 'meta']; + const validBidResponses = []; + serverResponse = serverResponse.body.tags; + + if (serverResponse && (serverResponse.length > 0)) { + serverResponse.forEach((bid) => { + const bidResponse = {}; + for (const requiredKey of requiredKeys) { + if (!bid.hasOwnProperty(requiredKey)) { + return []; + } + bidResponse[requiredKey] = bid[requiredKey]; + } + if (!(typeof bid.meta.advertiserDomains !== 'undefined' && bid.meta.advertiserDomains.length > 0)) { + return []; + } + validBidResponses.push(bidResponse); + }); + } + return validBidResponses; + }, + getUserSyncs: function(syncOptions, serverResponses, gdprConsent, uspConsent) {}, + onTimeout: function(timeoutData) {}, + onBidWon: function (bid) {} +}; + +registerBidder(spec); diff --git a/modules/admediaBidAdapter.md b/modules/admediaBidAdapter.md new file mode 100644 index 00000000000..fe4378e9f61 --- /dev/null +++ b/modules/admediaBidAdapter.md @@ -0,0 +1,35 @@ +# Overview + +**Module Name**: Admedia Bidder Adapter +**Module Type**: Bidder Adapter +**Maintainer**: developers@admedia.com + +# Description + +Module that connects to Admedia's bidder for bids. + +# Test Parameters +``` +var adUnits = [ + { + code: 'ad-div', + mediaTypes: { + banner: { + sizes: [[300, 250]], + } + }, + bids: [{ + + bidder: 'admedia', + params: { + placementId: '782332', + aid: '86858', + }, + refererInfo: { + page: "http://addreferrerhere.com" + } + + }] + } +]; +``` \ No newline at end of file diff --git a/modules/admixerBidAdapter.js b/modules/admixerBidAdapter.js index 90031ed7f5d..8dbcfdafd32 100644 --- a/modules/admixerBidAdapter.js +++ b/modules/admixerBidAdapter.js @@ -1,12 +1,14 @@ -import { logError } from '../src/utils.js'; +import {logError} from '../src/utils.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import {config} from '../src/config.js'; import {BANNER, VIDEO, NATIVE} from '../src/mediaTypes.js'; -import { convertOrtbRequestToProprietaryNative } from '../src/native.js'; +import {convertOrtbRequestToProprietaryNative} from '../src/native.js'; const BIDDER_CODE = 'admixer'; -const ALIASES = ['go2net', 'adblender', 'adsyield', 'futureads', 'smn']; +const BIDDER_CODE_ADX = 'admixeradx'; +const ALIASES = ['go2net', 'adblender', 'adsyield', 'futureads', 'admixeradx']; const ENDPOINT_URL = 'https://inv-nets.admixer.net/prebid.1.2.aspx'; +const ADX_ENDPOINT_URL = 'https://inv-nets.admixer.net/adxprebid.1.2.aspx'; export const spec = { code: BIDDER_CODE, aliases: ALIASES, @@ -57,6 +59,10 @@ export const spec = { if (bidderRequest.uspConsent) { payload.uspConsent = bidderRequest.uspConsent; } + let bidFloor = getBidFloor(bidderRequest); + if (bidFloor) { + payload.bidFloor = bidFloor; + } } validRequest.forEach((bid) => { let imp = {}; @@ -65,7 +71,11 @@ export const spec = { }); return { method: 'POST', - url: endpointUrl || ENDPOINT_URL, + url: + endpointUrl || + (bidderRequest.bidderCode === BIDDER_CODE_ADX + ? ADX_ENDPOINT_URL + : ENDPOINT_URL), data: payload, }; }, @@ -96,4 +106,16 @@ export const spec = { return pixels; } }; +function getBidFloor(bid) { + try { + const bidFloor = bid.getFloor({ + currency: 'USD', + mediaType: '*', + size: '*', + }); + return bidFloor.floor; + } catch (_) { + return 0; + } +} registerBidder(spec); diff --git a/modules/admixerIdSystem.js b/modules/admixerIdSystem.js index 49ffe4f4680..7fbebecfc12 100644 --- a/modules/admixerIdSystem.js +++ b/modules/admixerIdSystem.js @@ -9,8 +9,10 @@ import { logError, logInfo } from '../src/utils.js' import { ajax } from '../src/ajax.js'; import { submodule } from '../src/hook.js'; import {getStorageManager} from '../src/storageManager.js'; +import {MODULE_TYPE_UID} from '../src/activities/modules.js'; -export const storage = getStorageManager(); +const NAME = 'admixerId'; +export const storage = getStorageManager({moduleType: MODULE_TYPE_UID, moduleName: NAME}); /** @type {Submodule} */ export const admixerIdSubmodule = { @@ -18,7 +20,7 @@ export const admixerIdSubmodule = { * used to link submodule with config * @type {string} */ - name: 'admixerId', + name: NAME, /** * used to specify vendor id * @type {number} diff --git a/modules/adnuntiusBidAdapter.js b/modules/adnuntiusBidAdapter.js index 0ae8411c073..c3cd1963248 100644 --- a/modules/adnuntiusBidAdapter.js +++ b/modules/adnuntiusBidAdapter.js @@ -1,6 +1,6 @@ import { registerBidder } from '../src/adapters/bidderFactory.js'; import { BANNER, VIDEO } from '../src/mediaTypes.js'; -import { isStr, deepAccess, logInfo } from '../src/utils.js'; +import { isStr, deepAccess } from '../src/utils.js'; import { config } from '../src/config.js'; import { getStorageManager } from '../src/storageManager.js'; @@ -8,6 +8,7 @@ const BIDDER_CODE = 'adnuntius'; const ENDPOINT_URL = 'https://ads.adnuntius.delivery/i'; const GVLID = 855; const DEFAULT_VAST_VERSION = 'vast4' +// const DEFAULT_NATIVE = 'native' const checkSegment = function (segment) { if (isStr(segment)) return segment; @@ -27,8 +28,34 @@ const getSegmentsFromOrtb = function (ortb2) { return segments } +// function createNative(ad) { +// const native = {}; +// const assets = ad.assets +// native.title = ad.text.title.content; +// native.image = { +// url: assets.image.cdnId, +// height: assets.image.height, +// width: assets.image.width, +// }; +// if (assets.icon) { +// native.icon = { +// url: assets.icon.cdnId, +// height: assets.icon.height, +// width: assets.icon.width, +// }; +// } + +// native.sponsoredBy = ad.text.sponsoredBy?.content || ''; +// native.body = ad.text.body?.content || ''; +// native.cta = ad.text.cta?.content || ''; +// native.clickUrl = ad.destinationUrls.destination || ''; +// native.impressionTrackers = ad.impressionTrackingUrls || [ad.renderedPixel]; + +// return native; +// } + const handleMeta = function () { - const storage = getStorageManager({ gvlid: GVLID, bidderCode: BIDDER_CODE }) + const storage = getStorageManager({ bidderCode: BIDDER_CODE }) let adnMeta = null if (storage.localStorageIsEnabled()) { adnMeta = JSON.parse(storage.getDataFromLocalStorage('adn.metaData')) @@ -82,6 +109,10 @@ export const spec = { network += '_video' } + // if (bid.mediaTypes && bid.mediaTypes.native) { + // network += '_native' + // } + bidRequests[network] = bidRequests[network] || []; bidRequests[network].push(bid); @@ -99,6 +130,7 @@ export const spec = { const network = networkKeys[j]; const networkRequest = [...request] if (network.indexOf('_video') > -1) { networkRequest.push('tt=' + DEFAULT_VAST_VERSION) } + // if (network.indexOf('_native') > -1) { networkRequest.push('tt=' + DEFAULT_NATIVE) } requests.push({ method: 'POST', url: ENDPOINT_URL + '?' + networkRequest.join('&'), @@ -137,13 +169,14 @@ export const spec = { if (adUnit.vastXml) { adResponse[adUnit.targetId].vastXml = adUnit.vastXml - adResponse[adUnit.targetId].mediaType = 'video' + adResponse[adUnit.targetId].mediaType = VIDEO + // } else if (ad.assets && ad.assets.image && ad.text && ad.text.title && ad.text.body && ad.destinationUrls && ad.destinationUrls.destination) { + // adResponse[adUnit.targetId].native = createNative(ad); + // adResponse[adUnit.targetId].mediaType = NATIVE; } else { adResponse[adUnit.targetId].ad = adUnit.html } - logInfo('BID', adResponse) - return adResponse } else return response }, {}); diff --git a/modules/adpod.js b/modules/adpod.js index f1ab4bd2ef1..4ab8e4e5ab9 100644 --- a/modules/adpod.js +++ b/modules/adpod.js @@ -28,14 +28,13 @@ import { import { addBidToAuction, AUCTION_IN_PROGRESS, - callPrebidCache, doCallbacksIfTimedout, getPriceByGranularity, getPriceGranularity } from '../src/auction.js'; import {checkAdUnitSetup} from '../src/prebid.js'; import {checkVideoBidSetup} from '../src/video.js'; -import {module, setupBeforeHookFnOnce} from '../src/hook.js'; +import {getHook, module, setupBeforeHookFnOnce} from '../src/hook.js'; import {store} from '../src/videoCache.js'; import {config} from '../src/config.js'; import {ADPOD} from '../src/mediaTypes.js'; @@ -424,7 +423,7 @@ config.getConfig('adpod', config => adpodSetConfig(config.adpod)); * This function initializes the adpod module's hooks. This is called by the corresponding adserver video module. */ function initAdpodHooks() { - setupBeforeHookFnOnce(callPrebidCache, callPrebidCacheHook); + setupBeforeHookFnOnce(getHook('callPrebidCache'), callPrebidCacheHook); setupBeforeHookFnOnce(checkAdUnitSetup, checkAdUnitSetupHook); setupBeforeHookFnOnce(checkVideoBidSetup, checkVideoBidSetupHook); } diff --git a/modules/adqueryBidAdapter.js b/modules/adqueryBidAdapter.js index be5ca4b1057..63e0c7dbe22 100644 --- a/modules/adqueryBidAdapter.js +++ b/modules/adqueryBidAdapter.js @@ -11,7 +11,7 @@ const ADQUERY_USER_SYNC_DOMAIN = ADQUERY_BIDDER_DOMAIN_PROTOCOL + '://' + ADQUER const ADQUERY_DEFAULT_CURRENCY = 'PLN'; const ADQUERY_NET_REVENUE = true; const ADQUERY_TTL = 360; -const storage = getStorageManager({gvlid: ADQUERY_GVLID, bidderCode: ADQUERY_BIDDER_CODE}); +const storage = getStorageManager({bidderCode: ADQUERY_BIDDER_CODE}); /** @type {BidderSpec} */ export const spec = { diff --git a/modules/adqueryIdSystem.js b/modules/adqueryIdSystem.js index d9552a470d0..5171802caba 100644 --- a/modules/adqueryIdSystem.js +++ b/modules/adqueryIdSystem.js @@ -9,11 +9,12 @@ import {ajax} from '../src/ajax.js'; import {getStorageManager} from '../src/storageManager.js'; import {submodule} from '../src/hook.js'; import { isFn, isStr, isPlainObject, logError } from '../src/utils.js'; +import {MODULE_TYPE_UID} from '../src/activities/modules.js'; const MODULE_NAME = 'qid'; const AU_GVLID = 902; -export const storage = getStorageManager({gvlid: AU_GVLID, moduleName: 'qid'}); +export const storage = getStorageManager({moduleType: MODULE_TYPE_UID, moduleName: 'qid'}); /** * Param or default. diff --git a/modules/adrinoBidAdapter.js b/modules/adrinoBidAdapter.js index 2a5588eeb8c..efd23761bfd 100644 --- a/modules/adrinoBidAdapter.js +++ b/modules/adrinoBidAdapter.js @@ -31,11 +31,9 @@ export const spec = { buildRequests: function (validBidRequests, bidderRequest) { // convert Native ORTB definition to old-style prebid native definition validBidRequests = convertOrtbRequestToProprietaryNative(validBidRequests); - const bidRequests = []; + let bids = []; for (let i = 0; i < validBidRequests.length; i++) { - let host = this.getBidderConfig('host') || BIDDER_HOST; - let requestData = { bidId: validBidRequests[i].bidId, nativeParams: validBidRequests[i].nativeParams, @@ -52,27 +50,37 @@ export const spec = { } } - bidRequests.push({ - method: REQUEST_METHOD, - url: host + '/bidder/bid/', - data: requestData, - options: { - contentType: 'application/json', - withCredentials: false, - } - }); + bids.push(requestData); } + let host = this.getBidderConfig('host') || BIDDER_HOST; + let bidRequests = []; + bidRequests.push({ + method: REQUEST_METHOD, + url: host + '/bidder/bids/', + data: bids, + options: { + contentType: 'application/json', + withCredentials: false, + } + }); + return bidRequests; }, interpretResponse: function (serverResponse, bidRequest) { const response = serverResponse.body; - const bidResponses = []; - if (!response.noAd) { - bidResponses.push(response); + const output = []; + + if (response.bidResponses) { + for (const bidResponse of response.bidResponses) { + if (!bidResponse.noAd) { + output.push(bidResponse); + } + } } - return bidResponses; + + return output; }, onBidWon: function (bid) { diff --git a/modules/adriverIdSystem.js b/modules/adriverIdSystem.js index fb8ce99ec16..b3ab00350ea 100644 --- a/modules/adriverIdSystem.js +++ b/modules/adriverIdSystem.js @@ -8,11 +8,12 @@ import { logError, isPlainObject } from '../src/utils.js' import { ajax } from '../src/ajax.js'; import { submodule } from '../src/hook.js'; -import { getStorageManager } from '../src/storageManager.js'; +import {getStorageManager} from '../src/storageManager.js'; +import {MODULE_TYPE_UID} from '../src/activities/modules.js'; const MODULE_NAME = 'adriverId'; -export const storage = getStorageManager(); +export const storage = getStorageManager({moduleType: MODULE_TYPE_UID, moduleName: MODULE_NAME}); /** @type {Submodule} */ export const adriverIdSubmodule = { diff --git a/modules/adsinteractiveBidAdapter.js b/modules/adsinteractiveBidAdapter.js new file mode 100644 index 00000000000..304b8bcade0 --- /dev/null +++ b/modules/adsinteractiveBidAdapter.js @@ -0,0 +1,144 @@ +import { + deepAccess, +} from '../src/utils.js'; + +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { BANNER } from '../src/mediaTypes.js'; + +const ADSINTERACTIVE_CODE = 'adsinteractive'; +const USER_SYNC_URL_IMAGE = 'https://pb.adsinteractive.com/img'; +const USER_SYNC_URL_IFRAME = 'https://pb.adsinteractive.com/sync'; + +export const spec = { + code: ADSINTERACTIVE_CODE, + supportedMediaTypes: [BANNER], + + isBidRequestValid: (bid) => { + return ( + !!bid.params.adUnit && !!bid.bidId && bid.bidder === 'adsinteractive' + ); + }, + + buildRequests: (bidRequests, bidderRequest) => { + return bidRequests.map((bid) => { + var gdprConsent; + if (bidderRequest && bidderRequest.gdprConsent) { + gdprConsent = { + consent_string: bidderRequest.gdprConsent.consentString, + consent_required: bidderRequest.gdprConsent.gdprApplies, + }; + + if ( + bidderRequest.gdprConsent.addtlConsent && + bidderRequest.gdprConsent.addtlConsent.indexOf('~') !== -1 + ) { + let ac = bidderRequest.gdprConsent.addtlConsent; + let acStr = ac.substring(ac.indexOf('~') + 1); + gdprConsent.addtl_consent = acStr + .split('.') + .map((id) => parseInt(id, 10)); + } + } + + let url = 'https://pb.adsinteractive.com/prebid'; + const data = { + id: bid.bidId, + at: 1, + source: { fd: 0 }, + gdprConsent: gdprConsent, + site: { + page: bid.ortb2.site.page, + keywords: bid.ortb2.site.keywords, + domain: bid.ortb2.site.domain, + publisher: { + domain: bid.ortb2.site.domain, + }, + ext: { + amp: Number(bidderRequest.refererInfo.isAmp), + }, + }, + regs: bid.ortb2.regs, + device: bid.ortb2.device, + user: bid.ortb2.user, + imp: [ + { + id: bid.params.adUnit, + banner: { + format: bid.sizes.map((size) => ({ + w: size[0], + h: size[1], + })), + }, + ext: { + bidder: { + adUnit: bid.params.adUnit, + }, + }, + }, + ], + tmax: bidderRequest.timeout, + }; + const options = { + withCredentials: true, + }; + return { + method: 'POST', + url, + data, + options, + }; + }); + }, + + interpretResponse: (serverResponse, bidRequest) => { + let answer = []; + if (serverResponse && serverResponse.body && serverResponse.body.seatbid) { + serverResponse.body.seatbid.forEach((seatbid) => { + if (seatbid.bid.length) { + answer = [ + ...answer, + ...seatbid.bid + .filter((bid) => bid.price > 0) + .map((adsinteractiveBid) => { + const bid = { + id: adsinteractiveBid.id, + requestId: bidRequest.data.id, + cpm: adsinteractiveBid.price, + netRevenue: true, + ttl: 1000, + ad: adsinteractiveBid.adm, + meta: {advertiserDomains: adsinteractiveBid && adsinteractiveBid.adomain ? adsinteractiveBid.adomain : []}, + width: adsinteractiveBid.w, + height: adsinteractiveBid.h, + currency: serverResponse.body.cur || 'USD', + creativeId: adsinteractiveBid.crid || 0, + }; + return bid; + }), + ]; + } + }); + } + return answer; + }, + getUserSyncs: (syncOptions, serverResponse, gdprConsent, uspConsent) => { + if (syncOptions.iframeEnabled) { + const auid = serverResponse.filter(resp => deepAccess(resp, 'body.ext.auid')) + .map(resp => resp.body.ext.auid); + return [ + { + type: 'iframe', + url: USER_SYNC_URL_IFRAME + '?consent=' + gdprConsent.consentString + '&auid=' + auid, + }, + ]; + } else { + return [ + { + type: 'image', + url: USER_SYNC_URL_IMAGE, + }, + ]; + } + }, +}; +registerBidder(spec); diff --git a/modules/adsinteractiveBidAdapter.md b/modules/adsinteractiveBidAdapter.md new file mode 100644 index 00000000000..81afcd18200 --- /dev/null +++ b/modules/adsinteractiveBidAdapter.md @@ -0,0 +1,31 @@ +# Overview + +Module Name: AdsInteractive Bidder Adapter + +Module Type: Bidder Adapter + +Maintainer: it@adsinteractive.com + +# Description + +You can use this adapter to get a bid from adsinteractive.com. + +About us : https://www.adsinteractive.com + + +# Test Parameters +```javascript + var adUnits = [ + { + sizes: [[300, 250]], + bids: [ + { + bidder: "adsinteractive", + params: { + adUnit: "example_adunit_1" + } + } + ] + } + ]; +``` diff --git a/modules/adtelligentBidAdapter.js b/modules/adtelligentBidAdapter.js index a7c83d9c190..ebba4f3ec6b 100644 --- a/modules/adtelligentBidAdapter.js +++ b/modules/adtelligentBidAdapter.js @@ -24,6 +24,7 @@ const HOST_GETTERS = { pgam: () => 'ghb.pgamssp.com', ocm: () => 'ghb.cenarius.orangeclickmedia.com', vidcrunchllc: () => 'ghb.platform.vidcrunch.com', + '9dotsmedia': () => 'ghb.platform.audiodots.com' } const getUri = function (bidderCode) { let bidderWithoutSuffix = bidderCode.split('_')[0]; @@ -39,12 +40,18 @@ const syncsCache = {}; export const spec = { code: BIDDER_CODE, gvlid: 410, - aliases: ['onefiftytwomedia', 'appaloosa', 'bidsxchange', 'streamkey', 'janet', + aliases: [ + 'onefiftytwomedia', + 'appaloosa', + 'bidsxchange', + 'streamkey', + 'janet', { code: 'selectmedia', gvlid: 775 }, { code: 'navelix', gvlid: 380 }, 'pgam', { code: 'ocm', gvlid: 1148 }, { code: 'vidcrunchllc', gvlid: 1145 }, + '9dotsmedia' ], supportedMediaTypes: [VIDEO, BANNER], isBidRequestValid: function (bid) { diff --git a/modules/aduptechBidAdapter.js b/modules/aduptechBidAdapter.js index 5d8b69af77f..c7138e43cfe 100644 --- a/modules/aduptechBidAdapter.js +++ b/modules/aduptechBidAdapter.js @@ -1,9 +1,10 @@ -import {deepAccess, getAdUnitSizes} from '../src/utils.js'; +import {deepClone, getAdUnitSizes, isArray, isBoolean, isEmpty, isFn, isPlainObject} from '../src/utils.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import {BANNER, NATIVE} from '../src/mediaTypes.js'; import { convertOrtbRequestToProprietaryNative } from '../src/native.js'; export const BIDDER_CODE = 'aduptech'; +export const GVLID = 647; export const ENDPOINT_URL_PUBLISHER_PLACEHOLDER = '{PUBLISHER}'; export const ENDPOINT_URL = 'https://rtb.d.adup-tech.com/prebid/' + ENDPOINT_URL_PUBLISHER_PLACEHOLDER + '_bid'; export const ENDPOINT_METHOD = 'POST'; @@ -20,14 +21,14 @@ export const internal = { * @returns {null|Object.} */ extractGdpr: (bidderRequest) => { - if (bidderRequest && bidderRequest.gdprConsent) { - return { - consentString: bidderRequest.gdprConsent.consentString, - consentRequired: (typeof bidderRequest.gdprConsent.gdprApplies === 'boolean') ? bidderRequest.gdprConsent.gdprApplies : true - }; + if (!bidderRequest?.gdprConsent) { + return null; } - return null; + return { + consentString: bidderRequest.gdprConsent.consentString, + consentRequired: (isBoolean(bidderRequest.gdprConsent.gdprApplies)) ? bidderRequest.gdprConsent.gdprApplies : true + }; }, /** @@ -59,12 +60,34 @@ export const internal = { * @returns {null|Object.} */ extractBannerConfig: (bidRequest) => { - const sizes = getAdUnitSizes(bidRequest); - if (Array.isArray(sizes) && sizes.length > 0) { - return { sizes: sizes }; + const adUnitSizes = getAdUnitSizes(bidRequest); + if (!isArray(adUnitSizes) || isEmpty(adUnitSizes)) { + return null; } - return null; + const banner = { sizes: [] }; + + adUnitSizes.forEach(adUnitSize => { + const size = deepClone(adUnitSize); + + // try to add floor for each banner size + const floor = internal.getFloor(bidRequest, { mediaType: BANNER, size: adUnitSize }); + if (floor) { + size.push(floor.floor); + size.push(floor.currency); + } + + banner.sizes.push(size); + }); + + // try to add default floor for banner + const floor = internal.getFloor(bidRequest, { mediaType: BANNER, size: '*' }); + if (floor) { + banner.floorPrice = floor.floor; + banner.floorCurrency = floor.currency; + } + + return banner; }, /** @@ -74,11 +97,20 @@ export const internal = { * @returns {null|Object.} */ extractNativeConfig: (bidRequest) => { - if (bidRequest && deepAccess(bidRequest, 'mediaTypes.native')) { - return bidRequest.mediaTypes.native; + if (!bidRequest?.mediaTypes?.native) { + return null; } - return null; + const native = deepClone(bidRequest.mediaTypes.native); + + // try to add default floor for native + const floor = internal.getFloor(bidRequest, { mediaType: NATIVE, size: '*' }); + if (floor) { + native.floorPrice = floor.floor; + native.floorCurrency = floor.currency; + } + + return native; }, /** @@ -88,35 +120,33 @@ export const internal = { * @returns {null|Object.} */ extractParams: (bidRequest) => { - if (bidRequest && bidRequest.params) { - return bidRequest.params + if (!bidRequest?.params) { + return null; } - return null; + return deepClone(bidRequest.params); }, /** - * Extracts the floor price params from given bidRequest + * Try to get floor information via bidRequest.getFloor() * * @param {BidRequest} bidRequest - * @returns {undefined|float} + * @param {Object} options + * @returns {null|Object.} */ - extractFloorPrice: (bidRequest) => { - let floorPrice; - if (bidRequest && bidRequest.params && bidRequest.params.floor) { - // if there is a manual floorPrice set - floorPrice = !isNaN(parseInt(bidRequest.params.floor)) ? bidRequest.params.floor : undefined; - } - if (typeof bidRequest.getFloor === 'function') { - // use prebid floor module - let floorInfo; - try { - floorInfo = bidRequest.getFloor(); - } catch (e) {} - floorPrice = typeof floorInfo === 'object' && !isNaN(parseInt(floorInfo.floor)) ? floorInfo.floor : floorPrice; + getFloor: (bidRequest, options) => { + if (!isFn(bidRequest?.getFloor)) { + return null; } - return floorPrice; + try { + const floor = bidRequest.getFloor(options); + if (isPlainObject(floor) && !isNaN(floor.floor)) { + return floor; + } + } catch {} + + return null; }, /** @@ -128,11 +158,11 @@ export const internal = { groupBidRequestsByPublisher: (bidRequests) => { const groupedBidRequests = {}; - if (!bidRequests || bidRequests.length === 0) { + if (!bidRequests || isEmpty(bidRequests)) { return groupedBidRequests; } - bidRequests.forEach((bidRequest) => { + bidRequests.forEach(bidRequest => { const publisher = internal.extractParams(bidRequest).publisher; if (!publisher) { return; @@ -165,6 +195,7 @@ export const internal = { export const spec = { code: BIDDER_CODE, supportedMediaTypes: [BANNER, NATIVE], + gvlid: GVLID, /** * Validate given bid request @@ -205,7 +236,7 @@ export const spec = { const requests = []; // stop here on invalid or empty data - if (!bidderRequest || !validBidRequests || validBidRequests.length === 0) { + if (!bidderRequest || !validBidRequests || isEmpty(validBidRequests)) { return requests; } @@ -237,7 +268,7 @@ export const spec = { } // handle multiple bids per request - groupedBidRequests[publisher].forEach((bidRequest) => { + groupedBidRequests[publisher].forEach(bidRequest => { const bid = { bidId: bidRequest.bidId, transactionId: bidRequest.transactionId, @@ -257,10 +288,11 @@ export const spec = { bid.native = nativeConfig; } - // add floor price - const floorPrice = internal.extractFloorPrice(bidRequest); - if (floorPrice) { - bid.floorPrice = floorPrice; + // try to add default floor + const floor = internal.getFloor(bidRequest, { mediaType: '*', size: '*' }); + if (floor) { + bid.floorPrice = floor.floor; + bid.floorCurrency = floor.currency; } request.data.imp.push(bid); @@ -282,12 +314,12 @@ export const spec = { const bidResponses = []; // stop here on invalid or empty data - if (!response || !deepAccess(response, 'body.bids') || response.body.bids.length === 0) { + if (!response?.body?.bids || isEmpty(response.body.bids)) { return bidResponses; } // parse multiple bids per response - response.body.bids.forEach((bid) => { + response.body.bids.forEach(bid => { if (!bid || !bid.bid || !bid.creative) { return; } diff --git a/modules/adxcgAnalyticsAdapter.js b/modules/adxcgAnalyticsAdapter.js index f3bb1270334..21b6c1be783 100644 --- a/modules/adxcgAnalyticsAdapter.js +++ b/modules/adxcgAnalyticsAdapter.js @@ -3,6 +3,7 @@ import { ajax } from '../src/ajax.js'; import adapter from '../libraries/analyticsAdapter/AnalyticsAdapter.js'; import adapterManager from '../src/adapterManager.js'; import CONSTANTS from '../src/constants.json'; +import {getGlobal} from '../src/prebidGlobal.js'; /** * Analytics adapter from adxcg.com @@ -122,7 +123,7 @@ function send (data) { ats: adxcgAnalyticsAdapter.context.auctionTimestamp, aav: adxcgAnalyticsVersion, iob: intersectionObserverAvailable(window) ? '1' : '0', - pbv: $$PREBID_GLOBAL$$.version, + pbv: getGlobal().version, sz: window.screen.width + 'x' + window.screen.height } }); diff --git a/modules/adyoulikeBidAdapter.js b/modules/adyoulikeBidAdapter.js index f7473b3bad4..376d108e980 100644 --- a/modules/adyoulikeBidAdapter.js +++ b/modules/adyoulikeBidAdapter.js @@ -1,6 +1,5 @@ import {buildUrl, deepAccess, parseSizesInput} from '../src/utils.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; -import {createEidsArray} from './userId/eids.js'; import {find} from '../src/polyfill.js'; import {BANNER, NATIVE, VIDEO} from '../src/mediaTypes.js'; import { convertOrtbRequestToProprietaryNative } from '../src/native.js'; @@ -122,10 +121,12 @@ export const spec = { payload.ortb2 = bidderRequest.ortb2; } - if (deepAccess(bidderRequest, 'userId')) { - payload.userId = createEidsArray(bidderRequest.userId); + if (deepAccess(bidderRequest, 'userIdAsEids')) { + payload.userId = bidderRequest.userIdAsEids; } + payload.pbjs_version = '$prebid.version$'; + const data = JSON.stringify(payload); const options = { withCredentials: true diff --git a/modules/aidemBidAdapter.js b/modules/aidemBidAdapter.js index 41bf680db1c..2f8732a8ec4 100644 --- a/modules/aidemBidAdapter.js +++ b/modules/aidemBidAdapter.js @@ -1,4 +1,4 @@ -import {_each, contains, deepAccess, deepSetValue, getDNT, isBoolean, isStr, logError, logInfo} from '../src/utils.js'; +import {_each, contains, deepAccess, deepSetValue, getDNT, isBoolean, isStr, isNumber, logError, logInfo} from '../src/utils.js'; import {config} from '../src/config.js'; import {BANNER, VIDEO} from '../src/mediaTypes.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; @@ -21,6 +21,8 @@ export const ERROR_CODES = { SITE_ID_INVALID_VALUE: 4, MEDIA_TYPE_NOT_SUPPORTED: 5, PUBLISHER_ID_INVALID_VALUE: 6, + INVALID_RATELIMIT: 7, + PLACEMENT_ID_INVALID_VALUE: 8, }; const endpoints = { @@ -30,29 +32,29 @@ const endpoints = { timeout: `${BASE_URL}/notice/timeout`, error: `${BASE_URL}/notice/error`, } -} +}; export function setEndPoints(env = null, path = '', mediaType = BANNER) { switch (env) { case 'local': - endpoints.request = mediaType === BANNER ? `${LOCAL_BASE_URL}${path}/bid/request` : `${LOCAL_BASE_URL}${path}/bid/videorequest` - endpoints.notice.win = `${LOCAL_BASE_URL}${path}/notice/win` - endpoints.notice.error = `${LOCAL_BASE_URL}${path}/notice/error` - endpoints.notice.timeout = `${LOCAL_BASE_URL}${path}/notice/timeout` + endpoints.request = mediaType === BANNER ? `${LOCAL_BASE_URL}${path}/bid/request` : `${LOCAL_BASE_URL}${path}/bid/videorequest`; + endpoints.notice.win = `${LOCAL_BASE_URL}${path}/notice/win`; + endpoints.notice.error = `${LOCAL_BASE_URL}${path}/notice/error`; + endpoints.notice.timeout = `${LOCAL_BASE_URL}${path}/notice/timeout`; break; case 'main': - endpoints.request = mediaType === BANNER ? `${BASE_URL}${path}/bid/request` : `${BASE_URL}${path}/bid/videorequest` - endpoints.notice.win = `${BASE_URL}${path}/notice/win` - endpoints.notice.error = `${BASE_URL}${path}/notice/error` - endpoints.notice.timeout = `${BASE_URL}${path}/notice/timeout` + endpoints.request = mediaType === BANNER ? `${BASE_URL}${path}/bid/request` : `${BASE_URL}${path}/bid/videorequest`; + endpoints.notice.win = `${BASE_URL}${path}/notice/win`; + endpoints.notice.error = `${BASE_URL}${path}/notice/error`; + endpoints.notice.timeout = `${BASE_URL}${path}/notice/timeout`; break; } - return endpoints + return endpoints; } config.getConfig('aidem', function (config) { - if (config.aidem.env) { setEndPoints(config.aidem.env, config.aidem.path, config.aidem.mediaType) } -}) + if (config.aidem.env) { setEndPoints(config.aidem.env, config.aidem.path, config.aidem.mediaType); } +}); // AIDEM Custom FN function recur(obj) { @@ -121,8 +123,8 @@ function getDevice() { function getRegs() { let regs = {}; - const consentManagement = config.getConfig('consentManagement') - const coppa = config.getConfig('coppa') + const consentManagement = config.getConfig('consentManagement'); + const coppa = config.getConfig('coppa'); if (consentManagement && !!(consentManagement.gdpr)) { deepSetValue(regs, 'gdpr_applies', !!consentManagement.gdpr); } else { @@ -145,11 +147,16 @@ function getRegs() { } function getPageUrl(bidderRequest) { - return bidderRequest?.refererInfo?.page + return bidderRequest?.refererInfo?.page; } function buildWinNotice(bid) { + const params = bid.params[0]; + const app = deepAccess(bid, 'meta.ext.app') return { + publisherId: params.publisherId, + siteId: params.siteId, + placementId: params.placementId, burl: deepAccess(bid, 'meta.burl'), cpm: bid.cpm, currency: bid.currency, @@ -161,7 +168,10 @@ function buildWinNotice(bid) { ttl: bid.ttl, requestTimestamp: bid.requestTimestamp, responseTimestamp: bid.responseTimestamp, - } + mediatype: bid.mediaType, + environment: app ? 'app' : 'web', + ...app + }; } function buildErrorNotice(prebidErrorResponse) { @@ -171,29 +181,29 @@ function buildErrorNotice(prebidErrorResponse) { auctionId: prebidErrorResponse.auctionId, bidderRequestId: prebidErrorResponse.bidderRequestId, metrics: {} - } + }; } function hasValidFloor(obj) { - if (!obj) return false - const hasValue = !isNaN(Number(obj.value)) - const hasCurrency = contains(AVAILABLE_CURRENCIES, obj.currency) - return hasValue && hasCurrency + if (!obj) return false; + const hasValue = !isNaN(Number(obj.value)); + const hasCurrency = contains(AVAILABLE_CURRENCIES, obj.currency); + return hasValue && hasCurrency; } function getMediaType(bidRequest) { - if ((bidRequest.mediaTypes && bidRequest.mediaTypes.hasOwnProperty('video')) || bidRequest.params.hasOwnProperty('video')) { return VIDEO } - return BANNER + if ((bidRequest.mediaTypes && bidRequest.mediaTypes.hasOwnProperty('video')) || bidRequest.params.hasOwnProperty('video')) { return VIDEO; } + return BANNER; } function getPrebidRequestFields(bidderRequest, bidRequests) { - const payload = {} + const payload = {}; // Base Payload Data deepSetValue(payload, 'id', bidderRequest.auctionId); // Impressions - setPrebidImpressionObject(bidRequests, payload) + setPrebidImpressionObject(bidRequests, payload); // Device - deepSetValue(payload, 'device', getDevice()) + deepSetValue(payload, 'device', getDevice()); // Timeout deepSetValue(payload, 'tmax', bidderRequest.timeout); // Currency @@ -203,13 +213,13 @@ function getPrebidRequestFields(bidderRequest, bidRequests) { // Privacy Regs deepSetValue(payload, 'regs', getRegs()); // Site - setPrebidSiteObject(bidderRequest, payload) + setPrebidSiteObject(bidderRequest, payload); // Environment - setPrebidRequestEnvironment(payload) + setPrebidRequestEnvironment(payload); // AT auction type deepSetValue(payload, 'at', 1); - return payload + return payload; } function setPrebidImpressionObject(bidRequests, payload) { @@ -220,22 +230,24 @@ function setPrebidImpressionObject(bidRequests, payload) { deepSetValue(impressionObject, 'id', bidRequest.bidId); // Transaction id deepSetValue(impressionObject, 'tid', deepAccess(bidRequest, 'transactionId')); + // placement id + deepSetValue(impressionObject, 'tagid', deepAccess(bidRequest, 'params.placementId', null)); // Publisher id deepSetValue(payload, 'site.publisher.id', deepAccess(bidRequest, 'params.publisherId')); // Site id deepSetValue(payload, 'site.id', deepAccess(bidRequest, 'params.siteId')); - const mediaType = getMediaType(bidRequest) + const mediaType = getMediaType(bidRequest); switch (mediaType) { case 'banner': - setPrebidImpressionObjectBanner(bidRequest, impressionObject) + setPrebidImpressionObjectBanner(bidRequest, impressionObject); break; case 'video': - setPrebidImpressionObjectVideo(bidRequest, impressionObject) + setPrebidImpressionObjectVideo(bidRequest, impressionObject); break; } // Floor (optional) - setPrebidImpressionObjectFloor(bidRequest, impressionObject) + setPrebidImpressionObjectFloor(bidRequest, impressionObject); impressionObject.imp_ext = {}; @@ -265,13 +277,15 @@ function setPrebidRequestEnvironment(payload) { deepSetValue(payload, 'environment.inp.jp', window.JSON.parse.name === 'parse' && typeof window.JSON.parse.prototype === 'undefined'); deepSetValue(payload, 'environment.inp.ofe', window.Object.fromEntries.name === 'fromEntries' && typeof window.Object.fromEntries.prototype === 'undefined'); deepSetValue(payload, 'environment.inp.oa', window.Object.assign.name === 'assign' && typeof window.Object.assign.prototype === 'undefined'); + deepSetValue(payload, 'environment.wpar.innerWidth', window.innerWidth); + deepSetValue(payload, 'environment.wpar.innerHeight', window.innerHeight); } function setPrebidImpressionObjectFloor(bidRequest, impressionObject) { - const floor = deepAccess(bidRequest, 'params.floor') + const floor = deepAccess(bidRequest, 'params.floor'); if (hasValidFloor(floor)) { - deepSetValue(impressionObject, 'floor.value', floor.value) - deepSetValue(impressionObject, 'floor.currency', floor.currency) + deepSetValue(impressionObject, 'floor.value', floor.value); + deepSetValue(impressionObject, 'floor.currency', floor.currency); } } @@ -318,7 +332,7 @@ function getPrebidResponseBidObject(openRTBResponseBidObject) { deepSetValue(prebidResponseBidObject, 'currency', openRTBResponseBidObject.cur ? openRTBResponseBidObject.cur.toUpperCase() : DEFAULT_CURRENCY); deepSetValue(prebidResponseBidObject, 'width', openRTBResponseBidObject.w); deepSetValue(prebidResponseBidObject, 'height', openRTBResponseBidObject.h); - deepSetValue(prebidResponseBidObject, 'dealId', openRTBResponseBidObject.dealid) + deepSetValue(prebidResponseBidObject, 'dealId', openRTBResponseBidObject.dealid); deepSetValue(prebidResponseBidObject, 'netRevenue', true); deepSetValue(prebidResponseBidObject, 'ttl', 60000); @@ -331,13 +345,14 @@ function getPrebidResponseBidObject(openRTBResponseBidObject) { deepSetValue(prebidResponseBidObject, 'mediaType', BANNER); deepSetValue(prebidResponseBidObject, 'ad', openRTBResponseBidObject.adm); } - setPrebidResponseBidObjectMeta(prebidResponseBidObject, openRTBResponseBidObject) - return prebidResponseBidObject + setPrebidResponseBidObjectMeta(prebidResponseBidObject, openRTBResponseBidObject); + return prebidResponseBidObject; } function setPrebidResponseBidObjectMeta(prebidResponseBidObject, openRTBResponseBidObject) { logInfo('AIDEM Bid Adapter meta', openRTBResponseBidObject); - deepSetValue(prebidResponseBidObject, 'meta.advertiserDomains', openRTBResponseBidObject.adomain); + deepSetValue(prebidResponseBidObject, 'meta.advertiserDomains', deepAccess(openRTBResponseBidObject, 'meta.advertiserDomains')); + deepSetValue(prebidResponseBidObject, 'meta.ext', deepAccess(openRTBResponseBidObject, 'meta.ext')); if (openRTBResponseBidObject.cat && Array.isArray(openRTBResponseBidObject.cat)) { const primaryCatId = openRTBResponseBidObject.cat.shift(); deepSetValue(prebidResponseBidObject, 'meta.primaryCatId', primaryCatId); @@ -353,28 +368,28 @@ function setPrebidResponseBidObjectMeta(prebidResponseBidObject, openRTBResponse } function hasValidMediaType(bidRequest) { - const supported = hasBannerMediaType(bidRequest) || hasVideoMediaType(bidRequest) + const supported = hasBannerMediaType(bidRequest) || hasVideoMediaType(bidRequest); if (!supported) { logError('AIDEM Bid Adapter: media type not supported', { bidder: BIDDER_CODE, code: ERROR_CODES.MEDIA_TYPE_NOT_SUPPORTED }); } - return supported + return supported; } function hasBannerMediaType(bidRequest) { - return !!deepAccess(bidRequest, 'mediaTypes.banner') + return !!deepAccess(bidRequest, 'mediaTypes.banner'); } function hasVideoMediaType(bidRequest) { - return !!deepAccess(bidRequest, 'mediaTypes.video') + return !!deepAccess(bidRequest, 'mediaTypes.video'); } function hasValidBannerMediaType(bidRequest) { - const sizes = deepAccess(bidRequest, 'mediaTypes.banner.sizes') + const sizes = deepAccess(bidRequest, 'mediaTypes.banner.sizes'); if (!sizes) { logError('AIDEM Bid Adapter: media type sizes missing', { bidder: BIDDER_CODE, code: ERROR_CODES.PROPERTY_NOT_INCLUDED }); return false; } - return true + return true; } function hasValidVideoMediaType(bidRequest) { @@ -383,23 +398,38 @@ function hasValidVideoMediaType(bidRequest) { logError('AIDEM Bid Adapter: media type playerSize missing', { bidder: BIDDER_CODE, code: ERROR_CODES.PROPERTY_NOT_INCLUDED }); return false; } - return true + return true; } function hasValidVideoParameters(bidRequest) { - let valid = true + let valid = true; const adUnitsParameters = deepAccess(bidRequest, 'mediaTypes.video'); const bidderParameter = deepAccess(bidRequest, 'params.video'); for (let property of REQUIRED_VIDEO_PARAMS) { - const hasAdUnitParameter = adUnitsParameters.hasOwnProperty(property) - const hasBidderParameter = bidderParameter && bidderParameter.hasOwnProperty(property) + const hasAdUnitParameter = adUnitsParameters.hasOwnProperty(property); + const hasBidderParameter = bidderParameter && bidderParameter.hasOwnProperty(property); if (!hasAdUnitParameter && !hasBidderParameter) { logError(`AIDEM Bid Adapter: ${property} is not included in either the adunit or params level`, { bidder: BIDDER_CODE, code: ERROR_CODES.PROPERTY_NOT_INCLUDED }); - valid = false + valid = false; } } - return valid + return valid; +} + +function passesRateLimit(bidRequest) { + const rateLimit = deepAccess(bidRequest, 'params.rateLimit', 1); + if (!isNumber(rateLimit) || rateLimit > 1 || rateLimit < 0) { + logError('AIDEM Bid Adapter: invalid rateLimit (must be a number between 0 and 1)', { bidder: BIDDER_CODE, code: ERROR_CODES.INVALID_RATELIMIT }); + return false; + } + if (rateLimit !== 1) { + const randomRateValue = Math.random(); + if (randomRateValue > rateLimit) { + return false; + } + } + return true; } function hasValidParameters(bidRequest) { @@ -417,7 +447,7 @@ function hasValidParameters(bidRequest) { return false; } - return true + return true; } export const spec = { @@ -427,28 +457,32 @@ export const spec = { logInfo('bid: ', bidRequest); // check if request has valid mediaTypes - if (!hasValidMediaType(bidRequest)) return false + if (!hasValidMediaType(bidRequest)) return false; // check if request has valid media type parameters at adUnit level if (hasBannerMediaType(bidRequest) && !hasValidBannerMediaType(bidRequest)) { - return false + return false; } if (hasVideoMediaType(bidRequest) && !hasValidVideoMediaType(bidRequest)) { - return false + return false; } if (hasVideoMediaType(bidRequest) && !hasValidVideoParameters(bidRequest)) { - return false + return false; } - return hasValidParameters(bidRequest) + if (!hasValidParameters(bidRequest)) { + return false; + } + + return passesRateLimit(bidRequest); }, buildRequests: function(validBidRequests, bidderRequest) { logInfo('validBidRequests: ', validBidRequests); logInfo('bidderRequest: ', bidderRequest); - const prebidRequest = getPrebidRequestFields(bidderRequest, validBidRequests) + const prebidRequest = getPrebidRequestFields(bidderRequest, validBidRequests); const payloadString = JSON.stringify(prebidRequest); return { @@ -470,7 +504,7 @@ export const spec = { return; } logInfo('CPM OK'); - const bid = getPrebidResponseBidObject(bidObject) + const bid = getPrebidResponseBidObject(bidObject); bids.push(bid); }); return bids; @@ -479,14 +513,14 @@ export const spec = { onBidWon: function(bid) { // Bidder specific code logInfo('onBidWon bid: ', bid); - const notice = buildWinNotice(bid) + const notice = buildWinNotice(bid); ajax(endpoints.notice.win, null, JSON.stringify(notice), { method: 'POST', withCredentials: true }); }, onBidderError: function({ bidderRequest }) { // Bidder specific code - const notice = buildErrorNotice(bidderRequest) + const notice = buildErrorNotice(bidderRequest); ajax(endpoints.notice.error, null, JSON.stringify(notice), { method: 'POST', withCredentials: true }); }, -} +}; registerBidder(spec); diff --git a/modules/aidemBidAdapter.md b/modules/aidemBidAdapter.md index a5beca97d10..b59014c76ed 100644 --- a/modules/aidemBidAdapter.md +++ b/modules/aidemBidAdapter.md @@ -14,10 +14,12 @@ This module is GDPR and CCPA compliant, and no 3rd party userIds are allowed. ## Global Bid Params -| Name | Scope | Description | Example | Type | -|---------------|----------|---------------------|---------------|----------| -| `siteId` | required | Unique site ID | `'ABCDEF'` | `String` | -| `publisherId` | required | Unique publisher ID | `'ABCDEF'` | `String` | +| Name | Scope | Description | Example | Type | +|---------------|----------|-------------------------|------------|----------| +| `siteId` | required | Unique site ID | `'ABCDEF'` | `String` | +| `publisherId` | required | Unique publisher ID | `'ABCDEF'` | `String` | +| `placementId` | optional | Unique publisher tag ID | `'ABCDEF'` | `String` | +| `rateLimit` | optional | Limit the volume sent to AIDEM. Must be between 0 and 1 | `0.6` | `Number` | ### Banner Bid Params @@ -66,7 +68,8 @@ var adUnits = [{ bids: [{ bidder: 'aidem', params: { - siteId: 'prebid-test-site', + siteId: 'prebid-test-siteId', + publisherId: 'prebid-test-publisherId', }, }] }]; @@ -89,7 +92,8 @@ var adUnits = [{ bids: [{ bidder: 'aidem', params: { - siteId: 'prebid-test-site', + siteId: 'prebid-test-siteId', + publisherId: 'prebid-test-publisherId', }, }] }]; diff --git a/modules/airgridRtdProvider.js b/modules/airgridRtdProvider.js index 3578cc4b87e..c94a71eecde 100644 --- a/modules/airgridRtdProvider.js +++ b/modules/airgridRtdProvider.js @@ -8,12 +8,12 @@ import { config } from '../src/config.js'; import { submodule } from '../src/hook.js'; import { - mergeDeep, deepSetValue, deepAccess, } from '../src/utils.js'; import { getGlobal } from '../src/prebidGlobal.js'; -import { getStorageManager } from '../src/storageManager.js'; +import {getStorageManager} from '../src/storageManager.js'; +import {MODULE_TYPE_RTD} from '../src/activities/modules.js'; const MODULE_NAME = 'realTimeData'; const SUBMODULE_NAME = 'airgrid'; @@ -21,7 +21,7 @@ const AG_TCF_ID = 782; export const AG_AUDIENCE_IDS_KEY = 'edkt_matched_audience_ids'; export const storage = getStorageManager({ - gvlid: AG_TCF_ID, + moduleType: MODULE_TYPE_RTD, moduleName: SUBMODULE_NAME, }); @@ -85,13 +85,24 @@ function setAudiencesToAppNexusAdUnits(adUnits, audiences) { * @param {Array} audiences * @return {{}} a map from bidder code to ORTB2 config */ -export function getAudiencesAsBidderOrtb2(rtdConfig, audiences) { +export function setAudiencesAsBidderOrtb2(rtdConfig, audiences) { const bidders = deepAccess(rtdConfig, 'params.bidders'); - if (!bidders || bidders.length === 0) return {}; - const agOrtb2 = {} - deepSetValue(agOrtb2, 'ortb2.user.ext.data.airgrid', audiences || []); + if (!bidders || bidders.length === 0 || !audiences || audiences.length === 0) return; - return Object.fromEntries(bidders.map(bidder => [bidder, agOrtb2])); + const keywords = audiences.map( + (audienceId) => `perid=${audienceId}` + ).join(','); + + config.mergeBidderConfig({ + bidders: bidders, + config: { + ortb2: { + site: { + keywords, + } + } + } + }) } export function setAudiencesUsingAppNexusAuctionKeywords(audiences) { @@ -131,7 +142,7 @@ export function passAudiencesToBidders( const audiences = getMatchedAudiencesFromStorage(); if (audiences.length > 0) { setAudiencesUsingAppNexusAuctionKeywords(audiences); - mergeDeep(bidConfig?.ortb2Fragments?.bidder, getAudiencesAsBidderOrtb2(rtdConfig, audiences)); + setAudiencesAsBidderOrtb2(rtdConfig, audiences) if (adUnits) { setAudiencesToAppNexusAdUnits(adUnits, audiences); } @@ -144,6 +155,7 @@ export const airgridSubmodule = { name: SUBMODULE_NAME, init: init, getBidRequestData: passAudiencesToBidders, + gvlid: AG_TCF_ID }; submodule(MODULE_NAME, airgridSubmodule); diff --git a/modules/ajaBidAdapter.js b/modules/ajaBidAdapter.js index 121dc2ef96f..133a2fdbadf 100644 --- a/modules/ajaBidAdapter.js +++ b/modules/ajaBidAdapter.js @@ -1,20 +1,29 @@ -import { getBidIdParameter, tryAppendQueryString, createTrackPixelHtml, logError, logWarn } from '../src/utils.js'; +import { getBidIdParameter, tryAppendQueryString, createTrackPixelHtml, logError, logWarn, deepAccess } from '../src/utils.js'; import { Renderer } from '../src/Renderer.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; import { VIDEO, BANNER, NATIVE } from '../src/mediaTypes.js'; -import { convertOrtbRequestToProprietaryNative } from '../src/native.js'; -const BIDDER_CODE = 'aja'; +const BidderCode = 'aja'; const URL = 'https://ad.as.amanad.adtdp.com/v2/prebid'; -const SDK_TYPE = 5; -const AD_TYPE = { - BANNER: 1, - NATIVE: 2, - VIDEO: 3, +const SDKType = 5; +const AdType = { + Banner: 1, + Native: 2, + Video: 3, }; +const BannerSizeMap = { + '970x250': 1, + '300x250': 2, + '320x50': 3, + '728x90': 4, + '320x100': 6, + '336x280': 31, + '300x600': 32, +} + export const spec = { - code: BIDDER_CODE, + code: BidderCode, supportedMediaTypes: [VIDEO, BANNER, NATIVE], /** @@ -36,9 +45,6 @@ export const spec = { * @returns {ServerRequest|ServerRequest[]} */ buildRequests: function(validBidRequests, bidderRequest) { - // convert Native ORTB definition to old-style prebid native definition - validBidRequests = convertOrtbRequestToProprietaryNative(validBidRequests); - const bidRequests = []; const pageUrl = bidderRequest?.refererInfo?.page || undefined; @@ -48,7 +54,7 @@ export const spec = { const asi = getBidIdParameter('asi', bidRequest.params); queryString = tryAppendQueryString(queryString, 'asi', asi); - queryString = tryAppendQueryString(queryString, 'skt', SDK_TYPE); + queryString = tryAppendQueryString(queryString, 'skt', SDKType); queryString = tryAppendQueryString(queryString, 'tid', bidRequest.transactionId) queryString = tryAppendQueryString(queryString, 'prebid_id', bidRequest.bidId); queryString = tryAppendQueryString(queryString, 'prebid_ver', '$prebid.version$'); @@ -57,11 +63,32 @@ export const spec = { queryString = tryAppendQueryString(queryString, 'page_url', pageUrl); } + const banner = deepAccess(bidRequest, `mediaTypes.${BANNER}`) + if (banner) { + const adFormatIDs = []; + for (const size of banner.sizes || []) { + if (size.length !== 2) { + continue + } + + const adFormatID = BannerSizeMap[`${size[0]}x${size[1]}`]; + if (adFormatID) { + adFormatIDs.push(adFormatID); + } + } + queryString = tryAppendQueryString(queryString, 'ad_format_ids', adFormatIDs.join(',')); + } + const eids = bidRequest.userIdAsEids; if (eids && eids.length) { queryString = tryAppendQueryString(queryString, 'eids', JSON.stringify({ 'eids': eids, - })) + })); + } + + const sua = deepAccess(bidRequest, 'ortb2.device.sua'); + if (sua) { + queryString = tryAppendQueryString(queryString, 'sua', JSON.stringify(sua)); } bidRequests.push({ @@ -96,7 +123,7 @@ export const spec = { }, } - if (AD_TYPE.VIDEO === ad.ad_type) { + if (AdType.Video === ad.ad_type) { const videoAd = bidderResponseBody.ad.video; Object.assign(bid, { vastXml: videoAd.vtag, @@ -108,7 +135,7 @@ export const spec = { }); Array.prototype.push.apply(bid.meta.advertiserDomains, videoAd.adomain) - } else if (AD_TYPE.BANNER === ad.ad_type) { + } else if (AdType.Banner === ad.ad_type) { const bannerAd = bidderResponseBody.ad.banner; Object.assign(bid, { width: bannerAd.w, @@ -126,7 +153,7 @@ export const spec = { } Array.prototype.push.apply(bid.meta.advertiserDomains, bannerAd.adomain) - } else if (AD_TYPE.NATIVE === ad.ad_type) { + } else if (AdType.Native === ad.ad_type) { const nativeAds = ad.native.template_and_ads.ads; if (nativeAds.length === 0) { return []; diff --git a/modules/akamaiDapRtdProvider.js b/modules/akamaiDapRtdProvider.js index 1c2af70d737..f0bb7eb3a6c 100644 --- a/modules/akamaiDapRtdProvider.js +++ b/modules/akamaiDapRtdProvider.js @@ -10,6 +10,7 @@ import {getStorageManager} from '../src/storageManager.js'; import {submodule} from '../src/hook.js'; import {isPlainObject, mergeDeep, logMessage, logInfo, logError} from '../src/utils.js'; import { loadExternalScript } from '../src/adloader.js'; +import {MODULE_TYPE_RTD} from '../src/activities/modules.js'; const MODULE_NAME = 'realTimeData'; const SUBMODULE_NAME = 'dap'; @@ -23,7 +24,7 @@ export const DAP_DEFAULT_TOKEN_TTL = 3600; // in seconds export const DAP_MAX_RETRY_TOKENIZE = 1; export const DAP_CLIENT_ENTROPY = 'dap_client_entropy' -export const storage = getStorageManager({gvlid: null, moduleName: SUBMODULE_NAME}); +export const storage = getStorageManager({moduleType: MODULE_TYPE_RTD, moduleName: SUBMODULE_NAME}); let dapRetryTokenize = 0; /** diff --git a/modules/alkimiBidAdapter.js b/modules/alkimiBidAdapter.js index fe5a050f436..69b3fdae3d8 100644 --- a/modules/alkimiBidAdapter.js +++ b/modules/alkimiBidAdapter.js @@ -1,8 +1,8 @@ -import { registerBidder } from '../src/adapters/bidderFactory.js'; -import { deepClone, deepAccess } from '../src/utils.js'; -import { ajax } from '../src/ajax.js'; -import { VIDEO } from '../src/mediaTypes.js'; -import { config } from '../src/config.js'; +import {registerBidder} from '../src/adapters/bidderFactory.js'; +import {deepClone, deepAccess} from '../src/utils.js'; +import {ajax} from '../src/ajax.js'; +import {VIDEO} from '../src/mediaTypes.js'; +import {config} from '../src/config.js'; const BIDDER_CODE = 'alkimi'; export const ENDPOINT = 'https://exchange.alkimi-onboarding.com/bid?prebid=true'; @@ -20,18 +20,19 @@ export const spec = { let bidIds = []; let eids; validBidRequests.forEach(bidRequest => { - let sizes = prepareSizes(bidRequest.sizes) + let formatTypes = getFormatType(bidRequest) if (bidRequest.userIdAsEids) { eids = eids || bidRequest.userIdAsEids } + bids.push({ token: bidRequest.params.token, pos: bidRequest.params.pos, - bidFloor: bidRequest.params.bidFloor, - width: sizes[0].width, - height: sizes[0].height, - impMediaType: getFormatType(bidRequest), + bidFloor: getBidFloor(bidRequest, formatTypes), + sizes: prepareSizes(deepAccess(bidRequest, 'mediaTypes.banner.sizes')), + playerSizes: prepareSizes(deepAccess(bidRequest, 'mediaTypes.video.playerSize')), + impMediaTypes: formatTypes, adUnitCode: bidRequest.adUnitCode }) bidIds.push(bidRequest.bidId) @@ -41,7 +42,7 @@ export const spec = { let payload = { requestId: bidderRequest.auctionId, - signRequest: { bids, randomUUID: alkimiConfig && alkimiConfig.randomUUID }, + signRequest: {bids, randomUUID: alkimiConfig && alkimiConfig.randomUUID}, bidIds, referer: bidderRequest.refererInfo.page, signature: alkimiConfig && alkimiConfig.signature, @@ -85,7 +86,7 @@ export const spec = { return []; } - const { prebidResponse } = serverBody; + const {prebidResponse} = serverBody; if (!prebidResponse || typeof prebidResponse !== 'object') { return []; } @@ -113,7 +114,7 @@ export const spec = { let winUrl; if (bid.winUrl || bid.vastUrl) { winUrl = bid.winUrl ? bid.winUrl : bid.vastUrl; - winUrl = winUrl.replace(/\$\{AUCTION_PRICE\}/, bid.cpm); + winUrl = winUrl.replace(/\$\{AUCTION_PRICE}/, bid.cpm); } else if (bid.ad) { let trackImg = bid.ad.match(/(?!^)/); bid.ad = bid.ad.replace(trackImg[0], ''); @@ -129,13 +130,34 @@ export const spec = { } function prepareSizes(sizes) { - return sizes && sizes.map(size => ({ width: size[0], height: size[1] })); + return sizes ? sizes.map(size => ({width: size[0], height: size[1]})) : [] +} + +function prepareBidFloorSize(sizes) { + return sizes && sizes.length === 1 ? sizes : ['*']; +} + +function getBidFloor(bidRequest, formatTypes) { + let minFloor + if (typeof bidRequest.getFloor === 'function') { + const bidFloorSizes = prepareBidFloorSize(bidRequest.sizes) + formatTypes.forEach(formatType => { + bidFloorSizes.forEach(bidFloorSize => { + const floor = bidRequest.getFloor({currency: 'USD', mediaType: formatType.toLowerCase(), size: bidFloorSize}); + if (floor && !isNaN(floor.floor) && (floor.currency === 'USD')) { + minFloor = !minFloor || floor.floor < minFloor ? floor.floor : minFloor + } + }) + }) + } + return minFloor || bidRequest.params.bidFloor; } const getFormatType = bidRequest => { - if (deepAccess(bidRequest, 'mediaTypes.banner')) return 'Banner' - if (deepAccess(bidRequest, 'mediaTypes.video')) return 'Video' - if (deepAccess(bidRequest, 'mediaTypes.audio')) return 'Audio' + let formats = [] + if (deepAccess(bidRequest, 'mediaTypes.banner')) formats.push('Banner') + if (deepAccess(bidRequest, 'mediaTypes.video')) formats.push('Video') + return formats } registerBidder(spec); diff --git a/modules/amxBidAdapter.js b/modules/amxBidAdapter.js index c7bc99b6aa6..68a3a370c01 100644 --- a/modules/amxBidAdapter.js +++ b/modules/amxBidAdapter.js @@ -1,20 +1,21 @@ -import { registerBidder } from '../src/adapters/bidderFactory.js'; -import { BANNER, VIDEO } from '../src/mediaTypes.js'; +import {registerBidder} from '../src/adapters/bidderFactory.js'; +import {BANNER, VIDEO} from '../src/mediaTypes.js'; import { - parseUrl, - deepAccess, _each, + deepAccess, formatQS, getUniqueIdentifierStr, - triggerPixel, + isArray, isFn, logError, + parseUrl, + triggerPixel, } from '../src/utils.js'; -import { config } from '../src/config.js'; -import { getStorageManager } from '../src/storageManager.js'; +import {config} from '../src/config.js'; +import {getStorageManager} from '../src/storageManager.js'; const BIDDER_CODE = 'amx'; -const storage = getStorageManager({ gvlid: 737, bidderCode: BIDDER_CODE }); +const storage = getStorageManager({ bidderCode: BIDDER_CODE }); const SIMPLE_TLD_TEST = /\.com?\.\w{2,4}$/; const DEFAULT_ENDPOINT = 'https://prebid.a-mo.net/a/c'; const VERSION = 'pba1.3.2'; @@ -192,6 +193,53 @@ function resolveSize(bid, request, bidId) { return [bidRequest.aw, bidRequest.ah]; } +function isSyncEnabled(syncConfigP, syncType) { + if (syncConfigP == null) return false; + + const syncConfig = syncConfigP[syncType]; + if (syncConfig == null) { + return false; + } + + if (syncConfig.bidders === '*' || (isArray(syncConfig.bidders) && syncConfig.bidders.indexOf('amx') !== -1)) { + return syncConfig.filter == null || syncConfig.filter === 'include'; + } + + return false; +} + +const SYNC_IMAGE = 1; +const SYNC_IFRAME = 2; + +function getSyncSettings() { + const syncConfig = config.getConfig('userSync'); + if (syncConfig == null) { + return { + d: 0, + l: 0, + t: 0, + e: true + }; + } + + const settings = { d: syncConfig.syncDelay, l: syncConfig.syncsPerBidder, t: 0, e: syncConfig.syncEnabled } + const all = isSyncEnabled(syncConfig.filterSettings, 'all') + + if (all) { + settings.t = SYNC_IMAGE & SYNC_IFRAME; + return settings; + } + + if (isSyncEnabled(syncConfig.filterSettings, 'iframe')) { + settings.t |= SYNC_IFRAME; + } + if (isSyncEnabled(syncConfig.filterSettings, 'image')) { + settings.t |= SYNC_IMAGE; + } + + return settings; +} + function values(source) { if (Object.values != null) { return Object.values(source); @@ -202,6 +250,30 @@ function values(source) { }); } +function getGpp(bidderRequest) { + if (bidderRequest?.gppConsent != null) { + return bidderRequest.gppConsent; + } + + return bidderRequest?.ortb2?.regs?.gpp ?? {gppString: '', applicableSections: ''}; +} + +function buildReferrerInfo(bidderRequest) { + if (bidderRequest.refererInfo == null) { + return {r: '', t: false, c: '', l: 0, s: []} + } + + const re = bidderRequest.refererInfo; + + return { + r: re.topmostLocation, + t: re.reachedTop, + l: re.numIframes, + s: re.stack, + c: re.canonicalUrl, + } +} + const isTrue = (boolValue) => boolValue === true || boolValue === 1 || boolValue === 'true'; @@ -249,6 +321,7 @@ export const spec = { w: screen.width, gs: deepAccess(bidderRequest, 'gdprConsent.gdprApplies', ''), gc: deepAccess(bidderRequest, 'gdprConsent.consentString', ''), + gpp: getGpp(bidderRequest), u: refInfo(bidderRequest, 'page', loc.href), do: refInfo(bidderRequest, 'site', loc.hostname), re: refInfo(bidderRequest, 'ref'), @@ -259,8 +332,10 @@ export const spec = { m: createBidMap(bidRequests), cpp: config.getConfig('coppa') ? 1 : 0, fpd2: bidderRequest.ortb2, - tmax: config.getConfig('bidderTimeout'), + tmax: bidderRequest.timeout, amp: refInfo(bidderRequest, 'isAmp', null), + ri: buildReferrerInfo(bidderRequest), + sync: getSyncSettings(), eids: values( bidRequests.reduce((all, bid) => { // we only want unique ones in here @@ -287,17 +362,38 @@ export const spec = { }; }, - getUserSyncs(syncOptions, serverResponses) { + getUserSyncs(syncOptions, serverResponses, gdprConsent, uspConsent, gppConsent) { + const qp = { + gdpr_consent: enc(gdprConsent?.consentString || ''), + gdpr: enc(gdprConsent?.gdprApplies ? 1 : 0), + us_privacy: enc(uspConsent || ''), + gpp: enc(gppConsent?.gppString || ''), + gpp_sid: enc(gppConsent?.applicableSections || '') + }; + + const iframeSync = { + url: `https://prebid.a-mo.net/isyn?${formatQS(qp)}`, + type: 'iframe' + }; + if (serverResponses == null || serverResponses.length === 0) { + if (syncOptions.iframeEnabled) { + return [iframeSync] + } + return []; } + const output = []; + let hasFrame = false; + _each(serverResponses, function ({ body: response }) { if (response != null && response.p != null && response.p.hreq) { _each(response.p.hreq, function (syncPixel) { const pixelType = syncPixel.indexOf('__st=iframe') !== -1 ? 'iframe' : 'image'; if (syncOptions.iframeEnabled || pixelType === 'image') { + hasFrame = hasFrame || (pixelType === 'iframe') || (syncPixel.indexOf('cchain') !== -1) output.push({ url: syncPixel, type: pixelType, @@ -306,6 +402,11 @@ export const spec = { }); } }); + + if (!hasFrame && output.length < 2) { + output.push(iframeSync) + } + return output; }, diff --git a/modules/amxIdSystem.js b/modules/amxIdSystem.js index 9dbab496f2c..5deffa00dfe 100644 --- a/modules/amxIdSystem.js +++ b/modules/amxIdSystem.js @@ -10,28 +10,25 @@ import {ajaxBuilder} from '../src/ajax.js'; import {submodule} from '../src/hook.js'; import {getRefererInfo} from '../src/refererDetection.js'; import {deepAccess, logError} from '../src/utils.js'; +import {getStorageManager} from '../src/storageManager.js'; +import {MODULE_TYPE_UID} from '../src/activities/modules.js'; +import {domainOverrideToRootDomain} from '../libraries/domainOverrideToRootDomain/index.js'; const NAME = 'amxId'; const GVL_ID = 737; const ID_KEY = NAME; -const version = '1.0'; +const version = '2.0'; const SYNC_URL = 'https://id.a-mx.com/sync/'; const AJAX_TIMEOUT = 300; +const AJAX_OPTIONS = {method: 'GET', withCredentials: true, contentType: 'text/plain'}; -function validateConfig(config) { - if (config == null || config.storage == null) { - logError(`${NAME}: config.storage is required.`); - return false; - } - - if (config.storage.type !== 'html5') { - logError( - `${NAME} only supports storage.type "html5". ${config.storage.type} was provided` - ); - return false; - } +export const storage = getStorageManager({moduleName: NAME, moduleType: MODULE_TYPE_UID}); +const AMUID_KEY = '__amuidpb'; +const getBidAdapterID = () => storage.localStorageIsEnabled() ? storage.getDataFromLocalStorage(AMUID_KEY) : null; +function validateConfig(config) { if ( + config.storage != null && typeof config.storage.expires === 'number' && config.storage.expires > 30 ) { @@ -44,7 +41,7 @@ function validateConfig(config) { return true; } -function handleSyncResponse(client, response, callback) { +function handleSyncResponse(client, response, params, callback) { if (response.id != null && response.id.length > 0) { callback(response.id); return; @@ -72,7 +69,7 @@ function handleSyncResponse(client, response, callback) { logError(`${NAME} invalid value`, complete); callback(null); }, - }); + }, params, AJAX_OPTIONS); } export const amxIdSubmodule = { @@ -97,6 +94,8 @@ export const amxIdSubmodule = { ? { [ID_KEY]: value } : undefined, + domainOverride: domainOverrideToRootDomain(storage, NAME), + getId(config, consentData, _extant) { if (!validateConfig(config)) { return undefined; @@ -109,12 +108,18 @@ export const amxIdSubmodule = { const params = { tagId: deepAccess(config, 'params.tagId', ''), - // TODO: are these referer values correct? + ref: ref.ref, u: ref.location, + tl: ref.topmostLocation, + nf: ref.numIframes, + rt: ref.reachedTop, + v: '$prebid.version$', + av: version, vg: '$$PREBID_GLOBAL$$', us_privacy: usp, + am: getBidAdapterID(), gdpr: consent.gdprApplies ? 1 : 0, gdpr_consent: consent.consentString, }; @@ -131,7 +136,7 @@ export const amxIdSubmodule = { if (responseText != null && responseText.length > 0) { try { const parsed = JSON.parse(responseText); - handleSyncResponse(client, parsed, done); + handleSyncResponse(client, parsed, params, done); return; } catch (e) { logError(`${NAME} invalid response`, responseText); @@ -142,9 +147,7 @@ export const amxIdSubmodule = { }, }, params, - { - method: 'GET' - } + AJAX_OPTIONS ); return { callback }; diff --git a/modules/amxIdSystem.md b/modules/amxIdSystem.md index 9de93c761a1..5d2b0c48478 100644 --- a/modules/amxIdSystem.md +++ b/modules/amxIdSystem.md @@ -1,6 +1,6 @@ -# AMX RTB ID +# AMX ID -For help adding this module, please contact [prebid@amxrtb.com](prebid@amxrtb.com). +For help adding this module, please contact [info@amxdt.net](info@amxdt.net). ### Prebid Configuration @@ -29,23 +29,15 @@ pbjs.setConfig({ | Param under `userSync.userIds[]` | Scope | Type | Description | Example | | -------------------------------- | -------- | ------ | --------------------------- | ----------------------------------------- | | name | Required | string | ID for the amxId module | `"amxId"` | -| storage | Required | Object | Settings for amxId storage | See [storage settings](#storage-settings) | +| storage | Optional | Object | Settings for amxId storage | See [storage settings](#storage-settings) | | params | Optional | Object | Parameters for amxId module | See [params](#params) | ### Storage Settings -The following settings are available for the `storage` property in the `userSync.userIds[]` object: +The following settings are suggested for the `storage` property in the `userSync.userIds[]` object: -| Param under `storage` | Scope | Type | Description | Example | -| --------------------- | -------- | ------------ | -------------------------------------------------------------------------------- | --------- | -| name | Required | String | Where the ID will be stored | `"amxId"` | -| type | Required | String | This must be `"html5"` | `"html5"` | -| expires | Required | Number <= 30 | number of days until the stored ID expires. **Must be less than or equal to 30** | `14` | - -### Params - -The following options are available in the `params` property in `userSync.userIds[]`: - -| Param under `params` | Scope | Type | Description | Example | -| -------------------- | -------- | ------ | ------------------------------------------------------------------------- | ---------------- | -| tagId | Optional | String | Your AMX tagId (optional) | `cHJlYmlkLm9yZw` | +| Param under `storage` | Type | Description | Example | +| --------------------- | ------------ | -------------------------------------------------------------------------------- | --------- | +| name | String | Where the ID will be stored | `"amxId"` | +| type | String | For best performance, this should be `"html5"` | `"html5"` | +| expires | Number <= 30 | number of days until the stored ID expires. **Must be less than or equal to 30** | `14` | diff --git a/modules/aolBidAdapter.js b/modules/aolBidAdapter.js index d38c373ff1f..d15a5434e0c 100644 --- a/modules/aolBidAdapter.js +++ b/modules/aolBidAdapter.js @@ -35,7 +35,7 @@ const SUPPORTED_USER_ID_SOURCES = [ 'adserver.org', 'adtelligent.com', 'akamai.com', - 'amxrtb.com', + 'amxdt.net', 'audigent.com', 'britepool.com', 'criteo.com', @@ -159,6 +159,13 @@ export const spec = { if (bidderRequest) { consentData.gdpr = bidderRequest.gdprConsent; consentData.uspConsent = bidderRequest.uspConsent; + consentData.gppConsent = bidderRequest.gppConsent; + if (!consentData.gppConsent && bidderRequest.ortb2?.regs?.gpp) { + consentData.gppConsent = { + gppString: bidderRequest.ortb2.regs.gpp, + applicableSections: bidderRequest.ortb2.regs.gpp_sid + } + } } return bids.map(bid => { @@ -373,6 +380,11 @@ export const spec = { params.us_privacy = consentData.uspConsent; } + if (consentData.gppConsent && consentData.gppConsent.gppString) { + params.gpp = consentData.gppConsent.gppString; + params.gpp_sid = consentData.gppConsent.applicableSections; + } + return params; }, parsePixelItems(pixels) { diff --git a/modules/appnexusBidAdapter.js b/modules/appnexusBidAdapter.js index 77ffe0f6b94..8f499e1e31e 100644 --- a/modules/appnexusBidAdapter.js +++ b/modules/appnexusBidAdapter.js @@ -44,7 +44,7 @@ const URL_SIMPLE = 'https://ib.adnxs-simple.com/ut/v3/prebid'; const VIDEO_TARGETING = ['id', 'minduration', 'maxduration', 'skippable', 'playback_method', 'frameworks', 'context', 'skipoffset']; const VIDEO_RTB_TARGETING = ['minduration', 'maxduration', 'skip', 'skipafter', 'playbackmethod', 'api', 'startdelay']; -const USER_PARAMS = ['age', 'externalUid', 'segments', 'gender', 'dnt', 'language']; +const USER_PARAMS = ['age', 'externalUid', 'external_uid', 'segments', 'gender', 'dnt', 'language']; const APP_DEVICE_PARAMS = ['geo', 'device_id']; // appid is collected separately const DEBUG_PARAMS = ['enabled', 'dongle', 'member_id', 'debug_timeout']; const DEBUG_QUERY_PARAM_MAP = { @@ -94,7 +94,7 @@ const SCRIPT_TAG_START = ' { - ppId.uids.forEach(uid => { - eids.push({ source: ppId.source, id: uid.id }); - }); + bidRequests[0].userIdAsEids.forEach(eid => { + if (!eid || !eid.uids || eid.uids.length < 1) { return; } + eid.uids.forEach(uid => { + let tmp = {'source': eid.source, 'id': uid.id}; + if (eid.source == 'adserver.org') { + tmp.rti_partner = 'TDID'; + } else if (eid.source == 'uidapi.com') { + tmp.rti_partner = 'UID2'; + } + eids.push(tmp); }); - } - + }); if (eids.length) { payload.eids = eids; } @@ -424,8 +436,17 @@ export const spec = { } }, - getUserSyncs: function (syncOptions, responses, gdprConsent) { - if (syncOptions.iframeEnabled && hasPurpose1Consent(gdprConsent)) { + getUserSyncs: function (syncOptions, responses, gdprConsent, uspConsent, gppConsent) { + function checkGppStatus(gppConsent) { + // this is a temporary measure to supress usersync in US-based GPP regions + // this logic will be revised when proper signals (akin to purpose1 from TCF2) can be determined for US GPP + if (gppConsent && Array.isArray(gppConsent.applicableSections)) { + return gppConsent.applicableSections.every(sec => typeof sec === 'number' && sec <= 5); + } + return true; + } + + if (syncOptions.iframeEnabled && hasPurpose1Consent(gdprConsent) && checkGppStatus(gppConsent)) { return [{ type: 'iframe', url: 'https://acdn.adnxs.com/dmp/async_usersync.html' @@ -463,9 +484,6 @@ export const spec = { }, params); if (isOpenRtb) { - params.use_pmt_rule = (typeof params.usePaymentRule === 'boolean') ? params.usePaymentRule : false; - if (params.usePaymentRule) { delete params.usePaymentRule; } - if (isPopulatedArray(params.keywords)) { params.keywords.forEach(deleteValues); } @@ -477,6 +495,9 @@ export const spec = { delete params[paramKey]; } }); + + params.use_pmt_rule = (typeof params.use_payment_rule === 'boolean') ? params.use_payment_rule : false; + if (params.use_payment_rule) { delete params.use_payment_rule; } } return params; @@ -604,9 +625,8 @@ function newBid(serverBid, rtbBid, bidderRequest) { } }; - // WE DON'T FULLY SUPPORT THIS ATM - future spot for adomain code; creating a stub for 5.0 compliance if (rtbBid.adomain) { - bid.meta = Object.assign({}, bid.meta, { advertiserDomains: [] }); + bid.meta = Object.assign({}, bid.meta, { advertiserDomains: [rtbBid.adomain] }); } if (rtbBid.advertiser_id) { @@ -710,6 +730,7 @@ function newBid(serverBid, rtbBid, bidderRequest) { displayUrl: nativeAd.displayurl, clickTrackers: nativeAd.link.click_trackers, impressionTrackers: nativeAd.impression_trackers, + video: nativeAd.video, javascriptTrackers: jsTrackers }; if (nativeAd.main_img) { @@ -750,17 +771,25 @@ function newBid(serverBid, rtbBid, bidderRequest) { function bidToTag(bid) { const tag = {}; + Object.keys(bid.params).forEach(paramKey => { + let convertedKey = convertCamelToUnderscore(paramKey); + if (convertedKey !== paramKey) { + bid.params[convertedKey] = bid.params[paramKey]; + delete bid.params[paramKey]; + } + }); tag.sizes = transformSizes(bid.sizes); tag.primary_size = tag.sizes[0]; tag.ad_types = []; tag.uuid = bid.bidId; - if (bid.params.placementId) { - tag.id = parseInt(bid.params.placementId, 10); + if (bid.params.placement_id) { + tag.id = parseInt(bid.params.placement_id, 10); } else { - tag.code = bid.params.invCode; + tag.code = bid.params.inv_code; } - tag.allow_smaller_sizes = bid.params.allowSmallerSizes || false; - tag.use_pmt_rule = bid.params.usePaymentRule || false; + tag.allow_smaller_sizes = bid.params.allow_smaller_sizes || false; + tag.use_pmt_rule = (typeof bid.params.use_payment_rule === 'boolean') ? bid.params.use_payment_rule + : (typeof bid.params.use_pmt_rule === 'boolean') ? bid.params.use_pmt_rule : false; tag.prebid = true; tag.disable_psa = true; let bidFloor = getBidFloor(bid); @@ -777,26 +806,26 @@ function bidToTag(bid) { tag.position = (mediaTypePos === 3) ? 2 : mediaTypePos; } } - if (bid.params.trafficSourceCode) { - tag.traffic_source_code = bid.params.trafficSourceCode; + if (bid.params.traffic_source_code) { + tag.traffic_source_code = bid.params.traffic_source_code; } - if (bid.params.privateSizes) { - tag.private_sizes = transformSizes(bid.params.privateSizes); + if (bid.params.private_sizes) { + tag.private_sizes = transformSizes(bid.params.private_sizes); } - if (bid.params.supplyType) { - tag.supply_type = bid.params.supplyType; + if (bid.params.supply_type) { + tag.supply_type = bid.params.supply_type; } - if (bid.params.pubClick) { - tag.pubclick = bid.params.pubClick; + if (bid.params.pub_click) { + tag.pubclick = bid.params.pub_click; } - if (bid.params.extInvCode) { - tag.ext_inv_code = bid.params.extInvCode; + if (bid.params.ext_inv_code) { + tag.ext_inv_code = bid.params.ext_inv_code; } - if (bid.params.publisherId) { - tag.publisher_id = parseInt(bid.params.publisherId, 10); + if (bid.params.publisher_id) { + tag.publisher_id = parseInt(bid.params.publisher_id, 10); } - if (bid.params.externalImpId) { - tag.external_imp_id = bid.params.externalImpId; + if (bid.params.external_imp_id) { + tag.external_imp_id = bid.params.external_imp_id; } let ortb2ImpKwStr = deepAccess(bid, 'ortb2Imp.ext.data.keywords'); @@ -1197,17 +1226,6 @@ function parseMediaType(rtbBid) { } } -function addUserId(eids, id, source, rti) { - if (id) { - if (rti) { - eids.push({ source, id, rti_partner: rti }); - } else { - eids.push({ source, id }); - } - } - return eids; -} - function getBidFloor(bid) { if (!isFn(bid.getFloor)) { return (bid.params.reserve) ? bid.params.reserve : null; diff --git a/modules/appnexusBidAdapter.md b/modules/appnexusBidAdapter.md index 7ac70e67584..e8c57b782ec 100644 --- a/modules/appnexusBidAdapter.md +++ b/modules/appnexusBidAdapter.md @@ -3,7 +3,7 @@ ``` Module Name: Appnexus Bid Adapter Module Type: Bidder Adapter -Maintainer: prebid-js@xandr.com +Maintainer: prebid@microsoft.com ``` # Description diff --git a/modules/appushBidAdapter.js b/modules/appushBidAdapter.js new file mode 100644 index 00000000000..97772b65e45 --- /dev/null +++ b/modules/appushBidAdapter.js @@ -0,0 +1,188 @@ +import { isFn, deepAccess, logMessage, logError } from '../src/utils.js'; +import { convertOrtbRequestToProprietaryNative } from '../src/native.js'; + +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { BANNER, NATIVE, VIDEO } from '../src/mediaTypes.js'; +import { config } from '../src/config.js'; + +const BIDDER_CODE = 'appush'; +const AD_URL = 'https://hb.appush.com/pbjs'; + +function isBidResponseValid(bid) { + if (!bid.requestId || !bid.cpm || !bid.creativeId || !bid.ttl || !bid.currency) { + return false; + } + + switch (bid.mediaType) { + case BANNER: + return Boolean(bid.width && bid.height && bid.ad); + case VIDEO: + return Boolean(bid.vastUrl || bid.vastXml); + case NATIVE: + return Boolean(bid.native && bid.native.impressionTrackers && bid.native.impressionTrackers.length); + default: + return false; + } +} + +function getPlacementReqData(bid) { + const { params, bidId, mediaTypes } = bid; + const schain = bid.schain || {}; + const { placementId, endpointId } = params; + const bidfloor = getBidFloor(bid); + + const placement = { + bidId, + schain, + bidfloor + }; + + if (placementId) { + placement.placementId = placementId; + placement.type = 'publisher'; + } else if (endpointId) { + placement.endpointId = endpointId; + placement.type = 'network'; + } + + if (mediaTypes && mediaTypes[BANNER]) { + placement.adFormat = BANNER; + placement.sizes = mediaTypes[BANNER].sizes; + } else if (mediaTypes && mediaTypes[VIDEO]) { + placement.adFormat = VIDEO; + placement.playerSize = mediaTypes[VIDEO].playerSize; + placement.minduration = mediaTypes[VIDEO].minduration; + placement.maxduration = mediaTypes[VIDEO].maxduration; + placement.mimes = mediaTypes[VIDEO].mimes; + placement.protocols = mediaTypes[VIDEO].protocols; + placement.startdelay = mediaTypes[VIDEO].startdelay; + placement.placement = mediaTypes[VIDEO].placement; + placement.skip = mediaTypes[VIDEO].skip; + placement.skipafter = mediaTypes[VIDEO].skipafter; + placement.minbitrate = mediaTypes[VIDEO].minbitrate; + placement.maxbitrate = mediaTypes[VIDEO].maxbitrate; + placement.delivery = mediaTypes[VIDEO].delivery; + placement.playbackmethod = mediaTypes[VIDEO].playbackmethod; + placement.api = mediaTypes[VIDEO].api; + placement.linearity = mediaTypes[VIDEO].linearity; + } else if (mediaTypes && mediaTypes[NATIVE]) { + placement.native = mediaTypes[NATIVE]; + placement.adFormat = NATIVE; + } + + return placement; +} + +function getBidFloor(bid) { + if (!isFn(bid.getFloor)) { + return deepAccess(bid, 'params.bidfloor', 0); + } + + try { + const bidFloor = bid.getFloor({ + currency: 'USD', + mediaType: '*', + size: '*', + }); + return bidFloor.floor; + } catch (err) { + logError(err); + return 0; + } +} + +export const spec = { + code: BIDDER_CODE, + supportedMediaTypes: [BANNER, VIDEO, NATIVE], + + isBidRequestValid: (bid = {}) => { + const { params, bidId, mediaTypes } = bid; + let valid = Boolean(bidId && params && (params.placementId || params.endpointId)); + + if (mediaTypes && mediaTypes[BANNER]) { + valid = valid && Boolean(mediaTypes[BANNER] && mediaTypes[BANNER].sizes); + } else if (mediaTypes && mediaTypes[VIDEO]) { + valid = valid && Boolean(mediaTypes[VIDEO] && mediaTypes[VIDEO].playerSize); + } else if (mediaTypes && mediaTypes[NATIVE]) { + valid = valid && Boolean(mediaTypes[NATIVE]); + } else { + valid = false; + } + return valid; + }, + + buildRequests: (validBidRequests = [], bidderRequest = {}) => { + // convert Native ORTB definition to old-style prebid native definition + validBidRequests = convertOrtbRequestToProprietaryNative(validBidRequests); + + let deviceWidth = 0; + let deviceHeight = 0; + + let winLocation; + try { + const winTop = window.top; + deviceWidth = winTop.screen.width; + deviceHeight = winTop.screen.height; + winLocation = winTop.location; + } catch (e) { + logMessage(e); + winLocation = window.location; + } + + const refferUrl = bidderRequest.refererInfo && bidderRequest.refererInfo.page; + let refferLocation; + try { + refferLocation = refferUrl && new URL(refferUrl); + } catch (e) { + logMessage(e); + } + // TODO: does the fallback make sense here? + let location = refferLocation || winLocation; + const language = (navigator && navigator.language) ? navigator.language.split('-')[0] : ''; + const host = location.host; + const page = location.pathname; + const secure = location.protocol === 'https:' ? 1 : 0; + const placements = []; + const request = { + deviceWidth, + deviceHeight, + language, + secure, + host, + page, + placements, + coppa: config.getConfig('coppa') === true ? 1 : 0, + ccpa: bidderRequest.uspConsent || undefined, + gdpr: bidderRequest.gdprConsent || undefined, + tmax: bidderRequest.timeout + }; + + const len = validBidRequests.length; + for (let i = 0; i < len; i++) { + const bid = validBidRequests[i]; + placements.push(getPlacementReqData(bid)); + } + + return { + method: 'POST', + url: AD_URL, + data: request + }; + }, + + interpretResponse: (serverResponse) => { + let response = []; + for (let i = 0; i < serverResponse.body.length; i++) { + let resItem = serverResponse.body[i]; + if (isBidResponseValid(resItem)) { + const advertiserDomains = resItem.adomain && resItem.adomain.length ? resItem.adomain : []; + resItem.meta = { ...resItem.meta, advertiserDomains }; + + response.push(resItem); + } + } + return response; + } +}; + +registerBidder(spec); diff --git a/modules/appushBidAdapter.md b/modules/appushBidAdapter.md new file mode 100644 index 00000000000..7c04c3a6425 --- /dev/null +++ b/modules/appushBidAdapter.md @@ -0,0 +1,79 @@ +# Overview + +``` +Module Name: Appush Bidder Adapter +Module Type: Appush Bidder Adapter +Maintainer: support@appush.com +``` + +# Description + +Connects to Appush exchange for bids. +Appush bid adapter supports Banner, Video (instream and outstream) and Native. + +# Test Parameters +``` + var adUnits = [ + // Will return static test banner + { + code: 'adunit1', + mediaTypes: { + banner: { + sizes: [ [300, 250], [320, 50] ], + } + }, + bids: [ + { + bidder: 'appush', + params: { + placementId: 'testBanner', + } + } + ] + }, + { + code: 'addunit2', + mediaTypes: { + video: { + playerSize: [ [640, 480] ], + context: 'instream', + minduration: 5, + maxduration: 60, + } + }, + bids: [ + { + bidder: 'appush', + params: { + placementId: 'testVideo', + } + } + ] + }, + { + code: 'addunit3', + mediaTypes: { + native: { + title: { + required: true + }, + body: { + required: true + }, + icon: { + required: true, + size: [64, 64] + } + } + }, + bids: [ + { + bidder: 'appush', + params: { + placementId: 'testNative', + } + } + ] + } + ]; +``` \ No newline at end of file diff --git a/modules/apstreamBidAdapter.js b/modules/apstreamBidAdapter.js index 30abb17bfde..871b04442da 100644 --- a/modules/apstreamBidAdapter.js +++ b/modules/apstreamBidAdapter.js @@ -9,7 +9,7 @@ const CONSTANTS = { BIDDER_CODE: 'apstream', GVLID: 394 }; -const storage = getStorageManager({gvlid: CONSTANTS.GVLID, bidderCode: CONSTANTS.BIDDER_CODE}); +const storage = getStorageManager({bidderCode: CONSTANTS.BIDDER_CODE}); var dsuModule = (function() { 'use strict'; diff --git a/modules/arcspanRtdProvider.js b/modules/arcspanRtdProvider.js new file mode 100644 index 00000000000..a7ffa059279 --- /dev/null +++ b/modules/arcspanRtdProvider.js @@ -0,0 +1,73 @@ +import { submodule } from '../src/hook.js'; +import { mergeDeep } from '../src/utils.js'; +import {loadExternalScript} from '../src/adloader.js'; + +/** @type {string} */ +const MODULE_NAME = 'realTimeData'; +const SUBMODULE_NAME = 'arcspan'; + +/** @type {RtdSubmodule} */ +export const arcspanSubmodule = { + name: SUBMODULE_NAME, + init: init, + getBidRequestData: alterBidRequests, +}; + +function init(config, userConsent) { + if (typeof config.params.silo === 'undefined') { + return false; + } + if (typeof window.arcobj2 === 'undefined') { + var scriptUrl; + if (config.params.silo === 'test') { + scriptUrl = 'https://localhost:8080/as.js'; + } else { + scriptUrl = 'https://silo' + config.params.silo + '.p7cloud.net/as.js'; + } + loadExternalScript(scriptUrl, SUBMODULE_NAME); + } + return true; +} + +function alterBidRequests(reqBidsConfigObj, callback, config, userConsent) { + var _v1 = []; + var _v1s = []; + var _v2 = []; + var arcobj1 = window.arcobj1; + if (typeof arcobj1 != 'undefined') { + if (typeof arcobj1.page_iab_codes.text != 'undefined') { _v1 = _v1.concat(arcobj1.page_iab_codes.text); } + if (typeof arcobj1.page_iab_codes.images != 'undefined') { _v1 = _v1.concat(arcobj1.page_iab_codes.images); } + if (typeof arcobj1.page_iab.text != 'undefined') { _v1s = _v1s.concat(arcobj1.page_iab.text); } + if (typeof arcobj1.page_iab.images != 'undefined') { _v1s = _v1s.concat(arcobj1.page_iab.images); } + if (typeof arcobj1.page_iab_newcodes.text != 'undefined') { _v2 = [...new Set([..._v2, ...arcobj1.page_iab_newcodes.text])]; } + if (typeof arcobj1.page_iab_newcodes.images != 'undefined') { _v2 = [...new Set([..._v2, ...arcobj1.page_iab_newcodes.images])]; } + + var _content = {}; + _content.data = []; + var p = {}; + p.name = 'arcspan'; + p.segment = []; + p.ext = { segtax: 6 }; + _v2.forEach(function (e) { + p.segment = p.segment.concat({ id: e }); + }); + _content.data = _content.data.concat(p); + var _ortb2 = { + site: { + name: 'arcspan', + domain: new URL(location.href).hostname, + cat: _v1, + sectioncat: _v1, + pagecat: _v1, + page: location.href, + ref: document.referrer, + keywords: _v1s.toString(), + content: _content, + }, + }; + mergeDeep(reqBidsConfigObj.ortb2Fragments.global, _ortb2); + } + callback(); +} + +submodule(MODULE_NAME, arcspanSubmodule); diff --git a/modules/arcspanRtdProvider.md b/modules/arcspanRtdProvider.md new file mode 100644 index 00000000000..4aa1de02acf --- /dev/null +++ b/modules/arcspanRtdProvider.md @@ -0,0 +1,11 @@ +# Overview + +Module Name: ArcSpan Rtd Provider + +Module Type: Rtd Provider + +Maintainer: engineering@arcspan.com + +# Description + +RTD provider for ArcSpan Technologies. Contact jcabalugaz@arcspan.com for more information. diff --git a/modules/asoBidAdapter.js b/modules/asoBidAdapter.js index 83b182e4ca5..39c42f193b2 100644 --- a/modules/asoBidAdapter.js +++ b/modules/asoBidAdapter.js @@ -10,11 +10,11 @@ import { parseSizesInput, tryAppendQueryString } from '../src/utils.js'; -import {registerBidder} from '../src/adapters/bidderFactory.js'; -import {config} from '../src/config.js'; -import {BANNER, VIDEO} from '../src/mediaTypes.js'; -import {Renderer} from '../src/Renderer.js'; -import {parseDomain} from '../src/refererDetection.js'; +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { config } from '../src/config.js'; +import { BANNER, VIDEO } from '../src/mediaTypes.js'; +import { Renderer } from '../src/Renderer.js'; +import { parseDomain } from '../src/refererDetection.js'; const BIDDER_CODE = 'aso'; const DEFAULT_SERVER_URL = 'https://srv.aso1.net'; @@ -27,6 +27,9 @@ export const spec = { code: BIDDER_CODE, supportedMediaTypes: [BANNER, VIDEO], + aliases: [ + {code: 'bcmint'} + ], isBidRequestValid: bid => { return !!bid.params && !!bid.params.zone; diff --git a/modules/atsAnalyticsAdapter.js b/modules/atsAnalyticsAdapter.js index 0c0227ea34a..8e92146694f 100644 --- a/modules/atsAnalyticsAdapter.js +++ b/modules/atsAnalyticsAdapter.js @@ -4,8 +4,11 @@ import CONSTANTS from '../src/constants.json'; import adaptermanager from '../src/adapterManager.js'; import {ajax} from '../src/ajax.js'; import {getStorageManager} from '../src/storageManager.js'; +import {getGlobal} from '../src/prebidGlobal.js'; -export const storage = getStorageManager(); +import {MODULE_TYPE_ANALYTICS} from '../src/activities/modules.js'; +const MODULE_CODE = 'atsAnalytics'; +export const storage = getStorageManager({moduleType: MODULE_TYPE_ANALYTICS, moduleName: MODULE_CODE}); /** * Analytics adapter for - https://liveramp.com @@ -352,7 +355,7 @@ atsAnalyticsAdapter.callHandler = function (evtype, args) { let bidWonTimeout = atsAnalyticsAdapter.context.bidWonTimeout ? atsAnalyticsAdapter.context.bidWonTimeout : 2000; let events = []; setTimeout(() => { - let winningBids = $$PREBID_GLOBAL$$.getAllWinningBids(); + let winningBids = getGlobal().getAllWinningBids(); logInfo('ATS Analytics - winning bids: ', winningBids) // prepare format data for sending to analytics endpoint if (handlerRequest.length) { @@ -398,7 +401,7 @@ atsAnalyticsAdapter.callHandler = function (evtype, args) { adaptermanager.registerAnalyticsAdapter({ adapter: atsAnalyticsAdapter, - code: 'atsAnalytics', + code: MODULE_CODE, gvlid: 97 }); diff --git a/modules/audiencerunBidAdapter.js b/modules/audiencerunBidAdapter.js index 754a48ede75..efd88aa6f58 100644 --- a/modules/audiencerunBidAdapter.js +++ b/modules/audiencerunBidAdapter.js @@ -1,18 +1,17 @@ import { + _each, deepAccess, - isFn, - logError, - getValue, + formatQS, getBidIdParameter, - _each, + getValue, isArray, + isFn, + logError, triggerPixel, - formatQS, } from '../src/utils.js'; -import { config } from '../src/config.js'; -import { registerBidder } from '../src/adapters/bidderFactory.js'; -import { BANNER } from '../src/mediaTypes.js'; -import { createEidsArray } from './userId/eids.js'; +import {config} from '../src/config.js'; +import {registerBidder} from '../src/adapters/bidderFactory.js'; +import {BANNER} from '../src/mediaTypes.js'; const BIDDER_CODE = 'audiencerun'; const BASE_URL = 'https://d.audiencerun.com'; @@ -135,7 +134,7 @@ export const spec = { payload.uspConsent = deepAccess(bidderRequest, 'uspConsent'); payload.schain = deepAccess(bidRequests, '0.schain'); - payload.userId = deepAccess(bidRequests, '0.userId') ? createEidsArray(bidRequests[0].userId) : []; + payload.userId = deepAccess(bidRequests, '0.userIdAsEids') || [] if (bidderRequest && bidderRequest.gdprConsent) { payload.gdpr = { diff --git a/modules/bedigitechBidAdapter.js b/modules/bedigitechBidAdapter.js new file mode 100644 index 00000000000..6bfe95e85d0 --- /dev/null +++ b/modules/bedigitechBidAdapter.js @@ -0,0 +1,67 @@ +import {BANNER, NATIVE} from '../src/mediaTypes.js'; +import {registerBidder} from '../src/adapters/bidderFactory.js'; +import { _each, isArray } from '../src/utils.js'; + +const BEDIGITECH_CODE = 'bedigitech'; +const BEDIGITECH_ENDPOINT = 'https://bedigitalhb.s3.amazonaws.com/hb.js'; +const BEDIGITECH_REQUEST_METHOD = 'GET'; +const BEDIGITECH_CURRENCY = 'USD'; + +function interpretResponse(placementResponse, bidRequest, bids) { + const bid = { + id: placementResponse.id, + requestId: placementResponse.requestId, + cpm: placementResponse.cpm, + ad: decodeURIComponent(placementResponse.ad), + width: placementResponse.width || 0, + height: placementResponse.height || 0, + currency: placementResponse.currency || BEDIGITECH_CURRENCY, + ttl: placementResponse.ttl || 300, + creativeId: placementResponse.creativeId || 0, + meta: { + mediaType: BANNER, + }, + }; + bids.push(bid); +} + +export const spec = { + code: BEDIGITECH_CODE, + supportedMediaTypes: [BANNER, NATIVE], + isBidRequestValid: bid => { + return !!bid.params.placementId && !!bid.bidId && bid.bidder === 'bedigitech' + }, + + buildRequests: (bidRequests, bidderRequest) => { + return bidRequests.map(bid => { + let url = BEDIGITECH_ENDPOINT; + const data = {'pid': bid.params.placementId}; + + return { + method: BEDIGITECH_REQUEST_METHOD, + url, + data, + options: { + contentType: 'application/json', + withCredentials: false, + crossOrigin: true, + }, + }; + }); + }, + + interpretResponse: function(serverResponse, bidRequest) { + let bids = []; + + if (isArray(serverResponse.body)) { + _each(serverResponse.body, function(placementResponse) { + interpretResponse(placementResponse, bidRequest, bids); + }); + } + + return bids; + }, + +}; + +registerBidder(spec); diff --git a/modules/bedigitechBidAdapter.md b/modules/bedigitechBidAdapter.md new file mode 100644 index 00000000000..35cc0751187 --- /dev/null +++ b/modules/bedigitechBidAdapter.md @@ -0,0 +1,32 @@ +# Overview + +Module Name: Bedigitech Bidder Adapter + +Module Type: Bidder Adapter + +Maintainer: yogesh@thebeglobal.com + +# Description + +You can use this adapter to get a bid from bedigitech.com. + +About us : https://www.bedigitech.com/ + + +# Test Parameters +```javascript + var adUnits = [ + { + code: 'div-bedigitech-example', + sizes: [[300, 250]], + bids: [ + { + bidder: "bedigitech", + params: { + placementId: "1234" + } + } + ] + } + ]; +``` diff --git a/modules/beyondmediaBidAdapter.js b/modules/beyondmediaBidAdapter.js index 57c141dc2aa..bbcd972470c 100644 --- a/modules/beyondmediaBidAdapter.js +++ b/modules/beyondmediaBidAdapter.js @@ -145,7 +145,7 @@ export const spec = { coppa: config.getConfig('coppa') === true ? 1 : 0, ccpa: bidderRequest.uspConsent || undefined, gdpr: bidderRequest.gdprConsent || undefined, - tmax: config.getConfig('bidderTimeout') + tmax: bidderRequest.timeout }; const len = validBidRequests.length; diff --git a/modules/bidViewability.js b/modules/bidViewability.js index 362401e6d1c..a5cab99b1a7 100644 --- a/modules/bidViewability.js +++ b/modules/bidViewability.js @@ -7,7 +7,7 @@ import * as events from '../src/events.js'; import CONSTANTS from '../src/constants.json'; import {isFn, logWarn, triggerPixel} from '../src/utils.js'; import {getGlobal} from '../src/prebidGlobal.js'; -import adapterManager, {gdprDataHandler, uspDataHandler} from '../src/adapterManager.js'; +import adapterManager, {gdprDataHandler, uspDataHandler, gppDataHandler} from '../src/adapterManager.js'; import {find} from '../src/polyfill.js'; const MODULE_NAME = 'bidViewability'; @@ -44,6 +44,11 @@ export let fireViewabilityPixels = (globalModuleConfig, bid) => { const uspConsent = uspDataHandler.getConsentData(); if (uspConsent) { queryParams.us_privacy = uspConsent; } + const gppConsent = gppDataHandler.getConsentData(); + if (gppConsent) { + // TODO - need to know what to set here for queryParams... + } + bid[BID_VURL_ARRAY].forEach(url => { // add '?' if not present in URL if (Object.keys(queryParams).length > 0 && url.indexOf('?') === -1) { diff --git a/modules/bliinkBidAdapter.js b/modules/bliinkBidAdapter.js index 127b534c989..ee814807331 100644 --- a/modules/bliinkBidAdapter.js +++ b/modules/bliinkBidAdapter.js @@ -125,7 +125,7 @@ export const buildBid = (bidResponse) => { requestId: deepAccess(bidResponse, 'extras.transaction_id'), width: deepAccess(bidResponse, `creative.${bid.mediaType}.width`) || 1, height: deepAccess(bidResponse, `creative.${bid.mediaType}.height`) || 1, - ttl: 3600, + ttl: 300, netRevenue: true, }); }; diff --git a/modules/bluebillywigBidAdapter.js b/modules/bluebillywigBidAdapter.js index 27e310177f6..9022ca120af 100644 --- a/modules/bluebillywigBidAdapter.js +++ b/modules/bluebillywigBidAdapter.js @@ -4,7 +4,6 @@ import {registerBidder} from '../src/adapters/bidderFactory.js'; import {VIDEO} from '../src/mediaTypes.js'; import {config} from '../src/config.js'; import {Renderer} from '../src/Renderer.js'; -import {createEidsArray} from './userId/eids.js'; const DEV_MODE = window.location.search.match(/bbpbs_debug=true/); @@ -52,10 +51,9 @@ const BB_HELPERS = { else if (Array.isArray(adServerCur) && adServerCur.length) request.cur = [adServerCur[0]]; }, addUserIds: function(request, validBidRequests) { - const bidUserId = deepAccess(validBidRequests, '0.userId'); - const eids = createEidsArray(bidUserId); + const eids = deepAccess(validBidRequests, '0.userIdAsEids'); - if (eids.length) { + if (eids != null && eids.length) { deepSetValue(request, 'user.ext.eids', eids); } }, diff --git a/modules/blueconicRtdProvider.js b/modules/blueconicRtdProvider.js index 9a7f5984637..b6eb9374671 100644 --- a/modules/blueconicRtdProvider.js +++ b/modules/blueconicRtdProvider.js @@ -9,13 +9,14 @@ import {getStorageManager} from '../src/storageManager.js'; import {submodule} from '../src/hook.js'; import {mergeDeep, isPlainObject, logMessage, logError} from '../src/utils.js'; +import {MODULE_TYPE_RTD} from '../src/activities/modules.js'; const MODULE_NAME = 'realTimeData'; const SUBMODULE_NAME = 'blueconic'; export const RTD_LOCAL_NAME = 'bcPrebidData'; -export const storage = getStorageManager({moduleName: SUBMODULE_NAME}); +export const storage = getStorageManager({moduleType: MODULE_TYPE_RTD, moduleName: SUBMODULE_NAME}); /** * Try parsing stringified array of data. diff --git a/modules/braveBidAdapter.js b/modules/braveBidAdapter.js index 2e087b41868..ea8b4af690c 100644 --- a/modules/braveBidAdapter.js +++ b/modules/braveBidAdapter.js @@ -83,7 +83,7 @@ export const spec = { domain: parseUrl(page).hostname, page: page, }, - tmax: bidderRequest.timeout || config.getConfig('bidderTimeout') || 500, + tmax: bidderRequest.timeout, imp }; diff --git a/modules/brightcomBidAdapter.js b/modules/brightcomBidAdapter.js index bbe0203772a..c4cc5394a03 100644 --- a/modules/brightcomBidAdapter.js +++ b/modules/brightcomBidAdapter.js @@ -9,6 +9,7 @@ const URL = 'https://brightcombid.marphezis.com/hb'; export const spec = { code: BIDDER_CODE, supportedMediaTypes: [BANNER], + gvlid: 883, isBidRequestValid, buildRequests, interpretResponse, @@ -67,7 +68,7 @@ function buildRequests(bidReqs, bidderRequest) { w: screen.width, h: screen.height }, - tmax: config.getConfig('bidderTimeout') + tmax: bidderRequest?.timeout }; if (bidderRequest && bidderRequest.gdprConsent) { @@ -99,7 +100,6 @@ function buildRequests(bidReqs, bidderRequest) { method: 'POST', url: URL, data: JSON.stringify(brightcomBidReq), - options: {contentType: 'text/plain', withCredentials: false} }; } catch (e) { logError(e, {bidReqs, bidderRequest}); diff --git a/modules/brightcomSSPBidAdapter.js b/modules/brightcomSSPBidAdapter.js new file mode 100644 index 00000000000..b85a01c8fc7 --- /dev/null +++ b/modules/brightcomSSPBidAdapter.js @@ -0,0 +1,322 @@ +import { + getBidIdParameter, + isArray, + getWindowTop, + getUniqueIdentifierStr, + deepSetValue, + logError, + logWarn, + createTrackPixelHtml, + getWindowSelf, + isFn, + isPlainObject, +} from '../src/utils.js'; +import {registerBidder} from '../src/adapters/bidderFactory.js'; +import {BANNER} from '../src/mediaTypes.js'; +import {config} from '../src/config.js'; +import {ajax} from '../src/ajax.js'; + +const BIDDER_CODE = 'bcmssp'; +const URL = 'https://rt.marphezis.com/hb'; +const TRACK_EVENT_URL = 'https://rt.marphezis.com/prebid' + +export const spec = { + code: BIDDER_CODE, + gvlid: 883, + supportedMediaTypes: [BANNER], + isBidRequestValid, + buildRequests, + interpretResponse, + onBidderError, + onTimeout, + onBidWon, + getUserSyncs, +}; + +function buildRequests(bidReqs, bidderRequest) { + try { + const impressions = bidReqs.map(bid => { + let bidSizes = bid?.mediaTypes?.banner?.sizes || bid.sizes; + bidSizes = ((isArray(bidSizes) && isArray(bidSizes[0])) ? bidSizes : [bidSizes]); + bidSizes = bidSizes.filter(size => isArray(size)); + const processedSizes = bidSizes.map(size => ({w: parseInt(size[0], 10), h: parseInt(size[1], 10)})); + + const element = document.getElementById(bid.adUnitCode); + const minSize = _getMinSize(processedSizes); + const viewabilityAmount = _isViewabilityMeasurable(element) ? _getViewability(element, getWindowTop(), minSize) : 'na'; + const viewabilityAmountRounded = isNaN(viewabilityAmount) ? viewabilityAmount : Math.round(viewabilityAmount); + + const imp = { + id: bid.bidId, + banner: { + format: processedSizes, + ext: { + viewability: viewabilityAmountRounded + } + }, + tagid: String(bid.adUnitCode) + }; + + const bidFloor = _getBidFloor(bid); + + if (bidFloor) { + imp.bidfloor = bidFloor; + } + + return imp; + }) + + const referrer = bidderRequest?.refererInfo?.page || ''; + const publisherId = getBidIdParameter('publisherId', bidReqs[0].params); + + const payload = { + id: getUniqueIdentifierStr(), + imp: impressions, + site: { + domain: bidderRequest?.refererInfo?.domain || '', + page: referrer, + publisher: { + id: publisherId + } + }, + device: { + devicetype: _getDeviceType(), + w: screen.width, + h: screen.height + }, + tmax: bidderRequest?.timeout + }; + + if (bidderRequest?.gdprConsent) { + deepSetValue(payload, 'regs.ext.gdpr', +bidderRequest.gdprConsent.gdprApplies); + deepSetValue(payload, 'user.ext.consent', bidderRequest.gdprConsent.consentString); + } + + if (bidderRequest?.uspConsent) { + deepSetValue(payload, 'regs.ext.us_privacy', bidderRequest.uspConsent); + } + + if (config.getConfig('coppa') === true) { + deepSetValue(payload, 'regs.coppa', 1); + } + + if (bidReqs?.[0]?.schain) { + deepSetValue(payload, 'source.ext.schain', bidReqs[0].schain) + } + + if (bidReqs?.[0]?.userIdAsEids) { + deepSetValue(payload, 'user.ext.eids', bidReqs[0].userIdAsEids || []) + } + + if (bidReqs?.[0].userId) { + deepSetValue(payload, 'user.ext.ids', bidReqs[0].userId || []) + } + + return { + method: 'POST', + url: URL, + data: JSON.stringify(payload), + }; + } catch (e) { + logError(e, {bidReqs, bidderRequest}); + } +} + +function isBidRequestValid(bid) { + if (bid.bidder !== BIDDER_CODE || !bid.params || !bid.params.publisherId) { + return false; + } + + return true; +} + +function interpretResponse(serverResponse) { + let response = []; + if (!serverResponse.body || typeof serverResponse.body != 'object') { + logWarn('Brightcom server returned empty/non-json response: ' + JSON.stringify(serverResponse.body)); + return response; + } + + const {body: {id, seatbid}} = serverResponse; + + try { + if (id && seatbid && seatbid.length > 0 && seatbid[0].bid && seatbid[0].bid.length > 0) { + response = seatbid[0].bid.map(bid => { + return { + requestId: bid.impid, + cpm: parseFloat(bid.price), + width: parseInt(bid.w), + height: parseInt(bid.h), + creativeId: bid.crid || bid.id, + currency: 'USD', + netRevenue: true, + mediaType: BANNER, + ad: _getAdMarkup(bid), + ttl: 60, + meta: { + advertiserDomains: bid?.adomain || [] + } + }; + }); + } + } catch (e) { + logError(e, {id, seatbid}); + } + + return response; +} + +// Don't do user sync for now +function getUserSyncs(syncOptions, responses, gdprConsent) { + return []; +} + +function onTimeout(timeoutData) { + if (timeoutData === null) { + return; + } + + _trackEvent('timeout', timeoutData); +} + +function onBidderError(errorData) { + if (errorData === null || !errorData.bidderRequest) { + return; + } + + _trackEvent('error', errorData.bidderRequest) +} + +function onBidWon(bid) { + if (bid === null) { + return; + } + + _trackEvent('bidwon', bid) +} + +function _trackEvent(endpoint, data) { + ajax(`${TRACK_EVENT_URL}/${endpoint}`, null, JSON.stringify(data), { + method: 'POST', + withCredentials: false + }); +} + +function _isMobile() { + return (/(ios|ipod|ipad|iphone|android)/i).test(navigator.userAgent); +} + +function _isConnectedTV() { + return (/(smart[-]?tv|hbbtv|appletv|googletv|hdmi|netcast\.tv|viera|nettv|roku|\bdtv\b|sonydtv|inettvbrowser|\btv\b)/i).test(navigator.userAgent); +} + +function _getDeviceType() { + return _isMobile() ? 1 : _isConnectedTV() ? 3 : 2; +} + +function _getAdMarkup(bid) { + let adm = bid.adm; + if ('nurl' in bid) { + adm += createTrackPixelHtml(bid.nurl); + } + return adm; +} + +function _isViewabilityMeasurable(element) { + return !_isIframe() && element !== null; +} + +function _getViewability(element, topWin, {w, h} = {}) { + return getWindowTop().document.visibilityState === 'visible' ? _getPercentInView(element, topWin, {w, h}) : 0; +} + +function _isIframe() { + try { + return getWindowSelf() !== getWindowTop(); + } catch (e) { + return true; + } +} + +function _getMinSize(sizes) { + return sizes.reduce((min, size) => size.h * size.w < min.h * min.w ? size : min); +} + +function _getBoundingBox(element, {w, h} = {}) { + let {width, height, left, top, right, bottom} = element.getBoundingClientRect(); + + if ((width === 0 || height === 0) && w && h) { + width = w; + height = h; + right = left + w; + bottom = top + h; + } + + return {width, height, left, top, right, bottom}; +} + +function _getIntersectionOfRects(rects) { + const bbox = { + left: rects[0].left, right: rects[0].right, top: rects[0].top, bottom: rects[0].bottom + }; + + for (let i = 1; i < rects.length; ++i) { + bbox.left = Math.max(bbox.left, rects[i].left); + bbox.right = Math.min(bbox.right, rects[i].right); + + if (bbox.left >= bbox.right) { + return null; + } + + bbox.top = Math.max(bbox.top, rects[i].top); + bbox.bottom = Math.min(bbox.bottom, rects[i].bottom); + + if (bbox.top >= bbox.bottom) { + return null; + } + } + + bbox.width = bbox.right - bbox.left; + bbox.height = bbox.bottom - bbox.top; + + return bbox; +} + +function _getPercentInView(element, topWin, {w, h} = {}) { + const elementBoundingBox = _getBoundingBox(element, {w, h}); + + // Obtain the intersection of the element and the viewport + const elementInViewBoundingBox = _getIntersectionOfRects([{ + left: 0, top: 0, right: topWin.innerWidth, bottom: topWin.innerHeight + }, elementBoundingBox]); + + let elementInViewArea, elementTotalArea; + + if (elementInViewBoundingBox !== null) { + // Some or all of the element is in view + elementInViewArea = elementInViewBoundingBox.width * elementInViewBoundingBox.height; + elementTotalArea = elementBoundingBox.width * elementBoundingBox.height; + + return ((elementInViewArea / elementTotalArea) * 100); + } + + // No overlap between element and the viewport; therefore, the element + // lies completely out of view + return 0; +} + +function _getBidFloor(bid) { + if (!isFn(bid.getFloor)) { + return bid.params.bidFloor ? bid.params.bidFloor : null; + } + + let floor = bid.getFloor({ + currency: 'USD', mediaType: '*', size: '*' + }); + if (isPlainObject(floor) && !isNaN(floor.floor) && floor.currency === 'USD') { + return floor.floor; + } + return null; +} + +registerBidder(spec); diff --git a/modules/brightcomSSPBidAdapter.md b/modules/brightcomSSPBidAdapter.md new file mode 100644 index 00000000000..8d0e4ec70dc --- /dev/null +++ b/modules/brightcomSSPBidAdapter.md @@ -0,0 +1,46 @@ +# Overview + +``` +Module Name: Brightcom SSP Bid Adapter +Module Type: Bidder Adapter +Maintainer: alexandruc@brightcom.com +``` + +# Description + +Brightcom's adapter integration to the Prebid library. + +# Test Parameters + +``` +var adUnits = [ + { + code: 'test-leaderboard', + mediaTypes: { + banner: { + sizes: [[728, 90]] + } + }, + bids: [{ + bidder: 'bcmssp', + params: { + publisherId: 2141020, + bidFloor: 0.01 + } + }] + }, { + code: 'test-banner', + mediaTypes: { + banner: { + sizes: [[300, 250]] + } + }, + bids: [{ + bidder: 'bcmssp', + params: { + publisherId: 2141020 + } + }] + } +] +``` diff --git a/modules/browsiBidAdapter.js b/modules/browsiBidAdapter.js new file mode 100644 index 00000000000..f66b7b2c353 --- /dev/null +++ b/modules/browsiBidAdapter.js @@ -0,0 +1,171 @@ +import {registerBidder} from '../src/adapters/bidderFactory.js'; +import {config} from '../src/config.js'; +import {VIDEO} from '../src/mediaTypes.js'; +import {logError, logInfo, isArray, isStr} from '../src/utils.js'; + +const BIDDER_CODE = 'browsi'; +const DATA = 'brwvidtag'; +const ADAPTER = '__bad'; +const USP_TO_REPLACE = '__USP__'; +const GDPR_STR_TO_REPLACE = '__GDPR_STR__'; +const GDPR_TO_REPLACE = '__GDPR__'; +export const ENDPOINT = 'https://rtb.avantisvideo.com/api/v2/auction/getbid'; + +export const spec = { + code: BIDDER_CODE, + gvlid: 329, + supportedMediaTypes: [VIDEO], + /** + * Determines whether or not the given bid request is valid. + * @param bid + * @returns {boolean} + */ + isBidRequestValid: function (bid) { + if (!bid.params) { + return false; + } + const {pubId, tagId} = bid.params + const {mediaTypes} = bid; + return !!(validateBrowsiIds(pubId, tagId) && mediaTypes?.[VIDEO]); + }, + /** + * Make a server request from the list of BidRequests + * @param validBidRequests + * @param bidderRequest + * @returns ServerRequest Info describing the request to the server. + */ + buildRequests: function (validBidRequests, bidderRequest) { + const requests = []; + const {refererInfo, bidderRequestId, gdprConsent, uspConsent} = bidderRequest; + validBidRequests.forEach(bidRequest => { + const {bidId, adUnitCode, auctionId, transactionId, schain, params} = bidRequest; + const video = getVideoMediaType(bidRequest); + + const request = { + method: 'POST', + url: params.endpoint || ENDPOINT, + data: { + requestId: bidderRequestId, + bidId: bidId, + timeout: getTimeout(bidderRequest), + baData: getData(), + referer: refererInfo.page || refererInfo, + gdpr: gdprConsent, + ccpa: uspConsent, + sizes: video.playerSize, + video: video, + aUCode: adUnitCode, + aID: auctionId, + tID: transactionId, + schain: schain, + params: params + } + }; + requests.push(request); + }) + return requests; + }, + /** + * Unpack the response from the server into a list of bids. + * @param serverResponse A successful response from the server. + * @param request + * @returns {Bid[]} An array of bids which were nested inside the server. + */ + interpretResponse: function (serverResponse, request) { + const bidResponses = []; + const response = serverResponse?.body; + if (!response) { return bidResponses; } + const { + bidId, + w, + h, + vXml, + vUrl, + cpm, + cur, + ttl, + ...extraParams + } = response; + delete extraParams.userSyncs; + const bidResponse = { + requestId: request.data.bidId, + bidId, + vastXml: vXml, + vastUrl: vUrl, + cpm, + ttl, + mediaType: VIDEO, + width: w, + height: h, + currency: cur, + bidderCode: BIDDER_CODE, + ...extraParams + }; + bidResponses.push(bidResponse); + return bidResponses; + }, + /** + * Extracts user-syncs information from server response + * @param syncOptions {SyncOptions} + * @param serverResponses {ServerResponse[]} + * @param gdprConsent + * @param uspConsent + * @returns {UserSync[]} + */ + getUserSyncs: function (syncOptions, serverResponses, gdprConsent, uspConsent) { + const serverResponse = isArray(serverResponses) ? serverResponses[0] : serverResponses; + const syncParams = serverResponse?.body?.userSyncs; + const userSyncs = []; + const allowedTypes = []; + syncOptions.iframeEnabled && allowedTypes.push('iframe'); + syncOptions.pixelEnabled && allowedTypes.push('image'); + if (syncParams && allowedTypes.length) { + syncParams.forEach(syncParam => { + let { url, type } = syncParam; + if (!allowedTypes.includes(type)) { return; } + url = getValidUrl(url, gdprConsent, uspConsent); + userSyncs.push({ + type, + url + }); + }) + } + return userSyncs; + }, + onTimeout(timeoutData) { + logInfo(`${BIDDER_CODE} bidder timed out`, timeoutData); + }, + onBidderError: function ({error}) { + logError(`${BIDDER_CODE} bidder error`, error); + } +} +/** + * Replaces GdprConsent and uspConsent params in url + * @param url {String} + * @param gdprConsent + * @param uspConsent + * @returns {string} + */ +const getValidUrl = function (url, gdprConsent, uspConsent) { + let validUrl = url.replace(GDPR_TO_REPLACE, gdprConsent?.gdprApplies || '') + .replace(GDPR_STR_TO_REPLACE, encodeURIComponent(gdprConsent?.consentString || '')) + .replace(USP_TO_REPLACE, encodeURIComponent(uspConsent?.consentString || '')); + if (validUrl.indexOf('http') < 0) { + validUrl = 'http://' + validUrl; + } + return validUrl; +} + +const validateBrowsiIds = function (pubId, tagId) { + return pubId && tagId && isStr(pubId) && isStr(tagId); +} +const getData = function () { + return window[DATA]?.[ADAPTER]; +} +const getTimeout = function (bidderRequest) { + return bidderRequest.timeout || config.getConfig('bidderTimeout'); +} +const getVideoMediaType = function (bidRequest) { + return bidRequest.mediaTypes?.[VIDEO]; +} +registerBidder(spec); diff --git a/modules/browsiBidAdapter.md b/modules/browsiBidAdapter.md new file mode 100644 index 00000000000..cd032393c8e --- /dev/null +++ b/modules/browsiBidAdapter.md @@ -0,0 +1,38 @@ +# Overview + +``` +Module Name: Browsi Bid Adapter +Module Type: Bidder Adapter +Maintainer: support@browsi.com +``` + +# Description + +Connects to Browsi Ad server for bids. + +Browsi bid adapter supports Video media type. + +**Note:** The bid adapter requires correct setup and approval, including an existing publisher account. + +For more information about [Browsi](https://www.browsi.com), please contact [support@browsi.com](support@browsi.com). + +# Sample Ad Unit: +```javascript +let videoAdUnit = [ +{ + code: 'videoAdUnit', + mediaTypes: { + video: { + playerSize: [[300, 250]], + context: 'outstream' + }, + }, + bids: [{ + bidder: 'browsi', + params: { + pubId: '117a476f-9791-4a82-80db-4c01c1683db0', // Publisher ID provided by Browsi + tagId: '1' // Tag ID provided by Browsi + } + }] +}]; +``` diff --git a/modules/browsiRtdProvider.js b/modules/browsiRtdProvider.js index 31b2d709f35..4a61f40600d 100644 --- a/modules/browsiRtdProvider.js +++ b/modules/browsiRtdProvider.js @@ -24,8 +24,10 @@ import {find, includes} from '../src/polyfill.js'; import {getGlobal} from '../src/prebidGlobal.js'; import * as events from '../src/events.js'; import CONSTANTS from '../src/constants.json'; +import {MODULE_TYPE_RTD} from '../src/activities/modules.js'; +const MODULE_NAME = 'browsi'; -const storage = getStorageManager(); +const storage = getStorageManager({moduleType: MODULE_TYPE_RTD, moduleName: MODULE_NAME}); /** @type {ModuleParams} */ let _moduleParams = {}; @@ -336,7 +338,7 @@ export const browsiSubmodule = { * used to link submodule with realTimeData * @type {string} */ - name: 'browsi', + name: MODULE_NAME, /** * get data and send back to realTimeData module * @function diff --git a/modules/byDataAnalyticsAdapter.js b/modules/byDataAnalyticsAdapter.js index 84479ee618b..81fd4388c7d 100644 --- a/modules/byDataAnalyticsAdapter.js +++ b/modules/byDataAnalyticsAdapter.js @@ -5,9 +5,10 @@ import enc from 'crypto-js/enc-utf8'; import adapter from '../libraries/analyticsAdapter/AnalyticsAdapter.js'; import CONSTANTS from '../src/constants.json'; import adapterManager from '../src/adapterManager.js'; -import { getStorageManager } from '../src/storageManager.js'; +import {getStorageManager} from '../src/storageManager.js'; import { auctionManager } from '../src/auctionManager.js'; import { ajax } from '../src/ajax.js'; +import {MODULE_TYPE_ANALYTICS} from '../src/activities/modules.js'; const versionCode = '4.4.1' const secretKey = 'bydata@123456' @@ -16,7 +17,9 @@ const DEFAULT_EVENT_URL = 'https://pbjs-stream.bydata.com/topics/prebid' const analyticsType = 'endpoint' const isBydata = isKeyInUrl('bydata_debug') const adunitsMap = {} -const storage = getStorageManager(); +const MODULE_CODE = 'bydata'; +const storage = getStorageManager({moduleType: MODULE_TYPE_ANALYTICS, moduleName: MODULE_CODE}); + let initOptions = {} var payload = {} var winPayload = {} @@ -391,7 +394,7 @@ function sendDataOnKf(dataObj) { adapterManager.registerAnalyticsAdapter({ adapter: ascAdapter, - code: 'bydata' + code: MODULE_CODE, }); function _logInfo(message, meta) { diff --git a/modules/ccxBidAdapter.js b/modules/ccxBidAdapter.js index 7c6b0411023..28f3fe3a166 100644 --- a/modules/ccxBidAdapter.js +++ b/modules/ccxBidAdapter.js @@ -5,6 +5,7 @@ import {getStorageManager} from '../src/storageManager.js'; const BIDDER_CODE = 'ccx' const storage = getStorageManager({bidderCode: BIDDER_CODE}); const BID_URL = 'https://delivery.clickonometrics.pl/ortb/prebid/bid' +const GVLID = 773; const SUPPORTED_VIDEO_PROTOCOLS = [2, 3, 5, 6] const SUPPORTED_VIDEO_MIMES = ['video/mp4', 'video/x-flv'] const SUPPORTED_VIDEO_PLAYBACK_METHODS = [1, 2, 3, 4] @@ -19,8 +20,7 @@ function _getDeviceObj () { function _getSiteObj (bidderRequest) { let site = {} - // TODO: does the fallback to window.location make sense? - let url = bidderRequest?.refererInfo?.page || window.location.href + let url = bidderRequest?.refererInfo?.page || '' if (url.length > 0) { url = url.split('?')[0] } @@ -140,6 +140,7 @@ function _buildResponse (bid, currency, ttl) { export const spec = { code: BIDDER_CODE, + gvlid: GVLID, supportedMediaTypes: ['banner', 'video'], isBidRequestValid: function (bid) { diff --git a/modules/cleanmedianetBidAdapter.js b/modules/cleanmedianetBidAdapter.js index ba72eec3d95..46253d7af69 100644 --- a/modules/cleanmedianetBidAdapter.js +++ b/modules/cleanmedianetBidAdapter.js @@ -1,13 +1,34 @@ -import {deepAccess, getDNT, inIframe, isArray, isNumber, logError, logWarn} from '../src/utils.js'; +import { + deepAccess, + deepSetValue, + getDNT, + inIframe, + isArray, + isFn, + isNumber, + isPlainObject, + isStr, + logError, + logWarn +} from '../src/utils.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import {Renderer} from '../src/Renderer.js'; import {BANNER, VIDEO} from '../src/mediaTypes.js'; import {includes} from '../src/polyfill.js'; +const ENDPOINTS = { + 'cleanmedianet': 'https://bidder.cleanmediaads.com' +}; + +const DEFAULT_TTL = 360; + export const helper = { - getTopWindowDomain: function (url) { - const domainStart = url.indexOf('://') + '://'.length; - return url.substring(domainStart, url.indexOf('/', domainStart) < 0 ? url.length : url.indexOf('/', domainStart)); + getTopFrame: function () { + try { + return window.top === window ? 1 : 0; + } catch (e) { + } + return 0; }, startsWith: function (str, search) { return str.substr(0, search.length) === search; @@ -23,53 +44,50 @@ export const helper = { } } return BANNER; + }, + getBidFloor(bid) { + if (!isFn(bid.getFloor)) { + return bid.params.bidfloor ? bid.params.bidfloor : null; + } + + let bidFloor = bid.getFloor({ + mediaType: '*', + size: '*', + currency: 'USD' + }); + + if (isPlainObject(bidFloor) && !isNaN(bidFloor.floor) && bidFloor.currency === 'USD') { + return bidFloor.floor; + } + + return null; } }; export const spec = { code: 'cleanmedianet', aliases: [], - supportedMediaTypes: [BANNER, VIDEO], + supportedMediaTypes: ['banner', 'video'], isBidRequestValid: function (bid) { - return ( - !!bid.params.supplyPartnerId && - typeof bid.params.supplyPartnerId === 'string' && - (typeof bid.params.bidfloor === 'undefined' || - typeof bid.params.bidfloor === 'number') && - (typeof bid.params['adpos'] === 'undefined' || - typeof bid.params['adpos'] === 'number') && - (typeof bid.params['protocols'] === 'undefined' || - Array.isArray(bid.params['protocols'])) && - (typeof bid.params.instl === 'undefined' || - bid.params.instl === 0 || - bid.params.instl === 1) - ); + return !!bid.params.supplyPartnerId && isStr(bid.params.supplyPartnerId) && + (!bid.params['rtbEndpoint'] || isStr(bid.params['rtbEndpoint'])) && + (!bid.params.bidfloor || isNumber(bid.params.bidfloor)) && + (!bid.params['adpos'] || isNumber(bid.params['adpos'])) && + (!bid.params['protocols'] || Array.isArray(bid.params['protocols'])) && + (!bid.params.instl || bid.params.instl === 0 || bid.params.instl === 1); }, buildRequests: function (validBidRequests, bidderRequest) { return validBidRequests.map(bidRequest => { - const { - adUnitCode, - auctionId, - mediaTypes, - params, - sizes, - transactionId - } = bidRequest; - const baseEndpoint = 'https://bidder.cleanmediaads.com'; - const rtbEndpoint = - `${baseEndpoint}/r/${ - params.supplyPartnerId - }/bidr?rformat=open_rtb&reqformat=rtb_json&bidder=prebid` + - (params.query ? '&' + params.query : ''); - let url = bidderRequest.refererInfo.page; - + const {adUnitCode, auctionId, mediaTypes, params, sizes, transactionId} = bidRequest; + const baseEndpoint = (params['rtbEndpoint'] || ENDPOINTS['cleanmedianet']).replace(/^http:/, 'https:'); + const rtbEndpoint = `${baseEndpoint}/r/${params.supplyPartnerId}/bidr?rformat=open_rtb&reqformat=rtb_json&bidder=prebid` + (params.query ? '&' + params.query : ''); const rtbBidRequest = { id: auctionId, site: { domain: bidderRequest.refererInfo.domain, - page: url, + page: bidderRequest.refererInfo.page, ref: bidderRequest.refererInfo.ref }, device: { @@ -81,44 +99,35 @@ export const spec = { }, imp: [], ext: {}, - user: { - ext: {} - } + user: {ext: {}}, + source: {ext: {}}, + regs: {ext: {}} }; - if ( - bidderRequest.gdprConsent && - bidderRequest.gdprConsent.consentString && - bidderRequest.gdprConsent.gdprApplies - ) { - rtbBidRequest.ext.gdpr_consent = { - consent_string: bidderRequest.gdprConsent.consentString, - consent_required: bidderRequest.gdprConsent.gdprApplies - }; - rtbBidRequest.regs = { - ext: { - gdpr: bidderRequest.gdprConsent.gdprApplies === true ? 1 : 0 - } - }; - rtbBidRequest.user = { - ext: { - consent: bidderRequest.gdprConsent.consentString - } - }; + const gdprConsent = getGdprConsent(bidderRequest); + rtbBidRequest.ext.gdpr_consent = gdprConsent; + deepSetValue(rtbBidRequest, 'regs.ext.gdpr', gdprConsent.consent_required === true ? 1 : 0); + deepSetValue(rtbBidRequest, 'user.ext.consent', gdprConsent.consent_string); + + if (validBidRequests[0].schain) { + deepSetValue(rtbBidRequest, 'source.ext.schain', validBidRequests[0].schain); + } + + if (bidderRequest && bidderRequest.uspConsent) { + deepSetValue(rtbBidRequest, 'regs.ext.us_privacy', bidderRequest.uspConsent); } const imp = { id: transactionId, - instl: deepAccess(bidRequest.ortb2Imp, 'instl') === 1 || params.instl === 1 ? 1 : 0, + instl: deepAccess(bidderRequest.ortb2Imp, 'instl') === 1 || params.instl === 1 ? 1 : 0, tagid: adUnitCode, - bidfloor: 0, + bidfloor: helper.getBidFloor(bidRequest) || 0, bidfloorcur: 'USD', secure: 1 }; const hasFavoredMediaType = - params.favoredMediaType && - includes(this.supportedMediaTypes, params.favoredMediaType); + params.favoredMediaType && includes(this.supportedMediaTypes, params.favoredMediaType); if (!mediaTypes || mediaTypes.banner) { if (!hasFavoredMediaType || params.favoredMediaType === BANNER) { @@ -126,7 +135,7 @@ export const spec = { banner: { w: sizes.length ? sizes[0][0] : 300, h: sizes.length ? sizes[0][1] : 250, - pos: params.pos || 0, + pos: deepAccess(bidderRequest, 'mediaTypes.banner.pos') || params.pos || 0, topframe: inIframe() ? 0 : 1 } }); @@ -136,15 +145,25 @@ export const spec = { if (mediaTypes && mediaTypes.video) { if (!hasFavoredMediaType || params.favoredMediaType === VIDEO) { - let videoImp = { + const playerSize = mediaTypes.video.playerSize || sizes; + const videoImp = Object.assign({}, imp, { video: { - protocols: params.protocols || [1, 2, 3, 4, 5, 6], - pos: params.pos || 0, - ext: {context: mediaTypes.video.context} + protocols: bidRequest.mediaTypes.video.protocols || params.protocols || [1, 2, 3, 4, 5, 6], + pos: deepAccess(bidRequest, 'mediaTypes.video.pos') || params.pos || 0, + ext: { + context: mediaTypes.video.context + }, + mimes: bidRequest.mediaTypes.video.mimes, + maxduration: bidRequest.mediaTypes.video.maxduration, + api: bidRequest.mediaTypes.video.api, + skip: bidRequest.mediaTypes.video.skip || bidRequest.params.video.skip, + placement: bidRequest.mediaTypes.video.placement || bidRequest.params.video.placement, + minduration: bidRequest.mediaTypes.video.minduration || bidRequest.params.video.minduration, + playbackmethod: bidRequest.mediaTypes.video.playbackmethod || bidRequest.params.video.playbackmethod, + startdelay: bidRequest.mediaTypes.video.startdelay || bidRequest.params.video.startdelay } - }; + }); - let playerSize = mediaTypes.video.playerSize || sizes; if (isArray(playerSize[0])) { videoImp.video.w = playerSize[0][0]; videoImp.video.h = playerSize[0][1]; @@ -156,11 +175,20 @@ export const spec = { videoImp.video.h = 250; } - videoImp = Object.assign({}, imp, videoImp); rtbBidRequest.imp.push(videoImp); } } + let eids = []; + if (bidRequest && bidRequest.userId) { + addExternalUserId(eids, deepAccess(bidRequest, `userId.id5id.uid`), 'id5-sync.com', 'ID5ID'); + addExternalUserId(eids, deepAccess(bidRequest, `userId.tdid`), 'adserver.org', 'TDID'); + addExternalUserId(eids, deepAccess(bidRequest, `userId.idl_env`), 'liveramp.com', 'idl'); + } + if (eids.length > 0) { + rtbBidRequest.user.ext.eids = eids; + } + if (rtbBidRequest.imp.length === 0) { return; } @@ -181,10 +209,7 @@ export const spec = { return []; } - const bids = response.seatbid.reduce( - (acc, seatBid) => acc.concat(seatBid.bid), - [] - ); + const bids = response.seatbid.reduce((acc, seatBid) => acc.concat(seatBid.bid), []); let outBids = []; bids.forEach(bid => { @@ -193,75 +218,81 @@ export const spec = { cpm: bid.price, width: bid.w, height: bid.h, - ttl: 360, + ttl: DEFAULT_TTL, creativeId: bid.crid || bid.adid, netRevenue: true, currency: bid.cur || response.cur, - mediaType: helper.getMediaType(bid) + mediaType: helper.getMediaType(bid), }; - if ( - deepAccess( - bidRequest.bidRequest, - 'mediaTypes.' + outBid.mediaType - ) - ) { + if (bid.adomain && bid.adomain.length) { + outBid.meta = { + advertiserDomains: bid.adomain + } + } + + if (deepAccess(bidRequest.bidRequest, 'mediaTypes.' + outBid.mediaType)) { if (outBid.mediaType === BANNER) { outBids.push(Object.assign({}, outBid, {ad: bid.adm})); } else if (outBid.mediaType === VIDEO) { - const context = deepAccess( - bidRequest.bidRequest, - 'mediaTypes.video.context' - ); - outBids.push( - Object.assign({}, outBid, { - vastUrl: bid.ext.vast_url, - vastXml: bid.adm, - renderer: - context === 'outstream' - ? newRenderer(bidRequest.bidRequest, bid) - : undefined - }) - ); + const context = deepAccess(bidRequest.bidRequest, 'mediaTypes.video.context'); + outBids.push(Object.assign({}, outBid, { + vastUrl: bid.ext.vast_url, + vastXml: bid.adm, + renderer: context === 'outstream' ? newRenderer(bidRequest.bidRequest, bid) : undefined + })); } } }); return outBids; }, - getUserSyncs: function (syncOptions, serverResponses, gdprConsent) { + getUserSyncs: function (syncOptions, serverResponses, gdprConsent, uspConsent) { const syncs = []; - const gdprApplies = - gdprConsent && typeof gdprConsent.gdprApplies === 'boolean' - ? gdprConsent.gdprApplies - : false; - const suffix = gdprApplies - ? 'gc=' + encodeURIComponent(gdprConsent.consentString) - : 'gc=missing'; + let gdprApplies = false; + let consentString = ''; + let uspConsentString = ''; + + if (gdprConsent && (typeof gdprConsent.gdprApplies === 'boolean')) { + gdprApplies = gdprConsent.gdprApplies; + } + let gdpr = gdprApplies ? 1 : 0; + + if (gdprApplies && gdprConsent.consentString) { + consentString = encodeURIComponent(gdprConsent.consentString); + } + + if (uspConsent) { + uspConsentString = encodeURIComponent(uspConsent); + } + + const macroValues = { + gdpr: gdpr, + consent: consentString, + uspConsent: uspConsentString + }; + serverResponses.forEach(resp => { if (resp.body) { const bidResponse = resp.body; if (bidResponse.ext && Array.isArray(bidResponse.ext['utrk'])) { - bidResponse.ext['utrk'].forEach(pixel => { - const url = - pixel.url + - (pixel.url.indexOf('?') > 0 ? '&' + suffix : '?' + suffix); - return syncs.push({type: pixel.type, url}); - }); + bidResponse.ext['utrk'] + .forEach(pixel => { + const url = replaceMacros(pixel.url, macroValues); + syncs.push({type: pixel.type, url}); + }); } + if (Array.isArray(bidResponse.seatbid)) { bidResponse.seatbid.forEach(seatBid => { if (Array.isArray(seatBid.bid)) { seatBid.bid.forEach(bid => { if (bid.ext && Array.isArray(bid.ext['utrk'])) { - bid.ext['utrk'].forEach(pixel => { - const url = - pixel.url + - (pixel.url.indexOf('?') > 0 - ? '&' + suffix - : '?' + suffix); - return syncs.push({type: pixel.type, url}); - }); + bid.ext['utrk'] + .forEach(pixel => { + const url = replaceMacros(pixel.url, macroValues); + syncs.push({type: pixel.type, url}); + }); } }); } @@ -269,18 +300,16 @@ export const spec = { } } }); + return syncs; } }; function newRenderer(bidRequest, bid, rendererOptions = {}) { const renderer = Renderer.install({ - url: - (bidRequest.params && bidRequest.params.rendererUrl) || - (bid.ext && bid.ext.renderer_url) || - 'https://s.wlplayer.com/video/latest/renderer.js', + url: (bidRequest.params && bidRequest.params.rendererUrl) || (bid.ext && bid.ext.renderer_url) || 'https://s.gamoshi.io/video/latest/renderer.js', config: rendererOptions, - loaded: false + loaded: false, }); try { renderer.setRender(renderOutstream); @@ -300,10 +329,9 @@ function renderOutstream(bid) { width: bid.width, height: bid.height, events: { - ALL_ADS_COMPLETED: () => - window.setTimeout(() => { - window['GamoshiPlayer'].removeAd(unitId); - }, 300) + ALL_ADS_COMPLETED: () => window.setTimeout(() => { + window['GamoshiPlayer'].removeAd(unitId); + }, 300) }, vastUrl: bid.vastUrl, vastXml: bid.vastXml @@ -311,4 +339,41 @@ function renderOutstream(bid) { }); } +function addExternalUserId(eids, value, source, rtiPartner) { + if (isStr(value)) { + eids.push({ + source, + uids: [{ + id: value, + ext: { + rtiPartner + } + }] + }); + } +} + +function replaceMacros(url, macros) { + return url + .replace('[GDPR]', macros.gdpr) + .replace('[CONSENT]', macros.consent) + .replace('[US_PRIVACY]', macros.uspConsent); +} + +function getGdprConsent(bidderRequest) { + const gdprConsent = bidderRequest.gdprConsent; + + if (gdprConsent && gdprConsent.consentString && gdprConsent.gdprApplies) { + return { + consent_string: gdprConsent.consentString, + consent_required: gdprConsent.gdprApplies + }; + } + + return { + consent_required: false, + consent_string: '', + }; +} + registerBidder(spec); diff --git a/modules/cleanmedianetBidAdapter.md b/modules/cleanmedianetBidAdapter.md index f2bc8feb0f0..ee4e049e8d6 100644 --- a/modules/cleanmedianetBidAdapter.md +++ b/modules/cleanmedianetBidAdapter.md @@ -1,45 +1,49 @@ # Overview ``` -Module Name: Clean Media Net Adapter +Module Name: CleanMedia Bid Adapter Module Type: Bidder Adapter -Maintainer: dev@cleanmedia.net +Maintainer: dev@CleanMedia.net ``` # Description -Connects to Clean Media Net's Programmatic advertising platform as a service. +Connects to CleanMedia's Programmatic advertising platform as a service. -Clean Media bid adapter supports Banner & Video (Instream and Outstream). -The *only* required parameter (in the `params` section) is the `supplyPartnerId` parameter. +CleanMedia bid adapter supports Banner & Outstream Video. The *only* required parameter (in the `params` section) is the `supplyPartnerId` parameter. # Test Parameters ``` var adUnits = [ - // Banner adUnit + + // Banner adUnit { code: 'banner-div', sizes: [[300, 250]], bids: [{ bidder: 'cleanmedianet', params: { - // ID of the supply partner you created in the Clean Media Net dashboard + + // ID of the supply partner you created in the CleanMedia dashboard supplyPartnerId: '1253', - // OPTIONAL: custom bid floor + + // OPTIONAL: custom bid floor bidfloor: 0.01, - // OPTIONAL: if you know the ad position on the page, specify it here + + // OPTIONAL: if you know the ad position on the page, specify it here // (this corresponds to "Ad Position" in OpenRTB 2.3, section 5.4) //adpos: 1, - // OPTIONAL: whether this is an interstitial placement (0 or 1) + + // OPTIONAL: whether this is an interstitial placement (0 or 1) // (see "instl" property in "Imp" object in the OpenRTB 2.3, section 3.2.2) //instl: 0 } }] }, - // Video outstream adUnit + + // Video outstream adUnit { code: 'video-outstream', - sizes: [[300, 250]], mediaTypes: { video: { context: 'outstream', @@ -47,20 +51,62 @@ var adUnits = [ } }, bids: [ { - bidder: 'cleanmedianet', + bidder: 'CleanMedia', params: { - // ID of the supply partner you created in the dashboard + + // ID of the supply partner you created in the dashboard supplyPartnerId: '1254', - // OPTIONAL: custom bid floor + + // OPTIONAL: custom bid floor bidfloor: 0.01, - // OPTIONAL: if you know the ad position on the page, specify it here + + // OPTIONAL: if you know the ad position on the page, specify it here // (this corresponds to "Ad Position" in OpenRTB 2.3, section 5.4) //adpos: 1, - // OPTIONAL: whether this is an interstitial placement (0 or 1) + + // OPTIONAL: whether this is an interstitial placement (0 or 1) // (see "instl" property in "Imp" object in the OpenRTB 2.3, section 3.2.2) //instl: 0 } }] - } + }, + + // Multi-Format adUnit + { + code: 'banner-div', + mediaTypes: { + video: { + context: 'outstream', + playerSize: [300, 250] + }, + banner: { + sizes: [[300, 250]] + } + }, + bids: [{ + bidder: 'CleanMedia', + params: { + + // ID of the supply partner you created in the CleanMedia dashboard + supplyPartnerId: '1253', + + // OPTIONAL: custom bid floor + bidfloor: 0.01, + + // OPTIONAL: if you know the ad position on the page, specify it here + // (this corresponds to "Ad Position" in OpenRTB 2.3, section 5.4) + //adpos: 1, + + // OPTIONAL: whether this is an interstitial placement (0 or 1) + // (see "instl" property in "Imp" object in the OpenRTB 2.3, section 3.2.2) + //instl: 0, + + // OPTIONAL: enable enforcement bids of a specific media type (video, banner) + // in this ad placement + // query: 'key1=value1&k2=value2', + // favoredMediaType: 'video', + } + }] + }, ]; ``` diff --git a/modules/cointrafficBidAdapter.js b/modules/cointrafficBidAdapter.js index ce366cbecc8..380e1f5fc77 100644 --- a/modules/cointrafficBidAdapter.js +++ b/modules/cointrafficBidAdapter.js @@ -4,19 +4,20 @@ import { BANNER } from '../src/mediaTypes.js' import { config } from '../src/config.js' const BIDDER_CODE = 'cointraffic'; -const ENDPOINT_URL = 'https://apps-pbd.ctengine.io/pb/tmp'; +const ENDPOINT_URL = 'https://apps-pbd.ctraffic.io/pb/tmp'; const DEFAULT_CURRENCY = 'EUR'; const ALLOWED_CURRENCIES = [ 'EUR', 'USD', 'JPY', 'BGN', 'CZK', 'DKK', 'GBP', 'HUF', 'PLN', 'RON', 'SEK', 'CHF', 'ISK', 'NOK', 'HRK', 'RUB', 'TRY', 'AUD', 'BRL', 'CAD', 'CNY', 'HKD', 'IDR', 'ILS', 'INR', 'KRW', 'MXN', 'MYR', 'NZD', 'PHP', 'SGD', 'THB', 'ZAR', ]; +/** @type {BidderSpec} */ export const spec = { code: BIDDER_CODE, supportedMediaTypes: [BANNER], /** - * Determines whether or not the given bid request is valid. + * Determines whether the given bid request is valid. * * @param {BidRequest} bid The bid params to validate. * @return boolean True if this is a valid bid, and false otherwise. @@ -50,8 +51,7 @@ export const spec = { currency: currency, sizes: sizes, bidId: bidRequest.bidId, - // TODO: is 'page' the right value here? - referer: bidderRequest.refererInfo.page, + referer: bidderRequest.refererInfo.ref, }; return { diff --git a/modules/coinzillaBidAdapter.js b/modules/coinzillaBidAdapter.js index 7e9fb964a87..15731423c49 100644 --- a/modules/coinzillaBidAdapter.js +++ b/modules/coinzillaBidAdapter.js @@ -1,5 +1,4 @@ import { parseSizesInput } from '../src/utils.js'; -import {config} from '../src/config.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; const BIDDER_CODE = 'coinzilla'; @@ -78,7 +77,7 @@ export const spec = { dealId: dealId, currency: currency, netRevenue: netRevenue, - ttl: config.getConfig('_bidderTimeout'), + ttl: bidRequest.timeout, referrer: referrer, ad: response.ad, mediaType: response.mediaType, diff --git a/modules/colossussspBidAdapter.js b/modules/colossussspBidAdapter.js index 75e73ffda89..082fb0ac4db 100644 --- a/modules/colossussspBidAdapter.js +++ b/modules/colossussspBidAdapter.js @@ -87,6 +87,11 @@ export const spec = { logMessage(e); } + const firstPartyData = bidderRequest.ortb2 || {}; + const userObj = firstPartyData.user; + const siteObj = firstPartyData.site; + const appObj = firstPartyData.app; + // TODO: does the fallback to window.location make sense? const location = refferLocation || winLocation; let placements = []; @@ -97,6 +102,9 @@ export const spec = { secure: location.protocol === 'https:' ? 1 : 0, host: location.host, page: location.pathname, + userObj, + siteObj, + appObj, placements: placements }; diff --git a/modules/compassBidAdapter.js b/modules/compassBidAdapter.js index a89c4c617b6..addcdfebb27 100644 --- a/modules/compassBidAdapter.js +++ b/modules/compassBidAdapter.js @@ -155,7 +155,7 @@ export const spec = { coppa: config.getConfig('coppa') === true ? 1 : 0, ccpa: bidderRequest.uspConsent || undefined, gdpr: bidderRequest.gdprConsent || undefined, - tmax: config.getConfig('bidderTimeout') + tmax: bidderRequest.timeout }; const len = validBidRequests.length; diff --git a/modules/concertBidAdapter.js b/modules/concertBidAdapter.js index dcea60d5231..a25d9086446 100644 --- a/modules/concertBidAdapter.js +++ b/modules/concertBidAdapter.js @@ -5,7 +5,6 @@ import { hasPurpose1Consent } from '../src/utils/gpdr.js'; const BIDDER_CODE = 'concert'; const CONCERT_ENDPOINT = 'https://bids.concert.io'; -const USER_SYNC_URL = 'https://cdn.concert.io/lib/bids/sync.html'; export const spec = { code: BIDDER_CODE, @@ -43,14 +42,22 @@ export const spec = { pageUrl: bidderRequest.refererInfo.page, screen: [window.screen.width, window.screen.height].join('x'), debug: debugTurnedOn(), - uid: getUid(bidderRequest), + uid: getUid(bidderRequest, validBidRequests), optedOut: hasOptedOutOfPersonalization(), - adapterVersion: '1.1.1', + adapterVersion: '1.2.0', uspConsent: bidderRequest.uspConsent, - gdprConsent: bidderRequest.gdprConsent + gdprConsent: bidderRequest.gdprConsent, + gppConsent: bidderRequest.gppConsent, } }; + if (!payload.meta.gppConsent && bidderRequest.ortb2?.regs?.gpp) { + payload.meta.gppConsent = { + gppString: bidderRequest.ortb2.regs.gpp, + applicableSections: bidderRequest.ortb2.regs.gpp_sid + } + } + payload.slots = validBidRequests.map(bidRequest => { collectEid(eids, bidRequest); const adUnitElement = document.getElementById(bidRequest.adUnitCode) @@ -124,38 +131,6 @@ export const spec = { return bidResponses; }, - /** - * Register the user sync pixels which should be dropped after the auction. - * - * @param {SyncOptions} syncOptions Which user syncs are allowed? - * @param {ServerResponse[]} serverResponses List of server's responses. - * @param {gdprConsent} object GDPR consent object. - * @param {uspConsent} string US Privacy String. - * @return {UserSync[]} The user syncs which should be dropped. - */ - getUserSyncs: function(syncOptions, serverResponses, gdprConsent, uspConsent) { - const syncs = []; - if (syncOptions.iframeEnabled && !hasOptedOutOfPersonalization()) { - let params = []; - - if (gdprConsent && (typeof gdprConsent.gdprApplies === 'boolean')) { - params.push(`gdpr_applies=${gdprConsent.gdprApplies ? '1' : '0'}`); - } - if (gdprConsent && (typeof gdprConsent.consentString === 'string')) { - params.push(`gdpr_consent=${gdprConsent.consentString}`); - } - if (uspConsent && (typeof uspConsent === 'string')) { - params.push(`usp_consent=${uspConsent}`); - } - - syncs.push({ - type: 'iframe', - url: USER_SYNC_URL + (params.length > 0 ? `?${params.join('&')}` : '') - }); - } - return syncs; - }, - /** * Register bidder specific code, which will execute if bidder timed out after an auction * @param {data} Containing timeout specific data @@ -183,16 +158,23 @@ export const storage = getStorageManager({bidderCode: BIDDER_CODE}); /** * Check or generate a UID for the current user. */ -function getUid(bidderRequest) { +function getUid(bidderRequest, validBidRequests) { if (hasOptedOutOfPersonalization() || !consentAllowsPpid(bidderRequest)) { return false; } - const sharedId = deepAccess(bidderRequest, 'userId._sharedid.id'); + /** + * check for shareId or pubCommonId before generating a new one + * sharedId: @see https://docs.prebid.org/dev-docs/modules/userId.html + * pubCid (no longer supported): @see https://docs.prebid.org/dev-docs/modules/pubCommonId.html#adapter-integration + */ + const sharedId = + deepAccess(validBidRequests[0], 'userId.sharedid.id') || + deepAccess(validBidRequests[0], 'userId.pubcid') + const pubCid = deepAccess(validBidRequests[0], 'crumbs.pubcid'); - if (sharedId) { - return sharedId; - } + if (sharedId) return sharedId; + if (pubCid) return pubCid; const LEGACY_CONCERT_UID_KEY = 'c_uid'; const CONCERT_UID_KEY = 'vmconcert_uid'; @@ -229,16 +211,24 @@ function hasOptedOutOfPersonalization() { * @param {BidderRequest} bidderRequest Object which contains any data consent signals */ function consentAllowsPpid(bidderRequest) { - /* NOTE: We can't easily test GDPR consent, without the - * `consent-string` npm module; so will have to rely on that - * happening on the bid-server. */ - const uspConsent = !(bidderRequest?.uspConsent === 'string' && + let uspConsentAllows = true; + + // if a us privacy string was provided, but they explicitly opted out + if ( + typeof bidderRequest?.uspConsent === 'string' && bidderRequest?.uspConsent[0] === '1' && - bidderRequest?.uspConsent[2].toUpperCase() === 'Y'); + bidderRequest?.uspConsent[2].toUpperCase() === 'Y' // user has opted-out + ) { + uspConsentAllows = false; + } - const gdprConsent = bidderRequest?.gdprConsent && hasPurpose1Consent(bidderRequest?.gdprConsent); + /* + * True if the gdprConsent is null-y; or GDPR does not apply; or if purpose 1 consent was given. + * Much more nuanced GDPR requirements are tested on the bid server using the @iabtcf/core npm module; + */ + const gdprConsentAllows = hasPurpose1Consent(bidderRequest?.gdprConsent); - return (uspConsent || gdprConsent); + return (uspConsentAllows && gdprConsentAllows); } function collectEid(eids, bid) { diff --git a/modules/confiantRtdProvider.js b/modules/confiantRtdProvider.js new file mode 100644 index 00000000000..2c13ad87d75 --- /dev/null +++ b/modules/confiantRtdProvider.js @@ -0,0 +1,128 @@ +/** + * This module provides comprehensive detection of security, quality, and privacy threats by Confiant Inc, + * the industry leader in real-time detecting and blocking of bad ads + * + * The {@link module:modules/realTimeData} module is required + * The module will inject a Confiant Inc. script into the page to monitor ad impressions + * @module modules/confiantRtdProvider + * @requires module:modules/realTimeData + */ + +import { submodule } from '../src/hook.js'; +import { logError, generateUUID } from '../src/utils.js'; +import { loadExternalScript } from '../src/adloader.js'; +import * as events from '../src/events.js'; +import CONSTANTS from '../src/constants.json'; + +/** + * Injects the Confiant Inc. configuration script into the page, based on proprtyId provided + * @param {string} propertyId + */ +function injectConfigScript(propertyId) { + const scriptSrc = `https://cdn.confiant-integrations.net/${propertyId}/gpt_and_prebid/config.js`; + + loadExternalScript(scriptSrc, 'confiant', () => {}); +} + +/** + * Set up page with Confiant integration + * @param {Object} config + */ +function setupPage(config) { + const propertyId = config?.params?.propertyId; + if (!propertyId) { + logError('Confiant pbjs module: no propertyId provided'); + return false; + } + + const confiant = window.confiant || Object.create(null); + confiant[propertyId] = confiant[propertyId] || Object.create(null); + confiant[propertyId].clientSettings = confiant[propertyId].clientSettings || Object.create(null); + confiant[propertyId].clientSettings.isMGBL = true; + confiant[propertyId].clientSettings.prebidExcludeBidders = config?.params?.prebidExcludeBidders; + confiant[propertyId].clientSettings.prebidNameSpace = config?.params?.prebidNameSpace; + + if (config?.params?.shouldEmitBillableEvent) { + if (window.frames['cnftComm']) { + subscribeToConfiantCommFrame(window); + } else { + setUpMutationObserver(); + } + } + + injectConfigScript(propertyId); + return true; +} + +/** + * Subscribe to window's message events to report Billable events + * @param {Window} targetWindow window instance to subscribe to + */ +function subscribeToConfiantCommFrame(targetWindow) { + targetWindow.addEventListener('message', reportBillableEvents); +} + +let mutationObserver; +/** + * Set up mutation observer to subscribe to Confiant's communication channel ASAP + */ +function setUpMutationObserver() { + mutationObserver = new MutationObserver((mutations) => { + mutations.forEach((mutation) => { + mutation.addedNodes.forEach((addedNode) => { + if (addedNode.nodeName === 'IFRAME' && addedNode.name === 'cnftComm' && !addedNode.pbjsModuleSubscribed) { + addedNode.pbjsModuleSubscribed = true; + mutationObserver.disconnect(); + mutationObserver = null; + const iframeWindow = addedNode.contentWindow; + subscribeToConfiantCommFrame(iframeWindow); + } + }); + }); + }); + mutationObserver.observe(document.head, { childList: true, subtree: true }); +} + +/** + * Emit billable event when Confiant integration reports that it has monitored an impression + */ +function reportBillableEvents (e) { + events.emit(CONSTANTS.EVENTS.BILLABLE_EVENT, { + auctionId: e.data.auctionId, + billingId: generateUUID(), + transactionId: e.data.transactionId, + type: 'impression', + vendor: 'confiant' + }); +} + +/** + * Confiant submodule registration + */ +function registerConfiantSubmodule() { + submodule('realTimeData', { + name: 'confiant', + init: (config) => { + try { + return setupPage(config); + } catch (err) { + logError(err.message); + if (mutationObserver) { + mutationObserver.disconnect(); + } + return false; + } + } + }); +} + +registerConfiantSubmodule(); + +export default { + injectConfigScript, + setupPage, + subscribeToConfiantCommFrame, + setUpMutationObserver, + reportBillableEvents, + registerConfiantSubmodule +}; diff --git a/modules/confiantRtdProvider.md b/modules/confiantRtdProvider.md new file mode 100644 index 00000000000..e92c0aabcba --- /dev/null +++ b/modules/confiantRtdProvider.md @@ -0,0 +1,45 @@ +# Overview + +``` +Module Name: Confiant Inc. Rtd provider +Module Type: Rtd Provider +Maintainer: +``` + +Confiant’s module provides comprehensive detection of security, quality, and privacy threats across your ad stack. +Confiant is the industry leader in real-time detecting and blocking of bad ads when it comes to protecting your users and brand reputation. + +To start using this module, please contact [Confiant](https://www.confiant.com/contact) to get an account and customer key. + + +# Integration + +1) Build Prebid bundle with Confiant module included: + + +``` +gulp build --modules=confiantRtdProvider,... +``` + +2) Include the resulting bundle on your page. + +# Configuration + +Configuration of Confiant module is plain simple: + +```javascript +pbjs.setConfig({ + realTimeData: { + dataProviders: [{ + name: 'confiant', + params: { + // so please get in touch with us so we could help you to set up the module with proper parameters + propertyId: '', // required, string param, obtained from Confiant Inc. + prebidExcludeBidders: '', // optional, comma separated list of bidders to exclude from Confiant's prebid.js integration + prebidNameSpace: '', // optional, string param, namespace for prebid.js integration + shouldEmitBillableEvent: false, // optional, boolean param, upon being set to true enables firing of the BillableEvent upon Confiant's impression scanning + } + }] + } +}); +``` diff --git a/modules/connectIdSystem.js b/modules/connectIdSystem.js index 24ef8ad0af5..82669f12623 100644 --- a/modules/connectIdSystem.js +++ b/modules/connectIdSystem.js @@ -8,14 +8,86 @@ import {ajax} from '../src/ajax.js'; import {submodule} from '../src/hook.js'; import {includes} from '../src/polyfill.js'; -import {formatQS, logError} from '../src/utils.js'; +import {getRefererInfo} from '../src/refererDetection.js'; +import {getStorageManager} from '../src/storageManager.js'; +import {formatQS, isPlainObject, logError, parseUrl} from '../src/utils.js'; +import {uspDataHandler} from '../src/adapterManager.js'; +import {MODULE_TYPE_UID} from '../src/activities/modules.js'; const MODULE_NAME = 'connectId'; +const STORAGE_EXPIRY_DAYS = 14; const VENDOR_ID = 25; const PLACEHOLDER = '__PIXEL_ID__'; const UPS_ENDPOINT = `https://ups.analytics.yahoo.com/ups/${PLACEHOLDER}/fed`; const OVERRIDE_OPT_OUT_KEY = 'connectIdOptOut'; const INPUT_PARAM_KEYS = ['pixelId', 'he', 'puid']; +export const storage = getStorageManager({moduleType: MODULE_TYPE_UID, moduleName: MODULE_NAME}); + +/** + * @function + * @param {Object} obj + */ +function storeObject(obj) { + const expires = Date.now() + (60 * 60 * 24 * 1000 * STORAGE_EXPIRY_DAYS); + if (storage.cookiesAreEnabled()) { + setEtldPlusOneCookie(MODULE_NAME, JSON.stringify(obj), new Date(expires), getSiteHostname()); + } else if (storage.localStorageIsEnabled()) { + obj.__expires = expires; + storage.setDataInLocalStorage(MODULE_NAME, obj); + } +} + +/** + * Attempts to store a cookie on eTLD + 1 + * + * @function + * @param {String} key + * @param {String} value + * @param {Date} expirationDate + * @param {String} hostname + */ +function setEtldPlusOneCookie(key, value, expirationDate, hostname) { + const subDomains = hostname.split('.'); + for (let i = 0; i < subDomains.length; ++i) { + const domain = subDomains.slice(subDomains.length - i - 1, subDomains.length).join('.'); + try { + storage.setCookie(key, value, expirationDate.toUTCString(), null, '.' + domain); + const storedCookie = storage.getCookie(key); + if (storedCookie && storedCookie === value) { + break; + } + } catch (error) {} + } +} + +function getIdFromCookie() { + if (storage.cookiesAreEnabled()) { + try { + return JSON.parse(storage.getCookie(MODULE_NAME)); + } catch {} + } + return null; +} + +function getIdFromLocalStorage() { + if (storage.localStorageIsEnabled()) { + const storedIdData = storage.getDataFromLocalStorage(MODULE_NAME); + if (storedIdData) { + if (isPlainObject(storedIdData) && storedIdData.__expires && + storedIdData.__expires <= Date.now()) { + storage.removeDataFromLocalStorage(MODULE_NAME); + return null; + } + return storedIdData; + } + } + return null; +} + +function getSiteHostname() { + const pageInfo = parseUrl(getRefererInfo().page); + return pageInfo.hostname; +} /** @type {Submodule} */ export const connectIdSubmodule = { @@ -37,8 +109,8 @@ export const connectIdSubmodule = { if (connectIdSubmodule.userHasOptedOut()) { return undefined; } - return (typeof value === 'object' && value.connectid) - ? {connectId: value.connectid} : undefined; + return (isPlainObject(value) && (value.connectId || value.connectid)) + ? {connectId: value.connectId || value.connectid} : undefined; }, /** * Gets the Yahoo ConnectID @@ -54,18 +126,30 @@ export const connectIdSubmodule = { const params = config.params || {}; if (!params || (typeof params.he !== 'string' && typeof params.puid !== 'string') || (typeof params.pixelId === 'undefined' && typeof params.endpoint === 'undefined')) { - logError('The connectId submodule requires the \'pixelId\' and at least one of the \'he\' ' + - 'or \'puid\' parameters to be defined.'); + logError(`${MODULE_NAME} module: configurataion requires the 'pixelId' and at ` + + `least one of the 'he' or 'puid' parameters to be defined.`); return; } + const storedId = getIdFromCookie() || getIdFromLocalStorage(); + if (storedId) { + return {id: storedId}; + } + + const uspString = uspDataHandler.getConsentData() || ''; const data = { + v: '1', '1p': includes([1, '1', true], params['1p']) ? '1' : '0', gdpr: connectIdSubmodule.isEUConsentRequired(consentData) ? '1' : '0', - gdpr_consent: connectIdSubmodule.isEUConsentRequired(consentData) ? consentData.gdpr.consentString : '', - us_privacy: consentData && consentData.uspConsent ? consentData.uspConsent : '' + gdpr_consent: connectIdSubmodule.isEUConsentRequired(consentData) ? consentData.consentString : '', + us_privacy: uspString }; + let topmostLocation = getRefererInfo().topmostLocation; + if (typeof topmostLocation === 'string') { + data.url = topmostLocation.split('?')[0]; + } + INPUT_PARAM_KEYS.forEach(key => { if (typeof params[key] != 'undefined') { data[key] = params[key]; @@ -79,6 +163,11 @@ export const connectIdSubmodule = { if (response) { try { responseObj = JSON.parse(response); + if (isPlainObject(responseObj) && Object.keys(responseObj).length > 0) { + storeObject(responseObj); + } else { + logError(`${MODULE_NAME} module: UPS response returned an invalid payload ${response}`); + } } catch (error) { logError(error); } @@ -86,7 +175,7 @@ export const connectIdSubmodule = { callback(responseObj); }, error: error => { - logError(`${MODULE_NAME}: ID fetch encountered an error`, error); + logError(`${MODULE_NAME} module: ID fetch encountered an error`, error); callback(); } }; @@ -98,12 +187,12 @@ export const connectIdSubmodule = { }, /** - * Utility function that returns a boolean flag indicating if the opporunity + * Utility function that returns a boolean flag indicating if the opportunity * is subject to GDPR * @returns {Boolean} */ isEUConsentRequired(consentData) { - return !!(consentData && consentData.gdpr && consentData.gdpr.gdprApplies); + return !!(consentData?.gdprApplies); }, /** diff --git a/modules/connectIdSystem.md b/modules/connectIdSystem.md index c671c036b77..a3b69a0082c 100644 --- a/modules/connectIdSystem.md +++ b/modules/connectIdSystem.md @@ -27,8 +27,9 @@ The below parameters apply only to the Yahoo ConnectID user ID Module. | Param under usersync.userIds[] | Scope | Type | Description | Example | | --- | --- | --- | --- | --- | -| name | Required | String | ID value for the Yahoo ConnectID module - `"connectId"` | `"connectId"` | -| params | Required | Object | Data for Yahoo ConnectID initialization. | | -| params.pixelId | Required | Number | The Yahoo supplied publisher specific pixel Id. | `8976` | -| params.he | Optional | String | The SHA-256 hashed user email address. One of either the `he` parameter or the `puid` parameter must be supplied. | `"529cb86de31e9547a712d9f380146e98bbd39beec"` | -| params.puid | Optional | String | The publisher-supplied user identifier. One of either the `he` parameter or the `puid` parameter must be supplied. | `"P-975484817"` | +| name | Required | String | The name of this module. | `"connectId"` | +| params | Required | Object | Container of all module params. || +| params.pixelId | Required | Number | +The Yahoo-supplied publisher-specific pixel ID. | `"0000"` | +| params.he | Optional | String | The SHA-256 hashed user email address which has been lowercased prior to hashing. Pass both `he` and `puid` params if present, otherwise pass either of the two that is available. |`"ed8ddbf5a171981db8ef938596ca297d5e3f84bcc280041c5880dba3baf9c1d4"`| +| params.puid | Optional | String | The publisher supplied user identifier such as a first-party cookie. Pass both `he` and `puid` params if present, otherwise pass either of the two that is available. | `"ab9iibf5a231ii1db8ef911596ca297d5e3f84biii00041c5880dba3baf9c1da"` | diff --git a/modules/connectadBidAdapter.js b/modules/connectadBidAdapter.js index 5185308eab0..d53e3b28ab5 100644 --- a/modules/connectadBidAdapter.js +++ b/modules/connectadBidAdapter.js @@ -2,8 +2,6 @@ import { deepSetValue, convertTypes, tryAppendQueryString, logWarn } from '../sr import { registerBidder } from '../src/adapters/bidderFactory.js'; import { BANNER } from '../src/mediaTypes.js' import {config} from '../src/config.js'; -import {createEidsArray} from './userId/eids.js'; - const BIDDER_CODE = 'connectad'; const BIDDER_CODE_ALIAS = 'connectadrealtime'; const ENDPOINT_URL = 'https://i.connectad.io/api/v2'; @@ -73,8 +71,8 @@ export const spec = { } // EIDS Support - if (validBidRequests[0].userId) { - deepSetValue(data, 'user.ext.eids', createEidsArray(validBidRequests[0].userId)); + if (validBidRequests[0].userIdAsEids) { + deepSetValue(data, 'user.ext.eids', validBidRequests[0].userIdAsEids); } validBidRequests.map(bid => { diff --git a/modules/consentManagement.js b/modules/consentManagement.js index 95d622e55e4..6c22b3b9da4 100644 --- a/modules/consentManagement.js +++ b/modules/consentManagement.js @@ -10,6 +10,8 @@ import {gdprDataHandler} from '../src/adapterManager.js'; import {includes} from '../src/polyfill.js'; import {timedAuctionHook} from '../src/utils/perfMetrics.js'; import {registerOrtbProcessor, REQUEST} from '../src/pbjsORTB.js'; +import {enrichFPD} from '../src/fpd/enrichment.js'; +import {getGlobal} from '../src/prebidGlobal.js'; const DEFAULT_CMP = 'iab'; const DEFAULT_CONSENT_TIMEOUT = 10000; @@ -19,10 +21,10 @@ export let userCMP; export let consentTimeout; export let gdprScope; export let staticConsentData; +let actionTimeout; let consentData; let addedConsentHook = false; -let provisionalConsent; // add new CMPs here, with their dedicated lookup function const cmpCallMap = { @@ -45,7 +47,7 @@ function lookupStaticConsentData({onSuccess, onError}) { * @param {function({})} onSuccess acts as a success callback when CMP returns a value; pass along consentObjectfrom CMP * @param {function(string, ...{}?)} cmpError acts as an error callback while interacting with CMP; pass along an error message (string) and any extra error arguments (purely for logging) */ -function lookupIabConsent({onSuccess, onError}) { +function lookupIabConsent({onSuccess, onError, onEvent}) { function findCMP() { let f = window; let cmpFrame; @@ -79,10 +81,9 @@ function lookupIabConsent({onSuccess, onError}) { function cmpResponseCallback(tcfData, success) { logInfo('Received a response from CMP', tcfData); if (success) { + onEvent(tcfData); if (tcfData.gdprApplies === false || tcfData.eventStatus === 'tcloaded' || tcfData.eventStatus === 'useractioncomplete') { processCmpData(tcfData, {onSuccess, onError}); - } else { - provisionalConsent = tcfData; } } else { onError('CMP unable to register callback function. Please check CMP setup.'); @@ -93,7 +94,7 @@ function lookupIabConsent({onSuccess, onError}) { const { cmpFrame, cmpFunction } = findCMP(); if (!cmpFrame) { - return onError('CMP not found.'); + return onError('TCF2 CMP not found.'); } // to collect the consent information from the user, we perform two calls to the CMP in parallel: // first to collect the user's consent choices represented in an encoded string (via getConsentData) @@ -164,11 +165,24 @@ function lookupIabConsent({onSuccess, onError}) { function loadConsentData(cb) { let isDone = false; let timer = null; + let onTimeout, provisionalConsent; + let cmpLoaded = false; - function done(consentData, shouldCancelAuction, errMsg, ...extraArgs) { + function resetTimeout(timeout) { if (timer != null) { clearTimeout(timer); } + if (!isDone && timeout != null) { + if (timeout === 0) { + onTimeout() + } else { + timer = setTimeout(onTimeout, timeout); + } + } + } + + function done(consentData, shouldCancelAuction, errMsg, ...extraArgs) { + resetTimeout(null); isDone = true; gdprDataHandler.setConsentData(consentData); if (typeof cb === 'function') { @@ -185,25 +199,30 @@ function loadConsentData(cb) { onSuccess: (data) => done(data, false), onError: function (msg, ...extraArgs) { done(null, true, msg, ...extraArgs); + }, + onEvent: function (consentData) { + provisionalConsent = consentData; + if (cmpLoaded) return; + cmpLoaded = true; + if (actionTimeout != null) { + resetTimeout(actionTimeout); + } } } - cmpCallMap[userCMP](callbacks); - if (!isDone) { - const onTimeout = () => { - const continueToAuction = (data) => { - done(data, false, 'CMP did not load, continuing auction...'); - } - processCmpData(provisionalConsent, { - onSuccess: continueToAuction, - onError: () => continueToAuction(storeConsentData(undefined)) - }) - } - if (consentTimeout === 0) { - onTimeout(); - } else { - timer = setTimeout(onTimeout, consentTimeout); + onTimeout = () => { + const continueToAuction = (data) => { + done(data, false, `${cmpLoaded ? 'Timeout waiting for user action on CMP' : 'CMP did not load'}, continuing auction...`); } + processCmpData(provisionalConsent, { + onSuccess: continueToAuction, + onError: () => continueToAuction(storeConsentData(undefined)), + }) + } + + cmpCallMap[userCMP](callbacks); + if (!(actionTimeout != null && cmpLoaded)) { + resetTimeout(consentTimeout); } } @@ -269,11 +288,6 @@ function processCmpData(consentObject, {onSuccess, onError}) { ); } - // do extra things for static config - if (userCMP === 'static') { - consentObject = consentObject.getTCData; - } - if (checkData()) { onError(`CMP returned unexpected value during lookup process.`, consentObject); } else { @@ -313,11 +327,11 @@ export function resetConsentData() { * @param {{cmp:string, timeout:number, allowAuctionWithoutConsent:boolean, defaultGdprScope:boolean}} config required; consentManagement module config settings; cmp (string), timeout (int), allowAuctionWithoutConsent (boolean) */ export function setConsentConfig(config) { - // if `config.gdpr` or `config.usp` exist, assume new config format. + // if `config.gdpr`, `config.usp` or `config.gpp` exist, assume new config format. // else for backward compatability, just use `config` - config = config && (config.gdpr || config.usp ? config.gdpr : config); + config = config && (config.gdpr || config.usp || config.gpp ? config.gdpr : config); if (!config || typeof config !== 'object') { - logWarn('consentManagement config not defined, exiting consent manager'); + logWarn('consentManagement (gdpr) config not defined, exiting consent manager'); return; } if (isStr(config.cmpApi)) { @@ -334,6 +348,8 @@ export function setConsentConfig(config) { logInfo(`consentManagement config did not specify timeout. Using system default setting (${DEFAULT_CONSENT_TIMEOUT}).`); } + actionTimeout = isNumber(config.actionTimeout) ? config.actionTimeout : null; + // if true, then gdprApplies should be set to true gdprScope = config.defaultGdprScope === true; @@ -342,13 +358,17 @@ export function setConsentConfig(config) { if (userCMP === 'static') { if (isPlainObject(config.consentData)) { staticConsentData = config.consentData; + if (staticConsentData?.getTCData != null) { + // accept static config with or without `getTCData` - see https://github.com/prebid/Prebid.js/issues/9581 + staticConsentData = staticConsentData.getTCData; + } consentTimeout = 0; } else { logError(`consentManagement config with cmpApi: 'static' did not specify consentData. No consents will be available to adapters.`); } } if (!addedConsentHook) { - $$PREBID_GLOBAL$$.requestBids.before(requestBidsHook, 50); + getGlobal().requestBids.before(requestBidsHook, 50); } addedConsentHook = true; gdprDataHandler.enable(); @@ -356,22 +376,27 @@ export function setConsentConfig(config) { } config.getConfig('consentManagement', config => setConsentConfig(config.consentManagement)); -export function setOrtbGdpr(ortbRequest, bidderRequest) { - const consent = bidderRequest.gdprConsent; - if (consent) { - if (typeof consent.gdprApplies === 'boolean') { - deepSetValue(ortbRequest, 'regs.ext.gdpr', consent.gdprApplies ? 1 : 0); +export function enrichFPDHook(next, fpd) { + return next(fpd.then(ortb2 => { + const consent = gdprDataHandler.getConsentData(); + if (consent) { + if (typeof consent.gdprApplies === 'boolean') { + deepSetValue(ortb2, 'regs.ext.gdpr', consent.gdprApplies ? 1 : 0); + } + deepSetValue(ortb2, 'user.ext.consent', consent.consentString); } - deepSetValue(ortbRequest, 'user.ext.consent', consent.consentString); - } + return ortb2; + })); } +enrichFPD.before(enrichFPDHook); + export function setOrtbAdditionalConsent(ortbRequest, bidderRequest) { + // this is not a standardized name for addtlConsent, so keep this as an ORTB library processor rather than an FPD enrichment const addtl = bidderRequest.gdprConsent?.addtlConsent; if (addtl && typeof addtl === 'string') { deepSetValue(ortbRequest, 'user.ext.ConsentedProvidersSettings.consented_providers', addtl); } } -registerOrtbProcessor({type: REQUEST, name: 'gdpr', fn: setOrtbGdpr}); registerOrtbProcessor({type: REQUEST, name: 'gdprAddtlConsent', fn: setOrtbAdditionalConsent}) diff --git a/modules/consentManagementGpp.js b/modules/consentManagementGpp.js new file mode 100644 index 00000000000..3b0e1c25c6a --- /dev/null +++ b/modules/consentManagementGpp.js @@ -0,0 +1,387 @@ +/** + * This module adds GPP consentManagement support to prebid.js. It interacts with + * supported CMPs (Consent Management Platforms) to grab the user's consent information + * and make it available for any GPP supported adapters to read/pass this information to + * their system and for various other features/modules in Prebid.js. + */ +import {deepSetValue, isNumber, isPlainObject, isStr, logError, logInfo, logWarn} from '../src/utils.js'; +import {config} from '../src/config.js'; +import {gppDataHandler} from '../src/adapterManager.js'; +import {includes} from '../src/polyfill.js'; +import {timedAuctionHook} from '../src/utils/perfMetrics.js'; +import { enrichFPD } from '../src/fpd/enrichment.js'; +import {getGlobal} from '../src/prebidGlobal.js'; + +const DEFAULT_CMP = 'iab'; +const DEFAULT_CONSENT_TIMEOUT = 10000; +const CMP_VERSION = 1; + +export let userCMP; +export let consentTimeout; +export let staticConsentData; + +let consentData; +let addedConsentHook = false; + +// add new CMPs here, with their dedicated lookup function +const cmpCallMap = { + 'iab': lookupIabConsent, + 'static': lookupStaticConsentData +}; + +/** + * This function checks the state of the IAB gppData's applicableSection field (to ensure it's populated and has a valid value). + * section === 0 represents a CMP's default value when CMP is loading, it shoud not be used a real user's section. + * + * TODO --- The initial version of the GPP CMP API spec used this naming convention, but it was later changed as an update to the spec. + * CMPs should adjust their logic to use the new format (applicableSecctions), but that may not be the case with the initial release. + * Added support just in case for this transition period, can likely be removed at a later date... + * @param gppData represents the IAB gppData object + * @returns true|false + */ +function checkApplicableSectionIsReady(gppData) { + return gppData && Array.isArray(gppData.applicableSection) && gppData.applicableSection.length > 0 && gppData.applicableSection[0] !== 0; +} + +/** + * This function checks the state of the IAB gppData's applicableSections field (to ensure it's populated and has a valid value). + * section === 0 represents a CMP's default value when CMP is loading, it shoud not be used a real user's section. + * @param gppData represents the IAB gppData object + * @returns true|false + */ +function checkApplicableSectionsIsReady(gppData) { + return gppData && Array.isArray(gppData.applicableSections) && gppData.applicableSections.length > 0 && gppData.applicableSections[0] !== 0; +} + +/** + * This function reads the consent string from the config to obtain the consent information of the user. + * @param {function({})} onSuccess acts as a success callback when the value is read from config; pass along consentObject from CMP + */ +function lookupStaticConsentData({onSuccess, onError}) { + processCmpData(staticConsentData, {onSuccess, onError}); +} + +/** + * This function handles interacting with an IAB compliant CMP to obtain the consent information of the user. + * Given the async nature of the CMP's API, we pass in acting success/error callback functions to exit this function + * based on the appropriate result. + * @param {function({})} onSuccess acts as a success callback when CMP returns a value; pass along consentObjectfrom CMP + * @param {function(string, ...{}?)} cmpError acts as an error callback while interacting with CMP; pass along an error message (string) and any extra error arguments (purely for logging) + */ +function lookupIabConsent({onSuccess, onError}) { + const cmpApiName = '__gpp'; + const cmpCallbacks = {}; + let registeredPostMessageResponseListener = false; + + function findCMP() { + let f = window; + let cmpFrame; + let cmpDirectAccess = false; + while (true) { + try { + if (typeof f[cmpApiName] === 'function') { + cmpFrame = f; + cmpDirectAccess = true; + break; + } + } catch (e) {} + + // need separate try/catch blocks due to the exception errors thrown when trying to check for a frame that doesn't exist in 3rd party env + try { + if (f.frames['__gppLocator']) { + cmpFrame = f; + break; + } + } catch (e) {} + + if (f === window.top) break; + f = f.parent; + } + + return { + cmpFrame, + cmpDirectAccess + }; + } + + const {cmpFrame, cmpDirectAccess} = findCMP(); + + if (!cmpFrame) { + return onError('GPP CMP not found.'); + } + + const invokeCMP = (cmpDirectAccess) ? invokeCMPDirect : invokeCMPFrame; + + function invokeCMPDirect({command, callback, parameter, version = CMP_VERSION}, resultCb) { + if (typeof resultCb === 'function') { + resultCb(cmpFrame[cmpApiName](command, callback, parameter, version)); + } else { + cmpFrame[cmpApiName](command, callback, parameter, version); + } + } + + function invokeCMPFrame({command, callback, parameter, version = CMP_VERSION}, resultCb) { + const callName = `${cmpApiName}Call`; + if (!registeredPostMessageResponseListener) { + // when we get the return message, call the stashed callback; + window.addEventListener('message', readPostMessageResponse, false); + registeredPostMessageResponseListener = true; + } + + // call CMP via postMessage + const callId = Math.random().toString(); + const msg = { + [callName]: { + command: command, + parameter, + version, + callId: callId + } + }; + + // TODO? - add logic to check if random was already used in the same session, and roll another if so? + cmpCallbacks[callId] = (typeof callback === 'function') ? callback : resultCb; + cmpFrame.postMessage(msg, '*'); + + function readPostMessageResponse(event) { + const cmpDataPkgName = `${cmpApiName}Return`; + const json = (typeof event.data === 'string' && event.data.includes(cmpDataPkgName)) ? JSON.parse(event.data) : event.data; + if (json[cmpDataPkgName] && json[cmpDataPkgName].callId) { + const payload = json[cmpDataPkgName]; + + if (cmpCallbacks.hasOwnProperty(payload.callId)) { + cmpCallbacks[payload.callId](payload.returnValue); + } + } + } + } + + const startupMsg = (cmpDirectAccess) ? 'Detected GPP CMP API is directly accessible, calling it now...' + : 'Detected GPP CMP is outside the current iframe where Prebid.js is located, calling it now...'; + logInfo(startupMsg); + + invokeCMP({ + command: 'addEventListener', + callback: function (evt) { + if (evt) { + logInfo(`Received a ${(cmpDirectAccess ? 'direct' : 'postmsg')} response from GPP CMP for event`, evt); + if (evt.eventName === 'sectionChange' || evt.pingData.cmpStatus === 'loaded') { + invokeCMP({command: 'getGPPData'}, function (gppData) { + logInfo(`Received a ${cmpDirectAccess ? 'direct' : 'postmsg'} response from GPP CMP for getGPPData`, gppData); + processCmpData(gppData, {onSuccess, onError}); + }); + } else if (evt.pingData.cmpStatus === 'error') { + onError('CMP returned with a cmpStatus:error response. Please check CMP setup.'); + } + } + } + }); +} + +/** + * Look up consent data and store it in the `consentData` global as well as `adapterManager.js`' gdprDataHandler. + * + * @param cb A callback that takes: a boolean that is true if the auction should be canceled; an error message and extra + * error arguments that will be undefined if there's no error. + */ +function loadConsentData(cb) { + let isDone = false; + let timer = null; + + function done(consentData, shouldCancelAuction, errMsg, ...extraArgs) { + if (timer != null) { + clearTimeout(timer); + } + isDone = true; + gppDataHandler.setConsentData(consentData); + if (typeof cb === 'function') { + cb(shouldCancelAuction, errMsg, ...extraArgs); + } + } + + if (!includes(Object.keys(cmpCallMap), userCMP)) { + done(null, false, `GPP CMP framework (${userCMP}) is not a supported framework. Aborting consentManagement module and resuming auction.`); + return; + } + + const callbacks = { + onSuccess: (data) => done(data, false), + onError: function (msg, ...extraArgs) { + done(null, true, msg, ...extraArgs); + } + } + cmpCallMap[userCMP](callbacks); + + if (!isDone) { + const onTimeout = () => { + const continueToAuction = (data) => { + done(data, false, 'GPP CMP did not load, continuing auction...'); + } + processCmpData(consentData, { + onSuccess: continueToAuction, + onError: () => continueToAuction(storeConsentData(undefined)) + }) + } + if (consentTimeout === 0) { + onTimeout(); + } else { + timer = setTimeout(onTimeout, consentTimeout); + } + } +} + +/** + * Like `loadConsentData`, but cache and re-use previously loaded data. + * @param cb + */ +function loadIfMissing(cb) { + if (consentData) { + logInfo('User consent information already known. Pulling internally stored information...'); + // eslint-disable-next-line standard/no-callback-literal + cb(false); + } else { + loadConsentData(cb); + } +} + +/** + * If consentManagement module is enabled (ie included in setConfig), this hook function will attempt to fetch the + * user's encoded consent string from the supported CMP. Once obtained, the module will store this + * data as part of a gppConsent object which gets transferred to adapterManager's gppDataHandler object. + * This information is later added into the bidRequest object for any supported adapters to read/pass along to their system. + * @param {object} reqBidsConfigObj required; This is the same param that's used in pbjs.requestBids. + * @param {function} fn required; The next function in the chain, used by hook.js + */ +export const requestBidsHook = timedAuctionHook('gpp', function requestBidsHook(fn, reqBidsConfigObj) { + loadIfMissing(function (shouldCancelAuction, errMsg, ...extraArgs) { + if (errMsg) { + let log = logWarn; + if (shouldCancelAuction) { + log = logError; + errMsg = `${errMsg} Canceling auction as per consentManagement config.`; + } + log(errMsg, ...extraArgs); + } + + if (shouldCancelAuction) { + fn.stopTiming(); + if (typeof reqBidsConfigObj.bidsBackHandler === 'function') { + reqBidsConfigObj.bidsBackHandler(); + } else { + logError('Error executing bidsBackHandler'); + } + } else { + fn.call(this, reqBidsConfigObj); + } + }); +}); + +/** + * This function checks the consent data provided by CMP to ensure it's in an expected state. + * If it's bad, we call `onError` + * If it's good, then we store the value and call `onSuccess` + */ +function processCmpData(consentObject, {onSuccess, onError}) { + function checkData() { + const gppString = consentObject && consentObject.gppString; + const gppSection = (checkApplicableSectionsIsReady(consentObject)) ? consentObject.applicableSections + : (checkApplicableSectionIsReady(consentObject)) ? consentObject.applicableSection : []; + + return !!( + (!Array.isArray(gppSection)) || + (Array.isArray(gppSection) && (!gppString || !isStr(gppString))) + ); + } + + if (checkData()) { + onError(`CMP returned unexpected value during lookup process.`, consentObject); + } else { + onSuccess(storeConsentData(consentObject)); + } +} + +/** + * Stores CMP data locally in module to make information available in adaptermanager.js for later in the auction + * @param {object} cmpConsentObject required; an object representing user's consent choices (can be undefined in certain use-cases for this function only) + */ +function storeConsentData(cmpConsentObject) { + consentData = { + gppString: (cmpConsentObject) ? cmpConsentObject.gppString : undefined, + + fullGppData: (cmpConsentObject) || undefined, + }; + consentData.applicableSections = (checkApplicableSectionsIsReady(cmpConsentObject)) ? cmpConsentObject.applicableSections + : (checkApplicableSectionIsReady(cmpConsentObject)) ? cmpConsentObject.applicableSection : []; + consentData.apiVersion = CMP_VERSION; + return consentData; +} + +/** + * Simply resets the module's consentData variable back to undefined, mainly for testing purposes + */ +export function resetConsentData() { + consentData = undefined; + userCMP = undefined; + consentTimeout = undefined; + gppDataHandler.reset(); +} + +/** + * A configuration function that initializes some module variables, as well as add a hook into the requestBids function + * @param {{cmp:string, timeout:number, allowAuctionWithoutConsent:boolean, defaultGdprScope:boolean}} config required; consentManagement module config settings; cmp (string), timeout (int), allowAuctionWithoutConsent (boolean) + */ +export function setConsentConfig(config) { + config = config && config.gpp; + if (!config || typeof config !== 'object') { + logWarn('consentManagement.gpp config not defined, exiting consent manager module'); + return; + } + + if (isStr(config.cmpApi)) { + userCMP = config.cmpApi; + } else { + userCMP = DEFAULT_CMP; + logInfo(`consentManagement.gpp config did not specify cmp. Using system default setting (${DEFAULT_CMP}).`); + } + + if (isNumber(config.timeout)) { + consentTimeout = config.timeout; + } else { + consentTimeout = DEFAULT_CONSENT_TIMEOUT; + logInfo(`consentManagement.gpp config did not specify timeout. Using system default setting (${DEFAULT_CONSENT_TIMEOUT}).`); + } + + if (userCMP === 'static') { + if (isPlainObject(config.consentData)) { + staticConsentData = config.consentData; + consentTimeout = 0; + } else { + logError(`consentManagement.gpp config with cmpApi: 'static' did not specify consentData. No consents will be available to adapters.`); + } + } + + logInfo('consentManagement.gpp module has been activated...'); + + if (!addedConsentHook) { + getGlobal().requestBids.before(requestBidsHook, 50); + } + addedConsentHook = true; + gppDataHandler.enable(); + loadConsentData(); // immediately look up consent data to make it available without requiring an auction +} +config.getConfig('consentManagement', config => setConsentConfig(config.consentManagement)); + +export function enrichFPDHook(next, fpd) { + return next(fpd.then(ortb2 => { + const consent = gppDataHandler.getConsentData(); + if (consent) { + if (Array.isArray(consent.applicableSections)) { + deepSetValue(ortb2, 'regs.gpp_sid', consent.applicableSections); + } + deepSetValue(ortb2, 'regs.gpp', consent.gppString); + } + return ortb2; + })); +} + +enrichFPD.before(enrichFPDHook); diff --git a/modules/consentManagementUsp.js b/modules/consentManagementUsp.js index 805c796312c..fcc13152aa9 100644 --- a/modules/consentManagementUsp.js +++ b/modules/consentManagementUsp.js @@ -7,9 +7,9 @@ import {deepSetValue, isFn, isNumber, isPlainObject, isStr, logError, logInfo, logWarn} from '../src/utils.js'; import {config} from '../src/config.js'; import adapterManager, {uspDataHandler} from '../src/adapterManager.js'; -import {registerOrtbProcessor, REQUEST} from '../src/pbjsORTB.js'; import {timedAuctionHook} from '../src/utils/perfMetrics.js'; import {getHook} from '../src/hook.js'; +import {enrichFPD} from '../src/fpd/enrichment.js'; const DEFAULT_CONSENT_API = 'iab'; const DEFAULT_CONSENT_TIMEOUT = 50; @@ -100,6 +100,14 @@ function lookupUspConsent({onSuccess, onError}) { return onError('USP CMP not found.'); } + function registerDataDelHandler(invoker, arg2) { + try { + invoker('registerDeletion', arg2, adapterManager.callDataDeletionRequest); + } catch (e) { + logError('Error invoking CMP `registerDeletion`:', e); + } + } + // to collect the consent information from the user, we perform a call to USPAPI // to collect the user's consent choices represented as a string (via getUSPData) @@ -115,11 +123,7 @@ function lookupUspConsent({onSuccess, onError}) { USPAPI_VERSION, callbackHandler.consentDataCallback ); - uspapiFunction( - 'registerDeletion', - USPAPI_VERSION, - adapterManager.callDataDeletionRequest - ) + registerDataDelHandler(uspapiFunction, USPAPI_VERSION); } else { logInfo( 'Detected USP CMP is outside the current iframe where Prebid.js is located, calling it now...' @@ -129,11 +133,7 @@ function lookupUspConsent({onSuccess, onError}) { uspapiFrame, callbackHandler.consentDataCallback ); - callUspApiWhileInIframe( - 'registerDeletion', - uspapiFrame, - adapterManager.callDataDeletionRequest - ); + registerDataDelHandler(callUspApiWhileInIframe, uspapiFrame); } let listening = false; @@ -323,10 +323,14 @@ config.getConfig('consentManagement', config => setConsentConfig(config.consentM getHook('requestBids').before(requestBidsHook, 50); -export function setOrtbUsp(ortbRequest, bidderRequest) { - if (bidderRequest.uspConsent) { - deepSetValue(ortbRequest, 'regs.ext.us_privacy', bidderRequest.uspConsent); - } +export function enrichFPDHook(next, fpd) { + return next(fpd.then(ortb2 => { + const consent = uspDataHandler.getConsentData(); + if (consent) { + deepSetValue(ortb2, 'regs.ext.us_privacy', consent) + } + return ortb2; + })) } -registerOrtbProcessor({type: REQUEST, name: 'usp', fn: setOrtbUsp}); +enrichFPD.before(enrichFPDHook); diff --git a/modules/consumableBidAdapter.js b/modules/consumableBidAdapter.js index c91f1a7f906..4e2a92fb594 100644 --- a/modules/consumableBidAdapter.js +++ b/modules/consumableBidAdapter.js @@ -180,12 +180,26 @@ export const spec = { return bidResponses; }, - getUserSyncs: function(syncOptions, serverResponses) { + getUserSyncs: function(syncOptions, serverResponses, gdprConsent, uspConsent) { + let syncUrl = 'https://sync.serverbid.com/ss/' + siteId + '.html'; + if (syncOptions.iframeEnabled) { + if (gdprConsent && gdprConsent.consentString) { + if (typeof gdprConsent.gdprApplies === 'boolean') { + syncUrl = appendUrlParam(syncUrl, `gdpr=${Number(gdprConsent.gdprApplies)}&gdpr_consent=${gdprConsent.consentString}`); + } else { + syncUrl = appendUrlParam(syncUrl, `gdpr=0&gdpr_consent=${gdprConsent.consentString}`); + } + } + + if (uspConsent && uspConsent.consentString) { + syncUrl = appendUrlParam(syncUrl, `us_privacy=${uspConsent.consentString}`); + } + if (!serverResponses || serverResponses.length === 0 || !serverResponses[0].body.bdr || serverResponses[0].body.bdr !== 'cx') { return [{ type: 'iframe', - url: 'https://sync.serverbid.com/ss/' + siteId + '.html' + url: syncUrl }]; } } @@ -294,4 +308,8 @@ function getBidFloor(bid, sizes) { return floor; } +function appendUrlParam(url, queryString) { + return `${url}${url.indexOf('?') > -1 ? '&' : '?'}${queryString}`; +} + registerBidder(spec); diff --git a/modules/contentexchangeBidAdapter.js b/modules/contentexchangeBidAdapter.js index becc21555e3..be5900407ea 100644 --- a/modules/contentexchangeBidAdapter.js +++ b/modules/contentexchangeBidAdapter.js @@ -157,7 +157,7 @@ export const spec = { coppa: config.getConfig('coppa') === true ? 1 : 0, ccpa: bidderRequest.uspConsent || undefined, gdpr: bidderRequest.gdprConsent || undefined, - tmax: config.getConfig('bidderTimeout') + tmax: bidderRequest.timeout }; const len = validBidRequests.length; diff --git a/modules/conversantAnalyticsAdapter.js b/modules/conversantAnalyticsAdapter.js index 6e88f403bae..e934f1bb153 100644 --- a/modules/conversantAnalyticsAdapter.js +++ b/modules/conversantAnalyticsAdapter.js @@ -3,20 +3,25 @@ import adapter from '../libraries/analyticsAdapter/AnalyticsAdapter.js'; import CONSTANTS from '../src/constants.json'; import {getGlobal} from '../src/prebidGlobal.js'; import adapterManager from '../src/adapterManager.js'; -import {logInfo, logError, logMessage, deepAccess, isInteger} from '../src/utils.js'; +import {logInfo, logWarn, logError, logMessage, deepAccess, isInteger} from '../src/utils.js'; +import {getRefererInfo} from '../src/refererDetection.js'; const { - EVENTS: { AUCTION_END, AD_RENDER_FAILED, BID_TIMEOUT, BID_WON } + EVENTS: { AUCTION_END, AD_RENDER_FAILED, BID_TIMEOUT, BID_WON, BIDDER_ERROR } } = CONSTANTS; +// STALE_RENDER, TCF2_ENFORCEMENT would need to add extra calls for these as they likely occur after AUCTION_END? const GVLID = 24; const ANALYTICS_TYPE = 'endpoint'; // for local testing set domain to 127.0.0.1:8290 -const URL = 'https://web.hb.ad.cpe.dotomi.com/cvx/event/prebidanalytics'; +const DOMAIN = 'https://web.hb.ad.cpe.dotomi.com/'; +const ANALYTICS_URL = DOMAIN + 'cvx/event/prebidanalytics'; +const ERROR_URL = DOMAIN + 'cvx/event/prebidanalyticerrors'; const ANALYTICS_CODE = 'conversant'; export const CNVR_CONSTANTS = { LOG_PREFIX: 'Conversant analytics adapter: ', + ERROR_MISSING_DATA_PREFIX: 'Parsing method failed because of missing data: ', // Maximum time to keep an item in the cache before it gets purged MAX_MILLISECONDS_IN_CACHE: 30000, // How often cache cleanup will run @@ -41,6 +46,7 @@ let conversantAnalyticsEnabled = false; export const cnvrHelper = { // Turns on sampling for an instance of prebid analytics. doSample: true, + doSendErrorData: false, /** * Used to hold data for RENDER FAILED events so we can send a payload back that will match our original auction data. @@ -67,7 +73,12 @@ export const cnvrHelper = { /** * Lookup of auction IDs to auction start timestamps */ - auctionIdTimestampCache: {} + auctionIdTimestampCache: {}, + + /** + * Capture any bidder errors and bundle them with AUCTION_END + */ + bidderErrorCache: {} }; /** @@ -77,33 +88,99 @@ export const cnvrHelper = { let cacheCleanupInterval; let conversantAnalytics = Object.assign( - adapter({URL, ANALYTICS_TYPE}), + adapter({URL: ANALYTICS_URL, ANALYTICS_TYPE}), { track({eventType, args}) { - if (cnvrHelper.doSample) { - logMessage(CNVR_CONSTANTS.LOG_PREFIX + ' track(): ' + eventType, args); - switch (eventType) { - case AUCTION_END: - onAuctionEnd(args); - break; - case AD_RENDER_FAILED: - onAdRenderFailed(args); - break; - case BID_WON: - onBidWon(args); - break; - case BID_TIMEOUT: - onBidTimeout(args); - break; - } // END switch - } else { - logMessage(CNVR_CONSTANTS.LOG_PREFIX + ' - ' + eventType + ': skipped due to sampling'); - }// END IF(cnvrHelper.doSample) + try { + if (cnvrHelper.doSample) { + logMessage(CNVR_CONSTANTS.LOG_PREFIX + ' track(): ' + eventType, args); + switch (eventType) { + case AUCTION_END: + onAuctionEnd(args); + break; + case AD_RENDER_FAILED: + onAdRenderFailed(args); + break; + case BID_WON: + onBidWon(args); + break; + case BID_TIMEOUT: + onBidTimeout(args); + break; + case BIDDER_ERROR: + onBidderError(args) + } // END switch + } else { + logMessage(CNVR_CONSTANTS.LOG_PREFIX + ' - ' + eventType + ': skipped due to sampling'); + }// END IF(cnvrHelper.doSample) + } catch (e) { + // e = {stack:"...",message:"..."} + logError(CNVR_CONSTANTS.LOG_PREFIX + 'Caught error in handling ' + eventType + ' event: ' + e.message); + cnvrHelper.sendErrorData(eventType, e); + } } // END track() } ); // ================================================== EVENT HANDLERS =================================================== + +/** + * Handler for BIDDER_ERROR events, tries to capture as much data, save it in cache which is then picked up by + * AUCTION_END event and included in that payload. Was not able to see an easy way to get adUnitCode in this event + * so not including it for now. + * https://docs.prebid.org/dev-docs/bidder-adaptor.html#registering-on-bidder-error + * Trigger when the HTTP response status code is not between 200-299 and not equal to 304. + { + error: XMLHttpRequest, https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest + bidderRequest: { https://docs.prebid.org/dev-docs/bidder-adaptor.html#registering-on-bidder-error + { + auctionId: "b06c5141-fe8f-4cdf-9d7d-54415490a917", + auctionStart: 1579746300522, + bidderCode: "myBidderCode", + bidderRequestId: "15246a574e859f", + bids: [{...}], + gdprConsent: {consentString: "BOtmiBKOtmiBKABABAENAFAAAAACeAAA", vendorData: {...}, gdprApplies: true}, + refererInfo: { + canonicalUrl: null, + page: "http://mypage.org?pbjs_debug=true", + domain: "mypage.org", + ref: null, + numIframes: 0, + reachedTop: true, + isAmp: false, + stack: ["http://mypage.org?pbjs_debug=true"] + } + } + } +} + */ +function onBidderError(args) { + if (!cnvrHelper.doSendErrorData) { + logWarn(CNVR_CONSTANTS.LOG_PREFIX + 'Skipping bidder error parsing due to config disabling error logging, bidder error status = ' + args.error.status + ', Message = ' + args.error.statusText); + return; + } + + let error = args.error; + let bidRequest = args.bidderRequest; + let auctionId = bidRequest.auctionId; + let bidderCode = bidRequest.bidderCode; + logWarn(CNVR_CONSTANTS.LOG_PREFIX + 'onBidderError(): error received from bidder ' + bidderCode + '. Status = ' + error.status + ', Message = ' + error.statusText); + let errorObj = { + status: error.status, + message: error.statusText, + bidderCode: bidderCode, + url: cnvrHelper.getPageUrl(), + }; + if (cnvrHelper.bidderErrorCache[auctionId]) { + cnvrHelper.bidderErrorCache[auctionId]['errors'].push(errorObj); + } else { + cnvrHelper.bidderErrorCache[auctionId] = { + errors: [errorObj], + timeReceived: Date.now() + }; + } +} + /** * We get the list of timeouts before the endAution, cache them temporarily in a global cache and the endAuction event * will pick them up. Uses getLookupKey() to create the key to the entry from auctionId, adUnitCode and bidderCode. @@ -145,8 +222,13 @@ function onBidWon(args) { // Make sure we have all the data we need if (!bidderCode || !adUnitCode || !auctionId) { - logError(CNVR_CONSTANTS.LOG_PREFIX + 'onBidWon() did not get all the necessary data to process the event.'); - return; + let errorReason = 'auction id'; + if (!bidderCode) { + errorReason = 'bidder code'; + } else if (!adUnitCode) { + errorReason = 'ad unit code' + } + throw new Error(CNVR_CONSTANTS.ERROR_MISSING_DATA_PREFIX + errorReason); } if (cnvrHelper.auctionIdTimestampCache[auctionId]) { @@ -193,8 +275,12 @@ function onAdRenderFailed(args) { // Make sure we have all the data we need, adId is optional so it's not guaranteed, without that we can't match it up // to our adIdLookup data. if (!adId || !cnvrHelper.adIdLookup[adId]) { - logError(CNVR_CONSTANTS.LOG_PREFIX + "onAdRenderFailed(): Unable to process RENDER FAILED because adId is missing or doesn't match a record in our cache."); - return; // Either no adId to match against a bidWon event, or no data saved from a bidWon event that matches the adId + let errorMsg = 'ad id'; + if (adId) { + errorMsg = 'no lookup data for ad id'; + } + // Either no adId to match against a bidWon event, or no data saved from a bidWon event that matches the adId + throw new Error(CNVR_CONSTANTS.ERROR_MISSING_DATA_PREFIX + errorMsg); } const adIdObj = cnvrHelper.adIdLookup[adId]; const adUnitCode = adIdObj['adUnitCode']; @@ -203,8 +289,13 @@ function onAdRenderFailed(args) { delete cnvrHelper.adIdLookup[adId]; // cleanup our cache if (!bidderCode || !adUnitCode || !auctionId) { - logError(CNVR_CONSTANTS.LOG_PREFIX + 'onAdRenderFailed(): Unable to process RENDER FAILED because lookup cache did not have all the data we required.'); - return; + let errorReason = 'auction id'; + if (!bidderCode) { + errorReason = 'bidder code'; + } else if (!adUnitCode) { + errorReason = 'ad unit code' + } + throw new Error(CNVR_CONSTANTS.ERROR_MISSING_DATA_PREFIX + errorReason); } let timestamp = Date.now(); @@ -230,8 +321,7 @@ function onAdRenderFailed(args) { function onAuctionEnd(args) { const auctionId = args.auctionId; if (!auctionId) { - logError(CNVR_CONSTANTS.LOG_PREFIX + 'onAuctionEnd(): No auctionId in args supplied so unable to process event.'); - return; + throw new Error(CNVR_CONSTANTS.ERROR_MISSING_DATA_PREFIX + 'auction id'); } const auctionTimestamp = args.timestamp ? args.timestamp : Date.now(); @@ -240,8 +330,13 @@ function onAuctionEnd(args) { const auctionEndPayload = cnvrHelper.createPayload('auction_end', auctionId, auctionTimestamp); // Get bid request information from adUnits if (!Array.isArray(args.adUnits)) { - logError(CNVR_CONSTANTS.LOG_PREFIX + 'onAuctionEnd(): adUnits not defined in arguments.'); - return; + throw new Error(CNVR_CONSTANTS.ERROR_MISSING_DATA_PREFIX + 'no adUnits in event args'); + } + + // Write out any bid errors + if (cnvrHelper.bidderErrorCache[auctionId]) { + auctionEndPayload.bidderErrors = cnvrHelper.bidderErrorCache[auctionId].errors; + delete cnvrHelper.bidderErrorCache[auctionId]; } args.adUnits.forEach(adUnit => { @@ -298,7 +393,7 @@ function onAuctionEnd(args) { } }); } else { - logError(CNVR_CONSTANTS.LOG_PREFIX + 'onAuctionEnd(): noBids not defined in arguments.'); + logWarn(CNVR_CONSTANTS.LOG_PREFIX + 'onAuctionEnd(): noBids not defined in arguments.'); } // Get bid data from bids sent @@ -321,7 +416,7 @@ function onAuctionEnd(args) { } }); } else { - logError(CNVR_CONSTANTS.LOG_PREFIX + 'onAuctionEnd(): bidsReceived not defined in arguments.'); + logWarn(CNVR_CONSTANTS.LOG_PREFIX + 'onAuctionEnd(): bidsReceived not defined in arguments.'); } // We need to remove any duplicate ad sizes from merging ad-slots or overlap in different media types and also // media-types from merged ad-slots in twin bids. @@ -423,7 +518,8 @@ cnvrHelper.createPayload = function(payloadType, auctionId, timestamp) { sid: initOptions.site_id, auctionTimestamp: timestamp }, - adUnits: {} + adUnits: {}, + bidderErrors: [] }; }; @@ -488,13 +584,49 @@ cnvrHelper.getSampleRate = function(parentObj, propNm, defaultSampleRate) { return sampleRate; } +/** + * Helper to encapsulate logic for getting best known page url. Small but helpful in debugging/testing and if we ever want + * to add more logic to this. + * + * From getRefererInfo(): page = the best candidate for the current page URL: `canonicalUrl`, falling back to `location` + * @returns {*} Best guess at top URL based on logic from RefererInfo. + */ +cnvrHelper.getPageUrl = function() { + return getRefererInfo().page; +} + +/** + * Packages up an error that occured in analytics handling and sends it back to our servers for logging + * @param eventType = original event that was fired + * @param exception = {stack:"...",message:"..."}, exception that was triggered + */ +cnvrHelper.sendErrorData = function(eventType, exception) { + if (!cnvrHelper.doSendErrorData) { + logWarn(CNVR_CONSTANTS.LOG_PREFIX + 'Skipping sending error data due to config disabling error logging, error thrown = ' + exception); + return; + } + + let error = { + event: eventType, + siteId: initOptions.site_id, + message: exception.message, + stack: exception.stack, + prebidVersion: '$$REPO_AND_VERSION$$', // testing val sample: prebid_prebid_7.27.0-pre' + userAgent: navigator.userAgent, + url: cnvrHelper.getPageUrl() + }; + + // eslint-disable-next-line no-undef + ajax(ERROR_URL, function () {}, JSON.stringify(error), {contentType: 'text/plain'}); +} + /** * Helper function to send data back to server. Need to make sure we don't trigger a CORS preflight by not adding * extra header params. * @param payload our JSON payload from either AUCTION END, BID WIN, RENDER FAILED */ function sendData(payload) { - ajax(URL, function () {}, JSON.stringify(payload), {contentType: 'text/plain'}); + ajax(ANALYTICS_URL, function () {}, JSON.stringify(payload), {contentType: 'text/plain'}); } // =============================== BOILERPLATE FOR PRE-BID ANALYTICS SETUP ============================================ @@ -515,6 +647,7 @@ conversantAnalytics.enableAnalytics = function (config) { cnvrHelper.cleanCache(cnvrHelper.adIdLookup, currTime); cnvrHelper.cleanCache(cnvrHelper.timeoutCache, currTime); cnvrHelper.cleanCache(cnvrHelper.auctionIdTimestampCache, currTime); + cnvrHelper.cleanCache(cnvrHelper.bidderErrorCache, currTime); }, CNVR_CONSTANTS.CACHE_CLEANUP_TIME_IN_MILLIS ); @@ -529,6 +662,10 @@ conversantAnalytics.enableAnalytics = function (config) { // Math.random() pseudo-random number in the range 0 to less than 1 (inclusive of 0, but not 1) cnvrHelper.doSample = Math.random() < initOptions.cnvr_sample_rate; + if (initOptions.send_error_data !== undefined && initOptions.send_error_data !== null) { + cnvrHelper.doSendErrorData = !!initOptions.send_error_data; // Forces data into boolean type + } + conversantAnalyticsEnabled = true; conversantAnalytics.originEnableAnalytics(config); // call the base class function }; @@ -546,6 +683,7 @@ conversantAnalytics.disableAnalytics = function () { cnvrHelper.timeoutCache = {}; cnvrHelper.adIdLookup = {}; cnvrHelper.auctionIdTimestampCache = {}; + cnvrHelper.bidderErrorCache = {}; conversantAnalyticsEnabled = false; conversantAnalytics.originDisableAnalytics(); diff --git a/modules/conversantBidAdapter.js b/modules/conversantBidAdapter.js index 41a8200c6ea..c2ffba8bb48 100644 --- a/modules/conversantBidAdapter.js +++ b/modules/conversantBidAdapter.js @@ -142,6 +142,10 @@ export const spec = { } if (bidderRequest) { + if (bidderRequest.timeout) { + deepSetValue(payload, 'tmax', bidderRequest.timeout); + } + // Add GDPR flag and consent string if (bidderRequest.gdprConsent) { userExt.consent = bidderRequest.gdprConsent.consentString; diff --git a/modules/cpexIdSystem.md b/modules/cpexIdSystem.md deleted file mode 100644 index 8aceb7fe4ec..00000000000 --- a/modules/cpexIdSystem.md +++ /dev/null @@ -1,27 +0,0 @@ -## CPEx User ID Submodule - -CPExID is provided by [Czech Publisher Exchange](https://www.cpex.cz/), or CPEx. It is a user ID for ad targeting by using first party cookie, or localStorage mechanism. Please contact CPEx before using this ID. - -## Building Prebid with CPExID Support - -First, make sure to add the cpexId to your Prebid.js package with: - -``` -gulp build --modules=cpexIdSystem -``` - -The following configuration parameters are available: - -```javascript -pbjs.setConfig({ - userSync: { - userIds: [{ - name: 'cpexId' - }] - } -}); -``` - -| Param under userSync.userIds[] | Scope | Type | Description | Example | -| --- | --- | --- | --- | --- | -| name | Required | String | The name of this module. | `"cpexId"` | diff --git a/modules/craftBidAdapter.js b/modules/craftBidAdapter.js index 519c4dd9b6f..8ffdabcb597 100644 --- a/modules/craftBidAdapter.js +++ b/modules/craftBidAdapter.js @@ -33,9 +33,9 @@ export const spec = { buildRequests: function(bidRequests, bidderRequest) { // convert Native ORTB definition to old-style prebid native definition bidRequests = convertOrtbRequestToProprietaryNative(bidRequests); - + const bidRequest = bidRequests[0]; const tags = bidRequests.map(bidToTag); - const schain = bidRequests[0].schain; + const schain = bidRequest.schain; const payload = { tags: [...tags], ua: navigator.userAgent, @@ -44,26 +44,31 @@ export const spec = { }, schain: schain }; - if (bidderRequest && bidderRequest.gdprConsent) { - payload.gdpr_consent = { - consent_string: bidderRequest.gdprConsent.consentString, - consent_required: bidderRequest.gdprConsent.gdprApplies - }; - } - if (bidderRequest && bidderRequest.uspConsent) { - payload.us_privacy = bidderRequest.uspConsent; - } - if (bidderRequest && bidderRequest.refererInfo) { - let refererinfo = { - // TODO: this collects everything it finds, except for the canonical URL - rd_ref: bidderRequest.refererInfo.topmostLocation, - rd_top: bidderRequest.refererInfo.reachedTop, - rd_ifs: bidderRequest.refererInfo.numIframes, - }; - if (bidderRequest.refererInfo.stack) { - refererinfo.rd_stk = bidderRequest.refererInfo.stack.join(','); + if (bidderRequest) { + if (bidderRequest.gdprConsent) { + payload.gdpr_consent = { + consent_string: bidderRequest.gdprConsent.consentString, + consent_required: bidderRequest.gdprConsent.gdprApplies + }; + } + if (bidderRequest.uspConsent) { + payload.us_privacy = bidderRequest.uspConsent; + } + if (bidderRequest.refererInfo) { + let refererinfo = { + // TODO: this collects everything it finds, except for the canonical URL + rd_ref: bidderRequest.refererInfo.topmostLocation, + rd_top: bidderRequest.refererInfo.reachedTop, + rd_ifs: bidderRequest.refererInfo.numIframes, + }; + if (bidderRequest.refererInfo.stack) { + refererinfo.rd_stk = bidderRequest.refererInfo.stack.join(','); + } + payload.referrer_detection = refererinfo; + } + if (bidRequest.userId) { + payload.userId = bidRequest.userId } - payload.referrer_detection = refererinfo; } const request = formatRequest(payload, bidderRequest); return request; @@ -106,7 +111,7 @@ export const spec = { params = convertTypes({ 'sitekey': 'string', 'placementId': 'string', - 'keywords': transformBidderParamKeywords + 'keywords': transformBidderParamKeywords, }, params); if (isOpenRtb) { if (isPopulatedArray(params.keywords)) { @@ -148,11 +153,11 @@ function formatRequest(payload, bidderRequest) { withCredentials: false }; } - + const baseUrl = payload.tags[0].url || URL_BASE; const payloadString = JSON.stringify(payload); return { method: 'POST', - url: `${URL_BASE}/${payload.tags[0].sitekey}`, + url: `${baseUrl}/${payload.tags[0].sitekey}`, data: payloadString, bidderRequest, options diff --git a/modules/criteoBidAdapter.js b/modules/criteoBidAdapter.js index 5567103db69..a27f77349bf 100644 --- a/modules/criteoBidAdapter.js +++ b/modules/criteoBidAdapter.js @@ -11,12 +11,12 @@ import { getRefererInfo } from '../src/refererDetection.js'; import { hasPurpose1Consent } from '../src/utils/gpdr.js'; const GVLID = 91; -export const ADAPTER_VERSION = 34; +export const ADAPTER_VERSION = 35; const BIDDER_CODE = 'criteo'; const CDB_ENDPOINT = 'https://bidder.criteo.com/cdb'; const PROFILE_ID_INLINE = 207; export const PROFILE_ID_PUBLISHERTAG = 185; -export const storage = getStorageManager({ gvlid: GVLID, bidderCode: BIDDER_CODE }); +export const storage = getStorageManager({ bidderCode: BIDDER_CODE }); const LOG_PREFIX = 'Criteo: '; /* @@ -27,16 +27,13 @@ const LOG_PREFIX = 'Criteo: '; Unminified source code can be found in the privately shared repo: https://github.com/Prebid-org/prebid-js-external-js-criteo/blob/master/dist/prod.js */ const FAST_BID_VERSION_PLACEHOLDER = '%FAST_BID_VERSION%'; -export const FAST_BID_VERSION_CURRENT = 132; +export const FAST_BID_VERSION_CURRENT = 135; const FAST_BID_VERSION_LATEST = 'latest'; const FAST_BID_VERSION_NONE = 'none'; const PUBLISHER_TAG_URL_TEMPLATE = 'https://static.criteo.net/js/ld/publishertag.prebid' + FAST_BID_VERSION_PLACEHOLDER + '.js'; const FAST_BID_PUBKEY_E = 65537; const FAST_BID_PUBKEY_N = 'ztQYwCE5BU7T9CDM5he6rKoabstXRmkzx54zFPZkWbK530dwtLBDeaWBMxHBUT55CYyboR/EZ4efghPi3CoNGfGWezpjko9P6p2EwGArtHEeS4slhu/SpSIFMjG6fdrpRoNuIAMhq1Z+Pr/+HOd1pThFKeGFr2/NhtAg+TXAzaU='; -const SID_COOKIE_NAME = 'cto_sid'; -const IDCPY_COOKIE_NAME = 'cto_idcpy'; -const LWID_COOKIE_NAME = 'cto_lwid'; const OPTOUT_COOKIE_NAME = 'cto_optout'; const BUNDLE_COOKIE_NAME = 'cto_bundle'; const GUID_RETENTION_TIME_HOUR = 24 * 30 * 13; // 13 months @@ -78,15 +75,12 @@ export const spec = { const jsonHash = { bundle: readFromAllStorages(BUNDLE_COOKIE_NAME), cw: storage.cookiesAreEnabled(), - localWebId: readFromAllStorages(LWID_COOKIE_NAME), lsw: storage.localStorageIsEnabled(), optoutCookie: readFromAllStorages(OPTOUT_COOKIE_NAME), origin: origin, requestId: requestId, - secureIdCookie: readFromAllStorages(SID_COOKIE_NAME), tld: refererInfo.domain, topUrl: refererInfo.domain, - uid: readFromAllStorages(IDCPY_COOKIE_NAME), version: '$prebid.version$'.replace(/\./g, '_'), }; @@ -106,26 +100,13 @@ export const spec = { const response = event.data; if (response.optout) { - deleteFromAllStorages(IDCPY_COOKIE_NAME); - deleteFromAllStorages(SID_COOKIE_NAME); deleteFromAllStorages(BUNDLE_COOKIE_NAME); - deleteFromAllStorages(LWID_COOKIE_NAME); saveOnAllStorages(OPTOUT_COOKIE_NAME, true, OPTOUT_RETENTION_TIME_HOUR); } else { - if (response.uid) { - saveOnAllStorages(IDCPY_COOKIE_NAME, response.uid, GUID_RETENTION_TIME_HOUR); - } - if (response.bundle) { saveOnAllStorages(BUNDLE_COOKIE_NAME, response.bundle, GUID_RETENTION_TIME_HOUR); } - - if (response.removeSid) { - deleteFromAllStorages(SID_COOKIE_NAME); - } else if (response.sid) { - saveOnAllStorages(SID_COOKIE_NAME, response.sid, GUID_RETENTION_TIME_HOUR); - } } }, true); @@ -242,7 +223,7 @@ export const spec = { creativeId: slot.creativecode, width: slot.width, height: slot.height, - dealId: slot.dealCode, + dealId: slot.deal, }; if (body.ext?.paf?.transmission && slot.ext?.paf?.content_id) { const pafResponseMeta = { @@ -252,7 +233,7 @@ export const spec = { bid.meta = Object.assign({}, bid.meta, { paf: pafResponseMeta }); } if (slot.adomain) { - bid.meta = Object.assign({}, bid.meta, { advertiserDomains: slot.adomain }); + bid.meta = Object.assign({}, bid.meta, { advertiserDomains: [slot.adomain].flat() }); } if (slot.native) { if (bidRequest.params.nativeCallback) { @@ -407,16 +388,6 @@ function buildCdbUrl(context) { url += `&optout=1`; } - const sid = readFromAllStorages(SID_COOKIE_NAME); - if (sid) { - url += `&sid=${sid}`; - } - - const idcpy = readFromAllStorages(IDCPY_COOKIE_NAME); - if (idcpy) { - url += `&idcpy=${idcpy}`; - } - return url; } @@ -447,7 +418,9 @@ function buildCdbRequest(context, bidRequests, bidderRequest) { ext: bidderRequest.publisherExt, }, regs: { - coppa: bidderRequest.coppa === true ? 1 : (bidderRequest.coppa === false ? 0 : undefined) + coppa: bidderRequest.coppa === true ? 1 : (bidderRequest.coppa === false ? 0 : undefined), + gpp: bidderRequest.ortb2?.regs?.gpp, + gpp_sid: bidderRequest.ortb2?.regs?.gpp_sid }, slots: bidRequests.map(bidRequest => { networkId = bidRequest.params.networkId || networkId; @@ -523,9 +496,8 @@ function buildCdbRequest(context, bidRequests, bidderRequest) { } }; }; - request.user = { - ext: bidderRequest.userExt - }; + request.user = bidderRequest.ortb2?.user || {}; + request.site = bidderRequest.ortb2?.site || {}; if (bidderRequest && bidderRequest.ceh) { request.user.ceh = bidderRequest.ceh; } @@ -542,6 +514,10 @@ function buildCdbRequest(context, bidRequests, bidderRequest) { if (bidderRequest && bidderRequest.uspConsent) { request.user.uspIab = bidderRequest.uspConsent; } + if (bidderRequest && bidderRequest.ortb2?.device?.sua) { + request.user.ext = request.user.ext || {}; + request.user.ext.sua = bidderRequest.ortb2?.device?.sua || {}; + } return request; } diff --git a/modules/criteoIdSystem.js b/modules/criteoIdSystem.js index 867e4315945..e379fde0c5f 100644 --- a/modules/criteoIdSystem.js +++ b/modules/criteoIdSystem.js @@ -9,11 +9,12 @@ import { timestamp, parseUrl, triggerPixel, logError } from '../src/utils.js'; import { ajax } from '../src/ajax.js'; import { getRefererInfo } from '../src/refererDetection.js'; import { submodule } from '../src/hook.js'; -import { getStorageManager } from '../src/storageManager.js'; +import {getStorageManager} from '../src/storageManager.js'; +import {MODULE_TYPE_UID} from '../src/activities/modules.js'; const gvlid = 91; const bidderCode = 'criteo'; -export const storage = getStorageManager({ gvlid: gvlid, moduleName: bidderCode }); +export const storage = getStorageManager({moduleType: MODULE_TYPE_UID, moduleName: bidderCode}); const bididStorageKey = 'cto_bidid'; const bundleStorageKey = 'cto_bundle'; @@ -102,6 +103,9 @@ function callSyncPixel(domain, pixel) { saveOnAllStorages(pixel.storageKeyName, jsonResponse[pixel.bundlePropertyName], domain); } } + }, + error: error => { + logError(`criteoIdSystem: unable to sync user id`, error); } }, undefined, diff --git a/modules/cwireBidAdapter.js b/modules/cwireBidAdapter.js index a11899609bc..604d7235d0f 100644 --- a/modules/cwireBidAdapter.js +++ b/modules/cwireBidAdapter.js @@ -1,307 +1,234 @@ import {registerBidder} from '../src/adapters/bidderFactory.js'; import {getStorageManager} from '../src/storageManager.js'; -import {BANNER, VIDEO} from '../src/mediaTypes.js'; -import {OUTSTREAM} from '../src/video.js'; -import { - deepAccess, - generateUUID, - getBidIdParameter, - getParameterByName, - getValue, - isArray, - isNumber, - isStr, - logError, - logWarn, - parseSizesInput, -} from '../src/utils.js'; -import {Renderer} from '../src/Renderer.js'; -import {find} from '../src/polyfill.js'; +import {BANNER} from '../src/mediaTypes.js'; +import {generateUUID, getParameterByName, isNumber, logError, logInfo} from '../src/utils.js'; // ------------------------------------ const BIDDER_CODE = 'cwire'; -export const ENDPOINT_URL = 'https://embed.cwi.re/delivery/prebid'; -export const RENDERER_URL = 'https://cdn.cwi.re/prebid/renderer/LATEST/renderer.min.js'; -// ------------------------------------ -export const CW_PAGE_VIEW_ID = generateUUID(); -const LS_CWID_KEY = 'cw_cwid'; -const CW_GROUPS_QUERY = 'cwgroups'; -const CW_CREATIVE_QUERY = 'cwcreative'; +const CWID_KEY = 'cw_cwid'; -const storage = getStorageManager({bidderCode: BIDDER_CODE}); +export const BID_ENDPOINT = 'https://prebid.cwi.re/v1/bid'; +export const EVENT_ENDPOINT = 'https://prebid.cwi.re/v1/event'; /** - * ------------------------------------ - * ------------------------------------ - * @param bid - * @returns {Array} + * Allows limiting ad impressions per site render. Unique per prebid instance ID. */ -export function getSlotSizes(bid) { - return parseSizesInput(getAllMediaSizes(bid)); -} +export const pageViewId = generateUUID(); + +export const storage = getStorageManager({bidderCode: BIDDER_CODE}); /** - * ------------------------------------ - * ------------------------------------ + * Retrieve dimensions and CSS max height/width from a given slot and attach the properties to the bidRequest. * @param bid - * @returns {*[]} + * @returns {*&{cwExt: {dimensions: {width: number, height: number}, style: {maxWidth: number, maxHeight: number}}}} */ -export function getAllMediaSizes(bid) { - let playerSizes = deepAccess(bid, 'mediaTypes.video.playerSize'); - let videoSizes = deepAccess(bid, 'mediaTypes.video.sizes'); - let bannerSizes = deepAccess(bid, 'mediaTypes.banner.sizes'); - - const sizes = []; - - if (isArray(playerSizes)) { - playerSizes.forEach((s) => { - sizes.push(s); - }) - } - - if (isArray(videoSizes)) { - videoSizes.forEach((s) => { - sizes.push(s); - }) +function slotDimensions(bid) { + let adUnitCode = bid.adUnitCode; + let slotEl = document.getElementById(adUnitCode); + + if (slotEl) { + logInfo(`Slot element found: ${adUnitCode}`) + const slotW = slotEl.offsetWidth + const slotH = slotEl.offsetHeight + const cssMaxW = slotEl.style?.maxWidth; + const cssMaxH = slotEl.style?.maxHeight; + logInfo(`Slot dimensions (w/h): ${slotW} / ${slotH}`) + logInfo(`Slot Styles (maxW/maxH): ${cssMaxW} / ${cssMaxH}`) + + bid = { + ...bid, + cwExt: { + dimensions: { + width: slotW, + height: slotH, + }, + style: { + ...(cssMaxW) && { + maxWidth: cssMaxW + }, + ...(cssMaxH) && { + maxHeight: cssMaxH + } + } + } + } } + return bid +} - if (isArray(bannerSizes)) { - bannerSizes.forEach((s) => { - sizes.push(s); - }) +/** + * Extracts feature flags from a comma-separated url parameter `cwfeatures`. + * + * @returns *[] + */ +function getFeatureFlags() { + let ffParam = getParameterByName('cwfeatures') + if (ffParam) { + return ffParam.split(',') } - return sizes; + return [] } -const getQueryVariable = (variable) => { - let value = getParameterByName(variable); - if (value === '') { - value = null; +function getRefGroups() { + const groups = getParameterByName('cwgroups') + if (groups) { + return groups.split(',') } - return value; -}; + return [] +} /** - * ------------------------------------ - * ------------------------------------ - * @param validBidRequests - * @returns {*[]} + * Reads the CWID from local storage. */ -export const mapSlotsData = function(validBidRequests) { - const slots = []; - validBidRequests.forEach(bid => { - const bidObj = {}; - // get testing / debug params - let cwcreative = getValue(bid.params, 'cwcreative'); - let refgroups = getValue(bid.params, 'refgroups'); - let cwapikey = getValue(bid.params, 'cwapikey'); +function getCwid() { + return storage.localStorageIsEnabled() ? storage.getDataFromLocalStorage(CWID_KEY) : null; +} - // get the pacement and page ids - let placementId = getValue(bid.params, 'placementId'); - let pageId = getValue(bid.params, 'pageId'); - // get the rest of the auction/bid/transaction info - bidObj.auctionId = getBidIdParameter('auctionId', bid); - bidObj.adUnitCode = getBidIdParameter('adUnitCode', bid); - bidObj.bidId = getBidIdParameter('bidId', bid); - bidObj.bidderRequestId = getBidIdParameter('bidderRequestId', bid); - bidObj.placementId = placementId; - bidObj.pageId = pageId; - bidObj.mediaTypes = getBidIdParameter('mediaTypes', bid); - bidObj.transactionId = getBidIdParameter('transactionId', bid); - bidObj.sizes = getSlotSizes(bid); - bidObj.cwcreative = cwcreative; - bidObj.refgroups = refgroups; - bidObj.cwapikey = cwapikey; - slots.push(bidObj); - }); +function hasCwid() { + return storage.localStorageIsEnabled() && storage.getDataFromLocalStorage(CWID_KEY); +} - return slots; -}; +/** + * Store the CWID to local storage. + */ +function updateCwid(cwid) { + if (storage.localStorageIsEnabled()) { + storage.setDataInLocalStorage(CWID_KEY, cwid) + } else { + logInfo(`Could not set CWID ${cwid} in localstorage`); + } +} + +/** + * Extract and collect cwire specific extensions. + */ +function getCwExtension() { + const cwId = getCwid(); + const cwCreative = getParameterByName('cwcreative') + const cwGroups = getRefGroups() + const cwFeatures = getFeatureFlags(); + // Enable debug flag by passing ?cwdebug=true as url parameter. + // Note: pbjs_debug=true enables it on prebid level + // More info: https://docs.prebid.org/troubleshooting/troubleshooting-guide.html#turn-on-prebidjs-debug-messages + const debug = getParameterByName('cwdebug'); + + return { + ...(cwId) && { + cwid: cwId + }, + ...(cwGroups.length > 0) && { + refgroups: cwGroups + }, + ...(cwFeatures.length > 0) && { + featureFlags: cwFeatures + }, + ...(cwCreative) && { + cwcreative: cwCreative + }, + ...(debug) && { + debug: true + } + }; +} export const spec = { code: BIDDER_CODE, - supportedMediaTypes: [BANNER, VIDEO], + supportedMediaTypes: [BANNER], + /** - * Determines whether or not the given bid request is valid. + * Determines whether the given bid request is valid. * * @param {BidRequest} bid The bid params to validate. * @return boolean True if this is a valid bid, and false otherwise. */ - isBidRequestValid: function(bid) { - bid.params = bid.params || {}; - - if (bid.params.cwcreative && !isStr(bid.params.cwcreative)) { - logError('cwcreative must be of type string!'); - return false; - } - - if (!bid.params.placementId || !isNumber(bid.params.placementId)) { - logError('placementId not provided or invalid'); + isBidRequestValid: function (bid) { + if (!bid.params?.placementId || !isNumber(bid.params.placementId)) { + logError('placementId not provided or not a number'); return false; } - if (!bid.params.pageId || !isNumber(bid.params.pageId)) { - logError('pageId not provided'); + if (!bid.params?.pageId || !isNumber(bid.params.pageId)) { + logError('pageId not provided or not a number'); return false; } - return true; }, /** - * ------------------------------------ - * itterate trough slots array and try - * to extract first occurence of a given - * key, if not found - return null - * ------------------------------------ - */ - getFirstValueOrNull: function(slots, key) { - const found = slots.find((item) => { - return (typeof item[key] !== 'undefined'); - }); - - return (found) ? found[key] : null; - }, - - /** - * ------------------------------------ - * Make a server request from the - * list of BidRequests. - * ------------------------------------ - * @param {validBidRequests[]} - an array of bids + * Make a server request from the list of BidRequests. + * + * @param {validBidRequests[]} validBidRequests An array of bids. * @return ServerRequest Info describing the request to the server. */ - buildRequests: function(validBidRequests, bidderRequest) { - let slots = []; - let referer; - try { - referer = bidderRequest?.refererInfo?.page; - slots = mapSlotsData(validBidRequests); - } catch (e) { - logWarn(e); - } + buildRequests: function (validBidRequests, bidderRequest) { + // There are more fields on the refererInfo object + let referrer = bidderRequest?.refererInfo?.page - let refgroups = []; - - const cwCreative = getQueryVariable(CW_CREATIVE_QUERY) || null; - const cwCreativeIdFromConfig = this.getFirstValueOrNull(slots, 'cwcreative'); - const refGroupsFromConfig = this.getFirstValueOrNull(slots, 'refgroups'); - const cwApiKeyFromConfig = this.getFirstValueOrNull(slots, 'cwapikey'); - const rgQuery = getQueryVariable(CW_GROUPS_QUERY); - - if (refGroupsFromConfig !== null) { - refgroups = refGroupsFromConfig.split(','); - } - - if (rgQuery !== null) { - // override if query param is present - refgroups = []; - refgroups = rgQuery.split(','); - } - - const localStorageCWID = storage.localStorageIsEnabled() ? storage.getDataFromLocalStorage(LS_CWID_KEY) : null; + // process bid requests + let processed = validBidRequests + .map(bid => slotDimensions(bid)) + // Flattens the pageId and placement Id for backwards compatibility. + .map((bid) => ({...bid, pageId: bid.params?.pageId, placementId: bid.params?.placementId})); + const extensions = getCwExtension(); const payload = { - cwid: localStorageCWID, - refgroups, - cwcreative: cwCreative || cwCreativeIdFromConfig, - slots: slots, - cwapikey: cwApiKeyFromConfig, - httpRef: referer || '', - pageViewId: CW_PAGE_VIEW_ID, + slots: processed, + httpRef: referrer, + // TODO: Verify whether the auctionId and the usage of pageViewId make sense. + pageViewId: pageViewId, + sdk: { + version: '$prebid.version$' + }, + ...extensions }; - + const payloadString = JSON.stringify(payload); return { method: 'POST', - url: ENDPOINT_URL, - data: payload + url: BID_ENDPOINT, + data: payloadString, }; }, - /** * Unpack the response from the server into a list of bids. * * @param {ServerResponse} serverResponse A successful response from the server. * @return {Bid[]} An array of bids which were nested inside the server. */ - interpretResponse: function(serverResponse, bidRequest) { - const bidResponses = []; - - try { - if (typeof bidRequest.data === 'string') { - bidRequest.data = JSON.parse(bidRequest.data); + interpretResponse: function (serverResponse, bidRequest) { + if (!hasCwid()) { + const cwid = serverResponse.body?.cwid + if (cwid) { + updateCwid(cwid); } - const serverBody = serverResponse.body; - serverBody.bids.forEach((br) => { - const bidReq = find(bidRequest.data.slots, bid => bid.bidId === br.requestId); - - let mediaType = BANNER; - - const bidResponse = { - requestId: br.requestId, - cpm: br.cpm, - bidderCode: BIDDER_CODE, - width: br.dimensions[0], - height: br.dimensions[1], - creativeId: br.creativeId, - currency: br.currency, - netRevenue: br.netRevenue, - ttl: br.ttl, - meta: { - advertiserDomains: br.adomains ? br.advertiserDomains : [], - }, - - }; - - // ------------------------------------ - // IF BANNER - // ------------------------------------ - - if (deepAccess(bidReq, 'mediaTypes.banner')) { - bidResponse.ad = br.html; - } - // ------------------------------------ - // IF VIDEO - // ------------------------------------ - if (deepAccess(bidReq, 'mediaTypes.video')) { - mediaType = VIDEO; - bidResponse.vastXml = br.vastXml; - bidResponse.videoScript = br.html; - const mediaTypeContext = deepAccess(bidReq, 'mediaTypes.video.context'); - if (mediaTypeContext === OUTSTREAM) { - const r = Renderer.install({ - id: bidResponse.requestId, - adUnitCode: bidReq.adUnitCode, - url: RENDERER_URL, - loaded: false, - config: { - ...deepAccess(bidReq, 'mediaTypes.video'), - ...deepAccess(br, 'outstream', {}) - } - }); + } - // set renderer - try { - bidResponse.renderer = r; - bidResponse.renderer.setRender(function(bid) { - if (window.CWIRE && window.CWIRE.outstream) { - window.CWIRE.outstream.renderAd(bid); - } - }); - } catch (err) { - logWarn('Prebid Error calling setRender on newRenderer', err); - } - } - } + // Rename `html` response property to `ad` as used by prebid. + const bids = serverResponse.body?.bids.map(({html, ...rest}) => ({...rest, ad: html})); + return bids || []; + }, - bidResponse.mediaType = mediaType; - bidResponses.push(bidResponse); - }); - } catch (e) { - logWarn(e); + onBidWon: function (bid) { + logInfo(`Bid won.`) + const event = { + type: 'BID_WON', + payload: { + bid: bid + } } + navigator.sendBeacon(EVENT_ENDPOINT, JSON.stringify(event)) + }, - return bidResponses; + onBidderError: function (error, bidderRequest) { + logInfo(`Bidder error: ${error}`) + const event = { + type: 'BID_ERROR', + payload: { + error: error, + bidderRequest: bidderRequest + } + } + navigator.sendBeacon(EVENT_ENDPOINT, JSON.stringify(event)) }, + }; registerBidder(spec); diff --git a/modules/cwireBidAdapter.md b/modules/cwireBidAdapter.md index 188894cd202..9804250b906 100644 --- a/modules/cwireBidAdapter.md +++ b/modules/cwireBidAdapter.md @@ -1,25 +1,27 @@ # Overview -Module Name: C-WIRE Bid Adapter -Module Type: Adagio Adapter -Maintainer: publishers@cwire.ch +``` +Module Name: C-WIRE Bid Adapter +Module Type: Bidder Adapter +Maintainer: devs@cwire.com +``` ## Description -Connects to C-WIRE demand source to fetch bids. +Prebid.js Adapter for C-Wire. ## Configuration - Below, the list of C-WIRE params and where they can be set. -| Param name | Global config | AdUnit config | Type | Required | -| ---------- | ------------- | ------------- |--------| ---------| -| pageId | | x | number | YES | -| placementId | | x | number | YES | -| refgroups | | x | string | NO | -| cwcreative | | x | string | NO | -| cwapikey | | x | string | NO | +| Param name | URL parameter | AdUnit config | Type | Required | +|-------------|:-------------:|:-------------:|:--------:|:-------------:| +| pageId | | x | number | YES | +| placementId | | x | number | YES | +| cwgroups | x | | string | NO | +| cwcreative | x | | string | NO | +| cwdebug | x | | boolean | NO | +| cwfeatures | x | | string | NO | ### adUnit configuration @@ -32,17 +34,22 @@ var adUnits = [ bidder: 'cwire', mediaTypes: { banner: { - sizes: [[1, 1]], + sizes: [[400, 600]], } }, params: { pageId: 1422, // required - number placementId: 2211521, // required - number - cwcreative: '42', // optional - id of creative to force - refgroups: 'test-user', // optional - name of group or coma separated list of groups to force - cwapikey: 'api_key_xyz', // optional - api key for integration testing } }] } ]; ``` + +### URL parameters + +For debugging and testing purposes url parameters can be set. + +**Example:** + +`https://www.some-site.com/article.html?cwdebug=true&cwfeatures=feature1,feature2&cwcreative=1234` diff --git a/modules/cpexIdSystem.js b/modules/czechAdIdSystem.js similarity index 68% rename from modules/cpexIdSystem.js rename to modules/czechAdIdSystem.js index 5c2e6bc212d..957b3ed30bd 100644 --- a/modules/cpexIdSystem.js +++ b/modules/czechAdIdSystem.js @@ -1,27 +1,28 @@ /** * This module adds 'caid' to the User ID module * The {@link module:modules/userId} module is required - * @module modules/cpexIdSystem + * @module modules/czechAdIdSystem * @requires module:modules/userId */ import { submodule } from '../src/hook.js' -import { getStorageManager } from '../src/storageManager.js' +import {getStorageManager} from '../src/storageManager.js'; +import {MODULE_TYPE_UID} from '../src/activities/modules.js'; // Returns StorageManager -export const storage = getStorageManager({ gvlid: 570, moduleName: 'cpexId' }) +export const storage = getStorageManager({ moduleType: MODULE_TYPE_UID, moduleName: 'czechAdId' }) // Returns the id string from either cookie or localstorage const readId = () => { return storage.getCookie('czaid') || storage.getDataFromLocalStorage('czaid') } /** @type {Submodule} */ -export const cpexIdSubmodule = { - version: '0.0.5', +export const czechAdIdSubmodule = { + version: '0.1.0', /** * used to link submodule with config * @type {string} */ - name: 'cpexId', + name: 'czechAdId', /** * Vendor ID of Czech Publisher Exchange * @type {Number} @@ -32,7 +33,7 @@ export const cpexIdSubmodule = { * @function decode * @returns {(Object|undefined)} */ - decode () { return { cpexId: readId() } }, + decode () { return { czechAdId: readId() } }, /** * performs action to obtain id and return a value in the callback's response argument * @function @@ -44,4 +45,4 @@ export const cpexIdSubmodule = { } } -submodule('userId', cpexIdSubmodule) +submodule('userId', czechAdIdSubmodule) diff --git a/modules/czechAdIdSystem.md b/modules/czechAdIdSystem.md new file mode 100644 index 00000000000..5614016f524 --- /dev/null +++ b/modules/czechAdIdSystem.md @@ -0,0 +1,27 @@ +## CzechAdId User ID Submodule + +Czech Ad ID is a joint project of publishers of the [CPEx alliance](https://www.cpex.cz/) and [Seznam.cz](https://www.seznam.cz). It is a deterministic user ID that offers cross-domain and cross-device identification. For more information see [czechadid.cz](https://www.czechadid.cz)). + +## Building Prebid with CzechAdId Support + +First, make sure to add the czechAdId to your Prebid.js package with: + +``` +gulp build --modules=czechAdIdSystem +``` + +The following configuration parameters are available: + +```javascript +pbjs.setConfig({ + userSync: { + userIds: [{ + name: 'czechAdId' + }] + } +}); +``` + +| Param under userSync.userIds[] | Scope | Type | Description | Example | +| --- | --- | --- | --- | --- | +| name | Required | String | The name of this module. | `"czechAdId"` | diff --git a/modules/dacIdSystem.js b/modules/dacIdSystem.js index 856e1976bb1..5adca074c87 100644 --- a/modules/dacIdSystem.js +++ b/modules/dacIdSystem.js @@ -19,8 +19,9 @@ import { import { getStorageManager } from '../src/storageManager.js'; - -export const storage = getStorageManager(); +import {MODULE_TYPE_UID} from '../src/activities/modules.js'; +const MODULE_NAME = 'dacId'; +export const storage = getStorageManager({moduleType: MODULE_TYPE_UID, moduleName: MODULE_NAME}); export const FUUID_COOKIE_NAME = '_a1_f'; export const AONEID_COOKIE_NAME = '_a1_d'; @@ -116,7 +117,7 @@ export const dacIdSystemSubmodule = { * used to link submodule with config * @type {string} */ - name: 'dacId', + name: MODULE_NAME, /** * decode the stored id value for passing to bid requests diff --git a/modules/datawrkzBidAdapter.js b/modules/datawrkzBidAdapter.js index 71870d6437d..193a1723403 100644 --- a/modules/datawrkzBidAdapter.js +++ b/modules/datawrkzBidAdapter.js @@ -1,4 +1,4 @@ -import { deepAccess, getBidIdParameter, isArray, getUniqueIdentifierStr, contains } from '../src/utils.js'; +import { deepAccess, getBidIdParameter, isArray, getUniqueIdentifierStr, contains, isFn, isPlainObject } from '../src/utils.js'; import { config } from '../src/config.js'; import { Renderer } from '../src/Renderer.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; @@ -84,7 +84,7 @@ export const spec = { /* Generate bid request for banner adunit */ function buildBannerRequest(bidRequest, bidderRequest) { - let bidFloor = Number(getBidIdParameter('bidfloor', bidRequest.params)); + let bidFloor = getBidFloor(bidRequest); let adW = 0; let adH = 0; @@ -135,7 +135,7 @@ function buildNativeRequest(bidRequest, bidderRequest) { let counter = 0; let assets = []; - let bidFloor = Number(getBidIdParameter('bidfloor', bidRequest.params)); + let bidFloor = getBidFloor(bidRequest); let title = deepAccess(bidRequest, 'mediaTypes.native.title'); if (title && title.len) { @@ -196,7 +196,7 @@ function buildNativeRequest(bidRequest, bidderRequest) { /* Generate bid request for video adunit */ function buildVideoRequest(bidRequest, bidderRequest) { - let bidFloor = Number(getBidIdParameter('bidfloor', bidRequest.params)); + let bidFloor = getBidFloor(bidRequest); let sizeObj = getVideoAdUnitSize(bidRequest); @@ -414,6 +414,7 @@ function buildBannerResponse(bidRequest, bidResponse) { } let bidSizes = (deepAccess(bidRequest, 'mediaTypes.banner.sizes')) ? deepAccess(bidRequest, 'mediaTypes.banner.sizes') : bidRequest.sizes; bidResponse.requestId = bidRequest.bidId; + bidResponse.auctionId = bidRequest.auctionId; bidResponse.transactionId = bidRequest.transactionId; bidResponse.placementCode = placementCode; bidResponse.cpm = responseCPM; @@ -455,6 +456,7 @@ function buildNativeResponse(bidRequest, response) { return; } bidResponse.requestId = bidRequest.bidId; + bidResponse.auctionId = bidRequest.auctionId; bidResponse.transactionId = bidRequest.transactionId; bidResponse.placementCode = placementCode; bidResponse.cpm = responseCPM; @@ -507,6 +509,7 @@ function buildVideoResponse(bidRequest, response) { let context = bidRequest.mediaTypes.video.context; bidResponse.requestId = bidRequest.bidId; + bidResponse.auctionId = bidRequest.auctionId; bidResponse.transactionId = bidRequest.transactionId; bidResponse.placementCode = placementCode; bidResponse.cpm = responseCPM; @@ -630,4 +633,21 @@ function getNativeAssestObj(obj, assets) { } } +// BUILD REQUESTS: BIDFLOORS +function getBidFloor(bid) { + if (!isFn(bid.getFloor)) { + return (bid.params.bidfloor) ? bid.params.bidfloor : null; + } + + let floor = bid.getFloor({ + currency: 'USD', + mediaType: '*', + size: '*' + }); + if (isPlainObject(floor) && !isNaN(floor.floor) && floor.currency === 'USD') { + return floor.floor; + } + return null; +} + registerBidder(spec); diff --git a/modules/deepintentDpesIdSystem.js b/modules/deepintentDpesIdSystem.js index 43c7af1b3cc..f2b50d535eb 100644 --- a/modules/deepintentDpesIdSystem.js +++ b/modules/deepintentDpesIdSystem.js @@ -6,10 +6,11 @@ */ import { submodule } from '../src/hook.js'; -import { getStorageManager } from '../src/storageManager.js'; +import {getStorageManager} from '../src/storageManager.js'; +import {MODULE_TYPE_UID} from '../src/activities/modules.js'; const MODULE_NAME = 'deepintentId'; -export const storage = getStorageManager({gvlid: null, moduleName: MODULE_NAME}); +export const storage = getStorageManager({moduleType: MODULE_TYPE_UID, moduleName: MODULE_NAME}); /** @type {Submodule} */ export const deepintentDpesSubmodule = { diff --git a/modules/dfpAdServerVideo.js b/modules/dfpAdServerVideo.js index 072715b4bd6..3394fd8b3f4 100644 --- a/modules/dfpAdServerVideo.js +++ b/modules/dfpAdServerVideo.js @@ -8,10 +8,11 @@ import { deepAccess, isEmpty, logError, parseSizesInput, formatQS, parseUrl, bui import { config } from '../src/config.js'; import { getHook, submodule } from '../src/hook.js'; import { auctionManager } from '../src/auctionManager.js'; -import { gdprDataHandler, uspDataHandler } from '../src/adapterManager.js'; +import { gdprDataHandler, uspDataHandler, gppDataHandler } from '../src/adapterManager.js'; import * as events from '../src/events.js'; import CONSTANTS from '../src/constants.json'; import {getPPID} from '../src/adserver.js'; +import {getRefererInfo} from '../src/refererDetection.js'; /** * @typedef {Object} DfpVideoParams @@ -52,6 +53,10 @@ const defaultParamConstants = { export const adpodUtils = {}; +export const dep = { + ri: getRefererInfo +} + /** * Merge all the bid data and publisher-supplied options into a single URL, and then return it. * @@ -119,6 +124,11 @@ export function buildDfpVideoUrl(options) { const uspConsent = uspDataHandler.getConsentData(); if (uspConsent) { queryParams.us_privacy = uspConsent; } + const gppConsent = gppDataHandler.getConsentData(); + if (gppConsent) { + // TODO - need to know what to set here for queryParams... + } + if (!queryParams.ppid) { const ppid = getPPID(); if (ppid != null) { @@ -254,14 +264,7 @@ function buildUrlFromAdserverUrlComponents(components, bid, options) { * @return {string | undefined} The encoded vast url if it exists, or undefined */ function getDescriptionUrl(bid, components, prop) { - if (config.getConfig('cache.url')) { return; } - - if (!deepAccess(components, `${prop}.description_url`)) { - const vastUrl = bid && bid.vastUrl; - if (vastUrl) { return encodeURIComponent(vastUrl); } - } else { - logError(`input cannnot contain description_url`); - } + return deepAccess(components, `${prop}.description_url`) || dep.ri().page; } /** @@ -288,6 +291,8 @@ function getCustParams(bid, options, urlCustParams) { allTargetingData, adserverTargeting, ); + + // TODO: WTF is this? just firing random events, guessing at the argument, hoping noone notices? events.emit(CONSTANTS.EVENTS.SET_TARGETING, {[adUnit.code]: prebidTargetingSet}); // merge the prebid + publisher targeting sets diff --git a/modules/discoveryBidAdapter.js b/modules/discoveryBidAdapter.js index 0c536892716..a0f864d529f 100644 --- a/modules/discoveryBidAdapter.js +++ b/modules/discoveryBidAdapter.js @@ -6,7 +6,7 @@ import { BANNER, NATIVE } from '../src/mediaTypes.js'; const BIDDER_CODE = 'discovery'; const ENDPOINT_URL = 'https://rtb-jp.mediago.io/api/bid?tn='; const TIME_TO_LIVE = 500; -const storage = getStorageManager(); +const storage = getStorageManager({bidderCode: BIDDER_CODE}); let globals = {}; let itemMaps = {}; const MEDIATYPE = [BANNER, NATIVE]; @@ -40,7 +40,7 @@ const NATIVERET = { title: { len: 75, }, - } + }, ], plcmttype: 1, privacy: 1, @@ -63,9 +63,9 @@ const getUserID = () => { let idm = storage.getCookie(COOKIE_KEY_MGUID); if (idd && !idm) { - idm = idd + idm = idd; } else if (idm && !idd) { - idd = idm + idd = idm; } else if (!idd && !idm) { const uuid = utils.generateUUID(); storage.setCookie(COOKIE_KEY_MGUID, uuid); @@ -228,7 +228,7 @@ function getItems(validBidRequests, bidderRequest) { let id = '' + (i + 1); if (mediaTypes.native) { - ret = { ...NATIVERET, ...{ id, bidFloor } } + ret = { ...NATIVERET, ...{ id, bidFloor } }; } // banner if (mediaTypes.banner) { @@ -244,7 +244,9 @@ function getItems(validBidRequests, bidderRequest) { } } if (!matchSize) { - return {}; + matchSize = sizes[0] + ? { h: sizes[0].height || 0, w: sizes[0].width || 0 } + : { h: 0, w: 0 }; } ret = { id: id, @@ -253,6 +255,7 @@ function getItems(validBidRequests, bidderRequest) { h: matchSize.h, w: matchSize.w, pos: 1, + format: sizes, }, ext: {}, tagid: globals['tagid'], @@ -276,6 +279,11 @@ function getItems(validBidRequests, bidderRequest) { */ function getParam(validBidRequests, bidderRequest) { const pubcid = utils.deepAccess(validBidRequests[0], 'crumbs.pubcid'); + const sharedid = + utils.deepAccess(validBidRequests[0], 'userId.sharedid.id') || + utils.deepAccess(validBidRequests[0], 'userId.pubcid'); + const eids = validBidRequests[0].userIdAsEids || validBidRequests[0].userId; + let isMobile = getDevice() ? 1 : 0; // input test status by Publisher. more frequently for test true req let isTest = validBidRequests[0].params.test || 0; @@ -284,10 +292,12 @@ function getParam(validBidRequests, bidderRequest) { const timeout = bidderRequest.timeout || 2000; - const domain = utils.deepAccess(bidderRequest, 'refererInfo.domain') || document.domain; + const domain = + utils.deepAccess(bidderRequest, 'refererInfo.domain') || document.domain; const location = utils.deepAccess(bidderRequest, 'refererInfo.referer'); const page = utils.deepAccess(bidderRequest, 'refererInfo.page'); const referer = utils.deepAccess(bidderRequest, 'refererInfo.ref'); + const firstPartyData = bidderRequest.ortb2; if (items && items.length) { let c = { @@ -304,10 +314,15 @@ function getParam(validBidRequests, bidderRequest) { ua: navigator.userAgent, language: /en/.test(navigator.language) ? 'en' : navigator.language, }, + ext: { + eids, + firstPartyData, + }, user: { buyeruid: getUserID(), - id: pubcid, + id: sharedid || pubcid, }, + eids, tmax: timeout, site: { name: domain, @@ -317,7 +332,7 @@ function getParam(validBidRequests, bidderRequest) { mobile: isMobile, cat: [], // todo publisher: { - id: globals['publisher'] + id: globals['publisher'], // todo // name: xxx }, @@ -401,8 +416,8 @@ export const spec = { nurl: getKv(bid, 'nurl'), ttl: TIME_TO_LIVE, meta: { - advertiserDomains: getKv(bid, 'adomain') || [] - } + advertiserDomains: getKv(bid, 'adomain') || [], + }, }; if (mediaType === 'native') { const adm = getKv(bid, 'adm'); @@ -448,7 +463,7 @@ export const spec = { native.impressionTrackers.push(tracker.url); break; // case 2: - // native.javascriptTrackers = ``; + // native.javascriptTrackers = ``; // break; } }); @@ -484,8 +499,8 @@ export const spec = { */ onBidWon: function (bid) { if (bid['nurl']) { - utils.triggerPixel(bid['nurl']) + utils.triggerPixel(bid['nurl']); } - } + }, }; registerBidder(spec); diff --git a/modules/displayioBidAdapter.js b/modules/displayioBidAdapter.js index c3c6597dd1b..d46cc8ee309 100644 --- a/modules/displayioBidAdapter.js +++ b/modules/displayioBidAdapter.js @@ -2,12 +2,14 @@ import {registerBidder} from '../src/adapters/bidderFactory.js'; import {BANNER, VIDEO} from '../src/mediaTypes.js'; import {Renderer} from '../src/Renderer.js'; import {getWindowFromDocument, logWarn} from '../src/utils.js'; +import {getStorageManager} from '../src/storageManager.js'; const ADAPTER_VERSION = '1.1.0'; const BIDDER_CODE = 'displayio'; const BID_TTL = 300; const SUPPORTED_AD_TYPES = [BANNER, VIDEO]; const DEFAULT_CURRENCY = 'USD'; +const US_KEY = '_dio_us'; export const spec = { code: BIDDER_CODE, @@ -67,18 +69,26 @@ export const spec = { function getPayload (bid, bidderRequest) { const connection = navigator.connection || navigator.mozConnection || navigator.webkitConnection; - const userSession = 'us_web_xxxxxxxxxxxx'.replace(/[x]/g, c => { - let r = Math.random() * 16 | 0; - let v = c === 'x' ? r : (r & 0x3 | 0x8); - return v.toString(16); - }); + const storage = getStorageManager({bidderCode: BIDDER_CODE}); + const userSession = (() => { + let us = storage.getDataFromLocalStorage(US_KEY); + if (!us) { + us = 'us_web_xxxxxxxxxxxx'.replace(/[x]/g, c => { + let r = Math.random() * 16 | 0; + let v = c === 'x' ? r : (r & 0x3 | 0x8); + return v.toString(16); + }); + storage.setDataInLocalStorage(US_KEY, us); + } + return us + })(); const { params, adUnitCode, bidId } = bid; const { siteId, placementId, renderURL, pageCategory, keywords } = params; const { refererInfo, uspConsent, gdprConsent } = bidderRequest; - const mediation = {consent: '-1', gdpr: '-1'}; + const mediation = {gdprConsent: '', gdpr: '-1'}; if (gdprConsent && 'gdprApplies' in gdprConsent) { if (gdprConsent.consentString !== undefined) { - mediation.consent = gdprConsent.consentString; + mediation.gdprConsent = gdprConsent.consentString; } if (gdprConsent.gdprApplies !== undefined) { mediation.gdpr = gdprConsent.gdprApplies ? '1' : '0'; @@ -110,7 +120,7 @@ function getPayload (bid, bidderRequest) { dnt: window.doNotTrack === '1' || window.navigator.doNotTrack === '1' || false, iabConsent: {}, mediation: { - consent: mediation.consent, + gdprConsent: mediation.gdprConsent, gdpr: mediation.gdpr, } }, diff --git a/modules/distroscaleBidAdapter.js b/modules/distroscaleBidAdapter.js index 005dd3e67d6..7a2038ed3f0 100644 --- a/modules/distroscaleBidAdapter.js +++ b/modules/distroscaleBidAdapter.js @@ -132,6 +132,24 @@ export const spec = { // TODO: does the fallback to window.location make sense? var pageUrl = bidderRequest?.refererInfo?.page || window.location.href; + // check if dstag is already loaded in ancestry tree + var dsloaded = 0; + try { + var win = window; + while (true) { + if (win.vx.cs_loaded) { + dsloaded = 1; + } + if (win != win.parent) { + win = win.parent; + } else { + break; + } + } + } catch (error) { + // ignore exception + } + var payload = { id: '' + (new Date()).getTime(), at: AUCTION_TYPE, @@ -149,7 +167,9 @@ export const spec = { }, imp: [], user: {}, - ext: {} + ext: { + dsloaded: dsloaded + } }; validBidRequests.forEach(b => { diff --git a/modules/dspxBidAdapter.js b/modules/dspxBidAdapter.js index 8850eb282b5..3bfb660cd7e 100644 --- a/modules/dspxBidAdapter.js +++ b/modules/dspxBidAdapter.js @@ -1,12 +1,17 @@ -import { deepAccess } from '../src/utils.js'; +import {deepAccess, getBidIdParameter, logError, logMessage, logWarn, isFn} from '../src/utils.js'; import {config} from '../src/config.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; -import { BANNER, VIDEO } from '../src/mediaTypes.js'; +import {BANNER, VIDEO} from '../src/mediaTypes.js'; +import {Renderer} from '../src/Renderer.js'; +import {includes} from '../src/polyfill.js'; const BIDDER_CODE = 'dspx'; const ENDPOINT_URL = 'https://buyer.dspx.tv/request/'; const ENDPOINT_URL_DEV = 'https://dcbuyer.dspx.tv/request/'; const GVLID = 602; +const VIDEO_ORTB_PARAMS = ['mimes', 'minduration', 'maxduration', 'protocols', 'w', 'h', 'startdelay', 'placement', 'linearity', 'skip', 'skipmin', + 'skipafter', 'sequence', 'battr', 'maxextended', 'minbitrate', 'maxbitrate', 'boxingallowed', 'playbackmethod', 'playbackend', 'delivery', 'pos', 'companionad', + 'api', 'companiontype', 'ext']; export const spec = { code: BIDDER_CODE, @@ -17,24 +22,34 @@ export const spec = { return !!(bid.params.placement); }, buildRequests: function(validBidRequests, bidderRequest) { + let payload = {}; return validBidRequests.map(bidRequest => { const params = bidRequest.params; - const placementId = params.placement; const rnd = Math.floor(Math.random() * 99999999999); const referrer = bidderRequest.refererInfo.page; const bidId = bidRequest.bidId; - const isDev = params.devMode || false; const pbcode = bidRequest.adUnitCode || false; // div id const auctionId = bidRequest.auctionId || false; + const isDev = params.devMode || false; let endpoint = isDev ? ENDPOINT_URL_DEV : ENDPOINT_URL; + let placementId = params.placement; + + // dev config + if (isDev && params.dev) { + endpoint = params.dev.endpoint || endpoint; + placementId = params.dev.placement || placementId; + if (params.dev.pfilter !== undefined) { + params.pfilter = params.dev.pfilter; + } + } let mediaTypesInfo = getMediaTypesInfo(bidRequest); let type = isBannerRequest(bidRequest) ? BANNER : VIDEO; let sizes = mediaTypesInfo[type]; - let payload = { + payload = { _f: 'auto', alternative: 'prebid_js', inventory_item_id: placementId, @@ -47,10 +62,6 @@ export const spec = { pbver: '$prebid.version$' }; - if (mediaTypesInfo[VIDEO] !== undefined && params.vastFormat !== undefined) { - payload.vf = params.vastFormat; - } - if (params.pfilter !== undefined) { payload.pfilter = params.pfilter; } @@ -79,11 +90,49 @@ export const spec = { payload.prebidDevMode = 1; } - if (bidRequest.userId && bidRequest.userId.netId) { - payload.did_netid = bidRequest.userId.netId; + // fill userId params + if (bidRequest.userId) { + if (bidRequest.userId.netId) { + payload.did_netid = bidRequest.userId.netId; + } + if (bidRequest.userId.id5id) { + payload.did_id5 = bidRequest.userId.id5id.uid || '0'; + if (bidRequest.userId.id5id.ext.linkType !== undefined) { + payload.did_id5_linktype = bidRequest.userId.id5id.ext.linkType; + } + } + let uId2 = deepAccess(bidRequest, 'userId.uid2.id'); + if (uId2) { + payload.did_uid2 = uId2; + } + let sharedId = deepAccess(bidRequest, 'userId.sharedid.id'); + if (sharedId) { + payload.did_sharedid = sharedId; + } + let pubcId = deepAccess(bidRequest, 'userId.pubcid'); + if (pubcId) { + payload.did_pubcid = pubcId; + } + let crumbsPubcid = deepAccess(bidRequest, 'crumbs.pubcid'); + if (crumbsPubcid) { + payload.did_cpubcid = crumbsPubcid; + } + } + + if (bidRequest.schain) { + payload.schain = bidRequest.schain; } - if (bidRequest.userId && bidRequest.userId.uid2) { - payload.did_uid2 = bidRequest.userId.uid2; + + if (payload.pfilter === undefined || !payload.pfilter.floorprice) { + let bidFloor = getBidFloor(bidRequest); + if (bidFloor > 0) { + if (payload.pfilter !== undefined) { + payload.pfilter.floorprice = bidFloor; + } else { + payload.pfilter = { 'floorprice': bidFloor }; + } + // payload.bidFloor = bidFloor; + } } if (auctionId) { @@ -94,6 +143,17 @@ export const spec = { } payload.media_types = convertMediaInfoForRequest(mediaTypesInfo); + if (mediaTypesInfo[VIDEO] !== undefined) { + payload.vctx = getVideoContext(bidRequest); + if (params.vastFormat !== undefined) { + payload.vf = params.vastFormat; + } + payload.vpl = {}; + let videoParams = deepAccess(bidRequest, 'mediaTypes.video'); + Object.keys(videoParams) + .filter(key => includes(VIDEO_ORTB_PARAMS, key)) + .forEach(key => payload.vpl[key] = videoParams[key]); + } return { method: 'GET', @@ -103,6 +163,8 @@ export const spec = { }); }, interpretResponse: function(serverResponse, bidRequest) { + logMessage('DSPx: serverResponse', serverResponse); + logMessage('DSPx: bidRequest', bidRequest); const bidResponses = []; const response = serverResponse.body; const crid = response.crid || 0; @@ -126,13 +188,33 @@ export const spec = { advertiserDomains: response.adomain || [] } }; + + if (response.vastUrl) { + bidResponse.vastUrl = response.vastUrl; + bidResponse.mediaType = 'video'; + } if (response.vastXml) { bidResponse.vastXml = response.vastXml; bidResponse.mediaType = 'video'; - } else { + } + if (response.renderer) { + bidResponse.renderer = newRenderer(bidRequest, response); + } + + if (response.videoCacheKey) { + bidResponse.videoCacheKey = response.videoCacheKey; + } + + if (response.adTag) { bidResponse.ad = response.adTag; } + if (response.bid_appendix) { + Object.keys(response.bid_appendix).forEach(fieldName => { + bidResponse[fieldName] = response.bid_appendix[fieldName]; + }); + } + bidResponses.push(bidResponse); } return bidResponses; @@ -217,12 +299,22 @@ function isVideoRequest(bid) { * Get video sizes * * @param {BidRequest} bid - Bid request generated from ad slots - * @returns {object} True if it's a video bid + * @returns {object} */ function getVideoSizes(bid) { return parseSizes(deepAccess(bid, 'mediaTypes.video.playerSize') || bid.sizes); } +/** + * Get video context + * + * @param {BidRequest} bid - Bid request generated from ad slots + * @returns {object} + */ +function getVideoContext(bid) { + return deepAccess(bid, 'mediaTypes.video.context') || 'unknown'; +} + /** * Get banner sizes * @@ -296,4 +388,120 @@ function getMediaTypesInfo(bid) { return mediaTypesInfo; } +/** + * Get Bid Floor + * @param bid + * @returns {number|*} + */ +function getBidFloor(bid) { + if (!isFn(bid.getFloor)) { + return deepAccess(bid, 'params.bidfloor', 0); + } + + try { + const bidFloor = bid.getFloor({ + currency: 'EUR', + mediaType: '*', + size: '*', + }); + return bidFloor.floor; + } catch (_) { + return 0 + } +} + +/** + * Create a new renderer + * + * @param bidRequest + * @param response + * @returns {Renderer} + */ +function newRenderer(bidRequest, response) { + logMessage('DSPx: newRenderer', bidRequest, response); + const renderer = Renderer.install({ + id: response.renderer.id || response.bid_id, + url: (bidRequest.params && bidRequest.params.rendererUrl) || response.renderer.url, + config: response.renderer.options || deepAccess(bidRequest, 'renderer.options'), + loaded: false + }); + + try { + renderer.setRender(outstreamRender); + } catch (err) { + logWarn('Prebid Error calling setRender on renderer', err); + } + return renderer; +} + +/** + * Outstream Render Function + * + * @param bid + */ +function outstreamRender(bid) { + logMessage('DSPx: outstreamRender bid:', bid); + const embedCode = createOutstreamEmbedCode(bid); + try { + const inIframe = getBidIdParameter('iframe', bid.renderer.config); + if (inIframe && window.document.getElementById(inIframe).nodeName === 'IFRAME') { + const iframe = window.document.getElementById(inIframe); + let framedoc = iframe.contentDocument || (iframe.contentWindow && iframe.contentWindow.document); + framedoc.body.appendChild(embedCode); + if (typeof window.dspxRender === 'function') { + window.dspxRender(bid); + } else { + logError('[dspx][renderer] Error: dspxRender function is not found'); + } + return; + } + + const slot = getBidIdParameter('slot', bid.renderer.config) || bid.adUnitCode; + if (slot && window.document.getElementById(slot)) { + window.document.getElementById(slot).appendChild(embedCode); + if (typeof window.dspxRender === 'function') { + window.dspxRender(bid); + } else { + logError('[dspx][renderer] Error: dspxRender function is not found'); + } + } else if (slot) { + logError('[dspx][renderer] Error: slot not found'); + } + } catch (err) { + logError('[dspx][renderer] Error:' + err.message) + } +} + +/** + * create Outstream Embed Code Node + * + * @param bid + * @returns {DocumentFragment} + */ +function createOutstreamEmbedCode(bid) { + const fragment = window.document.createDocumentFragment(); + let div = window.document.createElement('div'); + div.innerHTML = deepAccess(bid, 'renderer.config.code', ''); + fragment.appendChild(div); + + // run scripts + var scripts = div.getElementsByTagName('script'); + var scriptsClone = []; + for (var idx = 0; idx < scripts.length; idx++) { + scriptsClone.push(scripts[idx]); + } + for (var i = 0; i < scriptsClone.length; i++) { + var currentScript = scriptsClone[i]; + var s = document.createElement('script'); + for (var j = 0; j < currentScript.attributes.length; j++) { + var a = currentScript.attributes[j]; + s.setAttribute(a.name, a.value); + } + s.appendChild(document.createTextNode(currentScript.innerHTML)); + currentScript.parentNode.replaceChild(s, currentScript); + } + + return fragment; +} + registerBidder(spec); diff --git a/modules/emx_digitalBidAdapter.js b/modules/emx_digitalBidAdapter.js index eb69e76a837..99f313b9484 100644 --- a/modules/emx_digitalBidAdapter.js +++ b/modules/emx_digitalBidAdapter.js @@ -354,16 +354,23 @@ export const spec = { }, getUserSyncs: function (syncOptions, responses, gdprConsent, uspConsent) { const syncs = []; + const consentParams = []; if (syncOptions.iframeEnabled) { let url = 'https://biddr.brealtime.com/check.html'; if (gdprConsent && typeof gdprConsent.consentString === 'string') { // add 'gdpr' only if 'gdprApplies' is defined if (typeof gdprConsent.gdprApplies === 'boolean') { - url += `?gdpr=${Number(gdprConsent.gdprApplies)}&gdpr_consent=${gdprConsent.consentString}`; + consentParams.push(`gdpr=${Number(gdprConsent.gdprApplies)}&gdpr_consent=${gdprConsent.consentString}`); } else { - url += `?gdpr_consent=${gdprConsent.consentString}`; + consentParams.push(`?gdpr_consent=${gdprConsent.consentString}`); } } + if (uspConsent && typeof uspConsent.consentString === 'string') { + consentParams.push(`usp=${uspConsent.consentString}`); + } + if (consentParams.length > 0) { + url = url + '?' + consentParams.join('&'); + } syncs.push({ type: 'iframe', url: url diff --git a/modules/enrichmentFpdModule.js b/modules/enrichmentFpdModule.js index a4d7c06d0fb..59d5d326109 100644 --- a/modules/enrichmentFpdModule.js +++ b/modules/enrichmentFpdModule.js @@ -1,190 +1,2 @@ - -/** - * This module sets default values and validates ortb2 first part data - * @module modules/firstPartyData - */ -import { timestamp, mergeDeep } from '../src/utils.js'; -import { submodule } from '../src/hook.js'; -import {getRefererInfo, parseDomain} from '../src/refererDetection.js'; -import { getCoreStorageManager } from '../src/storageManager.js'; -import {GreedyPromise} from '../src/utils/promise.js'; -import {getHighEntropySUA, getLowEntropySUA} from '../libraries/fpd/sua.js'; - -let ortb2; -let win = (window === window.top) ? window : window.top; -export const coreStorage = getCoreStorageManager('enrichmentFpd'); - -export const sua = {he: getHighEntropySUA, le: getLowEntropySUA}; - -/** - * Find the root domain - * @param {string|undefined} fullDomain - * @return {string} -*/ -export function findRootDomain(fullDomain = window.location.hostname) { - if (!coreStorage.cookiesAreEnabled()) { - return fullDomain; - } - - const domainParts = fullDomain.split('.'); - if (domainParts.length == 2) { - return fullDomain; - } - let rootDomain; - let continueSearching; - let startIndex = -2; - const TEST_COOKIE_NAME = `_rdc${Date.now()}`; - const TEST_COOKIE_VALUE = 'writeable'; - do { - rootDomain = domainParts.slice(startIndex).join('.'); - let expirationDate = new Date(timestamp() + 10 * 1000).toUTCString(); - - // Write a test cookie - coreStorage.setCookie( - TEST_COOKIE_NAME, - TEST_COOKIE_VALUE, - expirationDate, - 'Lax', - rootDomain, - undefined - ); - - // See if the write was successful - const value = coreStorage.getCookie(TEST_COOKIE_NAME, undefined); - if (value === TEST_COOKIE_VALUE) { - continueSearching = false; - // Delete our test cookie - coreStorage.setCookie( - TEST_COOKIE_NAME, - '', - 'Thu, 01 Jan 1970 00:00:01 GMT', - undefined, - rootDomain, - undefined - ); - } else { - startIndex += -1; - continueSearching = Math.abs(startIndex) <= domainParts.length; - } - } while (continueSearching); - return rootDomain; -} - -/** - * Checks for referer and if exists merges into ortb2 global data - */ -function setReferer() { - if (getRefererInfo().ref) mergeDeep(ortb2, { site: { ref: getRefererInfo().ref } }); -} - -/** - * Checks for canonical url and if exists merges into ortb2 global data - */ -function setPage() { - if (getRefererInfo().page) mergeDeep(ortb2, { site: { page: getRefererInfo().page } }); -} - -/** - * Checks for canonical url and if exists retrieves domain and merges into ortb2 global data - */ -function setDomain() { - const domain = parseDomain(getRefererInfo().page, {noLeadingWww: true}); - if (domain) { - mergeDeep(ortb2, { site: { domain: domain } }); - mergeDeep(ortb2, { site: { publisher: { domain: findRootDomain(domain) } } }); - }; -} - -/** - * Checks for screen/device width and height and sets dimensions - */ -function setDimensions() { - let width; - let height; - - try { - width = win.innerWidth || win.document.documentElement.clientWidth || win.document.body.clientWidth; - height = win.innerHeight || win.document.documentElement.clientHeight || win.document.body.clientHeight; - } catch (e) { - width = window.innerWidth || window.document.documentElement.clientWidth || window.document.body.clientWidth; - height = window.innerHeight || window.document.documentElement.clientHeight || window.document.body.clientHeight; - } - - mergeDeep(ortb2, { device: { w: width, h: height } }); -} - -/** - * Scans page for meta keywords, and if exists, merges into site.keywords - */ -function setKeywords() { - let keywords; - - try { - keywords = win.document.querySelector("meta[name='keywords']"); - } catch (e) { - keywords = window.document.querySelector("meta[name='keywords']"); - } - - if (keywords && keywords.content) mergeDeep(ortb2, { site: { keywords: keywords.content.replace(/\s/g, '') } }); -} - -function setDeviceSua(hints) { - let data = Array.isArray(hints) && hints.length === 0 - ? GreedyPromise.resolve(sua.le()) - : sua.he(hints); - return data.then((sua) => { - if (sua != null) { - mergeDeep(ortb2, {device: {sua}}); - } - }) -} - -/** - * Checks the Global Privacy Control status, and if exists and is true, merges into regs.ext.gpc - */ -function setGpc() { - const gpcValue = navigator.globalPrivacyControl; - if (gpcValue) { - mergeDeep(ortb2, { regs: { ext: { gpc: 1 } } }) - } -} - -/** - * Resets modules global ortb2 data - */ -export const resetEnrichments = () => { ortb2 = null }; - -function runEnrichments(fpdConf) { - setReferer(); - setPage(); - setDomain(); - setDimensions(); - setKeywords(); - setGpc(); - return setDeviceSua(fpdConf.uaHints).then(() => ortb2); -} - -export function processFpd(fpdConf, {global}) { - if (fpdConf.skipEnrichments) { - return {global}; - } else { - let ready; - if (ortb2 == null) { - ortb2 = {}; - ready = runEnrichments(fpdConf); - } else { - ready = GreedyPromise.resolve(); - } - return ready.then(() => ({ - global: mergeDeep({}, ortb2, global) - })) - } -} -/** @type {firstPartyDataSubmodule} */ -export const enrichmentsSubmodule = { - name: 'enrichments', - queue: 2, - processFpd -} - -submodule('firstPartyData', enrichmentsSubmodule) +// Logic from this module was moved into core since approx. 7.27 +// TODO: remove this in v8 diff --git a/modules/eplanningBidAdapter.js b/modules/eplanningBidAdapter.js index e230858487f..2216ab329b0 100644 --- a/modules/eplanningBidAdapter.js +++ b/modules/eplanningBidAdapter.js @@ -24,6 +24,7 @@ const VAST_INSTREAM = 1; const VAST_OUTSTREAM = 2; const VAST_VERSION_DEFAULT = 3; const DEFAULT_SIZE_VAST = '640x480'; +const MAX_LEN_URL = 255; export const spec = { code: BIDDER_CODE, @@ -60,7 +61,7 @@ export const spec = { params = { rnd: rnd, e: spaces.str, - ur: pageUrl || FILE, + ur: cutUrl(pageUrl || FILE), pbv: '$prebid.version$', ncb: '1', vs: spaces.vs @@ -70,7 +71,7 @@ export const spec = { } if (referrerUrl) { - params.fr = referrerUrl; + params.fr = cutUrl(referrerUrl); } if (bidderRequest && bidderRequest.gdprConsent) { @@ -491,6 +492,17 @@ function visibilityHandler(obj) { } } +function cutUrl (url) { + if (url.length > MAX_LEN_URL) { + url = url.split('?')[0]; + if (url.length > MAX_LEN_URL) { + url = url.slice(0, MAX_LEN_URL); + } + } + + return url; +} + function registerAuction(storageID) { let value; try { diff --git a/modules/express.js b/modules/express.js index 0b1780e3c26..a2998baed07 100644 --- a/modules/express.js +++ b/modules/express.js @@ -1,6 +1,8 @@ import { logMessage, logWarn, logError, logInfo } from '../src/utils.js'; +import {getGlobal} from '../src/prebidGlobal.js'; const MODULE_NAME = 'express'; +const pbjsInstance = getGlobal(); /** * Express Module @@ -12,7 +14,7 @@ const MODULE_NAME = 'express'; * * @param {Object[]} [adUnits = pbjs.adUnits] - an array of adUnits for express to operate on. */ -$$PREBID_GLOBAL$$.express = function(adUnits = $$PREBID_GLOBAL$$.adUnits) { +pbjsInstance.express = function(adUnits = pbjsInstance.adUnits) { logMessage('loading ' + MODULE_NAME); if (adUnits.length === 0) { @@ -138,10 +140,10 @@ $$PREBID_GLOBAL$$.express = function(adUnits = $$PREBID_GLOBAL$$.adUnits) { } if (adUnits.length) { - $$PREBID_GLOBAL$$.requestBids({ + pbjsInstance.requestBids({ adUnits: adUnits, bidsBackHandler: function () { - $$PREBID_GLOBAL$$.setTargetingForGPTAsync(); + pbjsInstance.setTargetingForGPTAsync(); fGptRefresh.apply(pads(), [ adUnits.map(function (adUnit) { return gptSlotCache[adUnit.code]; @@ -168,10 +170,10 @@ $$PREBID_GLOBAL$$.express = function(adUnits = $$PREBID_GLOBAL$$.adUnits) { } if (adUnits.length) { - $$PREBID_GLOBAL$$.requestBids({ + pbjsInstance.requestBids({ adUnits: adUnits, bidsBackHandler: function () { - $$PREBID_GLOBAL$$.setTargetingForGPTAsync(); + pbjsInstance.setTargetingForGPTAsync(); fGptRefresh.apply(pads(), [ adUnits.map(function (adUnit) { return gptSlotCache[adUnit.code]; diff --git a/modules/feedadBidAdapter.js b/modules/feedadBidAdapter.js index 0ab6def38ae..34e14cd674a 100644 --- a/modules/feedadBidAdapter.js +++ b/modules/feedadBidAdapter.js @@ -7,7 +7,23 @@ import {ajax} from '../src/ajax.js'; * Version of the FeedAd bid adapter * @type {string} */ -const VERSION = '1.0.3'; +const VERSION = '1.0.6'; + +/** + * @typedef {object} FeedAdUserSync + * @inner + * + * @property {string} type + * @property {string} url + */ + +/** + * @typedef {object} FeedAdBidExtension + * @inner + * + * @property {FeedAdUserSync[]} pixels + * @property {FeedAdUserSync[]} iframes + */ /** * @typedef {object} FeedAdApiBidRequest @@ -16,7 +32,8 @@ const VERSION = '1.0.3'; * @property {number} ad_type * @property {string} client_token * @property {string} placement_id - * @property {string} sdk_version + * @property {string} prebid_adapter_version + * @property {string} prebid_sdk_version * @property {boolean} app_hybrid * * @property {string} [app_bundle_id] @@ -40,7 +57,7 @@ const VERSION = '1.0.3'; * @property {string} requestId - bids[].bidId * @property {number} ttl - Time to live for this ad * @property {number} width - Width of creative returned in [].ad - * @property {object} [ext] - an extension object + * @property {FeedAdBidExtension} [ext] - an extension object */ /** @@ -62,6 +79,14 @@ const VERSION = '1.0.3'; * @property [device_platform] {1|2|3} 1 - Android | 2 - iOS | 3 - Windows */ +/** + * @typedef {object} FeedAdServerResponse + * @extends ServerResponse + * @inner + * + * @property {FeedAdApiBidResponse[]} body - the body of a FeedAd server response + */ + /** * The IAB TCF 2.0 vendor ID for the FeedAd GmbH */ @@ -181,7 +206,8 @@ function createApiBidRParams(request) { ad_type: 0, client_token: request.params.clientToken, placement_id: request.params.placementId, - sdk_version: `prebid_${VERSION}`, + prebid_adapter_version: VERSION, + prebid_sdk_version: '$prebid.version$', app_hybrid: false, }); } @@ -207,7 +233,6 @@ function buildRequests(validBidRequests, bidderRequest) { }) }); data.bids.forEach(bid => BID_METADATA[bid.bidId] = { - // TODO: is 'page' the right value here? referer: data.refererInfo.page, transactionId: bid.transactionId }); @@ -227,7 +252,7 @@ function buildRequests(validBidRequests, bidderRequest) { /** * Adapts the FeedAd server response to Prebid format - * @param {ServerResponse} serverResponse - the FeedAd server response + * @param {FeedAdServerResponse} serverResponse - the FeedAd server response * @param {BidRequest} request - the initial bid request * @returns {Bid[]} the FeedAd bids */ @@ -266,7 +291,8 @@ function createTrackingParams(data, klass) { prebid_bid_id: bidId, prebid_transaction_id: transactionId, referer, - sdk_version: VERSION + prebid_adapter_version: VERSION, + prebid_sdk_version: '$prebid.version$', }; } @@ -294,16 +320,20 @@ function trackingHandlerFactory(klass) { /** * Reads the user syncs off the server responses and converts them into Prebid.JS format * @param {SyncOptions} syncOptions - * @param {FeedAdApiBidResponse[]} serverResponses + * @param {FeedAdServerResponse[]} serverResponses * @param gdprConsent * @param uspConsent */ function getUserSyncs(syncOptions, serverResponses, gdprConsent, uspConsent) { - return serverResponses.map(response => response.ext) - .flatMap(extension => { + return serverResponses.flatMap(response => { + // merge all response bodies into one + const body = response.body; + return isArray(body) ? body : []; + }) + .flatMap(/** @param {FeedAdApiBidResponse} bidResponse */ bidResponse => { // extract user syncs from extension - const pixels = syncOptions.pixelEnabled && extension.pixels ? extension.pixels : []; - const iframes = syncOptions.iframeEnabled && extension.iframes ? extension.iframes : []; + const pixels = (syncOptions.pixelEnabled && bidResponse?.ext?.pixels) ? bidResponse.ext.pixels : []; + const iframes = (syncOptions.iframeEnabled && bidResponse?.ext?.iframes) ? bidResponse.ext.iframes : []; return pixels.concat(...iframes); }) .reduce((syncs, sync) => { diff --git a/modules/fintezaAnalyticsAdapter.js b/modules/fintezaAnalyticsAdapter.js index 88c5f85f15d..be661c96061 100644 --- a/modules/fintezaAnalyticsAdapter.js +++ b/modules/fintezaAnalyticsAdapter.js @@ -2,10 +2,12 @@ import { parseUrl, logError } from '../src/utils.js'; import { ajax } from '../src/ajax.js'; import adapter from '../libraries/analyticsAdapter/AnalyticsAdapter.js'; import adapterManager from '../src/adapterManager.js'; -import { getStorageManager } from '../src/storageManager.js'; +import {getStorageManager} from '../src/storageManager.js'; import CONSTANTS from '../src/constants.json'; +import {MODULE_TYPE_ANALYTICS} from '../src/activities/modules.js'; -const storage = getStorageManager(); +const MODULE_CODE = 'finteza'; +const storage = getStorageManager({moduleType: MODULE_TYPE_ANALYTICS, moduleName: MODULE_CODE}); const ANALYTICS_TYPE = 'endpoint'; const FINTEZA_HOST = 'https://content.mql5.com/tr'; @@ -439,7 +441,7 @@ fntzAnalyticsAdapter.enableAnalytics = function (config) { adapterManager.registerAnalyticsAdapter({ adapter: fntzAnalyticsAdapter, - code: 'finteza' + code: MODULE_CODE, }); export default fntzAnalyticsAdapter; diff --git a/modules/fledgeForGpt.js b/modules/fledgeForGpt.js index 0f7092baf03..f29ce7508d5 100644 --- a/modules/fledgeForGpt.js +++ b/modules/fledgeForGpt.js @@ -5,6 +5,7 @@ import { config } from '../src/config.js'; import { getHook } from '../src/hook.js'; import { getGptSlotForAdUnitCode, logInfo, logWarn } from '../src/utils.js'; +import {IMP, PBS, registerOrtbProcessor, RESPONSE} from '../src/pbjsORTB.js'; const MODULE = 'fledgeForGpt' @@ -21,22 +22,20 @@ export function init(cfg) { getHook('addComponentAuction').before(addComponentAuctionHook); isEnabled = true; } - logInfo(MODULE, `isEnabled`, cfg); + logInfo(`${MODULE} enabled (browser ${isFledgeSupported() ? 'supports' : 'does NOT support'} fledge)`, cfg); } else { if (isEnabled) { getHook('addComponentAuction').getHooks({hook: addComponentAuctionHook}).remove(); isEnabled = false; } - logInfo(MODULE, `isDisabled`, cfg); + logInfo(`${MODULE} disabled`, cfg); } } -export function addComponentAuctionHook(next, bidRequest, componentAuctionConfig) { +export function addComponentAuctionHook(next, adUnitCode, componentAuctionConfig) { const seller = componentAuctionConfig.seller; - const adUnitCode = bidRequest.adUnitCode; const gptSlot = getGptSlotForAdUnitCode(adUnitCode); if (gptSlot && gptSlot.setConfig) { - delete componentAuctionConfig.bidId; gptSlot.setConfig({ componentAuction: [{ configKey: seller, @@ -48,5 +47,54 @@ export function addComponentAuctionHook(next, bidRequest, componentAuctionConfig logWarn(MODULE, `unable to register component auction config for: ${adUnitCode} x ${seller}.`); } - next(bidRequest, componentAuctionConfig); + next(adUnitCode, componentAuctionConfig); } + +function isFledgeSupported() { + return 'runAdAuction' in navigator && 'joinAdInterestGroup' in navigator +} + +export function markForFledge(next, bidderRequests) { + if (isFledgeSupported()) { + bidderRequests.forEach((req) => { + req.fledgeEnabled = config.runWithBidder(req.bidderCode, () => config.getConfig('fledgeEnabled')) + }) + } + next(bidderRequests); +} +getHook('makeBidRequests').after(markForFledge); + +export function setImpExtAe(imp, bidRequest, context) { + if (!context.bidderRequest.fledgeEnabled) { + delete imp.ext?.ae; + } +} +registerOrtbProcessor({type: IMP, name: 'impExtAe', fn: setImpExtAe}); + +// to make it easier to share code between the PBS adapter and adapters whose backend is PBS, break up +// fledge response processing in two steps: first aggregate all the auction configs by their imp... + +export function parseExtPrebidFledge(response, ortbResponse, context) { + (ortbResponse.ext?.prebid?.fledge?.auctionconfigs || []).forEach((cfg) => { + const impCtx = context.impContext[cfg.impid]; + if (!impCtx?.imp?.ext?.ae) { + logWarn('Received fledge auction configuration for an impression that was not in the request or did not ask for it', cfg, impCtx?.imp); + } else { + impCtx.fledgeConfigs = impCtx.fledgeConfigs || []; + impCtx.fledgeConfigs.push(cfg); + } + }) +} +registerOrtbProcessor({type: RESPONSE, name: 'extPrebidFledge', fn: parseExtPrebidFledge, dialects: [PBS]}); + +// ...then, make them available in the adapter's response. This is the client side version, for which the +// interpretResponse api is {fledgeAuctionConfigs: [{bidId, config}]} + +export function setResponseFledgeConfigs(response, ortbResponse, context) { + const configs = Object.values(context.impContext) + .flatMap((impCtx) => (impCtx.fledgeConfigs || []).map(cfg => ({bidId: impCtx.bidRequest.bidId, config: cfg.config}))); + if (configs.length > 0) { + response.fledgeAuctionConfigs = configs; + } +} +registerOrtbProcessor({type: RESPONSE, name: 'fledgeAuctionConfigs', priority: -1, fn: setResponseFledgeConfigs, dialects: [PBS]}) diff --git a/modules/flocIdSystem.js b/modules/flocIdSystem.js deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/modules/fluctBidAdapter.js b/modules/fluctBidAdapter.js index 004d44e6737..97b9847eae4 100644 --- a/modules/fluctBidAdapter.js +++ b/modules/fluctBidAdapter.js @@ -1,4 +1,5 @@ -import { _each, isEmpty } from '../src/utils.js'; +import { _each, deepSetValue, isEmpty } from '../src/utils.js'; +import { config } from '../src/config.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; const BIDDER_CODE = 'fluct'; @@ -39,13 +40,12 @@ export const spec = { */ buildRequests: (validBidRequests, bidderRequest) => { const serverRequests = []; - // TODO: is 'page' the right value here? - const referer = bidderRequest.refererInfo.page; + const page = bidderRequest.refererInfo.page; _each(validBidRequests, (request) => { const data = Object(); - data.referer = referer; + data.page = page; data.adUnitCode = request.adUnitCode; data.bidId = request.bidId; data.transactionId = request.transactionId; @@ -53,6 +53,21 @@ export const spec = { eids: (request.userIdAsEids || []).filter((eid) => SUPPORTED_USER_ID_SOURCES.indexOf(eid.source) !== -1) }; + if (bidderRequest.gdprConsent) { + deepSetValue(data, 'regs.gdpr', { + consent: bidderRequest.gdprConsent.consentString, + gdprApplies: bidderRequest.gdprConsent.gdprApplies ? 1 : 0, + }); + } + if (bidderRequest.uspConsent) { + deepSetValue(data, 'regs.us_privacy', { + consent: bidderRequest.uspConsent, + }); + } + if (config.getConfig('coppa') === true) { + deepSetValue(data, 'regs.coppa', 1); + } + data.sizes = []; _each(request.sizes, (size) => { data.sizes.push({ @@ -141,8 +156,22 @@ export const spec = { * */ getUserSyncs: (syncOptions, serverResponses) => { - return []; - }, + // gdpr, us_privacy, and coppa params to be handled on the server end. + const usersyncs = serverResponses.reduce((acc, serverResponse) => [ + ...acc, + ...(serverResponse.body.usersyncs ?? []), + ], []); + const syncs = usersyncs.filter( + (sync) => ( + (sync['type'] === 'image' && syncOptions.pixelEnabled) || + (sync['type'] === 'iframe' && syncOptions.iframeEnabled) + ) + ).map((sync) => ({ + type: sync.type, + url: sync.url, + })); + return syncs; + } }; registerBidder(spec); diff --git a/modules/fpdModule/index.md b/modules/fpdModule/index.md index 638c966883a..238881db96b 100644 --- a/modules/fpdModule/index.md +++ b/modules/fpdModule/index.md @@ -16,9 +16,11 @@ Validation Submodule: - verify that certain OpenRTB attributes are not specified - optionally suppress user FPD based on the existence of _pubcid_optout +Topic Submodule: +- populate first party/third party topics data onto user.data in bid stream. 1. Module initializes on first load and set bidRequestHook -2. When hook runs, corresponding submodule init functions are run to perform enrichments/validations dependant on submodule +2. When hook runs, corresponding submodule init functions are run to perform enrichments/validations/topics dependant on submodule 3. After hook complete, it is disabled - meaning module only runs on first auction 4. To reinitiate the module, run pbjs.refreshFPD(), which allows module to rerun as if initial load @@ -43,4 +45,5 @@ pbjs.setConfig({ At least one of the submodules must be included in order to successfully run the corresponding above operations. enrichmentFpdModule -validationFpdModule \ No newline at end of file +validationFpdModule +topicsFpdModule \ No newline at end of file diff --git a/modules/freewheel-sspBidAdapter.js b/modules/freewheel-sspBidAdapter.js index 764e3d6e6c0..8f5af48d16b 100644 --- a/modules/freewheel-sspBidAdapter.js +++ b/modules/freewheel-sspBidAdapter.js @@ -1,6 +1,7 @@ -import { logWarn, isArray } from '../src/utils.js'; +import { logWarn, isArray, isFn, deepAccess } from '../src/utils.js'; import { BANNER, VIDEO } from '../src/mediaTypes.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { config } from '../src/config.js'; const BIDDER_CODE = 'freewheel-ssp'; @@ -213,6 +214,27 @@ function getAPIName(componentId) { return componentId.replace('-', ''); } +function getBidFloor(bid, config) { + if (!isFn(bid.getFloor)) { + return deepAccess(bid, 'params.bidfloor', 0); + } + + try { + const bidFloor = bid.getFloor({ + currency: getFloorCurrency(config), + mediaType: typeof bid.mediaTypes['banner'] == 'object' ? 'banner' : 'video', + size: '*', + }); + return bidFloor.floor; + } catch (e) { + return -1; + } +} + +function getFloorCurrency(config) { + return config.getConfig('floors.data.currency') != null ? config.getConfig('floors.data.currency') : 'USD'; +} + function formatAdHTML(bid, size) { var integrationType = bid.params.format; @@ -317,13 +339,18 @@ export const spec = { var zone = currentBidRequest.params.zoneId; var timeInMillis = new Date().getTime(); var keyCode = hashcode(zone + '' + timeInMillis); + var bidfloor = getBidFloor(currentBidRequest, config); + var requestParams = { reqType: 'AdsSetup', - protocolVersion: '2.0', + protocolVersion: '4.2', zoneId: zone, componentId: 'prebid', componentSubId: getComponentId(currentBidRequest.params.format), timestamp: timeInMillis, + _fw_bidfloor: (bidfloor > 0) ? bidfloor : 0, + _fw_bidfloorcur: (bidfloor > 0) ? getFloorCurrency(config) : '', + pbjs_version: '$prebid.version$', pKey: keyCode }; @@ -348,7 +375,19 @@ export const spec = { // Add schain object var schain = currentBidRequest.schain; if (schain) { - requestParams.schain = schain; + try { + requestParams.schain = JSON.stringify(schain); + } catch (error) { + logWarn('PREBID - ' + BIDDER_CODE + ': Unable to stringify the schain: ' + error); + } + } + + if (currentBidRequest.userIdAsEids && currentBidRequest.userIdAsEids.length > 0) { + try { + requestParams._fw_prebid_3p_UID = JSON.stringify(currentBidRequest.userIdAsEids); + } catch (error) { + logWarn('PREBID - ' + BIDDER_CODE + ': Unable to stringify the userIdAsEids: ' + error); + } } var vastParams = currentBidRequest.params.vastUrlParams; @@ -385,6 +424,14 @@ export const spec = { requestParams.playerSize = playerSize[0] + 'x' + playerSize[1]; } + // Add video context and placement in requestParams + if (currentBidRequest.mediaTypes.video) { + var videoContext = currentBidRequest.mediaTypes.video.context ? currentBidRequest.mediaTypes.video.context : 'instream'; + var videoPlacement = currentBidRequest.mediaTypes.video.placement ? currentBidRequest.mediaTypes.video.placement : 1; + requestParams.video_context = videoContext; + requestParams.video_placement = videoPlacement; + } + return { method: 'GET', url: FREEWHEEL_ADSSETUP, diff --git a/modules/ftrackIdSystem.js b/modules/ftrackIdSystem.js index 244807a3164..5f09a315b34 100644 --- a/modules/ftrackIdSystem.js +++ b/modules/ftrackIdSystem.js @@ -6,19 +6,19 @@ */ import * as utils from '../src/utils.js'; -import { submodule } from '../src/hook.js'; -import { getStorageManager } from '../src/storageManager.js'; -import { uspDataHandler } from '../src/adapterManager.js'; -import { loadExternalScript } from '../src/adloader.js'; +import {submodule} from '../src/hook.js'; +import {getStorageManager} from '../src/storageManager.js'; +import {uspDataHandler} from '../src/adapterManager.js'; +import {loadExternalScript} from '../src/adloader.js'; +import {MODULE_TYPE_UID} from '../src/activities/modules.js'; const MODULE_NAME = 'ftrackId'; const LOG_PREFIX = 'FTRACK - '; const LOCAL_STORAGE_EXP_DAYS = 30; -const VENDOR_ID = null; const LOCAL_STORAGE = 'html5'; const FTRACK_STORAGE_NAME = 'ftrackId'; const FTRACK_PRIVACY_STORAGE_NAME = `${FTRACK_STORAGE_NAME}_privacy`; -const storage = getStorageManager({gvlid: VENDOR_ID, moduleName: MODULE_NAME}); +const storage = getStorageManager({moduleType: MODULE_TYPE_UID, moduleName: MODULE_NAME}); let consentInfo = { gdpr: { diff --git a/modules/gdprEnforcement.js b/modules/gdprEnforcement.js index 9553ad0586a..798dfc848da 100644 --- a/modules/gdprEnforcement.js +++ b/modules/gdprEnforcement.js @@ -11,7 +11,13 @@ import {getHook} from '../src/hook.js'; import {validateStorageEnforcement} from '../src/storageManager.js'; import * as events from '../src/events.js'; import CONSTANTS from '../src/constants.json'; -import {VENDORLESS_GVLID} from '../src/consentHandler.js'; +import {GDPR_GVLIDS, VENDORLESS_GVLID} from '../src/consentHandler.js'; +import { + MODULE_TYPE_ANALYTICS, + MODULE_TYPE_BIDDER, + MODULE_TYPE_CORE, MODULE_TYPE_RTD, + MODULE_TYPE_UID +} from '../src/activities/modules.js'; export const STRICT_STORAGE_ENFORCEMENT = 'strictStorageEnforcement'; @@ -49,77 +55,56 @@ const analyticsBlocked = []; let hooksAdded = false; let strictStorageEnforcement = false; -// Helps in stubbing these functions in unit tests. -export const internal = { - getGvlidForBidAdapter, - getGvlidForUserIdModule, - getGvlidForAnalyticsAdapter -}; +const GVLID_LOOKUP_PRIORITY = [ + MODULE_TYPE_BIDDER, + MODULE_TYPE_UID, + MODULE_TYPE_ANALYTICS, + MODULE_TYPE_RTD +]; /** - * Returns GVL ID for a Bid adapter / an USERID submodule / an Analytics adapter. - * If modules of different types have the same moduleCode: For example, 'appnexus' is the code for both Bid adapter and Analytics adapter, - * then, we assume that their GVL IDs are same. This function first checks if GVL ID is defined for a Bid adapter, if not found, tries to find User ID - * submodule's GVL ID, if not found, tries to find Analytics adapter's GVL ID. In this process, as soon as it finds a GVL ID, it returns it - * without going to the next check. - * @param {{string|Object}} - module - * @return {number} - GVL ID + * Retrieve a module's GVL ID. */ -export function getGvlid(module, ...args) { - let gvlid = null; - if (module) { +export function getGvlid(moduleType, moduleName, fallbackFn) { + if (moduleName) { // Check user defined GVL Mapping in pbjs.setConfig() const gvlMapping = config.getConfig('gvlMapping'); - // For USER ID Module, we pass the submodule object itself as the "module" parameter, this check is required to grab the module code - const moduleCode = typeof module === 'string' ? module : module.name; - // Return GVL ID from user defined gvlMapping - if (gvlMapping && gvlMapping[moduleCode]) { - gvlid = gvlMapping[moduleCode]; - return gvlid; - } - - gvlid = internal.getGvlidForBidAdapter(moduleCode) || internal.getGvlidForUserIdModule(module) || internal.getGvlidForAnalyticsAdapter(moduleCode, ...args); - } - return gvlid; -} - -/** - * Returns GVL ID for a bid adapter. If the adapter does not have an associated GVL ID, it returns 'null'. - * @param {string=} bidderCode - The 'code' property of the Bidder spec. - * @return {number} GVL ID - */ -function getGvlidForBidAdapter(bidderCode) { - let gvlid = null; - bidderCode = bidderCode || config.getCurrentBidder(); - if (bidderCode) { - const bidder = adapterManager.getBidAdapter(bidderCode); - if (bidder && bidder.getSpec) { - gvlid = bidder.getSpec().gvlid; + if (gvlMapping && gvlMapping[moduleName]) { + return gvlMapping[moduleName]; + } else if (moduleType === MODULE_TYPE_CORE) { + return VENDORLESS_GVLID; + } else { + let {gvlid, modules} = GDPR_GVLIDS.get(moduleName); + if (gvlid == null && Object.keys(modules).length > 0) { + // this behavior is for backwards compatibility; if multiple modules with the same + // name declare different GVL IDs, pick the bidder's first, then userId, then analytics + for (const type of GVLID_LOOKUP_PRIORITY) { + if (modules.hasOwnProperty(type)) { + gvlid = modules[type]; + if (type !== moduleType && !fallbackFn) { + logWarn(`Multiple GVL IDs found for module '${moduleName}'; using the ${type} module's ID (${gvlid}) instead of the ${moduleType}'s ID (${modules[moduleType]})`) + } + break; + } + } + } + if (gvlid == null && fallbackFn) { + gvlid = fallbackFn(); + } + return gvlid || null; } } - return gvlid; -} - -/** - * Returns GVL ID for an userId submodule. If an userId submodules does not have an associated GVL ID, it returns 'null'. - * @param {Object} userIdModule - * @return {number} GVL ID - */ -function getGvlidForUserIdModule(userIdModule) { - return (typeof userIdModule === 'object' ? userIdModule.gvlid : null); + return null; } /** - * Returns GVL ID for an analytics adapter. If an analytics adapter does not have an associated GVL ID, it returns 'null'. - * @param {string} code - 'provider' property on the analytics adapter config - * @param {{}} config - analytics configuration object - * @return {number} GVL ID + * Retrieve GVL IDs that are dynamically set on analytics adapters. */ -function getGvlidForAnalyticsAdapter(code, config) { +export function getGvlidFromAnalyticsAdapter(code, config) { const adapter = adapterManager.getAnalyticsAdapter(code); - return adapter?.gvlid || ((gvlid) => { + return ((gvlid) => { if (typeof gvlid !== 'function') return gvlid; try { return gvlid.call(adapter.adapter, config); @@ -185,30 +170,33 @@ export function validateRules(rule, consentData, currentModule, gvlId) { /** * This hook checks whether module has permission to access device or not. Device access include cookie and local storage + * * @param {Function} fn reference to original function (used by hook logic) - * @param {Number=} gvlid gvlid of the module + * @param {string} moduleType type of the module * @param {string=} moduleName name of the module * @param result + * @param validate */ -export function deviceAccessHook(fn, gvlid, moduleName, result, {validate = validateRules} = {}) { +export function deviceAccessHook(fn, moduleType, moduleName, result, {validate = validateRules} = {}) { result = Object.assign({}, { hasEnforcementHook: true }); if (!hasDeviceAccess()) { logWarn('Device access is disabled by Publisher'); result.valid = false; - } else if (gvlid === VENDORLESS_GVLID && !strictStorageEnforcement) { + } else if (moduleType === MODULE_TYPE_CORE && !strictStorageEnforcement) { // for vendorless (core) storage, do not enforce rules unless strictStorageEnforcement is set result.valid = true; } else { const consentData = gdprDataHandler.getConsentData(); + let gvlid; if (shouldEnforce(consentData, 1, moduleName)) { const curBidder = config.getCurrentBidder(); // Bidders have a copy of storage object with bidder code binded. Aliases will also pass the same bidder code when invoking storage functions and hence if alias tries to access device we will try to grab the gvl id for alias instead of original bidder if (curBidder && (curBidder !== moduleName) && adapterManager.aliasRegistry[curBidder] === moduleName) { - gvlid = getGvlid(curBidder); + gvlid = getGvlid(moduleType, curBidder); } else { - gvlid = getGvlid(moduleName) || gvlid; + gvlid = getGvlid(moduleType, moduleName) } const curModule = moduleName || curBidder; let isAllowed = validate(purpose1Rule, consentData, curModule, gvlid,); @@ -223,7 +211,7 @@ export function deviceAccessHook(fn, gvlid, moduleName, result, {validate = vali result.valid = true; } } - fn.call(this, gvlid, moduleName, result); + fn.call(this, moduleType, moduleName, result); } /** @@ -235,7 +223,7 @@ export function userSyncHook(fn, ...args) { const consentData = gdprDataHandler.getConsentData(); const curBidder = config.getCurrentBidder(); if (shouldEnforce(consentData, 1, curBidder)) { - const gvlid = getGvlid(curBidder); + const gvlid = getGvlid(MODULE_TYPE_BIDDER, curBidder); let isAllowed = validateRules(purpose1Rule, consentData, curBidder, gvlid); if (isAllowed) { fn.call(this, ...args); @@ -257,8 +245,8 @@ export function userSyncHook(fn, ...args) { export function userIdHook(fn, submodules, consentData) { if (shouldEnforce(consentData, 1, 'User ID')) { let userIdModules = submodules.map((submodule) => { - const gvlid = getGvlid(submodule.submodule); const moduleName = submodule.submodule.name; + const gvlid = getGvlid(MODULE_TYPE_UID, moduleName); let isAllowed = validateRules(purpose1Rule, consentData, moduleName, gvlid); if (isAllowed) { return submodule; @@ -286,7 +274,7 @@ export function makeBidRequestsHook(fn, adUnits, ...args) { adUnits.forEach(adUnit => { adUnit.bids = adUnit.bids.filter(bid => { const currBidder = bid.bidder; - const gvlId = getGvlid(currBidder); + const gvlId = getGvlid(MODULE_TYPE_BIDDER, currBidder); if (includes(biddersBlocked, currBidder)) return false; const isAllowed = !!validateRules(purpose2Rule, consentData, currBidder, gvlId); if (!isAllowed) { @@ -316,7 +304,7 @@ export function enableAnalyticsHook(fn, config) { } config = config.filter(conf => { const analyticsAdapterCode = conf.provider; - const gvlid = getGvlid(analyticsAdapterCode, conf); + const gvlid = getGvlid(MODULE_TYPE_ANALYTICS, analyticsAdapterCode, () => getGvlidFromAnalyticsAdapter(analyticsAdapterCode, conf)); const isAllowed = !!validateRules(purpose7Rule, consentData, analyticsAdapterCode, gvlid); if (!isAllowed) { analyticsBlocked.push(analyticsAdapterCode); diff --git a/modules/glimpseBidAdapter.js b/modules/glimpseBidAdapter.js index bbb4dbb30cd..53dc0bd3e1a 100644 --- a/modules/glimpseBidAdapter.js +++ b/modules/glimpseBidAdapter.js @@ -1,5 +1,4 @@ import { registerBidder } from '../src/adapters/bidderFactory.js'; -import { config } from '../src/config.js'; import { BANNER } from '../src/mediaTypes.js'; import { getStorageManager } from '../src/storageManager.js'; import { @@ -12,10 +11,7 @@ import { const GVLID = 1012; const BIDDER_CODE = 'glimpse'; -const storageManager = getStorageManager({ - gvlid: GVLID, - bidderCode: BIDDER_CODE, -}); +const storageManager = getStorageManager({bidderCode: BIDDER_CODE}); const ENDPOINT = 'https://market.glimpsevault.io/public/v1/prebid'; const LOCAL_STORAGE_KEY = { vault: { @@ -102,7 +98,7 @@ function getReferer(bidderRequest) { function buildQuery(bidderRequest) { let url = appendQueryParam(ENDPOINT, 'ver', '$prebid.version$'); - const timeout = config.getConfig('bidderTimeout'); + const timeout = bidderRequest.timeout; url = appendQueryParam(url, 'tmax', timeout); if (gdprApplies(bidderRequest)) { diff --git a/modules/globalsunBidAdapter.js b/modules/globalsunBidAdapter.js new file mode 100644 index 00000000000..5b5d97c2cac --- /dev/null +++ b/modules/globalsunBidAdapter.js @@ -0,0 +1,212 @@ +import { isFn, deepAccess, logMessage, logError } from '../src/utils.js'; +import { convertOrtbRequestToProprietaryNative } from '../src/native.js'; + +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { BANNER, NATIVE, VIDEO } from '../src/mediaTypes.js'; +import { config } from '../src/config.js'; + +const BIDDER_CODE = 'globalsun'; +const AD_URL = 'https://endpoint.globalsun.io/pbjs'; +const SYNC_URL = 'https://cs.globalsun.io'; + +function isBidResponseValid(bid) { + if (!bid.requestId || !bid.cpm || !bid.creativeId || !bid.ttl || !bid.currency) { + return false; + } + + switch (bid.mediaType) { + case BANNER: + return Boolean(bid.width && bid.height && bid.ad); + case VIDEO: + return Boolean(bid.vastUrl || bid.vastXml); + case NATIVE: + return Boolean(bid.native && bid.native.impressionTrackers && bid.native.impressionTrackers.length); + default: + return false; + } +} + +function getPlacementReqData(bid) { + const { params, bidId, mediaTypes } = bid; + const schain = bid.schain || {}; + const { placementId, endpointId } = params; + const bidfloor = getBidFloor(bid); + + const placement = { + bidId, + schain, + bidfloor + }; + + if (placementId) { + placement.placementId = placementId; + placement.type = 'publisher'; + } else if (endpointId) { + placement.endpointId = endpointId; + placement.type = 'network'; + } + + if (mediaTypes && mediaTypes[BANNER]) { + placement.adFormat = BANNER; + placement.sizes = mediaTypes[BANNER].sizes; + } else if (mediaTypes && mediaTypes[VIDEO]) { + placement.adFormat = VIDEO; + placement.playerSize = mediaTypes[VIDEO].playerSize; + placement.minduration = mediaTypes[VIDEO].minduration; + placement.maxduration = mediaTypes[VIDEO].maxduration; + placement.mimes = mediaTypes[VIDEO].mimes; + placement.protocols = mediaTypes[VIDEO].protocols; + placement.startdelay = mediaTypes[VIDEO].startdelay; + placement.placement = mediaTypes[VIDEO].placement; + placement.skip = mediaTypes[VIDEO].skip; + placement.skipafter = mediaTypes[VIDEO].skipafter; + placement.minbitrate = mediaTypes[VIDEO].minbitrate; + placement.maxbitrate = mediaTypes[VIDEO].maxbitrate; + placement.delivery = mediaTypes[VIDEO].delivery; + placement.playbackmethod = mediaTypes[VIDEO].playbackmethod; + placement.api = mediaTypes[VIDEO].api; + placement.linearity = mediaTypes[VIDEO].linearity; + } else if (mediaTypes && mediaTypes[NATIVE]) { + placement.native = mediaTypes[NATIVE]; + placement.adFormat = NATIVE; + } + + return placement; +} + +function getBidFloor(bid) { + if (!isFn(bid.getFloor)) { + return deepAccess(bid, 'params.bidfloor', 0); + } + + try { + const bidFloor = bid.getFloor({ + currency: 'USD', + mediaType: '*', + size: '*', + }); + return bidFloor.floor; + } catch (err) { + logError(err); + return 0; + } +} + +export const spec = { + code: BIDDER_CODE, + supportedMediaTypes: [BANNER, VIDEO, NATIVE], + + isBidRequestValid: (bid = {}) => { + const { params, bidId, mediaTypes } = bid; + let valid = Boolean(bidId && params && (params.placementId || params.endpointId)); + + if (mediaTypes && mediaTypes[BANNER]) { + valid = valid && Boolean(mediaTypes[BANNER] && mediaTypes[BANNER].sizes); + } else if (mediaTypes && mediaTypes[VIDEO]) { + valid = valid && Boolean(mediaTypes[VIDEO] && mediaTypes[VIDEO].playerSize); + } else if (mediaTypes && mediaTypes[NATIVE]) { + valid = valid && Boolean(mediaTypes[NATIVE]); + } else { + valid = false; + } + return valid; + }, + + buildRequests: (validBidRequests = [], bidderRequest = {}) => { + // convert Native ORTB definition to old-style prebid native definition + validBidRequests = convertOrtbRequestToProprietaryNative(validBidRequests); + + let deviceWidth = 0; + let deviceHeight = 0; + + let winLocation; + try { + const winTop = window.top; + deviceWidth = winTop.screen.width; + deviceHeight = winTop.screen.height; + winLocation = winTop.location; + } catch (e) { + logMessage(e); + winLocation = window.location; + } + + const refferUrl = bidderRequest.refererInfo && bidderRequest.refererInfo.page; + let refferLocation; + try { + refferLocation = refferUrl && new URL(refferUrl); + } catch (e) { + logMessage(e); + } + // TODO: does the fallback make sense here? + let location = refferLocation || winLocation; + const language = (navigator && navigator.language) ? navigator.language.split('-')[0] : ''; + const host = location.host; + const page = location.pathname; + const secure = location.protocol === 'https:' ? 1 : 0; + const placements = []; + const request = { + deviceWidth, + deviceHeight, + language, + secure, + host, + page, + placements, + coppa: config.getConfig('coppa') === true ? 1 : 0, + ccpa: bidderRequest.uspConsent || undefined, + gdpr: bidderRequest.gdprConsent || undefined, + tmax: bidderRequest.timeout + }; + + const len = validBidRequests.length; + for (let i = 0; i < len; i++) { + const bid = validBidRequests[i]; + placements.push(getPlacementReqData(bid)); + } + + return { + method: 'POST', + url: AD_URL, + data: request + }; + }, + + interpretResponse: (serverResponse) => { + let response = []; + for (let i = 0; i < serverResponse.body.length; i++) { + let resItem = serverResponse.body[i]; + if (isBidResponseValid(resItem)) { + const advertiserDomains = resItem.adomain && resItem.adomain.length ? resItem.adomain : []; + resItem.meta = { ...resItem.meta, advertiserDomains }; + + response.push(resItem); + } + } + return response; + }, + + getUserSyncs: (syncOptions, serverResponses, gdprConsent, uspConsent) => { + let syncType = syncOptions.iframeEnabled ? 'iframe' : 'image'; + let syncUrl = SYNC_URL + `/${syncType}?pbjs=1`; + if (gdprConsent && gdprConsent.consentString) { + if (typeof gdprConsent.gdprApplies === 'boolean') { + syncUrl += `&gdpr=${Number(gdprConsent.gdprApplies)}&gdpr_consent=${gdprConsent.consentString}`; + } else { + syncUrl += `&gdpr=0&gdpr_consent=${gdprConsent.consentString}`; + } + } + if (uspConsent && uspConsent.consentString) { + syncUrl += `&ccpa_consent=${uspConsent.consentString}`; + } + + const coppa = config.getConfig('coppa') ? 1 : 0; + syncUrl += `&coppa=${coppa}`; + + return [{ + type: syncType, + url: syncUrl + }]; + } +}; + +registerBidder(spec); diff --git a/modules/globalsunBidAdapter.md b/modules/globalsunBidAdapter.md new file mode 100644 index 00000000000..07c3ce32155 --- /dev/null +++ b/modules/globalsunBidAdapter.md @@ -0,0 +1,79 @@ +# Overview + +``` +Module Name: Globalsun Bidder Adapter +Module Type: Globalsun Bidder Adapter +Maintainer: prebid@globalsun.io +``` + +# Description + +Connects to Globalsun exchange for bids. +Globalsun bid adapter supports Banner, Video (instream and outstream) and Native. + +# Test Parameters +``` + var adUnits = [ + // Will return static test banner + { + code: 'adunit1', + mediaTypes: { + banner: { + sizes: [ [300, 250], [320, 50] ], + } + }, + bids: [ + { + bidder: 'globalsun', + params: { + placementId: 'testBanner', + } + } + ] + }, + { + code: 'addunit2', + mediaTypes: { + video: { + playerSize: [ [640, 480] ], + context: 'instream', + minduration: 5, + maxduration: 60, + } + }, + bids: [ + { + bidder: 'globalsun', + params: { + placementId: 'testVideo', + } + } + ] + }, + { + code: 'addunit3', + mediaTypes: { + native: { + title: { + required: true + }, + body: { + required: true + }, + icon: { + required: true, + size: [64, 64] + } + } + }, + bids: [ + { + bidder: 'globalsun', + params: { + placementId: 'testNative', + } + } + ] + } + ]; +``` diff --git a/modules/gnetBidAdapter.js b/modules/gnetBidAdapter.js index 8bab043d0db..0b02a29c0d4 100644 --- a/modules/gnetBidAdapter.js +++ b/modules/gnetBidAdapter.js @@ -4,10 +4,9 @@ import { BANNER } from '../src/mediaTypes.js'; import { getStorageManager } from '../src/storageManager.js'; import {ajax} from '../src/ajax.js'; -const storage = getStorageManager(); - const BIDDER_CODE = 'gnet'; const ENDPOINT = 'https://service.gnetrtb.com/api'; +const storage = getStorageManager({bidderCode: BIDDER_CODE}); export const spec = { code: BIDDER_CODE, diff --git a/modules/gravitoIdSystem.js b/modules/gravitoIdSystem.js index 809263a1c68..aa25ea7db2c 100644 --- a/modules/gravitoIdSystem.js +++ b/modules/gravitoIdSystem.js @@ -6,9 +6,11 @@ */ import { submodule } from '../src/hook.js'; -import { getStorageManager } from '../src/storageManager.js'; +import {getStorageManager} from '../src/storageManager.js'; +import {MODULE_TYPE_UID} from '../src/activities/modules.js'; -export const storage = getStorageManager(); +const MODULE_NAME = 'gravitompId'; +export const storage = getStorageManager({moduleType: MODULE_TYPE_UID, moduleName: MODULE_NAME}); export const cookieKey = 'gravitompId'; @@ -17,7 +19,7 @@ export const gravitoIdSystemSubmodule = { * used to link submodule with config * @type {string} */ - name: 'gravitompId', + name: MODULE_NAME, /** * performs action to obtain id diff --git a/modules/greenbidsAnalyticsAdapter.js b/modules/greenbidsAnalyticsAdapter.js new file mode 100644 index 00000000000..38ed14dd295 --- /dev/null +++ b/modules/greenbidsAnalyticsAdapter.js @@ -0,0 +1,181 @@ +import {ajax} from '../src/ajax.js'; +import adapter from '../libraries/analyticsAdapter/AnalyticsAdapter.js'; +import CONSTANTS from '../src/constants.json'; +import adapterManager from '../src/adapterManager.js'; +import {deepClone, logError, logInfo} from '../src/utils.js'; + +const analyticsType = 'endpoint'; + +export const ANALYTICS_VERSION = '1.0.0'; + +const ANALYTICS_SERVER = 'https://europe-west2-greenbids-357713.cloudfunctions.net/publisher-analytics-endpoint'; + +const { + EVENTS: { + AUCTION_END, + BID_TIMEOUT, + } +} = CONSTANTS; + +export const BIDDER_STATUS = { + BID: 'bid', + NO_BID: 'noBid', + TIMEOUT: 'timeout' +}; + +const analyticsOptions = {}; + +export const parseBidderCode = function (bid) { + let bidderCode = bid.bidderCode || bid.bidder; + return bidderCode.toLowerCase(); +}; + +export const greenbidsAnalyticsAdapter = Object.assign(adapter({ANALYTICS_SERVER, analyticsType}), { + + cachedAuctions: {}, + + initConfig(config) { + /** + * Required option: pbuid + * @type {boolean} + */ + analyticsOptions.options = deepClone(config.options); + if (typeof config.options.pbuid !== 'string' || config.options.pbuid.length < 1) { + logError('"options.pbuid" is required.'); + return false; + } + analyticsOptions.sampled = true; + if (typeof config.options.sampling === 'number') { + analyticsOptions.sampled = Math.random() < parseFloat(config.options.sampling); + } + + analyticsOptions.pbuid = config.options.pbuid + analyticsOptions.server = ANALYTICS_SERVER; + return true; + }, + sendEventMessage(endPoint, data) { + logInfo(`AJAX: ${endPoint}: ` + JSON.stringify(data)); + + ajax(`${analyticsOptions.server}${endPoint}`, null, JSON.stringify(data), { + contentType: 'application/json' + }); + }, + createCommonMessage(auctionId) { + return { + version: ANALYTICS_VERSION, + auctionId: auctionId, + referrer: window.location.href, + sampling: analyticsOptions.options.sampling, + prebid: '$prebid.version$', + pbuid: analyticsOptions.pbuid, + adUnits: [], + }; + }, + serializeBidResponse(bid, status) { + return { + bidder: bid.bidder, + isTimeout: (status === BIDDER_STATUS.TIMEOUT), + hasBid: (status === BIDDER_STATUS.BID), + }; + }, + addBidResponseToMessage(message, bid, status) { + const adUnitCode = bid.adUnitCode.toLowerCase(); + const adUnitIndex = message.adUnits.findIndex((adUnit) => { + return adUnit.code === adUnitCode; + }); + if (adUnitIndex === -1) { + logError('Trying to add to non registered adunit'); + return; + } + const bidderIndex = message.adUnits[adUnitIndex].bidders.findIndex((bidder) => { + return bidder.bidder === bid.bidder; + }); + if (bidderIndex === -1) { + message.adUnits[adUnitIndex].bidders.push(this.serializeBidResponse(bid, status)); + } else { + if (status === BIDDER_STATUS.BID) { + message.adUnits[adUnitIndex].bidders[bidderIndex].hasBid = true; + } else if (status === BIDDER_STATUS.TIMEOUT) { + message.adUnits[adUnitIndex].bidders[bidderIndex].isTimeout = true; + } + } + }, + createBidMessage(auctionEndArgs, timeoutBids) { + logInfo(auctionEndArgs) + const {auctionId, timestamp, auctionEnd, adUnits, bidsReceived, noBids} = auctionEndArgs; + const message = this.createCommonMessage(auctionId); + + message.auctionElapsed = (auctionEnd - timestamp); + + adUnits.forEach((adUnit) => { + const adUnitCode = adUnit.code.toLowerCase(); + message.adUnits.push({ + code: adUnitCode, + mediaTypes: { + ...(adUnit.mediaTypes.banner !== undefined) && {banner: adUnit.mediaTypes.banner}, + ...(adUnit.mediaTypes.video !== undefined) && {video: adUnit.mediaTypes.video}, + ...(adUnit.mediaTypes.native !== undefined) && {native: adUnit.mediaTypes.native} + }, + bidders: [], + }); + }); + + // We enrich noBid then bids, then timeouts, because in case of a timeout, one response from a bidder + // Can be in the 3 arrays, and we want that case reflected in the call + noBids.forEach(bid => this.addBidResponseToMessage(message, bid, BIDDER_STATUS.NO_BID)); + + bidsReceived.forEach(bid => this.addBidResponseToMessage(message, bid, BIDDER_STATUS.BID)); + + timeoutBids.forEach(bid => this.addBidResponseToMessage(message, bid, BIDDER_STATUS.TIMEOUT)); + + return message; + }, + getCachedAuction(auctionId) { + this.cachedAuctions[auctionId] = this.cachedAuctions[auctionId] || { + timeoutBids: [], + }; + return this.cachedAuctions[auctionId]; + }, + handleAuctionEnd(auctionEndArgs) { + const cachedAuction = this.getCachedAuction(auctionEndArgs.auctionId); + this.sendEventMessage('/', + this.createBidMessage(auctionEndArgs, cachedAuction.timeoutBids) + ); + }, + handleBidTimeout(timeoutBids) { + timeoutBids.forEach((bid) => { + const cachedAuction = this.getCachedAuction(bid.auctionId); + cachedAuction.timeoutBids.push(bid); + }); + }, + track({eventType, args}) { + if (analyticsOptions.sampled) { + switch (eventType) { + case BID_TIMEOUT: + this.handleBidTimeout(args); + break; + case AUCTION_END: + this.handleAuctionEnd(args); + break; + } + } + }, + getAnalyticsOptions() { + return analyticsOptions; + }, +}); + +greenbidsAnalyticsAdapter.originEnableAnalytics = greenbidsAnalyticsAdapter.enableAnalytics; + +greenbidsAnalyticsAdapter.enableAnalytics = function(config) { + this.initConfig(config); + logInfo('loading greenbids analytics'); + greenbidsAnalyticsAdapter.originEnableAnalytics(config); +}; + +adapterManager.registerAnalyticsAdapter({ + adapter: greenbidsAnalyticsAdapter, + code: 'greenbids' +}); + +export default greenbidsAnalyticsAdapter; diff --git a/modules/greenbidsAnalyticsAdapter.md b/modules/greenbidsAnalyticsAdapter.md new file mode 100644 index 00000000000..46e3af2c5e2 --- /dev/null +++ b/modules/greenbidsAnalyticsAdapter.md @@ -0,0 +1,23 @@ +# Overview + +``` +Module Name: Greenbids Analytics Adapter +Module Type: Analytics Adapter +Maintainer: jb@greenbids.ai +``` + +# Description + +Analytics adapter for Greenbids + +# Test Parameters + +``` +{ + provider: 'greenbids', + options: { + pbuid: "PBUID_FROM_GREENBIDS" + sampling: 1.0 + } +} +``` diff --git a/modules/gridBidAdapter.js b/modules/gridBidAdapter.js index b300c5c58c0..792dc3b15b0 100644 --- a/modules/gridBidAdapter.js +++ b/modules/gridBidAdapter.js @@ -31,7 +31,7 @@ const TIME_TO_LIVE = 360; const USER_ID_KEY = 'tmguid'; const GVLID = 686; const RENDERER_URL = 'https://acdn.adnxs.com/video/outstream/ANOutstreamVideo.js'; -export const storage = getStorageManager({gvlid: GVLID, bidderCode: BIDDER_CODE}); +export const storage = getStorageManager({bidderCode: BIDDER_CODE}); const LOG_ERROR_MESS = { noAuid: 'Bid from response has no auid parameter - ', @@ -83,7 +83,6 @@ export const spec = { return null; } let pageKeywords = null; - let jwpseg = null; let content = null; let schain = null; let userIdAsEids = null; @@ -91,11 +90,10 @@ export const spec = { let userExt = null; let endpoint = null; let forceBidderName = false; - let {bidderRequestId, auctionId, gdprConsent, uspConsent, timeout, refererInfo} = bidderRequest || {}; + let {bidderRequestId, auctionId, gdprConsent, uspConsent, timeout, refererInfo, gppConsent} = bidderRequest || {}; const referer = refererInfo ? encodeURIComponent(refererInfo.page) : ''; - const bidderTimeout = config.getConfig('bidderTimeout') || timeout; - const tmax = timeout ? Math.min(bidderTimeout, timeout) : bidderTimeout; + const tmax = timeout; const imp = []; const bidsMap = {}; const requests = []; @@ -129,16 +127,11 @@ export const spec = { endpoint = ALIAS_CONFIG[bid.bidder] && ALIAS_CONFIG[bid.bidder].endpoint; } const { params: { uid, keywords, forceBidder, multiRequest }, mediaTypes, bidId, adUnitCode, rtd, ortb2Imp } = bid; - const { pubdata, secid, pubid, source, content: bidParamsContent } = bid.params; + const { secid, pubid, source, content: bidParamsContent } = bid.params; const bidFloor = _getFloor(mediaTypes || {}, bid); const jwTargeting = rtd && rtd.jwplayer && rtd.jwplayer.targeting; - if (jwTargeting) { - if (!jwpseg && jwTargeting.segments) { - jwpseg = jwTargeting.segments; - } - if (!content && jwTargeting.content) { - content = jwTargeting.content; - } + if (jwTargeting && !content && jwTargeting.content) { + content = jwTargeting.content; } let impObj = { @@ -152,12 +145,18 @@ export const spec = { if (ortb2Imp.instl) { impObj.instl = ortb2Imp.instl; } - if (ortb2Imp.ext && ortb2Imp.ext.data) { - impObj.ext.data = ortb2Imp.ext.data; - if (impObj.ext.data.adserver && impObj.ext.data.adserver.adslot) { - impObj.ext.gpid = impObj.ext.data.adserver.adslot.toString(); - } else { - impObj.ext.gpid = ortb2Imp.ext.data.pbadslot && ortb2Imp.ext.data.pbadslot.toString(); + + if (ortb2Imp.ext) { + if (ortb2Imp.ext.data) { + impObj.ext.data = ortb2Imp.ext.data; + if (impObj.ext.data.adserver && impObj.ext.data.adserver.adslot) { + impObj.ext.gpid = impObj.ext.data.adserver.adslot.toString(); + } else if (ortb2Imp.ext.data.pbadslot) { + impObj.ext.gpid = ortb2Imp.ext.data.pbadslot.toString(); + } + } + if (ortb2Imp.ext.gpid) { + impObj.ext.gpid = ortb2Imp.ext.gpid.toString(); } } } @@ -211,23 +210,12 @@ export const spec = { request.site.publisher = { id: pubid }; } - const reqJwpseg = (pubdata && pubdata.jwpseg) || (jwTargeting && jwTargeting.segments); - const siteContent = bidParamsContent || (jwTargeting && jwTargeting.content); if (siteContent) { request.site.content = siteContent; } - if (reqJwpseg && reqJwpseg.length) { - request.user = { - data: [{ - name: 'iow_labs_pub_data', - segment: segmentProcessing(reqJwpseg, 'jwpseg'), - }] - }; - } - requests.push(request); sources.push(source); bidsArray.push(bidObject); @@ -275,26 +263,16 @@ export const spec = { mainRequest.site.content = content; } - if (jwpseg && jwpseg.length) { - mainRequest.user = { - data: [{ - name: 'iow_labs_pub_data', - segment: segmentProcessing(jwpseg, 'jwpseg'), - }] - }; - } - [...requests, mainRequest].forEach((request) => { if (!request) { return; } + user = null; + const ortb2UserData = deepAccess(bidderRequest, 'ortb2.user.data'); if (ortb2UserData && ortb2UserData.length) { - user = request.user || { data: [] }; - user = mergeDeep(user, { - data: [...ortb2UserData] - }); + user = { data: [...ortb2UserData] }; } if (gdprConsent && gdprConsent.consentString) { @@ -368,11 +346,22 @@ export const spec = { } }; } + const ortb2Regs = deepAccess(bidderRequest, 'ortb2.regs') || {}; + if (gppConsent || ortb2Regs?.gpp) { + const gpp = { + gpp: gppConsent?.gppString ?? ortb2Regs?.gpp, + gpp_sid: gppConsent?.applicableSections ?? ortb2Regs?.gpp_sid + }; + request.regs = mergeDeep(request?.regs ?? {}, gpp); + } if (uspConsent) { if (!request.regs) { request.regs = {ext: {}}; } + if (!request.regs.ext) { + request.regs.ext = {}; + } request.regs.ext.us_privacy = uspConsent; } @@ -649,22 +638,6 @@ function getUserIdFromFPDStorage() { return storage.getDataFromLocalStorage(USER_ID_KEY) || makeNewUserIdInFPDStorage(); } -function segmentProcessing(segment, forceSegName) { - return segment - .map((seg) => { - const value = seg && (seg.id || seg); - if (typeof value === 'string' || typeof value === 'number') { - return { - value: value.toString(), - ...(forceSegName && { name: forceSegName }), - ...(seg.name && { name: seg.name }), - }; - } - return null; - }) - .filter((seg) => !!seg); -} - function reformatKeywords(pageKeywords) { const formatedPageKeywords = {}; Object.keys(pageKeywords).forEach((name) => { diff --git a/modules/gridNMBidAdapter.js b/modules/gridNMBidAdapter.js index c49b7619c07..36038d521bd 100644 --- a/modules/gridNMBidAdapter.js +++ b/modules/gridNMBidAdapter.js @@ -90,13 +90,12 @@ export const spec = { auctionId = bid.auctionId; } const { - params: { floorcpm, pubdata, source, secid, pubid, content, video }, + params: { floorcpm, source, secid, pubid, content, video }, mediaTypes, bidId, adUnitCode, rtd, ortb2Imp, sizes } = bid; const bidFloor = _getFloor(mediaTypes || {}, bid, isNumber(floorcpm) && floorcpm); const jwTargeting = rtd && rtd.jwplayer && rtd.jwplayer.targeting; - const jwpseg = (pubdata && pubdata.jwpseg) || (jwTargeting && jwTargeting.segments); const siteContent = content || (jwTargeting && jwTargeting.content); @@ -136,7 +135,7 @@ export const spec = { reqSource.ext.schain = schain; } - const bidderTimeout = config.getConfig('bidderTimeout') || timeout; + const bidderTimeout = timeout; const tmax = timeout ? Math.min(bidderTimeout, timeout) : bidderTimeout; const request = { @@ -156,15 +155,6 @@ export const spec = { request.site.content = siteContent; } - if (jwpseg && jwpseg.length) { - user = { - data: [{ - name: 'iow_labs_pub_data', - segment: segmentProcessing(jwpseg, 'jwpseg'), - }] - }; - } - if (gdprConsent && gdprConsent.consentString) { userExt = { consent: gdprConsent.consentString }; } @@ -429,20 +419,4 @@ export function getSyncUrl() { return SYNC_URL; } -function segmentProcessing(segment, forceSegName) { - return segment - .map((seg) => { - const value = seg && (seg.id || seg); - if (typeof value === 'string' || typeof value === 'number') { - return { - value: value.toString(), - ...(forceSegName && { name: forceSegName }), - ...(seg.name && { name: seg.name }), - }; - } - return null; - }) - .filter((seg) => !!seg); -} - registerBidder(spec); diff --git a/modules/growthCodeAnalyticsAdapter.js b/modules/growthCodeAnalyticsAdapter.js index 1f11b891139..a2ab4ddbfac 100644 --- a/modules/growthCodeAnalyticsAdapter.js +++ b/modules/growthCodeAnalyticsAdapter.js @@ -6,15 +6,16 @@ import adapter from '../libraries/analyticsAdapter/AnalyticsAdapter.js'; import adapterManager from '../src/adapterManager.js'; import * as utils from '../src/utils.js'; import CONSTANTS from '../src/constants.json'; -import { getStorageManager } from '../src/storageManager.js'; +import {getStorageManager} from '../src/storageManager.js'; import {getRefererInfo} from '../src/refererDetection.js'; import {logError, logInfo} from '../src/utils.js'; +import {MODULE_TYPE_ANALYTICS} from '../src/activities/modules.js'; const MODULE_NAME = 'growthCodeAnalytics'; const DEFAULT_PID = 'INVALID_PID' const ENDPOINT_URL = 'https://p2.gcprivacy.com/v1/pb/analytics' -export const storage = getStorageManager(); +export const storage = getStorageManager({moduleType: MODULE_TYPE_ANALYTICS, moduleName: MODULE_NAME}); let sessionId = utils.generateUUID(); diff --git a/modules/growthCodeIdSystem.js b/modules/growthCodeIdSystem.js index edd7cd33012..fae022f1a56 100644 --- a/modules/growthCodeIdSystem.js +++ b/modules/growthCodeIdSystem.js @@ -8,14 +8,15 @@ import {logError, logInfo, tryAppendQueryString} from '../src/utils.js'; import {ajax} from '../src/ajax.js'; import { submodule } from '../src/hook.js' -import { getStorageManager } from '../src/storageManager.js'; +import {getStorageManager} from '../src/storageManager.js'; +import {MODULE_TYPE_UID} from '../src/activities/modules.js'; -const GCID_EXPIRY = 7; const MODULE_NAME = 'growthCodeId'; const GC_DATA_KEY = '_gc_data'; +const GCID_KEY = 'gcid'; const ENDPOINT_URL = 'https://p2.gcprivacy.com/v1/pb?' -export const storage = getStorageManager({ gvlid: undefined, moduleName: MODULE_NAME }); +export const storage = getStorageManager({ moduleType: MODULE_TYPE_UID, moduleName: MODULE_NAME }); /** * Read GrowthCode data from cookie or local storage @@ -25,14 +26,16 @@ export const storage = getStorageManager({ gvlid: undefined, moduleName: MODULE_ export function readData(key) { try { let payload - if (storage.cookiesAreEnabled()) { - payload = tryParse(storage.getCookie(key)) + if (storage.cookiesAreEnabled(null)) { + payload = tryParse(storage.getCookie(key, null)) } if (storage.hasLocalStorage()) { - payload = tryParse(storage.getDataFromLocalStorage(key)) + payload = tryParse(storage.getDataFromLocalStorage(key, null)) } - if ((payload.expire_at !== undefined) && (payload.expire_at > (Date.now() / 1000))) { - return payload + if (payload !== undefined) { + if (payload.expire_at > (Date.now() / 1000)) { + return payload + } } } catch (error) { logError(error); @@ -50,12 +53,8 @@ function storeData(key, value) { logInfo(MODULE_NAME + ': storing data: key=' + key + ' value=' + value); if (value) { - if (storage.hasLocalStorage()) { - storage.setDataInLocalStorage(key, value); - } - const expiresStr = (new Date(Date.now() + (GCID_EXPIRY * (60 * 60 * 24 * 1000)))).toUTCString(); - if (storage.cookiesAreEnabled()) { - storage.setCookie(key, value, expiresStr, 'LAX'); + if (storage.hasLocalStorage(null)) { + storage.setDataInLocalStorage(key, value, null); } } } catch (error) { @@ -69,11 +68,15 @@ function storeData(key, value) { * @param {object|null} */ function tryParse(data) { + let payload; try { - return JSON.parse(data); + payload = JSON.parse(data); + if (payload == null) { + return undefined + } + return payload } catch (err) { - logError(err); - return null; + return undefined; } } @@ -149,6 +152,7 @@ export const growthCodeIdSubmodule = { // If response is a valid json and should save is true if (respJson) { storeData(GC_DATA_KEY, JSON.stringify(respJson)) + storeData(GCID_KEY, respJson.gc_id); callback(respJson); } else { callback(); diff --git a/modules/gumgumBidAdapter.js b/modules/gumgumBidAdapter.js index 93306984ab1..8a6c4efa7fc 100644 --- a/modules/gumgumBidAdapter.js +++ b/modules/gumgumBidAdapter.js @@ -6,13 +6,13 @@ import {getStorageManager} from '../src/storageManager.js'; import {includes} from '../src/polyfill.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; -const BIDDER_CODE = 'gumgum' +const BIDDER_CODE = 'gumgum'; const storage = getStorageManager({bidderCode: BIDDER_CODE}); -const ALIAS_BIDDER_CODE = ['gg'] -const BID_ENDPOINT = `https://g2.gumgum.com/hbid/imp` +const ALIAS_BIDDER_CODE = ['gg']; +const BID_ENDPOINT = `https://g2.gumgum.com/hbid/imp`; const JCSI = { t: 0, rq: 8, pbv: '$prebid.version$' } -const SUPPORTED_MEDIA_TYPES = [BANNER, VIDEO] -const TIME_TO_LIVE = 60 +const SUPPORTED_MEDIA_TYPES = [BANNER, VIDEO]; +const TIME_TO_LIVE = 60; const DELAY_REQUEST_TIME = 1800000; // setting to 30 mins let invalidRequestIds = {}; @@ -99,17 +99,6 @@ function getWrapperCode(wrapper, data) { return wrapper.replace('AD_JSON', window.btoa(JSON.stringify(data))) } -function _getDigiTrustQueryParams(userId) { - let digiTrustId = userId.digitrustid && userId.digitrustid.data; - // Verify there is an ID and this user has not opted out - if (!digiTrustId || (digiTrustId.privacy && digiTrustId.privacy.optout)) { - return {}; - } - return { - dt: digiTrustId.id - }; -} - /** * Serializes the supply chain object according to IAB standards * @see https://github.com/InteractiveAdvertisingBureau/openrtb/blob/master/supplychainobject.md @@ -264,7 +253,8 @@ function getEids(userId) { const idProperties = [ 'uid', 'eid', - 'lipbid' + 'lipbid', + 'envelope' ]; return Object.keys(userId).reduce(function (eids, provider) { @@ -293,7 +283,8 @@ function buildRequests(validBidRequests, bidderRequest) { const bids = []; const gdprConsent = bidderRequest && bidderRequest.gdprConsent; const uspConsent = bidderRequest && bidderRequest.uspConsent; - const timeout = config.getConfig('bidderTimeout'); + const gppConsent = bidderRequest && bidderRequest.gppConsent; + const timeout = bidderRequest && bidderRequest.timeout const coppa = config.getConfig('coppa') === true ? 1 : 0; const topWindowUrl = bidderRequest && bidderRequest.refererInfo && bidderRequest.refererInfo.page; _each(validBidRequests, bidRequest => { @@ -387,6 +378,17 @@ function buildRequests(validBidRequests, bidderRequest) { if (uspConsent) { data.uspConsent = uspConsent; } + if (gppConsent) { + data.gppConsent = { + gppString: bidderRequest.gppConsent.gppString, + gpp_sid: bidderRequest.gppConsent.applicableSections + } + } else if (!gppConsent && bidderRequest?.ortb2?.regs?.gpp) { + data.gppConsent = { + gppString: bidderRequest.ortb2.regs.gpp, + gpp_sid: bidderRequest.ortb2.regs.gpp_sid + }; + } if (coppa) { data.coppa = coppa; } @@ -403,7 +405,7 @@ function buildRequests(validBidRequests, bidderRequest) { sizes, url: BID_ENDPOINT, method: 'GET', - data: Object.assign(data, _getBrowserParams(topWindowUrl), _getDigiTrustQueryParams(userId)) + data: Object.assign(data, _getBrowserParams(topWindowUrl)) }); }); return bids; @@ -583,6 +585,7 @@ function getUserSyncs(syncOptions, serverResponses) { export const spec = { code: BIDDER_CODE, + gvlid: 61, aliases: ALIAS_BIDDER_CODE, isBidRequestValid, buildRequests, diff --git a/modules/hadronAnalyticsAdapter.js b/modules/hadronAnalyticsAdapter.js index 52829cf754d..e4c09c5b6c9 100644 --- a/modules/hadronAnalyticsAdapter.js +++ b/modules/hadronAnalyticsAdapter.js @@ -3,8 +3,9 @@ import adapter from '../libraries/analyticsAdapter/AnalyticsAdapter.js'; import adapterManager from '../src/adapterManager.js'; import * as utils from '../src/utils.js'; import CONSTANTS from '../src/constants.json'; -import { getStorageManager } from '../src/storageManager.js'; +import {getStorageManager} from '../src/storageManager.js'; import {getRefererInfo} from '../src/refererDetection.js'; +import {MODULE_TYPE_ANALYTICS} from '../src/activities/modules.js'; /** * hadronAnalyticsAdapter.js - Audigent Hadron Analytics Adapter @@ -14,8 +15,9 @@ const HADRON_ANALYTICS_URL = 'https://analytics.hadron.ad.gt/api/v1/analytics'; const HADRONID_ANALYTICS_VER = 'pbadgt0'; const DEFAULT_PARTNER_ID = 0; const AU_GVLID = 561; +const MODULE_CODE = 'hadronAnalytics'; -export const storage = getStorageManager(); +export const storage = getStorageManager({moduleType: MODULE_TYPE_ANALYTICS, moduleName: MODULE_CODE}); var viewId = utils.generateUUID(); @@ -191,7 +193,7 @@ function sendEvent(event) { adapterManager.registerAnalyticsAdapter({ adapter: hadronAnalyticsAdapter, - code: 'hadronAnalytics', + code: MODULE_CODE, gvlid: AU_GVLID }); diff --git a/modules/hadronIdSystem.js b/modules/hadronIdSystem.js index 2f10245cd59..596bf9611e6 100644 --- a/modules/hadronIdSystem.js +++ b/modules/hadronIdSystem.js @@ -8,18 +8,21 @@ import {ajax} from '../src/ajax.js'; import {getStorageManager} from '../src/storageManager.js'; import {submodule} from '../src/hook.js'; -import { isFn, isStr, isPlainObject, logError } from '../src/utils.js'; +import {isFn, isStr, isPlainObject, logError, logInfo} from '../src/utils.js'; +import {MODULE_TYPE_UID} from '../src/activities/modules.js'; +const HADRONID_LOCAL_NAME = 'auHadronId'; const MODULE_NAME = 'hadronId'; const AU_GVLID = 561; const DEFAULT_HADRON_URL_ENDPOINT = 'https://id.hadron.ad.gt/api/v1/pbhid'; -export const storage = getStorageManager({gvlid: AU_GVLID, moduleName: 'hadron'}); +export const storage = getStorageManager({moduleType: MODULE_TYPE_UID, moduleName: 'hadron'}); /** * Param or default. - * @param {String} param + * @param {String|function} param * @param {String} defaultVal + * @param arg */ function paramOrDefault(param, defaultVal, arg) { if (isFn(param)) { @@ -46,6 +49,7 @@ export const hadronIdSubmodule = { * @type {string} */ name: MODULE_NAME, + gvlid: AU_GVLID, /** * decode the stored id value for passing to bid requests * @function @@ -53,11 +57,11 @@ export const hadronIdSubmodule = { * @returns {Object} */ decode(value) { - let hadronId = storage.getDataFromLocalStorage('auHadronId'); + const hadronId = storage.getDataFromLocalStorage(HADRONID_LOCAL_NAME); if (isStr(hadronId)) { return {hadronId: hadronId}; } - return (value && typeof value['hadronId'] === 'string') ? { 'hadronId': value['hadronId'] } : undefined; + return (value && typeof value['hadronId'] === 'string') ? {'hadronId': value['hadronId']} : undefined; }, /** * performs action to obtain id and return a value in the callback's response argument @@ -70,37 +74,40 @@ export const hadronIdSubmodule = { config.params = {}; } const partnerId = config.params.partnerId | 0; - - const url = urlAddParams( - paramOrDefault(config.params.url, DEFAULT_HADRON_URL_ENDPOINT, config.params.urlArg), - `partner_id=${partnerId}&_it=prebid` - ); - + let hadronId = storage.getDataFromLocalStorage(HADRONID_LOCAL_NAME); + if (isStr(hadronId)) { + return {id: {hadronId}}; + } const resp = function (callback) { - let hadronId = storage.getDataFromLocalStorage('auHadronId'); - if (isStr(hadronId)) { - const responseObj = {hadronId: hadronId}; - callback(responseObj); - } else { - const callbacks = { - success: response => { - let responseObj; - if (response) { - try { - responseObj = JSON.parse(response); - } catch (error) { - logError(error); - } + let responseObj = {}; + const callbacks = { + success: response => { + if (response) { + try { + responseObj = JSON.parse(response); + } catch (error) { + logError(error); } - callback(responseObj); - }, - error: error => { - logError(`${MODULE_NAME}: ID fetch encountered an error`, error); - callback(); + logInfo(`Response from backend is ${responseObj}`); + hadronId = responseObj['hadronId']; + storage.setDataInLocalStorage(HADRONID_LOCAL_NAME, hadronId); + responseObj = {id: {hadronId}}; } - }; - ajax(url, callbacks, undefined, {method: 'GET'}); - } + callback(responseObj); + }, + error: error => { + logError(`${MODULE_NAME}: ID fetch encountered an error`, error); + callback(); + } + }; + logInfo('HadronId not found in storage, calling backend...'); + const url = urlAddParams( + // config.params.url and config.params.urlArg are not documented + // since their use is for debugging purposes only + paramOrDefault(config.params.url, DEFAULT_HADRON_URL_ENDPOINT, config.params.urlArg), + `partner_id=${partnerId}&_it=prebid` + ); + ajax(url, callbacks, undefined, {method: 'GET'}); }; return {callback: resp}; } diff --git a/modules/hadronRtdProvider.js b/modules/hadronRtdProvider.js index 0bd4e6f8344..6fb982815c1 100644 --- a/modules/hadronRtdProvider.js +++ b/modules/hadronRtdProvider.js @@ -12,16 +12,17 @@ import {getStorageManager} from '../src/storageManager.js'; import {submodule} from '../src/hook.js'; import {isFn, isStr, isArray, deepEqual, isPlainObject, logError, logInfo} from '../src/utils.js'; import {loadExternalScript} from '../src/adloader.js'; +import {MODULE_TYPE_RTD} from '../src/activities/modules.js'; const LOG_PREFIX = 'User ID - HadronRtdProvider submodule: '; const MODULE_NAME = 'realTimeData'; const SUBMODULE_NAME = 'hadron'; const AU_GVLID = 561; const HADRON_ID_DEFAULT_URL = 'https://id.hadron.ad.gt/api/v1/hadronid?_it=prebid'; -const HADRON_SEGMENT_URL = 'https://seg.hadron.ad.gt/api/v1/rtd'; +const HADRON_SEGMENT_URL = 'https://id.hadron.ad.gt/api/v1/rtd'; export const HALOID_LOCAL_NAME = 'auHadronId'; export const RTD_LOCAL_NAME = 'auHadronRtd'; -export const storage = getStorageManager({gvlid: AU_GVLID, moduleName: SUBMODULE_NAME}); +export const storage = getStorageManager({moduleType: MODULE_TYPE_RTD, moduleName: SUBMODULE_NAME}); /** * @param {string} url @@ -251,7 +252,8 @@ function init(provider, userConsent) { export const hadronSubmodule = { name: SUBMODULE_NAME, getBidRequestData: getRealTimeData, - init: init + init: init, + gvlid: AU_GVLID, }; submodule(MODULE_NAME, hadronSubmodule); diff --git a/modules/holidBidAdapter.js b/modules/holidBidAdapter.js new file mode 100644 index 00000000000..92b0a1c18db --- /dev/null +++ b/modules/holidBidAdapter.js @@ -0,0 +1,175 @@ +import { + deepAccess, + getBidIdParameter, + isStr, + logMessage, + triggerPixel, +} from '../src/utils.js' +import * as events from '../src/events.js' +import CONSTANTS from '../src/constants.json' +import { BANNER } from '../src/mediaTypes.js' + +import { registerBidder } from '../src/adapters/bidderFactory.js' + +const BIDDER_CODE = 'holid' +const GVLID = 1177 +const ENDPOINT = 'https://helloworld.holid.io/openrtb2/auction' +const COOKIE_SYNC_ENDPOINT = 'https://null.holid.io/sync.html' +const TIME_TO_LIVE = 300 +let wurlMap = {} + +events.on(CONSTANTS.EVENTS.BID_WON, bidWonHandler) + +export const spec = { + code: BIDDER_CODE, + gvlid: GVLID, + supportedMediaTypes: [BANNER], + + isBidRequestValid: function (bid) { + return !!bid.params.adUnitID + }, + + buildRequests: function (validBidRequests, _bidderRequest) { + return validBidRequests.map((bid) => { + const requestData = { + ...bid.ortb2, + id: bid.auctionId, + imp: [getImp(bid)], + } + + return { + method: 'POST', + url: ENDPOINT, + data: JSON.stringify(requestData), + bidId: bid.bidId, + } + }) + }, + + interpretResponse: function (serverResponse, bidRequest) { + const bidResponses = [] + + if (!serverResponse.body.seatbid) { + return [] + } + + serverResponse.body.seatbid.map((response) => { + response.bid.map((bid) => { + const requestId = bidRequest.bidId + const auctionId = bidRequest.auctionId + const wurl = deepAccess(bid, 'ext.prebid.events.win') + const bidResponse = { + requestId, + cpm: bid.price, + width: bid.w, + height: bid.h, + ad: bid.adm, + creativeId: bid.crid, + currency: serverResponse.body.cur, + netRevenue: true, + ttl: TIME_TO_LIVE, + } + + addWurl({ auctionId, requestId, wurl }) + + bidResponses.push(bidResponse) + }) + }) + + return bidResponses + }, + + getUserSyncs(optionsType, serverResponse, gdprConsent, uspConsent) { + if (!serverResponse || serverResponse.length === 0) { + return [] + } + + const syncs = [] + const bidders = getBidders(serverResponse) + + if (optionsType.iframeEnabled && bidders) { + const queryParams = [] + + queryParams.push('bidders=' + bidders) + queryParams.push('gdpr=' + +gdprConsent.gdprApplies) + queryParams.push('gdpr_consent=' + gdprConsent.consentString) + queryParams.push('usp_consent=' + (uspConsent || '')) + + let strQueryParams = queryParams.join('&') + + if (strQueryParams.length > 0) { + strQueryParams = '?' + strQueryParams + } + + syncs.push({ + type: 'iframe', + url: COOKIE_SYNC_ENDPOINT + strQueryParams + '&type=iframe', + }) + + return syncs + } + + return [] + }, +} + +function getImp(bid) { + const imp = { + ext: { + prebid: { + storedrequest: { + id: getBidIdParameter('adUnitID', bid.params), + }, + }, + }, + } + const sizes = + bid.sizes && !Array.isArray(bid.sizes[0]) ? [bid.sizes] : bid.sizes + + if (deepAccess(bid, 'mediaTypes.banner')) { + imp.banner = { + format: sizes.map((size) => { + return { w: size[0], h: size[1] } + }), + } + } + + return imp +} + +function getBidders(serverResponse) { + const bidders = serverResponse + .map((res) => Object.keys(res.body.ext.responsetimemillis || [])) + .flat(1) + + if (bidders.length) { + return encodeURIComponent(JSON.stringify([...new Set(bidders)])) + } +} + +function addWurl(auctionId, adId, wurl) { + if ([auctionId, adId].every(isStr)) { + wurlMap[`${auctionId}${adId}`] = wurl + } +} + +function removeWurl(auctionId, adId) { + delete wurlMap[`${auctionId}${adId}`] +} + +function getWurl(auctionId, adId) { + if ([auctionId, adId].every(isStr)) { + return wurlMap[`${auctionId}${adId}`] + } +} + +function bidWonHandler(bid) { + const wurl = getWurl(bid.auctionId, bid.adId) + if (wurl) { + logMessage(`Invoking image pixel for wurl on BID_WIN: "${wurl}"`) + triggerPixel(wurl) + removeWurl(bid.auctionId, bid.adId) + } +} + +registerBidder(spec) diff --git a/modules/holidBidAdapter.md b/modules/holidBidAdapter.md new file mode 100644 index 00000000000..1d83918c00a --- /dev/null +++ b/modules/holidBidAdapter.md @@ -0,0 +1,36 @@ +# Overview + +``` +Module Name: Holid Bid Adapter +Module Type: Bidder Adapter +Maintainer: richard@holid.se +``` + +# Description + +Currently module supports only banner mediaType. + +# Test Parameters + +## Sample Banner Ad Unit + +```js +var adUnits = [ + { + code: 'bannerAdUnit', + mediaTypes: { + banner: { + sizes: [[300, 250]], + }, + }, + bids: [ + { + bidder: 'holid', + params: { + adUnitID: '12345', + }, + }, + ], + }, +] +``` diff --git a/modules/id5AnalyticsAdapter.js b/modules/id5AnalyticsAdapter.js index d35cdf29fca..d0f3198e03d 100644 --- a/modules/id5AnalyticsAdapter.js +++ b/modules/id5AnalyticsAdapter.js @@ -4,6 +4,7 @@ import adapterManager from '../src/adapterManager.js'; import { ajax } from '../src/ajax.js'; import { logInfo, logError } from '../src/utils.js'; import * as events from '../src/events.js'; +import {getGlobal} from '../src/prebidGlobal.js'; const { EVENTS: { @@ -34,7 +35,7 @@ const FLUSH_EVENTS = [ const CONFIG_URL_PREFIX = 'https://api.id5-sync.com/analytics' const TZ = new Date().getTimezoneOffset(); -const PBJS_VERSION = $$PREBID_GLOBAL$$.version; +const PBJS_VERSION = getGlobal().version; const ID5_REDACTED = '__ID5_REDACTED__'; const isArray = Array.isArray; diff --git a/modules/id5IdSystem.js b/modules/id5IdSystem.js index c2c4a97c62e..b7ff836a7e6 100644 --- a/modules/id5IdSystem.js +++ b/modules/id5IdSystem.js @@ -20,6 +20,7 @@ import {submodule} from '../src/hook.js'; import {getRefererInfo} from '../src/refererDetection.js'; import {getStorageManager} from '../src/storageManager.js'; import {uspDataHandler} from '../src/adapterManager.js'; +import {MODULE_TYPE_UID} from '../src/activities/modules.js'; const MODULE_NAME = 'id5Id'; const GVLID = 131; @@ -34,7 +35,7 @@ const ID5_API_CONFIG_URL = 'https://id5-sync.com/api/config/prebid'; // cookie in the array is the most preferred to use const LEGACY_COOKIE_NAMES = ['pbjs-id5id', 'id5id.1st', 'id5id']; -export const storage = getStorageManager({gvlid: GVLID, moduleName: MODULE_NAME}); +export const storage = getStorageManager({moduleType: MODULE_TYPE_UID, moduleName: MODULE_NAME}); /** @type {Submodule} */ export const id5IdSubmodule = { @@ -113,6 +114,11 @@ export const id5IdSubmodule = { return undefined; } + if (!hasWriteConsentToLocalStorage(consentData)) { + logInfo(LOG_PREFIX + 'Skipping ID5 local storage write because no consent given.') + return undefined; + } + const resp = function (cbFunction) { new IdFetchFlow(submoduleConfig, consentData, cacheIdObj, uspDataHandler.getConsentData()).execute() .then(response => { @@ -140,6 +146,11 @@ export const id5IdSubmodule = { extendId(config, consentData, cacheIdObj) { hasRequiredConfig(config); + if (!hasWriteConsentToLocalStorage(consentData)) { + logInfo(LOG_PREFIX + 'No consent given for ID5 local storage writing, skipping nb increment.') + return cacheIdObj; + } + const partnerId = (config && config.params && config.params.partner) || 0; incrementNb(partnerId); @@ -247,11 +258,14 @@ class IdFetchFlow { 'gdpr': hasGdpr, 'nbPage': nbPage, 'o': 'pbjs', - 'rf': referer.topmostLocation, + 'tml': referer.topmostLocation, + 'ref': referer.ref, + 'cu': referer.canonicalUrl, 'top': referer.reachedTop ? 1 : 0, 'u': referer.stack[0] || window.location.href, 'v': '$prebid.version$', - 'storage': this.submoduleConfig.storage + 'storage': this.submoduleConfig.storage, + 'localStorage': storage.localStorageIsEnabled() ? 1 : 0 }; // pass in optional data, but only if populated @@ -374,4 +388,19 @@ export function storeInLocalStorage(key, value, expDays) { storage.setDataInLocalStorage(`${key}`, value); } +/** + * Check to see if we can write to local storage based on purpose consent 1, and that we have vendor consent (ID5=131) + * @param {ConsentData} consentData + * @returns {boolean} + */ +function hasWriteConsentToLocalStorage(consentData) { + const hasGdpr = consentData && typeof consentData.gdprApplies === 'boolean' && consentData.gdprApplies; + const localstorageConsent = deepAccess(consentData, `vendorData.purpose.consents.1`) + const id5VendorConsent = deepAccess(consentData, `vendorData.vendor.consents.${GVLID.toString()}`) + if (hasGdpr && (!localstorageConsent || !id5VendorConsent)) { + return false; + } + return true; +} + submodule('userId', id5IdSubmodule); diff --git a/modules/idWardRtdProvider.js b/modules/idWardRtdProvider.js index 9678739672d..29dda216fdc 100644 --- a/modules/idWardRtdProvider.js +++ b/modules/idWardRtdProvider.js @@ -8,11 +8,12 @@ import {getStorageManager} from '../src/storageManager.js'; import {submodule} from '../src/hook.js'; import {isPlainObject, mergeDeep, logMessage, logError} from '../src/utils.js'; +import {MODULE_TYPE_RTD} from '../src/activities/modules.js'; const MODULE_NAME = 'realTimeData'; const SUBMODULE_NAME = 'idWard'; -export const storage = getStorageManager({moduleName: SUBMODULE_NAME}); +export const storage = getStorageManager({moduleType: MODULE_TYPE_RTD, moduleName: SUBMODULE_NAME}); /** * Add real-time data & merge segments. * @param ortb2 object to merge into diff --git a/modules/identityLinkIdSystem.js b/modules/identityLinkIdSystem.js index df7b03b4e6e..ab10288f38f 100644 --- a/modules/identityLinkIdSystem.js +++ b/modules/identityLinkIdSystem.js @@ -9,8 +9,11 @@ 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'; +import {MODULE_TYPE_UID} from '../src/activities/modules.js'; -export const storage = getStorageManager(); +const MODULE_NAME = 'identityLink'; + +export const storage = getStorageManager({moduleType: MODULE_TYPE_UID, moduleName: MODULE_NAME}); /** @type {Submodule} */ export const identityLinkSubmodule = { @@ -18,7 +21,7 @@ export const identityLinkSubmodule = { * used to link submodule with config * @type {string} */ - name: 'identityLink', + name: MODULE_NAME, /** * used to specify vendor id * @type {number} diff --git a/modules/idxIdSystem.js b/modules/idxIdSystem.js index 908edad4c04..3c00bbde34c 100644 --- a/modules/idxIdSystem.js +++ b/modules/idxIdSystem.js @@ -6,11 +6,12 @@ */ import { isStr, isPlainObject, logError } from '../src/utils.js'; import { submodule } from '../src/hook.js'; -import { getStorageManager } from '../src/storageManager.js'; +import {getStorageManager} from '../src/storageManager.js'; +import {MODULE_TYPE_UID} from '../src/activities/modules.js'; const IDX_MODULE_NAME = 'idx'; const IDX_COOKIE_NAME = '_idx'; -export const storage = getStorageManager(); +export const storage = getStorageManager({moduleType: MODULE_TYPE_UID, moduleName: IDX_MODULE_NAME}); function readIDxFromCookie() { return storage.cookiesAreEnabled ? storage.getCookie(IDX_COOKIE_NAME) : null; diff --git a/modules/imRtdProvider.js b/modules/imRtdProvider.js index bc01896d062..26d49c49f8c 100644 --- a/modules/imRtdProvider.js +++ b/modules/imRtdProvider.js @@ -18,16 +18,18 @@ import { isFn } from '../src/utils.js' import {submodule} from '../src/hook.js'; +import {MODULE_TYPE_RTD} from '../src/activities/modules.js'; export const imUidLocalName = '__im_uid'; export const imVidCookieName = '_im_vid'; export const imRtdLocalName = '__im_sids'; -export const storage = getStorageManager(); const submoduleName = 'im'; const segmentsMaxAge = 3600000; // 1 hour (30 * 60 * 1000) const uidMaxAge = 1800000; // 30 minites (30 * 60 * 1000) const vidMaxAge = 97200000000; // 37 months ((365 * 3 + 30) * 24 * 60 * 60 * 1000) +export const storage = getStorageManager({moduleType: MODULE_TYPE_RTD, moduleName: submoduleName}); + function setImDataInCookie(value) { storage.setCookie( imVidCookieName, diff --git a/modules/impactifyBidAdapter.js b/modules/impactifyBidAdapter.js index 04f34bdc7d9..a44e1d4dd3f 100644 --- a/modules/impactifyBidAdapter.js +++ b/modules/impactifyBidAdapter.js @@ -2,7 +2,6 @@ import { deepAccess, deepSetValue, generateUUID } from '../src/utils.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; import { config } from '../src/config.js'; import { ajax } from '../src/ajax.js'; -import { createEidsArray } from './userId/eids.js'; const BIDDER_CODE = 'impactify'; const BIDDER_ALIAS = ['imp']; @@ -63,9 +62,8 @@ const createOpenRtbRequest = (validBidRequests, bidderRequest) => { if (schain) request.source.ext = { schain: schain }; // Set eids - let bidUserId = deepAccess(validBidRequests, '0.userId'); - let eids = createEidsArray(bidUserId); - if (eids.length) { + let eids = deepAccess(validBidRequests, '0.userIdAsEids'); + if (eids && eids.length) { deepSetValue(request, 'user.ext.eids', eids); } diff --git a/modules/improvedigitalBidAdapter.js b/modules/improvedigitalBidAdapter.js index 94f50094a91..437fcf7d5bb 100644 --- a/modules/improvedigitalBidAdapter.js +++ b/modules/improvedigitalBidAdapter.js @@ -378,7 +378,7 @@ const ID_RAZR = { } }; - const cfgStr = JSON.stringify(cfg).replace(/<\/script>/g, '\\x3C/script>'); + const cfgStr = JSON.stringify(cfg).replace(/<\/script>/ig, '\\x3C/script>'); const s = ``; bid.ad = bid.ad.replace(/]*>/, match => match + s); diff --git a/modules/imuIdSystem.js b/modules/imuIdSystem.js index 41ff95b6702..898f32b27b0 100644 --- a/modules/imuIdSystem.js +++ b/modules/imuIdSystem.js @@ -8,9 +8,12 @@ import { timestamp, logError } from '../src/utils.js'; import { ajax } from '../src/ajax.js' import { submodule } from '../src/hook.js'; -import { getStorageManager } from '../src/storageManager.js'; +import {getStorageManager} from '../src/storageManager.js'; +import {MODULE_TYPE_UID} from '../src/activities/modules.js'; -export const storage = getStorageManager(); +const MODULE_NAME = 'imuid'; + +export const storage = getStorageManager({moduleType: MODULE_TYPE_UID, moduleName: MODULE_NAME}); export const storageKey = '__im_uid'; export const storagePpKey = '__im_ppid'; @@ -112,7 +115,7 @@ export const imuIdSubmodule = { * used to link submodule with config * @type {string} */ - name: 'imuid', + name: MODULE_NAME, /** * decode the stored id value for passing to bid requests * @function diff --git a/modules/incrxBidAdapter.js b/modules/incrxBidAdapter.js index 914ef0b904e..d054309ee40 100644 --- a/modules/incrxBidAdapter.js +++ b/modules/incrxBidAdapter.js @@ -68,6 +68,8 @@ export const spec = { requestId: response.slotBidId, cpm: response.cpm, currency: response.currency || DEFAULT_CURRENCY, + adType: response.adType || '1', + settings: response.settings, width: response.adWidth, height: response.adHeight, ttl: CREATIVE_TTL, diff --git a/modules/insticatorBidAdapter.js b/modules/insticatorBidAdapter.js index 150e9d3c5c2..46ff17d2a5a 100644 --- a/modules/insticatorBidAdapter.js +++ b/modules/insticatorBidAdapter.js @@ -12,7 +12,7 @@ const USER_ID_COOKIE_EXP = 2592000000; // 30 days const BID_TTL = 300; // 5 minutes const GVLID = 910; -export const storage = getStorageManager({gvlid: GVLID, bidderCode: BIDDER_CODE}); +export const storage = getStorageManager({bidderCode: BIDDER_CODE}); config.setDefaults({ insticator: { diff --git a/modules/intentIqIdSystem.js b/modules/intentIqIdSystem.js index 563435dee65..b16624ac368 100644 --- a/modules/intentIqIdSystem.js +++ b/modules/intentIqIdSystem.js @@ -8,7 +8,8 @@ import { logError, logInfo } from '../src/utils.js'; import { ajax } from '../src/ajax.js'; import { submodule } from '../src/hook.js' -import { getStorageManager } from '../src/storageManager.js'; +import {getStorageManager} from '../src/storageManager.js'; +import {MODULE_TYPE_UID} from '../src/activities/modules.js'; const PCID_EXPIRY = 365; @@ -16,7 +17,7 @@ const MODULE_NAME = 'intentIqId'; export const FIRST_PARTY_KEY = '_iiq_fdata'; export var FIRST_PARTY_DATA_KEY = '_iiq_fdata'; -export const storage = getStorageManager({ gvlid: undefined, moduleName: MODULE_NAME }); +export const storage = getStorageManager({ moduleType: MODULE_TYPE_UID, moduleName: MODULE_NAME }); const INVALID_ID = 'INVALID_ID'; diff --git a/modules/invibesBidAdapter.js b/modules/invibesBidAdapter.js index c3e5cf6cca8..b2444043c22 100644 --- a/modules/invibesBidAdapter.js +++ b/modules/invibesBidAdapter.js @@ -17,7 +17,7 @@ const CONSTANTS = { DISABLE_USER_SYNC: true }; -const storage = getStorageManager({gvlid: CONSTANTS.INVIBES_VENDOR_ID, bidderCode: CONSTANTS.BIDDER_CODE}); +const storage = getStorageManager({bidderCode: CONSTANTS.BIDDER_CODE}); export const spec = { code: CONSTANTS.BIDDER_CODE, diff --git a/modules/iqzoneBidAdapter.js b/modules/iqzoneBidAdapter.js index f2de447d6a7..52f3be7e4b4 100644 --- a/modules/iqzoneBidAdapter.js +++ b/modules/iqzoneBidAdapter.js @@ -154,7 +154,7 @@ export const spec = { coppa: config.getConfig('coppa') === true ? 1 : 0, ccpa: bidderRequest.uspConsent || undefined, gdpr: bidderRequest.gdprConsent || undefined, - tmax: config.getConfig('bidderTimeout') + tmax: bidderRequest.timeout }; const len = validBidRequests.length; diff --git a/modules/ivsBidAdapter.js b/modules/ivsBidAdapter.js new file mode 100644 index 00000000000..47685fbbe46 --- /dev/null +++ b/modules/ivsBidAdapter.js @@ -0,0 +1,85 @@ +import { ortbConverter } from '../libraries/ortbConverter/converter.js'; +import { deepAccess, deepSetValue, getBidIdParameter, logError } from '../src/utils.js'; +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { VIDEO } from '../src/mediaTypes.js'; +import { INSTREAM } from '../src/video.js'; + +const BIDDER_CODE = 'ivs'; +const ENDPOINT_URL = 'https://a.ivstracker.net/prod/openrtb/2.5'; + +export const converter = ortbConverter({ + context: { + mediaType: VIDEO, + ttl: 360, + netRevenue: true + } +}); + +export const spec = { + code: BIDDER_CODE, + supportedMediaTypes: [VIDEO], + + /** + * Determines whether or not the given bid request is valid. + * + * @param {BidRequest} bid The bid params to validate. + * @return boolean True if this is a valid bid, and false otherwise. + */ + isBidRequestValid: function(bid) { + if (bid && typeof bid.params !== 'object') { + logError(BIDDER_CODE + ': params is not defined or is incorrect in the bidder settings.'); + return false; + } + + if (!deepAccess(bid, 'mediaTypes.video')) { + logError(BIDDER_CODE + ': mediaTypes.video is not present in the bidder settings.'); + return false; + } + + if (deepAccess(bid, 'mediaTypes.video.context') !== INSTREAM) { + logError(BIDDER_CODE + ': only instream video context is allowed.'); + return false; + } + + if (!getBidIdParameter('publisherId', bid.params)) { + logError(BIDDER_CODE + ': publisherId is not present in bidder params.'); + return false; + } + + return true; + }, + + /** + * Make a server request from the list of BidRequests. + * + * @param {validBidRequests[]} - an array of bids + * @param {bidderRequest} - master bidRequest object + * @return ServerRequest Info describing the request to the server. + */ + buildRequests: function(validBidRequests, bidderRequest) { + const data = converter.toORTB({ bidderRequest, validBidRequests, context: {mediaType: 'video'} }); + deepSetValue(data.site, 'publisher.id', validBidRequests[0].params.publisherId); + + return { + method: 'POST', + url: ENDPOINT_URL, + data: data, + options: { + contentType: 'application/json' + }, + }; + }, + + /** + * Unpack the response from the server into a list of bids. + * + * @param {ServerResponse} serverResponse A successful response from the server. + * @return {Bid[]} An array of bids which were nested inside the server. + */ + interpretResponse: function(serverResponse, bidRequest) { + if (!serverResponse.body) return; + return converter.fromORTB({ response: serverResponse.body, request: bidRequest.data }).bids; + }, +} + +registerBidder(spec); diff --git a/modules/ivsBidAdapter.md b/modules/ivsBidAdapter.md new file mode 100644 index 00000000000..d50061b640d --- /dev/null +++ b/modules/ivsBidAdapter.md @@ -0,0 +1,34 @@ +# Overview + +``` +Module Name: IVS Bidder Adapter +Module Type: Bidder Adapter +Maintainer: prebid@ivs.tv +``` + +# Description + +Module that connects to IVS's demand sources + +# Test Parameters +``` + var adUnits = [ + { + code: 'test-div', + mediaTypes: { + video: { + playerSize: [640, 480], + context: 'instream' + } + }, + bids: [ + { + bidder: 'ivs', + params: { + publisherId: '3001234' // required + } + } + ] + } + ]; +``` \ No newline at end of file diff --git a/modules/ixBidAdapter.js b/modules/ixBidAdapter.js index 27e6d0aee02..e791d933352 100644 --- a/modules/ixBidAdapter.js +++ b/modules/ixBidAdapter.js @@ -13,7 +13,6 @@ import { logError, logWarn, mergeDeep, - parseQueryStringParameters, safeJSONParse } from '../src/utils.js'; import { BANNER, VIDEO, NATIVE } from '../src/mediaTypes.js'; @@ -76,7 +75,8 @@ const SOURCE_RTI_MAPPING = { 'epsilon.com': '', // Publisher Link, publinkId 'audigent.com': '', // Hadron ID from Audigent, hadronId 'pubcid.org': '', // SharedID, pubcid - 'trustpid.com': '' // Trustpid + 'trustpid.com': '', // Trustpid + 'intimatemerger.com': '' }; const PROVIDERS = [ 'britepoolid', @@ -101,11 +101,14 @@ const VIDEO_PARAMS_ALLOW_LIST = [ const LOCAL_STORAGE_KEY = 'ixdiag'; export const LOCAL_STORAGE_FEATURE_TOGGLES_KEY = `${BIDDER_CODE}_features`; let hasRegisteredHandler = false; -export const storage = getStorageManager({ gvlid: GLOBAL_VENDOR_ID, bidderCode: BIDDER_CODE }); +export const storage = getStorageManager({ bidderCode: BIDDER_CODE }); export const FEATURE_TOGGLES = { + // Update with list of CFTs to be requested from Exchange + REQUESTED_FEATURE_TOGGLES: [], + featureToggles: {}, isFeatureEnabled: function (ft) { - return deepAccess(this.featureToggles, `features.${ft}.activated`) + return deepAccess(this.featureToggles, `features.${ft}.activated`, false) }, getFeatureToggles: function () { if (storage.localStorageIsEnabled()) { @@ -380,8 +383,8 @@ function parseBid(rawBid, currency, bidRequest) { bid.netRevenue = NET_REVENUE; bid.currency = currency; bid.creativeId = rawBid.hasOwnProperty('crid') ? rawBid.crid : '-'; - - if (rawBid.mtype == MEDIA_TYPES.Video) { + // If mtype = video is passed and vastURl is not set, set vastxml + if (rawBid.mtype == MEDIA_TYPES.Video && ((rawBid.ext && !rawBid.ext.vasturl) || !rawBid.ext)) { bid.vastXml = rawBid.adm; } else if (rawBid.ext && rawBid.ext.vasturl) { bid.vastUrl = rawBid.ext.vasturl; @@ -425,7 +428,6 @@ function parseBid(rawBid, currency, bidRequest) { if (rawBid.adomain && rawBid.adomain.length > 0) { bid.meta.advertiserDomains = rawBid.adomain; } - return bid; } @@ -598,29 +600,11 @@ function buildRequest(validBidRequests, bidderRequest, impressions, version) { // Get ids from Prebid User ID Modules let eidInfo = getEidInfo(deepAccess(validBidRequests, '0.userIdAsEids')); let userEids = eidInfo.toSend; - const pageUrl = deepAccess(bidderRequest, 'refererInfo.page'); - - let MAX_REQUEST_SIZE = 8000; - // Modify request size limit if its FT is enabeld. - if (FEATURE_TOGGLES.isFeatureEnabled('pbjs_use_32kb_size_limit')) { - MAX_REQUEST_SIZE = 32000 - } // RTI ids will be included in the bid request if the function getIdentityInfo() is loaded // and if the data for the partner exist if (window.headertag && typeof window.headertag.getIdentityInfo === 'function') { - let identityInfo = window.headertag.getIdentityInfo(); - if (identityInfo && typeof identityInfo === 'object') { - for (const partnerName in identityInfo) { - if (identityInfo.hasOwnProperty(partnerName)) { - let response = identityInfo[partnerName]; - if (!response.responsePending && response.data && typeof response.data === 'object' && - Object.keys(response.data).length && !eidInfo.seenSources[response.data.source]) { - userEids.push(response.data); - } - } - } - } + addRTI(userEids, eidInfo); } // If `roundel` alias bidder, only send requests if liveramp ids exist. @@ -628,9 +612,103 @@ function buildRequest(validBidRequests, bidderRequest, impressions, version) { return []; } - const r = {}; - const tmax = config.getConfig('bidderTimeout'); + const requests = []; + let r = createRequest(validBidRequests); + + // Add FTs to be requested from Exchange + r = addRequestedFeatureToggles(r, FEATURE_TOGGLES.REQUESTED_FEATURE_TOGGLES) + + // getting ixdiags for adunits of the video, outstream & multi format (MF) style + let ixdiag = buildIXDiag(validBidRequests); + for (var key in ixdiag) { + r.ext.ixdiag[key] = ixdiag[key]; + } + + r = enrichRequest(r, bidderRequest, impressions, validBidRequests, userEids); + + r = applyRegulations(r, bidderRequest); + + let payload = {}; + createPayload(validBidRequests, bidderRequest, r, baseUrl, requests, payload); + + const transactionIds = Object.keys(impressions); + let isFpdAdded = false; + for (let adUnitIndex = 0; adUnitIndex < transactionIds.length; adUnitIndex++) { + if (requests.length >= MAX_REQUEST_LIMIT) { + break; + } + + r = addImpressions(impressions, transactionIds, r, adUnitIndex); + + const fpd = deepAccess(bidderRequest, 'ortb2') || {}; + const site = { ...(fpd.site || fpd.context) }; + const user = { ...fpd.user }; + if (!isEmpty(fpd) && !isFpdAdded) { + r = addFPD(bidderRequest, r, fpd, site, user); + + const clonedRObject = deepClone(r); + + clonedRObject.site = mergeDeep({}, clonedRObject.site, site); + clonedRObject.user = mergeDeep({}, clonedRObject.user, user); + + r.site = mergeDeep({}, r.site, site); + r.user = mergeDeep({}, r.user, user); + isFpdAdded = true; + } + + // add identifiers info to ixDiag + r = addIdentifiersInfo(impressions, r, transactionIds, adUnitIndex, payload, baseUrl); + + const isLastAdUnit = adUnitIndex === transactionIds.length - 1; + + if (isLastAdUnit) { + requests.push({ + method: 'POST', + url: baseUrl + '?s=' + siteID, + data: deepClone(r), + option: { + contentType: 'text/plain', + }, + validBidRequests + }); + + r.imp = []; + isFpdAdded = false; + } + } + + return requests; +} + +/** + * addRTI adds RTI info of the partner to retrieved user IDs from prebid ID module. + * + * @param {array} userEids userEids info retrieved from prebid + * @param {array} eidInfo eidInfo info from prebid + */ +function addRTI(userEids, eidInfo) { + let identityInfo = window.headertag.getIdentityInfo(); + if (identityInfo && typeof identityInfo === 'object') { + for (const partnerName in identityInfo) { + if (identityInfo.hasOwnProperty(partnerName)) { + let response = identityInfo[partnerName]; + if (!response.responsePending && response.data && typeof response.data === 'object' && + Object.keys(response.data).length && !eidInfo.seenSources[response.data.source]) { + userEids.push(response.data); + } + } + } + } +} + +/** + * createRequest creates the base request object + * @param {array} validBidRequests A list of valid bid request config objects. + * @return {object} Object describing the request to the server. + */ +function createRequest(validBidRequests) { + const r = {}; // Since bidderRequestId are the same for different bid request, just use the first one. r.id = validBidRequests[0].bidderRequestId.toString(); r.site = {}; @@ -640,13 +718,39 @@ function buildRequest(validBidRequests, bidderRequest, impressions, version) { r.ext.ixdiag.ls = storage.localStorageIsEnabled(); r.imp = []; r.at = 1; + return r +} - // getting ixdiags for adunits of the video, outstream & multi format (MF) style - let ixdiag = buildIXDiag(validBidRequests); - for (var key in ixdiag) { - r.ext.ixdiag[key] = ixdiag[key]; +/** + * Adds requested feature toggles to the provided request object to be sent to Exchange. + * @param {object} r - The request object to add feature toggles to. + * @param {Array} requestedFeatureToggles - The list of feature toggles to add. + * @returns {object} The updated request object with the added feature toggles. + */ +function addRequestedFeatureToggles(r, requestedFeatureToggles) { + if (requestedFeatureToggles.length > 0) { + r.ext.features = {}; + // Loop through each feature toggle and add it to the features object. + // Add current activation status as well. + requestedFeatureToggles.forEach(toggle => { + r.ext.features[toggle] = { activated: FEATURE_TOGGLES.isFeatureEnabled(toggle) }; + }); } + return r; +} +/** + * enrichRequest adds userSync configs, source, and referer info to request and ixDiag objects. + * + * @param {object} r Base reuqest object. + * @param {object} bidderRequest An object containing other info like gdprConsent. + * @param {array} impressions A list of impressions to be added to the request. + * @param {array} validBidRequests A list of valid bid request config objects. + * @param {array} userEids User ID info retrieved from Prebid ID module. + * @return {object} Enriched object describing the request to the server. + */ +function enrichRequest(r, bidderRequest, impressions, validBidRequests, userEids) { + const tmax = deepAccess(bidderRequest, 'timeout'); if (tmax) { r.ext.ixdiag.tmax = tmax; } @@ -685,6 +789,17 @@ function buildRequest(validBidRequests, bidderRequest, impressions, version) { r.site.ref = document.referrer; } + return r +} + +/** + * applyRegulations applies regulation info such as GDPR and GPP to the reqeust obejct. + * + * @param {object} r Base reuqest object. + * @param {object} bidderRequest An object containing other info like gdprConsent. + * @return {object} Object enriched with regulation info describing the request to the server. + */ +function applyRegulations(r, bidderRequest) { // Apply GDPR information to the request if GDPR is enabled. if (bidderRequest) { if (bidderRequest.gdprConsent) { @@ -706,7 +821,7 @@ function buildRequest(validBidRequests, bidderRequest, impressions, version) { if (gdprConsent.hasOwnProperty('addtlConsent') && gdprConsent.addtlConsent) { r.user.ext.consented_providers_settings = { - consented_providers: gdprConsent.addtlConsent + addtl_consent: gdprConsent.addtlConsent }; } } @@ -717,16 +832,35 @@ function buildRequest(validBidRequests, bidderRequest, impressions, version) { usPrivacy = bidderRequest.uspConsent; } + const pageUrl = deepAccess(bidderRequest, 'refererInfo.page'); if (pageUrl) { r.site.page = pageUrl; } + + if (bidderRequest.gppConsent) { + deepSetValue(r, 'regs.gpp', bidderRequest.gppConsent.gppString); + deepSetValue(r, 'regs.gpp_sid', bidderRequest.gppConsent.applicableSections); + } } if (config.getConfig('coppa')) { deepSetValue(r, 'regs.coppa', 1); } - const payload = {}; + return r +} + +/** + * createPayload creates the payload to be sent with the request. + * + * @param {array} validBidRequests A list of valid bid request config objects. + * @param {object} bidderRequest An object containing other info like gdprConsent. + * @param {object} r Reuqest object. + * @param {string} baseUrl Base exchagne URL. + * @param {array} requests List of request obejcts. + * @param {object} payload Request payload object. + */ +function createPayload(validBidRequests, bidderRequest, r, baseUrl, requests, payload) { // Use the siteId in the first bid request as the main siteId. siteID = validBidRequests[0].params.siteId; payload.s = siteID; @@ -734,19 +868,6 @@ function buildRequest(validBidRequests, bidderRequest, impressions, version) { // Parse additional runtime configs. const bidderCode = (bidderRequest && bidderRequest.bidderCode) || 'ix'; const otherIxConfig = config.getConfig(bidderCode); - const requests = []; - let requestSequenceNumber = 0; - const transactionIds = Object.keys(impressions); - const baseRequestSize = `${baseUrl}${parseQueryStringParameters({ ...payload, r: JSON.stringify(r) })}`.length; - - if (baseRequestSize > MAX_REQUEST_SIZE) { - logError('IX Bid Adapter: Base request size has exceeded maximum request size.', { bidder: BIDDER_CODE, code: ERROR_CODES.EXCEEDS_MAX_SIZE }); - return requests; - } - - let currentRequestSize = baseRequestSize; - let fpdRequestSize = 0; - let isFpdAdded = false; if (otherIxConfig) { // Append firstPartyData to r.site.page if firstPartyData exists. @@ -760,182 +881,160 @@ function buildRequest(validBidRequests, bidderRequest, impressions, version) { } firstPartyString = firstPartyString.slice(0, -1); - fpdRequestSize = encodeURIComponent(firstPartyString).length; - - if (fpdRequestSize < MAX_REQUEST_SIZE) { - if ('page' in r.site) { - r.site.page += firstPartyString; - } else { - r.site.page = firstPartyString; - } - currentRequestSize += fpdRequestSize; + if ('page' in r.site) { + r.site.page += firstPartyString; } else { - logError('IX Bid Adapter: IX config FPD request size has exceeded maximum request size.', { bidder: BIDDER_CODE, code: ERROR_CODES.IX_FPD_EXCEEDS_MAX_SIZE }); + r.site.page = firstPartyString; } } } +} - for (let adUnitIndex = 0; adUnitIndex < transactionIds.length; adUnitIndex++) { - if (currentRequestSize >= MAX_REQUEST_SIZE || requests.length >= MAX_REQUEST_LIMIT) { - break; - } - - const adUnitImpressions = impressions[transactionIds[adUnitIndex]]; - const { missingImps: missingBannerImpressions = [], ixImps = [] } = adUnitImpressions; - let wasAdUnitImpressionsTrimmed = false; - let remainingRequestSize = MAX_REQUEST_SIZE - currentRequestSize; - const sourceImpressions = { ixImps, missingBannerImpressions }; - const impressionObjects = Object.keys(sourceImpressions) - .map((key) => sourceImpressions[key]) - .filter(item => Array.isArray(item)) - .reduce((acc, curr) => acc.concat(...curr), []); - - let currentImpressionSize = encodeURIComponent(JSON.stringify({ impressionObjects })).length; - - while (impressionObjects.length && currentImpressionSize > remainingRequestSize) { - wasAdUnitImpressionsTrimmed = true; - impressionObjects.pop(); - currentImpressionSize = encodeURIComponent(JSON.stringify({ impressionObjects })).length; - } - - const gpid = impressions[transactionIds[adUnitIndex]].gpid; - const dfpAdUnitCode = impressions[transactionIds[adUnitIndex]].dfp_ad_unit_code; - const tid = impressions[transactionIds[adUnitIndex]].tid; - const sid = impressions[transactionIds[adUnitIndex]].sid - - if (impressionObjects.length && BANNER in impressionObjects[0]) { - const { id, banner: { topframe } } = impressionObjects[0]; - const _bannerImpression = { - id, - banner: { - topframe, - format: impressionObjects.map(({ banner: { w, h }, ext }) => ({ w, h, ext })) - }, - }; - - for (let i = 0; i < _bannerImpression.banner.format.length; i++) { - // We add sid in imp.ext.sid therefore, remove from banner.format[].ext - if (_bannerImpression.banner.format[i].ext != null && _bannerImpression.banner.format[i].ext.sid != null) { - delete _bannerImpression.banner.format[i].ext.sid; - } - - // add floor per size - if ('bidfloor' in impressionObjects[i]) { - _bannerImpression.banner.format[i].ext.bidfloor = impressionObjects[i].bidfloor - } - } - - const position = impressions[transactionIds[adUnitIndex]].pos; - if (isInteger(position)) { - _bannerImpression.banner.pos = position; - } - - if (dfpAdUnitCode || gpid || tid || sid) { - _bannerImpression.ext = {}; - _bannerImpression.ext.dfp_ad_unit_code = dfpAdUnitCode; - _bannerImpression.ext.gpid = gpid; - _bannerImpression.ext.tid = tid; - _bannerImpression.ext.sid = sid; - } +/** + * addImpressions adds impressions to request object + * + * @param {array} impressions List of impressions to be added to the request. + * @param {array} transactionIds List of transaction Ids. + * @param {object} r Reuqest object. + * @param {int} adUnitIndex Index of the current add unit + * @return {object} Reqyest object with added impressions describing the request to the server. + */ +function addImpressions(impressions, transactionIds, r, adUnitIndex) { + const adUnitImpressions = impressions[transactionIds[adUnitIndex]]; + const { missingImps: missingBannerImpressions = [], ixImps = [] } = adUnitImpressions; + const sourceImpressions = { ixImps, missingBannerImpressions }; + const impressionObjects = Object.keys(sourceImpressions) + .map((key) => sourceImpressions[key]) + .filter(item => Array.isArray(item)) + .reduce((acc, curr) => acc.concat(...curr), []); + + const gpid = impressions[transactionIds[adUnitIndex]].gpid; + const dfpAdUnitCode = impressions[transactionIds[adUnitIndex]].dfp_ad_unit_code; + const tid = impressions[transactionIds[adUnitIndex]].tid; + const sid = impressions[transactionIds[adUnitIndex]].sid + + if (impressionObjects.length && BANNER in impressionObjects[0]) { + const { id, banner: { topframe } } = impressionObjects[0]; + const _bannerImpression = { + id, + banner: { + topframe, + format: impressionObjects.map(({ banner: { w, h }, ext }) => ({ w, h, ext })) + }, + }; - if ('bidfloor' in impressionObjects[0]) { - _bannerImpression.bidfloor = impressionObjects[0].bidfloor; + for (let i = 0; i < _bannerImpression.banner.format.length; i++) { + // We add sid in imp.ext.sid therefore, remove from banner.format[].ext + if (_bannerImpression.banner.format[i].ext != null && _bannerImpression.banner.format[i].ext.sid != null) { + delete _bannerImpression.banner.format[i].ext.sid; } - if ('bidfloorcur' in impressionObjects[0]) { - _bannerImpression.bidfloorcur = impressionObjects[0].bidfloorcur; + // add floor per size + if ('bidfloor' in impressionObjects[i]) { + _bannerImpression.banner.format[i].ext.bidfloor = impressionObjects[i].bidfloor; } - - r.imp.push(_bannerImpression); - } else { - // set imp.ext.gpid to resolved gpid for each imp - impressionObjects.forEach(imp => deepSetValue(imp, 'ext.gpid', gpid)); - r.imp.push(...impressionObjects); } - currentRequestSize += currentImpressionSize; - - const fpd = deepAccess(bidderRequest, 'ortb2') || {}; - - if (!isEmpty(fpd) && !isFpdAdded) { - r.ext.ixdiag.fpd = true; - - const site = { ...(fpd.site || fpd.context) }; + const position = impressions[transactionIds[adUnitIndex]].pos; + if (isInteger(position)) { + _bannerImpression.banner.pos = position; + } - Object.keys(site).forEach(key => { - if (FIRST_PARTY_DATA.SITE.indexOf(key) === -1) { - delete site[key]; - } - }); + if (dfpAdUnitCode || gpid || tid || sid) { + _bannerImpression.ext = {}; + _bannerImpression.ext.dfp_ad_unit_code = dfpAdUnitCode; + _bannerImpression.ext.gpid = gpid; + _bannerImpression.ext.tid = tid; + _bannerImpression.ext.sid = sid; + } - const user = { ...fpd.user }; + if ('bidfloor' in impressionObjects[0]) { + _bannerImpression.bidfloor = impressionObjects[0].bidfloor; + } - Object.keys(user).forEach(key => { - if (FIRST_PARTY_DATA.USER.indexOf(key) === -1) { - delete user[key]; - } - }); + if ('bidfloorcur' in impressionObjects[0]) { + _bannerImpression.bidfloorcur = impressionObjects[0].bidfloorcur; + } - const clonedRObject = deepClone(r); + r.imp.push(_bannerImpression); + } else { + // set imp.ext.gpid to resolved gpid for each imp + impressionObjects.forEach(imp => deepSetValue(imp, 'ext.gpid', gpid)); + r.imp.push(...impressionObjects); + } - clonedRObject.site = mergeDeep({}, clonedRObject.site, site); - clonedRObject.user = mergeDeep({}, clonedRObject.user, user); + return r; +} - const requestSize = `${baseUrl}${parseQueryStringParameters({ ...payload, r: JSON.stringify(clonedRObject) })}`.length; +/** + * addFPD adds ortb2 first party data to request object. + * + * @param {object} bidderRequest An object containing other info like gdprConsent. + * @param {object} r Reuqest object. + * @param {object} fpd ortb2 first party data. + * @param {object} site First party site data. + * @param {object} user First party user data. + * @return {object} Reqyest object with added FPD describing the request to the server. + */ +function addFPD(bidderRequest, r, fpd, site, user) { + r.ext.ixdiag.fpd = true; - if (requestSize < MAX_REQUEST_SIZE) { - r.site = mergeDeep({}, r.site, site); - r.user = mergeDeep({}, r.user, user); - isFpdAdded = true; - const fpdRequestSize = encodeURIComponent(JSON.stringify({ ...site, ...user })).length; - currentRequestSize += fpdRequestSize; - } else { - logError('IX Bid Adapter: FPD request size has exceeded maximum request size.', { bidder: BIDDER_CODE, code: ERROR_CODES.PB_FPD_EXCEEDS_MAX_SIZE }); - } + Object.keys(site).forEach(key => { + if (FIRST_PARTY_DATA.SITE.indexOf(key) === -1) { + delete site[key]; } + }); - // add identifiers info to ixDiag - const pbaAdSlot = impressions[transactionIds[adUnitIndex]].pbadslot; - const tagId = impressions[transactionIds[adUnitIndex]].tagId; - const adUnitCode = impressions[transactionIds[adUnitIndex]].adUnitCode; - const divId = impressions[transactionIds[adUnitIndex]].divId; - if (pbaAdSlot || tagId || adUnitCode || divId) { - const clonedRObject = deepClone(r); - const requestSize = `${baseUrl}${parseQueryStringParameters({ ...payload, r: JSON.stringify(clonedRObject) })}`.length; - if (requestSize < MAX_REQUEST_SIZE) { - r.ext.ixdiag.pbadslot = pbaAdSlot; - r.ext.ixdiag.tagid = tagId; - r.ext.ixdiag.adunitcode = adUnitCode; - r.ext.ixdiag.divId = divId; - } + Object.keys(user).forEach(key => { + if (FIRST_PARTY_DATA.USER.indexOf(key) === -1) { + delete user[key]; } + }); - const isLastAdUnit = adUnitIndex === transactionIds.length - 1; - - if (wasAdUnitImpressionsTrimmed || isLastAdUnit) { - if (!isLastAdUnit || requestSequenceNumber) { - r.ext.ixdiag.sn = requestSequenceNumber; - } - - requestSequenceNumber++; + if (fpd.device) { + const sua = {...fpd.device.sua}; + if (!isEmpty(sua)) { + deepSetValue(r, 'device.sua', sua); + } + } - requests.push({ - method: 'POST', - url: baseUrl + '?s=' + siteID, - data: deepClone(r), - option: { - contentType: 'text/plain', - }, - validBidRequests - }); + if (fpd.hasOwnProperty('regs') && !bidderRequest.gppConsent) { + if (fpd.regs.hasOwnProperty('gpp') && typeof fpd.regs.gpp == 'string') { + deepSetValue(r, 'regs.gpp', fpd.regs.gpp) + } - currentRequestSize = baseRequestSize; - r.imp = []; - isFpdAdded = false; + if (fpd.regs.hasOwnProperty('gpp_sid') && Array.isArray(fpd.regs.gpp_sid)) { + deepSetValue(r, 'regs.gpp_sid', fpd.regs.gpp_sid) } } - return requests; + return r; +} + +/** + * addIdentifiersInfo adds indentifier info to ixDaig. + * + * @param {array} impressions List of impressions to be added to the request. + * @param {object} r Reuqest object. + * @param {array} transactionIds List of transaction Ids. + * @param {int} adUnitIndex Index of the current add unit + * @param {object} payload Request payload object. + * @param {string} baseUrl Base exchagne URL. + * @return {object} Reqyest object with added indentigfier info to ixDiag. + */ +function addIdentifiersInfo(impressions, r, transactionIds, adUnitIndex, payload, baseUrl) { + const pbaAdSlot = impressions[transactionIds[adUnitIndex]].pbadslot; + const tagId = impressions[transactionIds[adUnitIndex]].tagId; + const adUnitCode = impressions[transactionIds[adUnitIndex]].adUnitCode; + const divId = impressions[transactionIds[adUnitIndex]].divId; + if (pbaAdSlot || tagId || adUnitCode || divId) { + r.ext.ixdiag.pbadslot = pbaAdSlot; + r.ext.ixdiag.tagid = tagId; + r.ext.ixdiag.adunitcode = adUnitCode; + r.ext.ixdiag.divId = divId; + } + + return r; } /** diff --git a/modules/jixieBidAdapter.js b/modules/jixieBidAdapter.js index f6f58883990..586fce84804 100644 --- a/modules/jixieBidAdapter.js +++ b/modules/jixieBidAdapter.js @@ -6,7 +6,6 @@ import {BANNER, VIDEO} from '../src/mediaTypes.js'; import {ajax} from '../src/ajax.js'; import {getRefererInfo} from '../src/refererDetection.js'; import {Renderer} from '../src/Renderer.js'; -import {createEidsArray} from './userId/eids.js'; const BIDDER_CODE = 'jixie'; export const storage = getStorageManager({bidderCode: BIDDER_CODE}); @@ -56,6 +55,8 @@ function fetchIds_() { if (tmp) ret.client_id_ls = tmp; tmp = storage.getDataFromLocalStorage('_jxxs'); if (tmp) ret.session_id_ls = tmp; + tmp = storage.getCookie('_jxtoko'); + if (tmp) ret.jxtoko_id = tmp; } catch (error) {} return ret; } @@ -188,12 +189,10 @@ export const spec = { let miscDims = internal.getMiscDims(); let schain = deepAccess(validBidRequests[0], 'schain'); + let eids1 = validBidRequests[0].userIdAsEids // all available user ids are sent to our backend in the standard array layout: - if (validBidRequests[0].userId) { - let eids1 = createEidsArray(validBidRequests[0].userId); - if (eids1.length) { - eids = eids1; - } + if (eids1 && eids1.length) { + eids = eids1; } // we want to send this blob of info to our backend: let pg = config.getConfig('priceGranularity'); diff --git a/modules/jwplayerVideoProvider.js b/modules/jwplayerVideoProvider.js index d606e8121a7..ed6b69d756a 100644 --- a/modules/jwplayerVideoProvider.js +++ b/modules/jwplayerVideoProvider.js @@ -7,7 +7,7 @@ import { AUTOSTART_BLOCKED, PLAY_ATTEMPT_FAILED, CONTENT_LOADED, PLAY, PAUSE, BUFFER, TIME, SEEK_START, SEEK_END, MUTE, VOLUME, RENDITION_UPDATE, ERROR, COMPLETE, PLAYLIST_COMPLETE, FULLSCREEN, PLAYER_RESIZE, VIEWABLE, CAST } from '../libraries/video/constants/events.js'; -import { PLAYBACK_MODE } from '../libraries/video/constants/enums.js'; +import { PLAYBACK_MODE } from '../libraries/video/constants/constants.js'; import stateFactory from '../libraries/video/shared/state.js'; import { JWPLAYER_VENDOR } from '../libraries/video/constants/vendorCodes.js'; import { getEventHandler } from '../libraries/video/shared/eventHandler.js'; @@ -47,24 +47,29 @@ export function JWPlayerProvider(config, jwplayer_, adState_, timeState_, callba function init() { if (!jwplayer) { - triggerSetupFailure(-1); // TODO: come up with code for player absent + triggerSetupFailure({ code: -1 }); // TODO: come up with error code schema- player is absent return; } playerVersion = jwplayer.version; if (playerVersion < minimumSupportedPlayerVersion) { - triggerSetupFailure(-2); // TODO: come up with code for version not supported + triggerSetupFailure({ code: -2 }); // TODO: come up with error code schema - version not supported + return; + } + + if (!document.getElementById(divId)) { + triggerSetupFailure({ code: -3 }); // TODO: come up with error code schema - missing div id return; } player = jwplayer(divId); - if (player.getState() === undefined) { + if (!player || !player.getState) { + triggerSetupFailure({ code: -4 }); // TODO: come up with error code schema - factory function failure + } else if (player.getState() === undefined) { setupPlayer(playerConfig); } else { - const payload = getSetupCompletePayload(); - setupCompleteCallbacks.forEach(callback => callback(SETUP_COMPLETE, payload)); - setupCompleteCallbacks = []; + triggerSetupComplete(); } } @@ -185,8 +190,12 @@ export function JWPlayerProvider(config, jwplayer_, adState_, timeState_, callba function onEvent(externalEventName, callback, basePayload) { if (externalEventName === SETUP_COMPLETE) { setupCompleteCallbacks.push(callback); - } else if (externalEventName === SETUP_FAILED) { + return; + } + + if (externalEventName === SETUP_FAILED) { setupFailedCallbacks.push(callback); + return; } if (!player) { @@ -196,25 +205,6 @@ export function JWPlayerProvider(config, jwplayer_, adState_, timeState_, callba let getEventPayload; switch (externalEventName) { - case SETUP_COMPLETE: - getEventPayload = () => { - setupCompleteCallbacks = []; - return getSetupCompletePayload(); - }; - break; - - case SETUP_FAILED: - getEventPayload = e => { - setupFailedCallbacks = []; - return { - playerVersion, - errorCode: e.code, - errorMessage: e.message, - sourceError: e.sourceError - }; - }; - break; - case AD_REQUEST: case AD_PLAY: case AD_PAUSE: @@ -489,7 +479,17 @@ export function JWPlayerProvider(config, jwplayer_, adState_, timeState_, callba if (!config) { return; } - player.setup(utils.getJwConfig(config)); + player.setup(utils.getJwConfig(config)).on('ready', triggerSetupComplete).on('setupError', triggerSetupFailure); + } + + function triggerSetupComplete() { + if (!setupCompleteCallbacks.length) { + return; + } + + const payload = getSetupCompletePayload(); + setupCompleteCallbacks.forEach(callback => callback(SETUP_COMPLETE, payload)); + setupCompleteCallbacks = []; } function getSetupCompletePayload() { @@ -504,7 +504,7 @@ export function JWPlayerProvider(config, jwplayer_, adState_, timeState_, callba }; } - function triggerSetupFailure(errorCode) { + function triggerSetupFailure(e) { if (!setupFailedCallbacks.length) { return; } @@ -513,9 +513,9 @@ export function JWPlayerProvider(config, jwplayer_, adState_, timeState_, callba divId, playerVersion, type: SETUP_FAILED, - errorCode, - errorMessage: '', - sourceError: null + errorCode: e.code, + errorMessage: e.message, + sourceError: e.sourceError }; setupFailedCallbacks.forEach(callback => callback(SETUP_FAILED, payload)); diff --git a/modules/kargoBidAdapter.js b/modules/kargoBidAdapter.js index 2c33c3f61d1..ecc40b26aa2 100644 --- a/modules/kargoBidAdapter.js +++ b/modules/kargoBidAdapter.js @@ -1,289 +1,504 @@ -import { _each, buildUrl, deepAccess, pick, triggerPixel } from '../src/utils.js'; +import { _each, isEmpty, buildUrl, deepAccess, pick, triggerPixel } from '../src/utils.js'; import { config } from '../src/config.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; import { getStorageManager } from '../src/storageManager.js'; import { BANNER, VIDEO } from '../src/mediaTypes.js'; -const BIDDER_CODE = 'kargo'; -const HOST = 'https://krk.kargo.com'; -const SYNC = 'https://crb.kargo.com/api/v1/initsyncrnd/{UUID}?seed={SEED}&idx={INDEX}&gdpr={GDPR}&gdpr_consent={GDPR_CONSENT}&us_privacy={US_PRIVACY}'; -const SYNC_COUNT = 5; -const GVLID = 972; -const SUPPORTED_MEDIA_TYPES = [BANNER, VIDEO]; -const storage = getStorageManager({gvlid: GVLID, bidderCode: BIDDER_CODE}); +const PREBID_VERSION = '$prebid.version$' + +const BIDDER = Object.freeze({ + CODE: 'kargo', + HOST: 'krk2.kargo.com', + REQUEST_METHOD: 'POST', + REQUEST_ENDPOINT: '/api/v1/prebid', + TIMEOUT_ENDPOINT: '/api/v1/event/timeout', + GVLID: 972, + SUPPORTED_MEDIA_TYPES: [BANNER, VIDEO], +}); + +const STORAGE = getStorageManager({bidderCode: BIDDER.CODE}); + +const CURRENCY = Object.freeze({ + KEY: 'currency', + US_DOLLAR: 'USD', +}); + +const REQUEST_KEYS = Object.freeze({ + SOCIAL_CANVAS: 'params.socialCanvas', + SUA: 'ortb2.device.sua', + TDID_ADAPTER: 'userId.tdid', +}); + +const SUA = Object.freeze({ + BROWSERS: 'browsers', + MOBILE: 'mobile', + MODEL: 'model', + PLATFORM: 'platform', + SOURCE: 'source', +}); + +const SUA_ATTRIBUTES = [ + SUA.BROWSERS, + SUA.MOBILE, + SUA.MODEL, + SUA.SOURCE, + SUA.PLATFORM, +]; + +const CERBERUS = Object.freeze({ + KEY: 'krg_crb', + SYNC_URL: 'https://crb.kargo.com/api/v1/initsyncrnd/{UUID}?seed={SEED}&idx={INDEX}&gdpr={GDPR}&gdpr_consent={GDPR_CONSENT}&us_privacy={US_PRIVACY}', + SYNC_COUNT: 5, + PAGE_VIEW_ID: 'pageViewId', + PAGE_VIEW_TIMESTAMP: 'pageViewTimestamp', + PAGE_VIEW_URL: 'pageViewUrl' +}); let sessionId, lastPageUrl, requestCounter; -export const spec = { - gvlid: GVLID, - code: BIDDER_CODE, - isBidRequestValid: function(bid) { - if (!bid || !bid.params) { - return false; - } +function isBidRequestValid(bid) { + if (!bid || !bid.params) { + return false; + } - return !!bid.params.placementId; - }, - buildRequests: function(validBidRequests, bidderRequest) { - const currencyObj = config.getConfig('currency'); - const currency = (currencyObj && currencyObj.adServerCurrency) || 'USD'; - const bidIDs = {}; - const bidSizes = {}; - - _each(validBidRequests, bid => { - bidIDs[bid.bidId] = bid.params.placementId; - bidSizes[bid.bidId] = bid.sizes; - }); + return !!bid.params.placementId; +} + +function buildRequests(validBidRequests, bidderRequest) { + const currencyObj = config.getConfig(CURRENCY.KEY); + const currency = (currencyObj && currencyObj.adServerCurrency) ? currencyObj.adServerCurrency : null; + const impressions = []; + + _each(validBidRequests, bid => { + impressions.push(getImpression(bid)) + }); + + const firstBidRequest = validBidRequests[0]; + const tdidAdapter = deepAccess(firstBidRequest, REQUEST_KEYS.TDID_ADAPTER); + + const metadata = getAllMetadata(bidderRequest); + + const krakenParams = Object.assign({}, { + pbv: PREBID_VERSION, + aid: firstBidRequest.auctionId, + sid: _getSessionId(), + url: metadata.pageURL, + timeout: bidderRequest.timeout, + ts: new Date().getTime(), + device: { + size: [ + window.screen.width, + window.screen.height + ] + }, + imp: impressions, + user: getUserIds(tdidAdapter, bidderRequest.uspConsent, bidderRequest.gdprConsent, firstBidRequest.userIdAsEids), + }); + + const reqCount = getRequestCount() + if (reqCount != null) { + krakenParams.requestCount = reqCount; + } - const firstBidRequest = validBidRequests[0]; - - const tdid = deepAccess(firstBidRequest, 'userId.tdid') - - const transformedParams = Object.assign({}, { - sessionId: spec._getSessionId(), - requestCount: spec._getRequestCount(), - timeout: bidderRequest.timeout, - currency, - cpmGranularity: 1, - timestamp: (new Date()).getTime(), - cpmRange: { - floor: 0, - ceil: 20 - }, - bidIDs, - bidSizes, - device: { - width: window.screen.width, - height: window.screen.height - }, - prebidRawBidRequests: validBidRequests - }, spec._getAllMetadata(bidderRequest, tdid)); - - // User Agent Client Hints / SUA - const uaClientHints = deepAccess(firstBidRequest, 'ortb2.device.sua'); - if (uaClientHints) { - transformedParams.device.sua = pick(uaClientHints, ['browsers', 'platform', 'mobile', 'model']); - } + if (currency != null && currency != CURRENCY.US_DOLLAR) { + krakenParams.cur = currency; + } - // Pull Social Canvas segments and embed URL - const socialCanvas = deepAccess(firstBidRequest, 'params.socialCanvas'); - if (socialCanvas) { - transformedParams.socialCanvasSegments = socialCanvas.segments; - transformedParams.socialEmbedURL = socialCanvas.embedURL; - } + if (metadata.rawCRB != null) { + krakenParams.rawCRB = metadata.rawCRB + } - const encodedParams = encodeURIComponent(JSON.stringify(transformedParams)); - return Object.assign({}, bidderRequest, { - method: 'GET', - url: `${HOST}/api/v2/bid`, - data: `json=${encodedParams}`, - currency: currency - }); - }, - interpretResponse: function(response, bidRequest) { - let bids = response.body; - const bidResponses = []; - for (let bidId in bids) { - let adUnit = bids[bidId]; - let meta = { - mediaType: BANNER - }; - - if (adUnit.metadata && adUnit.metadata.landingPageDomain) { - meta.clickUrl = adUnit.metadata.landingPageDomain[0]; - meta.advertiserDomains = adUnit.metadata.landingPageDomain; + if (metadata.rawCRBLocalStorage != null) { + krakenParams.rawCRBLocalStorage = metadata.rawCRBLocalStorage + } + + // Pull Social Canvas segments and embed URL + const socialCanvas = deepAccess(firstBidRequest, REQUEST_KEYS.SOCIAL_CANVAS); + + if (socialCanvas != null) { + krakenParams.socan = socialCanvas; + } + + // User Agent Client Hints / SUA + const uaClientHints = deepAccess(firstBidRequest, REQUEST_KEYS.SUA); + if (uaClientHints) { + const suaValidAttributes = [] + + SUA_ATTRIBUTES.forEach(suaKey => { + const suaValue = uaClientHints[suaKey]; + if (!suaValue) { + return; } - if (adUnit.mediaType && SUPPORTED_MEDIA_TYPES.includes(adUnit.mediaType)) { - meta.mediaType = adUnit.mediaType; + // Do not pass any empty strings + if (typeof suaValue == 'string' && suaValue.trim() === '') { + return; } - const bidResponse = { - ad: adUnit.adm, - requestId: bidId, - cpm: Number(adUnit.cpm), - width: adUnit.width, - height: adUnit.height, - ttl: 300, - creativeId: adUnit.id, - dealId: adUnit.targetingCustom, - netRevenue: true, - currency: adUnit.currency || bidRequest.currency, - mediaType: meta.mediaType, - meta: meta - }; - - if (meta.mediaType == VIDEO) { - bidResponse.vastXml = adUnit.adm; + switch (suaKey) { + case SUA.MOBILE && suaValue < 1: // Do not pass 0 value for mobile + case SUA.SOURCE && suaValue < 1: // Do not pass 0 value for source + break; + default: + suaValidAttributes.push(suaKey); } + }); - bidResponses.push(bidResponse); - } + krakenParams.device.sua = pick(uaClientHints, suaValidAttributes); + } + + const validPageId = getLocalStorageSafely(CERBERUS.PAGE_VIEW_ID) != null + const validPageTimestamp = getLocalStorageSafely(CERBERUS.PAGE_VIEW_TIMESTAMP) != null + const validPageUrl = getLocalStorageSafely(CERBERUS.PAGE_VIEW_URL) != null + + const page = {} + if (validPageId) { + page.id = getLocalStorageSafely(CERBERUS.PAGE_VIEW_ID); + } + if (validPageTimestamp) { + page.timestamp = Number(getLocalStorageSafely(CERBERUS.PAGE_VIEW_TIMESTAMP)); + } + if (validPageUrl) { + page.url = getLocalStorageSafely(CERBERUS.PAGE_VIEW_URL); + } + if (!isEmpty(page)) { + krakenParams.page = page; + } + + return Object.assign({}, bidderRequest, { + method: BIDDER.REQUEST_METHOD, + url: `https://${BIDDER.HOST}${BIDDER.REQUEST_ENDPOINT}`, + data: krakenParams, + currency: currency + }); +} + +function interpretResponse(response, bidRequest) { + let bids = response.body; + const bidResponses = []; + if (isEmpty(bids)) { return bidResponses; - }, - getUserSyncs: function(syncOptions, responses, gdprConsent, usPrivacy) { - const syncs = []; - const seed = spec._generateRandomUuid(); - const clientId = spec._getClientId(); - var gdpr = (gdprConsent && gdprConsent.gdprApplies) ? 1 : 0; - var gdprConsentString = (gdprConsent && gdprConsent.consentString) ? gdprConsent.consentString : ''; - // don't sync if opted out via usPrivacy - if (typeof usPrivacy == 'string' && usPrivacy.length == 4 && usPrivacy[0] == 1 && usPrivacy[2] == 'Y') { - return syncs; + } + + if (typeof bids !== 'object') { + return bidResponses; + } + + Object.entries(bids).forEach((entry) => { + const [bidID, adUnit] = entry; + + let meta = { + mediaType: adUnit.mediaType && BIDDER.SUPPORTED_MEDIA_TYPES.includes(adUnit.mediaType) ? adUnit.mediaType : BANNER + }; + + if (adUnit.metadata && adUnit.metadata.landingPageDomain) { + meta.clickUrl = adUnit.metadata.landingPageDomain[0]; + meta.advertiserDomains = adUnit.metadata.landingPageDomain; } - if (syncOptions.iframeEnabled && seed && clientId) { - for (let i = 0; i < SYNC_COUNT; i++) { - syncs.push({ - type: 'iframe', - url: SYNC.replace('{UUID}', clientId).replace('{SEED}', seed) - .replace('{INDEX}', i) - .replace('{GDPR}', gdpr) - .replace('{GDPR_CONSENT}', gdprConsentString) - .replace('{US_PRIVACY}', usPrivacy || '') - }); + + const bidResponse = { + requestId: bidID, + cpm: Number(adUnit.cpm), + width: adUnit.width, + height: adUnit.height, + ttl: 300, + creativeId: adUnit.id, + dealId: adUnit.targetingCustom, + netRevenue: true, + currency: adUnit.currency || bidRequest.currency, + mediaType: meta.mediaType, + meta: meta + }; + + if (meta.mediaType == VIDEO) { + if (adUnit.admUrl) { + bidResponse.vastUrl = adUnit.admUrl; + } else { + bidResponse.vastXml = adUnit.adm; } + } else { + bidResponse.ad = adUnit.adm; } + + bidResponses.push(bidResponse); + }) + + return bidResponses; +} + +function getUserSyncs(syncOptions, responses, gdprConsent, usPrivacy) { + const syncs = []; + const seed = _generateRandomUUID(); + const clientId = getClientId(); + + var gdpr = (gdprConsent && gdprConsent.gdprApplies) ? 1 : 0; + var gdprConsentString = (gdprConsent && gdprConsent.consentString) ? gdprConsent.consentString : ''; + + // don't sync if opted out via usPrivacy + if (typeof usPrivacy == 'string' && usPrivacy.length == 4 && usPrivacy[0] == 1 && usPrivacy[2] == 'Y') { return syncs; - }, - supportedMediaTypes: SUPPORTED_MEDIA_TYPES, - onTimeout: function(timeoutData) { - if (timeoutData == null) { - return; + } + if (syncOptions.iframeEnabled && seed && clientId) { + for (let i = 0; i < CERBERUS.SYNC_COUNT; i++) { + syncs.push({ + type: 'iframe', + url: CERBERUS.SYNC_URL.replace('{UUID}', clientId) + .replace('{SEED}', seed) + .replace('{INDEX}', i) + .replace('{GDPR}', gdpr) + .replace('{GDPR_CONSENT}', gdprConsentString) + .replace('{US_PRIVACY}', usPrivacy || '') + }); } + } + return syncs; +} - timeoutData.forEach((bid) => { - this._sendTimeoutData(bid.auctionId, bid.timeout); - }); - }, - - _getCrbFromCookie() { - try { - const crb = JSON.parse(storage.getCookie('krg_crb')); - if (crb && crb.v) { - let vParsed = JSON.parse(atob(crb.v)); - if (vParsed) { - return vParsed; - } +function onTimeout(timeoutData) { + if (timeoutData == null) { + return; + } + + timeoutData.forEach((bid) => { + sendTimeoutData(bid.auctionId, bid.timeout); + }); +} + +function _generateRandomUUID() { + try { + // crypto.getRandomValues is supported everywhere but Opera Mini for years + var buffer = new Uint8Array(16); + crypto.getRandomValues(buffer); + buffer[6] = (buffer[6] & ~176) | 64; + buffer[8] = (buffer[8] & ~64) | 128; + var hex = Array.prototype.map.call(new Uint8Array(buffer), function(x) { + return ('00' + x.toString(16)).slice(-2); + }).join(''); + return hex.slice(0, 8) + '-' + hex.slice(8, 12) + '-' + hex.slice(12, 16) + '-' + hex.slice(16, 20) + '-' + hex.slice(20); + } catch (e) { + return ''; + } +} + +function _getCrb() { + let localStorageCrb = getCrbFromLocalStorage(); + if (Object.keys(localStorageCrb).length) { + return localStorageCrb; + } + return getCrbFromCookie(); +} + +function _getSessionId() { + if (!sessionId) { + sessionId = _generateRandomUUID(); + } + return sessionId; +} + +function getCrbFromCookie() { + try { + const crb = JSON.parse(STORAGE.getCookie(CERBERUS.KEY)); + if (crb && crb.v) { + let vParsed = JSON.parse(atob(crb.v)); + if (vParsed) { + return vParsed; } - return {}; - } catch (e) { - return {}; } - }, + return {}; + } catch (e) { + return {}; + } +} - _getCrbFromLocalStorage() { - try { - return JSON.parse(atob(spec._getLocalStorageSafely('krg_crb'))); - } catch (e) { - return {}; - } - }, +function getCrbFromLocalStorage() { + try { + return JSON.parse(atob(getLocalStorageSafely(CERBERUS.KEY))); + } catch (e) { + return {}; + } +} - _getCrb() { - let localStorageCrb = spec._getCrbFromLocalStorage(); - if (Object.keys(localStorageCrb).length) { - return localStorageCrb; - } - return spec._getCrbFromCookie(); - }, - - _getLocalStorageSafely(key) { - try { - return storage.getDataFromLocalStorage(key); - } catch (e) { - return null; - } - }, - - _getUserIds(tdid, usp, gdpr) { - const crb = spec._getCrb(); - const userIds = { - kargoID: crb.lexId, - clientID: crb.clientId, - crbIDs: crb.syncIds || {}, - optOut: crb.optOut, - usp: usp - }; +function getLocalStorageSafely(key) { + try { + return STORAGE.getDataFromLocalStorage(key); + } catch (e) { + return null; + } +} + +function getUserIds(tdidAdapter, usp, gdpr, eids) { + const crb = spec._getCrb(); + const userIds = { + crbIDs: crb.syncIds || {} + }; + + // Pull Trade Desk ID from adapter + if (tdidAdapter) { + userIds.tdID = tdidAdapter; + } - try { - if (gdpr) { - userIds['gdpr'] = { - consent: gdpr.consentString || '', - applies: !!gdpr.gdprApplies, - } + // Pull Trade Desk ID from our storage + if (!tdidAdapter && crb.tdID) { + userIds.tdID = crb.tdID; + } + + if (usp) { + userIds.usp = usp; + } + + try { + if (gdpr) { + userIds['gdpr'] = { + consent: gdpr.consentString || '', + applies: !!gdpr.gdprApplies, } - } catch (e) { } - if (tdid) { - userIds.tdID = tdid; + } catch (e) { + } + + if (crb.lexId != null) { + userIds.kargoID = crb.lexId; + } + + if (crb.clientId != null) { + userIds.clientID = crb.clientId; + } + + if (crb.optOut != null) { + userIds.optOut = crb.optOut; + } + + if (eids != null) { + userIds.sharedIDEids = eids; + } + + return userIds; +} + +function getClientId() { + const crb = spec._getCrb(); + return crb.clientId; +} + +function getAllMetadata(bidderRequest) { + return { + pageURL: bidderRequest?.refererInfo?.page, + rawCRB: STORAGE.getCookie(CERBERUS.KEY), + rawCRBLocalStorage: getLocalStorageSafely(CERBERUS.KEY) + }; +} + +function getRequestCount() { + if (lastPageUrl === window.location.pathname) { + return ++requestCounter; + } + lastPageUrl = window.location.pathname; + return requestCounter = 0; +} + +function sendTimeoutData(auctionId, auctionTimeout) { + let params = { + aid: auctionId, + ato: auctionTimeout + }; + + try { + let timeoutRequestUrl = buildUrl({ + protocol: 'https', + hostname: BIDDER.HOST, + pathname: BIDDER.TIMEOUT_ENDPOINT, + search: params + }); + + triggerPixel(timeoutRequestUrl); + } catch (e) {} +} + +function getImpression(bid) { + const imp = { + id: bid.bidId, + tid: bid.transactionId, + pid: bid.params.placementId, + code: bid.adUnitCode + }; + + if (bid.floorData != null && bid.floorData.floorMin > 0) { + imp.floor = bid.floorData.floorMin; + } + + if (bid.bidRequestsCount > 0) { + imp.bidRequestCount = bid.bidRequestsCount; + } + + if (bid.bidderRequestsCount > 0) { + imp.bidderRequestCount = bid.bidderRequestsCount; + } + + if (bid.bidderWinsCount > 0) { + imp.bidderWinCount = bid.bidderWinsCount; + } + + const gpid = getGPID(bid) + if (gpid != null && gpid != '') { + imp.fpd = { + gpid: gpid + } + } + + if (bid.mediaTypes != null) { + if (bid.mediaTypes.banner != null) { + imp.banner = bid.mediaTypes.banner; } - return userIds; - }, - - _getClientId() { - const crb = spec._getCrb(); - return crb.clientId; - }, - - _getAllMetadata(bidderRequest, tdid) { - return { - userIDs: spec._getUserIds(tdid, bidderRequest.uspConsent, bidderRequest.gdprConsent), - pageURL: bidderRequest?.refererInfo?.page, - rawCRB: storage.getCookie('krg_crb'), - rawCRBLocalStorage: spec._getLocalStorageSafely('krg_crb') - }; - }, - _getSessionId() { - if (!sessionId) { - sessionId = spec._generateRandomUuid(); + if (bid.mediaTypes.video != null) { + imp.video = bid.mediaTypes.video; } - return sessionId; - }, - _getRequestCount() { - if (lastPageUrl === window.location.pathname) { - return ++requestCounter; + if (bid.mediaTypes.native != null) { + imp.native = bid.mediaTypes.native; } - lastPageUrl = window.location.pathname; - return requestCounter = 0; - }, - - _generateRandomUuid() { - try { - // crypto.getRandomValues is supported everywhere but Opera Mini for years - var buffer = new Uint8Array(16); - crypto.getRandomValues(buffer); - buffer[6] = (buffer[6] & ~176) | 64; - buffer[8] = (buffer[8] & ~64) | 128; - var hex = Array.prototype.map.call(new Uint8Array(buffer), function(x) { - return ('00' + x.toString(16)).slice(-2); - }).join(''); - return hex.slice(0, 8) + '-' + hex.slice(8, 12) + '-' + hex.slice(12, 16) + '-' + hex.slice(16, 20) + '-' + hex.slice(20); - } catch (e) { - return ''; + } + + return imp +} + +function getGPID(bid) { + if (bid.ortb2Imp != null) { + if (bid.ortb2Imp.gpid != null && bid.ortb2Imp.gpid != '') { + return bid.ortb2Imp.gpid; } - }, - _sendTimeoutData(auctionId, auctionTimeout) { - let params = { - aid: auctionId, - ato: auctionTimeout, - }; + if (bid.ortb2Imp.ext != null && bid.ortb2Imp.ext.data != null) { + if (bid.ortb2Imp.ext.data.pbAdSlot != null && bid.ortb2Imp.ext.data.pbAdSlot != '') { + return bid.ortb2Imp.ext.data.pbAdSlot; + } - try { - let timeoutRequestUrl = buildUrl({ - protocol: 'https', - hostname: 'krk.kargo.com', - pathname: '/api/v1/event/timeout', - search: params - }); + if (bid.ortb2Imp.ext.data.adServer != null && bid.ortb2Imp.ext.data.adServer.adSlot != null && bid.ortb2Imp.ext.data.adServer.adSlot != '') { + return bid.ortb2Imp.ext.data.adServer.adSlot; + } + } + } - triggerPixel(timeoutRequestUrl); - } catch (e) {} + if (bid.adUnitCode != null && bid.adUnitCode != '') { + return bid.adUnitCode; } + return ''; +} + +export const spec = { + gvlid: BIDDER.GVLID, + code: BIDDER.CODE, + isBidRequestValid, + buildRequests, + interpretResponse, + getUserSyncs, + supportedMediaTypes: BIDDER.SUPPORTED_MEDIA_TYPES, + onTimeout, + _getCrb, + _getSessionId }; + registerBidder(spec); diff --git a/modules/koblerBidAdapter.js b/modules/koblerBidAdapter.js index 1dc22d0099a..4eef99024f9 100644 --- a/modules/koblerBidAdapter.js +++ b/modules/koblerBidAdapter.js @@ -17,7 +17,6 @@ const BIDDER_ENDPOINT = 'https://bid.essrtb.com/bid/prebid_rtb_call'; const DEV_BIDDER_ENDPOINT = 'https://bid-service.dev.essrtb.com/bid/prebid_rtb_call'; const TIMEOUT_NOTIFICATION_ENDPOINT = 'https://bid.essrtb.com/notify/prebid_timeout'; const SUPPORTED_CURRENCY = 'USD'; -const DEFAULT_TIMEOUT = 1000; const TIME_TO_LIVE_IN_SECONDS = 10 * 60; export const isBidRequestValid = function (bid) { @@ -124,7 +123,7 @@ function getPageUrlFromRefererInfo() { function buildOpenRtbBidRequestPayload(validBidRequests, bidderRequest) { const imps = validBidRequests.map(buildOpenRtbImpObject); - const timeout = bidderRequest.timeout || config.getConfig('bidderTimeout') || DEFAULT_TIMEOUT; + const timeout = bidderRequest.timeout; const pageUrl = getPageUrlFromRequest(validBidRequests[0], bidderRequest) const request = { id: bidderRequest.auctionId, diff --git a/modules/kueezBidAdapter.js b/modules/kueezBidAdapter.js index 24d339d1938..6efb67ddec8 100644 --- a/modules/kueezBidAdapter.js +++ b/modules/kueezBidAdapter.js @@ -293,7 +293,7 @@ function generateSharedParams(sharedParams, bidderRequest) { const generalBidParams = getBidIdParameter('params', sharedParams); const userIds = getBidIdParameter('userId', sharedParams); const ortb2Metadata = bidderRequest.ortb2 || {}; - const timeout = config.getConfig('bidderTimeout'); + const timeout = bidderRequest.timeout; const params = { adapter_version: VERSION, diff --git a/modules/kueezRtbBidAdapter.js b/modules/kueezRtbBidAdapter.js index 1ee3b7331cb..3bd468a581f 100644 --- a/modules/kueezRtbBidAdapter.js +++ b/modules/kueezRtbBidAdapter.js @@ -1,7 +1,8 @@ -import { _each, deepAccess, parseSizesInput, parseUrl, uniques, isFn } from '../src/utils.js'; -import { registerBidder } from '../src/adapters/bidderFactory.js'; -import { BANNER } from '../src/mediaTypes.js'; -import { getStorageManager } from '../src/storageManager.js'; +import {_each, deepAccess, parseSizesInput, parseUrl, uniques, isFn} from '../src/utils.js'; +import {registerBidder} from '../src/adapters/bidderFactory.js'; +import {BANNER, VIDEO} from '../src/mediaTypes.js'; +import {getStorageManager} from '../src/storageManager.js'; +import { config } from '../src/config.js'; const GVLID = 1165; const DEFAULT_SUB_DOMAIN = 'exchange'; @@ -22,11 +23,11 @@ export const SUPPORTED_ID_SYSTEMS = { 'tdid': 1, 'pubProvidedId': 1 }; -const storage = getStorageManager({ gvlid: GVLID, bidderCode: BIDDER_CODE }); +const storage = getStorageManager({bidderCode: BIDDER_CODE}); function getTopWindowQueryParams() { try { - const parsedUrl = parseUrl(window.top.document.URL, { decodeSearchAsString: true }); + const parsedUrl = parseUrl(window.top.document.URL, {decodeSearchAsString: true}); return parsedUrl.search; } catch (e) { return ''; @@ -54,9 +55,22 @@ function isBidRequestValid(bid) { return !!(extractCID(params) && extractPID(params)); } -function buildRequest(bid, topWindowUrl, sizes, bidderRequest) { - const { params, bidId, userId, adUnitCode, schain } = bid; - let { bidFloor, ext } = params; +function buildRequest(bid, topWindowUrl, sizes, bidderRequest, bidderTimeout) { + const { + params, + bidId, + userId, + adUnitCode, + schain, + mediaTypes, + auctionId, + transactionId, + bidderRequestId, + bidRequestsCount, + bidderRequestsCount, + bidderWinsCount + } = bid; + let {bidFloor, ext} = params; const hashUrl = hashCode(topWindowUrl); const uniqueDealId = getUniqueDealId(hashUrl); const cId = extractCID(params); @@ -89,11 +103,25 @@ function buildRequest(bid, topWindowUrl, sizes, bidderRequest) { bidderVersion: BIDDER_VERSION, prebidVersion: '$prebid.version$', res: `${screen.width}x${screen.height}`, - schain: schain + schain: schain, + mediaTypes: mediaTypes, + auctionId: auctionId, + transactionId: transactionId, + bidderRequestId: bidderRequestId, + bidRequestsCount: bidRequestsCount, + bidderRequestsCount: bidderRequestsCount, + bidderWinsCount: bidderWinsCount, + bidderTimeout: bidderTimeout }; appendUserIdsToRequestPayload(data, userId); + const sua = deepAccess(bidderRequest, 'ortb2.device.sua'); + + if (sua) { + data.sua = sua; + } + if (bidderRequest.gdprConsent) { if (bidderRequest.gdprConsent.consentString) { data.gdprConsent = bidderRequest.gdprConsent.consentString; @@ -106,6 +134,14 @@ function buildRequest(bid, topWindowUrl, sizes, bidderRequest) { data.usPrivacy = bidderRequest.uspConsent; } + if (bidderRequest.gppConsent) { + data.gppString = bidderRequest.gppConsent.gppString; + data.gppSid = bidderRequest.gppConsent.applicableSections; + } else if (bidderRequest.ortb2?.regs?.gpp) { + data.gppString = bidderRequest.ortb2.regs.gpp; + data.gppSid = bidderRequest.ortb2.regs.gpp_sid; + } + const dto = { method: 'POST', url: `${createDomain(subDomain)}/prebid/multi/${cId}`, @@ -147,10 +183,11 @@ function appendUserIdsToRequestPayload(payloadRef, userIds) { function buildRequests(validBidRequests, bidderRequest) { const topWindowUrl = bidderRequest.refererInfo.page || bidderRequest.refererInfo.topmostLocation; + const bidderTimeout = config.getConfig('bidderTimeout'); const requests = []; validBidRequests.forEach(validBidRequest => { const sizes = parseSizesInput(validBidRequest.sizes); - const request = buildRequest(validBidRequest, topWindowUrl, sizes, bidderRequest); + const request = buildRequest(validBidRequest, topWindowUrl, sizes, bidderRequest, bidderTimeout); requests.push(request); }); return requests; @@ -160,18 +197,19 @@ function interpretResponse(serverResponse, request) { if (!serverResponse || !serverResponse.body) { return []; } - const { bidId } = request.data; - const { results } = serverResponse.body; + const {bidId} = request.data; + const {results} = serverResponse.body; let output = []; try { results.forEach(result => { - const { creativeId, ad, price, exp, width, height, currency, advertiserDomains } = result; + const {creativeId, ad, price, exp, width, height, currency, advertiserDomains, mediaType = BANNER} = result; if (!ad || !price) { return; } - output.push({ + + const response = { requestId: bidId, cpm: price, width: width, @@ -180,11 +218,22 @@ function interpretResponse(serverResponse, request) { currency: currency || CURRENCY, netRevenue: true, ttl: exp || TTL_SECONDS, - ad: ad, meta: { advertiserDomains: advertiserDomains || [] } - }) + }; + + if (mediaType === BANNER) { + Object.assign(response, { + ad: ad, + }); + } else { + Object.assign(response, { + vastXml: ad, + mediaType: VIDEO + }); + } + output.push(response); }); return output; } catch (e) { @@ -194,8 +243,8 @@ function interpretResponse(serverResponse, request) { function getUserSyncs(syncOptions, responses, gdprConsent = {}, uspConsent = '') { let syncs = []; - const { iframeEnabled, pixelEnabled } = syncOptions; - const { gdprApplies, consentString = '' } = gdprConsent; + const {iframeEnabled, pixelEnabled} = syncOptions; + const {gdprApplies, consentString = ''} = gdprConsent; const cidArr = responses.filter(resp => deepAccess(resp, 'body.cid')).map(resp => resp.body.cid).filter(uniques); const params = `?cid=${encodeURIComponent(cidArr.join(','))}&gdpr=${gdprApplies ? 1 : 0}&gdpr_consent=${encodeURIComponent(consentString || '')}&us_privacy=${encodeURIComponent(uspConsent || '')}` @@ -219,7 +268,9 @@ export function hashCode(s, prefix = '_') { let h = 0 let i = 0; if (l > 0) { - while (i < l) { h = (h << 5) - h + s.charCodeAt(i++) | 0; } + while (i < l) { + h = (h << 5) - h + s.charCodeAt(i++) | 0; + } } return prefix + h; } @@ -243,7 +294,8 @@ export function getUniqueDealId(key, expiry = UNIQUE_DEAL_ID_EXPIRY) { export function getStorageItem(key) { try { return tryParseJSON(storage.getDataFromLocalStorage(key)); - } catch (e) { } + } catch (e) { + } return null; } @@ -251,9 +303,10 @@ export function getStorageItem(key) { export function setStorageItem(key, value, timestamp) { try { const created = timestamp || Date.now(); - const data = JSON.stringify({ value, created }); + const data = JSON.stringify({value, created}); storage.setDataInLocalStorage(key, data); - } catch (e) { } + } catch (e) { + } } export function tryParseJSON(value) { @@ -268,7 +321,7 @@ export const spec = { code: BIDDER_CODE, version: BIDDER_VERSION, gvlid: GVLID, - supportedMediaTypes: [BANNER], + supportedMediaTypes: [BANNER, VIDEO], isBidRequestValid, buildRequests, interpretResponse, diff --git a/modules/kulturemediaBidAdapter.js b/modules/kulturemediaBidAdapter.js new file mode 100644 index 00000000000..0acdd6406cb --- /dev/null +++ b/modules/kulturemediaBidAdapter.js @@ -0,0 +1,472 @@ +import { + deepSetValue, + logInfo, + deepAccess, + logError, + isFn, + isPlainObject, + isStr, + isNumber, + isArray, logMessage +} from '../src/utils.js'; +import {registerBidder} from '../src/adapters/bidderFactory.js'; +import {BANNER, VIDEO} from '../src/mediaTypes.js'; + +const BIDDER_CODE = 'kulturemedia'; +const DEFAULT_BID_TTL = 300; +const DEFAULT_CURRENCY = 'USD'; +const DEFAULT_NET_REVENUE = true; +const DEFAULT_NETWORK_ID = 1; +const OPENRTB_VIDEO_PARAMS = [ + 'mimes', + 'minduration', + 'maxduration', + 'placement', + 'protocols', + 'startdelay', + 'skip', + 'skipafter', + 'minbitrate', + 'maxbitrate', + 'delivery', + 'playbackmethod', + 'api', + 'linearity' +]; + +export const spec = { + code: BIDDER_CODE, + VERSION: '1.0.0', + supportedMediaTypes: [BANNER, VIDEO], + ENDPOINT: 'https://ads.kulture.media/pbjs', + + /** + * Determines whether or not the given bid request is valid. + * + * @param {BidRequest} bidRequest The bid params to validate. + * @return boolean True if this is a valid bid, and false otherwise. + */ + isBidRequestValid: function (bid) { + return ( + _validateParams(bid) && + _validateBanner(bid) && + _validateVideo(bid) + ); + }, + + /** + * Make a server request from the list of BidRequests. + * + * @param {BidRequest[]} validBidRequests A non-empty list of bid requests which should be sent to the Server. + * @param {BidderRequest} bidderRequest bidder request object. + * @return ServerRequest Info describing the request to the server. + */ + buildRequests: function (validBidRequests, bidderRequest) { + if (!validBidRequests || !bidderRequest) { + return; + } + + // We need to refactor this to support mixed content when there are both + // banner and video bid requests + let openrtbRequest; + if (hasBannerMediaType(validBidRequests[0])) { + openrtbRequest = buildBannerRequestData(validBidRequests, bidderRequest); + } else if (hasVideoMediaType(validBidRequests[0])) { + openrtbRequest = buildVideoRequestData(validBidRequests[0], bidderRequest); + } + + // adding schain object + if (validBidRequests[0].schain) { + deepSetValue(openrtbRequest, 'source.ext.schain', validBidRequests[0].schain); + } + + // Attaching GDPR Consent Params + if (bidderRequest.gdprConsent) { + deepSetValue(openrtbRequest, 'user.ext.consent', bidderRequest.gdprConsent.consentString); + deepSetValue(openrtbRequest, 'regs.ext.gdpr', (bidderRequest.gdprConsent.gdprApplies ? 1 : 0)); + } + + // CCPA + if (bidderRequest.uspConsent) { + deepSetValue(openrtbRequest, 'regs.ext.us_privacy', bidderRequest.uspConsent); + } + + // EIDS + const eids = deepAccess(validBidRequests[0], 'userIdAsEids'); + if (Array.isArray(eids) && eids.length > 0) { + deepSetValue(openrtbRequest, 'user.ext.eids', eids); + } + + let publisherId = validBidRequests[0].params.publisherId; + let placementId = validBidRequests[0].params.placementId; + const networkId = validBidRequests[0].params.networkId || DEFAULT_NETWORK_ID; + + if (validBidRequests[0].params.e2etest) { + logMessage('E2E test mode enabled'); + publisherId = 'e2etest' + } + let baseEndpoint = spec.ENDPOINT + '?pid=' + publisherId; + + if (placementId) { + baseEndpoint += '&placementId=' + placementId + } + if (networkId) { + baseEndpoint += '&nId=' + networkId + } + + const payloadString = JSON.stringify(openrtbRequest); + return { + method: 'POST', + url: baseEndpoint, + data: payloadString, + }; + }, + + interpretResponse: function (serverResponse) { + const bidResponses = []; + const response = (serverResponse || {}).body; + // response is always one seat (exchange) with (optional) bids for each impression + if (response && response.seatbid && response.seatbid.length === 1 && response.seatbid[0].bid && response.seatbid[0].bid.length) { + response.seatbid[0].bid.forEach(bid => { + if (bid.adm && bid.price) { + bidResponses.push(_createBidResponse(bid)); + } + }) + } else { + logInfo('kulturemedia.interpretResponse :: no valid responses to interpret'); + } + return bidResponses; + }, + + getUserSyncs: function (syncOptions, serverResponses) { + logInfo('kulturemedia.getUserSyncs', 'syncOptions', syncOptions, 'serverResponses', serverResponses); + let syncs = []; + + if (!syncOptions.iframeEnabled && !syncOptions.pixelEnabled) { + return syncs; + } + + serverResponses.forEach(resp => { + const userSync = deepAccess(resp, 'body.ext.usersync'); + if (userSync) { + let syncDetails = []; + Object.keys(userSync).forEach(key => { + const value = userSync[key]; + if (value.syncs && value.syncs.length) { + syncDetails = syncDetails.concat(value.syncs); + } + }); + syncDetails.forEach(syncDetails => { + syncs.push({ + type: syncDetails.type === 'iframe' ? 'iframe' : 'image', + url: syncDetails.url + }); + }); + + if (!syncOptions.iframeEnabled) { + syncs = syncs.filter(s => s.type !== 'iframe') + } + if (!syncOptions.pixelEnabled) { + syncs = syncs.filter(s => s.type !== 'image') + } + } + }); + logInfo('kulturemedia.getUserSyncs result=%o', syncs); + return syncs; + }, + +}; + +/* ======================================= + * Util Functions + *======================================= */ + +/** + * @param {BidRequest} bidRequest bid request + */ +function hasBannerMediaType(bidRequest) { + return !!deepAccess(bidRequest, 'mediaTypes.banner'); +} + +/** + * @param {BidRequest} bidRequest bid request + */ +function hasVideoMediaType(bidRequest) { + return !!deepAccess(bidRequest, 'mediaTypes.video'); +} + +function _validateParams(bidRequest) { + if (!bidRequest.params) { + return false; + } + + if (bidRequest.params.e2etest) { + return true; + } + + if (!bidRequest.params.publisherId) { + logError('Validation failed: publisherId not declared'); + return false; + } + + if (!bidRequest.params.placementId) { + logError('Validation failed: placementId not declared'); + return false; + } + + const mediaTypesExists = hasVideoMediaType(bidRequest) || hasBannerMediaType(bidRequest); + if (!mediaTypesExists) { + return false; + } + + return true; +} + +/** + * Validates banner bid request. If it is not banner media type returns true. + * @param {object} bid, bid to validate + * @return boolean, true if valid, otherwise false + */ +function _validateBanner(bidRequest) { + // If there's no banner no need to validate + if (!hasBannerMediaType(bidRequest)) { + return true; + } + const banner = deepAccess(bidRequest, 'mediaTypes.banner'); + if (!Array.isArray(banner.sizes)) { + return false; + } + + return true; +} + +/** + * Validates video bid request. If it is not video media type returns true. + * @param {object} bid, bid to validate + * @return boolean, true if valid, otherwise false + */ +function _validateVideo(bidRequest) { + // If there's no video no need to validate + if (!hasVideoMediaType(bidRequest)) { + return true; + } + + const videoPlacement = deepAccess(bidRequest, 'mediaTypes.video', {}); + const videoBidderParams = deepAccess(bidRequest, 'params.video', {}); + const params = deepAccess(bidRequest, 'params', {}); + + if (params && params.e2etest) { + return true; + } + + const videoParams = { + ...videoPlacement, + ...videoBidderParams // Bidder Specific overrides + }; + + if (!Array.isArray(videoParams.mimes) || videoParams.mimes.length === 0) { + logError('Validation failed: mimes are invalid'); + return false; + } + + if (!Array.isArray(videoParams.protocols) || videoParams.protocols.length === 0) { + logError('Validation failed: protocols are invalid'); + return false; + } + + if (!videoParams.context) { + logError('Validation failed: context id not declared'); + return false; + } + + if (videoParams.context !== 'instream') { + logError('Validation failed: only context instream is supported '); + return false; + } + + if (typeof videoParams.playerSize === 'undefined' || !Array.isArray(videoParams.playerSize) || !Array.isArray(videoParams.playerSize[0])) { + logError('Validation failed: player size not declared or is not in format [[w,h]]'); + return false; + } + + return true; +} + +/** + * Prepares video request data. + * + * @param bidRequest + * @param bidderRequest + * @returns openrtbRequest + */ +function buildVideoRequestData(bidRequest, bidderRequest) { + const {params} = bidRequest; + + const videoAdUnit = deepAccess(bidRequest, 'mediaTypes.video', {}); + const videoBidderParams = deepAccess(bidRequest, 'params.video', {}); + + const videoParams = { + ...videoAdUnit, + ...videoBidderParams // Bidder Specific overrides + }; + + if (bidRequest.params && bidRequest.params.e2etest) { + videoParams.playerSize = [[640, 480]] + videoParams.conext = 'instream' + } + + const video = { + w: parseInt(videoParams.playerSize[0][0], 10), + h: parseInt(videoParams.playerSize[0][1], 10), + } + + // Obtain all ORTB params related video from Ad Unit + OPENRTB_VIDEO_PARAMS.forEach((param) => { + if (videoParams.hasOwnProperty(param)) { + video[param] = videoParams[param]; + } + }); + + // Placement Inference Rules: + // - If no placement is defined then default to 1 (In Stream) + video.placement = video.placement || 2; + + // - If product is instream (for instream context) then override placement to 1 + if (params.context === 'instream') { + video.startdelay = video.startdelay || 0; + video.placement = 1; + } + + // bid floor + const bidFloorRequest = { + currency: bidRequest.params.cur || 'USD', + mediaType: 'video', + size: '*' + }; + let floorData = bidRequest.params + if (isFn(bidRequest.getFloor)) { + floorData = bidRequest.getFloor(bidFloorRequest); + } else { + if (params.bidfloor) { + floorData = {floor: params.bidfloor, currency: params.currency || 'USD'}; + } + } + + const openrtbRequest = { + id: bidRequest.bidId, + imp: [ + { + id: '1', + video: video, + secure: isSecure() ? 1 : 0, + bidfloor: floorData.floor, + bidfloorcur: floorData.currency + } + ], + site: { + domain: bidderRequest.refererInfo.domain, + page: bidderRequest.refererInfo.page, + ref: bidderRequest.refererInfo.ref, + }, + ext: { + hb: 1, + prebidver: '$prebid.version$', + adapterver: spec.VERSION, + }, + }; + + // content + if (videoParams.content && isPlainObject(videoParams.content)) { + openrtbRequest.site.content = {}; + const contentStringKeys = ['id', 'title', 'series', 'season', 'genre', 'contentrating', 'language', 'url']; + const contentNumberkeys = ['episode', 'prodq', 'context', 'livestream', 'len']; + const contentArrayKeys = ['cat']; + const contentObjectKeys = ['ext']; + for (const contentKey in videoBidderParams.content) { + if ( + (contentStringKeys.indexOf(contentKey) > -1 && isStr(videoParams.content[contentKey])) || + (contentNumberkeys.indexOf(contentKey) > -1 && isNumber(videoParams.content[contentKey])) || + (contentObjectKeys.indexOf(contentKey) > -1 && isPlainObject(videoParams.content[contentKey])) || + (contentArrayKeys.indexOf(contentKey) > -1 && isArray(videoParams.content[contentKey]) && + videoParams.content[contentKey].every(catStr => isStr(catStr)))) { + openrtbRequest.site.content[contentKey] = videoParams.content[contentKey]; + } else { + logMessage('KultureMedia bid adapter validation error: ', contentKey, ' is either not supported is OpenRTB V2.5 or value is undefined'); + } + } + } + + return openrtbRequest; +} + +/** + * Prepares video request data. + * + * @param bidRequest + * @param bidderRequest + * @returns openrtbRequest + */ +function buildBannerRequestData(bidRequests, bidderRequest) { + const impr = bidRequests.map(bidRequest => ({ + id: bidRequest.bidId, + banner: { + format: bidRequest.mediaTypes.banner.sizes.map(sizeArr => ({ + w: sizeArr[0], + h: sizeArr[1] + })) + }, + ext: { + exchange: { + placementId: bidRequest.params.placementId + } + } + })); + + const openrtbRequest = { + id: bidderRequest.auctionId, + imp: impr, + site: { + domain: bidderRequest.refererInfo?.domain, + page: bidderRequest.refererInfo?.page, + ref: bidderRequest.refererInfo?.ref, + }, + ext: {} + }; + return openrtbRequest; +} + +function _createBidResponse(bid) { + const isADomainPresent = + bid.adomain && bid.adomain.length; + const bidResponse = { + requestId: bid.impid, + bidderCode: spec.code, + cpm: bid.price, + width: bid.w, + height: bid.h, + ad: bid.adm, + ttl: typeof bid.exp === 'number' ? bid.exp : DEFAULT_BID_TTL, + creativeId: bid.crid, + netRevenue: DEFAULT_NET_REVENUE, + currency: DEFAULT_CURRENCY, + mediaType: deepAccess(bid, 'ext.prebid.type', BANNER) + } + + if (isADomainPresent) { + bidResponse.meta = { + advertiserDomains: bid.adomain + }; + } + + if (bidResponse.mediaType === VIDEO) { + bidResponse.vastXml = bid.adm; + } + + return bidResponse; +} + +function isSecure() { + return document.location.protocol === 'https:'; +} + +registerBidder(spec); diff --git a/modules/kulturemediaBidAdapter.md b/modules/kulturemediaBidAdapter.md new file mode 100644 index 00000000000..0bd17e97982 --- /dev/null +++ b/modules/kulturemediaBidAdapter.md @@ -0,0 +1,140 @@ +# Overview + +``` +Module Name: Kulture Media Bid Adapter +Module Type: Bidder Adapter +Maintainer: devops@kulture.media +``` + +# Description + +Module that connects to Kulture Media's demand sources. +Kulture Media bid adapter supports Banner and Video. + + +# Test Parameters + +## Banner + +``` +var adUnits = [ + { + code: 'banner-ad-div', + mediaTypes: { + banner: { + sizes: [[300, 250], [300,600]] + } + }, + bids: [{ + bidder: 'kulturemedia', + params: { + placementId: 'test', + publisherId: 'test', + } + }] + } +]; +``` + +## Video + +We support the following OpenRTB params that can be specified in `mediaTypes.video` or in `bids[].params.video` +- 'mimes', +- 'minduration', +- 'maxduration', +- 'placement', +- 'protocols', +- 'startdelay', +- 'skip', +- 'skipafter', +- 'minbitrate', +- 'maxbitrate', +- 'delivery', +- 'playbackmethod', +- 'api', +- 'linearity' + + +## Instream Video adUnit using mediaTypes.video +*Note:* By default, the adapter will read the mandatory parameters from mediaTypes.video. +*Note:* The Video SSP ad server will respond with an VAST XML to load into your defined player. +``` + var adUnits = [ + { + code: 'video1', + mediaTypes: { + video: { + context: 'instream', + playerSize: [640, 480], + mimes: ['video/mp4', 'application/javascript'], + protocols: [2,5], + api: [2], + position: 1, + delivery: [2], + minduration: 10, + maxduration: 30, + placement: 1, + playbackmethod: [1,5], + } + }, + bids: [ + { + bidder: 'kulturemedia', + params: { + bidfloor: 0.5, + publisherId: '12345', + placementId: '6789' + } + } + ] + } + ] +``` + +# End To End testing mode +By passing bid.params.e2etest = true you will be able to receive a test creative + +## Banner +``` +var adUnits = [ + { + code: 'banner-ad-div', + mediaTypes: { + banner: { + sizes: [[300, 250], [300,600]] + } + }, + bids: [{ + bidder: 'kulturemedia', + params: { + e2etest: true + } + }] + } +]; +``` + +## Video +``` +var adUnits = [ + { + code: 'video1', + mediaTypes: { + video: { + context: "instream", + playerSize: [[640, 480]], + mimes: ['video/mp4'], + protocols: [2,5], + } + }, + bids: [ + { + bidder: 'kulturemedia', + params: { + e2etest: true + } + } + ] + } +] +``` diff --git a/modules/lassoBidAdapter.js b/modules/lassoBidAdapter.js index fafaa4dd9dc..ad25cbe1e85 100644 --- a/modules/lassoBidAdapter.js +++ b/modules/lassoBidAdapter.js @@ -40,14 +40,14 @@ export const spec = { auctionId: bidRequest.auctionId, bidId: bidRequest.bidId, transactionId: bidRequest.transactionId, - device: JSON.stringify(getDeviceData()), + device: encodeURIComponent(JSON.stringify(getDeviceData())), sizes, aimXR, uid: '$UID', params: JSON.stringify(bidRequest.params), crumbs: JSON.stringify(bidRequest.crumbs), prebidVersion: '$prebid.version$', - version: 2, + version: 3, coppa: config.getConfig('coppa') == true ? 1 : 0, ccpa: bidderRequest.uspConsent || undefined } diff --git a/modules/lemmaDigitalBidAdapter.js b/modules/lemmaDigitalBidAdapter.js new file mode 100644 index 00000000000..9fa3081a47e --- /dev/null +++ b/modules/lemmaDigitalBidAdapter.js @@ -0,0 +1,570 @@ +import * as utils from '../src/utils.js'; +import { config } from '../src/config.js'; +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { BANNER, VIDEO } from '../src/mediaTypes.js'; + +var BIDDER_CODE = 'lemmadigital'; +var LOG_WARN_PREFIX = 'LEMMADIGITAL: '; +var ENDPOINT = 'https://bid.lemmadigital.com/lemma/servad'; +var USER_SYNC = 'https://sync.lemmadigital.com/js/usersync.html?'; +var DEFAULT_CURRENCY = 'USD'; +var AUCTION_TYPE = 2; +var DEFAULT_TMAX = 300; +var DEFAULT_NET_REVENUE = false; +var DEFAULT_SECURE = 1; +var RESPONSE_TTL = 300; +var pubId = 0; +var adunitId = 0; + +export var spec = { + + code: BIDDER_CODE, + supportedMediaTypes: [BANNER, VIDEO], + + /** + * Determines whether or not the given bid request is valid. + * + * @param {BidRequest} bid The bid params to validate. + * @return boolean True if this is a valid bid, and false otherwise. + **/ + isBidRequestValid: (bid) => { + if (!bid || !bid.params) { + utils.logError(LOG_WARN_PREFIX, 'nil/empty bid object'); + return false; + } + if (!utils.isEmpty(bid.params.pubId) || !utils.isNumber(bid.params.pubId)) { + utils.logWarn(LOG_WARN_PREFIX + 'Error: publisherId is mandatory and cannot be string. Call to OpenBid will not be sent for ad unit: ' + JSON.stringify(bid)); + return false; + } + if (!bid.params.adunitId) { + utils.logWarn(LOG_WARN_PREFIX + 'Error: adUnitId is mandatory. Call to OpenBid will not be sent for ad unit: ' + JSON.stringify(bid)); + return false; + } + // video bid request validation + if (bid.params.hasOwnProperty('video')) { + if (!bid.params.video.hasOwnProperty('mimes') || !utils.isArray(bid.params.video.mimes) || bid.params.video.mimes.length === 0) { + utils.logWarn(LOG_WARN_PREFIX + 'Error: For video ads, mimes is mandatory and must specify atlease 1 mime value. Call to OpenBid will not be sent for ad unit:' + JSON.stringify(bid)); + return false; + } + } + return true; + }, + + /** + * Make a server request from the list of BidRequests. + * + * @param {validBidRequests[]} - an array of bids + * @return ServerRequest Info describing the request to the server. + **/ + buildRequests: (validBidRequests, bidderRequest) => { + if (validBidRequests.length === 0) { + return; + } + var refererInfo; + if (bidderRequest && bidderRequest.refererInfo) { + refererInfo = bidderRequest.refererInfo; + } + var conf = spec._setRefURL(refererInfo); + const request = spec._createoRTBRequest(validBidRequests, conf); + if (request && request.imp.length == 0) { + return; + } + spec._setOtherParams(bidderRequest, request); + const endPoint = spec._endPointURL(validBidRequests); + return { + method: 'POST', + url: endPoint, + data: JSON.stringify(request), + }; + }, + + /** + * Unpack the response from the server into a list of bids. + * + * @param {ServerResponse} response A successful response from the server. + * @return {Bid[]} An array of bids which were nested inside the server. + **/ + interpretResponse: (response, request) => { + return spec._parseRTBResponse(request, response.body); + }, + + /** + * Register the user sync pixels which should be dropped after the auction. + * @param {SyncOptions} syncOptions Which user syncs are allowed? + * @param {ServerResponse[]} serverResponses List of server's responses. + * @return {UserSync[]} The user syncs which should be dropped. + **/ + getUserSyncs: (syncOptions, serverResponses) => { + let syncurl = USER_SYNC + 'pid=' + pubId; + if (syncOptions.iframeEnabled) { + return [{ + type: 'iframe', + url: syncurl + }]; + } else { + utils.logWarn(LOG_WARN_PREFIX + 'Please enable iframe based user sync.'); + } + }, + + /** + * Generate UUID + */ + _createUUID: () => { + return new Date().getTime().toString(); + }, + + /** + * parse object + **/ + _parseJSON: function (rawPayload) { + try { + if (rawPayload) { + return JSON.parse(rawPayload); + } + } catch (ex) { + utils.logError(LOG_WARN_PREFIX, 'Exception: ', ex); + } + return null; + }, + + /** + * + * set referal url + */ + _setRefURL: (refererInfo) => { + var conf = {}; + conf.pageURL = (refererInfo && refererInfo.referer) ? refererInfo.referer : window.location.href; + if (refererInfo && refererInfo.referer) { + conf.refURL = refererInfo.referer; + } else { + conf.refURL = ''; + } + return conf; + }, + + /** + * set other params into oRTB request + */ + _setOtherParams: (request, ortbRequest) => { + var params = request && request.params ? request.params : null; + if (params) { + ortbRequest.tmax = params.tmax; + ortbRequest.bcat = params.bcat; + } + }, + + /** + * create IAB standard OpenRTB bid request + **/ + _createoRTBRequest: (bidRequests, conf) => { + var oRTBObject = {}; + try { + oRTBObject = { + id: spec._createUUID(), + at: AUCTION_TYPE, + tmax: DEFAULT_TMAX, + cur: [DEFAULT_CURRENCY], + imp: spec._getImpressionArray(bidRequests), + user: {}, + ext: {} + }; + var bid = bidRequests[0]; + + var site = spec._getSiteObject(bid, conf); + if (site) { + oRTBObject.site = site; + // add the content object from config in request + if (typeof config.getConfig('content') === 'object') { + oRTBObject.site.content = config.getConfig('content'); + } + } + var app = spec._getAppObject(bid); + if (app) { + oRTBObject.app = app; + if (typeof oRTBObject.app.content !== 'object' && typeof config.getConfig('content') === 'object') { + oRTBObject.app.content = + config.getConfig('content') || undefined; + } + } + var device = spec._getDeviceObject(bid); + if (device) { + oRTBObject.device = device; + } + var source = spec._getSourceObject(bid); + if (source) { + oRTBObject.source = source; + } + return oRTBObject; + } catch (ex) { + utils.logError(LOG_WARN_PREFIX, 'ERROR ', ex); + } + }, + + /** + * create impression array objects + **/ + _getImpressionArray: (request) => { + var impArray = []; + var map = request.map(bid => spec._getImpressionObject(bid)); + if (map) { + map.forEach(o => { + if (o) { + impArray.push(o); + } + }); + } + return impArray; + }, + + /** + * create impression (single) object + **/ + _getImpressionObject: (bid) => { + var impression = {}; + var bObj; + var vObj; + var sizes = bid.hasOwnProperty('sizes') ? bid.sizes : []; + var mediaTypes = ''; + var format = []; + var params = bid && bid.params ? bid.params : null; + impression = { + id: bid.bidId, + tagid: params.adunitId ? params.adunitId.toString() : undefined, + secure: DEFAULT_SECURE, + bidfloorcur: params.currency ? params.currency : DEFAULT_CURRENCY + }; + if (params.bidFloor) { + impression.bidfloor = params.bidFloor; + } + if (bid.hasOwnProperty('mediaTypes')) { + for (mediaTypes in bid.mediaTypes) { + switch (mediaTypes) { + case BANNER: + bObj = spec._getBannerRequest(bid); + if (bObj) { + impression.banner = bObj; + } + break; + case VIDEO: + vObj = spec._getVideoRequest(bid); + if (vObj) { + impression.video = vObj; + } + break; + } + } + } else { + bObj = { + pos: 0, + w: sizes && sizes[0] ? sizes[0][0] : 0, + h: sizes && sizes[0] ? sizes[0][1] : 0, + }; + if (utils.isArray(sizes) && sizes.length > 1) { + sizes = sizes.splice(1, sizes.length - 1); + sizes.forEach(size => { + format.push({ + w: size[0], + h: size[1] + }); + }); + bObj.format = format; + } + impression.banner = bObj; + } + spec._setFloor(impression, bid); + return impression.hasOwnProperty(BANNER) || + impression.hasOwnProperty(VIDEO) ? impression : undefined; + }, + + /** + * set bid floor + **/ + _setFloor: (impObj, bid) => { + let bidFloor = -1; + // get lowest floor from floorModule + if (typeof bid.getFloor === 'function') { + [BANNER, VIDEO].forEach(mediaType => { + if (impObj.hasOwnProperty(mediaType)) { + let floorInfo = bid.getFloor({ currency: impObj.bidfloorcur, mediaType: mediaType, size: '*' }); + if (typeof floorInfo === 'object' && floorInfo.currency === impObj.bidfloorcur && !isNaN(parseInt(floorInfo.floor))) { + let mediaTypeFloor = parseFloat(floorInfo.floor); + bidFloor = (bidFloor == -1 ? mediaTypeFloor : Math.min(mediaTypeFloor, bidFloor)); + } + } + }); + } + // get highest from impObj.bidfllor and floor from floor module + // as we are using Math.max, it is ok if we have not got any floor from floorModule, then value of bidFloor will be -1 + if (impObj.bidfloor) { + bidFloor = Math.max(bidFloor, impObj.bidfloor); + } + + // assign value only if bidFloor is > 0 + impObj.bidfloor = ((!isNaN(bidFloor) && bidFloor > 0) ? bidFloor : undefined); + }, + + /** + * parse Open RTB response + **/ + _parseRTBResponse: (request, response) => { + var bidResponses = []; + try { + if (response.seatbid) { + var currency = response.curr || DEFAULT_CURRENCY; + var seatbid = response.seatbid; + seatbid.forEach(seatbidder => { + var bidder = seatbidder.bid; + bidder.forEach(bid => { + var req = spec._parseJSON(request.data); + var newBid = { + requestId: bid.impid, + cpm: parseFloat(bid.price).toFixed(2), + width: bid.w, + height: bid.h, + creativeId: bid.crid, + currency: currency, + netRevenue: DEFAULT_NET_REVENUE, + ttl: RESPONSE_TTL, + referrer: req.site.ref, + ad: bid.adm + }; + if (bid.dealid) { + newBid.dealId = bid.dealid; + } + if (req.imp && req.imp.length > 0) { + req.imp.forEach(robj => { + if (bid.impid === robj.id) { + spec._checkMediaType(bid.adm, newBid); + switch (newBid.mediaType) { + case BANNER: + break; + case VIDEO: + newBid.width = bid.hasOwnProperty('w') ? bid.w : robj.video.w; + newBid.height = bid.hasOwnProperty('h') ? bid.h : robj.video.h; + newBid.vastXml = bid.adm; + break; + } + } + }); + } + bidResponses.push(newBid); + }); + }); + } + } catch (error) { + utils.logError(LOG_WARN_PREFIX, 'ERROR ', error); + } + return bidResponses; + }, + + /** + * get bid request api end point url + **/ + _endPointURL: (request) => { + var params = request && request[0].params ? request[0].params : null; + if (params) { + pubId = params.pubId ? params.pubId : 0; + adunitId = params.adunitId ? params.adunitId : 0; + return ENDPOINT + '?pid=' + pubId + '&aid=' + adunitId; + } + return null; + }, + + /** + * get domain name from url + **/ + _getDomain: (url) => { + var a = document.createElement('a'); + a.setAttribute('href', url); + return a.hostname; + }, + + /** + * create the site object + **/ + _getSiteObject: (request, conf) => { + var params = request && request.params ? request.params : null; + if (params) { + pubId = params.pubId ? params.pubId : '0'; + var siteId = params.siteId ? params.siteId : '0'; + var appParams = params.app; + if (!appParams) { + return { + publisher: { + id: pubId.toString() + }, + domain: spec._getDomain(conf.pageURL), + id: siteId.toString(), + ref: conf.refURL, + page: conf.pageURL, + cat: params.category, + pagecat: params.page_category + }; + } + } + return null; + }, + + /** + * create the app object + **/ + _getAppObject: (request) => { + var params = request && request.params ? request.params : null; + if (params) { + pubId = params.pubId ? params.pubId : 0; + var appParams = params.app; + if (appParams) { + return { + publisher: { + id: pubId.toString(), + }, + id: appParams.id, + name: appParams.name, + bundle: appParams.bundle, + storeurl: appParams.storeUrl, + domain: appParams.domain, + cat: appParams.cat || params.category, + pagecat: appParams.pagecat || params.page_category + }; + } + } + return null; + }, + + /** + * create the device object + **/ + _getDeviceObject: (request) => { + var params = request && request.params ? request.params : null; + if (params) { + return { + dnt: utils.getDNT() ? 1 : 0, + ua: navigator.userAgent, + language: (navigator.language || navigator.browserLanguage || navigator.userLanguage || navigator.systemLanguage), + w: (window.screen.width || window.innerWidth), + h: (window.screen.height || window.innerHeigh), + geo: { + country: params.country, + lat: params.latitude, + lon: params.longitude, + accuracy: params.accuracy, + region: params.region, + city: params.city, + zip: params.zip + }, + ip: params.ip, + make: params.make, + model: params.model, + os: params.os, + carrier: params.carrier, + devicetype: params.device_type, + ifa: params.ifa, + }; + } + return null; + }, + + /** + * create source object + */ + _getSourceObject: (request) => { + var params = request && request.params ? request.params : null; + if (params) { + return { + pchain: params.pchain, + ext: { + schain: request.schain + }, + }; + } + return null; + }, + + /** + * get request ad sizes + **/ + _getSizes: (request) => { + if (request && request.sizes && utils.isArray(request.sizes[0]) && request.sizes[0].length > 0) { + return request.sizes[0]; + } + return null; + }, + + /** + * create the banner object + **/ + _getBannerRequest: (bid) => { + var bObj; + var adFormat = []; + if (utils.deepAccess(bid, 'mediaTypes.banner')) { + var params = bid ? bid.params : null; + var bannerData = params && params.banner; + var sizes = spec._getSizes(bid) || []; + if (sizes && sizes.length == 0) { + sizes = bid.mediaTypes.banner.sizes[0]; + } + if (sizes && sizes.length > 0) { + bObj = {}; + bObj.w = sizes[0]; + bObj.h = sizes[1]; + bObj.pos = 0; + if (bannerData) { + bObj = utils.deepClone(bannerData); + } + sizes = bid.mediaTypes.banner.sizes; + if (sizes.length > 0) { + adFormat = []; + sizes.forEach(function (size) { + if (size.length > 1) { + adFormat.push({ w: size[0], h: size[1] }); + } + }); + if (adFormat.length > 0) { + bObj.format = adFormat; + } + } + } else { + utils.logWarn(LOG_WARN_PREFIX + 'Error: mediaTypes.banner.sizes missing for adunit: ' + bid.params.adunitId); + } + } + return bObj; + }, + + /** + * create the video object + **/ + _getVideoRequest: (bid) => { + var vObj; + if (utils.deepAccess(bid, 'mediaTypes.video')) { + var params = bid ? bid.params : null; + var videoData = utils.mergeDeep(utils.deepAccess(bid.mediaTypes, 'video'), params.video); + var sizes = bid.mediaTypes.video ? bid.mediaTypes.video.playerSize : [] + if (sizes && sizes.length > 0) { + vObj = {}; + if (videoData) { + vObj = utils.deepClone(videoData); + } + vObj.w = sizes[0]; + vObj.h = sizes[1]; + } else { + utils.logWarn(LOG_WARN_PREFIX + 'Error: mediaTypes.video.sizes missing for adunit: ' + bid.params.adunitId); + } + } + return vObj; + }, + + /** + * check media type + **/ + _checkMediaType: (adm, newBid) => { + // Create a regex here to check the strings + var videoRegex = new RegExp(/VAST.*version/); + if (videoRegex.test(adm)) { + newBid.mediaType = VIDEO; + } else { + newBid.mediaType = BANNER; + } + } +}; + +registerBidder(spec); diff --git a/modules/lemmaDigitalBidAdapter.md b/modules/lemmaDigitalBidAdapter.md new file mode 100644 index 00000000000..5a22a7588da --- /dev/null +++ b/modules/lemmaDigitalBidAdapter.md @@ -0,0 +1,61 @@ +# Overview + +``` +Module Name: Lemmadigital Bid Adapter +Module Type: Bidder Adapter +Maintainer: lemmadev@lemmatechnologies.com +``` + +# Description + +Connects to Lemma exchange for bids. +Lemmadigital bid adapter supports Video, Banner formats. + +# Sample Banner Ad Unit: For Publishers +``` +var adUnits = [{ + code: 'div-lemma-ad-1', + mediaTypes: { + banner: { + sizes: [[300, 250], [300, 600]], // required + } + }, + // Replace this object to test a new Adapter! + bids: [{ + bidder: 'lemmadigital', + params: { + pubId: 1, // required + adunitId: '3768', // required + latitude: 37.3230, + longitude: -122.0322, + device_type: 2 + } + }] +}]; +``` + +# Sample Video Ad Unit: For Publishers +``` +var adUnits = [{ + mediaTypes: { + video: { + playerSize: [640, 480], // required + context: 'instream' + } + }, + // Replace this object to test a new Adapter! + bids: [{ + bidder: 'lemmadigital', + params: { + pubId: 1, // required + adunitId: '3769', // required + latitude: 37.3230, + longitude: -122.0322, + device_type: 4, + video: { + mimes: ['video/mp4','video/x-flv'], // required + } + } + }] +}]; +``` diff --git a/modules/limelightDigitalBidAdapter.js b/modules/limelightDigitalBidAdapter.js index 87cf6e4fe21..601d78578b3 100644 --- a/modules/limelightDigitalBidAdapter.js +++ b/modules/limelightDigitalBidAdapter.js @@ -158,7 +158,12 @@ function buildPlacement(bidRequest) { type: bidRequest.params.adUnitType.toUpperCase(), publisherId: bidRequest.params.publisherId, userIdAsEids: bidRequest.userIdAsEids, - supplyChain: bidRequest.schain + supplyChain: bidRequest.schain, + custom1: bidRequest.params.custom1, + custom2: bidRequest.params.custom2, + custom3: bidRequest.params.custom3, + custom4: bidRequest.params.custom4, + custom5: bidRequest.params.custom5 } } } diff --git a/modules/limelightDigitalBidAdapter.md b/modules/limelightDigitalBidAdapter.md index a4abb6f1411..2c773859a7f 100644 --- a/modules/limelightDigitalBidAdapter.md +++ b/modules/limelightDigitalBidAdapter.md @@ -24,7 +24,12 @@ var adUnits = [{ params: { host: 'exchange-9qao.ortb.net', adUnitId: 0, - adUnitType: 'banner' + adUnitType: 'banner', + custom1: 'custom1', + custom2: 'custom2', + custom3: 'custom3', + custom4: 'custom4', + custom5: 'custom5' } }] }]; @@ -40,7 +45,12 @@ var videoAdUnit = [{ params: { host: 'exchange-9qao.ortb.net', adUnitId: 0, - adUnitType: 'video' + adUnitType: 'video', + custom1: 'custom1', + custom2: 'custom2', + custom3: 'custom3', + custom4: 'custom4', + custom5: 'custom5' } }] }]; diff --git a/modules/liveIntentIdSystem.js b/modules/liveIntentIdSystem.js index de70b0eaccd..3ecd061085c 100644 --- a/modules/liveIntentIdSystem.js +++ b/modules/liveIntentIdSystem.js @@ -7,12 +7,14 @@ import { triggerPixel, logError } from '../src/utils.js'; import { ajaxBuilder } from '../src/ajax.js'; import { submodule } from '../src/hook.js'; -import { LiveConnect } from 'live-connect-js/esm/initializer.js'; +import { LiveConnect } from 'live-connect-js'; // eslint-disable-line prebid/validate-imports import { gdprDataHandler, uspDataHandler } from '../src/adapterManager.js'; -import { getStorageManager } from '../src/storageManager.js'; +import {getStorageManager} from '../src/storageManager.js'; +import {MODULE_TYPE_UID} from '../src/activities/modules.js'; +const EVENTS_TOPIC = 'pre_lips' const MODULE_NAME = 'liveIntentId'; -export const storage = getStorageManager({gvlid: null, moduleName: MODULE_NAME}); +export const storage = getStorageManager({moduleType: MODULE_TYPE_UID, moduleName: MODULE_NAME}); const defaultRequestedAttributes = {'nonId': true} const calls = { ajaxGet: (url, onSuccess, onError, timeout) => { @@ -39,14 +41,22 @@ let liveConnect = null; * This function is used in tests */ export function reset() { - if (window && window.liQ) { - window.liQ = []; + if (window && window.liQ_instances) { + window.liQ_instances.forEach(i => i.eventBus.off(EVENTS_TOPIC, setEventFiredFlag)) + window.liQ_instances = []; } liveIntentIdSubmodule.setModuleMode(null) eventFired = false; liveConnect = null; } +/** + * This function is also used in tests + */ +export function setEventFiredFlag() { + eventFired = true; +} + function parseLiveIntentCollectorConfig(collectConfig) { const config = {}; collectConfig = collectConfig || {} @@ -100,6 +110,7 @@ function initializeLiveConnect(configParams) { liveConnectConfig.wrapperName = 'prebid'; liveConnectConfig.identityResolutionConfig = identityResolutionConfig; liveConnectConfig.identifiersToResolve = configParams.identifiersToResolve || []; + liveConnectConfig.fireEventDelay = configParams.fireEventDelay; const usPrivacyString = uspDataHandler.getConsentData(); if (usPrivacyString) { liveConnectConfig.usPrivacyString = usPrivacyString; @@ -121,8 +132,14 @@ function initializeLiveConnect(configParams) { function tryFireEvent() { if (!eventFired && liveConnect) { - liveConnect.fire(); - eventFired = true; + const eventDelay = liveConnect.config.fireEventDelay || 500 + setTimeout(() => { + const instances = window.liQ_instances + instances.forEach(i => i.eventBus.once(EVENTS_TOPIC, setEventFiredFlag)) + if (!eventFired && liveConnect) { + liveConnect.fire(); + } + }, eventDelay) } } @@ -171,6 +188,14 @@ export const liveIntentIdSubmodule = { result.uid2 = { 'id': value.uid2 } } + if (value.bidswitch) { + result.bidswitch = { 'id': value.bidswitch } + } + + if (value.medianet) { + result.medianet = { 'id': value.medianet } + } + return result } diff --git a/modules/livewrappedBidAdapter.js b/modules/livewrappedBidAdapter.js index 3839eb80b91..af754ae3ff0 100644 --- a/modules/livewrappedBidAdapter.js +++ b/modules/livewrappedBidAdapter.js @@ -1,4 +1,4 @@ -import {deepAccess, getWindowTop, isSafariBrowser, mergeDeep} from '../src/utils.js'; +import {deepAccess, getWindowTop, isSafariBrowser, mergeDeep, isFn, isPlainObject} from '../src/utils.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import {config} from '../src/config.js'; import {find} from '../src/polyfill.js'; @@ -68,7 +68,9 @@ export const spec = { bidUrl = bidUrl ? bidUrl.params.bidUrl : URL; url = url ? url.params.url : (getAppDomain() || getTopWindowLocation(bidderRequest)); test = test ? test.params.test : undefined; - var adRequests = bidRequests.map(bidToAdRequest); + const currency = config.getConfig('currency.adServerCurrency') || 'USD'; + var adRequests = bidRequests.map(b => bidToAdRequest(b, currency)); + const adRequestsContainFloors = adRequests.some(r => r.flr !== undefined); if (eids) { ortb2 = mergeDeep(mergeDeep({}, ortb2 || {}), eids); @@ -96,7 +98,8 @@ export const spec = { rcv: getAdblockerRecovered(), adRequests: [...adRequests], rtbData: ortb2, - schain: schain + schain: schain, + flrCur: adRequestsContainFloors ? currency : undefined }; if (config.getConfig().debug) { @@ -223,13 +226,14 @@ function hasPubcid(bid) { return !!bid.crumbs && !!bid.crumbs.pubcid; } -function bidToAdRequest(bid) { +function bidToAdRequest(bid, currency) { var adRequest = { adUnitId: bid.params.adUnitId, callerAdUnitId: bid.params.adUnitName || bid.adUnitCode || bid.placementCode, bidId: bid.bidId, transactionId: bid.transactionId, formats: getSizes(bid).map(sizeToFormat), + flr: getBidFloor(bid, currency), options: bid.params.options }; @@ -264,6 +268,22 @@ function sizeToFormat(size) { } } +function getBidFloor(bid, currency) { + if (!isFn(bid.getFloor)) { + return undefined; + } + + const floor = bid.getFloor({ + currency: currency, + mediaType: '*', + size: '*' + }); + + return isPlainObject(floor) && !isNaN(floor.floor) && floor.currency == currency + ? floor.floor + : undefined; +} + function getAdblockerRecovered() { try { return getWindowTop().I12C && getWindowTop().I12C.Morph === 1; @@ -302,21 +322,13 @@ function getDeviceIfa() { } function getDeviceWidth() { - let device = config.getConfig('device'); - if (typeof device === 'object' && device.width) { - return device.width; - } - - return window.innerWidth; + const device = config.getConfig('device') || {}; + return device.w || window.innerWidth; } function getDeviceHeight() { - let device = config.getConfig('device'); - if (typeof device === 'object' && device.height) { - return device.height; - } - - return window.innerHeight; + const device = config.getConfig('device') || {}; + return device.h || window.innerHeight; } function getCoppa() { diff --git a/modules/lotamePanoramaIdSystem.js b/modules/lotamePanoramaIdSystem.js index 832d98e4f83..02b01b8bd9d 100644 --- a/modules/lotamePanoramaIdSystem.js +++ b/modules/lotamePanoramaIdSystem.js @@ -16,8 +16,9 @@ import { } from '../src/utils.js'; import { ajax } from '../src/ajax.js'; import { submodule } from '../src/hook.js'; -import { getStorageManager } from '../src/storageManager.js'; +import {getStorageManager} from '../src/storageManager.js'; import { uspDataHandler } from '../src/adapterManager.js'; +import {MODULE_TYPE_UID} from '../src/activities/modules.js'; const KEY_ID = 'panoramaId'; const KEY_EXPIRY = `${KEY_ID}_expiry`; @@ -29,8 +30,9 @@ const DAY_MS = 60 * 60 * 24 * 1000; const MISSING_CORE_CONSENT = 111; const GVLID = 95; const ID_HOST = 'id.crwdcntrl.net'; +const ID_HOST_COOKIELESS = 'c.ltmsphrcl.net'; -export const storage = getStorageManager({gvlid: GVLID, moduleName: MODULE_NAME}); +export const storage = getStorageManager({moduleType: MODULE_TYPE_UID, moduleName: MODULE_NAME}); let cookieDomain; /** @@ -252,6 +254,13 @@ export const lotamePanoramaIdSubmodule = { usPrivacy = getFromStorage('us_privacy'); } + const getRequestHost = function() { + if (navigator.userAgent && navigator.userAgent.indexOf('Safari') != -1 && navigator.userAgent.indexOf('Chrome') == -1) { + return ID_HOST_COOKIELESS; + } + return ID_HOST; + } + const resolveIdFunction = function (callback) { let queryParams = {}; if (storedUserId) { @@ -288,7 +297,7 @@ export const lotamePanoramaIdSubmodule = { const url = buildUrl({ protocol: 'https', - host: ID_HOST, + host: getRequestHost(), pathname: '/id', search: isEmpty(queryParams) ? undefined : queryParams, }); diff --git a/modules/luponmediaBidAdapter.js b/modules/luponmediaBidAdapter.js index 835c04ba074..059a07b9999 100755 --- a/modules/luponmediaBidAdapter.js +++ b/modules/luponmediaBidAdapter.js @@ -292,7 +292,7 @@ function newOrtbBidRequest(bidRequest, bidderRequest, currentImps) { source: { tid: bidRequest.transactionId }, - tmax: config.getConfig('timeout') || 1500, + tmax: bidderRequest.timeout, imp: currentImps.concat([{ id: bidRequest.bidId, secure: 1, diff --git a/modules/mabidderBidAdapter.js b/modules/mabidderBidAdapter.js index 12da791d2c4..632403c6643 100644 --- a/modules/mabidderBidAdapter.js +++ b/modules/mabidderBidAdapter.js @@ -1,5 +1,7 @@ import { registerBidder } from '../src/adapters/bidderFactory.js'; import { BANNER } from '../src/mediaTypes.js'; +import {getGlobal} from '../src/prebidGlobal.js'; + const BIDDER_CODE = 'mabidder'; export const baseUrl = 'https://prebid.ecdrsvc.com/bid'; export const spec = { @@ -33,7 +35,7 @@ export const spec = { url: baseUrl, method: 'POST', data: { - v: $$PREBID_GLOBAL$$.version, + v: getGlobal().version, bids: bids, url: bidderRequest.refererInfo.page || '', referer: bidderRequest.refererInfo.ref || '', diff --git a/modules/magniteAnalyticsAdapter.js b/modules/magniteAnalyticsAdapter.js index 99f384e9eff..7cede6af38d 100644 --- a/modules/magniteAnalyticsAdapter.js +++ b/modules/magniteAnalyticsAdapter.js @@ -1,14 +1,33 @@ -import { generateUUID, mergeDeep, deepAccess, parseUrl, logError, pick, isEmpty, logWarn, debugTurnedOn, parseQS, getWindowLocation, isAdUnitCodeMatchingSlot, isNumber, deepSetValue, deepClone, logInfo, isGptPubadsDefined } from '../src/utils.js'; +import { + debugTurnedOn, + deepAccess, + deepClone, + deepSetValue, + generateUUID, + getWindowLocation, + isAdUnitCodeMatchingSlot, + isEmpty, + isGptPubadsDefined, + isNumber, + logError, + logInfo, + logWarn, + mergeDeep, + parseQS, + parseUrl, + pick +} from '../src/utils.js'; import adapter from '../libraries/analyticsAdapter/AnalyticsAdapter.js'; import adapterManager from '../src/adapterManager.js'; import CONSTANTS from '../src/constants.json'; -import { ajax } from '../src/ajax.js'; -import { config } from '../src/config.js'; -import { getGlobal } from '../src/prebidGlobal.js'; -import { getStorageManager } from '../src/storageManager.js'; +import {ajax} from '../src/ajax.js'; +import {config} from '../src/config.js'; +import {getGlobal} from '../src/prebidGlobal.js'; +import {getStorageManager} from '../src/storageManager.js'; +import {MODULE_TYPE_ANALYTICS} from '../src/activities/modules.js'; const RUBICON_GVL_ID = 52; -export const storage = getStorageManager({ gvlid: RUBICON_GVL_ID, moduleName: 'magnite' }); +export const storage = getStorageManager({ moduleType: MODULE_TYPE_ANALYTICS, moduleName: 'magnite' }); const COOKIE_NAME = 'mgniSession'; const LAST_SEEN_EXPIRE_TIME = 1800000; // 30 mins const END_EXPIRE_TIME = 21600000; // 6 hours @@ -37,7 +56,8 @@ const { BIDDER_DONE, BID_TIMEOUT, BID_WON, - BILLABLE_EVENT + BILLABLE_EVENT, + SEAT_NON_BID }, STATUS: { GOOD, @@ -305,6 +325,10 @@ const getTopLevelDetails = () => { } } + if (browser) { + deepSetValue(payload, rubiConf.pbaBrowserLocation || 'client.browser', browser); + } + // Add DM wrapper details if (rubiConf.wrapperName) { payload.wrapper = { @@ -460,7 +484,7 @@ const findMatchingAdUnitFromAuctions = (matchesFunction, returnFirstMatch) => { } } return matches; -} +}; const getRenderingIds = bidWonData => { // if bid caching off -> return the bidWon auction id @@ -595,6 +619,28 @@ const subscribeToGamSlots = () => { }); } +/** + * Lazy parsing of UA to determine browser + * @param {string} userAgent string from prebid ortb ua or navigator + * @returns {string} lazily guessed browser name + */ +export const detectBrowserFromUa = userAgent => { + let normalizedUa = userAgent.toLowerCase(); + + if (normalizedUa.includes('edg')) { + return 'Edge'; + } else if ((/opr|opera|opt/i).test(normalizedUa)) { + return 'Opera'; + } else if ((/chrome|crios/i).test(normalizedUa)) { + return 'Chrome'; + } else if ((/fxios|firefox/i).test(normalizedUa)) { + return 'Firefox'; + } else if (normalizedUa.includes('safari') && !(/chromium|ucbrowser/i).test(normalizedUa)) { + return 'Safari'; + } + return 'OTHER'; +} + let accountId; let endpoint; @@ -643,11 +689,20 @@ magniteAdapter.disableAnalytics = function () { accountId = undefined; resetConfs(); magniteAdapter.originDisableAnalytics(); -} +}; + +magniteAdapter.onDataDeletionRequest = function () { + if (storage.localStorageIsEnabled()) { + storage.removeDataFromLocalStorage(COOKIE_NAME); + } else { + throw Error('Unable to access local storage, no data deleted'); + } +}; magniteAdapter.MODULE_INITIALIZED_TIME = Date.now(); magniteAdapter.referrerHostname = ''; +let browser; magniteAdapter.track = ({ eventType, args }) => { switch (eventType) { case AUCTION_INIT: @@ -667,6 +722,12 @@ magniteAdapter.track = ({ eventType, args }) => { ]); auctionData.accountId = accountId; + // get browser + if (!browser) { + const userAgent = deepAccess(args, 'bidderRequests.0.ortb2.device.ua', navigator.userAgent) || ''; + browser = detectBrowserFromUa(userAgent); + } + // Order bidders were called auctionData.bidderOrder = args.bidderRequests.map(bidderRequest => bidderRequest.bidderCode); @@ -780,7 +841,16 @@ magniteAdapter.track = ({ eventType, args }) => { auctionEntry.floors.dealsEnforced = args.floorData.enforcements.floorDeals; } - // Log error if no matching bid! + // no-bid from server. report it! + if (!bid && args.seatBidId) { + bid = adUnit.bids[args.seatBidId] = { + bidder: args.bidderCode, + source: 'server', + bidId: args.seatBidId, + unknownBid: true + }; + } + if (!bid) { logError(`${MODULE_NAME}: Could not find associated bid request for bid response with requestId: `, args.requestId); break; @@ -811,6 +881,9 @@ magniteAdapter.track = ({ eventType, args }) => { bid.pbsBidId = pbsBidId; } break; + case SEAT_NON_BID: + handleNonBidEvent(args); + break; case BIDDER_DONE: const serverError = deepAccess(args, 'serverErrors.0'); const serverResponseTimeMs = args.serverResponseTimeMs; @@ -898,6 +971,66 @@ magniteAdapter.track = ({ eventType, args }) => { } }; +const handleNonBidEvent = function(args) { + const {seatnonbid, auctionId} = args; + const auction = deepAccess(cache, `auctions.${auctionId}.auction`); + // if no auction just bail + if (!auction) { + logWarn(`Unable to match nonbid to auction`); + return; + } + const adUnits = auction.adUnits; + seatnonbid.forEach(seatnonbid => { + let {seat} = seatnonbid; + seatnonbid.nonbid.forEach(nonbid => { + try { + const {status, impid} = nonbid; + const matchingTid = Object.keys(adUnits).find(tid => adUnits[tid].adUnitCode === impid); + const adUnit = adUnits[matchingTid]; + const statusInfo = statusMap[status] || { status: 'no-bid' }; + adUnit.bids[generateUUID()] = { + bidder: seat, + source: 'server', + isSeatNonBid: true, + clientLatencyMillis: Date.now() - auction.auctionStart, + ...statusInfo + }; + } catch (error) { + logWarn(`Unable to match nonbid to adUnit`); + } + }); + }); +}; + +const statusMap = { + 0: { + status: 'no-bid' + }, + 100: { + status: 'error', + error: { + code: 'request-error', + description: 'general error' + } + }, + 101: { + status: 'error', + error: { + code: 'timeout-error', + description: 'prebid server timeout' + } + }, + 200: { + status: 'rejected' + }, + 202: { + status: 'rejected' + }, + 301: { + status: 'rejected-ipf' + } +}; + adapterManager.registerAnalyticsAdapter({ adapter: magniteAdapter, code: 'magnite', diff --git a/modules/marsmediaAnalyticsAdapter.js b/modules/marsmediaAnalyticsAdapter.js index c86cc4dfbc2..f1e53a3c20c 100644 --- a/modules/marsmediaAnalyticsAdapter.js +++ b/modules/marsmediaAnalyticsAdapter.js @@ -1,6 +1,7 @@ import {ajax} from '../src/ajax.js'; import adapter from '../libraries/analyticsAdapter/AnalyticsAdapter.js'; import adapterManager from '../src/adapterManager.js'; +import {getGlobal} from '../src/prebidGlobal.js'; /**** * Mars Media Analytics @@ -33,7 +34,7 @@ var marsmediaAnalyticsAdapter = Object.assign(adapter( success: function() {}, error: function() {} }, - JSON.stringify({act: 'prebid_analytics', params: events, 'pbjs': $$PREBID_GLOBAL$$.getBidResponses(), ver: MARS_VERSION}), + JSON.stringify({act: 'prebid_analytics', params: events, 'pbjs': getGlobal().getBidResponses(), ver: MARS_VERSION}), { method: 'POST' } diff --git a/modules/mathildeadsBidAdapter.js b/modules/mathildeadsBidAdapter.js index 6b886b72955..929cee8f3c0 100644 --- a/modules/mathildeadsBidAdapter.js +++ b/modules/mathildeadsBidAdapter.js @@ -155,7 +155,7 @@ export const spec = { coppa: config.getConfig('coppa') === true ? 1 : 0, ccpa: bidderRequest.uspConsent || undefined, gdpr: bidderRequest.gdprConsent || undefined, - tmax: config.getConfig('bidderTimeout') + tmax: bidderRequest.timeout }; const len = validBidRequests.length; diff --git a/modules/mediafuseBidAdapter.js b/modules/mediafuseBidAdapter.js index e61c2e65c39..87347ca8d27 100644 --- a/modules/mediafuseBidAdapter.js +++ b/modules/mediafuseBidAdapter.js @@ -62,7 +62,7 @@ const SCRIPT_TAG_START = '> // a random number from -// (a / 4)) -// ) // 8 to 11 -// .toString(16) // in hexadecimal -// : ( // or otherwise a concatenated string: -// [1e7] + // 10000000 + -// 1e3 + // -1000 + -// 4e3 + // -4000 + -// 8e3 + // -80000000 + -// 1e11 -// ) // -100000000000, -// .replace( -// // replacing -// /[018]/g, // zeroes, ones, and eights with -// getRandomId // random hex digits -// ); -// } - /* ----- mguid:start ------ */ const COOKIE_KEY_MGUID = '__mguid_'; @@ -148,12 +116,12 @@ function isMobileAndTablet() { */ // function getBidFloor(bid, mediaType, sizes) { // var floor; -// var size = sizes.length === 1 ? sizes[0] : "*"; -// if (typeof bid.getFloor === "function") { -// const floorInfo = bid.getFloor({ currency: "USD", mediaType, size }); +// var size = sizes.length === 1 ? sizes[0] : '*'; +// if (typeof bid.getFloor === 'function') { +// const floorInfo = bid.getFloor({ currency: 'USD', mediaType, size }); // if ( -// typeof floorInfo === "object" && -// floorInfo.currency === "USD" && +// typeof floorInfo === 'object' && +// floorInfo.currency === 'USD' && // !isNaN(parseFloat(floorInfo.floor)) // ) { // floor = parseFloat(floorInfo.floor); @@ -247,7 +215,9 @@ function getItems(validBidRequests, bidderRequest) { } } if (!matchSize) { - return {}; + matchSize = sizes[0] + ? { h: sizes[0].height || 0, w: sizes[0].width || 0 } + : { h: 0, w: 0 }; } const bidFloor = getBidFloor(req); @@ -255,7 +225,6 @@ function getItems(validBidRequests, bidderRequest) { // utils.deepAccess(req, 'ortb2Imp.ext.gpid') || // utils.deepAccess(req, 'ortb2Imp.ext.data.pbadslot') || // utils.deepAccess(req, 'params.placementId', 0); - // console.log("wjh getItems:", req, bidFloor, gpid); // if (mediaTypes.native) {} // banner广告类型 @@ -268,9 +237,10 @@ function getItems(validBidRequests, bidderRequest) { h: matchSize.h, w: matchSize.w, pos: 1, + format: sizes, }, ext: { - // gpid: gpid, // 加入后无法返回广告 + // gpid: gpid, // 加入后无法返回广告 }, }; itemMaps[id] = { @@ -296,6 +266,8 @@ function getParam(validBidRequests, bidderRequest) { const sharedid = utils.deepAccess(validBidRequests[0], 'userId.sharedid.id') || utils.deepAccess(validBidRequests[0], 'userId.pubcid'); + const eids = validBidRequests[0].userIdAsEids || validBidRequests[0].userId; + let isMobile = isMobileAndTablet() ? 1 : 0; // input test status by Publisher. more frequently for test true req let isTest = validBidRequests[0].params.test || 0; @@ -309,6 +281,7 @@ function getParam(validBidRequests, bidderRequest) { const referer = utils.deepAccess(bidderRequest, 'refererInfo.ref'); const timeout = bidderRequest.timeout || 2000; + const firstPartyData = bidderRequest.ortb2; if (items && items.length) { let c = { @@ -318,19 +291,24 @@ function getParam(validBidRequests, bidderRequest) { cur: ['USD'], device: { connectiontype: 0, - // ip: '64.188.178.115', + // ip: '98.61.5.0', js: 1, - // language: "en", - // os: "Microsoft Windows", - // ua: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36 Edge/18.19043", + // language: 'en', + // os: 'Microsoft Windows', + // ua: 'Mozilla/5.0 (Linux; Android 12; SM-G970U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Mobile Safari/537.36', os: navigator.platform || '', ua: navigator.userAgent, language: /en/.test(navigator.language) ? 'en' : navigator.language, }, - ext: {}, + ext: { + eids, + firstPartyData, + }, user: { - id: sharedid || pubcid || getUserID(), + buyeruid: getUserID(), + id: sharedid || pubcid, }, + eids, site: { name: domain, domain: domain, @@ -420,12 +398,12 @@ export const spec = { nurl: getProperty(bid, 'nurl'), // adserverTargeting: { // granularityMultiplier: 0.1, - // priceGranularity: "pbHg", - // pbMg: "0.01", + // priceGranularity: 'pbHg', + // pbMg: '0.01', // }, - // pbMg: "0.01", + // pbMg: '0.01', // granularityMultiplier: 0.1, - // priceGranularity: "pbHg", + // priceGranularity: 'pbHg', }; bidResponses.push(bidResponse); } diff --git a/modules/mediakeysBidAdapter.js b/modules/mediakeysBidAdapter.js index 60ced650329..de86bfa248f 100644 --- a/modules/mediakeysBidAdapter.js +++ b/modules/mediakeysBidAdapter.js @@ -20,7 +20,6 @@ import { import {registerBidder} from '../src/adapters/bidderFactory.js'; import {config} from '../src/config.js'; import {BANNER, NATIVE, VIDEO} from '../src/mediaTypes.js'; -import {createEidsArray} from './userId/eids.js'; import { convertOrtbRequestToProprietaryNative } from '../src/native.js'; const AUCTION_TYPE = 1; @@ -638,8 +637,8 @@ export const spec = { deepSetValue(payload, 'regs.coppa', 1); } - if (deepAccess(validBidRequests[0], 'userId')) { - deepSetValue(payload, 'user.ext.eids', createEidsArray(validBidRequests[0].userId)); + if (deepAccess(validBidRequests[0], 'userIdAsEids')) { + deepSetValue(payload, 'user.ext.eids', validBidRequests[0].userIdAsEids); } // Assign payload.site from refererinfo diff --git a/modules/medianetAnalyticsAdapter.js b/modules/medianetAnalyticsAdapter.js index d2d3d2bf888..b902727a730 100644 --- a/modules/medianetAnalyticsAdapter.js +++ b/modules/medianetAnalyticsAdapter.js @@ -8,8 +8,7 @@ import { logError, logInfo, triggerPixel, - uniques, - getHighestCpm + uniques } from '../src/utils.js'; import adapter from '../libraries/analyticsAdapter/AnalyticsAdapter.js'; import adapterManager from '../src/adapterManager.js'; @@ -18,6 +17,7 @@ import {ajax} from '../src/ajax.js'; import {getRefererInfo} from '../src/refererDetection.js'; import {AUCTION_COMPLETED, AUCTION_IN_PROGRESS, getPriceGranularity} from '../src/auction.js'; import {includes} from '../src/polyfill.js'; +import {getGlobal} from '../src/prebidGlobal.js'; const analyticsType = 'endpoint'; const ENDPOINT = 'https://pb-logs.media.net/log?logid=kfk&evtid=prebid_analytics_events_client'; @@ -37,10 +37,11 @@ const PRICE_GRANULARITY = { const MEDIANET_BIDDER_CODE = 'medianet'; // eslint-disable-next-line no-undef -const PREBID_VERSION = $$PREBID_GLOBAL$$.version; +const PREBID_VERSION = getGlobal().version; const ERROR_CONFIG_JSON_PARSE = 'analytics_config_parse_fail'; const ERROR_CONFIG_FETCH = 'analytics_config_ajax_fail'; const ERROR_WINNING_BID_ABSENT = 'winning_bid_absent'; +const ERROR_WINNING_AUCTION_MISSING = 'winning_auction_missing'; const BID_SUCCESS = 1; const BID_NOBID = 2; const BID_TIMEOUT = 3; @@ -72,7 +73,7 @@ class ErrorLogger { this.evtid = 'projectevents'; this.project = 'prebidanalytics'; this.dn = pageDetails.domain || ''; - this.requrl = pageDetails.requrl || ''; + this.requrl = pageDetails.topmostLocation || ''; this.pbversion = PREBID_VERSION; this.cid = config.cid || ''; this.rd = additionalData; @@ -270,6 +271,52 @@ class AdSlot { } } +class BidWrapper { + constructor() { + this.bidReqs = []; + this.bidObjs = []; + } + + findReqBid(bidId) { + return this.bidReqs.find(bid => { + return bid['bidId'] === bidId + }); + } + + findBidObj(key, value) { + return this.bidObjs.find(bid => { + return bid[key] === value + }); + } + + addBidReq(bidRequest) { + this.bidReqs.push(bidRequest) + } + + addBidObj(bidObj) { + if (!(bidObj instanceof Bid)) { + bidObj = Bid.getInstance(bidObj); + } + const bidReq = this.findReqBid(bidObj.bidId); + if (bidReq instanceof Bid) { + bidReq.used = true; + } + this.bidObjs.push(bidObj); + } + + getAdSlotBids(adSlot) { + const bidResponses = this.getAdSlotBidObjs(adSlot); + return bidResponses.map((bid) => bid.getLoggingData()); + } + + getAdSlotBidObjs(adSlot) { + const bidResponses = this.bidObjs + .filter((bid) => bid.adUnitCode === adSlot); + const remResponses = this.bidReqs.filter(bid => !bid.used && bid.adUnitCode === adSlot); + return [...bidResponses, ...remResponses]; + } +} + class Bid { constructor(bidId, bidder, src, start, adUnitCode, mediaType, allMediaTypeSizes) { this.bidId = bidId; @@ -299,6 +346,9 @@ class Bid { this.floorPrice = undefined; this.floorRule = undefined; this.serverLatencyMillis = undefined; + this.used = false; + this.originalRequestId = bidId; + this.requestId = undefined; } get size() { @@ -308,8 +358,15 @@ class Bid { return this.width + 'x' + this.height; } + static getInstance(bidProps) { + const bidObj = new Bid(); + return bidProps && Object.assign(bidObj, bidProps); + } + getLoggingData() { return { + reqId: this.requestId || this.bidId, + ogReqId: this.originalRequestId, adid: this.adId, pvnm: this.bidder, src: this.src, @@ -341,7 +398,7 @@ class Auction { constructor(acid) { this.acid = acid; this.status = AUCTION_IN_PROGRESS; - this.bids = []; + this.bidWrapper = new BidWrapper(); this.adSlots = {}; this.auctionInitTime = undefined; this.auctionStartTime = undefined; @@ -378,24 +435,31 @@ class Auction { addSlot({ adUnitCode, supplyAdCode, mediaTypes, allMediaTypeSizes, tmax, adext, context }) { if (adUnitCode && this.adSlots[adUnitCode] === undefined) { this.adSlots[adUnitCode] = new AdSlot(tmax, supplyAdCode, context, adext); - this.addBid(new Bid('-1', DUMMY_BIDDER, 'client', '-1', adUnitCode, mediaTypes, allMediaTypeSizes)); + this.addBidObj(new Bid('-1', DUMMY_BIDDER, 'client', Date.now(), adUnitCode, mediaTypes, allMediaTypeSizes)); } } addBid(bid) { - this.bids.push(bid); + this.bidWrapper.addBidReq(bid); } - findBid(key, value) { - return this.bids.filter(bid => { - return bid[key] === value - })[0]; + addBidObj(bidObj) { + this.bidWrapper.addBidObj(bidObj) } - getAdslotBids(adslot) { - return this.bids - .filter((bid) => bid.adUnitCode === adslot) - .map((bid) => bid.getLoggingData()); + findReqBid(bidId) { + return this.bidWrapper.findReqBid(bidId) + } + + findBidObj(key, value) { + return this.bidWrapper.findBidObj(key, value) + } + + getAdSlotBids(adSlot) { + return this.bidWrapper.getAdSlotBids(adSlot); + } + getAdSlotBidObjs(adSlot) { + return this.bidWrapper.getAdSlotBidObjs(adSlot); } _mergeFieldsToLog(objParams) { @@ -494,41 +558,26 @@ function _getSizes(mediaTypes, sizes) { } } -/* - - The code is used to determine if the current bid is higher than the previous bid. - - If it is, then the code will return true and if not, it will return false. - */ -function canSelectCurrentBid(previousBid, currentBid) { - if (!(previousBid instanceof Bid)) return false; - - // For first bid response the previous bid will be containing bid request obj - // in which the cpm would be undefined so the current bid can directly be selected. - const isFirstBidResponse = previousBid.cpm === undefined && currentBid.cpm !== undefined; - if (isFirstBidResponse) return true; - - // if there are 2 bids, get the highest bid - const selectedBid = getHighestCpm(previousBid, currentBid); - - // Return true if selectedBid is currentBid, - // The timeToRespond field is used as an identifier for distinguishing - // between the current iterating bid and the previous bid. - return selectedBid.timeToRespond === currentBid.timeToRespond; -} - function bidResponseHandler(bid) { - const { width, height, mediaType, cpm, requestId, timeToRespond, auctionId, dealId } = bid; - const {originalCpm, bidderCode, creativeId, adId, currency} = bid; + const { width, height, mediaType, cpm, requestId, timeToRespond, auctionId, dealId, originalRequestId, bidder } = bid; + const {originalCpm, creativeId, adId, currency} = bid; if (!(auctions[auctionId] instanceof Auction)) { return; } - let bidObj = auctions[auctionId].findBid('bidId', requestId); - if (!canSelectCurrentBid(bidObj, bid)) { - return; + const reqId = originalRequestId || requestId; + const bidReq = auctions[auctionId].findReqBid(reqId); + + if (!(bidReq instanceof Bid)) return; + + let bidObj = auctions[auctionId].findBidObj('bidId', requestId); + let isBidOverridden = true; + if (!bidObj || bidObj.status === BID_SUCCESS) { + bidObj = {}; + isBidOverridden = false; } - Object.assign( - bidObj, - { cpm, width, height, mediaType, timeToRespond, dealId, creativeId }, + Object.assign(bidObj, bidReq, + { cpm, width, height, mediaType, timeToRespond, dealId, creativeId, originalRequestId, requestId }, { adId, currency } ); bidObj.floorPrice = deepAccess(bid, 'floorData.floorValue'); @@ -547,7 +596,7 @@ function bidResponseHandler(bid) { bidObj.status = BID_SUCCESS; } - if (bidderCode === MEDIANET_BIDDER_CODE && bid.ext instanceof Object) { + if (bidder === MEDIANET_BIDDER_CODE && bid.ext instanceof Object) { Object.assign( bidObj, { 'ext': bid.ext }, @@ -558,6 +607,7 @@ function bidResponseHandler(bid) { if (typeof bid.serverResponseTimeMs !== 'undefined') { bidObj.serverLatencyMillis = bid.serverResponseTimeMs; } + !isBidOverridden && auctions[auctionId].addBidObj(bidObj); } function noBidResponseHandler({ auctionId, bidId }) { @@ -567,11 +617,13 @@ function noBidResponseHandler({ auctionId, bidId }) { if (auctions[auctionId].hasEnded()) { return; } - let bidObj = auctions[auctionId].findBid('bidId', bidId); - if (!(bidObj instanceof Bid)) { + const bidReq = auctions[auctionId].findReqBid(bidId); + if (!(bidReq instanceof Bid) || bidReq.used) { return; } + const bidObj = {...bidReq}; bidObj.status = BID_NOBID; + auctions[auctionId].addBidObj(bidObj); } function bidTimeoutHandler(timedOutBids) { @@ -579,11 +631,13 @@ function bidTimeoutHandler(timedOutBids) { if (!(auctions[auctionId] instanceof Auction)) { return; } - let bidObj = auctions[auctionId].findBid('bidId', bidId); - if (!(bidObj instanceof Bid)) { + const bidReq = auctions[auctionId].findReqBid('bidId', bidId); + if (!(bidReq instanceof Bid) || bidReq.used) { return; } + const bidObj = {...bidReq}; bidObj.status = BID_TIMEOUT; + auctions[auctionId].addBidObj(bidObj); }) } @@ -614,13 +668,13 @@ function setTargetingHandler(params) { const winnerAdId = params[adunit][CONSTANTS.TARGETING_KEYS.AD_ID]; let winningBid; let bidAdIds = Object.keys(targetingObj).map(k => targetingObj[k]); - auctionObj.bids.filter((bid) => bidAdIds.indexOf(bid.adId) !== -1).map(function(bid) { + auctionObj.bidWrapper.bidObjs.filter((bid) => bidAdIds.indexOf(bid.adId) !== -1).map(function(bid) { bid.iwb = 1; if (bid.adId === winnerAdId) { winningBid = bid; } }); - auctionObj.bids.forEach(bid => { + auctionObj.bidWrapper.bidObjs.forEach(bid => { if (bid.bidder === DUMMY_BIDDER && bid.adUnitCode === adunit) { bid.iwb = bidAdIds.length === 0 ? 0 : 1; bid.width = deepAccess(winningBid, 'width'); @@ -633,16 +687,27 @@ function setTargetingHandler(params) { } function bidWonHandler(bid) { - const { auctionId, adUnitCode, adId } = bid; + const { auctionId, adUnitCode, adId, bidder, requestId, originalRequestId } = bid; if (!(auctions[auctionId] instanceof Auction)) { + new ErrorLogger(ERROR_WINNING_AUCTION_MISSING, { + adId, + auctionId, + adUnitCode, + bidder, + requestId, + originalRequestId + }).send(); return; } - let bidObj = auctions[auctionId].findBid('adId', adId); + let bidObj = auctions[auctionId].findBidObj('adId', adId); if (!(bidObj instanceof Bid)) { new ErrorLogger(ERROR_WINNING_BID_ABSENT, { - adId: adId, - acid: auctionId, + adId, + auctionId, adUnitCode, + bidder, + requestId, + originalRequestId }).send(); return; } @@ -696,13 +761,13 @@ function fireAuctionLog(acid, adtag, logType, adId) { let bidParams; if (logType === LOG_TYPE.RA) { - const winningBidObj = auctions[acid].findBid('adId', adId); + const winningBidObj = auctions[acid].findBidObj('adId', adId); if (!winningBidObj) return; const winLogData = winningBidObj.getLoggingData(); bidParams = [winLogData]; commonParams.lper = 1; } else { - bidParams = auctions[acid].getAdslotBids(adtag).map(({winner, ...restParams}) => restParams); + bidParams = auctions[acid].getAdSlotBids(adtag).map(({winner, ...restParams}) => restParams); delete commonParams.wts; } let mnetPresent = bidParams.filter(b => b.pvnm === MEDIANET_BIDDER_CODE).length > 0; @@ -822,8 +887,8 @@ medianetAnalytics.enableAnalytics = function (configuration) { logError('Media.net Analytics adapter: cid is required.'); return; } - $$PREBID_GLOBAL$$.medianetGlobals = $$PREBID_GLOBAL$$.medianetGlobals || {}; - $$PREBID_GLOBAL$$.medianetGlobals.analyticsEnabled = true; + getGlobal().medianetGlobals = getGlobal().medianetGlobals || {}; + getGlobal().medianetGlobals.analyticsEnabled = true; pageDetails = new PageDetail(); diff --git a/modules/medianetBidAdapter.js b/modules/medianetBidAdapter.js index 01652a3fac0..c398d8fd5db 100644 --- a/modules/medianetBidAdapter.js +++ b/modules/medianetBidAdapter.js @@ -17,6 +17,7 @@ import {BANNER, NATIVE, VIDEO} from '../src/mediaTypes.js'; import {getRefererInfo} from '../src/refererDetection.js'; import {Renderer} from '../src/Renderer.js'; import { convertOrtbRequestToProprietaryNative } from '../src/native.js'; +import {getGlobal} from '../src/prebidGlobal.js'; const BIDDER_CODE = 'medianet'; const BID_URL = 'https://prebid.media.net/rtb/prebid'; @@ -51,7 +52,7 @@ const aliases = [ { code: 'aax', gvlid: 720 }, ]; -$$PREBID_GLOBAL$$.medianetGlobals = $$PREBID_GLOBAL$$.medianetGlobals || {}; +getGlobal().medianetGlobals = getGlobal().medianetGlobals || {}; function getTopWindowReferrer() { try { @@ -177,7 +178,7 @@ function extParams(bidRequest, bidderRequests) { const coppaApplies = !!(config.getConfig('coppa')); return Object.assign({}, { customer_id: params.cid }, - { prebid_version: $$PREBID_GLOBAL$$.version }, + { prebid_version: getGlobal().version }, { gdpr_applies: gdprApplies }, (gdprApplies) && { gdpr_consent_string: gdpr.consentString || '' }, { usp_applies: uspApplies }, @@ -185,7 +186,7 @@ function extParams(bidRequest, bidderRequests) { {coppa_applies: coppaApplies}, windowSize.w !== -1 && windowSize.h !== -1 && { screen: windowSize }, userId && { user_id: userId }, - $$PREBID_GLOBAL$$.medianetGlobals.analyticsEnabled && { analytics: true }, + getGlobal().medianetGlobals.analyticsEnabled && { analytics: true }, !isEmpty(sChain) && {schain: sChain} ); } @@ -333,7 +334,7 @@ function generatePayload(bidRequests, bidderRequests) { id: bidRequests[0].auctionId, imp: bidRequests.map(request => slotParams(request)), ortb2: bidderRequests.ortb2, - tmax: bidderRequests.timeout || config.getConfig('bidderTimeout') + tmax: bidderRequests.timeout } } @@ -358,7 +359,7 @@ function getLoggingData(event, data) { params.evtid = 'projectevents'; params.project = 'prebid'; params.acid = deepAccess(data, '0.auctionId') || ''; - params.cid = $$PREBID_GLOBAL$$.medianetGlobals.cid || ''; + params.cid = getGlobal().medianetGlobals.cid || ''; params.crid = data.map((adunit) => deepAccess(adunit, 'params.0.crid') || adunit.adUnitCode).join('|'); params.adunit_count = data.length || 0; params.dn = mnData.urlData.domain || ''; @@ -442,7 +443,7 @@ export const spec = { return false; } - Object.assign($$PREBID_GLOBAL$$.medianetGlobals, !$$PREBID_GLOBAL$$.medianetGlobals.cid && {cid: bid.params.cid}); + Object.assign(getGlobal().medianetGlobals, !getGlobal().medianetGlobals.cid && {cid: bid.params.cid}); return true; }, diff --git a/modules/medianetRtdProvider.js b/modules/medianetRtdProvider.js index 3e01d0e5631..5a159b39081 100644 --- a/modules/medianetRtdProvider.js +++ b/modules/medianetRtdProvider.js @@ -59,14 +59,14 @@ function onAuctionInitEvent(auctionInit) { }, SOURCE)); } -function getTargetingData(adUnitCode) { - const adUnits = getAdUnits(undefined, adUnitCode); +function getTargetingData(adUnitCodes, config, consent, auction) { + const adUnits = getAdUnits(auction.adUnits, adUnitCodes); let targetingData = {}; if (window.mnjs.loaded && isFn(window.mnjs.getTargetingData)) { - targetingData = window.mnjs.getTargetingData(adUnitCode, adUnits, SOURCE) || {}; + targetingData = window.mnjs.getTargetingData(adUnitCodes, adUnits, SOURCE) || {}; } const targeting = {}; - adUnitCode.forEach(adUnitCode => { + adUnitCodes.forEach(adUnitCode => { targeting[adUnitCode] = targeting[adUnitCode] || {}; targetingData[adUnitCode] = targetingData[adUnitCode] || {}; targeting[adUnitCode] = { diff --git a/modules/merkleIdSystem.js b/modules/merkleIdSystem.js index 6a10c2a94eb..c522d588970 100644 --- a/modules/merkleIdSystem.js +++ b/modules/merkleIdSystem.js @@ -9,13 +9,14 @@ import { logInfo, logError, logWarn } from '../src/utils.js'; import * as ajaxLib from '../src/ajax.js'; import {submodule} from '../src/hook.js' import {getStorageManager} from '../src/storageManager.js'; +import {MODULE_TYPE_UID} from '../src/activities/modules.js'; const MODULE_NAME = 'merkleId'; const ID_URL = 'https://prebid.sv.rkdms.com/identity/'; const DEFAULT_REFRESH = 7 * 3600; const SESSION_COOKIE_NAME = '_svsid'; -export const storage = getStorageManager(); +export const storage = getStorageManager({moduleType: MODULE_TYPE_UID, moduleName: MODULE_NAME}); function getSession(configParams) { let session = null; diff --git a/modules/mgidBidAdapter.js b/modules/mgidBidAdapter.js index 036effecd88..0079936d803 100644 --- a/modules/mgidBidAdapter.js +++ b/modules/mgidBidAdapter.js @@ -1,14 +1,31 @@ -import { _each, deepAccess, isPlainObject, isArray, isStr, logInfo, parseUrl, isEmpty, triggerPixel, logWarn, getBidIdParameter, isFn, isNumber } from '../src/utils.js'; +import { + _each, + deepAccess, + isPlainObject, + isArray, + isStr, + logInfo, + parseUrl, + isEmpty, + triggerPixel, + logWarn, + getBidIdParameter, + isFn, + isNumber, + isBoolean, + isInteger, deepSetValue, +} from '../src/utils.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import {BANNER, NATIVE} from '../src/mediaTypes.js'; import {config} from '../src/config.js'; import { getStorageManager } from '../src/storageManager.js'; import { convertOrtbRequestToProprietaryNative } from '../src/native.js'; +import {USERSYNC_DEFAULT_CONFIG} from '../src/userSync.js'; const GVLID = 358; const DEFAULT_CUR = 'USD'; const BIDDER_CODE = 'mgid'; -export const storage = getStorageManager({gvlid: GVLID, bidderCode: BIDDER_CODE}); +export const storage = getStorageManager({bidderCode: BIDDER_CODE}); const ENDPOINT_URL = 'https://prebid.mgid.com/prebid/'; const LOG_WARN_PREFIX = '[MGID warn]: '; const LOG_INFO_PREFIX = '[MGID info]: '; @@ -64,7 +81,7 @@ _each(NATIVE_ASSETS, anAsset => { _NATIVE_ASSET_ID_TO_KEY_MAP[anAsset.ID] = anAs _each(NATIVE_ASSETS, anAsset => { _NATIVE_ASSET_KEY_TO_ASSET_MAP[anAsset.KEY] = anAsset }); export const spec = { - VERSION: '1.5', + VERSION: '1.6', code: BIDDER_CODE, gvlid: GVLID, supportedMediaTypes: [BANNER, NATIVE], @@ -115,22 +132,19 @@ export const spec = { /** * Make a server request from the list of BidRequests. * - * @param {validBidRequests[]} - an array of bids + * @param {BidRequest[]} validBidRequests A non-empty list of bid requests which should be sent to the Server. + * @param bidderRequest * @return ServerRequest Info describing the request to the server. */ buildRequests: (validBidRequests, bidderRequest) => { // convert Native ORTB definition to old-style prebid native definition validBidRequests = convertOrtbRequestToProprietaryNative(validBidRequests); - + const [bidRequest] = validBidRequests; logInfo(LOG_INFO_PREFIX + `buildRequests`); if (validBidRequests.length === 0) { return; } const info = pageInfo(); - // TODO: the fallback seems to never be used here, and probably in the wrong order - const page = info.location || deepAccess(bidderRequest, 'refererInfo.page') - const hostname = parseUrl(page).hostname; - let domain = extractDomainFromHost(hostname) || hostname; const accountId = setOnAny(validBidRequests, 'params.accountId'); const muid = getLocalStorageSafely('mgMuidn'); let url = (setOnAny(validBidRequests, 'params.bidUrl') || ENDPOINT_URL) + accountId; @@ -182,31 +196,108 @@ export const spec = { let request = { id: deepAccess(bidderRequest, 'bidderRequestId'), - site: {domain, page}, + site: ortb2Data?.site || {}, cur: [cur], geo: {utcoffset: info.timeOffset}, - device: { - ua: navigator.userAgent, - js: 1, - dnt: (navigator.doNotTrack === 'yes' || navigator.doNotTrack === '1' || navigator.msDoNotTrack === '1') ? 1 : 0, - h: screen.height, - w: screen.width, - language: getLanguage() - }, + device: ortb2Data?.device || {}, ext: { mgid_ver: spec.VERSION, prebid_ver: '$prebid.version$', - ...ortb2Data }, - imp + imp, + tmax: bidderRequest?.timeout || config.getConfig('bidderTimeout') || 500, }; - if (bidderRequest && bidderRequest.gdprConsent) { - request.user = {ext: {consent: bidderRequest.gdprConsent.consentString}}; - request.regs = {ext: {gdpr: (bidderRequest.gdprConsent.gdprApplies ? 1 : 0)}} + // request level + const bcat = ortb2Data?.bcat || bidRequest?.params?.bcat || []; + const badv = ortb2Data?.badv || bidRequest?.params?.badv || []; + const wlang = ortb2Data?.wlang || bidRequest?.params?.wlang || []; + if (bcat.length > 0) { + request.bcat = bcat; + } + if (badv.length > 0) { + request.badv = badv; + } + if (wlang.length > 0) { + request.wlang = wlang; + } + // site level + const page = deepAccess(bidderRequest, 'refererInfo.page') || info.location + if (!isStr(deepAccess(request.site, 'domain'))) { + const hostname = parseUrl(page).hostname; + request.site.domain = extractDomainFromHost(hostname) || hostname + } + if (!isStr(deepAccess(request.site, 'page'))) { + request.site.page = page + } + if (!isStr(deepAccess(request.site, 'ref'))) { + const ref = deepAccess(bidderRequest, 'refererInfo.ref') || info.referrer; + if (ref) { + request.site.ref = ref + } + } + // device level + if (!isStr(deepAccess(request.device, 'ua'))) { + request.device.ua = navigator.userAgent; + } + request.device.js = 1; + if (!isInteger(deepAccess(request.device, 'dnt'))) { + request.device.dnt = (navigator?.doNotTrack === 'yes' || navigator?.doNotTrack === '1' || navigator?.msDoNotTrack === '1') ? 1 : 0; + } + if (!isInteger(deepAccess(request.device, 'h'))) { + request.device.h = screen.height; } - if (info.referrer) { - request.site.ref = info.referrer + if (!isInteger(deepAccess(request.device, 'w'))) { + request.device.w = screen.width; + } + if (!isStr(deepAccess(request.device, 'language'))) { + request.device.language = getLanguage(); + } + // user & regs & privacy + if (isPlainObject(ortb2Data?.user)) { + request.user = ortb2Data.user; + } + if (isPlainObject(ortb2Data?.regs)) { + request.regs = ortb2Data.regs; + } + if (bidderRequest && isPlainObject(bidderRequest.gdprConsent)) { + if (!isStr(deepAccess(request.user, 'ext.consent'))) { + deepSetValue(request, 'user.ext.consent', bidderRequest.gdprConsent?.consentString); + } + if (!isBoolean(deepAccess(request.regs, 'ext.gdpr'))) { + deepSetValue(request, 'regs.ext.gdpr', bidderRequest.gdprConsent?.gdprApplies ? 1 : 0); + } + } + const userId = deepAccess(bidderRequest, 'userId') + if (isStr(userId)) { + deepSetValue(request, 'user.id', userId); + } + const eids = setOnAny(validBidRequests, 'userIdAsEids') + if (eids && eids.length > 0) { + deepSetValue(request, 'user.ext.eids', eids); + } + if (bidderRequest && isStr(bidderRequest.uspConsent)) { + if (!isBoolean(deepAccess(request.regs, 'ext.us_privacy'))) { + deepSetValue(request, 'regs.ext.us_privacy', bidderRequest.uspConsent); + } } + if (bidderRequest && isPlainObject(bidderRequest.gppConsent)) { + if (!isStr(deepAccess(request.regs, 'gpp'))) { + deepSetValue(request, 'regs.gpp', bidderRequest.gppConsent?.gppString); + } + if (!isArray(deepAccess(request.regs, 'gpp_sid'))) { + deepSetValue(request, 'regs.gpp_sid', bidderRequest.gppConsent?.applicableSections); + } + } + if (config.getConfig('coppa')) { + if (!isInteger(deepAccess(request.regs, 'coppa'))) { + deepSetValue(request, 'regs.coppa', 1); + } + } + const schain = setOnAny(validBidRequests, 'schain'); + if (schain) { + deepSetValue(request, 'source.ext.schain', schain); + } + logInfo(LOG_INFO_PREFIX + `buildRequest:`, request); return { method: 'POST', @@ -218,6 +309,7 @@ export const spec = { * Unpack the response from the server into a list of bids. * * @param {ServerResponse} serverResponse A successful response from the server. + * @param bidRequests * @return {Bid[]} An array of bids which were nested inside the server. */ interpretResponse: (serverResponse, bidRequests) => { @@ -268,8 +360,66 @@ export const spec = { } logInfo(LOG_INFO_PREFIX + `onBidWon`); }, - getUserSyncs: (syncOptions, serverResponses) => { + getUserSyncs: (syncOptions, serverResponses, gdprConsent, uspConsent, gppConsent) => { logInfo(LOG_INFO_PREFIX + `getUserSyncs`); + const spb = isPlainObject(config.getConfig('userSync')) && + isNumber(config.getConfig('userSync').syncsPerBidder) + ? config.getConfig('userSync').syncsPerBidder : USERSYNC_DEFAULT_CONFIG.syncsPerBidder; + + if (spb > 0 && isPlainObject(syncOptions) && (syncOptions.iframeEnabled || syncOptions.pixelEnabled)) { + let pixels = []; + if (serverResponses && + isArray(serverResponses) && + serverResponses.length > 0 && + isPlainObject(serverResponses[0].body) && + isPlainObject(serverResponses[0].body.ext) && + isArray(serverResponses[0].body.ext.cm) && + serverResponses[0].body.ext.cm.length > 0) { + pixels = serverResponses[0].body.ext.cm; + } + + const syncs = []; + const query = []; + query.push('cbuster=' + Math.round(new Date().getTime())); + query.push('consentData=' + encodeURIComponent(isPlainObject(gdprConsent) && isStr(gdprConsent?.consentString) ? gdprConsent.consentString : '')); + if (isPlainObject(gdprConsent) && typeof gdprConsent?.gdprApplies === 'boolean' && gdprConsent.gdprApplies) { + query.push('gdprApplies=1'); + } else { + query.push('gdprApplies=0'); + } + if (isPlainObject(uspConsent) && uspConsent?.consentString) { + query.push(`uspString=${encodeURIComponent(uspConsent?.consentString)}`); + } + if (isPlainObject(gppConsent) && gppConsent?.gppString) { + query.push(`gppString=${encodeURIComponent(gppConsent?.gppString)}`); + } + if (config.getConfig('coppa')) { + query.push('coppa=1') + } + if (syncOptions.iframeEnabled) { + syncs.push({ + type: 'iframe', + url: 'https://cm.mgid.com/i.html?' + query.join('&') + }); + } else if (syncOptions.pixelEnabled) { + if (pixels.length === 0) { + for (let i = 0; i < spb; i++) { + syncs.push({ + type: 'image', + url: 'https://cm.mgid.com/i.gif?' + query.join('&') // randomly selects partner if sync required + }); + } + } else { + for (let i = 0; i < spb && i < pixels.length; i++) { + syncs.push({ + type: 'image', + url: pixels[i] + (pixels[i].indexOf('?') > 0 ? '&' : '?') + query.join('&') + }); + } + } + } + return syncs; + } } }; @@ -287,6 +437,7 @@ function setOnAny(collection, key) { /** * Unpack the Server's Bid into a Prebid-compatible one. * @param serverBid + * @param cur * @return Bid */ function prebidBid(serverBid, cur) { @@ -330,7 +481,7 @@ function setMediaType(bid, newBid) { } function extractDomainFromHost(pageHost) { - if (pageHost == 'localhost') { + if (pageHost === 'localhost') { return 'localhost' } let domain = null; @@ -600,6 +751,7 @@ function pageInfo() { * Get the floor price from bid.params for backward compatibility. * If not found, then check floor module. * @param bid A valid bid object + * @param cur * @returns {*|number} floor price */ function getBidFloor(bid, cur) { diff --git a/modules/mgidRtdProvider.js b/modules/mgidRtdProvider.js index f30f14ea528..fd2c0bbe6fd 100644 --- a/modules/mgidRtdProvider.js +++ b/modules/mgidRtdProvider.js @@ -3,6 +3,7 @@ import {ajax} from '../src/ajax.js'; import {deepAccess, logError, logInfo, mergeDeep} from '../src/utils.js'; import {getStorageManager} from '../src/storageManager.js'; import {getRefererInfo} from '../src/refererDetection.js'; +import {MODULE_TYPE_RTD} from '../src/activities/modules.js'; const MODULE_NAME = 'realTimeData'; const SUBMODULE_NAME = 'mgid'; @@ -13,7 +14,7 @@ const ORTB2_NAME = 'www.mgid.com' const GVLID = 358; /** @type {?Object} */ export const storage = getStorageManager({ - gvlid: GVLID, + moduleType: MODULE_TYPE_RTD, moduleName: SUBMODULE_NAME }); @@ -185,6 +186,7 @@ export const mgidSubmodule = { name: SUBMODULE_NAME, init: init, getBidRequestData: getBidRequestData, + gvlid: GVLID }; submodule(MODULE_NAME, mgidSubmodule); diff --git a/modules/minutemediaBidAdapter.js b/modules/minutemediaBidAdapter.js index d953558bf31..82eb48a5a2b 100644 --- a/modules/minutemediaBidAdapter.js +++ b/modules/minutemediaBidAdapter.js @@ -366,7 +366,7 @@ function generateGeneralParams(generalObject, bidderRequest) { const {syncEnabled, filterSettings} = config.getConfig('userSync') || {}; const {bidderCode} = bidderRequest; const generalBidParams = generalObject.params; - const timeout = config.getConfig('bidderTimeout'); + const timeout = bidderRequest.timeout; // these params are snake_case instead of camelCase to allow backwards compatability on the server. // in the future, these will be converted to camelCase to match our convention. @@ -428,5 +428,9 @@ function generateGeneralParams(generalObject, bidderRequest) { generalParams.page_url = deepAccess(bidderRequest, 'refererInfo.page') || window.location.href } + if (config.getConfig('coppa') === true) { + generalParams.coppa = 1; + } + return generalParams } diff --git a/modules/minutemediaplusBidAdapter.js b/modules/minutemediaplusBidAdapter.js new file mode 100644 index 00000000000..c07f227bcfa --- /dev/null +++ b/modules/minutemediaplusBidAdapter.js @@ -0,0 +1,331 @@ +import {_each, deepAccess, parseSizesInput, parseUrl, uniques, isFn} from '../src/utils.js'; +import {registerBidder} from '../src/adapters/bidderFactory.js'; +import {BANNER, VIDEO} from '../src/mediaTypes.js'; +import {getStorageManager} from '../src/storageManager.js'; +import {config} from '../src/config.js'; + +const GVLID = 918; +const DEFAULT_SUB_DOMAIN = 'exchange'; +const BIDDER_CODE = 'mmplus'; +const BIDDER_VERSION = '1.0.0'; +const CURRENCY = 'USD'; +const TTL_SECONDS = 60 * 5; +const UNIQUE_DEAL_ID_EXPIRY = 1000 * 60 * 15; +export const SUPPORTED_ID_SYSTEMS = { + 'britepoolid': 1, + 'criteoId': 1, + 'id5id': 1, + 'idl_env': 1, + 'lipb': 1, + 'netId': 1, + 'parrableId': 1, + 'pubcid': 1, + 'tdid': 1, + 'pubProvidedId': 1 +}; +const storage = getStorageManager({bidderCode: BIDDER_CODE}); + +function getTopWindowQueryParams() { + try { + const parsedUrl = parseUrl(window.top.document.URL, {decodeSearchAsString: true}); + return parsedUrl.search; + } catch (e) { + return ''; + } +} + +export function createDomain(subDomain = DEFAULT_SUB_DOMAIN) { + return `https://${subDomain}.minutemedia-prebid.com`; +} + +export function extractCID(params) { + return params.cId || params.CID || params.cID || params.CId || params.cid || params.ciD || params.Cid || params.CiD; +} + +export function extractPID(params) { + return params.pId || params.PID || params.pID || params.PId || params.pid || params.piD || params.Pid || params.PiD; +} + +export function extractSubDomain(params) { + return params.subDomain || params.SubDomain || params.Subdomain || params.subdomain || params.SUBDOMAIN || params.subDOMAIN; +} + +function isBidRequestValid(bid) { + const params = bid.params || {}; + return !!(extractCID(params) && extractPID(params)); +} + +function buildRequest(bid, topWindowUrl, sizes, bidderRequest, bidderTimeout) { + const { + params, + bidId, + userId, + adUnitCode, + schain, + mediaTypes, + auctionId, + transactionId, + bidderRequestId, + bidRequestsCount, + bidderRequestsCount, + bidderWinsCount + } = bid; + let {bidFloor, ext} = params; + const hashUrl = hashCode(topWindowUrl); + const uniqueDealId = getUniqueDealId(hashUrl); + const cId = extractCID(params); + const pId = extractPID(params); + const subDomain = extractSubDomain(params); + + if (isFn(bid.getFloor)) { + const floorInfo = bid.getFloor({ + currency: 'USD', + mediaType: '*', + size: '*' + }); + + if (floorInfo.currency === 'USD') { + bidFloor = floorInfo.floor; + } + } + + let data = { + url: encodeURIComponent(topWindowUrl), + uqs: getTopWindowQueryParams(), + cb: Date.now(), + bidFloor: bidFloor, + bidId: bidId, + referrer: bidderRequest.refererInfo.ref, + adUnitCode: adUnitCode, + publisherId: pId, + sizes: sizes, + uniqueDealId: uniqueDealId, + bidderVersion: BIDDER_VERSION, + prebidVersion: '$prebid.version$', + res: `${screen.width}x${screen.height}`, + schain: schain, + mediaTypes: mediaTypes, + auctionId: auctionId, + transactionId: transactionId, + bidderRequestId: bidderRequestId, + bidRequestsCount: bidRequestsCount, + bidderRequestsCount: bidderRequestsCount, + bidderWinsCount: bidderWinsCount, + bidderTimeout: bidderTimeout + }; + + appendUserIdsToRequestPayload(data, userId); + + const sua = deepAccess(bidderRequest, 'ortb2.device.sua'); + + if (sua) { + data.sua = sua; + } + + if (bidderRequest.gdprConsent) { + if (bidderRequest.gdprConsent.consentString) { + data.gdprConsent = bidderRequest.gdprConsent.consentString; + } + if (bidderRequest.gdprConsent.gdprApplies !== undefined) { + data.gdpr = bidderRequest.gdprConsent.gdprApplies ? 1 : 0; + } + } + if (bidderRequest.uspConsent) { + data.usPrivacy = bidderRequest.uspConsent; + } + + if (bidderRequest.gppConsent) { + data.gppString = bidderRequest.gppConsent.gppString; + data.gppSid = bidderRequest.gppConsent.applicableSections; + } else if (bidderRequest.ortb2?.regs?.gpp) { + data.gppString = bidderRequest.ortb2.regs.gpp; + data.gppSid = bidderRequest.ortb2.regs.gpp_sid; + } + + const dto = { + method: 'POST', + url: `${createDomain(subDomain)}/prebid/multi/${cId}`, + data: data + }; + + _each(ext, (value, key) => { + dto.data['ext.' + key] = value; + }); + + return dto; +} + +function appendUserIdsToRequestPayload(payloadRef, userIds) { + let key; + _each(userIds, (userId, idSystemProviderName) => { + if (SUPPORTED_ID_SYSTEMS[idSystemProviderName]) { + key = `uid.${idSystemProviderName}`; + + switch (idSystemProviderName) { + case 'digitrustid': + payloadRef[key] = deepAccess(userId, 'data.id'); + break; + case 'lipb': + payloadRef[key] = userId.lipbid; + break; + case 'parrableId': + payloadRef[key] = userId.eid; + break; + case 'id5id': + payloadRef[key] = userId.uid; + break; + default: + payloadRef[key] = userId; + } + } + }); +} + +function buildRequests(validBidRequests, bidderRequest) { + const topWindowUrl = bidderRequest.refererInfo.page || bidderRequest.refererInfo.topmostLocation; + const bidderTimeout = config.getConfig('bidderTimeout'); + const requests = []; + validBidRequests.forEach(validBidRequest => { + const sizes = parseSizesInput(validBidRequest.sizes); + const request = buildRequest(validBidRequest, topWindowUrl, sizes, bidderRequest, bidderTimeout); + requests.push(request); + }); + return requests; +} + +function interpretResponse(serverResponse, request) { + if (!serverResponse || !serverResponse.body) { + return []; + } + const {bidId} = request.data; + const {results} = serverResponse.body; + + let output = []; + + try { + results.forEach(result => { + const {creativeId, ad, price, exp, width, height, currency, advertiserDomains, mediaType = BANNER} = result; + if (!ad || !price) { + return; + } + + const response = { + requestId: bidId, + cpm: price, + width: width, + height: height, + creativeId: creativeId, + currency: currency || CURRENCY, + netRevenue: true, + ttl: exp || TTL_SECONDS, + meta: { + advertiserDomains: advertiserDomains || [] + } + }; + + if (mediaType === BANNER) { + Object.assign(response, { + ad: ad, + }); + } else { + Object.assign(response, { + vastXml: ad, + mediaType: VIDEO + }); + } + output.push(response); + }); + return output; + } catch (e) { + return []; + } +} + +function getUserSyncs(syncOptions, responses, gdprConsent = {}, uspConsent = '') { + let syncs = []; + const {iframeEnabled, pixelEnabled} = syncOptions; + const {gdprApplies, consentString = ''} = gdprConsent; + + const cidArr = responses.filter(resp => deepAccess(resp, 'body.cid')).map(resp => resp.body.cid).filter(uniques); + const params = `?cid=${encodeURIComponent(cidArr.join(','))}&gdpr=${gdprApplies ? 1 : 0}&gdpr_consent=${encodeURIComponent(consentString || '')}&us_privacy=${encodeURIComponent(uspConsent || '')}` + if (iframeEnabled) { + syncs.push({ + type: 'iframe', + url: `https://sync.minutemedia-prebid.com/api/sync/iframe/${params}` + }); + } + if (pixelEnabled) { + syncs.push({ + type: 'image', + url: `https://sync.minutemedia-prebid.com/api/sync/image/${params}` + }); + } + return syncs; +} + +export function hashCode(s, prefix = '_') { + const l = s.length; + let h = 0 + let i = 0; + if (l > 0) { + while (i < l) { + h = (h << 5) - h + s.charCodeAt(i++) | 0; + } + } + return prefix + h; +} + +export function getUniqueDealId(key, expiry = UNIQUE_DEAL_ID_EXPIRY) { + const storageKey = `u_${key}`; + const now = Date.now(); + const data = getStorageItem(storageKey); + let uniqueId; + + if (!data || !data.value || now - data.created > expiry) { + uniqueId = `${key}_${now.toString()}`; + setStorageItem(storageKey, uniqueId); + } else { + uniqueId = data.value; + } + + return uniqueId; +} + +export function getStorageItem(key) { + try { + return tryParseJSON(storage.getDataFromLocalStorage(key)); + } catch (e) { + } + + return null; +} + +export function setStorageItem(key, value, timestamp) { + try { + const created = timestamp || Date.now(); + const data = JSON.stringify({value, created}); + storage.setDataInLocalStorage(key, data); + } catch (e) { + } +} + +export function tryParseJSON(value) { + try { + return JSON.parse(value); + } catch (e) { + return value; + } +} + +export const spec = { + code: BIDDER_CODE, + version: BIDDER_VERSION, + gvlid: GVLID, + supportedMediaTypes: [BANNER, VIDEO], + isBidRequestValid, + buildRequests, + interpretResponse, + getUserSyncs +}; + +registerBidder(spec); diff --git a/modules/minutemediaplusBidAdapter.md b/modules/minutemediaplusBidAdapter.md new file mode 100644 index 00000000000..410c00e7017 --- /dev/null +++ b/modules/minutemediaplusBidAdapter.md @@ -0,0 +1,35 @@ +# Overview + +**Module Name:** MinuteMediaPlus Bidder Adapter + +**Module Type:** Bidder Adapter + +**Maintainer:** hb@minutemedia.com + +# Description + +Module that connects to MinuteMediaPlus's demand sources. + +# Test Parameters +```js +var adUnits = [ + { + code: 'test-ad', + sizes: [[300, 250]], + bids: [ + { + bidder: 'mmplus', + params: { + cId: '562524b21b1c1f08117fc7f9', + pId: '59ac17c192832d0011283fe3', + bidFloor: 0.0001, + ext: { + param1: 'loremipsum', + param2: 'dolorsitamet' + } + } + } + ] + } +]; +``` diff --git a/modules/missenaBidAdapter.js b/modules/missenaBidAdapter.js index 2ec9d39fc5d..f461440c5a1 100644 --- a/modules/missenaBidAdapter.js +++ b/modules/missenaBidAdapter.js @@ -1,9 +1,11 @@ -import { formatQS, logInfo } from '../src/utils.js'; +import { buildUrl, formatQS, logInfo, triggerPixel } from '../src/utils.js'; import { BANNER } from '../src/mediaTypes.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; const BIDDER_CODE = 'missena'; const ENDPOINT_URL = 'https://bid.missena.io/'; +const EVENTS_DOMAIN = 'events.missena.io'; +const EVENTS_DOMAIN_DEV = 'events.staging.missena.xyz'; export const spec = { aliases: [BIDDER_CODE], @@ -30,6 +32,7 @@ export const spec = { buildRequests: function (validBidRequests, bidderRequest) { return validBidRequests.map((bidRequest) => { const payload = { + adunit: bidRequest.adUnitCode, request_id: bidRequest.bidId, timeout: bidderRequest.timeout, }; @@ -48,6 +51,16 @@ export const spec = { if (bidRequest.params.test) { payload.test = bidRequest.params.test; } + if (bidRequest.params.placement) { + payload.placement = bidRequest.params.placement; + } + if (bidRequest.params.formats) { + payload.formats = bidRequest.params.formats; + } + if (bidRequest.params.isInternal) { + payload.is_internal = bidRequest.params.isInternal; + } + payload.userEids = bidRequest.userIdAsEids || []; return { method: 'POST', url: baseUrl + '?' + formatQS({ t: bidRequest.params.apiKey }), @@ -109,6 +122,15 @@ export const spec = { * @param {Bid} The bid that won the auction */ onBidWon: function (bid) { + const hostname = bid.params[0].baseUrl ? EVENTS_DOMAIN_DEV : EVENTS_DOMAIN; + triggerPixel( + buildUrl({ + protocol: 'https', + hostname, + pathname: '/v1/bidsuccess', + search: { t: bid.params[0].apiKey, provider: bid.meta?.networkName, cpm: bid.cpm, currency: bid.currency }, + }) + ); logInfo('Missena - Bid won', bid); }, }; diff --git a/modules/mwOpenLinkIdSystem.js b/modules/mwOpenLinkIdSystem.js index 552223fa73c..9b1035cbf18 100644 --- a/modules/mwOpenLinkIdSystem.js +++ b/modules/mwOpenLinkIdSystem.js @@ -8,14 +8,15 @@ import { timestamp, logError, deepClone, generateUUID, isPlainObject } from '../src/utils.js'; import { ajax } from '../src/ajax.js'; import { submodule } from '../src/hook.js'; -import { getStorageManager } from '../src/storageManager.js'; +import {getStorageManager} from '../src/storageManager.js'; +import {MODULE_TYPE_UID} from '../src/activities/modules.js'; const openLinkID = { name: 'mwol', cookie_expiration: (86400 * 1000 * 365 * 1) // 1 year } -const storage = getStorageManager(); +const storage = getStorageManager({moduleType: MODULE_TYPE_UID, moduleName: openLinkID.name}); function getExpirationDate() { return (new Date(timestamp() + openLinkID.cookie_expiration)).toGMTString(); diff --git a/modules/nativoBidAdapter.js b/modules/nativoBidAdapter.js index 7eb37ea8b82..a92168492d0 100644 --- a/modules/nativoBidAdapter.js +++ b/modules/nativoBidAdapter.js @@ -140,13 +140,9 @@ export const spec = { const floorPriceData = {} let placementId, pageUrl validBidRequests.forEach((bidRequest) => { - pageUrl = deepAccess( - bidRequest, - 'params.url', - ) - if (pageUrl == undefined || pageUrl === '') { - pageUrl = bidderRequest.refererInfo.location - } + pageUrl = + getPageUrlFromBidRequest(bidRequest) || + bidderRequest.refererInfo.location placementId = deepAccess(bidRequest, 'params.placementId') @@ -380,10 +376,12 @@ export const spec = { return syncs } - body = - typeof response.body === 'string' - ? JSON.parse(response.body) - : response.body + try { + body = + typeof response.body === 'string' + ? JSON.parse(response.body) + : response.body + } catch (err) { return } // Make sure we have valid content if (!body || !body.seatbid || body.seatbid.length === 0) return @@ -465,7 +463,7 @@ export function parseFloorPriceData(bidRequest) { // Setup price floor data per media type let mediaTypeData = bidMediaTypes[mediaType] let mediaTypeFloorPriceData = {} - let mediaTypeSizes = mediaTypeData.sizes || mediaTypeData.playerSize || []; + let mediaTypeSizes = mediaTypeData.sizes || mediaTypeData.playerSize || [] // Step through each size of the media type so we can get floor data for each size per media type mediaTypeSizes.forEach((size) => { // Get floor price data per the getFloor method and respective media type / size combination @@ -634,3 +632,37 @@ function appendFilterData(filter, filterData) { filterData.forEach((ad) => filter.add(ad)) } } + +export function getPageUrlFromBidRequest(bidRequest) { + let paramPageUrl = deepAccess(bidRequest, 'params.url') + + if (paramPageUrl == undefined) return + + if (hasProtocol(paramPageUrl)) return paramPageUrl + + paramPageUrl = addProtocol(paramPageUrl) + + try { + const url = new URL(paramPageUrl) + return url.href + } catch (err) {} +} + +export function hasProtocol(url) { + const protocolRegexp = /^http[s]?\:/ + return protocolRegexp.test(url) +} + +export function addProtocol(url) { + if (hasProtocol(url)) { + return url + } + + let protocolPrefix = 'https:' + + if (url.indexOf('//') !== 0) { + protocolPrefix += '//' + } + + return `${protocolPrefix}${url}` +} diff --git a/modules/naveggIdSystem.js b/modules/naveggIdSystem.js index 25f0afe4733..878ae7fadb2 100644 --- a/modules/naveggIdSystem.js +++ b/modules/naveggIdSystem.js @@ -7,7 +7,8 @@ import { isStr, isPlainObject, logError } from '../src/utils.js'; import { submodule } from '../src/hook.js'; import { ajax } from '../src/ajax.js'; -import { getStorageManager } from '../src/storageManager.js'; +import {getStorageManager} from '../src/storageManager.js'; +import {MODULE_TYPE_UID} from '../src/activities/modules.js'; const MODULE_NAME = 'naveggId'; const OLD_NAVEGG_ID = 'nid'; @@ -16,7 +17,7 @@ const BASE_URL = 'https://id.navegg.com/uid/'; const DEFAULT_EXPIRE = 8 * 24 * 3600 * 1000; const INVALID_EXPIRE = 3600 * 1000; -export const storage = getStorageManager(); +export const storage = getStorageManager({moduleType: MODULE_TYPE_UID, moduleName: MODULE_NAME}); function getNaveggIdFromApi() { const callbacks = { diff --git a/modules/neuwoRtdProvider.js b/modules/neuwoRtdProvider.js new file mode 100644 index 00000000000..00a3c59b4a6 --- /dev/null +++ b/modules/neuwoRtdProvider.js @@ -0,0 +1,174 @@ +import { deepAccess, deepSetValue, generateUUID, logError, logInfo, mergeDeep } from '../src/utils.js'; +import { getRefererInfo } from '../src/refererDetection.js'; +import { ajax } from '../src/ajax.js'; +import { submodule } from '../src/hook.js'; +import * as events from '../src/events.js'; +import CONSTANTS from '../src/constants.json'; + +export const DATA_PROVIDER = 'neuwo.ai'; +const SEGTAX_IAB = 6 // IAB - Content Taxonomy version 2 +const RESPONSE_IAB_TIER_1 = 'marketing_categories.iab_tier_1' +const RESPONSE_IAB_TIER_2 = 'marketing_categories.iab_tier_2' + +function init(config = {}, userConsent) { + config.params = config.params || {} + // ignore module if publicToken is missing (module setup failure) + if (!config.params.publicToken) { + logError('publicToken missing', 'NeuwoRTDModule', 'config.params.publicToken') + return false; + } + if (!config.params.apiUrl) { + logError('apiUrl missing', 'NeuwoRTDModule', 'config.params.apiUrl') + return false; + } + return true; +} + +export function getBidRequestData(reqBidsConfigObj, callback, config, userConsent) { + config.params = config.params || {}; + logInfo('NeuwoRTDModule', 'starting getBidRequestData') + + const wrappedArgUrl = encodeURIComponent(config.params.argUrl || getRefererInfo().page); + /* adjust for pages api.url?prefix=test (to add params with '&') as well as api.url (to add params with '?') */ + const joiner = config.params.apiUrl.indexOf('?') < 0 ? '?' : '&' + const url = config.params.apiUrl + joiner + [ + 'token=' + config.params.publicToken, + 'url=' + wrappedArgUrl + ].join('&') + const billingId = generateUUID(); + + const success = (responseContent) => { + logInfo('NeuwoRTDModule', 'GetAiTopics: response', responseContent) + try { + const jsonContent = JSON.parse(responseContent); + if (jsonContent.marketing_categories) { + events.emit(CONSTANTS.EVENTS.BILLABLE_EVENT, { type: 'request', billingId, vendor: neuwoRtdModule.name }) + } + injectTopics(jsonContent, reqBidsConfigObj, billingId) + } catch (ex) { + logError('NeuwoRTDModule', 'Response to JSON parse error', ex) + } + callback() + } + + const error = (err) => { + logError('xhr error', null, err); + callback() + } + + ajax(url, {success, error}, null, { + // could assume Origin header is set, or + // customHeaders: { 'Origin': 'Origin' } + }) +} + +export function addFragment(base, path, addition) { + const container = {} + deepSetValue(container, path, addition) + mergeDeep(base, container) +} + +/** + * Concatenate a base array and an array within an object + * non-array bases will be arrays, non-arrays at object key will be discarded + * @param {array} base base array to add to + * @param {object} source object to get an array from + * @param {string} key dot-notated path to array within object + * @returns base + source[key] if that's an array + */ +function combineArray(base, source, key) { + if (Array.isArray(base) === false) base = [] + const addition = deepAccess(source, key, []) + if (Array.isArray(addition)) return base.concat(addition) + else return base +} + +export function injectTopics(topics, bidsConfig) { + topics = topics || {} + + // join arrays of IAB category details to single array + const combinedTiers = combineArray( + combineArray([], topics, RESPONSE_IAB_TIER_1), + topics, RESPONSE_IAB_TIER_2) + + const segment = pickSegments(combinedTiers) + // effectively gets topics.marketing_categories.iab_tier_1, topics.marketing_categories.iab_tier_2 + // used as FPD segments content + + const IABSegments = { + name: DATA_PROVIDER, + ext: { segtax: SEGTAX_IAB }, + segment + } + + addFragment(bidsConfig.ortb2Fragments.global, 'site.content.data', [IABSegments]) + + // upgrade category taxonomy to IAB 2.2, inject result to page categories + if (segment.length > 0) { + addFragment(bidsConfig.ortb2Fragments.global, 'site.pagecat', segment.map(s => s.id)) + } + + logInfo('NeuwoRTDModule', 'injectTopics: post-injection bidsConfig', bidsConfig) +} + +/* eslint-disable object-property-newline */ +const D_IAB_ID = { // Content Taxonomy version 2.0 final release November 2017 [sic] (Taxonomy ID Mapping, IAB versions 2.0 - 2.2) + 'IAB19-1': '603', 'IAB6-1': '193', 'IAB5-2': '133', 'IAB20-1': '665', 'IAB20-2': '656', 'IAB23-2': '454', 'IAB3-2': '102', 'IAB20-3': '672', 'IAB8-5': '211', + 'IAB8-18': '211', 'IAB7-4': '288', 'IAB7-5': '233', 'IAB17-12': '484', 'IAB19-3': '608', 'IAB21-1': '442', 'IAB9-2': '248', 'IAB15-1': '456', 'IAB9-17': '265', 'IAB20-4': '658', + 'IAB2-3': '30', 'IAB2-1': '32', 'IAB17-1': '518', 'IAB2-2': '34', 'IAB2': '1', 'IAB8-2': '215', 'IAB17-2': '545', 'IAB17-26': '547', 'IAB9-3': '249', 'IAB18-1': '553', 'IAB20-5': '674', + 'IAB15-2': '465', 'IAB3-3': '119', 'IAB16-2': '423', 'IAB9-4': '259', 'IAB9-5': '270', 'IAB18-2': '574', 'IAB17-4': '549', 'IAB7-33': '312', 'IAB1-1': '42', 'IAB17-5': '485', 'IAB23-3': '458', + 'IAB20-6': '675', 'IAB3': '53', 'IAB20-7': '676', 'IAB19-5': '633', 'IAB20-9': '677', 'IAB9-6': '250', 'IAB17-6': '499', 'IAB2-4': '25', 'IAB9-7': '271', 'IAB4-11': '125', 'IAB4-1': '126', + 'IAB4': '123', 'IAB16-3': '424', 'IAB2-5': '18', 'IAB17-7': '486', 'IAB15-3': '466', 'IAB23-5': '459', 'IAB9-9': '260', 'IAB2-22': '19', 'IAB17-8': '500', 'IAB9-10': '261', 'IAB5-5': '137', + 'IAB9-11': '262', 'IAB2-21': '3', 'IAB19-2': '610', 'IAB19-8': '600', 'IAB19-9': '601', 'IAB3-5': '121', 'IAB9-15': '264', 'IAB2-6': '8', 'IAB2-7': '9', 'IAB22-2': '474', 'IAB17-9': '491', + 'IAB2-8': '10', 'IAB20-12': '678', 'IAB17-3': '492', 'IAB19-12': '611', 'IAB14-1': '188', 'IAB6-3': '194', 'IAB7-17': '316', 'IAB19-13': '612', 'IAB8-8': '217', 'IAB9-1': '205', 'IAB19-22': '613', + 'IAB8-9': '218', 'IAB14-2': '189', 'IAB16-4': '425', 'IAB9-12': '251', 'IAB5': '132', 'IAB6-9': '190', 'IAB19-15': '623', 'IAB17-17': '496', 'IAB20-14': '659', 'IAB6': '186', 'IAB20-26': '666', + 'IAB17-10': '510', 'IAB13-4': '396', 'IAB1-3': '201', 'IAB16-1': '426', 'IAB17-11': '511', 'IAB17-13': '511', 'IAB17-32': '511', 'IAB7-1': '225', 'IAB8': '210', 'IAB8-10': '219', 'IAB9-13': '266', + 'IAB10-4': '275', 'IAB9-14': '273', 'IAB15-8': '469', 'IAB15-4': '470', 'IAB17-15': '512', 'IAB3-7': '77', 'IAB19-16': '614', 'IAB3-8': '78', 'IAB2-10': '22', 'IAB2-12': '22', 'IAB2-11': '11', + 'IAB8-12': '221', 'IAB7-35': '223', 'IAB7-38': '223', 'IAB7-24': '296', 'IAB13-5': '411', 'IAB7-25': '234', 'IAB23-6': '460', 'IAB9': '239', 'IAB7-26': '235', 'IAB10': '274', 'IAB10-1': '278', + 'IAB10-2': '279', 'IAB19-17': '634', 'IAB10-5': '280', 'IAB5-10': '145', 'IAB5-11': '146', 'IAB20-17': '667', 'IAB17-16': '497', 'IAB20-18': '668', 'IAB3-9': '55', 'IAB1-4': '440', 'IAB17-18': '514', + 'IAB17-27': '515', 'IAB10-3': '282', 'IAB19-25': '618', 'IAB17-19': '516', 'IAB13-6': '398', 'IAB10-7': '283', 'IAB12-1': '382', 'IAB19-24': '624', 'IAB6-4': '195', 'IAB23-7': '461', 'IAB9-19': '252', + 'IAB4-4': '128', 'IAB4-5': '127', 'IAB23-8': '462', 'IAB10-8': '284', 'IAB5-8': '147', 'IAB16-5': '427', 'IAB11-2': '383', 'IAB12-3': '384', 'IAB3-10': '57', 'IAB2-13': '23', 'IAB9-20': '241', + 'IAB3-1': '58', 'IAB3-11': '58', 'IAB14-4': '191', 'IAB17-20': '520', 'IAB7-31': '228', 'IAB7-37': '301', 'IAB3-12': '107', 'IAB2-14': '13', 'IAB17-25': '519', 'IAB2-15': '27', 'IAB1-5': '324', + 'IAB1-6': '338', 'IAB9-16': '243', 'IAB13-8': '412', 'IAB12-2': '385', 'IAB9-21': '253', 'IAB8-6': '222', 'IAB7-32': '229', 'IAB2-16': '14', 'IAB17-23': '521', 'IAB13-9': '413', 'IAB17-24': '501', + 'IAB9-22': '254', 'IAB15-5': '244', 'IAB6-2': '196', 'IAB6-5': '197', 'IAB6-6': '198', 'IAB2-17': '24', 'IAB13-2': '405', 'IAB13': '391', 'IAB13-7': '410', 'IAB13-12': '415', 'IAB16': '422', + 'IAB9-23': '255', 'IAB7-36': '236', 'IAB15-6': '471', 'IAB2-18': '15', 'IAB11-4': '386', 'IAB1-2': '432', 'IAB5-9': '139', 'IAB6-7': '305', 'IAB5-12': '149', 'IAB5-13': '134', 'IAB19-4': '631', + 'IAB19-19': '631', 'IAB19-20': '631', 'IAB19-32': '631', 'IAB9-24': '245', 'IAB21': '441', 'IAB21-3': '451', 'IAB23': '453', 'IAB10-9': '276', 'IAB4-9': '130', 'IAB16-6': '429', 'IAB4-6': '129', + 'IAB13-10': '416', 'IAB2-19': '28', 'IAB17-28': '525', 'IAB9-25': '272', 'IAB17-29': '527', 'IAB17-30': '227', 'IAB17-31': '530', 'IAB22-1': '481', 'IAB15': '464', 'IAB9-26': '246', 'IAB9-27': '256', + 'IAB9-28': '267', 'IAB17-33': '502', 'IAB19-35': '627', 'IAB2-20': '4', 'IAB7-39': '307', 'IAB19-30': '605', 'IAB22': '473', 'IAB17-34': '503', 'IAB17-35': '531', 'IAB7-19': '309', 'IAB7-40': '310', + 'IAB19-6': '635', 'IAB7-41': '237', 'IAB17-36': '504', 'IAB17-44': '533', 'IAB20-23': '662', 'IAB15-7': '472', 'IAB20-24': '671', 'IAB5-14': '136', 'IAB6-8': '199', 'IAB17': '483', 'IAB9-29': '263', + 'IAB2-23': '5', 'IAB13-11': '414', 'IAB4-3': '395', 'IAB18': '552', 'IAB7-42': '311', 'IAB17-37': '505', 'IAB17-38': '537', 'IAB17-39': '538', 'IAB19-26': '636', 'IAB19': '596', 'IAB1-7': '640', + 'IAB17-40': '539', 'IAB7-43': '293', 'IAB20': '653', 'IAB8-16': '212', 'IAB8-17': '213', 'IAB16-7': '430', 'IAB9-30': '680', 'IAB17-41': '541', 'IAB17-42': '542', 'IAB17-43': '506', 'IAB15-10': '390', + 'IAB19-23': '607', 'IAB19-34': '629', 'IAB14-7': '165', 'IAB7-44': '231', 'IAB7-45': '238', 'IAB9-31': '257', 'IAB5-1': '135', 'IAB7-2': '301', 'IAB18-6': '580', 'IAB7-3': '297', 'IAB23-1': '453', + 'IAB8-1': '214', 'IAB7-6': '312', 'IAB7-7': '300', 'IAB7-8': '301', 'IAB13-1': '410', 'IAB7-9': '301', 'IAB15-9': '465', 'IAB7-10': '313', 'IAB3-4': '602', 'IAB20-8': '660', 'IAB8-3': '214', + 'IAB20-10': '660', 'IAB7-11': '314', 'IAB20-11': '660', 'IAB23-4': '459', 'IAB9-8': '270', 'IAB8-4': '214', 'IAB7-12': '306', 'IAB7-13': '313', 'IAB7-14': '287', 'IAB18-5': '575', 'IAB7-15': '315', + 'IAB8-7': '214', 'IAB19-11': '616', 'IAB7-16': '289', 'IAB7-18': '301', 'IAB7-20': '290', 'IAB20-13': '659', 'IAB7-21': '313', 'IAB18-3': '579', 'IAB13-3': '52', 'IAB20-15': '659', 'IAB8-11': '214', + 'IAB7-22': '318', 'IAB20-16': '659', 'IAB7-23': '313', 'IAB7': '223', 'IAB10-6': '634', 'IAB7-27': '318', 'IAB11-1': '388', 'IAB7-29': '318', 'IAB7-30': '304', 'IAB19-18': '619', 'IAB8-13': '214', + 'IAB20-19': '659', 'IAB20-20': '657', 'IAB8-14': '214', 'IAB18-4': '565', 'IAB23-9': '459', 'IAB11': '379', 'IAB8-15': '214', 'IAB20-21': '662', 'IAB17-21': '492', 'IAB17-22': '518', 'IAB12': '379', + 'IAB23-10': '453', 'IAB7-34': '301', 'IAB4-8': '395', 'IAB26-3': '608', 'IAB20-25': '151', 'IAB20-27': '659' +} + +export function convertSegment(segment) { + if (!segment) return {} + return { + id: D_IAB_ID[segment.id || segment.ID] + } +} + +/** + * map array of objects to segments + * @param {Array[{ID: string}]} normalizable + * @returns array of IAB "segments" + */ +export function pickSegments(normalizable) { + if (Array.isArray(normalizable) === false) return [] + return normalizable.map(convertSegment) + .filter(t => t.id) +} + +export const neuwoRtdModule = { + name: 'NeuwoRTDModule', + init, + getBidRequestData +} + +submodule('realTimeData', neuwoRtdModule) diff --git a/modules/neuwoRtdProvider.md b/modules/neuwoRtdProvider.md new file mode 100644 index 00000000000..2adead66d4e --- /dev/null +++ b/modules/neuwoRtdProvider.md @@ -0,0 +1,49 @@ +# Overview + +Module Name: Neuwo Rtd Provider +Module Type: Rtd Provider +Maintainer: neuwo.ai + +# Description + +The Neuwo AI RTD module is an advanced AI solution for real-time data processing in the field of contextual targeting and advertising. With its cutting-edge algorithms, it allows advertisers to target their audiences with the highest level of precision based on context, while also delivering a seamless user experience. + +The module provides advertiser with valuable insights and real-time contextual bidding capabilities. Whether you're a seasoned advertising professional or just starting out, Neuwo AI RTD module is the ultimate tool for contextual targeting and advertising. + +The benefit of Neuwo AI RTD module is that it provides an alternative solution for advertisers to target their audiences and deliver relevant advertisements, as the widespread use of cookies for tracking and targeting is becoming increasingly limited. + +The RTD module uses cutting-edge algorithms to process real-time data, allowing advertisers to target their audiences based on contextual information, such as segments, IAB Tiers and brand safety. The RTD module is designed to be flexible and scalable, making it an ideal solution for advertisers looking to stay ahead of the curve in the post-cookie era. + +Generate your token at: [https://neuwo.ai/generatetoken/] + +# Configuration + +```javascript + +const neuwoDataProvider = { + name: 'NeuwoRTDModule', + params: { + publicToken: '', + apiUrl: '' + } +} +pbjs.setConfig({realTimeData: { dataProviders: [ neuwoDataProvider ]}}) + +``` + +# Testing + +## Add development tools if necessary + +- Install node for npm +- run in prebid.js source folder: +`npm ci` +`npm i -g gulp-cli` + +## Serve + +`gulp serve --modules=rtdModule,neuwoRtdProvider,appnexusBidAdapter` + +- in your browser, navigate to: + +`http://localhost:9999/integrationExamples/gpt/neuwoRtdProvider_example.html` diff --git a/modules/nextMillenniumBidAdapter.js b/modules/nextMillenniumBidAdapter.js index 802a8ac25b0..6f9385094e7 100644 --- a/modules/nextMillenniumBidAdapter.js +++ b/modules/nextMillenniumBidAdapter.js @@ -1,4 +1,5 @@ import { + isArray, _each, createTrackPixelHtml, deepAccess, @@ -14,6 +15,7 @@ import { import CONSTANTS from '../src/constants.json'; import { BANNER, VIDEO } from '../src/mediaTypes.js'; +import {config} from '../src/config.js'; import * as events from '../src/events.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; @@ -23,17 +25,21 @@ const BIDDER_CODE = 'nextMillennium'; const ENDPOINT = 'https://pbs.nextmillmedia.com/openrtb2/auction'; const TEST_ENDPOINT = 'https://test.pbs.nextmillmedia.com/openrtb2/auction'; const SYNC_ENDPOINT = 'https://cookies.nextmillmedia.com/sync?'; +const REPORT_ENDPOINT = 'https://report2.hb.brainlyads.com/statistics/metric'; const TIME_TO_LIVE = 360; const VIDEO_PARAMS = [ 'api', 'linearity', 'maxduration', 'mimes', 'minduration', 'placement', 'playbackmethod', 'protocols', 'startdelay' ]; +const sendingDataStatistic = initSendingDataStatistic(); +events.on(CONSTANTS.EVENTS.AUCTION_INIT, auctionInitHandler); + const EXPIRENCE_WURL = 20 * 60000; const wurlMap = {}; +cleanWurl(); events.on(CONSTANTS.EVENTS.BID_WON, bidWonHandler); -cleanWurl(); export const spec = { code: BIDDER_CODE, @@ -82,6 +88,7 @@ export const spec = { }; const imp = { + id: bid.adUnitCode, ext: { prebid: { storedrequest: {id} @@ -135,6 +142,7 @@ export const spec = { const urlParameters = parseUrl(getWindowTop().location.href).search; const isTest = urlParameters['pbs'] && urlParameters['pbs'] === 'test'; + const params = bid.params; requests.push({ method: 'POST', @@ -146,6 +154,7 @@ export const spec = { }, bidId, + params, auctionId, }); }); @@ -160,6 +169,7 @@ export const spec = { _each(response.seatbid, (resp) => { _each(resp.bid, (bid) => { const requestId = bidRequest.bidId; + const params = bidRequest.params; const auctionId = bidRequest.auctionId; const wurl = deepAccess(bid, 'ext.prebid.events.win'); addWurl({auctionId, requestId, wurl}); @@ -168,12 +178,13 @@ export const spec = { const bidResponse = { requestId, + params, cpm: bid.price, width: bid.w, height: bid.h, creativeId: bid.adid, currency: response.cur, - netRevenue: false, + netRevenue: true, ttl: TIME_TO_LIVE, meta: { advertiserDomains: bid.adomain || [] @@ -199,24 +210,95 @@ export const spec = { getUserSyncs: function (syncOptions, responses, gdprConsent, uspConsent) { const pixels = []; - let syncUrl = SYNC_ENDPOINT; - if (gdprConsent && gdprConsent.gdprApplies) { - syncUrl += 'gdpr=1&gdpr_consent=' + gdprConsent.consentString; - } - if (uspConsent) { - syncUrl += 'us_privacy=' + uspConsent; - } + if (isArray(responses)) { + responses.forEach(response => { + if (syncOptions.pixelEnabled) { + deepAccess(response, 'body.ext.sync.image', []).forEach(imgUrl => { + pixels.push({ + type: 'image', + url: replaceUsersyncMacros(imgUrl, gdprConsent, uspConsent) + }); + }) + } - if (syncOptions.iframeEnabled) { - pixels.push({type: 'iframe', url: syncUrl + 'type=iframe'}); - } - if (syncOptions.pixelEnabled) { - pixels.push({type: 'image', url: syncUrl + 'type=image'}); + if (syncOptions.iframeEnabled) { + deepAccess(response, 'body.ext.sync.iframe', []).forEach(iframeUrl => { + pixels.push({ + type: 'iframe', + url: replaceUsersyncMacros(iframeUrl, gdprConsent, uspConsent) + }); + }) + } + }) } + if (!pixels.length) { + let syncUrl = SYNC_ENDPOINT; + if (gdprConsent && gdprConsent.gdprApplies) syncUrl += 'gdpr=1&gdpr_consent=' + gdprConsent.consentString + '&'; + if (uspConsent) syncUrl += 'us_privacy=' + uspConsent + '&'; + if (syncOptions.iframeEnabled) pixels.push({type: 'iframe', url: syncUrl + 'type=iframe'}); + if (syncOptions.pixelEnabled) pixels.push({type: 'image', url: syncUrl + 'type=image'}); + } return pixels; }, + + getUrlPixelMetric(eventName, bid) { + const bidder = bid.bidder || bid.bidderCode; + if (bidder != BIDDER_CODE) return; + + let params; + if (bid.params) { + params = Array.isArray(bid.params) ? bid.params : [bid.params]; + } else { + if (Array.isArray(bid.bids)) params = bid.bids.map(bidI => bidI.params); + }; + + if (!params.length) return; + + const placementIdsArray = []; + const groupIdsArray = []; + params.forEach(paramsI => { + if (paramsI.group_id) { + groupIdsArray.push(paramsI.group_id); + } else { + if (paramsI.placement_id) placementIdsArray.push(paramsI.placement_id); + }; + }); + + const placementIds = (placementIdsArray.length && `&placements=${placementIdsArray.join(';')}`) || ''; + const groupIds = (groupIdsArray.length && `&groups=${groupIdsArray.join(';')}`) || ''; + + if (!(groupIds || placementIds)) { + return; + }; + + const url = `${REPORT_ENDPOINT}?event=${eventName}&bidder=${bidder}&source=pbjs${groupIds}${placementIds}`; + + return url; + }, +}; + +function replaceUsersyncMacros(url, gdprConsent, uspConsent) { + const { consentString, gdprApplies } = gdprConsent || {}; + + if (gdprApplies) { + const gdpr = Number(gdprApplies); + url = url.replace('{{.GDPR}}', gdpr); + + if (gdpr == 1 && consentString && consentString.length > 0) { + url = url.replace('{{.GDPRConsent}}', consentString); + } + } else { + url = url.replace('{{.GDPR}}', 0); + url = url.replace('{{.GDPRConsent}}', ''); + } + + if (uspConsent) { + url = url.replace('{{.USPrivacy}}', uspConsent); + } + + return url; }; function getAdEl(bid) { @@ -338,6 +420,10 @@ function bidWonHandler(bid) { }; } +function auctionInitHandler() { + sendingDataStatistic.initEvents(); +} + function cleanWurl() { const dateNow = Date.now(); Object.keys(wurlMap).forEach(key => { @@ -349,4 +435,79 @@ function cleanWurl() { setTimeout(cleanWurl, 60000); } +function initSendingDataStatistic() { + class SendingDataStatistic { + eventNames = [ + CONSTANTS.EVENTS.BID_TIMEOUT, + CONSTANTS.EVENTS.BID_RESPONSE, + CONSTANTS.EVENTS.BID_REQUESTED, + CONSTANTS.EVENTS.NO_BID, + ]; + + disabledSending = false; + enabledSending = false; + eventHendlers = {}; + + initEvents() { + this.disabledSending = !!config.getBidderConfig()?.nextMillennium?.disabledSendingStatisticData; + if (this.disabledSending) { + this.removeEvents(); + } else { + this.createEvents(); + }; + } + + createEvents() { + if (this.enabledSending) return; + + this.enabledSending = true; + for (let eventName of this.eventNames) { + if (!this.eventHendlers[eventName]) { + this.eventHendlers[eventName] = this.eventHandler(eventName); + }; + + events.on(eventName, this.eventHendlers[eventName]); + }; + } + + removeEvents() { + if (!this.enabledSending) return; + + this.enabledSending = false; + for (let eventName of this.eventNames) { + if (!this.eventHendlers[eventName]) continue; + + events.off(eventName, this.eventHendlers[eventName]); + }; + } + + eventHandler(eventName) { + const eventHandlerFunc = this.getEventHandler(eventName); + if (eventName == CONSTANTS.EVENTS.BID_TIMEOUT) { + return bids => { + if (this.disabledSending || !Array.isArray(bids)) return; + + for (let bid of bids) { + eventHandlerFunc(bid); + }; + } + }; + + return eventHandlerFunc; + } + + getEventHandler(eventName) { + return bid => { + if (this.disabledSending) return; + + const url = spec.getUrlPixelMetric(eventName, bid); + if (!url) return; + triggerPixel(url); + }; + } + }; + + return new SendingDataStatistic(); +} + registerBidder(spec); diff --git a/modules/nexx360BidAdapter.js b/modules/nexx360BidAdapter.js index ff4ca77aac2..9a7541fdd96 100644 --- a/modules/nexx360BidAdapter.js +++ b/modules/nexx360BidAdapter.js @@ -1,254 +1,174 @@ import {config} from '../src/config.js'; -import * as utils from '../src/utils.js'; +import { deepAccess, deepSetValue, generateUUID, logError, logInfo } from '../src/utils.js'; +import {Renderer} from '../src/Renderer.js'; +import {getStorageManager} from '../src/storageManager.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; -import {BANNER, VIDEO} from '../src/mediaTypes.js'; +import {BANNER, NATIVE, VIDEO} from '../src/mediaTypes.js'; import {getGlobal} from '../src/prebidGlobal.js'; +import {ortbConverter} from '../libraries/ortbConverter/converter.js' +import { INSTREAM, OUTSTREAM } from '../src/video.js'; + +const OUTSTREAM_RENDERER_URL = 'https://acdn.adnxs.com/video/outstream/ANOutstreamVideo.js'; -const VIDEO_TARGETING = ['startdelay', 'mimes', 'minduration', 'maxduration', 'delivery', - 'startdelay', 'skip', 'playbackmethod', 'api', 'protocol', 'boxingallowed', 'maxextended', - 'linearity', 'delivery', 'protocols', 'placement', 'minbitrate', 'maxbitrate', 'battr', 'ext']; const BIDDER_CODE = 'nexx360'; const REQUEST_URL = 'https://fast.nexx360.io/booster'; - -const PAGE_VIEW_ID = utils.generateUUID(); - -const BIDDER_VERSION = '1.0'; - +const PAGE_VIEW_ID = generateUUID(); +const BIDDER_VERSION = '2.0'; const GVLID = 965; +const NEXXID_KEY = 'nexx360_storage'; -export const spec = { - code: BIDDER_CODE, - gvlid: GVLID, - aliases: [ - { code: 'revenuemaker' }, - { code: 'firstid-ssp' }, - ], - supportedMediaTypes: [BANNER, VIDEO], - isBidRequestValid, - buildRequests, - interpretResponse, - getUserSyncs, - // onBidWon, -}; - -registerBidder(spec); +const ALIASES = [ + { code: 'revenuemaker' }, + { code: 'first-id', gvlid: 1178 }, + { code: 'adwebone' }, + { code: 'league-m', gvlid: 965 } +]; -/** - * Determines whether or not the given bid request is valid. - * - * @param {BidRequest} bid The bid params to validate. - * @return boolean True if this is a valid bid, and false otherwise. - */ -function isBidRequestValid(bid) { - return !!bid.params.tagId || !!bid.params.videoTagId; -}; +export const storage = getStorageManager({ + bidderCode: BIDDER_CODE, +}); /** - * Make a server request from the list of BidRequests. - * - * @param {validBidRequests[]} - an array of bids - * @return ServerRequest Info describing the request to the server. - */ + * Get the NexxId + * @param + * @return {object | false } false if localstorageNotEnabled + */ -function buildRequests(bids, bidderRequest) { - const data = getBaseRequest(bids[0], bidderRequest); - bids.forEach((bid) => { - const impObject = createImpObject(bid); - if (isBannerBid(bid)) impObject.banner = getBannerObject(bid); - if (isVideoBid(bid)) impObject.video = getVideoObject(bid); - data.imp.push(impObject); - }); - return { - method: 'POST', - url: REQUEST_URL, - data, +export function getNexx360LocalStorage() { + if (!storage.localStorageIsEnabled()) { + logInfo(`localstorage not enabled for Nexx360`); + return false; } -} - -function createImpObject(bid) { - const floor = getFloor(bid, BANNER); - const imp = { - id: bid.bidId, - tagid: bid.adUnitCode, - ext: { - divId: bid.adUnitCode, - nexx360: { - videoTagId: bid.params.videoTagId, - tagId: bid.params.tagId, - allBids: bid.params.allBids === true, - } - } - }; - if (bid.params.customParams) { - utils.deepSetValue(imp, 'ext.customParams', bid.params.customParams); + const output = storage.getDataFromLocalStorage(NEXXID_KEY); + if (output === null) { + const nexx360Storage = { nexx360Id: generateUUID() }; + storage.setDataInLocalStorage(NEXXID_KEY, JSON.stringify(nexx360Storage)); + return nexx360Storage; } - enrichImp(imp, bid, floor); - return imp; -} - -function getBannerObject(bid) { - return { - format: toFormat(bid.mediaTypes.banner.sizes), - topframe: utils.inIframe() ? 0 : 1 - }; -} - -function getVideoObject(bid) { - let width, height; - const videoParams = utils.deepAccess(bid, `mediaTypes.video`); - const playerSize = utils.deepAccess(bid, 'mediaTypes.video.playerSize'); - const context = utils.deepAccess(bid, 'mediaTypes.video.context'); - // normalize config for video size - if (utils.isArray(bid.sizes) && bid.sizes.length === 2 && !utils.isArray(bid.sizes[0])) { - width = parseInt(bid.sizes[0], 10); - height = parseInt(bid.sizes[1], 10); - } else if (utils.isArray(bid.sizes) && utils.isArray(bid.sizes[0]) && bid.sizes[0].length === 2) { - width = parseInt(bid.sizes[0][0], 10); - height = parseInt(bid.sizes[0][1], 10); - } else if (utils.isArray(playerSize) && playerSize.length === 2) { - width = parseInt(playerSize[0], 10); - height = parseInt(playerSize[1], 10); + try { + return JSON.parse(output) + } catch (e) { + return false; } - const video = { - playerSize: [height, width], - context, - }; - - Object.keys(videoParams) - .filter(param => VIDEO_TARGETING.includes(param)) - .forEach(param => video[param] = videoParams[param]); - return video; -} - -function isVideoBid(bid) { - return utils.deepAccess(bid, 'mediaTypes.video'); -} - -function isBannerBid(bid) { - return utils.deepAccess(bid, 'mediaTypes.banner') || !isVideoBid(bid); } -function toFormat(sizes) { - return sizes.map((s) => { - return { w: s[0], h: s[1] }; - }); -} - -function getFloor(bid, mediaType) { - let floor = 0; - if (typeof bid.getFloor === 'function') { - const floorInfo = bid.getFloor({ - currency: 'USD', - mediaType: mediaType, - size: '*' - }); - - if (typeof floorInfo === 'object' && - floorInfo.currency === 'USD' && - !isNaN(parseFloat(floorInfo.floor))) { - floor = Math.max(floor, parseFloat(floorInfo.floor)); - } +function getAdContainer(container) { + if (document.getElementById(container)) { + return document.getElementById(container); } - - return floor; } -function enrichImp(imp, bid, floor) { - if (bid.params.customParams) { - utils.deepSetValue(imp, 'ext.customParams', bid.params.customParams); - } - if (floor > 0) { - imp.bidfloor = floor; - imp.bidfloorcur = 'USD'; - } else if (bid.params.customFloor) { - imp.bidfloor = bid.params.customFloor; - } - if (bid.ortb2Imp && bid.ortb2Imp.ext && bid.ortb2Imp.ext.data) { - imp.ext.data = bid.ortb2Imp.ext.data; - } -} - -function getBaseRequest(bid, bidderRequest) { - let req = { - id: bidderRequest.auctionId, - imp: [], - cur: [config.getConfig('currency.adServerCurrency') || 'USD'], - at: 1, - tmax: config.getConfig('bidderTimeout'), - site: { - page: bidderRequest.refererInfo.topmostLocation || bidderRequest.refererInfo.page, - domain: bidderRequest.refererInfo.domain, - }, - regs: { - coppa: (config.getConfig('coppa') === true || bid.params.coppa) ? 1 : 0, - }, - device: { - dnt: (utils.getDNT() || bid.params.doNotTrack) ? 1 : 0, - h: screen.height, - w: screen.width, - ua: window.navigator.userAgent, - language: window.navigator.language.split('-').shift() - }, - user: {}, - ext: { - source: 'prebid.js', - version: '$prebid.version$', - pageViewId: PAGE_VIEW_ID, - bidderVersion: BIDDER_VERSION, - } - }; - - if (bid.params.platform) { - utils.deepSetValue(req, 'ext.platform', bid.params.platform); - } - if (bid.params.response_template_name) { - utils.deepSetValue(req, 'ext.response_template_name', bid.params.response_template_name); - } - req.test = config.getConfig('debug') ? 1 : 0; - if (bidderRequest.gdprConsent) { - if (bidderRequest.gdprConsent.gdprApplies !== undefined) { - utils.deepSetValue(req, 'regs.ext.gdpr', bidderRequest.gdprConsent.gdprApplies === true ? 1 : 0); - } - if (bidderRequest.gdprConsent.consentString !== undefined) { - utils.deepSetValue(req, 'user.ext.consent', bidderRequest.gdprConsent.consentString); +const converter = ortbConverter({ + context: { + netRevenue: true, // or false if your adapter should set bidResponse.netRevenue = false + ttl: 90, // default bidResponse.ttl (when not specified in ORTB response.seatbid[].bid[].exp) + }, + imp(buildImp, bidRequest, context) { + // console.log(bidRequest, context); + const imp = buildImp(bidRequest, context); + const tagid = bidRequest.params.adUnitName ? bidRequest.params.adUnitName : bidRequest.adUnitCode; + deepSetValue(imp, 'tagid', tagid); + deepSetValue(imp, 'ext.adUnitCode', bidRequest.adUnitCode); + const divId = bidRequest.params.divId ? bidRequest.params.divId : bidRequest.adUnitCode; + deepSetValue(imp, 'ext.divId', divId); + const slotEl = getAdContainer(divId); + if (slotEl) { + deepSetValue(imp, 'ext.dimensions.slotW', slotEl.offsetWidth); + deepSetValue(imp, 'ext.dimensions.slotH', slotEl.offsetHeight); + deepSetValue(imp, 'ext.dimensions.cssMaxW', slotEl.style?.maxWidth); + deepSetValue(imp, 'ext.dimensions.cssMaxH', slotEl.style?.maxHeight); } - if (bidderRequest.gdprConsent.addtlConsent !== undefined) { - utils.deepSetValue(req, 'user.ext.ConsentedProvidersSettings.consented_providers', bidderRequest.gdprConsent.addtlConsent); + deepSetValue(imp, 'ext.nexx360', bidRequest.params.tagId); + deepSetValue(imp, 'ext.nexx360.tagId', bidRequest.params.tagId); + deepSetValue(imp, 'ext.nexx360.videoTagId', bidRequest.params.videoTagId); + deepSetValue(imp, 'ext.nexx360.allBids', bidRequest.params.allBids); + if (imp.video) { + const playerSize = deepAccess(bidRequest, 'mediaTypes.video.playerSize'); + const videoContext = deepAccess(bidRequest, 'mediaTypes.video.context'); + deepSetValue(imp, 'video.ext.playerSize', playerSize); + deepSetValue(imp, 'video.ext.context', videoContext); } + + if (bidRequest.params.adUnitName) deepSetValue(imp, 'ext.adUnitName', bidRequest.params.adUnitName); + if (bidRequest.params.adUnitPath) deepSetValue(imp, 'ext.adUnitPath', bidRequest.params.adUnitPath); + return imp; + }, + request(buildRequest, imps, bidderRequest, context) { + const request = buildRequest(imps, bidderRequest, context); + const nexx360LocalStorage = getNexx360LocalStorage(); + if (nexx360LocalStorage) deepSetValue(request, 'ext.nexx360Id', nexx360LocalStorage.nexx360Id); + deepSetValue(request, 'ext.version', '$prebid.version$'); + deepSetValue(request, 'ext.source', 'prebid.js'); + deepSetValue(request, 'ext.pageViewId', PAGE_VIEW_ID); + deepSetValue(request, 'ext.bidderVersion', BIDDER_VERSION); + deepSetValue(request, 'cur', [config.getConfig('currency.adServerCurrency') || 'USD']); + if (!request.user) deepSetValue(request, 'user', {}); + return request; + }, +}); + +/** + * Determines whether or not the given bid request is valid. + * + * @param {BidRequest} bid The bid params to validate. + * @return boolean True if this is a valid bid, and false otherwise. + */ +function isBidRequestValid(bid) { + if (bid.params.adUnitName && (typeof bid.params.adUnitName !== 'string' || bid.params.adUnitName === '')) { + logError('bid.params.adUnitName needs to be a string'); + return false; } - if (bidderRequest.uspConsent) { - utils.deepSetValue(req, 'regs.ext.us_privacy', bidderRequest.uspConsent); + if (bid.params.adUnitPath && (typeof bid.params.adUnitPath !== 'string' || bid.params.adUnitPath === '')) { + logError('bid.params.adUnitPath needs to be a string'); + return false; } - if (bid.schain) { - utils.deepSetValue(req, 'source.ext.schain', bid.schain); + if (bid.params.divId && (typeof bid.params.divId !== 'string' || bid.params.divId === '')) { + logError('bid.params.divId needs to be a string'); + return false; } - if (bid.userIdAsEids) { - utils.deepSetValue(req, 'user.ext.eids', bid.userIdAsEids); + if (bid.params.allBids && typeof bid.params.allBids !== 'boolean') { + logError('bid.params.allBids needs to be a boolean'); + return false; } - const commonFpd = bidderRequest.ortb2 || {}; - if (commonFpd.site) { - utils.mergeDeep(req, {site: commonFpd.site}); + if (!bid.params.tagId && !bid.params.videoTagId && !bid.params.nativeTagId) { + logError('bid.params.tagId or bid.params.videoTagId or bid.params.nativeTagId must be defined'); + return false; } - if (commonFpd.user) { - utils.mergeDeep(req, {user: commonFpd.user}); + return true; +}; + +/** + * Make a server request from the list of BidRequests. + * + * @param {validBidRequests[]} - an array of bids + * @return ServerRequest Info describing the request to the server. + */ + +function buildRequests(bidRequests, bidderRequest) { + const data = converter.toORTB({bidRequests, bidderRequest}) + return { + method: 'POST', + url: REQUEST_URL, + data, } - return req; } /** - * Unpack the response from the server into a list of bids. - * - * @param {ServerResponse} serverResponse A successful response from the server. - * @return {Bid[]} An array of bids which were nested inside the server. - */ -function interpretResponse(response, req) { - const { bidderSettings } = getGlobal(); - const allowAlternateBidderCodes = bidderSettings && bidderSettings.standard ? bidderSettings.standard.allowAlternateBidderCodes : false; - const respBody = response.body; + * Unpack the response from the server into a list of bids. + * + * @param {ServerResponse} serverResponse A successful response from the server. + * @return {Bid[]} An array of bids which were nested inside the server. + */ + +function interpretResponse(serverResponse) { + const respBody = serverResponse.body; if (!respBody || !Array.isArray(respBody.seatbid)) { return []; } + const { bidderSettings } = getGlobal(); + const allowAlternateBidderCodes = bidderSettings && bidderSettings.standard ? bidderSettings.standard.allowAlternateBidderCodes : false; + let bids = []; respBody.seatbid.forEach(seatbid => { bids = [...bids, ...seatbid.bid.map(bid => { @@ -259,25 +179,30 @@ function interpretResponse(response, req) { height: bid.h, creativeId: bid.crid, dealId: bid.dealid, - currency: respBody.cur || 'USD', + currency: respBody.cur, netRevenue: true, ttl: 120, - bidderCode: allowAlternateBidderCodes ? `n360-${bid.ssp}` : 'nexx360', - mediaType: bid.type === 'banner' ? 'banner' : 'video', - meta: { advertiserDomains: bid.adomain }, + mediaType: [OUTSTREAM, INSTREAM].includes(bid.ext.mediaType) ? 'video' : bid.ext.mediaType, + meta: { + advertiserDomains: bid.adomain, + demandSource: bid.ext.ssp, + }, }; - // if (bid.dealid) response.dealid = bid.dealid; - - if (response.mediaType === 'banner') { - response.adUrl = bid.adUrl; - } + if (allowAlternateBidderCodes) response.bidderCode = `n360-${bid.ext.ssp}`; - if (['instream', 'outstream'].includes(bid.type)) response.vastXml = bid.vastXml; + if (bid.ext.mediaType === BANNER) response.adUrl = bid.ext.adUrl; + if ([INSTREAM, OUTSTREAM].includes(bid.ext.mediaType)) response.vastXml = bid.ext.vastXml; - if (bid.ext) { - response.meta.networkId = bid.ext.dsp_id; - response.meta.advertiserId = bid.ext.buyer_id; - response.meta.brandId = bid.ext.brand_id; + if (bid.ext.mediaType === OUTSTREAM) { + response.renderer = createRenderer(bid, OUTSTREAM_RENDERER_URL); + response.divId = bid.ext.divId + }; + if (bid.ext.mediaType === NATIVE) { + try { + response.native = { + ortb: JSON.parse(bid.adm) + } + } catch (e) {} } return response; })]; @@ -286,17 +211,65 @@ function interpretResponse(response, req) { } /** - * Register the user sync pixels which should be dropped after the auction. - * - * @param {SyncOptions} syncOptions Which user syncs are allowed? - * @param {ServerResponse[]} serverResponses List of server's responses. - * @return {UserSync[]} The user syncs which should be dropped. - */ + * Register the user sync pixels which should be dropped after the auction. + * + * @param {SyncOptions} syncOptions Which user syncs are allowed? + * @param {ServerResponse[]} serverResponses List of server's responses. + * @return {UserSync[]} The user syncs which should be dropped. + */ function getUserSyncs(syncOptions, serverResponses, gdprConsent, uspConsent) { - if (typeof serverResponses === 'object' && serverResponses != null && serverResponses.length > 0 && serverResponses[0].hasOwnProperty('body') && - serverResponses[0].body.hasOwnProperty('cookies') && typeof serverResponses[0].body.cookies === 'object') { - return serverResponses[0].body.cookies.slice(0, 5); + if (typeof serverResponses === 'object' && + serverResponses != null && + serverResponses.length > 0 && + serverResponses[0].hasOwnProperty('body') && + serverResponses[0].body.hasOwnProperty('ext') && + serverResponses[0].body.ext.hasOwnProperty('cookies') && + typeof serverResponses[0].body.ext.cookies === 'object') { + return serverResponses[0].body.ext.cookies.slice(0, 5); } else { return []; } }; + +function outstreamRender(response) { + response.renderer.push(() => { + window.ANOutstreamVideo.renderAd({ + sizes: [response.width, response.height], + targetId: response.divId, + adResponse: response.vastXml, + rendererOptions: { + showBigPlayButton: false, + showProgressBar: 'bar', + showVolume: false, + allowFullscreen: true, + skippable: false, + content: response.vastXml + } + }); + }); +} + +function createRenderer(bid, url) { + const renderer = Renderer.install({ + id: bid.id, + url: url, + loaded: false, + adUnitCode: bid.ext.adUnitCode, + targetId: bid.ext.divId, + }); + renderer.setRender(outstreamRender); + return renderer; +} + +export const spec = { + code: BIDDER_CODE, + gvlid: GVLID, + aliases: ALIASES, + supportedMediaTypes: [BANNER, VIDEO, NATIVE], + isBidRequestValid, + buildRequests, + interpretResponse, + getUserSyncs, +}; + +registerBidder(spec); diff --git a/modules/nobidBidAdapter.js b/modules/nobidBidAdapter.js index 20878b545e4..7bf1c4b80db 100644 --- a/modules/nobidBidAdapter.js +++ b/modules/nobidBidAdapter.js @@ -7,8 +7,8 @@ import {hasPurpose1Consent} from '../src/utils/gpdr.js'; const GVLID = 816; const BIDDER_CODE = 'nobid'; -const storage = getStorageManager({gvlid: GVLID, bidderCode: BIDDER_CODE}); -window.nobidVersion = '1.3.2'; +const storage = getStorageManager({bidderCode: BIDDER_CODE}); +window.nobidVersion = '1.3.3'; window.nobid = window.nobid || {}; window.nobid.bidResponses = window.nobid.bidResponses || {}; window.nobid.timeoutTotal = 0; @@ -175,6 +175,9 @@ function nobidBuildRequests(bids, bidderRequest) { if (adunitObject.div) { a.d = adunitObject.div; } + if (adunitObject.floor) { + a.floor = adunitObject.floor; + } if (adunitObject.targeting) { a.g = adunitObject.targeting; } else { @@ -201,6 +204,12 @@ function nobidBuildRequests(bids, bidderRequest) { adunits.push(a); return adunits; } + function getFloor (bid) { + if (bid && typeof bid.getFloor === 'function' && bid.getFloor().floor) { + return bid.getFloor().floor; + } + return null; + } if (typeof window.nobid.refreshLimit !== 'undefined') { if (window.nobid.refreshLimit < window.nobid.refreshCount) return false; } @@ -227,6 +236,7 @@ function nobidBuildRequests(bids, bidderRequest) { if (bid.mediaType === VIDEO || (videoMediaType && (context === 'instream' || context === 'outstream'))) { adType = 'video'; } + const floor = getFloor(bid); if (siteId) { newAdunit({ @@ -235,7 +245,8 @@ function nobidBuildRequests(bids, bidderRequest) { siteId: siteId, placementId: placementId, ad_type: adType, - params: bid.params + params: bid.params, + floor: floor }, adunits); } diff --git a/modules/nobidBidAdapter.md b/modules/nobidBidAdapter.md index 9e47aa5f43f..4449ad5c88b 100644 --- a/modules/nobidBidAdapter.md +++ b/modules/nobidBidAdapter.md @@ -4,7 +4,7 @@ title: Nobid description: Prebid Nobid Bidder Adaptor biddercode: nobid hide: true -media_types: banner +media_types: banner, video gdpr_supported: true usp_supported: true --- @@ -51,4 +51,4 @@ usp_supported: true ] } ]; -``` \ No newline at end of file +``` diff --git a/modules/novatiqIdSystem.js b/modules/novatiqIdSystem.js index 7bd1ee8acd9..7a801a945ae 100644 --- a/modules/novatiqIdSystem.js +++ b/modules/novatiqIdSystem.js @@ -8,7 +8,10 @@ import { logInfo, getWindowLocation } from '../src/utils.js'; import { ajax } from '../src/ajax.js'; import { submodule } from '../src/hook.js'; -import { getStorageManager } from '../src/storageManager.js'; +import {getStorageManager} from '../src/storageManager.js'; +import {MODULE_TYPE_UID} from '../src/activities/modules.js'; + +const MODULE_NAME = 'novatiq'; /** @type {Submodule} */ export const novatiqIdSubmodule = { @@ -17,7 +20,7 @@ export const novatiqIdSubmodule = { * used to link submodule with config * @type {string} */ - name: 'novatiq', + name: MODULE_NAME, /** * used to specify vendor id * @type {number} @@ -35,6 +38,16 @@ export const novatiqIdSubmodule = { snowflake: novatiqId } }; + + if (novatiqId.syncResponse !== undefined) { + responseObj.novatiq.ext = {}; + responseObj.novatiq.ext.syncResponse = novatiqId.syncResponse; + } + + if (typeof config != 'undefined' && typeof config.params !== 'undefined' && typeof config.params.removeAdditionalInfo !== 'undefined' && config.params.removeAdditionalInfo === true) { + delete responseObj.novatiq.snowflake.syncResponse; + } + return responseObj; }, @@ -120,7 +133,7 @@ export const novatiqIdSubmodule = { getNovatiqId(urlParams) { // standard uuid format let uuidFormat = [1e7] + -1e3 + -4e3 + -8e3 + -1e11; - if (urlParams.useStandardUuid == false) { + if (urlParams.useStandardUuid === false) { // novatiq standard uuid(like) format uuidFormat = uuidFormat + 1e3; } @@ -207,7 +220,7 @@ export const novatiqIdSubmodule = { let sharedId = null; if (this.useSharedId(configParams)) { let cookieOrStorageID = this.getCookieOrStorageID(configParams); - const storage = getStorageManager({gvlid: this.gvlid, moduleName: 'pubCommonId'}); + const storage = getStorageManager({moduleType: MODULE_TYPE_UID, moduleName: MODULE_NAME}); // first check local storage if (storage.hasLocalStorage()) { diff --git a/modules/oguryBidAdapter.js b/modules/oguryBidAdapter.js index da62ce5c0a1..d09320c00fe 100644 --- a/modules/oguryBidAdapter.js +++ b/modules/oguryBidAdapter.js @@ -11,7 +11,7 @@ const DEFAULT_TIMEOUT = 1000; const BID_HOST = 'https://mweb-hb.presage.io/api/header-bidding-request'; const TIMEOUT_MONITORING_HOST = 'https://ms-ads-monitoring-events.presage.io'; const MS_COOKIE_SYNC_DOMAIN = 'https://ms-cookie-sync.presage.io'; -const ADAPTER_VERSION = '1.4.0'; +const ADAPTER_VERSION = '1.4.1'; function getClientWidth() { const documentElementClientWidth = window.top.document.documentElement.clientWidth @@ -90,7 +90,8 @@ function buildRequests(validBidRequests, bidderRequest) { }, device: { w: getClientWidth(), - h: getClientHeight() + h: getClientHeight(), + pxratio: window.devicePixelRatio } }; diff --git a/modules/onetagBidAdapter.js b/modules/onetagBidAdapter.js index 40ef6d596be..2ae879cdcbc 100644 --- a/modules/onetagBidAdapter.js +++ b/modules/onetagBidAdapter.js @@ -3,7 +3,7 @@ import { BANNER, VIDEO } from '../src/mediaTypes.js'; import { INSTREAM, OUTSTREAM } from '../src/video.js'; import { Renderer } from '../src/Renderer.js'; -import {find} from '../src/polyfill.js'; +import { find } from '../src/polyfill.js'; import { getStorageManager } from '../src/storageManager.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; import { deepClone, logError, deepAccess } from '../src/utils.js'; @@ -13,7 +13,7 @@ const USER_SYNC_ENDPOINT = 'https://onetag-sys.com/usync/'; const BIDDER_CODE = 'onetag'; const GVLID = 241; -const storage = getStorageManager({gvlid: GVLID, bidderCode: BIDDER_CODE}); +const storage = getStorageManager({ bidderCode: BIDDER_CODE }); /** * Determines whether or not the given bid request is valid. @@ -53,7 +53,7 @@ export function isValid(type, bid) { function buildRequests(validBidRequests, bidderRequest) { const payload = { bids: requestsToBids(validBidRequests), - ...getPageInfo() + ...getPageInfo(bidderRequest) }; if (bidderRequest && bidderRequest.gdprConsent) { payload.gdprConsent = { @@ -61,6 +61,12 @@ function buildRequests(validBidRequests, bidderRequest) { consentRequired: bidderRequest.gdprConsent.gdprApplies }; } + if (bidderRequest && bidderRequest.gppConsent) { + payload.gppConsent = { + consentString: bidderRequest.gppConsent.gppString, + applicableSections: bidderRequest.gppConsent.applicableSections + } + } if (bidderRequest && bidderRequest.uspConsent) { payload.usPrivacy = bidderRequest.uspConsent; } @@ -74,7 +80,10 @@ function buildRequests(validBidRequests, bidderRequest) { if (storage.hasLocalStorage()) { payload.onetagSid = storage.getDataFromLocalStorage('onetag_sid'); } - } catch (e) {} + } catch (e) { } + const connection = navigator.connection || navigator.webkitConnection; + payload.networkConnectionType = (connection && connection.type) ? connection.type : null; + payload.networkEffectiveConnectionType = (connection && connection.effectiveType) ? connection.effectiveType : null; return { method: 'POST', url: ENDPOINT, @@ -112,7 +121,7 @@ function interpretResponse(serverResponse, bidderRequest) { if (bid.mediaType === BANNER) { responseBid.ad = bid.ad; } else if (bid.mediaType === VIDEO) { - const {context, adUnitCode} = find(requestData.bids, (item) => + const { context, adUnitCode } = find(requestData.bids, (item) => item.bidId === bid.requestId && item.type === VIDEO ); @@ -141,7 +150,7 @@ function createRenderer(bid, rendererOptions = {}) { loaded: false }); try { - renderer.setRender(({renderer, width, height, vastXml, adUnitCode}) => { + renderer.setRender(({ renderer, width, height, vastXml, adUnitCode }) => { renderer.push(() => { window.onetag.Player.init({ ...bid, @@ -162,7 +171,6 @@ function createRenderer(bid, rendererOptions = {}) { function getFrameNesting() { let topmostFrame = window; let parent = window.parent; - let currentFrameNesting = 0; try { while (topmostFrame !== topmostFrame.parent) { parent = topmostFrame.parent; @@ -170,13 +178,8 @@ function getFrameNesting() { parent.location.href; topmostFrame = topmostFrame.parent; } - } catch (e) { - currentFrameNesting = parent === topmostFrame.top ? 1 : 2; - } - return { - topmostFrame, - currentFrameNesting - } + } catch (e) { } + return topmostFrame; } function getDocumentVisibility(window) { @@ -197,21 +200,15 @@ function getDocumentVisibility(window) { /** * Returns information about the page needed by the server in an object to be converted in JSON - * @returns {{location: *, referrer: (*|string), masked: *, wWidth: (*|Number), wHeight: (*|Number), sWidth, sHeight, date: string, timeOffset: number}} + * @returns {{location: *, referrer: (*|string), stack: (*|Array.), numIframes: (*|Number), wWidth: (*|Number), wHeight: (*|Number), sWidth, sHeight, date: string, timeOffset: number}} */ -function getPageInfo() { - const { topmostFrame, currentFrameNesting } = getFrameNesting(); +function getPageInfo(bidderRequest) { + const topmostFrame = getFrameNesting(); return { - location: topmostFrame.location.href, - referrer: - topmostFrame.document.referrer !== '' - ? topmostFrame.document.referrer - : null, - ancestorOrigin: - window.location.ancestorOrigins && window.location.ancestorOrigins.length > 0 - ? window.location.ancestorOrigins[window.location.ancestorOrigins.length - 1] - : null, - masked: currentFrameNesting, + location: deepAccess(bidderRequest, 'refererInfo.page', null), + referrer: deepAccess(bidderRequest, 'refererInfo.ref', null), + stack: deepAccess(bidderRequest, 'refererInfo.stack', []), + numIframes: deepAccess(bidderRequest, 'refererInfo.numIframes', 0), wWidth: topmostFrame.innerWidth, wHeight: topmostFrame.innerHeight, oWidth: topmostFrame.outerWidth, @@ -230,7 +227,7 @@ function getPageInfo() { timing: getTiming(), version: { prebid: '$prebid.version$', - adapter: '1.1.0' + adapter: '1.1.1' } }; } @@ -344,12 +341,12 @@ function getSizes(sizes) { const ret = []; for (let i = 0; i < sizes.length; i++) { const size = sizes[i]; - ret.push({width: size[0], height: size[1]}) + ret.push({ width: size[0], height: size[1] }) } return ret; } -function getUserSyncs(syncOptions, serverResponses, gdprConsent, uspConsent) { +function getUserSyncs(syncOptions, serverResponses, gdprConsent, uspConsent, gppConsent) { let syncs = []; let params = ''; if (gdprConsent) { @@ -360,6 +357,11 @@ function getUserSyncs(syncOptions, serverResponses, gdprConsent, uspConsent) { params += '&gdpr_consent=' + gdprConsent.consentString; } } + if (gppConsent) { + if (typeof gppConsent.gppString === 'string') { + params += '&gpp_consent=' + gppConsent.gppString; + } + } if (uspConsent && typeof uspConsent === 'string') { params += '&us_privacy=' + uspConsent; } diff --git a/modules/onomagicBidAdapter.js b/modules/onomagicBidAdapter.js index d99455f3f73..edab625e541 100644 --- a/modules/onomagicBidAdapter.js +++ b/modules/onomagicBidAdapter.js @@ -13,7 +13,6 @@ import { } from '../src/utils.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import {BANNER} from '../src/mediaTypes.js'; -import {config} from '../src/config.js'; const BIDDER_CODE = 'onomagic'; const URL = 'https://bidder.onomagic.com/hb'; @@ -80,7 +79,7 @@ function buildRequests(bidReqs, bidderRequest) { w: screen.width, h: screen.height }, - tmax: config.getConfig('bidderTimeout') + tmax: bidderRequest?.timeout }; return { diff --git a/modules/openxBidAdapter.js b/modules/openxBidAdapter.js index 14525cd0cfc..03423a028b4 100644 --- a/modules/openxBidAdapter.js +++ b/modules/openxBidAdapter.js @@ -1,604 +1,238 @@ -import { - _each, - _map, - convertTypes, - deepAccess, - deepSetValue, - inIframe, - isArray, - parseSizesInput, - parseUrl -} from '../src/utils.js'; import {config} from '../src/config.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; +import * as utils from '../src/utils.js'; +import {mergeDeep} from '../src/utils.js'; import {BANNER, VIDEO} from '../src/mediaTypes.js'; -import {includes} from '../src/polyfill.js'; +import {ortbConverter} from '../libraries/ortbConverter/converter.js'; -const SUPPORTED_AD_TYPES = [BANNER, VIDEO]; -const VIDEO_TARGETING = ['startdelay', 'mimes', 'minduration', 'maxduration', - 'startdelay', 'skippable', 'playbackmethod', 'api', 'protocols', 'boxingallowed', - 'linearity', 'delivery', 'protocol', 'placement', 'minbitrate', 'maxbitrate']; -const BIDDER_CODE = 'openx'; -const BIDDER_CONFIG = 'hb_pb'; -const BIDDER_VERSION = '3.0.3'; - -const DEFAULT_CURRENCY = 'USD'; - -export const USER_ID_CODE_TO_QUERY_ARG = { - britepoolid: 'britepoolid', // BritePool ID - criteoId: 'criteoid', // CriteoID - fabrickId: 'nuestarid', // Fabrick ID by Nuestar - hadronId: 'audigentid', // Hadron ID from Audigent - id5id: 'id5id', // ID5 ID - idl_env: 'lre', // LiveRamp IdentityLink - IDP: 'zeotapid', // zeotapIdPlus ID+ - idxId: 'idxid', // idIDx, - intentIqId: 'intentiqid', // IntentIQ ID - lipb: 'lipbid', // LiveIntent ID - lotamePanoramaId: 'lotameid', // Lotame Panorama ID - merkleId: 'merkleid', // Merkle ID - netId: 'netid', // netID - parrableId: 'parrableid', // Parrable ID - pubcid: 'pubcid', // PubCommon ID - quantcastId: 'quantcastid', // Quantcast ID - tapadId: 'tapadid', // Tapad Id - tdid: 'ttduuid', // The Trade Desk Unified ID - uid2: 'uid2', // Unified ID 2.0 - admixerId: 'admixerid', // AdMixer ID - deepintentId: 'deepintentid', // DeepIntent ID - dmdId: 'dmdid', // DMD Marketing Corp ID - nextrollId: 'nextrollid', // NextRoll ID - novatiq: 'novatiqid', // Novatiq ID - mwOpenLinkId: 'mwopenlinkid', // MediaWallah OpenLink ID - dapId: 'dapid', // Akamai DAP ID - amxId: 'amxid', // AMX RTB ID - kpuid: 'kpuid', // Kinesso ID - publinkId: 'publinkid', // Publisher Link - naveggId: 'naveggid', // Navegg ID - imuid: 'imuid', // IM-UID by Intimate Merger - adtelligentId: 'adtelligentid' // Adtelligent ID +const bidderConfig = 'hb_pb_ortb'; +const bidderVersion = '2.0'; +export const REQUEST_URL = 'https://rtb.openx.net/openrtbb/prebidjs'; +export const SYNC_URL = 'https://u.openx.net/w/1.0/pd'; +export const DEFAULT_PH = '2d1251ae-7f3a-47cf-bd2a-2f288854a0ba'; +export const spec = { + code: 'openx', + supportedMediaTypes: [BANNER, VIDEO], + isBidRequestValid, + buildRequests, + interpretResponse, + getUserSyncs, + transformBidParams }; -export const spec = { - code: BIDDER_CODE, - gvlid: 69, - supportedMediaTypes: SUPPORTED_AD_TYPES, - isBidRequestValid: function (bidRequest) { - const hasDelDomainOrPlatform = bidRequest.params.delDomain || bidRequest.params.platform; - if (deepAccess(bidRequest, 'mediaTypes.banner') && hasDelDomainOrPlatform) { - return !!bidRequest.params.unit || deepAccess(bidRequest, 'mediaTypes.banner.sizes.length') > 0; - } +registerBidder(spec); - return !!(bidRequest.params.unit && hasDelDomainOrPlatform); +const converter = ortbConverter({ + context: { + netRevenue: true, + ttl: 300 }, - buildRequests: function (bidRequests, bidderRequest) { - if (bidRequests.length === 0) { - return []; - } - - let requests = []; - let [videoBids, bannerBids] = partitionByVideoBids(bidRequests); - - // build banner requests - if (bannerBids.length > 0) { - requests.push(buildOXBannerRequest(bannerBids, bidderRequest)); - } - // build video requests - if (videoBids.length > 0) { - videoBids.forEach(videoBid => { - requests.push(buildOXVideoRequest(videoBid, bidderRequest)) - }); + imp(buildImp, bidRequest, context) { + const imp = buildImp(bidRequest, context); + mergeDeep(imp, { + tagid: bidRequest.params.unit, + ext: { + divid: bidRequest.adUnitCode + } + }); + if (bidRequest.params.customParams) { + utils.deepSetValue(imp, 'ext.customParams', bidRequest.params.customParams); } - - return requests; - }, - interpretResponse: function ({body: oxResponseObj}, serverRequest) { - let mediaType = getMediaTypeFromRequest(serverRequest); - - return mediaType === VIDEO ? createVideoBidResponses(oxResponseObj, serverRequest.payload) - : createBannerBidResponses(oxResponseObj, serverRequest.payload); - }, - getUserSyncs: function (syncOptions, responses, gdprConsent, uspConsent) { - if (syncOptions.iframeEnabled || syncOptions.pixelEnabled) { - let pixelType = syncOptions.iframeEnabled ? 'iframe' : 'image'; - let url = deepAccess(responses, '0.body.ads.pixels') || - deepAccess(responses, '0.body.pixels') || - generateDefaultSyncUrl(gdprConsent, uspConsent); - - return [{ - type: pixelType, - url: url - }]; + if (bidRequest.params.customFloor && !imp.bidfloor) { + imp.bidfloor = bidRequest.params.customFloor; } + return imp; }, - transformBidParams: function(params, isOpenRtb) { - return convertTypes({ - 'unit': 'string', - 'customFloor': 'number' - }, params); - } -}; - -function generateDefaultSyncUrl(gdprConsent, uspConsent) { - let url = 'https://u.openx.net/w/1.0/pd'; - let queryParamStrings = []; - - if (gdprConsent) { - queryParamStrings.push('gdpr=' + (gdprConsent.gdprApplies ? 1 : 0)); - queryParamStrings.push('gdpr_consent=' + encodeURIComponent(gdprConsent.consentString || '')); - } - - // CCPA - if (uspConsent) { - queryParamStrings.push('us_privacy=' + encodeURIComponent(uspConsent)); - } - - return `${url}${queryParamStrings.length > 0 ? '?' + queryParamStrings.join('&') : ''}`; -} - -function isVideoRequest(bidRequest) { - return (deepAccess(bidRequest, 'mediaTypes.video') && !deepAccess(bidRequest, 'mediaTypes.banner')) || bidRequest.mediaType === VIDEO; -} - -function createBannerBidResponses(oxResponseObj, {bids, startTime}) { - let adUnits = oxResponseObj.ads.ad; - let bidResponses = []; - for (let i = 0; i < adUnits.length; i++) { - let adUnit = adUnits[i]; - let adUnitIdx = parseInt(adUnit.idx, 10); - let bidResponse = {}; - - bidResponse.requestId = bids[adUnitIdx].bidId; - - if (adUnit.pub_rev) { - bidResponse.cpm = Number(adUnit.pub_rev) / 1000; - } else { - // No fill, do not add the bidresponse - continue; - } - let creative = adUnit.creative[0]; - if (creative) { - bidResponse.width = creative.width; - bidResponse.height = creative.height; + request(buildRequest, imps, bidderRequest, context) { + const req = buildRequest(imps, bidderRequest, context); + mergeDeep(req, { + at: 1, + ext: { + bc: `${bidderConfig}_${bidderVersion}` + } + }) + const bid = context.bidRequests[0]; + if (bid.params.coppa) { + utils.deepSetValue(req, 'regs.coppa', 1); } - bidResponse.creativeId = creative.id; - bidResponse.ad = adUnit.html; - if (adUnit.deal_id) { - bidResponse.dealId = adUnit.deal_id; + if (bid.params.doNotTrack) { + utils.deepSetValue(req, 'device.dnt', 1); } - // default 5 mins - bidResponse.ttl = 300; - // true is net, false is gross - bidResponse.netRevenue = true; - bidResponse.currency = adUnit.currency; - - // additional fields to add - if (adUnit.tbd) { - bidResponse.tbd = adUnit.tbd; + if (bid.params.platform) { + utils.deepSetValue(req, 'ext.platform', bid.params.platform); } - bidResponse.ts = adUnit.ts; - - bidResponse.meta = {}; - if (adUnit.brand_id) { - bidResponse.meta.brandId = adUnit.brand_id; + if (bid.params.delDomain) { + utils.deepSetValue(req, 'ext.delDomain', bid.params.delDomain); } - - if (adUnit.adomain && length(adUnit.adomain) > 0) { - bidResponse.meta.advertiserDomains = adUnit.adomain; - } else { - bidResponse.meta.advertiserDomains = []; + if (bid.params.response_template_name) { + utils.deepSetValue(req, 'ext.response_template_name', bid.params.response_template_name); } - - if (adUnit.adv_id) { - bidResponse.meta.dspid = adUnit.adv_id; + if (bid.params.test) { + req.test = 1 } - - bidResponses.push(bidResponse); - } - return bidResponses; -} - -function getViewportDimensions(isIfr) { - let width; - let height; - let tWin = window; - let tDoc = document; - let docEl = tDoc.documentElement; - let body; - - if (isIfr) { - try { - tWin = window.top; - tDoc = window.top.document; - } catch (e) { - return; + return req; + }, + bidResponse(buildBidResponse, bid, context) { + const bidResponse = buildBidResponse(bid, context); + if (bid.ext) { + bidResponse.meta.networkId = bid.ext.dsp_id; + bidResponse.meta.advertiserId = bid.ext.buyer_id; + bidResponse.meta.brandId = bid.ext.brand_id; + } + const {ortbResponse} = context; + if (ortbResponse.ext && ortbResponse.ext.paf) { + bidResponse.meta.paf = Object.assign({}, ortbResponse.ext.paf); + bidResponse.meta.paf.content_id = utils.deepAccess(bid, 'ext.paf.content_id'); + } + return bidResponse; + }, + response(buildResponse, bidResponses, ortbResponse, context) { + // pass these from request to the responses for use in userSync + const {ortbRequest} = context; + if (ortbRequest.ext) { + if (ortbRequest.ext.delDomain) { + utils.deepSetValue(ortbResponse, 'ext.delDomain', ortbRequest.ext.delDomain); + } + if (ortbRequest.ext.platform) { + utils.deepSetValue(ortbResponse, 'ext.platform', ortbRequest.ext.platform); + } } - body = tDoc.body; - - width = tWin.innerWidth || docEl.clientWidth || body.clientWidth; - height = tWin.innerHeight || docEl.clientHeight || body.clientHeight; - } else { - width = tWin.innerWidth || docEl.clientWidth; - height = tWin.innerHeight || docEl.clientHeight; - } - - return `${width}x${height}`; -} - -function formatCustomParms(customKey, customParams) { - let value = customParams[customKey]; - if (isArray(value)) { - // if value is an array, join them with commas first - value = value.join(','); - } - // return customKey=customValue format, escaping + to . and / to _ - return (customKey.toLowerCase() + '=' + value.toLowerCase()).replace('+', '.').replace('/', '_') -} - -function partitionByVideoBids(bidRequests) { - return bidRequests.reduce(function (acc, bid) { - // Fallback to banner ads if nothing specified - if (isVideoRequest(bid)) { - acc[0].push(bid); + const response = buildResponse(bidResponses, ortbResponse, context); + // TODO: we may want to standardize this and move fledge logic to ortbConverter + let fledgeAuctionConfigs = utils.deepAccess(ortbResponse, 'ext.fledge_auction_configs'); + if (fledgeAuctionConfigs) { + fledgeAuctionConfigs = Object.entries(fledgeAuctionConfigs).map(([bidId, cfg]) => { + return { + bidId, + config: Object.assign({ + auctionSignals: {}, + }, cfg) + } + }); + return { + bids: response.bids, + fledgeAuctionConfigs, + } } else { - acc[1].push(bid); - } - return acc; - }, [[], []]); -} - -function getMediaTypeFromRequest(serverRequest) { - return /avjp$/.test(serverRequest.url) ? VIDEO : BANNER; -} - -function buildCommonQueryParamsFromBids(bids, bidderRequest) { - const isInIframe = inIframe(); - let defaultParams; - - defaultParams = { - ju: bidderRequest.refererInfo.page, - ch: document.charSet || document.characterSet, - res: `${screen.width}x${screen.height}x${screen.colorDepth}`, - ifr: isInIframe, - tz: new Date().getTimezoneOffset(), - tws: getViewportDimensions(isInIframe), - be: 1, - bc: bids[0].params.bc || `${BIDDER_CONFIG}_${BIDDER_VERSION}`, - dddid: _map(bids, bid => bid.transactionId).join(','), - nocache: new Date().getTime() - }; - - const userDataSegments = buildFpdQueryParams('user.data', bidderRequest.ortb2); - if (userDataSegments.length > 0) { - defaultParams.sm = userDataSegments; - } - - const siteContentDataSegments = buildFpdQueryParams('site.content.data', bidderRequest.ortb2); - if (siteContentDataSegments.length > 0) { - defaultParams.scsm = siteContentDataSegments; - } - - if (bids[0].params.platform) { - defaultParams.ph = bids[0].params.platform; - } - - if (bidderRequest.gdprConsent) { - let gdprConsentConfig = bidderRequest.gdprConsent; - - if (gdprConsentConfig.consentString !== undefined) { - defaultParams.gdpr_consent = gdprConsentConfig.consentString; - } - - if (gdprConsentConfig.gdprApplies !== undefined) { - defaultParams.gdpr = gdprConsentConfig.gdprApplies ? 1 : 0; - } - - if (config.getConfig('consentManagement.cmpApi') === 'iab') { - defaultParams.x_gdpr_f = 1; + return response.bids } - } - - if (bidderRequest && bidderRequest.uspConsent) { - defaultParams.us_privacy = bidderRequest.uspConsent; - } - - // normalize publisher common id - if (deepAccess(bids[0], 'crumbs.pubcid')) { - deepSetValue(bids[0], 'userId.pubcid', deepAccess(bids[0], 'crumbs.pubcid')); - } - defaultParams = appendUserIdsToQueryParams(defaultParams, bids[0].userId); - - // supply chain support - if (bids[0].schain) { - defaultParams.schain = serializeSupplyChain(bids[0].schain); - } - - return defaultParams; -} - -function buildFpdQueryParams(fpdPath, ortb2) { - const firstPartyData = deepAccess(ortb2, fpdPath); - if (!Array.isArray(firstPartyData) || !firstPartyData.length) { - return ''; - } - const fpd = firstPartyData - .filter( - data => (Array.isArray(data.segment) && - data.segment.length > 0 && - data.name !== undefined && - data.name.length > 0) - ) - .reduce((acc, data) => { - const name = typeof data.ext === 'object' && data.ext.segtax ? `${data.name}/${data.ext.segtax}` : data.name; - acc[name] = (acc[name] || []).concat(data.segment.map(seg => seg.id)); - return acc; - }, {}) - return Object.keys(fpd) - .map((name, _) => name + ':' + fpd[name].join('|')) - .join(',') -} - -function appendUserIdsToQueryParams(queryParams, userIds) { - _each(userIds, (userIdObjectOrValue, userIdProviderKey) => { - const key = USER_ID_CODE_TO_QUERY_ARG[userIdProviderKey]; - - if (USER_ID_CODE_TO_QUERY_ARG.hasOwnProperty(userIdProviderKey)) { - switch (userIdProviderKey) { - case 'merkleId': - queryParams[key] = userIdObjectOrValue.id; - break; - case 'uid2': - queryParams[key] = userIdObjectOrValue.id; - break; - case 'lipb': - queryParams[key] = userIdObjectOrValue.lipbid; - if (Array.isArray(userIdObjectOrValue.segments) && userIdObjectOrValue.segments.length > 0) { - const liveIntentSegments = 'liveintent:' + userIdObjectOrValue.segments.join('|'); - queryParams.sm = `${queryParams.sm ? queryParams.sm + ',' : ''}${liveIntentSegments}`; + }, + overrides: { + imp: { + bidfloor(setBidFloor, imp, bidRequest, context) { + // enforce floors should always be in USD + // TODO: does it make sense that request.cur can be any currency, but request.imp[].bidfloorcur must be USD? + const floor = {}; + setBidFloor(floor, bidRequest, {...context, currency: 'USD'}); + if (floor.bidfloorcur === 'USD') { + Object.assign(imp, floor); + } + }, + video(orig, imp, bidRequest, context) { + if (FEATURES.VIDEO) { + // `orig` is the video imp processor, which looks at bidRequest.mediaTypes[VIDEO] + // to populate imp.video + // alter its input `bidRequest` to also pick up parameters from `bidRequest.params` + let videoParams = bidRequest.mediaTypes[VIDEO]; + if (videoParams) { + videoParams = Object.assign({}, videoParams, bidRequest.params.video); + bidRequest = {...bidRequest, mediaTypes: {[VIDEO]: videoParams}} + } + orig(imp, bidRequest, context); + if (imp.video && videoParams?.context === 'outstream') { + imp.video.placement = imp.video.placement || 4; } - break; - case 'parrableId': - queryParams[key] = userIdObjectOrValue.eid; - break; - case 'id5id': - queryParams[key] = userIdObjectOrValue.uid; - break; - case 'novatiq': - queryParams[key] = userIdObjectOrValue.snowflake; - break; - default: - queryParams[key] = userIdObjectOrValue; + } } } - }); - - return queryParams; -} - -function serializeSupplyChain(supplyChain) { - return `${supplyChain.ver},${supplyChain.complete}!${serializeSupplyChainNodes(supplyChain.nodes)}`; -} - -function serializeSupplyChainNodes(supplyChainNodes) { - const supplyChainNodePropertyOrder = ['asi', 'sid', 'hp', 'rid', 'name', 'domain']; + } +}); - return supplyChainNodes.map(supplyChainNode => { - return supplyChainNodePropertyOrder.map(property => supplyChainNode[property] || '') - .join(','); - }).join('!'); +function transformBidParams(params, isOpenRtb) { + return utils.convertTypes({ + 'unit': 'string', + 'customFloor': 'number' + }, params); } -function buildOXBannerRequest(bids, bidderRequest) { - let customParamsForAllBids = []; - let hasCustomParam = false; - let queryParams = buildCommonQueryParamsFromBids(bids, bidderRequest); - let auids = _map(bids, bid => bid.params.unit); +function isBidRequestValid(bidRequest) { + const hasDelDomainOrPlatform = bidRequest.params.delDomain || + bidRequest.params.platform; - queryParams.aus = _map(bids, bid => parseSizesInput(bid.mediaTypes.banner.sizes).join(',')).join('|'); - queryParams.divids = _map(bids, bid => encodeURIComponent(bid.adUnitCode)).join(','); - // gpid - queryParams.aucs = _map(bids, function (bid) { - let gpid = deepAccess(bid, 'ortb2Imp.ext.data.pbadslot'); - return encodeURIComponent(gpid || '') - }).join(','); - - if (auids.some(auid => auid)) { - queryParams.auid = auids.join(','); - } - - if (bids.some(bid => bid.params.doNotTrack)) { - queryParams.ns = 1; + if (utils.deepAccess(bidRequest, 'mediaTypes.banner') && + hasDelDomainOrPlatform) { + return !!bidRequest.params.unit || + utils.deepAccess(bidRequest, 'mediaTypes.banner.sizes.length') > 0; } - if (config.getConfig('coppa') === true || bids.some(bid => bid.params.coppa)) { - queryParams.tfcd = 1; - } + return !!(bidRequest.params.unit && hasDelDomainOrPlatform); +} - bids.forEach(function (bid) { - if (bid.params.customParams) { - let customParamsForBid = _map(Object.keys(bid.params.customParams), customKey => formatCustomParms(customKey, bid.params.customParams)); - let formattedCustomParams = window.btoa(customParamsForBid.join('&')); - hasCustomParam = true; - customParamsForAllBids.push(formattedCustomParams); - } else { - customParamsForAllBids.push(''); - } +function buildRequests(bids, bidderRequest) { + let videoBids = bids.filter(bid => isVideoBid(bid)); + let bannerBids = bids.filter(bid => isBannerBid(bid)); + let requests = bannerBids.length ? [createRequest(bannerBids, bidderRequest, BANNER)] : []; + videoBids.forEach(bid => { + requests.push(createRequest([bid], bidderRequest, VIDEO)); }); - if (hasCustomParam) { - queryParams.tps = customParamsForAllBids.join(','); - } - - enrichQueryWithFloors(queryParams, BANNER, bids); - - let url = queryParams.ph - ? `https://u.openx.net/w/1.0/arj` - : `https://${bids[0].params.delDomain}/w/1.0/arj`; - - return { - method: 'GET', - url: url, - data: queryParams, - payload: {'bids': bids, 'startTime': new Date()} - }; + return requests; } -function buildOXVideoRequest(bid, bidderRequest) { - let oxVideoParams = generateVideoParameters(bid, bidderRequest); - let url = oxVideoParams.ph - ? `https://u.openx.net/v/1.0/avjp` - : `https://${bid.params.delDomain}/v/1.0/avjp`; +function createRequest(bidRequests, bidderRequest, mediaType) { return { - method: 'GET', - url: url, - data: oxVideoParams, - payload: {'bid': bid, 'startTime': new Date()} - }; -} - -function generateVideoParameters(bid, bidderRequest) { - const videoMediaType = deepAccess(bid, `mediaTypes.video`); - let queryParams = buildCommonQueryParamsFromBids([bid], bidderRequest); - let oxVideoConfig = deepAccess(bid, 'params.video') || {}; - let context = deepAccess(bid, 'mediaTypes.video.context'); - let playerSize = deepAccess(bid, 'mediaTypes.video.playerSize'); - let width; - let height; - - // normalize config for video size - if (isArray(bid.sizes) && bid.sizes.length === 2 && !isArray(bid.sizes[0])) { - width = parseInt(bid.sizes[0], 10); - height = parseInt(bid.sizes[1], 10); - } else if (isArray(bid.sizes) && isArray(bid.sizes[0]) && bid.sizes[0].length === 2) { - width = parseInt(bid.sizes[0][0], 10); - height = parseInt(bid.sizes[0][1], 10); - } else if (isArray(playerSize) && playerSize.length === 2) { - width = parseInt(playerSize[0], 10); - height = parseInt(playerSize[1], 10); - } - - let openRtbParams = {w: width, h: height}; - - // legacy openrtb params could be in video, openrtb, or video.openrtb - let legacyParams = bid.params.video || bid.params.openrtb || {}; - if (legacyParams.openrtb) { - legacyParams = legacyParams.openrtb; - } - // support for video object or full openrtb object - if (isArray(legacyParams.imp)) { - legacyParams = legacyParams.imp[0].video; + method: 'POST', + url: config.getConfig('openxOrtbUrl') || REQUEST_URL, + data: converter.toORTB({bidRequests, bidderRequest, context: {mediaType}}) } - Object.keys(legacyParams) - .filter(param => includes(VIDEO_TARGETING, param)) - .forEach(param => openRtbParams[param] = legacyParams[param]); - - // 5.0 openrtb video params - Object.keys(videoMediaType) - .filter(param => includes(VIDEO_TARGETING, param)) - .forEach(param => openRtbParams[param] = videoMediaType[param]); - - let openRtbReq = { - imp: [ - { - video: openRtbParams - } - ] - }; - - queryParams['openrtb'] = JSON.stringify(openRtbReq); - - queryParams.auid = bid.params.unit; - // override prebid config with openx config if available - queryParams.vwd = width || oxVideoConfig.vwd; - queryParams.vht = height || oxVideoConfig.vht; - - if (context === 'outstream') { - queryParams.vos = '101'; - } - - if (oxVideoConfig.mimes) { - queryParams.vmimes = oxVideoConfig.mimes; - } - - if (bid.params.test) { - queryParams.vtest = 1; - } - - let gpid = deepAccess(bid, 'ortb2Imp.ext.data.pbadslot'); - if (gpid) { - queryParams.aucs = encodeURIComponent(gpid); - } - - // each video bid makes a separate request - enrichQueryWithFloors(queryParams, VIDEO, [bid]); - - return queryParams; } -function createVideoBidResponses(response, {bid, startTime}) { - let bidResponses = []; - - if (response !== undefined && response.vastUrl !== '' && response.pub_rev > 0) { - let vastQueryParams = parseUrl(response.vastUrl).search || {}; - let bidResponse = {}; - bidResponse.requestId = bid.bidId; - if (response.deal_id) { - bidResponse.dealId = response.deal_id; - } - // default 5 mins - bidResponse.ttl = 300; - // true is net, false is gross - bidResponse.netRevenue = true; - bidResponse.currency = response.currency; - bidResponse.cpm = parseInt(response.pub_rev, 10) / 1000; - bidResponse.width = parseInt(response.width, 10); - bidResponse.height = parseInt(response.height, 10); - bidResponse.creativeId = response.adid; - bidResponse.vastUrl = response.vastUrl; - bidResponse.mediaType = VIDEO; +function isVideoBid(bid) { + return utils.deepAccess(bid, 'mediaTypes.video'); +} - // enrich adunit with vast parameters - response.ph = vastQueryParams.ph; - response.colo = vastQueryParams.colo; - response.ts = vastQueryParams.ts; +function isBannerBid(bid) { + return utils.deepAccess(bid, 'mediaTypes.banner') || !isVideoBid(bid); +} - bidResponses.push(bidResponse); +function interpretResponse(resp, req) { + if (!resp.body) { + resp.body = {nbr: 0}; } - - return bidResponses; + return converter.fromORTB({request: req.data, response: resp.body}); } -function enrichQueryWithFloors(queryParams, mediaType, bids) { - let customFloorsForAllBids = []; - let hasCustomFloor = false; - bids.forEach(function (bid) { - let floor = getBidFloor(bid, mediaType); - - if (floor) { - customFloorsForAllBids.push(floor); - hasCustomFloor = true; +/** + * @param syncOptions + * @param responses + * @param gdprConsent + * @param uspConsent + * @return {{type: (string), url: (*|string)}[]} + */ +function getUserSyncs(syncOptions, responses, gdprConsent, uspConsent) { + if (syncOptions.iframeEnabled || syncOptions.pixelEnabled) { + let pixelType = syncOptions.iframeEnabled ? 'iframe' : 'image'; + let queryParamStrings = []; + let syncUrl = SYNC_URL; + if (gdprConsent) { + queryParamStrings.push('gdpr=' + (gdprConsent.gdprApplies ? 1 : 0)); + queryParamStrings.push('gdpr_consent=' + encodeURIComponent(gdprConsent.consentString || '')); + } + if (uspConsent) { + queryParamStrings.push('us_privacy=' + encodeURIComponent(uspConsent)); + } + if (responses.length > 0 && responses[0].body && responses[0].body.ext) { + const ext = responses[0].body.ext; + if (ext.delDomain) { + syncUrl = `https://${ext.delDomain}/w/1.0/pd` + } else if (ext.platform) { + queryParamStrings.push('ph=' + ext.platform) + } } else { - customFloorsForAllBids.push(0); + queryParamStrings.push('ph=' + DEFAULT_PH) } - }); - if (hasCustomFloor) { - queryParams.aumfs = customFloorsForAllBids.join(','); + return [{ + type: pixelType, + url: `${syncUrl}${queryParamStrings.length > 0 ? '?' + queryParamStrings.join('&') : ''}` + }]; } } - -function getBidFloor(bidRequest, mediaType) { - let floorInfo = {}; - const currency = config.getConfig('currency.adServerCurrency') || DEFAULT_CURRENCY; - - if (typeof bidRequest.getFloor === 'function') { - floorInfo = bidRequest.getFloor({ - currency: currency, - mediaType: mediaType, - size: '*' - }); - } - let floor = floorInfo.floor || bidRequest.params.customFloor || 0; - - return Math.round(floor * 1000); // normalize to micro currency -} - -registerBidder(spec); diff --git a/modules/openxBidAdapter.md b/modules/openxBidAdapter.md index 0690bf6b4fc..a39aa1580cd 100644 --- a/modules/openxBidAdapter.md +++ b/modules/openxBidAdapter.md @@ -9,30 +9,31 @@ Maintainer: team-openx@openx.com # Description Module that connects to OpenX's demand sources. -Note there is an updated version of the OpenX bid adapter called openxOrtbBidAdapter. -Publishers are welcome to test the other adapter and give feedback. Please note you should only include either openxBidAdapter or openxOrtbBidAdapter in your build. +Note that this adapter mirrors openxOrtbBidAdapter and any updates must be +completed in both adapters. +openxOrtbBidAdapter will be removed in a future release and should not be used. +Please note you should only include either openxBidAdapter or openxOrtbBidAdapter in your build. # Bid Parameters ## Banner -| Name | Scope | Type | Description | Example | -|---------------------------------|----------|---------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------------| -| `delDomain` ~~or `platform`~~** | required | String | OpenX delivery domain provided by your OpenX representative. | "PUBLISHER-d.openx.net" | -| `unit` | required | String | OpenX ad unit ID provided by your OpenX representative. | "1611023122" | -| `customParams` | optional | Object | User-defined targeting key-value pairs. customParams applies to a specific unit. | `{key1: "v1", key2: ["v2","v3"]}` | -| `customFloor` | optional | Number | Minimum price in USD. customFloor applies to a specific unit. For example, use the following value to set a $1.50 floor: 1.50

**WARNING:**
Misuse of this parameter can impact revenue

Note: OpenX suggests using the [Price Floor Module](https://docs.prebid.org/dev-docs/modules/floors.html) instead of customFloor. The Price Floor Module is prioritized over customFloor if both are present. | 1.50 | -| `doNotTrack` | optional | Boolean | Prevents advertiser from using data for this user.

**WARNING:**
Request-level setting. May impact revenue. | true | -| `coppa` | optional | Boolean | Enables Child's Online Privacy Protection Act (COPPA) regulations. | true | - -** platform is deprecated. Please use delDomain instead. If you have any questions please contact your representative. +| Name | Scope | Type | Description | Example +| ---- | ----- | ---- | ----------- | ------- +| `delDomain` or `platform` | required | String | OpenX delivery domain or platform id provided by your OpenX representative. | "PUBLISHER-d.openx.net" or "555not5a-real-plat-form-id0123456789" +| `unit` | required | String | OpenX ad unit ID provided by your OpenX representative. | "1611023122" +| `customParams` | optional | Object | User-defined targeting key-value pairs. customParams applies to a specific unit. | `{key1: "v1", key2: ["v2","v3"]}` +| `customFloor` | optional | Number | Minimum price in USD. customFloor applies to a specific unit. For example, use the following value to set a $1.50 floor: 1.50

**WARNING:**
Misuse of this parameter can impact revenue | 1.50 +| `doNotTrack` | optional | Boolean | Prevents advertiser from using data for this user.

**WARNING:**
Request-level setting. May impact revenue. | true +| `coppa` | optional | Boolean | Enables Child's Online Privacy Protection Act (COPPA) regulations. Use of `pbjs.setConfig({coppa: true});` is now preferred. | true ## Video -| Name | Scope | Type | Description | Example | -|-------------|----------|--------------------|--------------------------------------------------------------|----------------------------------------------------------------| -| `unit` | required | String | OpenX ad unit ID provided by your OpenX representative. | "1611023122" | -| `delDomain` | required | String | OpenX delivery domain provided by your OpenX representative. | "PUBLISHER-d.openx.net" | -| `openrtb` | optional | OpenRTB Impression | An OpenRtb Impression with Video subtype properties | `{ imp: [{ video: {mimes: ['video/x-ms-wmv, video/mp4']} }] }` | +| Name | Scope | Type | Description | Example +| ---- | ----- | ---- | ----------- | ------- +| `unit` | required | String | OpenX ad unit ID provided by your OpenX representative. | "1611023122" +| `delDomain` | required | String | OpenX delivery domain provided by your OpenX representative. | "PUBLISHER-d.openx.net" +| `video` | optional | OpenRTB video subtypes | Use of adUnit.mediaTypes.video is now preferred. | `{ video: {mimes: ['video/mp4']}` + # Example ```javascript @@ -70,7 +71,8 @@ var adUnits = [ mediaTypes: { video: { playerSize: [640, 480], - context: 'instream' + context: 'instream', + mimes: ['video/x-ms-wmv, video/mp4'] } }, bids: [{ @@ -79,10 +81,10 @@ var adUnits = [ unit: '1611023124', delDomain: 'PUBLISHER-d.openx.net', video: { - mimes: ['video/x-ms-wmv, video/mp4'] + mimes: ['video/x-ms-wmv, video/mp4'] // mediaTypes.video preferred } } - }] + }]p } ]; ``` diff --git a/modules/openxOrtbBidAdapter.js b/modules/openxOrtbBidAdapter.js index 200d2cf0fed..5afee034d5f 100644 --- a/modules/openxOrtbBidAdapter.js +++ b/modules/openxOrtbBidAdapter.js @@ -29,13 +29,6 @@ const converter = ortbConverter({ }, imp(buildImp, bidRequest, context) { const imp = buildImp(bidRequest, context); - if (bidRequest.mediaTypes[VIDEO]?.context === 'outstream') { - imp.video.placement = imp.video.placement || 4; - } - if (imp.ext?.ae && !context.bidderRequest.fledgeEnabled) { - // TODO: we may want to standardize this and move fledge logic to ortbConverter - delete imp.ext.ae; - } mergeDeep(imp, { tagid: bidRequest.params.unit, ext: { @@ -59,6 +52,9 @@ const converter = ortbConverter({ } }) const bid = context.bidRequests[0]; + if (bid.params.coppa) { + utils.deepSetValue(req, 'regs.coppa', 1); + } if (bid.params.doNotTrack) { utils.deepSetValue(req, 'device.dnt', 1); } @@ -106,10 +102,12 @@ const converter = ortbConverter({ let fledgeAuctionConfigs = utils.deepAccess(ortbResponse, 'ext.fledge_auction_configs'); if (fledgeAuctionConfigs) { fledgeAuctionConfigs = Object.entries(fledgeAuctionConfigs).map(([bidId, cfg]) => { - return Object.assign({ + return { bidId, - auctionSignals: {} - }, cfg); + config: Object.assign({ + auctionSignals: {}, + }, cfg) + } }); return { bids: response.bids, @@ -129,6 +127,22 @@ const converter = ortbConverter({ if (floor.bidfloorcur === 'USD') { Object.assign(imp, floor); } + }, + video(orig, imp, bidRequest, context) { + if (FEATURES.VIDEO) { + // `orig` is the video imp processor, which looks at bidRequest.mediaTypes[VIDEO] + // to populate imp.video + // alter its input `bidRequest` to also pick up parameters from `bidRequest.params` + let videoParams = bidRequest.mediaTypes[VIDEO]; + if (videoParams) { + videoParams = Object.assign({}, videoParams, bidRequest.params.video); + bidRequest = {...bidRequest, mediaTypes: {[VIDEO]: videoParams}} + } + orig(imp, bidRequest, context); + if (imp.video && videoParams?.context === 'outstream') { + imp.video.placement = imp.video.placement || 4; + } + } } } } diff --git a/modules/openxOrtbBidAdapter.md b/modules/openxOrtbBidAdapter.md index fd926b27b9f..b5e1820021a 100644 --- a/modules/openxOrtbBidAdapter.md +++ b/modules/openxOrtbBidAdapter.md @@ -1,15 +1,17 @@ # Overview ``` -Module Name: OpenX OpenRTB Bidder Adapter +Module Name: OpenX Bidder Adapter Module Type: Bidder Adapter Maintainer: team-openx@openx.com ``` # Description +DEPRECATED. Use openxBidAdapter. -This is an updated version of the OpenX bid adapter which calls our new serving architecture. -Publishers are welcome to test this adapter and give feedback. Please note you should only include either openxBidAdapter or openxOrtbBidAdapter in your build. +This adapter was originally an adapter used to test OpenX serving architecture changes. +This adapter now mirrors openxBidAdapter and this adapter will be removed in a future release. Please use openxBidAdapter. +Please note you should only include either openxBidAdapter or openxOrtbBidAdapter in your build. # Bid Parameters ## Banner @@ -21,7 +23,7 @@ Publishers are welcome to test this adapter and give feedback. Please note you s | `customParams` | optional | Object | User-defined targeting key-value pairs. customParams applies to a specific unit. | `{key1: "v1", key2: ["v2","v3"]}` | `customFloor` | optional | Number | Minimum price in USD. customFloor applies to a specific unit. For example, use the following value to set a $1.50 floor: 1.50

**WARNING:**
Misuse of this parameter can impact revenue | 1.50 | `doNotTrack` | optional | Boolean | Prevents advertiser from using data for this user.

**WARNING:**
Request-level setting. May impact revenue. | true -| `coppa` | optional | Boolean | Enables Child's Online Privacy Protection Act (COPPA) regulations. | true +| `coppa` | optional | Boolean | Enables Child's Online Privacy Protection Act (COPPA) regulations. Use of `pbjs.setConfig({coppa: true});` is now preferred. | true ## Video @@ -29,7 +31,7 @@ Publishers are welcome to test this adapter and give feedback. Please note you s | ---- | ----- | ---- | ----------- | ------- | `unit` | required | String | OpenX ad unit ID provided by your OpenX representative. | "1611023122" | `delDomain` | required | String | OpenX delivery domain provided by your OpenX representative. | "PUBLISHER-d.openx.net" -| `video` | optional | OpenRTB video subtypes | Alternatively can be added under adUnit.mediaTypes.video | `{ video: {mimes: ['video/mp4']}` +| `video` | optional | OpenRTB video subtypes | Use of adUnit.mediaTypes.video is now preferred. | `{ video: {mimes: ['video/mp4']}` # Example @@ -68,7 +70,8 @@ var adUnits = [ mediaTypes: { video: { playerSize: [640, 480], - context: 'instream' + context: 'instream', + mimes: ['video/x-ms-wmv, video/mp4'] } }, bids: [{ @@ -77,10 +80,10 @@ var adUnits = [ unit: '1611023124', delDomain: 'PUBLISHER-d.openx.net', video: { - mimes: ['video/x-ms-wmv, video/mp4'] + mimes: ['video/x-ms-wmv, video/mp4'] // mediaTypes.video preferred } } - }] + }]p } ]; ``` diff --git a/modules/operaadsBidAdapter.js b/modules/operaadsBidAdapter.js index aa548debf32..48c6246ce6b 100644 --- a/modules/operaadsBidAdapter.js +++ b/modules/operaadsBidAdapter.js @@ -228,7 +228,7 @@ function buildOpenRtbBidRequest(bidRequest, bidderRequest) { // build OpenRTB request body const payload = { id: bidderRequest.auctionId, - tmax: bidderRequest.timeout || config.getConfig('bidderTimeout'), + tmax: bidderRequest.timeout, test: config.getConfig('debug') ? 1 : 0, imp: createImp(bidRequest), device: getDevice(), diff --git a/modules/optidigitalBidAdapter.js b/modules/optidigitalBidAdapter.js new file mode 100755 index 00000000000..4372aa830e6 --- /dev/null +++ b/modules/optidigitalBidAdapter.js @@ -0,0 +1,221 @@ +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { BANNER } from '../src/mediaTypes.js'; +import { deepAccess, parseSizesInput, getAdUnitSizes } from '../src/utils.js'; + +const BIDDER_CODE = 'optidigital'; +const GVL_ID = 915; +const ENDPOINT_URL = 'https://pbs.optidigital.com/bidder'; +const USER_SYNC_URL_IFRAME = 'https://scripts.opti-digital.com/js/presync.html?endpoint=optidigital'; +let CUR = 'USD'; + +export const spec = { + code: BIDDER_CODE, + gvlid: GVL_ID, + supportedMediaTypes: [BANNER], + /** + * Determines whether or not the given bid request is valid. + * + * @param {BidRequest} bid The bid params to validate. + * @return boolean True if this is a valid bid, and false otherwise. + */ + isBidRequestValid: function(bid) { + let isValid = false; + if (typeof bid.params !== 'undefined' && bid.params.placementId && bid.params.publisherId) { + isValid = true; + } + + return isValid; + }, + /** + * Make a server request from the list of BidRequests. + * + * @param {validBidRequests[]} - an array of bids + * @return ServerRequest Info describing the request to the server. + */ + buildRequests: function(validBidRequests, bidderRequest) { + if (!validBidRequests || validBidRequests.length === 0 || !bidderRequest || !bidderRequest.bids) { + return []; + } + + const ortb2 = bidderRequest.ortb2 || { + bcat: [], + badv: [] + }; + + const payload = { + referrer: (bidderRequest.refererInfo && bidderRequest.refererInfo.page) ? bidderRequest.refererInfo.page : '', + hb_version: '$prebid.version$', + deviceWidth: document.documentElement.clientWidth, + auctionId: deepAccess(validBidRequests[0], 'auctionId'), + bidderRequestId: deepAccess(validBidRequests[0], 'bidderRequestId'), + publisherId: deepAccess(validBidRequests[0], 'params.publisherId'), + imp: validBidRequests.map(bidRequest => buildImp(bidRequest, ortb2)), + badv: ortb2.badv || deepAccess(validBidRequests[0], 'params.badv') || [], + bcat: ortb2.bcat || deepAccess(validBidRequests[0], 'params.bcat') || [], + bapp: deepAccess(validBidRequests[0], 'params.bapp') || [] + } + + if (validBidRequests[0].params.pageTemplate && validBidRequests[0].params.pageTemplate !== '') { + payload.pageTemplate = validBidRequests[0].params.pageTemplate; + } + + if (validBidRequests[0].schain) { + payload.schain = validBidRequests[0].schain; + } + + const gdpr = deepAccess(bidderRequest, 'gdprConsent'); + if (bidderRequest && gdpr) { + const isConsentString = typeof gdpr.consentString === 'string'; + payload.gdpr = { + consent: isConsentString ? gdpr.consentString : '', + required: true + }; + } + if (bidderRequest && !gdpr) { + payload.gdpr = { + consent: '', + required: false + } + } + + if (window.location.href.indexOf('optidigitalTestMode=true') !== -1) { + payload.testMode = true; + } + + if (bidderRequest && bidderRequest.uspConsent) { + payload.uspConsent = bidderRequest.uspConsent; + } + + const payloadObject = JSON.stringify(payload); + return { + method: 'POST', + url: ENDPOINT_URL, + data: payloadObject + }; + }, + /** + * Unpack the response from the server into a list of bids. + * + * @param {ServerResponse} serverResponse A successful response from the server. + * @return {Bid[]} An array of bids which were nested inside the server. + */ + interpretResponse: function(serverResponse, bidRequest) { + const bidResponses = []; + serverResponse = serverResponse.body; + + if (serverResponse.bids) { + serverResponse.bids.forEach((bid) => { + const bidResponse = { + placementId: bid.placementId, + transactionId: bid.transactionId, + requestId: bid.bidId, + ttl: bid.ttl, + creativeId: bid.creativeId, + currency: bid.cur, + cpm: bid.cpm, + width: bid.w, + height: bid.h, + ad: bid.adm, + netRevenue: true, + meta: { + advertiserDomains: bid.adomain && bid.adomain.length > 0 ? bid.adomain : [] + } + }; + bidResponses.push(bidResponse); + }); + } + return bidResponses; + }, + + /** + * Register the user sync pixels which should be dropped after the auction. + * + * @param {SyncOptions} syncOptions Which user syncs are allowed? + * @param {ServerResponse[]} serverResponses List of server's responses. + * @return {UserSync[]} The user syncs which should be dropped. + */ + getUserSyncs: function(syncOptions, serverResponses, gdprConsent, uspConsent) { + let syncurl = ''; + + // Attaching GDPR Consent Params in UserSync url + if (gdprConsent) { + syncurl += '&gdpr=' + (gdprConsent.gdprApplies ? 1 : 0); + syncurl += '&gdpr_consent=' + encodeURIComponent(gdprConsent.consentString || ''); + } + if (uspConsent && uspConsent.consentString) { + syncurl += `&ccpa_consent=${uspConsent.consentString}`; + } + + if (syncOptions.iframeEnabled) { + return [{ + type: 'iframe', + url: USER_SYNC_URL_IFRAME + syncurl + }]; + } + }, +}; + +function buildImp(bidRequest, ortb2) { + let imp = {}; + imp = { + sizes: parseSizesInput(deepAccess(bidRequest, 'mediaTypes.banner.sizes')), + bidId: deepAccess(bidRequest, 'bidId'), + adUnitCode: deepAccess(bidRequest, 'adUnitCode'), + transactionId: deepAccess(bidRequest, 'transactionId'), + placementId: deepAccess(bidRequest, 'params.placementId') + }; + + if (bidRequest.params.divId && bidRequest.params.divId !== '') { + if (getAdContainer(bidRequest.params.divId)) { + imp.adContainerWidth = getAdContainer(bidRequest.params.divId).offsetWidth; + imp.adContainerHeight = getAdContainer(bidRequest.params.divId).offsetHeight; + } + } + + let floorSizes = []; + if (deepAccess(bidRequest, 'mediaTypes.banner')) { + floorSizes = getAdUnitSizes(bidRequest); + } + + if (bidRequest.params.currency && bidRequest.params.currency !== '') { + CUR = bidRequest.params.currency; + } + + let bidFloor = _getFloor(bidRequest, floorSizes, CUR); + if (bidFloor) { + imp.bidFloor = bidFloor; + } + + let battr = ortb2.battr || deepAccess(bidRequest, 'params.battr'); + if (battr && Array.isArray(battr) && battr.length) { + imp.battr = battr; + } + + return imp; +} + +function getAdContainer(container) { + if (document.getElementById(container)) { + return document.getElementById(container); + } +} + +function _getFloor (bid, sizes, currency) { + let floor = null; + let size = sizes.length === 1 ? sizes[0] : '*'; + if (typeof bid.getFloor === 'function') { + try { + const floorInfo = bid.getFloor({ + currency: currency, + mediaType: 'banner', + size: size + }); + if (typeof floorInfo === 'object' && floorInfo.currency === CUR && !isNaN(parseFloat(floorInfo.floor))) { + floor = parseFloat(floorInfo.floor); + } + } catch (err) {} + } + return floor !== null ? floor : bid.params.floor; +} + +registerBidder(spec); diff --git a/modules/optidigitalBidAdapter.md b/modules/optidigitalBidAdapter.md new file mode 100755 index 00000000000..466dfb3bef2 --- /dev/null +++ b/modules/optidigitalBidAdapter.md @@ -0,0 +1,46 @@ +# Overview + +**Module Name**: OptiDigital Bidder Adapter +**Module Type**: Bidder Adapter +**Maintainer**: prebid@optidigital.com + +# Description + +Bidder Adapter for Prebid.js. + +## AdUnits configuration example +``` + var adUnits = [{ + code: 'your-slot_1-div', //use exactly the same code as your slot div id. + mediaTypes: { + banner: { + sizes: [[300,600]] + } + }, + bids: [{ + bidder: 'optidigital', + params: { + publisherId: 'test', + placementId: 'Billboard_Top', + divId: 'Billboard_Top_3c5425', // optional parameter + pageTemplate: 'home', // optional parameter + badv: ['example.com'], // optional parameter + bcat: ['IAB1-1'], // optional parameter + bapp: ['com.blocked'], // optional parameter + battr: [1, 2] // optional parameter + } + }] + }]; +``` + +## UserSync example + +``` +pbjs.setConfig({ + userSync: { + iframeEnabled: true, + syncEnabled: true, + syncDelay: 3000 + } +}); +``` diff --git a/modules/orbidderBidAdapter.js b/modules/orbidderBidAdapter.js index 9979b1fdc3b..b84c67ba6d2 100644 --- a/modules/orbidderBidAdapter.js +++ b/modules/orbidderBidAdapter.js @@ -3,6 +3,7 @@ import {registerBidder} from '../src/adapters/bidderFactory.js'; import { getStorageManager } from '../src/storageManager.js'; import { BANNER, NATIVE } from '../src/mediaTypes.js'; import { convertOrtbRequestToProprietaryNative } from '../src/native.js'; +import {getGlobal} from '../src/prebidGlobal.js'; const storageManager = getStorageManager({bidderCode: 'orbidder'}); @@ -96,7 +97,7 @@ export const spec = { method: 'POST', options: { withCredentials: true }, data: { - v: $$PREBID_GLOBAL$$.version, + v: getGlobal().version, pageUrl: referer, bidId: bidRequest.bidId, auctionId: bidRequest.auctionId, diff --git a/modules/orbitsoftBidAdapter.js b/modules/orbitsoftBidAdapter.js new file mode 100644 index 00000000000..d72a8719bd8 --- /dev/null +++ b/modules/orbitsoftBidAdapter.js @@ -0,0 +1,150 @@ +import * as utils from '../src/utils.js'; +import {registerBidder} from '../src/adapters/bidderFactory.js'; +import {config} from '../src/config.js'; + +const BIDDER_CODE = 'orbitsoft'; +let styleParamsMap = { + 'title.family': 'f1', // headerFont + 'title.size': 'fs1', // headerFontSize + 'title.weight': 'w1', // headerWeight + 'title.style': 's1', // headerStyle + 'title.color': 'c3', // headerColor + 'description.family': 'f2', // descriptionFont + 'description.size': 'fs2', // descriptionFontSize + 'description.weight': 'w2', // descriptionWeight + 'description.style': 's2', // descriptionStyle + 'description.color': 'c4', // descriptionColor + 'url.family': 'f3', // urlFont + 'url.size': 'fs3', // urlFontSize + 'url.weight': 'w3', // urlWeight + 'url.style': 's3', // urlStyle + 'url.color': 'c5', // urlColor + 'colors.background': 'c2', // borderColor + 'colors.border': 'c1', // borderColor + 'colors.link': 'c6', // lnkColor +}; +export const spec = { + code: BIDDER_CODE, + aliases: ['oas', '152media'], // short code and customer aliases + isBidRequestValid: function (bid) { + switch (true) { + case !('params' in bid): + utils.logError(bid.bidder + ': No required params'); + return false; + case !(bid.params.placementId): + utils.logError(bid.bidder + ': No required param placementId'); + return false; + case !(bid.params.requestUrl): + utils.logError(bid.bidder + ': No required param requestUrl'); + return false; + } + return true; + }, + buildRequests: function (validBidRequests) { + let bidRequest; + let serverRequests = []; + for (let i = 0; i < validBidRequests.length; i++) { + bidRequest = validBidRequests[i]; + let bidRequestParams = bidRequest.params; + let placementId = utils.getBidIdParameter('placementId', bidRequestParams); + let requestUrl = utils.getBidIdParameter('requestUrl', bidRequestParams); + let referrer = utils.getBidIdParameter('ref', bidRequestParams); + let location = utils.getBidIdParameter('loc', bidRequestParams); + // Append location & referrer + if (location === '') { + location = utils.getWindowLocation(); + } + if (referrer === '' && bidRequest && bidRequest.refererInfo) { + referrer = bidRequest.refererInfo.referer; + } + + // Styles params + let stylesParams = utils.getBidIdParameter('style', bidRequestParams); + let stylesParamsArray = {}; + for (let currentValue in stylesParams) { + if (stylesParams.hasOwnProperty(currentValue)) { + let currentStyle = stylesParams[currentValue]; + for (let field in currentStyle) { + if (currentStyle.hasOwnProperty(field)) { + let styleField = styleParamsMap[currentValue + '.' + field]; + if (typeof styleField !== 'undefined') { + stylesParamsArray[styleField] = currentStyle[field]; + } + } + } + } + } + // Custom params + let customParams = utils.getBidIdParameter('customParams', bidRequestParams); + let customParamsArray = {}; + for (let customField in customParams) { + if (customParams.hasOwnProperty(customField)) { + customParamsArray['c.' + customField] = customParams[customField]; + } + } + + // Sizes params (not supports by server, for future features) + let sizesParams = bidRequest.sizes; + let parsedSizes = utils.parseSizesInput(sizesParams); + let requestData = Object.assign({ + 'scid': placementId, + 'callback_uid': utils.generateUUID(), + 'loc': location, + 'ref': referrer, + 'size': parsedSizes + }, stylesParamsArray, customParamsArray); + + serverRequests.push({ + method: 'POST', + url: requestUrl, + data: requestData, + options: {withCredentials: false}, + bidRequest: bidRequest + }); + } + return serverRequests; + }, + interpretResponse: function (serverResponse, request) { + let bidResponses = []; + if (!serverResponse || serverResponse.error) { + utils.logError(BIDDER_CODE + ': Server response error'); + return bidResponses; + } + + const serverBody = serverResponse.body; + if (!serverBody) { + utils.logError(BIDDER_CODE + ': Empty bid response'); + return bidResponses; + } + + const CPM = serverBody.cpm; + const WIDTH = serverBody.width; + const HEIGHT = serverBody.height; + const CREATIVE = serverBody.content_url; + const CALLBACK_UID = serverBody.callback_uid; + const TIME_TO_LIVE = config.getConfig('_bidderTimeout'); + const REFERER = utils.getWindowTop(); + let bidRequest = request.bidRequest; + if (CPM > 0 && WIDTH > 0 && HEIGHT > 0) { + let bidResponse = { + requestId: bidRequest.bidId, + cpm: CPM, + width: WIDTH, + height: HEIGHT, + creativeId: CALLBACK_UID, + ttl: TIME_TO_LIVE, + referrer: REFERER, + currency: 'USD', + netRevenue: true, + adUrl: CREATIVE, + meta: { + advertiserDomains: serverBody.adomain ? serverBody.adomain : [] + } + }; + bidResponses.push(bidResponse); + } + + return bidResponses; + } +}; +registerBidder(spec); diff --git a/modules/outbrainBidAdapter.js b/modules/outbrainBidAdapter.js index 3db1da0d689..6bcbc6a1cba 100644 --- a/modules/outbrainBidAdapter.js +++ b/modules/outbrainBidAdapter.js @@ -4,11 +4,13 @@ import { registerBidder } from '../src/adapters/bidderFactory.js'; -import { NATIVE, BANNER } from '../src/mediaTypes.js'; -import { deepAccess, deepSetValue, replaceAuctionPrice, _map, isArray } from '../src/utils.js'; +import { NATIVE, BANNER, VIDEO } from '../src/mediaTypes.js'; +import { OUTSTREAM } from '../src/video.js'; +import { deepAccess, deepSetValue, replaceAuctionPrice, _map, isArray, logWarn } from '../src/utils.js'; import { ajax } from '../src/ajax.js'; import { config } from '../src/config.js'; import { convertOrtbRequestToProprietaryNative } from '../src/native.js'; +import { Renderer } from '../src/Renderer.js'; const BIDDER_CODE = 'outbrain'; const GVLID = 164; @@ -22,11 +24,12 @@ const NATIVE_PARAMS = { body: { id: 4, name: 'data', type: 2 }, cta: { id: 1, type: 12, name: 'data' } }; +const OUTSTREAM_RENDERER_URL = 'https://acdn.adnxs.com/video/outstream/ANOutstreamVideo.js'; export const spec = { code: BIDDER_CODE, gvlid: GVLID, - supportedMediaTypes: [ NATIVE, BANNER ], + supportedMediaTypes: [ NATIVE, BANNER, VIDEO ], isBidRequestValid: (bid) => { if (typeof bid.params !== 'object') { return false; @@ -50,7 +53,7 @@ export const spec = { return ( !!config.getConfig('outbrain.bidderUrl') && - !!(bid.nativeParams || bid.sizes) + (!!(bid.nativeParams || bid.sizes) || isValidVideoRequest(bid)) ); }, buildRequests: (validBidRequests, bidderRequest) => { @@ -85,6 +88,8 @@ export const spec = { assets: getNativeAssets(bid) }) } + } else if (isVideoRequest(bid)) { + imp.video = getVideoAsset(bid); } else { imp.banner = { format: transformSizes(bid.sizes) @@ -163,7 +168,12 @@ export const spec = { return bids.map((bid, id) => { const bidResponse = bidResponses[id]; if (bidResponse) { - const type = bid.nativeParams ? NATIVE : BANNER; + let type = BANNER; + if (bid.nativeParams) { + type = NATIVE; + } else if (isVideoRequest(bid)) { + type = VIDEO; + } const bidObject = { requestId: bid.bidId, cpm: bidResponse.price, @@ -176,10 +186,16 @@ export const spec = { }; if (type === NATIVE) { bidObject.native = parseNative(bidResponse); - } else { + } else if (type === BANNER) { bidObject.ad = bidResponse.adm; bidObject.width = bidResponse.w; bidObject.height = bidResponse.h; + } else if (type === VIDEO) { + bidObject.vastXml = bidResponse.adm; + const videoContext = deepAccess(bid, 'mediaTypes.video.context'); + if (videoContext === OUTSTREAM) { + bidObject.renderer = createRenderer(bid); + } } bidObject.meta = {}; if (bidResponse.adomain && bidResponse.adomain.length > 0) { @@ -304,6 +320,27 @@ function getNativeAssets(bid) { }).filter(Boolean); } +function getVideoAsset(bid) { + const sizes = flatten(bid.mediaTypes.video.playerSize); + return { + w: parseInt(sizes[0], 10), + h: parseInt(sizes[1], 10), + protocols: bid.mediaTypes.video.protocols, + playbackmethod: bid.mediaTypes.video.playbackmethod, + mimes: bid.mediaTypes.video.mimes, + skip: bid.mediaTypes.video.skip, + delivery: bid.mediaTypes.video.delivery, + api: bid.mediaTypes.video.api, + minbitrate: bid.mediaTypes.video.minbitrate, + maxbitrate: bid.mediaTypes.video.maxbitrate, + minduration: bid.mediaTypes.video.minduration, + maxduration: bid.mediaTypes.video.maxduration, + startdelay: bid.mediaTypes.video.startdelay, + placement: bid.mediaTypes.video.placement, + linearity: bid.mediaTypes.video.linearity + }; +} + /* Turn bid request sizes into ut-compatible format */ function transformSizes(requestSizes) { if (!isArray(requestSizes)) { @@ -338,3 +375,63 @@ function _getFloor(bid, type) { } return null; } + +function isVideoRequest(bid) { + return bid.mediaType === 'video' || !!deepAccess(bid, 'mediaTypes.video'); +} + +function createRenderer(bid) { + let config = {}; + let playerUrl = OUTSTREAM_RENDERER_URL; + let render = function (bid) { + bid.renderer.push(() => { + window.ANOutstreamVideo.renderAd({ + sizes: bid.sizes, + targetId: bid.adUnitCode, + adResponse: { content: bid.vastXml } + }); + }); + }; + + let externalRenderer = deepAccess(bid, 'mediaTypes.video.renderer'); + if (!externalRenderer) { + externalRenderer = deepAccess(bid, 'renderer'); + } + + if (externalRenderer) { + config = externalRenderer.options; + playerUrl = externalRenderer.url; + render = externalRenderer.render; + } + + const renderer = Renderer.install({ + id: bid.adUnitCode, + url: playerUrl, + config: config, + adUnitCode: bid.adUnitCode, + loaded: false + }); + try { + renderer.setRender(render); + } catch (err) { + logWarn('Prebid Error calling setRender on renderer', err); + } + return renderer; +} + +function isValidVideoRequest(bid) { + const videoAdUnit = deepAccess(bid, 'mediaTypes.video') + if (!videoAdUnit) { + return false; + } + + if (!Array.isArray(videoAdUnit.playerSize)) { + return false; + } + + if (videoAdUnit.context == '') { + return false; + } + + return true; +} diff --git a/modules/parrableIdSystem.js b/modules/parrableIdSystem.js index 4a777097914..01ffec3c249 100644 --- a/modules/parrableIdSystem.js +++ b/modules/parrableIdSystem.js @@ -14,6 +14,7 @@ import {submodule} from '../src/hook.js'; import {getRefererInfo} from '../src/refererDetection.js'; import {uspDataHandler} from '../src/adapterManager.js'; import {getStorageManager} from '../src/storageManager.js'; +import {MODULE_TYPE_UID} from '../src/activities/modules.js'; const PARRABLE_URL = 'https://h.parrable.com/prebid'; const PARRABLE_COOKIE_NAME = '_parrable_id'; @@ -22,8 +23,9 @@ const LEGACY_ID_COOKIE_NAME = '_parrable_eid'; const LEGACY_OPTOUT_COOKIE_NAME = '_parrable_optout'; const ONE_YEAR_MS = 364 * 24 * 60 * 60 * 1000; const EXPIRE_COOKIE_DATE = 'Thu, 01 Jan 1970 00:00:00 GMT'; +const MODULE_NAME = 'parrableId'; -const storage = getStorageManager({gvlid: PARRABLE_GVLID}); +const storage = getStorageManager({moduleType: MODULE_TYPE_UID, moduleName: MODULE_NAME}); function getExpirationDate() { const oneYearFromNow = new Date(timestamp() + ONE_YEAR_MS); @@ -336,7 +338,7 @@ export const parrableIdSubmodule = { * used to link submodule with config * @type {string} */ - name: 'parrableId', + name: MODULE_NAME, /** * Global Vendor List ID * @type {number} diff --git a/modules/permutiveRtdProvider.js b/modules/permutiveRtdProvider.js index c11d1c12436..ab827f2b6a5 100644 --- a/modules/permutiveRtdProvider.js +++ b/modules/permutiveRtdProvider.js @@ -8,14 +8,20 @@ import {getGlobal} from '../src/prebidGlobal.js'; import {submodule} from '../src/hook.js'; import {getStorageManager} from '../src/storageManager.js'; -import {deepAccess, deepSetValue, isFn, logError, mergeDeep, isPlainObject, safeJSONParse} from '../src/utils.js'; +import {deepAccess, deepSetValue, isFn, logError, mergeDeep, isPlainObject, safeJSONParse, prefixLog} from '../src/utils.js'; import {includes} from '../src/polyfill.js'; +import {MODULE_TYPE_RTD} from '../src/activities/modules.js'; const MODULE_NAME = 'permutive' +const logger = prefixLog('[PermutiveRTD]') + export const PERMUTIVE_SUBMODULE_CONFIG_KEY = 'permutive-prebid-rtd' +export const PERMUTIVE_STANDARD_KEYWORD = 'p_standard' +export const PERMUTIVE_CUSTOM_COHORTS_KEYWORD = 'permutive' +export const PERMUTIVE_STANDARD_AUD_KEYWORD = 'p_standard_aud' -export const storage = getStorageManager({gvlid: null, moduleName: MODULE_NAME}) +export const storage = getStorageManager({moduleType: MODULE_TYPE_RTD, moduleName: MODULE_NAME}) function init(moduleConfig, userConsent) { readPermutiveModuleConfigFromCache() @@ -23,30 +29,6 @@ function init(moduleConfig, userConsent) { return true } -/** - * Set segment targeting from cache and then try to wait for Permutive - * to initialise to get realtime segment targeting - * @param {Object} reqBidsConfigObj - * @param {function} callback - Called when submodule is done - * @param {customModuleConfig} reqBidsConfigObj - Publisher config for module - */ -export function initSegments (reqBidsConfigObj, callback, customModuleConfig) { - const permutiveOnPage = isPermutiveOnPage() - const moduleConfig = getModuleConfig(customModuleConfig) - const segmentData = getSegments(moduleConfig.params.maxSegs) - - setSegments(reqBidsConfigObj, moduleConfig, segmentData) - - if (moduleConfig.waitForIt && permutiveOnPage) { - window.permutive.ready(function () { - setSegments(reqBidsConfigObj, moduleConfig, segmentData) - callback() - }, 'realtime') - } else { - callback() - } -} - function liftIntoParams(params) { return isPlainObject(params) ? { params } : {} } @@ -108,32 +90,54 @@ export function getModuleConfig(customModuleConfig) { /** * Sets ortb2 config for ac bidders - * @param {Object} bidderOrtb2 + * @param {Object} bidderOrtb2 - The ortb2 object for the all bidders * @param {Object} customModuleConfig - Publisher config for module */ -export function setBidderRtb (bidderOrtb2, customModuleConfig) { - const moduleConfig = getModuleConfig(customModuleConfig) +export function setBidderRtb (bidderOrtb2, moduleConfig, segmentData) { const acBidders = deepAccess(moduleConfig, 'params.acBidders') const maxSegs = deepAccess(moduleConfig, 'params.maxSegs') const transformationConfigs = deepAccess(moduleConfig, 'params.transformations') || [] - const segmentData = getSegments(maxSegs) - acBidders.forEach(function (bidder) { + const ssps = segmentData?.ssp?.ssps ?? [] + const sspCohorts = segmentData?.ssp?.cohorts ?? [] + + const bidders = new Set([...acBidders, ...ssps]) + bidders.forEach(function (bidder) { const currConfig = { ortb2: bidderOrtb2[bidder] || {} } - const nextConfig = updateOrtbConfig(currConfig, segmentData.ac, transformationConfigs) // ORTB2 uses the `ac` segment IDs - bidderOrtb2[bidder] = nextConfig.ortb2; + + let cohorts = [] + + const isAcBidder = acBidders.indexOf(bidder) > -1 + if (isAcBidder) { + cohorts = segmentData.ac + } + + const isSspBidder = ssps.indexOf(bidder) > -1 + if (isSspBidder) { + cohorts = [...new Set([...cohorts, ...sspCohorts])].slice(0, maxSegs) + } + + const nextConfig = updateOrtbConfig(bidder, currConfig, cohorts, sspCohorts, transformationConfigs, segmentData) + bidderOrtb2[bidder] = nextConfig.ortb2 }) } /** * Updates `user.data` object in existing bidder config with Permutive segments + * @param string bidder - The bidder * @param {Object} currConfig - Current bidder config * @param {Object[]} transformationConfigs - array of objects with `id` and `config` properties, used to determine * the transformations on user data to include the ORTB2 object * @param {string[]} segmentIDs - Permutive segment IDs + * @param {string[]} sspSegmentIDs - Permutive SSP segment IDs + * @param {Object} segmentData - The segments available for targeting * @return {Object} Merged ortb2 object */ -function updateOrtbConfig (currConfig, segmentIDs, transformationConfigs) { +function updateOrtbConfig(bidder, currConfig, segmentIDs, sspSegmentIDs, transformationConfigs, segmentData) { + logger.logInfo(`Current ortb2 config`, { bidder, config: currConfig }) + + const customCohortsData = deepAccess(segmentData, bidder) || [] + const name = 'permutive.com' const permutiveUserData = { @@ -145,15 +149,59 @@ function updateOrtbConfig (currConfig, segmentIDs, transformationConfigs) { .filter(({ id }) => ortb2UserDataTransformations.hasOwnProperty(id)) .map(({ id, config }) => ortb2UserDataTransformations[id](permutiveUserData, config)) + const customCohortsUserData = { + name: PERMUTIVE_CUSTOM_COHORTS_KEYWORD, + segment: customCohortsData.map(cohortID => ({ id: cohortID })), + } + const ortbConfig = mergeDeep({}, currConfig) const currentUserData = deepAccess(ortbConfig, 'ortb2.user.data') || [] const updatedUserData = currentUserData - .filter(el => el.name !== name) - .concat(permutiveUserData, transformedUserData) + .filter(el => el.name !== permutiveUserData.name && el.name !== customCohortsUserData.name) + .concat(permutiveUserData, transformedUserData, customCohortsUserData) + logger.logInfo(`Updating ortb2.user.data`, { bidder, user_data: updatedUserData }) deepSetValue(ortbConfig, 'ortb2.user.data', updatedUserData) + // Set ortb2.user.keywords + const currentKeywords = deepAccess(ortbConfig, 'ortb2.user.keywords') + const keywordGroups = { + [PERMUTIVE_STANDARD_KEYWORD]: segmentIDs, + [PERMUTIVE_STANDARD_AUD_KEYWORD]: sspSegmentIDs, + [PERMUTIVE_CUSTOM_COHORTS_KEYWORD]: customCohortsData, + } + + // Transform groups of key-values into a single array of strings + // i.e { permutive: ['1', '2'], p_standard: ['3', '4'] } => ['permutive=1', 'permutive=2', 'p_standard=3',' p_standard=4'] + const transformedKeywordGroups = Object.entries(keywordGroups) + .flatMap(([keyword, ids]) => ids.map(id => `${keyword}=${id}`)) + + const keywords = [ + currentKeywords, + ...transformedKeywordGroups, + ] + .filter(Boolean) + .join(',') + + logger.logInfo(`Updating ortb2.user.keywords`, { + bidder, + keywords, + }) + deepSetValue(ortbConfig, 'ortb2.user.keywords', keywords) + + // Set user extensions + if (segmentIDs.length > 0) { + deepSetValue(ortbConfig, `ortb2.user.ext.data.${PERMUTIVE_STANDARD_KEYWORD}`, segmentIDs) + logger.logInfo(`Extending ortb2.user.ext.data with "${PERMUTIVE_STANDARD_KEYWORD}"`, segmentIDs) + } + + if (customCohortsData.length > 0) { + deepSetValue(ortbConfig, `ortb2.user.ext.data.${PERMUTIVE_CUSTOM_COHORTS_KEYWORD}`, customCohortsData.map(String)) + logger.logInfo(`Extending ortb2.user.ext.data with "${PERMUTIVE_CUSTOM_COHORTS_KEYWORD}"`, customCohortsData) + } + + logger.logInfo(`Updated ortb2 config`, { bidder, config: ortbConfig }) return ortbConfig } @@ -222,38 +270,31 @@ function getCustomBidderFn (moduleConfig, bidder) { * @return {Object} Bidder function */ function getDefaultBidderFn (bidder) { + const isPStandardTargetingEnabled = (data, acEnabled) => { + return (acEnabled && data.ac && data.ac.length) || (data.ssp && data.ssp.cohorts && data.ssp.cohorts.length) + } + const pStandardTargeting = (data, acEnabled) => { + const ac = (acEnabled) ? (data.ac ?? []) : [] + const ssp = data?.ssp?.cohorts ?? [] + return [...new Set([...ac, ...ssp])] + } const bidderMap = { - appnexus: function (bid, data, acEnabled) { - if (acEnabled && data.ac && data.ac.length) { - deepSetValue(bid, 'params.keywords.p_standard', data.ac) - } - if (data.appnexus && data.appnexus.length) { - deepSetValue(bid, 'params.keywords.permutive', data.appnexus) - } - - return bid - }, - rubicon: function (bid, data, acEnabled) { - if (acEnabled && data.ac && data.ac.length) { - deepSetValue(bid, 'params.visitor.p_standard', data.ac) - } - if (data.rubicon && data.rubicon.length) { - const rubiconCohorts = deepAccess(bid, 'params.video') ? data.rubicon.map(String) : data.rubicon - deepSetValue(bid, 'params.visitor.permutive', rubiconCohorts) - } - - return bid - }, ozone: function (bid, data, acEnabled) { - if (acEnabled && data.ac && data.ac.length) { - deepSetValue(bid, 'params.customData.0.targeting.p_standard', data.ac) + if (isPStandardTargetingEnabled(data, acEnabled)) { + const segments = pStandardTargeting(data, acEnabled) + deepSetValue(bid, 'params.customData.0.targeting.p_standard', segments) } return bid } } - return bidderMap[bidder] + // On no default bidder just return the same bid as passed in + function bidIdentity(bid) { + return bid + } + + return bidderMap[bidder] || bidIdentity } /** @@ -281,19 +322,30 @@ export function isPermutiveOnPage () { * @return {Object} */ export function getSegments (maxSegs) { - const legacySegs = readSegments('_psegs').map(Number).filter(seg => seg >= 1000000).map(String) - const _ppam = readSegments('_ppam') - const _pcrprs = readSegments('_pcrprs') + const legacySegs = readSegments('_psegs', []).map(Number).filter(seg => seg >= 1000000).map(String) + const _ppam = readSegments('_ppam', []) + const _pcrprs = readSegments('_pcrprs', []) const segments = { ac: [..._pcrprs, ..._ppam, ...legacySegs], - rubicon: readSegments('_prubicons'), - appnexus: readSegments('_papns'), - gam: readSegments('_pdfps'), + ix: readSegments('_pindexs', []), + rubicon: readSegments('_prubicons', []), + appnexus: readSegments('_papns', []), + gam: readSegments('_pdfps', []), + ssp: readSegments('_pssps', { + cohorts: [], + ssps: [] + }), } for (const bidder in segments) { - segments[bidder] = segments[bidder].slice(0, maxSegs) + if (bidder === 'ssp') { + if (segments[bidder].cohorts && Array.isArray(segments[bidder].cohorts)) { + segments[bidder].cohorts = segments[bidder].cohorts.slice(0, maxSegs) + } + } else { + segments[bidder] = segments[bidder].slice(0, maxSegs) + } } return segments @@ -301,15 +353,17 @@ export function getSegments (maxSegs) { /** * Gets an array of segment IDs from LocalStorage - * or returns an empty array + * or return the default value provided. + * @template A * @param {string} key - * @return {string[]|number[]} + * @param {A} defaultValue + * @return {A} */ -function readSegments (key) { +function readSegments (key, defaultValue) { try { - return JSON.parse(storage.getDataFromLocalStorage(key) || '[]') + return JSON.parse(storage.getDataFromLocalStorage(key)) || defaultValue } catch (e) { - return [] + return defaultValue } } @@ -341,17 +395,55 @@ function iabSegmentId(permutiveSegmentId, iabIds) { return iabIds[permutiveSegmentId] || unknownIabSegmentId } +/** + * Pull the latest configuration and cohort information and update accordingly. + * + * @param reqBidsConfigObj - Bidder provided config for request + * @param customModuleConfig - Publisher provide config + */ +export function readAndSetCohorts(reqBidsConfigObj, moduleConfig) { + const segmentData = getSegments(deepAccess(moduleConfig, 'params.maxSegs')) + + makeSafe(function () { + // Legacy route with custom parameters + // ACK policy violation, in process of removing + setSegments(reqBidsConfigObj, moduleConfig, segmentData) + }); + + makeSafe(function () { + // Route for bidders supporting ORTB2 + setBidderRtb(reqBidsConfigObj.ortb2Fragments?.bidder, moduleConfig, segmentData) + }) +} + +let permutiveSDKInRealTime = false + /** @type {RtdSubmodule} */ export const permutiveSubmodule = { name: MODULE_NAME, getBidRequestData: function (reqBidsConfigObj, callback, customModuleConfig) { + const completeBidRequestData = () => { + logger.logInfo(`Request data updated`) + callback() + } + + const moduleConfig = getModuleConfig(customModuleConfig) + + readAndSetCohorts(reqBidsConfigObj, moduleConfig) + makeSafe(function () { - // Legacy route with custom parameters - initSegments(reqBidsConfigObj, callback, customModuleConfig) - }); - makeSafe(function () { - // Route for bidders supporting ORTB2 - setBidderRtb(reqBidsConfigObj.ortb2Fragments?.bidder, customModuleConfig) + if (permutiveSDKInRealTime || !(moduleConfig.waitForIt && isPermutiveOnPage())) { + return completeBidRequestData() + } + + window.permutive.ready(function () { + logger.logInfo(`SDK is realtime, updating cohorts`) + permutiveSDKInRealTime = true + readAndSetCohorts(reqBidsConfigObj, getModuleConfig(customModuleConfig)) + completeBidRequestData() + }, 'realtime') + + logger.logInfo(`Registered cohort update when SDK is realtime`) }) }, init: init diff --git a/modules/permutiveRtdProvider.md b/modules/permutiveRtdProvider.md index 7d8f073357c..9399dffab93 100644 --- a/modules/permutiveRtdProvider.md +++ b/modules/permutiveRtdProvider.md @@ -13,7 +13,7 @@ gulp build --modules=rtdModule,permutiveRtdProvider > Note that the global RTD module, `rtdModule`, is a prerequisite of the Permutive RTD module. -You then need to enable the Permutive RTD in your Prebid configuration, using the below format: +You then need to enable the Permutive RTD in your Prebid configuration. Below is an example of the format: ```javascript pbjs.setConfig({ @@ -37,59 +37,16 @@ pbjs.setConfig({ The parameters below provide configurability for general behaviours of the RTD submodule, as well as enabling settings for specific use cases mentioned above (e.g. acbidders). -| Name | Type | Description | Default | -|------------------------|----------|-----------------------------------------------------------------------------------------------|---------| -| name | String | This should always be `permutive` | - | -| waitForIt | Boolean | Should be `true` if there's an `auctionDelay` defined (optional) | `false` | -| params | Object | | - | -| params.acBidders | String[] | An array of bidders which should receive AC cohorts. | `[]` | -| params.maxSegs | Integer | Maximum number of cohorts to be included in either the `permutive` or `p_standard` key-value. | `500` | -| params.transformations | Object[] | An array of configurations for ORTB2 user data transformations | | -| params.overwrites | Object | An object specifying functions for custom targeting logic for bidders. | - | - -##### The `transformations` parameter - -This array contains configurations for transformations we'll apply to the Permutive object in the ORTB2 `user.data` array. The results of these transformations will be appended to the `user.data` array that's attached to ORTB2 bid requests. - -###### Supported transformations - -| Name | ID | Config structure | Description | -|----------------|-----|---------------------------------------------------|--------------------------------------------------------------------------------------| -| IAB taxonomies | iab | { segtax: number, iabIds: Object} | Transform segment IDs from Permutive to IAB (note: alpha version, subject to change) | - -##### The `overwrites` parameter - -The keys for this object should match a bidder (e.g. `rubicon`), which then can define a function to overwrite the customer targeting logic. - -```javascript -{ - params: { - overwrites: { - rubicon: function customTargeting(bid, data, acEnabled, utils, defaultFn) { - if (defaultFn) { - bid = defaultFn(bid, data, acEnabled) - } - if (data.gam && data.gam.length) { - utils.deepSetValue(bid, 'params.visitor.permutive', data.gam) - } - } - } - } -} -``` - -###### `customTargeting` function parameters - -| Name | Description | -|--------------|--------------------------------------------------------------------------------| -| `bid` | The bid request object. | -| `data` | Permutive's targeting data read from localStorage. | -| `acEnabled` | Boolean stating whether Audience Connect is enabled via `acBidders`. | -| `utils` | An object of helpful utilities. `(deepSetValue, deepAccess, isFn, mergeDeep)`. | -| `defaultFn` | The default targeting function. | - - +## Parameters +{: .table .table-bordered .table-striped } +| Name | Type | Description | Default | +| ---------------------- | -------------------- | --------------------------------------------------------------------------------------------- | ------------------ | +| name | String | This should always be `permutive` | - | +| waitForIt | Boolean | Should be `true` if there's an `auctionDelay` defined (optional) | `false` | +| params | Object | | - | +| params.acBidders | String[] | An array of bidder codes to share cohorts with in certain versions of Prebid, see below | `[]` | +| params.maxSegs | Integer | Maximum number of cohorts to be included in either the `permutive` or `p_standard` key-value. | `500` | #### Context @@ -100,7 +57,7 @@ As Prebid utilizes TCF vendor consent, for the Permutive RTD module to load, Per #### Instructions -1. Publisher enables GDPR rules within Prebid. +1. Publisher enables rules within Prebid GDPR module 2. Label Permutive as an exception, as shown below. ```javascript [ @@ -123,29 +80,58 @@ Before making any updates to this configuration, please ensure that this approac ## Cohort Activation with Permutive RTD Module -### _Enabling Standard Cohorts_ - **Note**: Publishers must be enabled on the above Permutive RTD Submodule to enable Standard Cohorts. -The acbidders config in the Permutive RTD module allows publishers to determine which demand partners (SSPs) will receive standard cohorts via the user.data ortb2 object. Cohorts will be sent in the `p_standard` key-value. +### _Enabling Publisher Cohorts_ + +#### Standard Cohorts + +The Permutive RTD module sets Standard Cohort IDs as bidder-specific ortb2.user.data first-party data, following the Prebid ortb2 convention. Cohorts will be sent in the `p_standard` key-value. + +For Prebid versions below 7.29.0, populate the acbidders config in the Permutive RTD with an array of bidder codes with whom you wish to share Standard Cohorts with. You also need to permission the bidders by communicating the bidder list to the Permutive team at strategicpartnershipops@permutive.com. -The Permutive RTD module sets standard cohort IDs as bidder-specific ortb2.user.data first-party data, following the Prebid ortb2 convention. +For Prebid versions 7.29.0 and above, do not populate bidder codes in acbidders for the purpose of sharing Standard Cohorts (Note: there may be other business needs that require you to populate acbidders for Prebid versions 7.29.0+, see Advertiser Cohorts below). To share Standard Cohorts with bidders in Prebid versions 7.29.0 and above, communicate the bidder list to the Permutive team at strategicpartnershipops@permutive.com. -There are **two** ways to assign which demand partner bidders (e.g. SSPs) will receive Standard Cohort information via the Audience Connector (acbidders) config: +#### _Bidder Specific Requirements for Standard Cohorts_ +For PubMatic or OpenX: Please ensure you are using Prebid.js 7.13 (or later) +For Xandr: Please ensure you are using Prebid.js 7.29 (or later) +For Equativ: Please ensure you are using Prebid.js 7.26 (or later) + +#### Custom Cohorts + +The Permutive RTD module also supports passing any of the **Custom** Cohorts created in the dashboard to some SSP partners for targeting +e.g. setting up publisher deals. For these activations, cohort IDs are set in bidder-specific locations per ad unit (custom parameters). + +Currently, bidders with known support for custom cohort targeting are: + +- Xandr +- Magnite + +When enabling the respective Activation for a cohort in Permutive, this module will automatically attach that cohort ID to the bid request. +There is no need to enable individual bidders in the module configuration, it will automatically reflect which SSP integrations you have enabled in your Permutive dashboard. +Permutive cohorts will be sent in the permutive key-value. + + +### _Enabling Advertiser Cohorts_ + +If you are connecting to an Advertiser seat within Permutive to share Advertiser Cohorts, populate the acbidders config in the Permutive RTD with an array of bidder codes with whom you wish to share Advertiser Cohorts with. + +### _Managing acbidders_ + +If your business needs require you to populate acbidders with bidder codes based on the criteria above, there are **two** ways to manage it. #### Option 1 - Automated -New demand partner bidders may be added to the acbidders config directly within the Permutive Platform. +If you are using Prebid.js v7.13.0+, bidders may be added to or removed from the acbidders config directly within the Permutive Dashboard. **Permutive can do this on your behalf**. Simply contact your Permutive CSM with strategicpartnershipops@permutive.com on cc, indicating which bidders you would like added. -Or, a publisher may do this themselves within the UI using the below instructions. +Or, a publisher may do this themselves within the Permutive Dashboard using the below instructions. ##### Create Integration -In order to update acbidders via the Permutive dashboard, -it is necessary to first enable the prebid integration in the integrations page (settings). +In order to manage acbidders via the Permutive dashboard, it is necessary to first enable the Prebid integration via the integrations page (settings). **Note on Revenue Insights:** The prebid integration includes a feature for revenue insights, which is not required for the purpose of updating acbidders config. @@ -153,31 +139,15 @@ Please see [this document](https://support.permutive.com/hc/en-us/articles/36001 ##### Update acbidders -The input for the “Data Provider config” is currently a multi-input free text. -A valid “bidder code” needs to be entered in order to enable Standard Cohorts to be passed to the desired partner. -The [prebid Bidders page](https://docs.prebid.org/dev-docs/bidders.html) contains instructions and a link to a list of possible bidder codes. +The input for the “Data Provider config” is a multi-input free text. A valid “bidder code” needs to be entered in order to enable Standard or Advertiser Cohorts to be passed to the desired partner. The [prebid Bidders page](https://docs.prebid.org/dev-docs/bidders.html) contains instructions and a link to a list of possible bidder codes. -Acbidders can be added or removed from the list using this feature, however, this will not impact any acbidders that have been applied using the manual method below. +Bidders can be added or removed from acbidders using this feature, however, this will not impact any bidders that have been applied using the manual method below. #### Option 2 - Manual -As a secondary option, new demand partner bidders may be added manually. +As a secondary option, bidders may be added manually. -To do so, a Publisher may define which bidders should receive Standard Cohorts by +To do so, define which bidders should receive Standard or Advertiser Cohorts by including the _bidder code_ of any bidder in the `acBidders` array. -**Note:** If a Publisher ever needs to remove a manually-added bidder, the bidder will also need to be removed manually. - -### _Enabling Custom Cohort IDs for Targeting_ - -Separately from Standard Cohorts - The Permutive RTD module also supports passing any of the **custom** cohorts created in the dashboard to some SSP partners for targeting -e.g. setting up publisher deals. For these activations, cohort IDs are set in bidder-specific locations per ad unit (custom parameters). - -Currently, bidders with known support for custom cohort targeting are: - -- Xandr -- Magnite - -When enabling the respective Activation for a cohort in Permutive, this module will automatically attach that cohort ID to the bid request. -There is no need to enable individual bidders in the module configuration, it will automatically reflect which SSP integrations you have enabled in your Permutive dashboard. -Permutive cohorts will be sent in the permutive key-value. +**Note:** If you ever need to remove a manually-added bidder, the bidder will also need to be removed manually. diff --git a/modules/pixfutureBidAdapter.js b/modules/pixfutureBidAdapter.js index 41b1151e72a..5019b31b90b 100644 --- a/modules/pixfutureBidAdapter.js +++ b/modules/pixfutureBidAdapter.js @@ -14,6 +14,7 @@ import { transformBidderParamKeywords } from '../src/utils.js'; import { auctionManager } from '../src/auctionManager.js'; +import {getGlobal} from '../src/prebidGlobal.js'; const SOURCE = 'pbjs'; const storageManager = getStorageManager({bidderCode: 'pixfuture'}); @@ -132,7 +133,7 @@ export const spec = { method: 'POST', options: {withCredentials: true}, data: { - v: $$PREBID_GLOBAL$$.version, + v: getGlobal().version, pageUrl: referer, bidId: bidRequest.bidId, auctionId: bidRequest.auctionId, diff --git a/modules/prebidServerBidAdapter/index.js b/modules/prebidServerBidAdapter/index.js index 072c280aecf..ffb02204ed0 100644 --- a/modules/prebidServerBidAdapter/index.js +++ b/modules/prebidServerBidAdapter/index.js @@ -20,7 +20,7 @@ import { import CONSTANTS from '../../src/constants.json'; import adapterManager from '../../src/adapterManager.js'; import {config} from '../../src/config.js'; -import {isValid} from '../../src/adapters/bidderFactory.js'; +import {addComponentAuction, isValid} from '../../src/adapters/bidderFactory.js'; import * as events from '../../src/events.js'; import {includes} from '../../src/polyfill.js'; import {S2S_VENDORS} from './config.js'; @@ -216,16 +216,29 @@ export function resetSyncedStatus() { /** * @param {Array} bidderCodes list of bidders to request user syncs for. */ -function queueSync(bidderCodes, gdprConsent, uspConsent, s2sConfig) { +function queueSync(bidderCodes, gdprConsent, uspConsent, gppConsent, s2sConfig) { if (_s2sConfigs.length === _syncCount) { return; } _syncCount++; + let filterSettings = {}; + const userSyncFilterSettings = getConfig('userSync.filterSettings'); + + if (userSyncFilterSettings) { + const { all, iframe, image } = userSyncFilterSettings; + const ifrm = iframe || all; + const img = image || all; + + if (ifrm) filterSettings = Object.assign({ iframe: ifrm }, filterSettings); + if (img) filterSettings = Object.assign({ image: img }, filterSettings); + } + const payload = { uuid: generateUUID(), bidders: bidderCodes, - account: s2sConfig.accountId + account: s2sConfig.accountId, + filterSettings }; let userSyncLimit = s2sConfig.userSyncLimit; @@ -246,6 +259,15 @@ function queueSync(bidderCodes, gdprConsent, uspConsent, s2sConfig) { payload.us_privacy = uspConsent; } + if (gppConsent) { + // proposing the following formatting, can adjust if needed... + // update - leaving this param as an array, since it's part of a POST payload where the [] characters shouldn't matter too much + payload.gpp_sid = gppConsent.applicableSections + // should we add check if applicableSections was not equal to -1 (where user was out of scope)? + // this would be similar to what was done above for TCF + payload.gpp = gppConsent.gppString; + } + if (typeof s2sConfig.coopSync === 'boolean') { payload.coopSync = s2sConfig.coopSync; } @@ -330,7 +352,7 @@ function doBidderSync(type, url, bidder, done, timeout) { * * @param {Array} bidders a list of bidder names */ -function doClientSideSyncs(bidders, gdprConsent, uspConsent) { +function doClientSideSyncs(bidders, gdprConsent, uspConsent, gppConsent) { bidders.forEach(bidder => { let clientAdapter = adapterManager.getBidAdapter(bidder); if (clientAdapter && clientAdapter.registerSyncs) { @@ -341,7 +363,8 @@ function doClientSideSyncs(bidders, gdprConsent, uspConsent) { clientAdapter, [], gdprConsent, - uspConsent + uspConsent, + gppConsent ) ); } @@ -411,12 +434,13 @@ function getMatchingConsentUrl(urlProp, gdprConsent) { } function getConsentData(bidRequests) { - let gdprConsent, uspConsent; + let gdprConsent, uspConsent, gppConsent; if (Array.isArray(bidRequests) && bidRequests.length > 0) { gdprConsent = bidRequests[0].gdprConsent; uspConsent = bidRequests[0].uspConsent; + gppConsent = bidRequests[0].gppConsent; } - return { gdprConsent, uspConsent }; + return { gdprConsent, uspConsent, gppConsent }; } /** @@ -433,7 +457,7 @@ export function PrebidServer() { done = adapterMetrics.startTiming('total').stopBefore(done); bidRequests.forEach(req => useMetrics(req.metrics).join(adapterMetrics, {continuePropagation: false})); - let { gdprConsent, uspConsent } = getConsentData(bidRequests); + let { gdprConsent, uspConsent, gppConsent } = getConsentData(bidRequests); if (Array.isArray(_s2sConfigs)) { if (s2sBidRequest.s2sConfig && s2sBidRequest.s2sConfig.syncEndpoint && getMatchingConsentUrl(s2sBidRequest.s2sConfig.syncEndpoint, gdprConsent)) { @@ -441,16 +465,19 @@ export function PrebidServer() { .map(bidder => adapterManager.aliasRegistry[bidder] || bidder) .filter((bidder, index, array) => (array.indexOf(bidder) === index)); - queueSync(syncBidders, gdprConsent, uspConsent, s2sBidRequest.s2sConfig); + queueSync(syncBidders, gdprConsent, uspConsent, gppConsent, s2sBidRequest.s2sConfig); } processPBSRequest(s2sBidRequest, bidRequests, ajax, { - onResponse: function (isValid, requestedBidders) { + onResponse: function (isValid, requestedBidders, response) { if (isValid) { bidRequests.forEach(bidderRequest => events.emit(CONSTANTS.EVENTS.BIDDER_DONE, bidderRequest)); } + if (shouldEmitNonbids(s2sBidRequest.s2sConfig, response)) { + emitNonBids(response.ext.seatnonbid, bidRequests[0].auctionId); + } done(); - doClientSideSyncs(requestedBidders, gdprConsent, uspConsent); + doClientSideSyncs(requestedBidders, gdprConsent, uspConsent, gppConsent); }, onError: done, onBid: function ({adUnit, bid}) { @@ -469,6 +496,9 @@ export function PrebidServer() { addBidResponse.reject(adUnit, bid, CONSTANTS.REJECTION_REASON.INVALID); } } + }, + onFledge: ({adUnitCode, config}) => { + addComponentAuction(adUnitCode, config); } }) } @@ -494,7 +524,7 @@ export function PrebidServer() { * @param onError {function(String, {})} invoked on HTTP failure - with status message and XHR error * @param onBid {function({})} invoked once for each bid in the response - with the bid as returned by interpretResponse */ -export const processPBSRequest = hook('sync', function (s2sBidRequest, bidRequests, ajax, {onResponse, onError, onBid}) { +export const processPBSRequest = hook('sync', function (s2sBidRequest, bidRequests, ajax, {onResponse, onError, onBid, onFledge}) { let { gdprConsent } = getConsentData(bidRequests); const adUnits = deepClone(s2sBidRequest.ad_units); @@ -518,8 +548,11 @@ export const processPBSRequest = hook('sync', function (s2sBidRequest, bidReques let result; try { result = JSON.parse(response); - const bids = s2sBidRequest.metrics.measureTime('interpretResponse', () => interpretPBSResponse(result, request).bids); + const {bids, fledgeAuctionConfigs} = s2sBidRequest.metrics.measureTime('interpretResponse', () => interpretPBSResponse(result, request)); bids.forEach(onBid); + if (fledgeAuctionConfigs) { + fledgeAuctionConfigs.forEach(onFledge); + } } catch (error) { logError(error); } @@ -527,7 +560,7 @@ export const processPBSRequest = hook('sync', function (s2sBidRequest, bidReques logError('error parsing response: ', result ? result.status : 'not valid JSON'); onResponse(false, requestedBidders); } else { - onResponse(true, requestedBidders); + onResponse(true, requestedBidders, result); } }, error: function () { @@ -543,6 +576,17 @@ export const processPBSRequest = hook('sync', function (s2sBidRequest, bidReques } }, 'processPBSRequest'); +function shouldEmitNonbids(s2sConfig, response) { + return s2sConfig?.extPrebid?.returnallbidstatus && response?.ext?.seatnonbid; +} + +function emitNonBids(seatnonbid, auctionId) { + events.emit(CONSTANTS.EVENTS.SEAT_NON_BID, { + seatnonbid, + auctionId + }); +} + /** * Global setter that sets eids permissions for bidders * This setter is to be used by userId module when included diff --git a/modules/prebidServerBidAdapter/ortbConverter.js b/modules/prebidServerBidAdapter/ortbConverter.js index 0aee261c25d..820c34c66fd 100644 --- a/modules/prebidServerBidAdapter/ortbConverter.js +++ b/modules/prebidServerBidAdapter/ortbConverter.js @@ -32,6 +32,12 @@ const PBS_CONVERTER = ortbConverter({ imp(buildImp, proxyBidRequest, context) { Object.assign(context, proxyBidRequest.pbsData); const imp = buildImp(proxyBidRequest, context); + (proxyBidRequest.bids || []).forEach(bid => { + if (bid.ortb2Imp && Object.keys(bid.ortb2Imp).length > 0) { + // set bidder-level imp attributes; see https://github.com/prebid/prebid-server/issues/2335 + deepSetValue(imp, `ext.prebid.imp.${bid.bidder}`, bid.ortb2Imp); + } + }); if (Object.values(SUPPORTED_MEDIA_TYPES).some(mtype => imp[mtype])) { imp.secure = context.s2sBidRequest.s2sConfig.secure; return imp; @@ -47,7 +53,7 @@ const PBS_CONVERTER = ortbConverter({ request.tmax = s2sBidRequest.s2sConfig.timeout; deepSetValue(request, 'source.tid', proxyBidderRequest.auctionId); - [request.app, request.site].forEach(section => { + [request.app, request.dooh, request.site].forEach(section => { if (section && !section.publisher?.id) { deepSetValue(section, 'publisher.id', s2sBidRequest.s2sConfig.accountId); } @@ -215,6 +221,13 @@ const PBS_CONVERTER = ortbConverter({ serverSideStats(orig, response, ortbResponse, context) { // override to process each request context.actualBidderRequests.forEach(req => orig(response, ortbResponse, {...context, bidderRequest: req, bidRequests: req.bids})); + }, + fledgeAuctionConfigs(orig, response, ortbResponse, context) { + const configs = Object.values(context.impContext) + .flatMap((impCtx) => (impCtx.fledgeConfigs || []).map(cfg => ({adUnitCode: impCtx.adUnit.code, config: cfg.config}))); + if (configs.length > 0) { + response.fledgeAuctionConfigs = configs; + } } } }, @@ -246,12 +259,16 @@ export function buildPBSRequest(s2sBidRequest, bidderRequests, adUnits, requeste impIds.add(impId) proxyBidRequests.push({ ...adUnit, + adUnitCode: adUnit.code, ...getDefinedParams(actualBidRequests.values().next().value || {}, ['userId', 'userIdAsEids', 'schain']), - pbsData: {impId, actualBidRequests, adUnit} + pbsData: {impId, actualBidRequests, adUnit}, }); }); - const proxyBidderRequest = Object.fromEntries(Object.entries(bidderRequests[0]).filter(([k]) => !BIDDER_SPECIFIC_REQUEST_PROPS.has(k))) + const proxyBidderRequest = { + ...Object.fromEntries(Object.entries(bidderRequests[0]).filter(([k]) => !BIDDER_SPECIFIC_REQUEST_PROPS.has(k))), + fledgeEnabled: bidderRequests.some(req => req.fledgeEnabled) + } return PBS_CONVERTER.toORTB({ bidderRequest: proxyBidderRequest, diff --git a/modules/prebidmanagerAnalyticsAdapter.js b/modules/prebidmanagerAnalyticsAdapter.js index 1180a74db30..b877918d16d 100644 --- a/modules/prebidmanagerAnalyticsAdapter.js +++ b/modules/prebidmanagerAnalyticsAdapter.js @@ -2,16 +2,17 @@ import { generateUUID, getParameterByName, logError, parseUrl, logInfo } from '. import {ajaxBuilder} from '../src/ajax.js'; import adapter from '../libraries/analyticsAdapter/AnalyticsAdapter.js'; import adapterManager from '../src/adapterManager.js'; -import { getStorageManager } from '../src/storageManager.js'; +import {getStorageManager} from '../src/storageManager.js'; import CONSTANTS from '../src/constants.json'; +import {MODULE_TYPE_ANALYTICS} from '../src/activities/modules.js'; /** * prebidmanagerAnalyticsAdapter.js - analytics adapter for prebidmanager */ -export const storage = getStorageManager({gvlid: undefined, moduleName: 'prebidmanager'}); -const DEFAULT_EVENT_URL = 'https://endpoint.prebidmanager.com/endpoint'; +export const storage = getStorageManager({moduleType: MODULE_TYPE_ANALYTICS, moduleName: 'prebidmanager'}); +const DEFAULT_EVENT_URL = 'https://endpt.prebidmanager.com/endpoint'; const analyticsType = 'endpoint'; -const analyticsName = 'Prebid Manager Analytics: '; +const analyticsName = 'Prebid Manager Analytics'; let ajax = ajaxBuilder(0); @@ -28,8 +29,8 @@ var w = window; var d = document; var e = d.documentElement; var g = d.getElementsByTagName('body')[0]; -var x = w.innerWidth || e.clientWidth || g.clientWidth; -var y = w.innerHeight || e.clientHeight || g.clientHeight; +var x = (w && w.innerWidth) || (e && e.clientWidth) || (g && g.clientWidth); +var y = (w && w.innerHeight) || (e && e.clientHeight) || (g && g.clientHeight); var _pageView = { eventType: 'pageView', @@ -56,13 +57,20 @@ prebidmanagerAnalytics.originEnableAnalytics = prebidmanagerAnalytics.enableAnal prebidmanagerAnalytics.enableAnalytics = function (config) { initOptions = config.options || {}; initOptions.url = initOptions.url || DEFAULT_EVENT_URL; - pmAnalyticsEnabled = true; + initOptions.sampling = initOptions.sampling || 1; + + if (Math.floor(Math.random() * initOptions.sampling) === 0) { + pmAnalyticsEnabled = true; + flushInterval = setInterval(flush, 1000); + } else { + logInfo(`${analyticsName} isn't enabled because of sampling`); + } + prebidmanagerAnalytics.originEnableAnalytics(config); - flushInterval = setInterval(flush, 1000); }; prebidmanagerAnalytics.originDisableAnalytics = prebidmanagerAnalytics.disableAnalytics; -prebidmanagerAnalytics.disableAnalytics = function() { +prebidmanagerAnalytics.disableAnalytics = function () { if (!pmAnalyticsEnabled) { return; } @@ -95,7 +103,7 @@ function collectUtmTagData() { }); } } catch (e) { - logError(`${analyticsName}Error`, e); + logError(`${analyticsName} Error`, e); pmUtmTags['error_utm'] = 1; } return pmUtmTags; @@ -126,6 +134,16 @@ function flush() { pageInfo: collectPageInfo(), }; + if ('version' in initOptions) { + data.version = initOptions.version; + } + if ('tcf_compliant' in initOptions) { + data.tcf_compliant = initOptions.tcf_compliant; + } + if ('sampling' in initOptions) { + data.sampling = initOptions.sampling; + } + ajax( initOptions.url, () => logInfo(`${analyticsName} sent events batch`), @@ -142,65 +160,156 @@ function flush() { } } +function trimAdUnit(adUnit) { + if (!adUnit) return adUnit; + const res = {}; + res.code = adUnit.code; + res.sizes = adUnit.sizes; + return res; +} + +function trimBid(bid) { + if (!bid) return bid; + const res = {}; + res.auctionId = bid.auctionId; + res.bidder = bid.bidder; + res.bidderRequestId = bid.bidderRequestId; + res.bidId = bid.bidId; + res.crumbs = bid.crumbs; + res.cpm = bid.cpm; + res.currency = bid.currency; + res.mediaTypes = bid.mediaTypes; + res.sizes = bid.sizes; + res.transactionId = bid.transactionId; + res.adUnitCode = bid.adUnitCode; + res.bidRequestsCount = bid.bidRequestsCount; + res.serverResponseTimeMs = bid.serverResponseTimeMs; + return res; +} + +function trimBidderRequest(bidderRequest) { + if (!bidderRequest) return bidderRequest; + const res = {}; + res.auctionId = bidderRequest.auctionId; + res.auctionStart = bidderRequest.auctionStart; + res.bidderRequestId = bidderRequest.bidderRequestId; + res.bidderCode = bidderRequest.bidderCode; + res.bids = bidderRequest.bids && bidderRequest.bids.map(trimBid); + return res; +} + function handleEvent(eventType, eventArgs) { - eventArgs = eventArgs ? JSON.parse(JSON.stringify(eventArgs)) : {}; - var pmEvent = {}; + try { + eventArgs = eventArgs ? JSON.parse(JSON.stringify(eventArgs)) : {}; + } catch (e) { + // keep eventArgs as is + } + + const pmEvent = {}; switch (eventType) { case CONSTANTS.EVENTS.AUCTION_INIT: { - pmEvent = eventArgs; + pmEvent.auctionId = eventArgs.auctionId; + pmEvent.timeout = eventArgs.timeout; + pmEvent.eventType = eventArgs.eventType; + pmEvent.adUnits = eventArgs.adUnits && eventArgs.adUnits.map(trimAdUnit) + pmEvent.bidderRequests = eventArgs.bidderRequests && eventArgs.bidderRequests.map(trimBidderRequest) _startAuction = pmEvent.timestamp; _bidRequestTimeout = pmEvent.timeout; break; } case CONSTANTS.EVENTS.AUCTION_END: { - pmEvent = eventArgs; + pmEvent.auctionId = eventArgs.auctionId; + pmEvent.end = eventArgs.end; + pmEvent.start = eventArgs.start; + pmEvent.adUnitCodes = eventArgs.adUnitCodes; + pmEvent.bidsReceived = eventArgs.bidsReceived && eventArgs.bidsReceived.map(trimBid); pmEvent.start = _startAuction; pmEvent.end = Date.now(); break; } case CONSTANTS.EVENTS.BID_ADJUSTMENT: { - pmEvent.bidders = eventArgs; break; } case CONSTANTS.EVENTS.BID_TIMEOUT: { - pmEvent.bidders = eventArgs; + pmEvent.bidders = eventArgs && eventArgs.map ? eventArgs.map(trimBid) : eventArgs; pmEvent.duration = _bidRequestTimeout; break; } case CONSTANTS.EVENTS.BID_REQUESTED: { - pmEvent = eventArgs; + pmEvent.auctionId = eventArgs.auctionId; + pmEvent.bidderCode = eventArgs.bidderCode; + pmEvent.doneCbCallCount = eventArgs.doneCbCallCount; + pmEvent.start = eventArgs.start; + pmEvent.bidderRequestId = eventArgs.bidderRequestId; + pmEvent.bids = eventArgs.bids && eventArgs.bids.map(trimBid); + pmEvent.auctionStart = eventArgs.auctionStart; + pmEvent.timeout = eventArgs.timeout; break; } case CONSTANTS.EVENTS.BID_RESPONSE: { - pmEvent = eventArgs; - delete pmEvent.ad; + pmEvent.bidderCode = eventArgs.bidderCode; + pmEvent.width = eventArgs.width; + pmEvent.height = eventArgs.height; + pmEvent.adId = eventArgs.adId; + pmEvent.mediaType = eventArgs.mediaType; + pmEvent.cpm = eventArgs.cpm; + pmEvent.currency = eventArgs.currency; + pmEvent.requestId = eventArgs.requestId; + pmEvent.adUnitCode = eventArgs.adUnitCode; + pmEvent.auctionId = eventArgs.auctionId; + pmEvent.timeToRespond = eventArgs.timeToRespond; + pmEvent.responseTimestamp = eventArgs.responseTimestamp; + pmEvent.requestTimestamp = eventArgs.requestTimestamp; + pmEvent.netRevenue = eventArgs.netRevenue; + pmEvent.size = eventArgs.size; + pmEvent.adserverTargeting = eventArgs.adserverTargeting; break; } case CONSTANTS.EVENTS.BID_WON: { - pmEvent = eventArgs; - delete pmEvent.ad; - delete pmEvent.adUrl; + pmEvent.auctionId = eventArgs.auctionId; + pmEvent.adId = eventArgs.adId; + pmEvent.adserverTargeting = eventArgs.adserverTargeting; + pmEvent.adUnitCode = eventArgs.adUnitCode; + pmEvent.bidderCode = eventArgs.bidderCode; + pmEvent.height = eventArgs.height; + pmEvent.mediaType = eventArgs.mediaType; + pmEvent.netRevenue = eventArgs.netRevenue; + pmEvent.cpm = eventArgs.cpm; + pmEvent.requestTimestamp = eventArgs.requestTimestamp; + pmEvent.responseTimestamp = eventArgs.responseTimestamp; + pmEvent.size = eventArgs.size; + pmEvent.width = eventArgs.width; + pmEvent.currency = eventArgs.currency; + pmEvent.bidder = eventArgs.bidder; break; } case CONSTANTS.EVENTS.BIDDER_DONE: { - pmEvent = eventArgs; + pmEvent.auctionId = eventArgs.auctionId; + pmEvent.auctionStart = eventArgs.auctionStart; + pmEvent.bidderCode = eventArgs.bidderCode; + pmEvent.bidderRequestId = eventArgs.bidderRequestId; + pmEvent.bids = eventArgs.bids && eventArgs.bids.map(trimBid); + pmEvent.doneCbCallCount = eventArgs.doneCbCallCount; + pmEvent.start = eventArgs.start; + pmEvent.timeout = eventArgs.timeout; + pmEvent.tid = eventArgs.tid; + pmEvent.src = eventArgs.src; break; } case CONSTANTS.EVENTS.SET_TARGETING: { - pmEvent.targetings = eventArgs; break; } case CONSTANTS.EVENTS.REQUEST_BIDS: { - pmEvent = eventArgs; break; } case CONSTANTS.EVENTS.ADD_AD_UNITS: { - pmEvent = eventArgs; break; } case CONSTANTS.EVENTS.AD_RENDER_FAILED: { - pmEvent = eventArgs; + pmEvent.bid = eventArgs.bid; + pmEvent.message = eventArgs.message; + pmEvent.reason = eventArgs.reason; break; } default: @@ -215,7 +324,7 @@ function handleEvent(eventType, eventArgs) { function sendEvent(event) { _eventQueue.push(event); - logInfo(`${analyticsName}Event ${event.eventType}:`, event); + logInfo(`${analyticsName} Event ${event.eventType}:`, event); if (event.eventType === CONSTANTS.EVENTS.AUCTION_END) { flush(); diff --git a/modules/priceFloors.js b/modules/priceFloors.js index 8b5976149d0..92aecb0ca50 100644 --- a/modules/priceFloors.js +++ b/modules/priceFloors.js @@ -28,6 +28,7 @@ import {auctionManager} from '../src/auctionManager.js'; import {IMP, PBS, registerOrtbProcessor, REQUEST} from '../src/pbjsORTB.js'; import {timedAuctionHook, timedBidResponseHook} from '../src/utils/perfMetrics.js'; import {beConvertCurrency} from '../src/utils/currency.js'; +import {adjustCpm} from '../src/utils/cpm.js'; /** * @summary This Module is intended to provide users with the ability to dynamically set and enforce price floors on a per auction basis. @@ -179,12 +180,8 @@ function generatePossibleEnumerations(arrayOfFields, delimiter) { /** * @summary If a the input bidder has a registered cpmadjustment it returns the input CPM after being adjusted */ -export function getBiddersCpmAdjustment(bidderName, inputCpm, bid = {}) { - const adjustmentFunction = bidderSettings.get(bidderName, 'bidCpmAdjustment'); - if (adjustmentFunction) { - return parseFloat(adjustmentFunction(inputCpm, {...bid, cpm: inputCpm})); - } - return parseFloat(inputCpm); +export function getBiddersCpmAdjustment(inputCpm, bid, bidRequest) { + return parseFloat(adjustCpm(inputCpm, {...bid, cpm: inputCpm}, bidRequest)); } /** @@ -249,8 +246,14 @@ export function getFloor(requestParams = {currency: 'USD', mediaType: '*', size: // if cpmAdjustment flag is true and we have a valid floor then run the adjustment on it if (floorData.enforcement.bidAdjustment && floorInfo.matchingFloor) { - let cpmAdjustment = getBiddersCpmAdjustment(bidRequest.bidder, floorInfo.matchingFloor); - floorInfo.matchingFloor = cpmAdjustment ? calculateAdjustedFloor(floorInfo.matchingFloor, cpmAdjustment) : floorInfo.matchingFloor; + // pub provided inverse function takes precedence, otherwise do old adjustment stuff + const inverseFunction = bidderSettings.get(bidRequest.bidder, 'inverseBidAdjustment'); + if (inverseFunction) { + floorInfo.matchingFloor = inverseFunction(floorInfo.matchingFloor, bidRequest); + } else { + let cpmAdjustment = getBiddersCpmAdjustment(floorInfo.matchingFloor, null, bidRequest); + floorInfo.matchingFloor = cpmAdjustment ? calculateAdjustedFloor(floorInfo.matchingFloor, cpmAdjustment) : floorInfo.matchingFloor; + } } if (floorInfo.matchingFloor) { @@ -307,6 +310,8 @@ export function getFloorDataFromAdUnits(adUnits) { // copy over the new rules into our values object Object.assign(accum.values, newRules); } + } else if (adUnit.floors != null) { + logWarn(`adUnit '${adUnit.code}' provides an invalid \`floor\` definition, it will be ignored for floor calculations`, adUnit); } return accum; }, {}); @@ -731,7 +736,7 @@ export const addBidResponseHook = timedBidResponseHook('priceFloors', function a } // ok we got the bid response cpm in our desired currency. Now we need to run the bidders CPMAdjustment function if it exists - adjustedCpm = getBiddersCpmAdjustment(bid.bidderCode, adjustedCpm, bid); + adjustedCpm = getBiddersCpmAdjustment(adjustedCpm, bid, matchingBidRequest); // add necessary data information for analytics adapters / floor providers would possibly need addFloorDataToBid(floorData, floorInfo, bid, adjustedCpm); diff --git a/modules/proxistoreBidAdapter.js b/modules/proxistoreBidAdapter.js index 04d363be7fb..1f113f9c432 100644 --- a/modules/proxistoreBidAdapter.js +++ b/modules/proxistoreBidAdapter.js @@ -3,9 +3,9 @@ import { registerBidder } from '../src/adapters/bidderFactory.js'; const BIDDER_CODE = 'proxistore'; const PROXISTORE_VENDOR_ID = 418; -const COOKIE_BASE_URL = 'https://abs.proxistore.com/v3/rtb/prebid/multi'; +const COOKIE_BASE_URL = 'https://api.proxistore.com/v3/rtb/prebid/multi'; const COOKIE_LESS_URL = - 'https://abs.cookieless-proxistore.com/v3/rtb/prebid/multi'; + 'https://api.cookieless-proxistore.com/v3/rtb/prebid/multi'; function _createServerRequest(bidRequests, bidderRequest) { var sizeIds = []; diff --git a/modules/pubCommonId.js b/modules/pubCommonId.js index 1fde8f8db5b..ba24190322e 100644 --- a/modules/pubCommonId.js +++ b/modules/pubCommonId.js @@ -7,11 +7,15 @@ import { logMessage, parseUrl, buildUrl, triggerPixel, generateUUID, isArray } f import { config } from '../src/config.js'; import * as events from '../src/events.js'; import CONSTANTS from '../src/constants.json'; -import { getStorageManager } from '../src/storageManager.js'; +import {getStorageManager} from '../src/storageManager.js'; import {timedAuctionHook} from '../src/utils/perfMetrics.js'; -import {VENDORLESS_GVLID} from '../src/consentHandler.js'; +import {GDPR_GVLIDS, VENDORLESS_GVLID} from '../src/consentHandler.js'; +import {MODULE_TYPE_UID} from '../src/activities/modules.js'; +import {getGlobal} from '../src/prebidGlobal.js'; -const storage = getStorageManager({moduleName: 'pubCommonId', gvlid: VENDORLESS_GVLID}); +const storage = getStorageManager({moduleType: MODULE_TYPE_UID, moduleName: 'pubCommonId'}); +// register our GVL ID directly, since this is not a "real" user ID module we don't have a spec where to declare it +GDPR_GVLIDS.register(MODULE_TYPE_UID, 'pubCommonId', VENDORLESS_GVLID); const ID_NAME = '_pubcid'; const OPTOUT_NAME = '_pubcid_optout'; @@ -168,7 +172,7 @@ export function getPubcidConfig() { return pubcidConfig; } */ export const requestBidHook = timedAuctionHook('pubCommonId', function requestBidHook(next, config) { - let adUnits = config.adUnits || $$PREBID_GLOBAL$$.adUnits; + let adUnits = config.adUnits || getGlobal().adUnits; let pubcid = null; // Pass control to the next function if not enabled @@ -292,7 +296,7 @@ export function initPubcid() { (storage.hasLocalStorage() && readValue(OPTOUT_NAME, LOCAL_STORAGE)); if (!optout) { - $$PREBID_GLOBAL$$.requestBids.before(requestBidHook); + getGlobal().requestBids.before(requestBidHook); } } diff --git a/modules/publinkIdSystem.js b/modules/publinkIdSystem.js index 9d5645a38cb..6a5504b5ba0 100644 --- a/modules/publinkIdSystem.js +++ b/modules/publinkIdSystem.js @@ -10,13 +10,14 @@ import {getStorageManager} from '../src/storageManager.js'; import {ajax} from '../src/ajax.js'; import { parseUrl, buildUrl, logError } from '../src/utils.js'; import {uspDataHandler} from '../src/adapterManager.js'; +import {MODULE_TYPE_UID} from '../src/activities/modules.js'; const MODULE_NAME = 'publinkId'; const GVLID = 24; const PUBLINK_COOKIE = '_publink'; const PUBLINK_S2S_COOKIE = '_publink_srv'; -export const storage = getStorageManager({gvlid: GVLID}); +export const storage = getStorageManager({moduleType: MODULE_TYPE_UID, moduleName: MODULE_NAME}); function isHex(s) { return /^[A-F0-9]+$/i.test(s); diff --git a/modules/pubmaticAnalyticsAdapter.js b/modules/pubmaticAnalyticsAdapter.js index c8dc7cef15d..9e2a5b1cfeb 100755 --- a/modules/pubmaticAnalyticsAdapter.js +++ b/modules/pubmaticAnalyticsAdapter.js @@ -30,6 +30,11 @@ const DEFAULT_PUBLISHER_ID = 0; const DEFAULT_PROFILE_ID = 0; const DEFAULT_PROFILE_VERSION_ID = 0; const enc = window.encodeURIComponent; +const MEDIATYPE = { + BANNER: 0, + VIDEO: 1, + NATIVE: 2 +} /// /////////// VARIABLES ////////////// let publisherId = DEFAULT_PUBLISHER_ID; // int: mandatory @@ -89,6 +94,7 @@ function copyRequiredBidDetails(bid) { 'status', () => NO_BID, // default a bid to NO_BID until response is recieved or bid is timed out 'finalSource as source', 'params', + 'floorData', 'adUnit', () => pick(bid, [ 'adUnitCode', 'transactionId', @@ -153,6 +159,7 @@ function parseBidResponse(bid) { 'bidId', 'mediaType', 'params', + 'floorData', 'mi', 'regexPattern', () => bid.regexPattern || undefined, 'partnerImpId', // partner impression ID @@ -217,6 +224,38 @@ function getAdDomain(bidResponse) { } } +function isObject(object) { + return typeof object === 'object' && object !== null; +}; + +function isEmptyObject(object) { + return isObject(object) && Object.keys(object).length === 0; +}; + +/** + * Prepare meta object to pass in logger call + * @param {*} meta + */ +export function getMetadata(meta) { + if (!meta || isEmptyObject(meta)) return; + const metaObj = {}; + if (meta.networkId) metaObj.nwid = meta.networkId; + if (meta.advertiserId) metaObj.adid = meta.advertiserId; + if (meta.networkName) metaObj.nwnm = meta.networkName; + if (meta.primaryCatId) metaObj.pcid = meta.primaryCatId; + if (meta.advertiserName) metaObj.adnm = meta.advertiserName; + if (meta.agencyId) metaObj.agid = meta.agencyId; + if (meta.agencyName) metaObj.agnm = meta.agencyName; + if (meta.brandId) metaObj.brid = meta.brandId; + if (meta.brandName) metaObj.brnm = meta.brandName; + if (meta.dchain) metaObj.dc = meta.dchain; + if (meta.demandSource) metaObj.ds = meta.demandSource; + if (meta.secondaryCatIds) metaObj.scids = meta.secondaryCatIds; + + if (isEmptyObject(metaObj)) return; + return metaObj; +} + function gatherPartnerBidsForAdUnitForLogger(adUnit, adUnitId, highestBid) { highestBid = (highestBid && highestBid.length > 0) ? highestBid[0] : null; return Object.keys(adUnit.bids).reduce(function(partnerBids, bidId) { @@ -243,17 +282,40 @@ function gatherPartnerBidsForAdUnitForLogger(adUnit, adUnitId, highestBid) { 'af': bid.bidResponse ? (bid.bidResponse.mediaType || undefined) : undefined, 'ocpm': bid.bidResponse ? (bid.bidResponse.originalCpm || 0) : 0, 'ocry': bid.bidResponse ? (bid.bidResponse.originalCurrency || CURRENCY_USD) : CURRENCY_USD, - 'piid': bid.bidResponse ? (bid.bidResponse.partnerImpId || EMPTY_STRING) : EMPTY_STRING + 'piid': bid.bidResponse ? (bid.bidResponse.partnerImpId || EMPTY_STRING) : EMPTY_STRING, + 'frv': (s2sBidders.indexOf(bid.bidder) > -1) ? undefined : (bid.bidResponse ? (bid.bidResponse.floorData ? bid.bidResponse.floorData.floorRuleValue : undefined) : undefined), + 'md': bid.bidResponse ? getMetadata(bid.bidResponse.meta) : undefined }); }); return partnerBids; }, []) } +function getSizesForAdUnit(adUnit) { + var bid = Object.values(adUnit.bids).filter((bid) => !!bid.bidResponse && bid.bidResponse.mediaType === 'native')[0]; + if (!!bid || (bid === undefined && adUnit.dimensions.length === 0)) { + return ['1x1']; + } else { + return adUnit.dimensions.map(function (e) { + return e[0] + 'x' + e[1]; + }) + } +} + +function getAdUnitAdFormats(adUnit) { + var af = adUnit ? Object.keys(adUnit.mediaTypes || {}).map(format => MEDIATYPE[format.toUpperCase()]) : []; + return af; +} + +function getAdUnit(adUnits, adUnitId) { + return adUnits.filter(adUnit => (adUnit.divID && adUnit.divID == adUnitId) || (adUnit.code == adUnitId))[0]; +} + function executeBidsLoggerCall(e, highestCpmBids) { let auctionId = e.auctionId; let referrer = config.getConfig('pageUrl') || cache.auctions[auctionId].referer || ''; let auctionCache = cache.auctions[auctionId]; + let floorData = auctionCache.floorData; let outputObj = { s: [] }; let pixelURL = END_POINT_BID_LOGGER; @@ -283,12 +345,21 @@ function executeBidsLoggerCall(e, highestCpmBids) { return 0; })(); + if (floorData) { + outputObj['fmv'] = floorData.floorRequestData ? floorData.floorRequestData.modelVersion || undefined : undefined; + outputObj['ft'] = floorData.floorResponseData ? (floorData.floorResponseData.enforcements.enforceJS == false ? 0 : 1) : undefined; + } + outputObj.s = Object.keys(auctionCache.adUnitCodes).reduce(function(slotsArray, adUnitId) { let adUnit = auctionCache.adUnitCodes[adUnitId]; + let origAdUnit = getAdUnit(auctionCache.origAdUnits, adUnitId) || {}; let slotObject = { 'sn': adUnitId, - 'sz': adUnit.dimensions.map(e => e[0] + 'x' + e[1]), - 'ps': gatherPartnerBidsForAdUnitForLogger(adUnit, adUnitId, highestCpmBids.filter(bid => bid.adUnitCode === adUnitId)) + 'au': origAdUnit.adUnitId || adUnitId, + 'mt': getAdUnitAdFormats(origAdUnit), + 'sz': getSizesForAdUnit(adUnit, adUnitId), + 'ps': gatherPartnerBidsForAdUnitForLogger(adUnit, adUnitId, highestCpmBids.filter(bid => bid.adUnitCode === adUnitId)), + 'fskp': floorData ? (floorData.floorRequestData ? (floorData.floorRequestData.skipped == false ? 0 : 1) : undefined) : undefined, }; slotsArray.push(slotObject); return slotsArray; @@ -318,6 +389,7 @@ function executeBidWonLoggerCall(auctionId, adUnitId) { } const adapterName = getAdapterNameForAlias(winningBid.adapterCode || winningBid.bidder); + let origAdUnit = getAdUnit(cache.auctions[auctionId].origAdUnits, adUnitId) || {}; let pixelURL = END_POINT_WIN_BID_LOGGER; pixelURL += 'pubid=' + publisherId; pixelURL += '&purl=' + enc(config.getConfig('pageUrl') || cache.auctions[auctionId].referer || ''); @@ -327,6 +399,7 @@ function executeBidWonLoggerCall(auctionId, adUnitId) { pixelURL += '&pid=' + enc(profileId); pixelURL += '&pdvid=' + enc(profileVersionId); pixelURL += '&slot=' + enc(adUnitId); + pixelURL += '&au=' + enc(origAdUnit.adUnitId || adUnitId); pixelURL += '&pn=' + enc(adapterName); pixelURL += '&bc=' + enc(winningBid.bidderCode || winningBid.bidder); pixelURL += '&en=' + enc(winningBid.bidResponse.bidPriceUSD); @@ -358,6 +431,8 @@ function auctionInitHandler(args) { 'bidderDonePendingCount', () => args.bidderRequests.length, ]); cacheEntry.adUnitCodes = {}; + cacheEntry.floorData = {}; + cacheEntry.origAdUnits = args.adUnits; cacheEntry.referer = args.bidderRequests[0].refererInfo.topmostLocation; cache.auctions[args.auctionId] = cacheEntry; } @@ -372,6 +447,9 @@ function bidRequestedHandler(args) { }; } cache.auctions[args.auctionId].adUnitCodes[bid.adUnitCode].bids[bid.bidId] = [copyRequiredBidDetails(bid)]; + if (bid.floorData) { + cache.auctions[args.auctionId].floorData['floorRequestData'] = bid.floorData; + } }) } @@ -386,6 +464,11 @@ function bidResponseHandler(args) { bid = copyRequiredBidDetails(args); cache.auctions[args.auctionId].adUnitCodes[args.adUnitCode].bids[args.requestId].push(bid); } + + if (args.floorData) { + cache.auctions[args.auctionId].floorData['floorResponseData'] = args.floorData; + } + bid.adId = args.adId; bid.source = formatSource(bid.source || args.source); setBidStatus(bid, args); diff --git a/modules/pubmaticBidAdapter.js b/modules/pubmaticBidAdapter.js index 296a4314ac8..43a688f20e9 100644 --- a/modules/pubmaticBidAdapter.js +++ b/modules/pubmaticBidAdapter.js @@ -1,10 +1,10 @@ -import { getBidRequest, logWarn, _each, isBoolean, isStr, isArray, inIframe, mergeDeep, deepAccess, isNumber, deepSetValue, logInfo, logError, deepClone, convertTypes, uniques } from '../src/utils.js'; +import { getBidRequest, logWarn, isBoolean, isStr, isArray, inIframe, mergeDeep, deepAccess, isNumber, deepSetValue, logInfo, logError, deepClone, convertTypes, uniques, isPlainObject, isInteger } from '../src/utils.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; import { BANNER, VIDEO, NATIVE, ADPOD } from '../src/mediaTypes.js'; import { config } from '../src/config.js'; import { Renderer } from '../src/Renderer.js'; import { bidderSettings } from '../src/bidderSettings.js'; -import { convertOrtbRequestToProprietaryNative } from '../src/native.js'; +import CONSTANTS from '../src/constants.json'; const BIDDER_CODE = 'pubmatic'; const LOG_WARN_PREFIX = 'PubMatic: '; @@ -20,6 +20,7 @@ const PREBID_NATIVE_HELP_LINK = 'http://prebid.org/dev-docs/show-native-ads.html const PUBLICATION = 'pubmatic'; // Your publication on Blue Billywig, potentially with environment (e.g. publication.bbvms.com or publication.test.bbvms.com) const RENDERER_URL = 'https://pubmatic.bbvms.com/r/'.concat('$RENDERER', '.js'); // URL of the renderer application const MSG_VIDEO_PLACEMENT_MISSING = 'Video.Placement param missing'; + const CUSTOM_PARAMS = { 'kadpageurl': '', // Custom page url 'gender': '', // User gender @@ -55,56 +56,11 @@ const VIDEO_CUSTOM_PARAMS = { 'skip': DATA_TYPES.NUMBER } -const NATIVE_ASSETS = { - 'TITLE': { ID: 1, KEY: 'title', TYPE: 0 }, - 'IMAGE': { ID: 2, KEY: 'image', TYPE: 0 }, - 'ICON': { ID: 3, KEY: 'icon', TYPE: 0 }, - 'SPONSOREDBY': { ID: 4, KEY: 'sponsoredBy', TYPE: 1 }, // please note that type of SPONSORED is also 1 - 'BODY': { ID: 5, KEY: 'body', TYPE: 2 }, // please note that type of DESC is also set to 2 - 'CLICKURL': { ID: 6, KEY: 'clickUrl', TYPE: 0 }, - 'VIDEO': { ID: 7, KEY: 'video', TYPE: 0 }, - 'EXT': { ID: 8, KEY: 'ext', TYPE: 0 }, - 'DATA': { ID: 9, KEY: 'data', TYPE: 0 }, - 'LOGO': { ID: 10, KEY: 'logo', TYPE: 0 }, - 'SPONSORED': { ID: 11, KEY: 'sponsored', TYPE: 1 }, // please note that type of SPONSOREDBY is also set to 1 - 'DESC': { ID: 12, KEY: 'data', TYPE: 2 }, // please note that type of BODY is also set to 2 - 'RATING': { ID: 13, KEY: 'rating', TYPE: 3 }, - 'LIKES': { ID: 14, KEY: 'likes', TYPE: 4 }, - 'DOWNLOADS': { ID: 15, KEY: 'downloads', TYPE: 5 }, - 'PRICE': { ID: 16, KEY: 'price', TYPE: 6 }, - 'SALEPRICE': { ID: 17, KEY: 'saleprice', TYPE: 7 }, - 'PHONE': { ID: 18, KEY: 'phone', TYPE: 8 }, - 'ADDRESS': { ID: 19, KEY: 'address', TYPE: 9 }, - 'DESC2': { ID: 20, KEY: 'desc2', TYPE: 10 }, - 'DISPLAYURL': { ID: 21, KEY: 'displayurl', TYPE: 11 }, - 'CTA': { ID: 22, KEY: 'cta', TYPE: 12 } -}; - const NATIVE_ASSET_IMAGE_TYPE = { 'ICON': 1, - 'LOGO': 2, 'IMAGE': 3 } -// check if title, image can be added with mandatory field default values -const NATIVE_MINIMUM_REQUIRED_IMAGE_ASSETS = [ - { - id: NATIVE_ASSETS.SPONSOREDBY.ID, - required: true, - data: { - type: 1 - } - }, - { - id: NATIVE_ASSETS.TITLE.ID, - required: true, - }, - { - id: NATIVE_ASSETS.IMAGE.ID, - required: true, - } -] - const NET_REVENUE = true; const dealChannelValues = { 1: 'PMP', @@ -175,16 +131,9 @@ const MEDIATYPE = [ let publisherId = 0; let isInvalidNativeRequest = false; -let NATIVE_ASSET_ID_TO_KEY_MAP = {}; -let NATIVE_ASSET_KEY_TO_ASSET_MAP = {}; let biddersList = ['pubmatic']; const allBiddersList = ['all']; -// loading NATIVE_ASSET_ID_TO_KEY_MAP -_each(NATIVE_ASSETS, anAsset => { NATIVE_ASSET_ID_TO_KEY_MAP[anAsset.ID] = anAsset.KEY }); -// loading NATIVE_ASSET_KEY_TO_ASSET_MAP -_each(NATIVE_ASSETS, anAsset => { NATIVE_ASSET_KEY_TO_ASSET_MAP[anAsset.KEY] = anAsset }); - export function _getDomainFromURL(url) { let anchor = document.createElement('a'); anchor.href = url; @@ -355,137 +304,189 @@ function _checkParamDataType(key, value, datatype) { return UNDEFINED; } -function _commonNativeRequestObject(nativeAsset, params) { - var key = nativeAsset.KEY; - return { - id: nativeAsset.ID, - required: params[key].required ? 1 : 0, - data: { - type: nativeAsset.TYPE, - len: params[key].len, - ext: params[key].ext - } - }; -} +// TODO delete this code when removing native 1.1 support +const PREBID_NATIVE_DATA_KEYS_TO_ORTB = { + 'desc': 'desc', + 'desc2': 'desc2', + 'body': 'desc', + 'body2': 'desc2', + 'sponsoredBy': 'sponsored', + 'cta': 'ctatext', + 'rating': 'rating', + 'address': 'address', + 'downloads': 'downloads', + 'likes': 'likes', + 'phone': 'phone', + 'price': 'price', + 'salePrice': 'saleprice', + 'displayUrl': 'displayurl', + 'saleprice': 'saleprice', + 'displayurl': 'displayurl' +}; -function _createNativeRequest(params) { - var nativeRequestObject = { +const { NATIVE_IMAGE_TYPES, NATIVE_KEYS_THAT_ARE_NOT_ASSETS, NATIVE_KEYS, NATIVE_ASSET_TYPES } = CONSTANTS; +const PREBID_NATIVE_DATA_KEY_VALUES = Object.values(PREBID_NATIVE_DATA_KEYS_TO_ORTB); + +// TODO remove this function when the support for 1.1 is removed +/** + * Copy of the function toOrtbNativeRequest from core native.js to handle the title len/length + * and ext and mimes parameters from legacy assets. + * @param {object} legacyNativeAssets + * @returns an OpenRTB format of the same bid request + */ +export function toOrtbNativeRequest(legacyNativeAssets) { + if (!legacyNativeAssets && !isPlainObject(legacyNativeAssets)) { + logWarn(`${LOG_WARN_PREFIX}: Native assets object is empty or not an object: ${legacyNativeAssets}`); + isInvalidNativeRequest = true; + return; + } + const ortb = { + ver: '1.2', assets: [] }; - for (var key in params) { - if (params.hasOwnProperty(key)) { - var assetObj = {}; - if (!(nativeRequestObject.assets && nativeRequestObject.assets.length > 0 && nativeRequestObject.assets.hasOwnProperty(key))) { - switch (key) { - case NATIVE_ASSETS.TITLE.KEY: - if (params[key].len || params[key].length) { - assetObj = { - id: NATIVE_ASSETS.TITLE.ID, - required: params[key].required ? 1 : 0, - title: { - len: params[key].len || params[key].length, - ext: params[key].ext - } - }; - } else { - logWarn(LOG_WARN_PREFIX + 'Error: Title Length is required for native ad: ' + JSON.stringify(params)); + for (let key in legacyNativeAssets) { + // skip conversion for non-asset keys + if (NATIVE_KEYS_THAT_ARE_NOT_ASSETS.includes(key)) continue; + if (!NATIVE_KEYS.hasOwnProperty(key) && !PREBID_NATIVE_DATA_KEY_VALUES.includes(key)) { + logWarn(`${LOG_WARN_PREFIX}: Unrecognized native asset code: ${key}. Asset will be ignored.`); + continue; + } + + const asset = legacyNativeAssets[key]; + let required = 0; + if (asset.required && isBoolean(asset.required)) { + required = Number(asset.required); + } + const ortbAsset = { + id: ortb.assets.length, + required + }; + // data cases + if (key in PREBID_NATIVE_DATA_KEYS_TO_ORTB) { + ortbAsset.data = { + type: NATIVE_ASSET_TYPES[PREBID_NATIVE_DATA_KEYS_TO_ORTB[key]] + } + if (asset.len || asset.length) { + ortbAsset.data.len = asset.len || asset.length; + } + if (asset.ext) { + ortbAsset.data.ext = asset.ext; + } + // icon or image case + } else if (key === 'icon' || key === 'image') { + ortbAsset.img = { + type: key === 'icon' ? NATIVE_IMAGE_TYPES.ICON : NATIVE_IMAGE_TYPES.MAIN, + } + // if min_width and min_height are defined in aspect_ratio, they are preferred + if (asset.aspect_ratios) { + if (!isArray(asset.aspect_ratios)) { + logWarn(`${LOG_WARN_PREFIX}: image.aspect_ratios was passed, but it's not a an array: ${asset.aspect_ratios}`); + } else if (!asset.aspect_ratios.length) { + logWarn(`${LOG_WARN_PREFIX}: image.aspect_ratios was passed, but it's empty: ${asset.aspect_ratios}`); + } else { + const { min_width: minWidth, min_height: minHeight } = asset.aspect_ratios[0]; + if (!isInteger(minWidth) || !isInteger(minHeight)) { + logWarn(`${LOG_WARN_PREFIX}: image.aspect_ratios min_width or min_height are invalid: ${minWidth}, ${minHeight}`); + } else { + ortbAsset.img.wmin = minWidth; + ortbAsset.img.hmin = minHeight; + } + const aspectRatios = asset.aspect_ratios + .filter((ar) => ar.ratio_width && ar.ratio_height) + .map(ratio => `${ratio.ratio_width}:${ratio.ratio_height}`); + if (aspectRatios.length > 0) { + ortbAsset.img.ext = { + aspectratios: aspectRatios } - break; - case NATIVE_ASSETS.IMAGE.KEY: - assetObj = { - id: NATIVE_ASSETS.IMAGE.ID, - required: params[key].required ? 1 : 0, - img: { - type: NATIVE_ASSET_IMAGE_TYPE.IMAGE, - w: params[key].w || params[key].width || (params[key].sizes ? params[key].sizes[0] : UNDEFINED), - h: params[key].h || params[key].height || (params[key].sizes ? params[key].sizes[1] : UNDEFINED), - wmin: params[key].wmin || params[key].minimumWidth || (params[key].minsizes ? params[key].minsizes[0] : UNDEFINED), - hmin: params[key].hmin || params[key].minimumHeight || (params[key].minsizes ? params[key].minsizes[1] : UNDEFINED), - mimes: params[key].mimes, - ext: params[key].ext, - } - }; - break; - case NATIVE_ASSETS.ICON.KEY: - assetObj = { - id: NATIVE_ASSETS.ICON.ID, - required: params[key].required ? 1 : 0, - img: { - type: NATIVE_ASSET_IMAGE_TYPE.ICON, - w: params[key].w || params[key].width || (params[key].sizes ? params[key].sizes[0] : UNDEFINED), - h: params[key].h || params[key].height || (params[key].sizes ? params[key].sizes[1] : UNDEFINED), - } - }; - break; - case NATIVE_ASSETS.VIDEO.KEY: - assetObj = { - id: NATIVE_ASSETS.VIDEO.ID, - required: params[key].required ? 1 : 0, - video: { - minduration: params[key].minduration, - maxduration: params[key].maxduration, - protocols: params[key].protocols, - mimes: params[key].mimes, - ext: params[key].ext - } - }; - break; - case NATIVE_ASSETS.EXT.KEY: - assetObj = { - id: NATIVE_ASSETS.EXT.ID, - required: params[key].required ? 1 : 0, - }; - break; - case NATIVE_ASSETS.LOGO.KEY: - assetObj = { - id: NATIVE_ASSETS.LOGO.ID, - required: params[key].required ? 1 : 0, - img: { - type: NATIVE_ASSET_IMAGE_TYPE.LOGO, - w: params[key].w || params[key].width || (params[key].sizes ? params[key].sizes[0] : UNDEFINED), - h: params[key].h || params[key].height || (params[key].sizes ? params[key].sizes[1] : UNDEFINED) - } - }; - break; - case NATIVE_ASSETS.SPONSOREDBY.KEY: - case NATIVE_ASSETS.BODY.KEY: - case NATIVE_ASSETS.RATING.KEY: - case NATIVE_ASSETS.LIKES.KEY: - case NATIVE_ASSETS.DOWNLOADS.KEY: - case NATIVE_ASSETS.PRICE.KEY: - case NATIVE_ASSETS.SALEPRICE.KEY: - case NATIVE_ASSETS.PHONE.KEY: - case NATIVE_ASSETS.ADDRESS.KEY: - case NATIVE_ASSETS.DESC2.KEY: - case NATIVE_ASSETS.DISPLAYURL.KEY: - case NATIVE_ASSETS.CTA.KEY: - assetObj = _commonNativeRequestObject(NATIVE_ASSET_KEY_TO_ASSET_MAP[key], params); - break; + } + } + } + + ortbAsset.img.w = asset.w || asset.width; + ortbAsset.img.h = asset.h || asset.height; + ortbAsset.img.wmin = asset.wmin || asset.minimumWidth || (asset.minsizes ? asset.minsizes[0] : UNDEFINED); + ortbAsset.img.hmin = asset.hmin || asset.minimumHeight || (asset.minsizes ? asset.minsizes[1] : UNDEFINED); + + // if asset.sizes exist, by OpenRTB spec we should remove wmin and hmin + if (asset.sizes) { + if (asset.sizes.length !== 2 || !isInteger(asset.sizes[0]) || !isInteger(asset.sizes[1])) { + logWarn(`${LOG_WARN_PREFIX}: image.sizes was passed, but its value is not an array of integers: ${asset.sizes}`); + } else { + logInfo(`${LOG_WARN_PREFIX}: if asset.sizes exist, by OpenRTB spec we should remove wmin and hmin`); + ortbAsset.img.w = asset.sizes[0]; + ortbAsset.img.h = asset.sizes[1]; + delete ortbAsset.img.hmin; + delete ortbAsset.img.wmin; } } + asset.ext && (ortbAsset.img.ext = asset.ext); + asset.mimes && (ortbAsset.img.mimes = asset.mimes); + // title case + } else if (key === 'title') { + ortbAsset.title = { + // in openRTB, len is required for titles, while in legacy prebid was not. + // for this reason, if len is missing in legacy prebid, we're adding a default value of 140. + len: asset.len || asset.length || 140 + } + asset.ext && (ortbAsset.title.ext = asset.ext); + // all extensions to the native bid request are passed as is + } else if (key === 'ext') { + ortbAsset.ext = asset; + // in `ext` case, required field is not needed + delete ortbAsset.required; } - if (assetObj && assetObj.id) { - nativeRequestObject.assets[nativeRequestObject.assets.length] = assetObj; + ortb.assets.push(ortbAsset); + } + + if (ortb.assets.length < 1) { + logWarn(`${LOG_WARN_PREFIX}: Could not find any valid asset`); + isInvalidNativeRequest = true; + return; + } + + return ortb; +} +// TODO delete this code when removing native 1.1 support + +function _createNativeRequest(params) { + var nativeRequestObject; + + // TODO delete this code when removing native 1.1 support + if (!params.ortb) { // legacy assets definition found + nativeRequestObject = toOrtbNativeRequest(params); + } else { // ortb assets definition found + params = params.ortb; + // TODO delete this code when removing native 1.1 support + nativeRequestObject = { ver: '1.2', ...params, assets: [] }; + const { assets } = params; + + const isValidAsset = (asset) => asset.title || asset.img || asset.data || asset.video; + + if (assets.length < 1 || !assets.some(asset => isValidAsset(asset))) { + logWarn(`${LOG_WARN_PREFIX}: Native assets object is empty or contains some invalid object`); + isInvalidNativeRequest = true; + return nativeRequestObject; } - }; - // for native image adtype prebid has to have few required assests i.e. title,sponsoredBy, image - // if any of these are missing from the request then request will not be sent - var requiredAssetCount = NATIVE_MINIMUM_REQUIRED_IMAGE_ASSETS.length; - var presentrequiredAssetCount = 0; - NATIVE_MINIMUM_REQUIRED_IMAGE_ASSETS.forEach(ele => { - var lengthOfExistingAssets = nativeRequestObject.assets.length; - for (var i = 0; i < lengthOfExistingAssets; i++) { - if (ele.id == nativeRequestObject.assets[i].id) { - presentrequiredAssetCount++; - break; + assets.forEach(asset => { + var assetObj = asset; + if (assetObj.img) { + if (assetObj.img.type == NATIVE_ASSET_IMAGE_TYPE.IMAGE) { + assetObj.w = assetObj.w || assetObj.width || (assetObj.sizes ? assetObj.sizes[0] : UNDEFINED); + assetObj.h = assetObj.h || assetObj.height || (assetObj.sizes ? assetObj.sizes[1] : UNDEFINED); + assetObj.wmin = assetObj.wmin || assetObj.minimumWidth || (assetObj.minsizes ? assetObj.minsizes[0] : UNDEFINED); + assetObj.hmin = assetObj.hmin || assetObj.minimumHeight || (assetObj.minsizes ? assetObj.minsizes[1] : UNDEFINED); + } else if (assetObj.img.type == NATIVE_ASSET_IMAGE_TYPE.ICON) { + assetObj.w = assetObj.w || assetObj.width || (assetObj.sizes ? assetObj.sizes[0] : UNDEFINED); + assetObj.h = assetObj.h || assetObj.height || (assetObj.sizes ? assetObj.sizes[1] : UNDEFINED); + } + } + + if (assetObj && assetObj.id !== undefined && isValidAsset(assetObj)) { + nativeRequestObject.assets.push(assetObj); } } - }); - if (requiredAssetCount == presentrequiredAssetCount) { - isInvalidNativeRequest = false; - } else { - isInvalidNativeRequest = true; + ); } return nativeRequestObject; } @@ -607,7 +608,7 @@ function _addDealCustomTargetings(imp, bid) { } } -function _addJWPlayerSegmentData(imp, bid, isS2S) { +function _addJWPlayerSegmentData(imp, bid) { var jwSegData = (bid.rtd && bid.rtd.jwplayer && bid.rtd.jwplayer.targeting) || undefined; var jwPlayerData = ''; const jwMark = 'jw-'; @@ -624,15 +625,11 @@ function _addJWPlayerSegmentData(imp, bid, isS2S) { var ext; - if (isS2S) { - (imp.dctr === undefined || imp.dctr.length == 0) ? imp.dctr = jwPlayerData : imp.dctr += '|' + jwPlayerData; - } else { - ext = imp.ext; - ext && ext.key_val === undefined ? ext.key_val = jwPlayerData : ext.key_val += '|' + jwPlayerData; - } + ext = imp.ext; + ext && ext.key_val === undefined ? ext.key_val = jwPlayerData : ext.key_val += '|' + jwPlayerData; } -function _createImpressionObject(bid, conf) { +function _createImpressionObject(bid) { var impObj = {}; var bannerObj; var videoObj; @@ -665,11 +662,15 @@ function _createImpressionObject(bid, conf) { } break; case NATIVE: + // TODO uncomment below line when removing native 1.1 support + // nativeObj['request'] = JSON.stringify(_createNativeRequest(bid.nativeOrtbRequest)); + // TODO delete below line when removing native 1.1 support nativeObj['request'] = JSON.stringify(_createNativeRequest(bid.nativeParams)); if (!isInvalidNativeRequest) { impObj.native = nativeObj; } else { logWarn(LOG_WARN_PREFIX + 'Error: Error in Native adunit ' + bid.params.adUnit + '. Ignoring the adunit. Refer to ' + PREBID_NATIVE_HELP_LINK + ' for more details.'); + isInvalidNativeRequest = false; } break; case VIDEO: @@ -825,7 +826,6 @@ function _checkMediaType(bid, newBid) { } function _parseNativeResponse(bid, newBid) { - newBid.native = {}; if (bid.hasOwnProperty('adm')) { var adm = ''; try { @@ -834,53 +834,15 @@ function _parseNativeResponse(bid, newBid) { logWarn(LOG_WARN_PREFIX + 'Error: Cannot parse native reponse for ad response: ' + newBid.adm); return; } - if (adm && adm.native && adm.native.assets && adm.native.assets.length > 0) { - newBid.mediaType = NATIVE; - for (let i = 0, len = adm.native.assets.length; i < len; i++) { - switch (adm.native.assets[i].id) { - case NATIVE_ASSETS.TITLE.ID: - newBid.native.title = adm.native.assets[i].title && adm.native.assets[i].title.text; - break; - case NATIVE_ASSETS.IMAGE.ID: - newBid.native.image = { - url: adm.native.assets[i].img && adm.native.assets[i].img.url, - height: adm.native.assets[i].img && adm.native.assets[i].img.h, - width: adm.native.assets[i].img && adm.native.assets[i].img.w, - }; - break; - case NATIVE_ASSETS.ICON.ID: - newBid.native.icon = { - url: adm.native.assets[i].img && adm.native.assets[i].img.url, - height: adm.native.assets[i].img && adm.native.assets[i].img.h, - width: adm.native.assets[i].img && adm.native.assets[i].img.w, - }; - break; - case NATIVE_ASSETS.SPONSOREDBY.ID: - case NATIVE_ASSETS.BODY.ID: - case NATIVE_ASSETS.LIKES.ID: - case NATIVE_ASSETS.DOWNLOADS.ID: - case NATIVE_ASSETS.PRICE: - case NATIVE_ASSETS.SALEPRICE.ID: - case NATIVE_ASSETS.PHONE.ID: - case NATIVE_ASSETS.ADDRESS.ID: - case NATIVE_ASSETS.DESC2.ID: - case NATIVE_ASSETS.CTA.ID: - case NATIVE_ASSETS.RATING.ID: - case NATIVE_ASSETS.DISPLAYURL.ID: - newBid.native[NATIVE_ASSET_ID_TO_KEY_MAP[adm.native.assets[i].id]] = adm.native.assets[i].data && adm.native.assets[i].data.value; - break; - } - } - newBid.native.clickUrl = adm.native.link && adm.native.link.url; - newBid.native.clickTrackers = (adm.native.link && adm.native.link.clicktrackers) || []; - newBid.native.impressionTrackers = adm.native.imptrackers || []; - newBid.native.jstracker = adm.native.jstracker || []; - if (!newBid.width) { - newBid.width = DEFAULT_WIDTH; - } - if (!newBid.height) { - newBid.height = DEFAULT_HEIGHT; - } + newBid.native = { + ortb: { ...adm.native } + }; + newBid.mediaType = NATIVE; + if (!newBid.width) { + newBid.width = DEFAULT_WIDTH; + } + if (!newBid.height) { + newBid.height = DEFAULT_HEIGHT; } } } @@ -977,6 +939,49 @@ function isNonEmptyArray(test) { return false; } +/** + * Prepare meta object to pass as params + * @param {*} br : bidResponse + * @param {*} bid : bids + */ +export function prepareMetaObject(br, bid, seat) { + br.meta = {}; + + if (bid.ext && bid.ext.dspid) { + br.meta.networkId = bid.ext.dspid; + br.meta.demandSource = bid.ext.dspid; + } + + // NOTE: We will not recieve below fields from the translator response also not sure on what will be the key names for these in the response, + // when we needed we can add it back. + // New fields added, assignee fields name may change + // if (bid.ext.networkName) br.meta.networkName = bid.ext.networkName; + // if (bid.ext.advertiserName) br.meta.advertiserName = bid.ext.advertiserName; + // if (bid.ext.agencyName) br.meta.agencyName = bid.ext.agencyName; + // if (bid.ext.brandName) br.meta.brandName = bid.ext.brandName; + if (bid.ext && bid.ext.dchain) { + br.meta.dchain = bid.ext.dchain; + } + + const advid = seat || (bid.ext && bid.ext.advid); + if (advid) { + br.meta.advertiserId = advid; + br.meta.agencyId = advid; + br.meta.buyerId = advid; + } + + if (bid.adomain && isNonEmptyArray(bid.adomain)) { + br.meta.advertiserDomains = bid.adomain; + br.meta.clickUrl = bid.adomain[0]; + br.meta.brandId = bid.adomain[0]; + } + + if (bid.cat && isNonEmptyArray(bid.cat)) { + br.meta.secondaryCatIds = bid.cat; + br.meta.primaryCatId = bid.cat[0]; + } +} + export const spec = { code: BIDDER_CODE, gvlid: 76, @@ -1040,7 +1045,7 @@ export const spec = { */ buildRequests: (validBidRequests, bidderRequest) => { // convert Native ORTB definition to old-style prebid native definition - validBidRequests = convertOrtbRequestToProprietaryNative(validBidRequests); + // validBidRequests = convertOrtbRequestToProprietaryNative(validBidRequests); var refererInfo; if (bidderRequest && bidderRequest.refererInfo) { refererInfo = bidderRequest.refererInfo; @@ -1086,7 +1091,7 @@ export const spec = { if (bid.params.hasOwnProperty('acat') && isArray(bid.params.acat)) { allowedIabCategories = allowedIabCategories.concat(bid.params.acat); } - var impObj = _createImpressionObject(bid, conf); + var impObj = _createImpressionObject(bid); if (impObj) { payload.imp.push(impObj); } @@ -1189,6 +1194,10 @@ export const spec = { if (commonFpd.bcat) { blockedIabCategories = blockedIabCategories.concat(commonFpd.bcat); } + // check if fpd ortb2 contains device property with sua object + if (commonFpd.device?.sua) { + payload.device.sua = commonFpd.device?.sua; + } if (commonFpd.ext?.prebid?.bidderparams?.[bidderRequest.bidderCode]?.acat) { const acatParams = commonFpd.ext.prebid.bidderparams[bidderRequest.bidderCode].acat; @@ -1202,7 +1211,7 @@ export const spec = { // bidderRequest has timeout property if publisher sets during calling requestBids function from page // if not bidderRequest contains global value set by Prebid if (bidderRequest?.timeout) { - payload.tmax = bidderRequest.timeout || config.getConfig('bidderTimeout'); + payload.tmax = bidderRequest.timeout; } else { payload.tmax = window?.PWT?.versionDetails?.timeout; } @@ -1293,17 +1302,7 @@ export const spec = { newBid['dealChannel'] = dealChannelValues[bid.ext.deal_channel] || null; } - newBid.meta = {}; - if (bid.ext && bid.ext.dspid) { - newBid.meta.networkId = bid.ext.dspid; - } - if (bid.ext && bid.ext.advid) { - newBid.meta.buyerId = bid.ext.advid; - } - if (bid.adomain && bid.adomain.length > 0) { - newBid.meta.advertiserDomains = bid.adomain; - newBid.meta.clickUrl = bid.adomain[0]; - } + prepareMetaObject(newBid, bid, seatbidder.seat); // adserverTargeting if (seatbidder.ext && seatbidder.ext.buyid) { @@ -1371,7 +1370,6 @@ export const spec = { */ transformBidParams: function (params, isOpenRtb, adUnit, bidRequests) { - _addJWPlayerSegmentData(params, adUnit.bids[0], true); return convertTypes({ 'publisherId': 'string', 'adSlot': 'string' diff --git a/modules/pubwiseAnalyticsAdapter.js b/modules/pubwiseAnalyticsAdapter.js index 19a74c7c245..6aed462f2d5 100644 --- a/modules/pubwiseAnalyticsAdapter.js +++ b/modules/pubwiseAnalyticsAdapter.js @@ -3,8 +3,10 @@ import {ajax} from '../src/ajax.js'; import adapter from '../libraries/analyticsAdapter/AnalyticsAdapter.js'; import adapterManager from '../src/adapterManager.js'; import CONSTANTS from '../src/constants.json'; -import { getStorageManager } from '../src/storageManager.js'; -const storage = getStorageManager(); +import {getStorageManager} from '../src/storageManager.js'; +import {MODULE_TYPE_ANALYTICS} from '../src/activities/modules.js'; +const MODULE_CODE = 'pubwise'; +const storage = getStorageManager({moduleType: MODULE_TYPE_ANALYTICS, moduleName: MODULE_CODE}); /**** * PubWise.io Analytics @@ -304,11 +306,14 @@ pubwiseAnalytics.storeSessionID = function (userSessID) { // ensure a session exists, if not make one, always store it pubwiseAnalytics.ensureSession = function () { - if (sessionExpired() === true || userSessionID() === null || userSessionID() === '') { + let sessionId = userSessionID(); + if (sessionExpired() === true || sessionId === null || sessionId === '') { let generatedId = generateUUID(); expireUtmData(); this.storeSessionID(generatedId); sessionData.sessionId = generatedId; + } else if (sessionId != null) { + sessionData.sessionId = sessionId; } // eslint-disable-next-line // console.log('ensured session'); @@ -331,7 +336,7 @@ pubwiseAnalytics.enableAnalytics = function (config) { adapterManager.registerAnalyticsAdapter({ adapter: pubwiseAnalytics, - code: 'pubwise', + code: MODULE_CODE, gvlid: 842 }); diff --git a/modules/pubwiseBidAdapter.js b/modules/pubwiseBidAdapter.js index 5e381e74a18..a7381bb2884 100644 --- a/modules/pubwiseBidAdapter.js +++ b/modules/pubwiseBidAdapter.js @@ -1,19 +1,32 @@ -import { _each, isStr, deepClone, isArray, deepSetValue, inIframe, logMessage, logInfo, logWarn, logError } from '../src/utils.js'; -import { config } from '../src/config.js'; + +import { _each, isBoolean, isEmptyStr, isNumber, isStr, deepClone, isArray, deepSetValue, inIframe, mergeDeep, deepAccess, logMessage, logInfo, logWarn, logError } from '../src/utils.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; -import { BANNER, NATIVE } from '../src/mediaTypes.js'; -const VERSION = '0.2.0'; +import { BANNER, NATIVE, VIDEO } from '../src/mediaTypes.js'; +import { config } from '../src/config.js'; +import { convertOrtbRequestToProprietaryNative } from '../src/native.js'; +import { OUTSTREAM, INSTREAM } from '../src/video.js'; + +const VERSION = '0.3.0'; const GVLID = 842; const NET_REVENUE = true; const UNDEFINED = undefined; const DEFAULT_CURRENCY = 'USD'; const AUCTION_TYPE = 1; const BIDDER_CODE = 'pwbid'; +const LOG_PREFIX = 'PubWise: '; const ENDPOINT_URL = 'https://bid.pubwise.io/prebid'; +// const ENDPOINT_URL = 'https://bid.pubwise.io/prebid'; // testing observable endpoint const DEFAULT_WIDTH = 0; const DEFAULT_HEIGHT = 0; const PREBID_NATIVE_HELP_LINK = 'https://prebid.org/dev-docs/show-native-ads.html'; // const USERSYNC_URL = '//127.0.0.1:8080/usersync' +const MSG_VIDEO_PLACEMENT_MISSING = 'Video.Placement param missing'; + +const MEDIATYPE = [ + BANNER, + VIDEO, + NATIVE +] const CUSTOM_PARAMS = { 'gender': '', // User gender @@ -22,6 +35,32 @@ const CUSTOM_PARAMS = { 'lon': '', // User Location - Longitude }; +const DATA_TYPES = { + 'NUMBER': 'number', + 'STRING': 'string', + 'BOOLEAN': 'boolean', + 'ARRAY': 'array', + 'OBJECT': 'object' +}; + +const VIDEO_CUSTOM_PARAMS = { + 'mimes': DATA_TYPES.ARRAY, + 'minduration': DATA_TYPES.NUMBER, + 'maxduration': DATA_TYPES.NUMBER, + 'startdelay': DATA_TYPES.NUMBER, + 'playbackmethod': DATA_TYPES.ARRAY, + 'api': DATA_TYPES.ARRAY, + 'protocols': DATA_TYPES.ARRAY, + 'w': DATA_TYPES.NUMBER, + 'h': DATA_TYPES.NUMBER, + 'battr': DATA_TYPES.ARRAY, + 'linearity': DATA_TYPES.NUMBER, + 'placement': DATA_TYPES.NUMBER, + 'minbitrate': DATA_TYPES.NUMBER, + 'maxbitrate': DATA_TYPES.NUMBER, + 'skip': DATA_TYPES.NUMBER +} + // rtb native types are meant to be dynamic and extendable // the extendable data asset types are nicely aligned // in practice we set an ID that is distinct for each real type of return @@ -88,7 +127,7 @@ _each(NATIVE_ASSETS, anAsset => { NATIVE_ASSET_KEY_TO_ASSET_MAP[anAsset.KEY] = a export const spec = { code: BIDDER_CODE, gvlid: GVLID, - supportedMediaTypes: [BANNER, NATIVE], + supportedMediaTypes: [BANNER, VIDEO, NATIVE], /** * Determines whether or not the given bid request is valid. * @@ -96,18 +135,40 @@ export const spec = { * @return boolean True if this is a valid bid, and false otherwise. */ isBidRequestValid: function (bid) { - // siteId is required + // siteId is required for any type if (bid.params && bid.params.siteId) { // it must be a string if (!isStr(bid.params.siteId)) { _logWarn('siteId is required for bid', bid); return false; } - } else { - return false; + + // video ad validation + if (bid.hasOwnProperty('mediaTypes') && bid.mediaTypes.hasOwnProperty(VIDEO)) { + // bid.mediaTypes.video.mimes OR bid.params.video.mimes should be present and must be a non-empty array + let mediaTypesVideoMimes = deepAccess(bid.mediaTypes, 'video.mimes'); + let paramsVideoMimes = deepAccess(bid, 'params.video.mimes'); + if (_isNonEmptyArray(mediaTypesVideoMimes) === false && _isNonEmptyArray(paramsVideoMimes) === false) { + _logWarn('Error: For video ads, bid.mediaTypes.video.mimes OR bid.params.video.mimes should be present and must be a non-empty array. Call suppressed:', JSON.stringify(bid)); + return false; + } + + if (!bid.mediaTypes[VIDEO].hasOwnProperty('context')) { + _logError(`no context specified in bid. Rejecting bid: `, JSON.stringify(bid)); + return false; + } + + if (bid.mediaTypes[VIDEO].context === 'outstream') { + delete bid.mediaTypes[VIDEO]; + _logWarn(`outstream not currently supported `, JSON.stringify(bid)); + return false; + } + } + + return true; } - return true; + return false; }, /** * Make a server request from the list of BidRequests. @@ -116,6 +177,7 @@ export const spec = { * @return ServerRequest Info describing the request to the server. */ buildRequests: function (validBidRequests, bidderRequest) { + validBidRequests = convertOrtbRequestToProprietaryNative(validBidRequests); var refererInfo; if (bidderRequest && bidderRequest.refererInfo) { refererInfo = bidderRequest.refererInfo; @@ -210,7 +272,7 @@ export const spec = { return { method: 'POST', - url: ENDPOINT_URL, + url: _getEndpointURL(bid), data: payload, options: options, bidderRequest: bidderRequest, @@ -231,6 +293,7 @@ export const spec = { // try { if (response.body && response.body.seatbid && isArray(response.body.seatbid)) { + _logInfo('interpretResponse response body', response.body); // Supporting multiple bid responses for same adSize respCur = response.body.cur || respCur; response.body.seatbid.forEach(seatbidder => { @@ -254,10 +317,24 @@ export const spec = { if (parsedRequest.imp && parsedRequest.imp.length > 0) { parsedRequest.imp.forEach(req => { if (bid.impid === req.id) { - _checkMediaType(bid.adm, newBid); + _checkMediaType(bid, newBid); switch (newBid.mediaType) { case BANNER: break; + case VIDEO: + const videoContext = deepAccess(request, 'mediaTypes.video.context'); + switch (videoContext) { + case OUTSTREAM: + // not currently supported + break; + case INSTREAM: + break; + } + newBid.width = bid.hasOwnProperty('w') ? bid.w : req.video.w; + newBid.height = bid.hasOwnProperty('h') ? bid.h : req.video.h; + newBid.vastXml = bid.adm; + newBid.vastUrl = bid.vastUrl; + break; case NATIVE: _parseNativeResponse(bid, newBid); break; @@ -289,20 +366,31 @@ export const spec = { } } -function _checkMediaType(adm, newBid) { - // Create a regex here to check the strings - var admJSON = ''; - if (adm.indexOf('"ver":') >= 0) { - try { - admJSON = JSON.parse(adm.replace(/\\/g, '')); - if (admJSON && admJSON.assets) { - newBid.mediaType = NATIVE; +function _checkMediaType(bid, newBid) { + // Check Various ADM Aspects to Determine Media Type + if (bid.ext && bid.ext['bidtype'] != undefined) { + // this is the most explicity check + newBid.mediaType = MEDIATYPE[bid.ext.bidtype]; + } else { + _logInfo('bid.ext.bidtype does not exist, checking alternatively for mediaType'); + var adm = bid.adm; + var videoRegex = new RegExp(/VAST\s+version/); + + if (adm.indexOf('"ver":') >= 0) { + try { + var admJSON = ''; + admJSON = JSON.parse(adm.replace(/\\/g, '')); + if (admJSON && admJSON.assets) { + newBid.mediaType = NATIVE; + } + } catch (e) { + _logWarn('Error: Cannot parse native reponse for ad response: ', adm); } - } catch (e) { - _logWarn('Error: Cannot parse native reponse for ad response: ' + adm); + } else if (videoRegex.test(adm)) { + newBid.mediaType = VIDEO; + } else { + newBid.mediaType = BANNER; } - } else { - newBid.mediaType = BANNER; } } @@ -416,7 +504,8 @@ function _createOrtbTemplate(conf) { dnt: (navigator.doNotTrack == 'yes' || navigator.doNotTrack == '1' || navigator.msDoNotTrack == '1') ? 1 : 0, h: screen.height, w: screen.width, - language: navigator.language + language: navigator.language, + devicetype: _getDeviceType() }, user: {}, ext: { @@ -428,6 +517,7 @@ function _createOrtbTemplate(conf) { function _createImpressionObject(bid, conf) { var impObj = {}; var bannerObj; + var videoObj; var nativeObj = {}; var mediaTypes = ''; @@ -459,6 +549,12 @@ function _createImpressionObject(bid, conf) { _logWarn('Error: Error in Native adunit ' + bid.params.adUnit + '. Ignoring the adunit. Refer to ' + PREBID_NATIVE_HELP_LINK + ' for more details.'); } break; + case VIDEO: + videoObj = _createVideoRequest(bid); + if (videoObj !== UNDEFINED) { + impObj.video = videoObj; + } + break; } } } else { @@ -468,7 +564,8 @@ function _createImpressionObject(bid, conf) { _addFloorFromFloorModule(impObj, bid); return impObj.hasOwnProperty(BANNER) || - impObj.hasOwnProperty(NATIVE) ? impObj : UNDEFINED; + impObj.hasOwnProperty(NATIVE) || + impObj.hasOwnProperty(VIDEO) ? impObj : UNDEFINED; } function _parseSlotParam(paramName, paramValue) { @@ -492,7 +589,7 @@ function _parseSlotParam(paramName, paramValue) { } function _parseAdSlot(bid) { - _logInfo('parseAdSlot bid', bid) + _logInfo('parseAdSlot bid', bid); if (bid.adUnitCode) { bid.params.adUnit = bid.adUnitCode; } else { @@ -504,7 +601,7 @@ function _parseAdSlot(bid) { if (bid.hasOwnProperty('mediaTypes')) { if (bid.mediaTypes.hasOwnProperty(BANNER) && - bid.mediaTypes.banner.hasOwnProperty('sizes')) { // if its a banner, has mediaTypes and sizes + bid.mediaTypes.banner.hasOwnProperty('sizes')) { // if its a banner, has mediaTypes and sizes var i = 0; var sizeArray = []; for (;i < bid.mediaTypes.banner.sizes.length; i++) { @@ -522,7 +619,7 @@ function _parseAdSlot(bid) { } } } else { - _logWarn('MediaTypes are Required for all Adunit Configs', bid) + _logWarn('MediaTypes are Required for all Adunit Configs', bid); } } @@ -558,7 +655,7 @@ function _addFloorFromFloorModule(impObj, bid) { // get lowest floor from floorModule if (typeof bid.getFloor === 'function' && !config.getConfig('pubwise.disableFloors')) { - [BANNER, NATIVE].forEach(mediaType => { + [BANNER, VIDEO, NATIVE].forEach(mediaType => { if (impObj.hasOwnProperty(mediaType)) { let floorInfo = bid.getFloor({ currency: impObj.bidFloorCur, mediaType: mediaType, size: '*' }); if (typeof floorInfo === 'object' && floorInfo.currency === impObj.bidFloorCur && !isNaN(parseInt(floorInfo.floor))) { @@ -746,28 +843,162 @@ function _createBannerRequest(bid) { _logWarn('Error: mediaTypes.banner.size missing for adunit: ' + bid.params.adUnit + '. Ignoring the banner impression in the adunit.'); bannerObj = UNDEFINED; } + return bannerObj; } // various error levels are not always used // eslint-disable-next-line no-unused-vars function _logMessage(textValue, objectValue) { - logMessage('PubWise: ' + textValue, objectValue); + objectValue = objectValue || ''; + logMessage(LOG_PREFIX + textValue, objectValue); } // eslint-disable-next-line no-unused-vars function _logInfo(textValue, objectValue) { - logInfo('PubWise: ' + textValue, objectValue); + objectValue = objectValue || ''; + logInfo(LOG_PREFIX + textValue, objectValue); } // eslint-disable-next-line no-unused-vars function _logWarn(textValue, objectValue) { - logWarn('PubWise: ' + textValue, objectValue); + objectValue = objectValue || ''; + logWarn(LOG_PREFIX + textValue, objectValue); } // eslint-disable-next-line no-unused-vars function _logError(textValue, objectValue) { - logError('PubWise: ' + textValue, objectValue); + objectValue = objectValue || ''; + logError(LOG_PREFIX + textValue, objectValue); +} + +function _checkVideoPlacement(videoData, adUnitCode) { + // Check for video.placement property. If property is missing display log message. + if (!deepAccess(videoData, 'placement')) { + _logWarn(`${MSG_VIDEO_PLACEMENT_MISSING} for ${adUnitCode}`, adUnitCode); + }; +} + +function _createVideoRequest(bid) { + var videoData = mergeDeep(deepAccess(bid.mediaTypes, 'video'), bid.params.video); + var videoObj; + + if (videoData !== UNDEFINED) { + videoObj = {}; + _checkVideoPlacement(videoData, bid.adUnitCode); + for (var key in VIDEO_CUSTOM_PARAMS) { + if (videoData.hasOwnProperty(key)) { + videoObj[key] = _checkParamDataType(key, videoData[key], VIDEO_CUSTOM_PARAMS[key]); + } + } + // read playersize and assign to h and w. + if (isArray(bid.mediaTypes.video.playerSize[0])) { + videoObj.w = parseInt(bid.mediaTypes.video.playerSize[0][0], 10); + videoObj.h = parseInt(bid.mediaTypes.video.playerSize[0][1], 10); + } else if (isNumber(bid.mediaTypes.video.playerSize[0])) { + videoObj.w = parseInt(bid.mediaTypes.video.playerSize[0], 10); + videoObj.h = parseInt(bid.mediaTypes.video.playerSize[1], 10); + } + } else { + videoObj = UNDEFINED; + _logWarn('Error: Video config params missing for adunit: ' + bid.params.adUnit + ' with mediaType set as video. Ignoring video impression in the adunit.', bid.params); + } + return videoObj; +} + +/** + * Determines if the array has values + * + * @param {object} test + * @returns {boolean} + */ +function _isNonEmptyArray(test) { + if (isArray(test) === true) { + if (test.length > 0) { + return true; + } + } + return false; +} + +/** + * Returns the overridden bid endpoint_url if it is set, primarily used for testing + * + * @param {object} bid the current bid + * @returns + */ +function _getEndpointURL(bid) { + if (!isEmptyStr(bid?.params?.endpoint_url) && bid?.params?.endpoint_url != UNDEFINED) { + return bid.params.endpoint_url; + } + + return ENDPOINT_URL; +} + +/** + * + * @param {object} key + * @param {object}} value + * @param {object} datatype + * @returns + */ +function _checkParamDataType(key, value, datatype) { + var errMsg = 'Ignoring param key: ' + key + ', expects ' + datatype + ', found ' + typeof value; + var functionToExecute; + switch (datatype) { + case DATA_TYPES.BOOLEAN: + functionToExecute = isBoolean; + break; + case DATA_TYPES.NUMBER: + functionToExecute = isNumber; + break; + case DATA_TYPES.STRING: + functionToExecute = isStr; + break; + case DATA_TYPES.ARRAY: + functionToExecute = isArray; + break; + } + if (functionToExecute(value)) { + return value; + } + _logWarn(errMsg, key); + return UNDEFINED; +} + +function _isMobile() { + if (navigator.userAgentData && navigator.userAgentData.mobile) { + return true; + } else { + return (/(mobi)/i).test(navigator.userAgent); + } +} + +function _isConnectedTV() { + return (/(smart[-]?tv|hbbtv|appletv|googletv|hdmi|netcast\.tv|viera|nettv|roku|\bdtv\b|sonydtv|inettvbrowser|\btv\b)/i).test(navigator.userAgent); +} + +function _isTablet() { + return (/ipad|android 3.0|xoom|sch-i800|playbook|tablet|kindle/i.test(navigator.userAgent.toLowerCase())); +} + +/** + * Very high level device detection, order matters + */ +function _getDeviceType() { + if (_isTablet()) { + return 5; + } + + if (_isMobile()) { + return 4; + } + + if (_isConnectedTV()) { + return 3; + } + + return 2; } // function _decorateLog() { @@ -777,6 +1008,7 @@ function _logError(textValue, objectValue) { // these are exported only for testing so maintaining the JS convention of _ to indicate the intent export { + _checkVideoPlacement, _checkMediaType, _parseAdSlot } diff --git a/modules/pubxBidAdapter.js b/modules/pubxBidAdapter.js index 17c4ba3848e..ee28d549475 100644 --- a/modules/pubxBidAdapter.js +++ b/modules/pubxBidAdapter.js @@ -1,4 +1,4 @@ -import { deepSetValue } from '../src/utils.js'; +import { deepSetValue, deepAccess } from '../src/utils.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; const BIDDER_CODE = 'pubx'; @@ -16,8 +16,11 @@ export const spec = { const bidId = bidRequest.bidId; const params = bidRequest.params; const sid = params.sid; + const pageUrl = deepAccess(bidRequest, 'ortb2.site.page').replace(/\?.*$/, ''); + const pageEnc = encodeURIComponent(pageUrl); const payload = { - sid: sid + sid: sid, + pu: pageEnc, }; return { id: bidId, diff --git a/modules/pubxaiAnalyticsAdapter.js b/modules/pubxaiAnalyticsAdapter.js index f7e73dd5fdd..19a3c236942 100644 --- a/modules/pubxaiAnalyticsAdapter.js +++ b/modules/pubxaiAnalyticsAdapter.js @@ -3,10 +3,11 @@ import { ajax } from '../src/ajax.js'; import adapter from '../libraries/analyticsAdapter/AnalyticsAdapter.js'; import adapterManager from '../src/adapterManager.js'; import CONSTANTS from '../src/constants.json'; +import {getGlobal} from '../src/prebidGlobal.js'; const emptyUrl = ''; const analyticsType = 'endpoint'; -const pubxaiAnalyticsVersion = 'v1.1.0'; +const pubxaiAnalyticsVersion = 'v1.2.0'; const defaultHost = 'api.pbxai.com'; const auctionPath = '/analytics/auction'; const winningBidPath = '/analytics/bidwon'; @@ -154,7 +155,7 @@ function send(data, status) { search: location.search }); if (typeof data !== 'undefined' && typeof data.auctionInit !== 'undefined') { - data.pageDetail.adUnitCount = data.auctionInit.adUnitCodes ? data.auctionInit.adUnitCodes.length : null; + data.pageDetail.adUnits = data.auctionInit.adUnitCodes; data.initOptions.auctionId = data.auctionInit.auctionId; delete data.auctionInit; @@ -180,7 +181,7 @@ function send(data, status) { search: { auctionTimestamp: auctionTimestamp, pubxaiAnalyticsVersion: pubxaiAnalyticsVersion, - prebidVersion: $$PREBID_GLOBAL$$.version + prebidVersion: getGlobal().version } }); if (status == 'bidwon') { diff --git a/modules/pulsepointBidAdapter.js b/modules/pulsepointBidAdapter.js index 25f82fb60d9..015e80d5692 100644 --- a/modules/pulsepointBidAdapter.js +++ b/modules/pulsepointBidAdapter.js @@ -16,6 +16,7 @@ const DEFAULT_BID_TTL = 20; const DEFAULT_CURRENCY = 'USD'; const DEFAULT_NET_REVENUE = true; const KNOWN_PARAMS = ['cp', 'ct', 'cf', 'video', 'battr', 'bcat', 'badv', 'bidfloor']; +const DEFAULT_TMAX = 500; /** * PulsePoint Bid Adapter. @@ -54,6 +55,7 @@ export const spec = { user: user(bidRequests[0], bidderRequest), regs: regs(bidderRequest), source: source(bidRequests[0].schain), + tmax: bidderRequest.timeout || DEFAULT_TMAX, }; return { method: 'POST', diff --git a/modules/quantcastBidAdapter.js b/modules/quantcastBidAdapter.js index 19a559edfda..2c721a61616 100644 --- a/modules/quantcastBidAdapter.js +++ b/modules/quantcastBidAdapter.js @@ -22,7 +22,7 @@ export const QUANTCAST_PROTOCOL = 'https'; export const QUANTCAST_PORT = '8443'; export const QUANTCAST_FPA = '__qca'; -export const storage = getStorageManager({gvlid: QUANTCAST_VENDOR_ID, bidderCode: BIDDER_CODE}); +export const storage = getStorageManager({bidderCode: BIDDER_CODE}); function makeVideoImp(bid) { const videoInMediaType = deepAccess(bid, 'mediaTypes.video') || {}; diff --git a/modules/quantcastIdSystem.js b/modules/quantcastIdSystem.js index 97cd01f98da..6a07082b61c 100644 --- a/modules/quantcastIdSystem.js +++ b/modules/quantcastIdSystem.js @@ -6,9 +6,10 @@ */ import {submodule} from '../src/hook.js' -import { getStorageManager } from '../src/storageManager.js'; +import {getStorageManager} from '../src/storageManager.js'; import { triggerPixel, logInfo } from '../src/utils.js'; import { uspDataHandler, coppaDataHandler, gdprDataHandler } from '../src/adapterManager.js'; +import {MODULE_TYPE_UID} from '../src/activities/modules.js'; const QUANTCAST_FPA = '__qca'; const DEFAULT_COOKIE_EXP_DAYS = 392; // (13 months - 2 days) @@ -23,8 +24,9 @@ const QC_TCF_CONSENT_FIRST_PURPOSES = [PURPOSE_DATA_COLLECT]; const QC_TCF_CONSENT_ONLY_PUPROSES = [PURPOSE_DATA_COLLECT]; const GDPR_PRIVACY_STRING = gdprDataHandler.getConsentData(); const US_PRIVACY_STRING = uspDataHandler.getConsentData(); +const MODULE_NAME = 'quantcastId'; -export const storage = getStorageManager(); +export const storage = getStorageManager({moduleType: MODULE_TYPE_UID, moduleName: MODULE_NAME}); export function firePixel(clientId, cookieExpDays = DEFAULT_COOKIE_EXP_DAYS) { // check for presence of Quantcast Measure tag _qevent obj and publisher provided clientID @@ -160,7 +162,7 @@ export const quantcastIdSubmodule = { * used to link submodule with config * @type {string} */ - name: 'quantcastId', + name: MODULE_NAME, /** * Vendor id of Quantcast diff --git a/modules/realvuAnalyticsAdapter.js b/modules/realvuAnalyticsAdapter.js index 0811c70a2f7..e4bcf1b474a 100644 --- a/modules/realvuAnalyticsAdapter.js +++ b/modules/realvuAnalyticsAdapter.js @@ -2,9 +2,12 @@ import adapter from '../libraries/analyticsAdapter/AnalyticsAdapter.js'; import adapterManager from '../src/adapterManager.js'; import CONSTANTS from '../src/constants.json'; -import { getStorageManager } from '../src/storageManager.js'; +import {getStorageManager} from '../src/storageManager.js'; import { logMessage, logError } from '../src/utils.js'; -const storage = getStorageManager(); +import {MODULE_TYPE_ANALYTICS} from '../src/activities/modules.js'; +const MODULE_CODE = 'realvuAnalytics'; + +const storage = getStorageManager({moduleType: MODULE_TYPE_ANALYTICS, moduleName: MODULE_CODE}); let realvuAnalyticsAdapter = adapter({ global: 'realvuAnalytics', @@ -967,7 +970,7 @@ realvuAnalyticsAdapter.disableAnalytics = function () { adapterManager.registerAnalyticsAdapter({ adapter: realvuAnalyticsAdapter, - code: 'realvuAnalytics' + code: MODULE_CODE, }); export default realvuAnalyticsAdapter; diff --git a/modules/relevadRtdProvider.js b/modules/relevadRtdProvider.js new file mode 100644 index 00000000000..a7d0305da62 --- /dev/null +++ b/modules/relevadRtdProvider.js @@ -0,0 +1,365 @@ +/** + * This module adds Relevad provider to the real time data module + * The {@link module:modules/realTimeData} module is required + * The module will fetch categories and segments from Relevad server and pass them to the bidders + * @module modules/relevadRtdProvider + * @requires module:modules/realTimeData + */ + +import {deepSetValue, isEmpty, logError, mergeDeep} from '../src/utils.js'; +import {submodule} from '../src/hook.js'; +import {ajax} from '../src/ajax.js'; +import {findIndex} from '../src/polyfill.js'; +import {getRefererInfo} from '../src/refererDetection.js'; +import {config} from '../src/config.js'; + +const MODULE_NAME = 'realTimeData'; +const SUBMODULE_NAME = 'RelevadRTDModule'; + +const SEGTAX_IAB = 6; // IAB Content Taxonomy v2 +const CATTAX_IAB = 6; // IAB Contextual Taxonomy v2.2 +const RELEVAD_API_DOMAIN = 'https://prebid.relestar.com'; +const entries = Object.entries; +const AJAX_OPTIONS = { + withCredentials: true, + referrerPolicy: 'unsafe-url', + crossOrigin: true, +}; + +export let serverData = {}; // Tracks data returned from Relevad RTD server + +/** + * Provides contextual IAB categories and segments to the bidders. + * + * @param {} reqBidsConfigObj Bids request configuration + * @param {Function} onDone Ajax callbacek + * @param {} moduleConfig Rtd module configuration + * @param {} userConsent user GDPR consent + */ +export function getBidRequestData(reqBidsConfigObj, onDone, moduleConfig, userConsent) { + moduleConfig.params = moduleConfig.params || {}; + moduleConfig.params.partnerid = moduleConfig.params.partnerid ? moduleConfig.params.partnerid : 1; + + let adunitInfo = reqBidsConfigObj.adUnits.map(adunit => { return [adunit.code, adunit.bids.map(bid => { return [bid.bidder, bid.params] })]; }); + serverData.page = moduleConfig.params.actualUrl || getRefererInfo().page || ''; + const url = (RELEVAD_API_DOMAIN + '/apis/rweb2/' + + '?url=' + encodeURIComponent(serverData.page) + + '&au=' + encodeURIComponent(JSON.stringify(adunitInfo)) + + '&pid=' + encodeURIComponent(moduleConfig.params?.publisherid || '') + + '&aid=' + encodeURIComponent(moduleConfig.params?.apikey || '') + + '&cid=' + encodeURIComponent(moduleConfig.params?.partnerid || '') + + '&gdpra=' + encodeURIComponent(userConsent?.gdpr?.gdprApplies || '') + + '&gdprc=' + encodeURIComponent(userConsent?.gdpr?.consentString || '') + ); + + ajax(url, + { + success: function (response, req) { + if (req.status === 200) { + try { + const data = JSON.parse(response); + serverData.rawdata = data; + if (data) { + addRtdData(reqBidsConfigObj, data, moduleConfig); + } + } catch (e) { + logError(SUBMODULE_NAME, 'unable to parse data: ' + e); + } + onDone(); + } + }, + error: function () { + logError(SUBMODULE_NAME, 'unable to receive data'); + onDone(); + } + }, + null, + { method: 'GET', ...AJAX_OPTIONS, }, + ); +} + +/** + * Sets global ORTB user and site data + * + * @param {dictionary} ortb2 The gloabl ORTB structure + * @param {dictionary} rtdData Rtd segments and categories + */ +export function setGlobalOrtb2(ortb2, rtdData) { + try { + let addOrtb2 = composeOrtb2Data(rtdData, 'site'); + !isEmpty(addOrtb2) && mergeDeep(ortb2, addOrtb2); + } catch (e) { + logError(e) + } +} + +/** + * Compose ORTB2 data fragment from RTD data + * + * @param {dictionary} rtdData RTD segments and categories + * @param {string} prefix Site path prefix + * @return {dictionary} ORTB2 fragment ready to be merged into global or bidder ORTB + */ +function composeOrtb2Data(rtdData, prefix) { + const segments = rtdData.segments; + const categories = rtdData.categories; + const content = rtdData.content; + let addOrtb2 = {}; + + !isEmpty(segments) && deepSetValue(addOrtb2, 'user.ext.data.relevad_rtd', segments); + !isEmpty(categories.cat) && deepSetValue(addOrtb2, prefix + '.cat', categories.cat); + !isEmpty(categories.pagecat) && deepSetValue(addOrtb2, prefix + '.pagecat', categories.pagecat); + !isEmpty(categories.sectioncat) && deepSetValue(addOrtb2, prefix + '.sectioncat', categories.sectioncat); + !isEmpty(categories.cattax) && deepSetValue(addOrtb2, prefix + '.cattax', categories.cattax); + + if (!isEmpty(content) && !isEmpty(content.segs) && content.segtax) { + const contentSegments = { + name: 'relevad', + ext: { segtax: content.segtax }, + segment: content.segs.map(x => { return {id: x}; }) + }; + deepSetValue(addOrtb2, prefix + '.content.data', [contentSegments]); + } + return addOrtb2; +} + +/** + * Sets ORTB user and site data for a given bidder + * + * @param {dictionary} bidderOrtbFragment The bidder ORTB fragment + * @param {object} bidder The bidder name + * @param {object} rtdData RTD categories and segments + */ +function setBidderSiteAndContent(bidderOrtbFragment, bidder, rtdData) { + try { + let addOrtb2 = composeOrtb2Data(rtdData, 'site'); + !isEmpty(rtdData.segments) && deepSetValue(addOrtb2, 'user.ext.data.relevad_rtd', rtdData.segments); + !isEmpty(rtdData.categories?.sectioncat) && deepSetValue(addOrtb2, 'site.ext.data.relevad_rtd', rtdData.categories.sectioncat); + if (isEmpty(addOrtb2)) { + return; + } + bidderOrtbFragment[bidder] = bidderOrtbFragment[bidder] || {}; + mergeDeep(bidderOrtbFragment[bidder], addOrtb2); + } catch (e) { + logError(e) + } +} + +/** + * Filters dictionary entries + * + * @param {array of {key:value}} dict A dictionary with numeric values + * @param {string} minscore The minimum value + * @return {array[names]} Array of category names with scores greater or equal to minscore + */ +function filterByScore(dict, minscore) { + if (dict && !isEmpty(dict)) { + minscore = minscore && typeof minscore == 'number' ? minscore : 30; + try { + const filteredCategories = Object.keys(Object.fromEntries(Object.entries(dict).filter(([k, v]) => v > minscore))); + return isEmpty(filteredCategories) ? null : filteredCategories; + } catch (e) { + logError(e); + } + } + return null; +} + +/** + * Filters RTD by relevancy score + * + * @param {object} data The Input RTD + * @param {string} minscore The minimum relevancy score + * @return {object} Filtered RTD + */ +function getFiltered(data, minscore) { + let relevadData = {'segments': []}; + + minscore = minscore && typeof minscore == 'number' ? minscore : 30; + + const cats = filterByScore(data.cats, minscore); + const pcats = filterByScore(data.pcats, minscore) || cats; + const scats = filterByScore(data.scats, minscore) || pcats; + const cattax = data.cattax ? data.cattax : CATTAX_IAB; + relevadData.categories = {cat: cats, pagecat: pcats, sectioncat: scats, cattax: cattax}; + + const contsegs = filterByScore(data.contsegs, minscore); + const segtax = data.segtax ? data.segtax : SEGTAX_IAB; + relevadData.content = {segs: contsegs, segtax: segtax}; + + try { + if (data && data.segments) { + for (let segId in data.segments) { + if (data.segments.hasOwnProperty(segId)) { + relevadData.segments.push(data.segments[segId].toString()); + } + } + } + } catch (e) { + logError(e); + } + return relevadData; +} + +/** + * Adds Rtd data to global ORTB structure and bidder requests + * + * @param {} reqBids The bid requests list + * @param {} data The Rtd data + * @param {} moduleConfig The Rtd module configuration + */ +export function addRtdData(reqBids, data, moduleConfig) { + moduleConfig = moduleConfig || {}; + moduleConfig.params = moduleConfig.params || {}; + const globalMinScore = moduleConfig.params.hasOwnProperty('minscore') ? moduleConfig.params.minscore : 30; + const relevadData = getFiltered(data, globalMinScore); + const relevadList = relevadData.segments.concat(relevadData.categories.pagecat); + // Publisher side bidder whitelist + const biddersParamsExist = !!(moduleConfig?.params?.bidders); + // RTD Server-side bidder whitelist + const wl = data.wl || null; + const noWhitelists = !biddersParamsExist && isEmpty(wl); + + // Add RTD data to the global ORTB fragments when no whitelists present + noWhitelists && setGlobalOrtb2(reqBids.ortb2Fragments?.global, relevadData); + + // Target GAM/GPT + let setgpt = moduleConfig.params.setgpt || !moduleConfig.params.hasOwnProperty('setgpt'); + if (moduleConfig.dryrun || (typeof window.googletag !== 'undefined' && setgpt)) { + try { + if (window.googletag && window.googletag.pubads && (typeof window.googletag.pubads === 'function')) { + window.googletag.pubads().getSlots().forEach(function (n) { + if (typeof n.setTargeting !== 'undefined' && relevadList && relevadList.length > 0) { + n.setTargeting('relevad_rtd', relevadList); + } + }); + } + } catch (e) { + logError(e); + } + } + + // Set per-bidder RTD + const adUnits = reqBids.adUnits; + adUnits.forEach(adUnit => { + noWhitelists && deepSetValue(adUnit, 'ortb2Imp.ext.data.relevad_rtd', relevadList); + + adUnit.hasOwnProperty('bids') && adUnit.bids.forEach(bid => { + let bidderIndex = (moduleConfig.params.hasOwnProperty('bidders') ? findIndex(moduleConfig.params.bidders, function (i) { + return i.bidder === bid.bidder; + }) : false); + const indexFound = !!(typeof bidderIndex == 'number' && bidderIndex >= 0); + try { + if ( + !biddersParamsExist || + (indexFound && + (!moduleConfig.params.bidders[bidderIndex].hasOwnProperty('adUnitCodes') || + moduleConfig.params.bidders[bidderIndex].adUnitCodes.indexOf(adUnit.code) !== -1 + ) + ) + ) { + let wb = isEmpty(wl) || wl[bid.bidder] === true; + if (!wb && !isEmpty(wl[bid.bidder])) { + wb = true; + for (const [key, value] of entries(wl[bid.bidder])) { + let params = bid?.params || {}; + wb = wb && (key in params) && params[key] == value; + } + } + if (wb && !isEmpty(relevadList)) { + setBidderSiteAndContent(reqBids.ortb2Fragments?.bidder, bid.bidder, relevadData); + deepSetValue(bid, 'params.keywords.relevad_rtd', relevadList); + deepSetValue(bid, 'params.target', [].concat(bid.params?.target ? [bid.params.target] : []).concat(relevadList.map(entry => { return 'relevad_rtd=' + entry; })).join(';')); + let firstPartyData = {}; + firstPartyData[bid.bidder] = { firstPartyData: { relevad_rtd: relevadList } }; + config.setConfig(firstPartyData); + !isEmpty(relevadData.segments) && deepSetValue(bid, 'ortb2.user.ext.data.segments', relevadData.segments); + !isEmpty(relevadData.categories) && deepSetValue(bid, 'ortb2.user.ext.data.contextual_categories', relevadData.categories.pagecat); + !isEmpty(relevadData.categories) && deepSetValue(bid, 'ortb2.site.ext.data.relevad_rtd', relevadData.categories.pagecat); + !isEmpty(relevadData.segments) && deepSetValue(bid, 'ortb2.user.ext.data.relevad_rtd', relevadData.segments); + } + } + } catch (e) { + logError(e); + } + }); + }); + + serverData = {...serverData, ...relevadData}; + return adUnits; +} + +/** + * Sends bid info to the RTD server + * + * @param {JSON} data Bids information + * @param {object} config Configuraion + */ +function sendBids(data, config) { + let dataJson = JSON.stringify(data); + + if (!config.dryrun) { + ajax(RELEVAD_API_DOMAIN + '/apis/bids/', () => {}, dataJson, AJAX_OPTIONS); + } + serverData = { clientdata: data }; +}; + +/** + * Processes AUCTION_END event + * + * @param {object} auctionDetails Auction details + * @param {object} config Module configuration + * @param {object} userConsent User GDPR consent object + */ +function onAuctionEnd(auctionDetails, config, userConsent) { + let adunitObj = {}; + let adunits = []; + + // Add Bids Received + auctionDetails.bidsReceived.forEach((bidObj) => { + if (!adunitObj[bidObj.adUnitCode]) { adunitObj[bidObj.adUnitCode] = []; } + + adunitObj[bidObj.adUnitCode].push({ + bidder: bidObj.bidderCode || bidObj.bidder, + cpm: bidObj.cpm, + currency: bidObj.currency, + dealId: bidObj.dealId, + type: bidObj.mediaType, + ttr: bidObj.timeToRespond, + size: bidObj.size + }); + }); + + entries(adunitObj).forEach(([adunitCode, bidsReceived]) => { + adunits.push({code: adunitCode, bids: bidsReceived}); + }); + + let data = { + event: 'bids', + adunits: adunits, + reledata: serverData.rawdata, + pid: encodeURIComponent(config.params?.publisherid || ''), + aid: encodeURIComponent(config.params?.apikey || ''), + cid: encodeURIComponent(config.params?.partnerid || ''), + gdpra: encodeURIComponent(userConsent?.gdpr?.gdprApplies || ''), + gdprc: encodeURIComponent(userConsent?.gdpr?.consentString || ''), + } + if (!config.dryrun) { + data.page = serverData?.page || config?.params?.actualUrl || getRefererInfo().page || ''; + } + + sendBids(data, config); +} + +export function init(config) { + return true; +} + +export const relevadSubmodule = { + name: SUBMODULE_NAME, + init: init, + onAuctionEndEvent: onAuctionEnd, + getBidRequestData: getBidRequestData +}; + +submodule(MODULE_NAME, relevadSubmodule); diff --git a/modules/relevadRtdProvider.md b/modules/relevadRtdProvider.md new file mode 100644 index 00000000000..fcbc7a7fb36 --- /dev/null +++ b/modules/relevadRtdProvider.md @@ -0,0 +1,108 @@ +# Relevad Real-Time Data Submodule + +Module Name: Relevad Rtd Provider +Module Type: Rtd Provider +Maintainer: anna@relevad.com + +# Description + +Relevad is a contextual semantic analytics company. Our privacy-first, cookieless contextual categorization, segmentation, and keyword generation platform is designed to help publishers and advertisers optimize targeting and increase ad inventory yield. + +Our real-time data processing module provides quality contextual IAB categories and segments along with their relevancy scores to the publisher’s web page. It places them into auction bid requests as global and/or bidder-specific: + +| Attrubute Type | ORTB2 Attribute | +| -------------- | ------------------------------------------------------------ | +| Contextual | “site.cat”: [IAB category codes]
“site.pagecat”: [IAB category codes],
“site.sectioncat”: [IAB category codes]
“site.cattax”: 6 | +| Content | “site.content.data”: {“name”: “relevad”, “ext”: …, “segment”: …} | +| User Data | “user.ext.data.relevad_rtd”: {segments} | + +Publisher may configre minimum relevancy score to restrict the categories and segments we pass to the bidders. +Relevad service does not use browser cookies and is fully GDPR compliant. + +### Publisher Integration + +Compile the Relevad RTD module into the Prebid.js package with + +`gulp build --modules=rtdModule,relevadRtdProvider` + +Add Relevad RTD provider to your Prebid config. Here is an example: + +``` +pbjs.setConfig( + ... + realTimeData: { + auctionDelay: 1000, + dataProviders: [ + { + name: "RelevadRTDModule", + waitForIt: true, + params: { + partnerId: your_partner_id, // Your Relevad partner id. + setgpt: true, // Target or not google GAM/GPT on your page. + minscore: 30, // Minimum relevancy score (0-100). If absent, defaults to 30. + + // The list of bidders to target with Relevad categories and segments. If absent or empty, target all bidders. + bidders: [ + { bidder: "appnexus", // Bidder name + adUnitCodes: ['adUnit-1','adUnit-2'], // List of adUnit codes to target. If absent or empty, target all ad units. + minscore: 70, // Minimum relevancy score for this bidder (0-100). If absent, defaults to the global minscore. + }, + ... + ] + } + } + ] + } + ... +} +``` + +### Relevad Real Time Submodule Configuration Parameters + + + +{: .table .table-bordered .table-striped } +| Name |Type | Description | Notes | +| :------------ | :------------ | :------------ |:------------ | +| name | String | Relevad RTD module name | Mandatory, must be **RelevadRTDModule** | +| waitForIt | Boolean | Whether to delay auction for the RTD module response | Optional. Defaults to false.We recommend setting it to true. Relevad RTD service is very fast. | +| params | Object | | Relevad RTD module configuration | +| params.partnerid | String | Relevad Partner ID, required to enable the service | Mandatory | +| params.publisherid | String | Relevad publisher id | Mandatory | +| params.apikey | String | Relevad API key | Mandatory | +| param.actualUrl | String | Your page URL. When present, will be categorized instead of the browser-provided URL | Optional, defaults to the browser-providedURL | +| params.setgpt | Boolean | Target or not Google GPT/GAM when it is configured on your page | Optional, defaults to true. | +| params.minscore | Integer | Minimum categorization relevancy score in the range of 0-100. Our categories and segments come with their relevancy scores. We’ll send to the bidders only categories and segments with the scores higher than the minscore. |Optional, defaults to 30| +| params.bidders | Dictionary | Bidders with which to share category and segment information | Optional. If empty or absent, target all bidders. | + + + +#### Bidder-specific configuration. Every bidder may have these configuration parameters + +| Name |Type | Description | Notes | +| :------------ | :------------ | :------------ |:------------ | +| bidder | String | Bidder name | Mandatory. Example: “appnexus” | +| adUnitCodes | Array of Strings | List of specific AdUnit codes you with to target | Optional. If empty or absent, all ad units are targeted. | +| minscore | Integer | Bidder-specific minimum categorization relevancy score (0, 100) | Optional, defaults to global minscore above. | + +If you do not have your own `partnerid, publisherid, apikey` please reach out to [info@relevad.com](mailto:info@relevad.com). + +### Testing + +To view an example of the on page setup required: + +```bash +gulp serve-fast --modules=rtdModule,relevadRtdProvider +``` + +Then in your browser access: + +``` +http://localhost:9999/integrationExamples/gpt/relevadRtdProvider_example.html +``` + +Run the unit tests for Relevad RTD module: + +```bash +gulp test --file "test/spec/modules/relevadRtdProvider_spec.js" +``` \ No newline at end of file diff --git a/modules/resetdigitalBidAdapter.js b/modules/resetdigitalBidAdapter.js index a606f0c0b7d..93846e402e9 100644 --- a/modules/resetdigitalBidAdapter.js +++ b/modules/resetdigitalBidAdapter.js @@ -1,9 +1,11 @@ - -import { timestamp, deepAccess } from '../src/utils.js'; +import { timestamp, deepAccess, isStr, deepClone } from '../src/utils.js'; import { getOrigin } from '../libraries/getOrigin/index.js'; import { config } from '../src/config.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; +import {BANNER} from '../src/mediaTypes.js'; + const BIDDER_CODE = 'resetdigital'; +const CURRENCY = 'USD'; export const spec = { code: BIDDER_CODE, @@ -43,16 +45,70 @@ export const spec = { }; } + if (bidderRequest && bidderRequest.uspConsent) { + payload.ccpa = bidderRequest.uspConsent; + } + + function getOrtb2Keywords(ortb2Obj) { + const fields = ['site.keywords', 'site.content.keywords', 'user.keywords', 'app.keywords', 'app.content.keywords']; + let result = []; + + fields.forEach(path => { + let keyStr = deepAccess(ortb2Obj, path); + if (isStr(keyStr)) result.push(keyStr); + }); + return result; + } + + // get the ortb2 keywords data (if it exists) + let ortb2 = deepClone(bidderRequest && bidderRequest.ortb2); + let ortb2KeywordsList = getOrtb2Keywords(ortb2); + // get meta keywords data (if it exists) + let metaKeywords = document.getElementsByTagName('meta')['keywords']; + if (metaKeywords && metaKeywords.content) { + metaKeywords = metaKeywords.content.split(','); + } + for (let x = 0; x < validBidRequests.length; x++) { - let req = validBidRequests[x] + let req = validBidRequests[x]; + + let bidFloor = req.params.bidFloor ? req.params.bidFloor : null; + let bidFloorCur = req.params.bidFloor ? req.params.bidFloorCur : null; + + if (typeof req.getFloor === 'function') { + const floorInfo = req.getFloor({ + currency: CURRENCY, + mediaType: BANNER, + size: '*' + }); + if (typeof floorInfo === 'object' && floorInfo.currency === CURRENCY && !isNaN(parseFloat(floorInfo.floor))) { + bidFloor = parseFloat(floorInfo.floor); + bidFloorCur = CURRENCY; + } + } + + // get param kewords (if it exists) + let paramsKeywords = req.params.keywords ? req.params.keywords.split(',') : []; + // merge all keywords + let keywords = ortb2KeywordsList.concat(paramsKeywords).concat(metaKeywords); payload.imps.push({ pub_id: req.params.pubId, + site_id: req.params.siteID ? req.params.siteID : null, + placement_id: req.params.placement ? req.params.placement : null, + position: req.params.position ? req.params.position : null, + bid_floor: bidFloor, + bid_floor_cur: bidFloorCur, + lat_long: req.params.latLong ? req.params.latLong : null, + inventory: req.params.inventory ? req.params.inventory : null, + visitor: req.params.visitor ? req.params.visitor : null, + keywords: keywords.join(','), zone_id: req.params.zoneId, bid_id: req.bidId, imp_id: req.transactionId, sizes: req.sizes, force_bid: req.params.forceBid, + coppa: config.getConfig('coppa') === true ? 1 : 0, media_types: deepAccess(req, 'mediaTypes') }); } @@ -62,7 +118,8 @@ export const spec = { return { method: 'POST', url: url, - data: JSON.stringify(payload) + data: JSON.stringify(payload), + bids: validBidRequests }; }, interpretResponse: function(serverResponse, bidRequest) { diff --git a/modules/resetdigitalBidAdapter.md b/modules/resetdigitalBidAdapter.md index 2f9f69b5e84..a368c7f5633 100644 --- a/modules/resetdigitalBidAdapter.md +++ b/modules/resetdigitalBidAdapter.md @@ -21,17 +21,43 @@ Video is supported but requires a publisher supplied renderer at this time. mediaTypes: { banner: { sizes: [[300,250]] + }, + + }, + bids: [ + { + bidder: "resetdigital", + params: { + pubId: "your-pub-id", + site_id: "your-site-id", + forceBid: true, + } } + ] + } + ]; + + + var videoAdUnits = [ + { + code: 'your-div', + mediaTypes: { + video: { + playerSize: [640, 480] + }, + }, bids: [ { bidder: "resetdigital", params: { pubId: "your-pub-id", - forceBid: true + site_id: "your-site-id", + forceBid: true, } } ] } ]; + ``` diff --git a/modules/riseBidAdapter.js b/modules/riseBidAdapter.js index 0c4d6148f2b..b536c29e034 100644 --- a/modules/riseBidAdapter.js +++ b/modules/riseBidAdapter.js @@ -384,7 +384,7 @@ function generateGeneralParams(generalObject, bidderRequest) { const {syncEnabled, filterSettings} = config.getConfig('userSync') || {}; const {bidderCode} = bidderRequest; const generalBidParams = generalObject.params; - const timeout = config.getConfig('bidderTimeout'); + const timeout = bidderRequest.timeout; // these params are snake_case instead of camelCase to allow backwards compatability on the server. // in the future, these will be converted to camelCase to match our convention. @@ -449,5 +449,9 @@ function generateGeneralParams(generalObject, bidderRequest) { generalParams.page_url = deepAccess(bidderRequest, 'refererInfo.page') || deepAccess(window, 'location.href'); } + if (config.getConfig('coppa') === true) { + generalParams.coppa = 1; + } + return generalParams; } diff --git a/modules/rivrAnalyticsAdapter.js b/modules/rivrAnalyticsAdapter.js index cf14ff44571..c74ce519ab9 100644 --- a/modules/rivrAnalyticsAdapter.js +++ b/modules/rivrAnalyticsAdapter.js @@ -2,6 +2,7 @@ import {ajax} from '../src/ajax.js'; import adapter from '../libraries/analyticsAdapter/AnalyticsAdapter.js'; import adapterManager from '../src/adapterManager.js'; import * as utils from '../src/utils.js'; +import {getGlobal} from '../src/prebidGlobal.js'; const analyticsType = 'endpoint'; @@ -20,7 +21,7 @@ rivrAnalytics.originEnableAnalytics = rivrAnalytics.enableAnalytics; // override enableAnalytics so we can get access to the config passed in from the page rivrAnalytics.enableAnalytics = (config) => { if (window.rivraddon && window.rivraddon.analytics) { - window.rivraddon.analytics.enableAnalytics(config, {utils, ajax, pbjsGlobalVariable: $$PREBID_GLOBAL$$}); + window.rivraddon.analytics.enableAnalytics(config, {utils, ajax, pbjsGlobalVariable: getGlobal()}); rivrAnalytics.originEnableAnalytics(config); } }; diff --git a/modules/roxotAnalyticsAdapter.js b/modules/roxotAnalyticsAdapter.js index 1ded17f3a5b..2c3be3e1757 100644 --- a/modules/roxotAnalyticsAdapter.js +++ b/modules/roxotAnalyticsAdapter.js @@ -5,8 +5,11 @@ import adapterManager from '../src/adapterManager.js'; import {includes} from '../src/polyfill.js'; import {ajaxBuilder} from '../src/ajax.js'; import {getStorageManager} from '../src/storageManager.js'; +import {MODULE_TYPE_ANALYTICS} from '../src/activities/modules.js'; -const storage = getStorageManager(); +const MODULE_CODE = 'roxot'; + +const storage = getStorageManager({moduleType: MODULE_TYPE_ANALYTICS, moduleName: MODULE_CODE}); let ajax = ajaxBuilder(0); @@ -504,7 +507,7 @@ function buildLogMessage(message) { adapterManager.registerAnalyticsAdapter({ adapter: roxotAdapter, - code: 'roxot' + code: MODULE_CODE, }); export default roxotAdapter; diff --git a/modules/rtbhouseBidAdapter.js b/modules/rtbhouseBidAdapter.js index 9f498014a8e..5c3d430aadf 100644 --- a/modules/rtbhouseBidAdapter.js +++ b/modules/rtbhouseBidAdapter.js @@ -56,7 +56,7 @@ export const spec = { site: mapSite(validBidRequests, bidderRequest), cur: DEFAULT_CURRENCY_ARR, test: validBidRequests[0].params.test || 0, - source: mapSource(validBidRequests[0]), + source: mapSource(validBidRequests[0], bidderRequest), }; if (bidderRequest && bidderRequest.gdprConsent && bidderRequest.gdprConsent.gdprApplies) { @@ -171,10 +171,12 @@ export const spec = { if (fledgeAuctionConfigs) { fledgeAuctionConfigs = Object.entries(fledgeAuctionConfigs).map(([bidId, cfg]) => { - return Object.assign({ + return { bidId, - auctionSignals: {} - }, cfg); + config: Object.assign({ + auctionSignals: {} + }, cfg) + } }); logInfo('Response with FLEDGE:', { bids, fledgeAuctionConfigs }); return { @@ -228,6 +230,13 @@ function mapImpression(slot, bidderRequest) { delete imp.ext.ae; } } + + const tid = deepAccess(slot, 'ortb2Imp.ext.tid'); + if (tid) { + imp.ext = imp.ext || {}; + imp.ext.tid = tid; + } + return imp; } @@ -283,9 +292,9 @@ function mapSite(slot, bidderRequest) { * @param {object} slot Ad Unit Params by Prebid * @returns {object} Source by OpenRTB 2.5 §3.2.2 */ -function mapSource(slot) { +function mapSource(slot, bidderRequest) { const source = { - tid: slot.transactionId, + tid: bidderRequest?.auctionId || '', }; return source; diff --git a/modules/rtdModule/index.js b/modules/rtdModule/index.js index 876da8dda37..29e2ce3de43 100644 --- a/modules/rtdModule/index.js +++ b/modules/rtdModule/index.js @@ -163,9 +163,11 @@ import {getHook, module} from '../../src/hook.js'; import {logError, logInfo, logWarn} from '../../src/utils.js'; import * as events from '../../src/events.js'; import CONSTANTS from '../../src/constants.json'; -import adapterManager, {gdprDataHandler, uspDataHandler} from '../../src/adapterManager.js'; +import adapterManager, {gdprDataHandler, uspDataHandler, gppDataHandler} from '../../src/adapterManager.js'; import {find} from '../../src/polyfill.js'; import {timedAuctionHook} from '../../src/utils/perfMetrics.js'; +import {GDPR_GVLIDS} from '../../src/consentHandler.js'; +import {MODULE_TYPE_RTD} from '../../src/activities/modules.js'; /** @type {string} */ const MODULE_NAME = 'realTimeData'; @@ -188,6 +190,7 @@ let _userConsent; */ export function attachRealTimeDataProvider(submodule) { registeredSubModules.push(submodule); + GDPR_GVLIDS.register(MODULE_TYPE_RTD, submodule.name, submodule.gvlid) return function detach() { const idx = registeredSubModules.indexOf(submodule) if (idx >= 0) { @@ -246,6 +249,7 @@ function getConsentData() { return { gdpr: gdprDataHandler.getConsentData(), usp: uspDataHandler.getConsentData(), + gpp: gppDataHandler.getConsentData(), coppa: !!(config.getConfig('coppa')) } } diff --git a/modules/rubiconAnalyticsAdapter.js b/modules/rubiconAnalyticsAdapter.js index 76043b71c64..7bbc435cb27 100644 --- a/modules/rubiconAnalyticsAdapter.js +++ b/modules/rubiconAnalyticsAdapter.js @@ -5,10 +5,11 @@ import CONSTANTS from '../src/constants.json'; import { ajax } from '../src/ajax.js'; import { config } from '../src/config.js'; import { getGlobal } from '../src/prebidGlobal.js'; -import { getStorageManager } from '../src/storageManager.js'; +import {getStorageManager} from '../src/storageManager.js'; +import {MODULE_TYPE_ANALYTICS} from '../src/activities/modules.js'; const RUBICON_GVL_ID = 52; -export const storage = getStorageManager({gvlid: RUBICON_GVL_ID, moduleName: 'rubicon'}); +export const storage = getStorageManager({moduleType: MODULE_TYPE_ANALYTICS, moduleName: 'rubicon'}); const COOKIE_NAME = 'rpaSession'; const LAST_SEEN_EXPIRE_TIME = 1800000; // 30 mins const END_EXPIRE_TIME = 21600000; // 6 hours diff --git a/modules/rubiconBidAdapter.js b/modules/rubiconBidAdapter.js index bd53e9d5104..36e077aeab8 100644 --- a/modules/rubiconBidAdapter.js +++ b/modules/rubiconBidAdapter.js @@ -1,5 +1,12 @@ +import { ortbConverter } from '../libraries/ortbConverter/converter.js'; +import { pbsExtensions } from '../libraries/pbsExtensions/pbsExtensions.js'; +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { config } from '../src/config.js'; +import { BANNER, NATIVE, VIDEO } from '../src/mediaTypes.js'; +import { find } from '../src/polyfill.js'; +import { getGlobal } from '../src/prebidGlobal.js'; +import { Renderer } from '../src/Renderer.js'; import { - _each, convertTypes, deepAccess, deepSetValue, @@ -11,14 +18,8 @@ import { logMessage, logWarn, mergeDeep, - parseSizesInput + parseSizesInput, _each } from '../src/utils.js'; -import {registerBidder} from '../src/adapters/bidderFactory.js'; -import {config} from '../src/config.js'; -import {BANNER, VIDEO} from '../src/mediaTypes.js'; -import {find} from '../src/polyfill.js'; -import {Renderer} from '../src/Renderer.js'; -import {getGlobal} from '../src/prebidGlobal.js'; const DEFAULT_INTEGRATION = 'pbjs_lite'; const DEFAULT_PBS_INTEGRATION = 'pbjs'; @@ -115,12 +116,14 @@ var sizeMap = { 257: '400x600', 258: '500x200', 259: '998x200', + 261: '480x480', 264: '970x1000', 265: '1920x1080', 274: '1800x200', 278: '320x500', 282: '320x400', 288: '640x380', + 524: '1x2', 548: '500x1000', 550: '980x480', 552: '300x200', @@ -137,17 +140,101 @@ var sizeMap = { 580: '505x656', 622: '192x160' }; + _each(sizeMap, (item, key) => sizeMap[item] = key); +export const converter = ortbConverter({ + request(buildRequest, imps, bidderRequest, context) { + const {bidRequests} = context; + const data = buildRequest(imps, bidderRequest, context); + data.cur = ['USD']; + data.test = config.getConfig('debug') ? 1 : 0; + deepSetValue(data, 'ext.prebid.cache', { + vastxml: { + returnCreative: rubiConf.returnVast === true + } + }); + + deepSetValue(data, 'ext.prebid.bidders', { + rubicon: { + integration: rubiConf.int_type || DEFAULT_PBS_INTEGRATION, + } + }); + + deepSetValue(data, 'ext.prebid.targeting.pricegranularity', getPriceGranularity(config)); + + let modules = (getGlobal()).installedModules; + if (modules && (!modules.length || modules.indexOf('rubiconAnalyticsAdapter') !== -1)) { + deepSetValue(data, 'ext.prebid.analytics', {'rubicon': {'client-analytics': true}}); + } + + addOrtbFirstPartyData(data, bidRequests); + + delete data?.ext?.prebid?.storedrequest; + + // floors + if (rubiConf.disableFloors === true) { + delete data.ext.prebid.floors; + } + + // If the price floors module is active, then we need to signal to PBS! If floorData obj is present is best way to check + const haveFloorDataBidRequests = bidRequests.filter(bidRequest => typeof bidRequest.floorData === 'object'); + if (haveFloorDataBidRequests.length > 0) { + data.ext.prebid.floors = { enabled: false }; + } + return data; + }, + imp(buildImp, bidRequest, context) { + // skip banner-only requests + const bidRequestType = bidType(bidRequest); + if (bidRequestType.includes(BANNER) && bidRequestType.length == 1) return; + + const imp = buildImp(bidRequest, context); + imp.id = bidRequest.adUnitCode; + delete imp.banner; + if (config.getConfig('s2sConfig.defaultTtl')) { + imp.exp = config.getConfig('s2sConfig.defaultTtl'); + }; + bidRequest.params.position === 'atf' && (imp.video.pos = 1); + bidRequest.params.position === 'btf' && (imp.video.pos = 3); + delete imp.ext?.prebid?.storedrequest; + + setBidFloors(bidRequest, imp); + + return imp; + }, + bidResponse(buildBidResponse, bid, context) { + const bidResponse = buildBidResponse(bid, context); + bidResponse.meta.mediaType = deepAccess(bid, 'ext.prebid.type'); + const {bidRequest} = context; + if (bidResponse.mediaType === VIDEO && bidRequest.mediaTypes.video.context === 'outstream') { + bidResponse.renderer = outstreamRenderer(bidResponse); + } + bidResponse.width = bid.w || deepAccess(bidRequest, 'mediaTypes.video.w') || deepAccess(bidRequest, 'params.video.playerWidth'); + bidResponse.height = bid.h || deepAccess(bidRequest, 'mediaTypes.video.h') || deepAccess(bidRequest, 'params.video.playerHeight'); + + if (deepAccess(bid, 'ext.bidder.rp.advid')) { + deepSetValue(bidResponse, 'meta.advertiserId', bid.ext.bidder.rp.advid); + } + return bidResponse; + }, + context: { + netRevenue: rubiConf.netRevenue !== false, // If anything other than false, netRev is true + ttl: 300, + }, + processors: pbsExtensions +}); + export const spec = { code: 'rubicon', gvlid: GVLID, - supportedMediaTypes: [BANNER, VIDEO], + supportedMediaTypes: [BANNER, VIDEO, NATIVE], /** * @param {object} bid * @return boolean */ isBidRequestValid: function (bid) { + let valid = true; if (typeof bid.params !== 'object') { return false; } @@ -159,15 +246,16 @@ export const spec = { return false } } - let bidFormat = bidType(bid, true); + let bidFormats = bidType(bid, true); // bidType is undefined? Return false - if (!bidFormat) { + if (!bidFormats.length) { return false; - } else if (bidFormat === 'video') { // bidType is video, make sure it has required params - return hasValidVideoParams(bid); + } else if (bidFormats.includes(VIDEO)) { // bidType is video, make sure it has required params + valid = hasValidVideoParams(bid); } - // bidType is banner? return true - return true; + const hasBannerOrNativeMediaType = [BANNER, NATIVE].filter(mediaType => bidFormats.includes(mediaType)).length > 0; + if (!hasBannerOrNativeMediaType) return valid; + return valid && hasBannerOrNativeMediaType; }, /** * @param {BidRequest[]} bidRequests @@ -177,166 +265,57 @@ export const spec = { buildRequests: function (bidRequests, bidderRequest) { // separate video bids because the requests are structured differently let requests = []; - const videoRequests = bidRequests.filter(bidRequest => bidType(bidRequest) === 'video').map(bidRequest => { - bidRequest.startTime = new Date().getTime(); - - const data = { - id: bidRequest.transactionId, - test: config.getConfig('debug') ? 1 : 0, - cur: ['USD'], - source: { - tid: bidRequest.transactionId - }, - tmax: bidderRequest.timeout, - imp: [{ - exp: config.getConfig('s2sConfig.defaultTtl'), - id: bidRequest.adUnitCode, - secure: 1, - ext: { - [bidRequest.bidder]: bidRequest.params - }, - video: deepAccess(bidRequest, 'mediaTypes.video') || {} - }], - ext: { - prebid: { - channel: { - name: 'pbjs', - version: $$PREBID_GLOBAL$$.version - }, - cache: { - vastxml: { - returnCreative: rubiConf.returnVast === true - } - }, - targeting: { - includewinners: true, - // includebidderkeys always false for openrtb - includebidderkeys: false, - pricegranularity: getPriceGranularity(config) - }, - bidders: { - rubicon: { - integration: rubiConf.int_type || DEFAULT_PBS_INTEGRATION - } - } - } - } - } - - // Add alias if it is there - if (bidRequest.bidder !== 'rubicon') { - data.ext.prebid.aliases = { - [bidRequest.bidder]: 'rubicon' - } - } - - let modules = (getGlobal()).installedModules; - if (modules && (!modules.length || modules.indexOf('rubiconAnalyticsAdapter') !== -1)) { - deepSetValue(data, 'ext.prebid.analytics', {'rubicon': {'client-analytics': true}}); - } - - let bidFloor; - if (typeof bidRequest.getFloor === 'function' && !rubiConf.disableFloors) { - let floorInfo; - try { - floorInfo = bidRequest.getFloor({ - currency: 'USD', - mediaType: 'video', - size: parseSizes(bidRequest, 'video') - }); - } catch (e) { - logError('Rubicon: getFloor threw an error: ', e); - } - bidFloor = typeof floorInfo === 'object' && floorInfo.currency === 'USD' && !isNaN(parseInt(floorInfo.floor)) ? parseFloat(floorInfo.floor) : undefined; - } else { - bidFloor = parseFloat(deepAccess(bidRequest, 'params.floor')); - } - if (!isNaN(bidFloor)) { - data.imp[0].bidfloor = bidFloor; - } - - // If the price floors module is active, then we need to signal to PBS! If floorData obj is present is best way to check - if (typeof bidRequest.floorData === 'object') { - data.ext.prebid.floors = { enabled: false }; - } - - // if value is set, will overwrite with same value - data.imp[0].ext[bidRequest.bidder].video.size_id = determineRubiconVideoSizeId(bidRequest) - - appendSiteAppDevice(data, bidRequest, bidderRequest); - - addVideoParameters(data, bidRequest); - - if (bidderRequest.gdprConsent) { - // note - gdprApplies & consentString may be undefined in certain use-cases for consentManagement module - let gdprApplies; - if (typeof bidderRequest.gdprConsent.gdprApplies === 'boolean') { - gdprApplies = bidderRequest.gdprConsent.gdprApplies ? 1 : 0; - } - - deepSetValue(data, 'regs.ext.gdpr', gdprApplies); - deepSetValue(data, 'user.ext.consent', bidderRequest.gdprConsent.consentString); - } - - if (bidderRequest.uspConsent) { - deepSetValue(data, 'regs.ext.us_privacy', bidderRequest.uspConsent); - } - - const eids = deepAccess(bidderRequest, 'bids.0.userIdAsEids'); - if (eids && eids.length) { - deepSetValue(data, 'user.ext.eids', eids); - } - - // set user.id value from config value - const configUserId = config.getConfig('user.id'); - if (configUserId) { - deepSetValue(data, 'user.id', configUserId); - } - - if (config.getConfig('coppa') === true) { - deepSetValue(data, 'regs.coppa', 1); - } - - if (bidRequest.schain && hasValidSupplyChainParams(bidRequest.schain)) { - deepSetValue(data, 'source.ext.schain', bidRequest.schain); - } - - const multibid = config.getConfig('multibid'); - if (multibid) { - deepSetValue(data, 'ext.prebid.multibid', multibid.reduce((result, i) => { - let obj = {}; - - Object.keys(i).forEach(key => { - obj[key.toLowerCase()] = i[key]; - }); - - result.push(obj); - - return result; - }, [])); - } - - applyFPD(bidRequest, VIDEO, data); - - // set ext.prebid.auctiontimestamp using auction time - deepSetValue(data.imp[0], 'ext.prebid.auctiontimestamp', bidderRequest.auctionStart); + let filteredHttpRequest = []; + let filteredRequests; + + filteredRequests = bidRequests.filter(req => { + const mediaTypes = bidType(req) || []; + const { length } = mediaTypes; + const { bidonmultiformat, video } = req.params || {}; + + return ( + // if there's just one mediaType and it's video or native, just send it! + (length === 1 && (mediaTypes.includes(VIDEO) || mediaTypes.includes(NATIVE))) || + // if it's two mediaTypes, and they don't contain banner, send to PBS both native & video + (length === 2 && !mediaTypes.includes(BANNER)) || + // if it contains the video param and the Video mediaType, send Video to PBS (not native!) + (video && mediaTypes.includes(VIDEO)) || + // if bidonmultiformat is on, send everything to PBS + (bidonmultiformat && (mediaTypes.includes(VIDEO) || mediaTypes.includes(NATIVE))) + ) + }); - // set storedrequests to undefined so not sent to PBS - // top level and imp level both 'ext.prebid' objects are set above so no exception thrown here - data.ext.prebid.storedrequest = undefined; - data.imp[0].ext.prebid.storedrequest = undefined; + if (filteredRequests && filteredRequests.length) { + const data = converter.toORTB({bidRequests: filteredRequests, bidderRequest}); - return { + filteredHttpRequest.push({ method: 'POST', url: `https://${rubiConf.videoHost || 'prebid-server'}.rubiconproject.com/openrtb2/auction`, data, - bidRequest - } - }); + bidRequest: filteredRequests + }); + } + const bannerBidRequests = bidRequests.filter((req) => { + const mediaTypes = bidType(req) || []; + const {bidonmultiformat, video} = req.params || {}; + return ( + // Send to fastlane if: it must include BANNER and... + mediaTypes.includes(BANNER) && ( + // if it's just banner + (mediaTypes.length === 1) || + // if bidonmultiformat is true + bidonmultiformat || + // if bidonmultiformat is false and there's no video parameter + (!bidonmultiformat && !video) || + // if there's video parameter, but there's no video mediatype + (!bidonmultiformat && video && !mediaTypes.includes(VIDEO)) + ) + ); + }); if (config.getConfig('rubicon.singleRequest') !== true) { // bids are not grouped if single request mode is not enabled - requests = videoRequests.concat(bidRequests.filter(bidRequest => bidType(bidRequest) === 'banner').map(bidRequest => { + requests = filteredHttpRequest.concat(bannerBidRequests.map(bidRequest => { const bidParams = spec.createSlotParams(bidRequest, bidderRequest); return { method: 'GET', @@ -351,8 +330,7 @@ export const spec = { } else { // single request requires bids to be grouped by site id into a single request // note: groupBy wasn't used because deep property access was needed - const nonVideoRequests = bidRequests.filter(bidRequest => bidType(bidRequest) === 'banner'); - const groupedBidRequests = nonVideoRequests.reduce((groupedBids, bid) => { + const groupedBidRequests = bannerBidRequests.reduce((groupedBids, bid) => { (groupedBids[bid.params['siteId']] = groupedBids[bid.params['siteId']] || []).push(bid); return groupedBids; }, {}); @@ -361,7 +339,7 @@ export const spec = { const SRA_BID_LIMIT = 10; // multiple requests are used if bids groups have more than 10 bids - requests = videoRequests.concat(Object.keys(groupedBidRequests).reduce((aggregate, bidGroupKey) => { + requests = filteredHttpRequest.concat(Object.keys(groupedBidRequests).reduce((aggregate, bidGroupKey) => { // for each partioned bidGroup, append a bidRequest to requests list partitionArray(groupedBidRequests[bidGroupKey], SRA_BID_LIMIT).forEach(bidsInGroup => { const combinedSlotParams = spec.combineSlotUrlParams(bidsInGroup.map(bidRequest => { @@ -415,7 +393,6 @@ export const spec = { 'tk_flint', 'x_source.tid', 'l_pb_bid_id', - 'x_source.pchain', 'p_screen_res', 'rp_floor', 'rp_secure', @@ -488,8 +465,8 @@ export const spec = { 'rp_secure': '1', 'tk_flint': `${rubiConf.int_type || DEFAULT_INTEGRATION}_v$prebid.version$`, 'x_source.tid': bidRequest.transactionId, + 'x_imp.ext.tid': bidRequest.transactionId, 'l_pb_bid_id': bidRequest.bidId, - 'x_source.pchain': params.pchain, 'p_screen_res': _getScreenResolution(), 'tk_user_key': params.userId, 'p_geo.latitude': isNaN(parseFloat(latitude)) ? undefined : parseFloat(latitude).toFixed(4), @@ -614,106 +591,36 @@ export const spec = { /** * @param {*} responseObj - * @param {BidRequest|Object.} bidRequest - if request was SRA the bidRequest argument will be a keyed BidRequest array object, + * @param {BidRequest|Object.} request - if request was SRA the bidRequest argument will be a keyed BidRequest array object, * non-SRA responses return a plain BidRequest object * @return {Bid[]} An array of bids which */ - interpretResponse: function (responseObj, {bidRequest}) { + interpretResponse: function (responseObj, request) { responseObj = responseObj.body; + const {data} = request; // check overall response if (!responseObj || typeof responseObj !== 'object') { return []; } - // video response from PBS Java openRTB + // Response from PBS Java openRTB if (responseObj.seatbid) { const responseErrors = deepAccess(responseObj, 'ext.errors.rubicon'); if (Array.isArray(responseErrors) && responseErrors.length > 0) { logWarn('Rubicon: Error in video response'); } - const bids = []; - responseObj.seatbid.forEach(seatbid => { - (seatbid.bid || []).forEach(bid => { - let bidObject = { - requestId: bidRequest.bidId, - currency: responseObj.cur || 'USD', - creativeId: bid.crid, - cpm: bid.price || 0, - bidderCode: seatbid.seat, - ttl: 300, - netRevenue: rubiConf.netRevenue !== false, // If anything other than false, netRev is true - width: bid.w || deepAccess(bidRequest, 'mediaTypes.video.w') || deepAccess(bidRequest, 'params.video.playerWidth'), - height: bid.h || deepAccess(bidRequest, 'mediaTypes.video.h') || deepAccess(bidRequest, 'params.video.playerHeight'), - }; - - if (bid.id) { - bidObject.seatBidId = bid.id; - } - - if (bid.dealid) { - bidObject.dealId = bid.dealid; - } - - if (bid.adomain) { - deepSetValue(bidObject, 'meta.advertiserDomains', Array.isArray(bid.adomain) ? bid.adomain : [bid.adomain]); - } - - if (deepAccess(bid, 'ext.bidder.rp.advid')) { - deepSetValue(bidObject, 'meta.advertiserId', bid.ext.bidder.rp.advid); - } - - let serverResponseTimeMs = deepAccess(responseObj, 'ext.responsetimemillis.rubicon'); - if (bidRequest && serverResponseTimeMs) { - bidRequest.serverResponseTimeMs = serverResponseTimeMs; - } - - if (deepAccess(bid, 'ext.prebid.type') === VIDEO) { - bidObject.mediaType = VIDEO; - deepSetValue(bidObject, 'meta.mediaType', VIDEO); - const extPrebidTargeting = deepAccess(bid, 'ext.prebid.targeting'); - - // If ext.prebid.targeting exists, add it as a property value named 'adserverTargeting' - if (extPrebidTargeting && typeof extPrebidTargeting === 'object') { - bidObject.adserverTargeting = extPrebidTargeting; - } - - // try to get cache values from 'response.ext.prebid.cache.js' - // else try 'bid.ext.prebid.targeting' as fallback - if (bid.ext.prebid.cache && typeof bid.ext.prebid.cache.vastXml === 'object' && bid.ext.prebid.cache.vastXml.cacheId && bid.ext.prebid.cache.vastXml.url) { - bidObject.videoCacheKey = bid.ext.prebid.cache.vastXml.cacheId; - bidObject.vastUrl = bid.ext.prebid.cache.vastXml.url; - } else if (extPrebidTargeting && extPrebidTargeting.hb_uuid && extPrebidTargeting.hb_cache_host && extPrebidTargeting.hb_cache_path) { - bidObject.videoCacheKey = extPrebidTargeting.hb_uuid; - // build url using key and cache host - bidObject.vastUrl = `https://${extPrebidTargeting.hb_cache_host}${extPrebidTargeting.hb_cache_path}?uuid=${extPrebidTargeting.hb_uuid}`; - } - - if (bid.adm) { bidObject.vastXml = bid.adm; } - if (bid.nurl) { bidObject.vastUrl = bid.nurl; } - if (!bidObject.vastUrl && bid.nurl) { bidObject.vastUrl = bid.nurl; } - - const videoContext = deepAccess(bidRequest, 'mediaTypes.video.context'); - if (videoContext.toLowerCase() === 'outstream') { - bidObject.renderer = outstreamRenderer(bidObject); - } - } else { - logWarn('Rubicon: video response received non-video media type'); - } - - bids.push(bidObject); - }); - }); - + const bids = converter.fromORTB({request: data, response: responseObj}).bids; return bids; } let ads = responseObj.ads; let lastImpId; let multibid = 0; + const {bidRequest} = request; // video ads array is wrapped in an object - if (typeof bidRequest === 'object' && !Array.isArray(bidRequest) && bidType(bidRequest) === 'video' && typeof ads === 'object') { + if (typeof bidRequest === 'object' && !Array.isArray(bidRequest) && bidType(bidRequest).includes(VIDEO) && typeof ads === 'object') { ads = ads[bidRequest.adUnitCode]; } @@ -779,7 +686,6 @@ export const spec = { } else { logError(`Rubicon: bidRequest undefined at index position:${i}`, bidRequest, responseObj); } - return bids; }, []).sort((adA, adB) => { return (adB.cpm || 0.0) - (adA.cpm || 0.0); @@ -918,7 +824,7 @@ function outstreamRenderer(rtbBid) { function parseSizes(bid, mediaType) { let params = bid.params; - if (mediaType === 'video') { + if (mediaType === VIDEO) { let size = []; if (params.video && params.video.playerWidth && params.video.playerHeight) { size = [ @@ -948,65 +854,6 @@ function parseSizes(bid, mediaType) { return masSizeOrdering(sizes); } -/** - * @param {Object} data - * @param bidRequest - * @param bidderRequest - */ -function appendSiteAppDevice(data, bidRequest, bidderRequest) { - if (!data) return; - - // ORTB specifies app OR site - if (typeof config.getConfig('app') === 'object') { - data.app = config.getConfig('app'); - } else { - data.site = { - page: _getPageUrl(bidRequest, bidderRequest) - } - } - if (typeof config.getConfig('device') === 'object') { - data.device = config.getConfig('device'); - } - // Add language to site and device objects if there - if (bidRequest.params.video.language) { - ['site', 'device'].forEach(function(param) { - if (data[param]) { - if (param === 'site') { - data[param].content = Object.assign({language: bidRequest.params.video.language}, data[param].content) - } else { - data[param] = Object.assign({language: bidRequest.params.video.language}, data[param]) - } - } - }); - } -} - -/** - * @param {Object} data - * @param {BidRequest} bidRequest - */ -function addVideoParameters(data, bidRequest) { - if (typeof data.imp[0].video === 'object' && data.imp[0].video.skip === undefined) { - data.imp[0].video.skip = bidRequest.params.video.skip; - } - if (typeof data.imp[0].video === 'object' && data.imp[0].video.skipafter === undefined) { - data.imp[0].video.skipafter = bidRequest.params.video.skipdelay; - } - // video.pos can already be specified by adunit.mediatypes.video.pos. - // but if not, it might be specified in the params - if (typeof data.imp[0].video === 'object' && data.imp[0].video.pos === undefined) { - if (bidRequest.params.position === 'atf') { - data.imp[0].video.pos = 1; - } else if (bidRequest.params.position === 'btf') { - data.imp[0].video.pos = 3; - } - } - - const size = parseSizes(bidRequest, 'video') - data.imp[0].video.w = size[0] - data.imp[0].video.h = size[1] -} - function applyFPD(bidRequest, mediaType, data) { const BID_FPD = { user: {ext: {data: {...bidRequest.params.visitor}}}, @@ -1117,11 +964,15 @@ function mapSizes(sizes) { export function classifiedAsVideo(bidRequest) { let isVideo = typeof deepAccess(bidRequest, `mediaTypes.${VIDEO}`) !== 'undefined'; let isBanner = typeof deepAccess(bidRequest, `mediaTypes.${BANNER}`) !== 'undefined'; + let isBidOnMultiformat = typeof deepAccess(bidRequest, `params.bidonmultiformat`) !== 'undefined'; let isMissingVideoParams = typeof deepAccess(bidRequest, 'params.video') !== 'object'; // If an ad has both video and banner types, a legacy implementation allows choosing video over banner // based on whether or not there is a video object defined in the params // Given this legacy implementation, other code depends on params.video being defined + // if it's bidonmultiformat, we don't care of the video object + if (isVideo && isBidOnMultiformat) return true; + if (isBanner && isMissingVideoParams) { isVideo = false; } @@ -1132,13 +983,14 @@ export function classifiedAsVideo(bidRequest) { } /** - * Determine bidRequest mediaType + * Determine bidRequest mediaTypes. All mediaTypes must be correct. If one fails, all the others will fail too. * @param bid the bid to test - * @param log whether we should log errors/warnings for invalid bids - * @returns {string|undefined} Returns 'video' or 'banner' if resolves to a type, or undefined otherwise (invalid). + * @param log boolean. whether we should log errors/warnings for invalid bids + * @returns {string|undefined} Returns an array containing one of 'video' or 'banner' or 'native' if resolves to a type. */ function bidType(bid, log = false) { // Is it considered video ad unit by rubicon + let bidTypes = []; if (classifiedAsVideo(bid)) { // Removed legacy mediaType support. new way using mediaTypes.video object is now required // We require either context as instream or outstream @@ -1146,37 +998,43 @@ function bidType(bid, log = false) { if (log) { logError('Rubicon: mediaTypes.video.context must be outstream or instream'); } - return; + return bidTypes; } // we require playerWidth and playerHeight to come from one of params.playerWidth/playerHeight or mediaTypes.video.playerSize or adUnit.sizes - if (parseSizes(bid, 'video').length < 2) { + if (parseSizes(bid, VIDEO).length < 2) { if (log) { logError('Rubicon: could not determine the playerSize of the video'); } - return; + return bidTypes; } if (log) { logMessage('Rubicon: making video request for adUnit', bid.adUnitCode); } - return 'video'; - } else { + bidTypes.push(VIDEO); + } + if (typeof deepAccess(bid, `mediaTypes.${NATIVE}`) !== 'undefined') { + bidTypes.push(NATIVE); + } + + if (typeof deepAccess(bid, `mediaTypes.${BANNER}`) !== 'undefined') { // we require banner sizes to come from one of params.sizes or mediaTypes.banner.sizes or adUnit.sizes, in that order // if we cannot determine them, we reject it! - if (parseSizes(bid, 'banner').length === 0) { + if (parseSizes(bid, BANNER).length === 0) { if (log) { logError('Rubicon: could not determine the sizes for banner request'); } - return; + return bidTypes; } // everything looks good for banner so lets do it if (log) { logMessage('Rubicon: making banner request for adUnit', bid.adUnitCode); } - return 'banner'; + bidTypes.push(BANNER); } + return bidTypes; } export const resetRubiConf = () => rubiConf = {}; @@ -1306,4 +1164,64 @@ export function resetUserSync() { hasSynced = false; } +/** + * Sets the floor on the bidRequest. imp.bidfloor and imp.bidfloorcur + * should be already set by the conversion library. if they're not, + * or invalid, try to read from params.floor. + * @param {*} bidRequest + * @param {*} imp + */ +function setBidFloors(bidRequest, imp) { + if (imp.bidfloorcur != 'USD') { + delete imp.bidfloor; + delete imp.bidfloorcur; + } + + if (!imp.bidfloor) { + let bidFloor = parseFloat(deepAccess(bidRequest, 'params.floor')); + + if (!isNaN(bidFloor)) { + imp.bidfloor = bidFloor; + imp.bidfloorcur = 'USD'; + } + } +} + +function addOrtbFirstPartyData(data, nonBannerRequests) { + let fpd = {}; + const keywords = new Set(); + nonBannerRequests.forEach(bidRequest => { + const bidFirstPartyData = { + user: {ext: {data: {...bidRequest.params.visitor}}}, + site: {ext: {data: {...bidRequest.params.inventory}}} + }; + + // add site.content.language + const impThatHasVideoLanguage = data.imp.find(imp => imp.ext?.prebid?.bidder?.rubicon?.video?.language); + if (impThatHasVideoLanguage) { + bidFirstPartyData.site.content = { + language: impThatHasVideoLanguage.ext?.prebid?.bidder?.rubicon?.video?.language + } + } + + if (bidRequest.params.keywords) { + const keywordsArray = (!Array.isArray(bidRequest.params.keywords) ? bidRequest.params.keywords.split(',') : bidRequest.params.keywords); + keywordsArray.forEach(keyword => keywords.add(keyword)); + } + fpd = mergeDeep(fpd, bidRequest.ortb2 || {}, bidFirstPartyData); + + // add user.id from config. + // NOTE: This is DEPRECATED. user.id should come from setConfig({ortb2}). + const configUserId = config.getConfig('user.id'); + fpd.user.id = fpd.user.id || configUserId; + }); + + mergeDeep(data, fpd); + + if (keywords && keywords.size) { + deepSetValue(data, 'site.keywords', Array.from(keywords.values()).join(',')); + } + delete data?.ext?.prebid?.storedrequest; +} + registerBidder(spec); diff --git a/modules/scatteredBidAdapter.js b/modules/scatteredBidAdapter.js new file mode 100644 index 00000000000..47dc09cd1b2 --- /dev/null +++ b/modules/scatteredBidAdapter.js @@ -0,0 +1,72 @@ +// jshint esversion: 6, es3: false, node: true +'use strict'; + +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { BANNER } from '../src/mediaTypes.js'; +import { deepAccess, logInfo } from '../src/utils.js'; +import { ortbConverter } from '../libraries/ortbConverter/converter.js'; + +const BIDDER_CODE = 'scattered'; +const GVLID = 1179; +export const converter = ortbConverter({ + context: { + mediaType: BANNER, + ttl: 360, + netRevenue: true + } +}) + +export const spec = { + code: BIDDER_CODE, + gvlid: GVLID, + supportedMediaTypes: [BANNER], + + // 1. + isBidRequestValid: function (bid) { + const bidderDomain = deepAccess(bid, 'params.bidderDomain') + if (bidderDomain === undefined || bidderDomain === '') { + return false + } + + const sizes = deepAccess(bid, 'mediaTypes.banner.sizes') + if (sizes === undefined || sizes.length < 1) { + return false + } + + return true + }, + + // 2. + buildRequests: function (bidRequests, bidderRequest) { + return { + method: 'POST', + url: 'https://' + getKeyOnAny(bidRequests, 'params.bidderDomain'), + data: converter.toORTB({ bidderRequest, bidRequests }), + options: { + contentType: 'application/json' + }, + }; + }, + + // 3. + interpretResponse: function (response, request) { + if (!response.body) return; + return converter.fromORTB({ response: response.body, request: request.data }).bids; + }, + + // 4 + onBidWon: function (bid) { + logInfo('onBidWon', bid) + } +} + +function getKeyOnAny(collection, key) { + for (let i = 0; i < collection.length; i++) { + const result = deepAccess(collection[i], key); + if (result) { + return result; + } + } +} + +registerBidder(spec); diff --git a/modules/scatteredBidAdapter.md b/modules/scatteredBidAdapter.md new file mode 100644 index 00000000000..031d953e32b --- /dev/null +++ b/modules/scatteredBidAdapter.md @@ -0,0 +1,36 @@ +# Overview + +``` +Module Name: Scattered Adapter +Module Type: Bidder Adapter +Maintainer: office@scattered.pl +``` + +# Description + +Module that connects to Scattered's demand sources. +It uses OpenRTB standard to communicate between the adapter and bidding servers. + +# Test Parameters + +```javascript + var adUnits = [ + { + code: 'test-div', + mediaTypes: { + banner: { + sizes: [[300, 250]], // a display size + } + }, + bids: [ + { + bidder: "scattered", + params: { + bidderDomain: "prebid-test.scattered.eu/bid", + test: 0 + } + } + ] + } + ]; +``` \ No newline at end of file diff --git a/modules/seedingAllianceBidAdapter.js b/modules/seedingAllianceBidAdapter.js index 64b9cd5d4aa..29953da7ffa 100755 --- a/modules/seedingAllianceBidAdapter.js +++ b/modules/seedingAllianceBidAdapter.js @@ -2,120 +2,111 @@ 'use strict'; import { registerBidder } from '../src/adapters/bidderFactory.js'; -import { NATIVE } from '../src/mediaTypes.js'; -import { _map, deepSetValue, isEmpty, deepAccess } from '../src/utils.js'; +import { NATIVE, BANNER } from '../src/mediaTypes.js'; +import { _map, isArray, isEmpty, deepSetValue, replaceAuctionPrice } from '../src/utils.js'; import { config } from '../src/config.js'; import { convertOrtbRequestToProprietaryNative } from '../src/native.js'; -const BIDDER_CODE = 'seedingAlliance'; const GVL_ID = 371; +const BIDDER_CODE = 'seedingAlliance'; const DEFAULT_CUR = 'EUR'; const ENDPOINT_URL = 'https://b.nativendo.de/cds/rtb/bid?format=openrtb2.5&ssp=pb'; -const NATIVE_ASSET_IDS = {0: 'title', 1: 'body', 2: 'sponsoredBy', 3: 'image', 4: 'cta', 5: 'icon'}; +const NATIVE_ASSET_IDS = { 0: 'title', 1: 'body', 2: 'sponsoredBy', 3: 'image', 4: 'cta', 5: 'icon' }; const NATIVE_PARAMS = { - title: { - id: 0, - name: 'title' - }, - - body: { - id: 1, - name: 'data', - type: 2 - }, - - sponsoredBy: { - id: 2, - name: 'data', - type: 1 - }, - - image: { - id: 3, - type: 3, - name: 'img' - }, - - cta: { - id: 4, - type: 12, - name: 'data' - }, - - icon: { - id: 5, - type: 1, - name: 'img' - } + title: { id: 0, name: 'title' }, + body: { id: 1, name: 'data', type: 2 }, + sponsoredBy: { id: 2, name: 'data', type: 1 }, + image: { id: 3, type: 3, name: 'img' }, + cta: { id: 4, type: 12, name: 'data' }, + icon: { id: 5, type: 1, name: 'img' } }; export const spec = { code: BIDDER_CODE, - gvlid: GVL_ID, + supportedMediaTypes: [NATIVE, BANNER], - supportedMediaTypes: [NATIVE], - - isBidRequestValid: function(bid) { + isBidRequestValid: function (bid) { return !!bid.params.adUnitId; }, - buildRequests: (validBidRequests, bidderRequest) => { + buildRequests: (validBidRequests = [], bidderRequest) => { // convert Native ORTB definition to old-style prebid native definition validBidRequests = convertOrtbRequestToProprietaryNative(validBidRequests); - const pt = setOnAny(validBidRequests, 'params.pt') || setOnAny(validBidRequests, 'params.priceType') || 'net'; - const tid = bidderRequest.auctionId; - const cur = [config.getConfig('currency.adServerCurrency') || DEFAULT_CUR]; let url = bidderRequest.refererInfo.page; - const imp = validBidRequests.map((bid, id) => { - const assets = _map(bid.nativeParams, (bidParams, key) => { - const props = NATIVE_PARAMS[key]; - - const asset = { - required: bidParams.required & 1 - }; + const imps = validBidRequests.map((bidRequest, id) => { + const imp = { + id: String(id + 1), + tagid: bidRequest.params.adUnitId + }; - if (props) { - asset.id = props.id; + /** + * Native Ad + */ + if (bidRequest.nativeParams) { + const assets = _map(bidRequest.nativeParams, (nativeAsset, key) => { + const props = NATIVE_PARAMS[key]; + + if (props) { + let wmin, hmin, w, h; + let aRatios = nativeAsset.aspect_ratios; + + if (aRatios && aRatios[0]) { + aRatios = aRatios[0]; + wmin = aRatios.min_width || 0; + hmin = aRatios.ratio_height * wmin / aRatios.ratio_width | 0; + } - let w, h; + if (nativeAsset.sizes) { + const sizes = flatten(nativeAsset.sizes); + w = parseInt(sizes[0], 10); + h = parseInt(sizes[1], 10); + } - if (bidParams.sizes) { - w = bidParams.sizes[0]; - h = bidParams.sizes[1]; + const asset = { + id: props.id, + required: nativeAsset.required & 1 + }; + + asset[props.name] = { + len: nativeAsset.len, + type: props.type, + wmin, + hmin, + w, + h + }; + + return asset; + } else { + // TODO Filter impressions with required assets we don't support } + }).filter(Boolean); - asset[props.name] = { - len: bidParams.len, - type: props.type, - w, - h - }; + imp.native = { + request: { + assets + } + }; + } else { + let sizes = transformSizes(bidRequest.sizes); - return asset; + imp.banner = { + format: sizes, + w: sizes[0] ? sizes[0].w : 0, + h: sizes[0] ? sizes[0].h : 0 } - }) - .filter(Boolean); + } - if (bid.params.url) { - url = bid.params.url; + if (bidRequest.params.url) { + url = bidRequest.params.url; } - return { - id: String(id + 1), - tagid: bid.params.adUnitId, - tid: tid, - pt: pt, - native: { - request: { - assets - } - } - }; + return imp; }); const request = { @@ -123,12 +114,9 @@ export const spec = { site: { page: url }, - device: { - ua: navigator.userAgent - }, - cur, - imp, - user: {}, + cur: [config.getConfig('currency.adServerCurrency') || DEFAULT_CUR], + imp: imps, + tmax: bidderRequest.timeout, regs: { ext: { gdpr: 0, @@ -137,23 +125,22 @@ export const spec = { } }; - if (bidderRequest && bidderRequest.gdprConsent) { + if (bidderRequest.gdprConsent) { + request.user = {}; + deepSetValue(request, 'user.ext.consent', bidderRequest.gdprConsent.consentString); deepSetValue(request, 'regs.ext.gdpr', (typeof bidderRequest.gdprConsent.gdprApplies === 'boolean' && bidderRequest.gdprConsent.gdprApplies) ? 1 : 0); } return { method: 'POST', - url: ENDPOINT_URL, + url: config.getConfig('seedingAlliance.endpoint') || ENDPOINT_URL, data: JSON.stringify(request), - options: { - contentType: 'application/json' - }, - bids: validBidRequests + bidRequests: validBidRequests }; }, - interpretResponse: function(serverResponse, { bids }) { + interpretResponse: function (serverResponse, { bidRequests }) { if (isEmpty(serverResponse.body)) { return []; } @@ -165,35 +152,72 @@ export const spec = { return result; }, []) : []; - return bids - .map((bid, id) => { + return bidRequests + .map((bidRequest, id) => { const bidResponse = bidResponses[id]; + const type = bidRequest.nativeParams ? NATIVE : BANNER; + if (bidResponse) { - return { - requestId: bid.bidId, + const bidObject = { + requestId: bidRequest.bidId, // TODO get this value from response? cpm: bidResponse.price, creativeId: bidResponse.crid, - ttl: 1000, - netRevenue: (!bid.netRevenue || bid.netRevenue === 'net'), + ttl: 600, + netRevenue: true, currency: cur, - mediaType: NATIVE, + mediaType: type, bidderCode: BIDDER_CODE, - native: parseNative(bidResponse), meta: { advertiserDomains: bidResponse.adomain && bidResponse.adomain.length > 0 ? bidResponse.adomain : [] } }; + + if (type === NATIVE) { + bidObject.native = parseNative(bidResponse); + bidObject.mediaType = NATIVE; + } + + if (type === BANNER) { + bidObject.ad = replaceAuctionPrice(bidResponse.adm, bidResponse.price); + bidObject.width = bidResponse.w; + bidObject.height = bidResponse.h; + bidObject.mediaType = BANNER; + } + + return bidObject; } }) .filter(Boolean); } }; -registerBidder(spec); +function transformSizes(requestSizes) { + if (!isArray(requestSizes)) { + return []; + } + + if (requestSizes.length === 2 && !isArray(requestSizes[0])) { + return [{ + w: parseInt(requestSizes[0], 10), + h: parseInt(requestSizes[1], 10) + }]; + } else if (isArray(requestSizes[0])) { + return requestSizes.map(item => ({ + w: parseInt(item[0], 10), + h: parseInt(item[1], 10) + })); + } + + return []; +} + +function flatten(arr) { + return [].concat(...arr); +} function parseNative(bid) { - const {assets, link, imptrackers} = bid.adm.native; + const { assets, link, imptrackers } = bid.adm.native; let clickUrl = link.url.replace(/\$\{AUCTION_PRICE\}/g, bid.price); @@ -228,15 +252,4 @@ function parseNative(bid) { return result; } -function setOnAny(collection, key) { - for (let i = 0, result; i < collection.length; i++) { - result = deepAccess(collection[i], key); - if (result) { - return result; - } - } -} - -function flatten(arr) { - return [].concat(...arr); -} +registerBidder(spec); diff --git a/modules/seedtagBidAdapter.js b/modules/seedtagBidAdapter.js index 1a4f903789b..f54245a41ab 100644 --- a/modules/seedtagBidAdapter.js +++ b/modules/seedtagBidAdapter.js @@ -60,6 +60,10 @@ function hasVideoMediaType(bid) { return !!bid.mediaTypes && !!bid.mediaTypes.video; } +function hasBannerMediaType(bid) { + return !!bid.mediaTypes && !!bid.mediaTypes.banner; +} + function hasMandatoryDisplayParams(bid) { const p = bid.params; return ( @@ -72,17 +76,27 @@ function hasMandatoryDisplayParams(bid) { function hasMandatoryVideoParams(bid) { const videoParams = getVideoParams(bid); - return ( + let isValid = !!bid.params.publisherId && !!bid.params.adUnitId && hasVideoMediaType(bid) && !!videoParams.playerSize && isArray(videoParams.playerSize) && - videoParams.playerSize.length > 0 && - // only instream is supported for video - videoParams.context === 'instream' && - bid.params.placement === 'inStream' - ); + videoParams.playerSize.length > 0; + + switch (bid.params.placement) { + // instream accept only video format + case 'inStream': + return isValid && videoParams.context === 'instream'; + // outstream accept banner/native/video format + default: + return ( + isValid && + videoParams.context === 'outstream' && + hasBannerMediaType(bid) && + hasMandatoryDisplayParams(bid) + ); + } } function buildBidRequest(validBidRequest) { @@ -260,6 +274,18 @@ export const spec = { payload.coppa = coppa; } + if (bidderRequest.gppConsent) { + payload.gppConsent = { + gppString: bidderRequest.gppConsent.gppString, + applicableSections: bidderRequest.gppConsent.applicableSections + } + } else if (bidderRequest.ortb2?.regs?.gpp) { + payload.gppConsent = { + gppString: bidderRequest.ortb2.regs.gpp, + applicableSections: bidderRequest.ortb2.regs.gpp_sid + } + } + const payloadString = JSON.stringify(payload); return { method: 'POST', diff --git a/modules/sharedIdSystem.js b/modules/sharedIdSystem.js index 7562b472047..ef56e10870b 100644 --- a/modules/sharedIdSystem.js +++ b/modules/sharedIdSystem.js @@ -5,13 +5,15 @@ * @requires module:modules/userId */ -import { parseUrl, buildUrl, triggerPixel, logInfo, hasDeviceAccess, generateUUID } from '../src/utils.js'; +import {parseUrl, buildUrl, triggerPixel, logInfo, hasDeviceAccess, generateUUID} from '../src/utils.js'; import {submodule} from '../src/hook.js'; -import { coppaDataHandler } from '../src/adapterManager.js'; +import {coppaDataHandler} from '../src/adapterManager.js'; import {getStorageManager} from '../src/storageManager.js'; import {VENDORLESS_GVLID} from '../src/consentHandler.js'; +import {MODULE_TYPE_UID} from '../src/activities/modules.js'; +import {domainOverrideToRootDomain} from '../libraries/domainOverrideToRootDomain/index.js'; -export const storage = getStorageManager({moduleName: 'pubCommonId', gvlid: VENDORLESS_GVLID}); +export const storage = getStorageManager({moduleType: MODULE_TYPE_UID, moduleName: 'pubCommonId'}); const COOKIE = 'cookie'; const LOCAL_STORAGE = 'html5'; const OPTOUT_NAME = '_pubcid_optout'; @@ -38,12 +40,15 @@ function readValue(name, type) { } } -function getIdCallback(pubcid, pixelCallback) { - return function (callback) { - if (typeof pixelCallback === 'function') { - pixelCallback(); +function getIdCallback(pubcid, pixelUrl) { + return function (callback, getStoredId) { + if (pixelUrl) { + queuePixelCallback(pixelUrl, pubcid, () => { + callback(getStoredId() || pubcid); + })(); + } else { + callback(pubcid); } - callback(pubcid); } } @@ -58,7 +63,7 @@ function queuePixelCallback(pixelUrl, id = '', callback) { const targetUrl = buildUrl(urlInfo); return function () { - triggerPixel(targetUrl); + triggerPixel(targetUrl, callback); }; } @@ -125,8 +130,7 @@ export const sharedIdSystemSubmodule = { if (!newId) newId = (create && hasDeviceAccess()) ? generateUUID() : undefined; } - const pixelCallback = queuePixelCallback(pixelUrl, newId); - return {id: newId, callback: getIdCallback(newId, pixelCallback)}; + return {id: newId, callback: getIdCallback(newId, pixelUrl)}; }, /** * performs action to extend an id. There are generally two ways to extend the expiration time @@ -169,31 +173,7 @@ export const sharedIdSystemSubmodule = { } }, - domainOverride: function () { - const domainElements = document.domain.split('.'); - const cookieName = `_gd${Date.now()}`; - for (let i = 0, topDomain, testCookie; i < domainElements.length; i++) { - const nextDomain = domainElements.slice(i).join('.'); - - // write test cookie - storage.setCookie(cookieName, '1', undefined, undefined, nextDomain); - - // read test cookie to verify domain was valid - testCookie = storage.getCookie(cookieName); - - // delete test cookie - storage.setCookie(cookieName, '', 'Thu, 01 Jan 1970 00:00:01 GMT', undefined, nextDomain); - - if (testCookie === '1') { - // cookie was written successfully using test domain so the topDomain is updated - topDomain = nextDomain; - } else { - // cookie failed to write using test domain so exit by returning the topDomain - return topDomain; - } - } - } - + domainOverride: domainOverrideToRootDomain(storage, 'sharedId'), }; submodule('userId', sharedIdSystemSubmodule); diff --git a/modules/sharethroughBidAdapter.js b/modules/sharethroughBidAdapter.js index 0c8b84fd0c5..9c91af8b130 100644 --- a/modules/sharethroughBidAdapter.js +++ b/modules/sharethroughBidAdapter.js @@ -2,7 +2,6 @@ import { deepAccess, generateUUID, inIframe } from '../src/utils.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; import { config } from '../src/config.js'; import { BANNER, VIDEO } from '../src/mediaTypes.js'; -import { createEidsArray } from './userId/eids.js'; const VERSION = '4.3.0'; const BIDDER_CODE = 'sharethrough'; @@ -22,7 +21,7 @@ export const sharethroughAdapterSpec = { isBidRequestValid: bid => !!bid.params.pkey && bid.bidder === BIDDER_CODE, buildRequests: (bidRequests, bidderRequest) => { - const timeout = config.getConfig('bidderTimeout'); + const timeout = bidderRequest.timeout; const firstPartyData = bidderRequest.ortb2 || {}; const nonHttp = sharethroughInternal.getProtocol().indexOf('http') < 0; @@ -66,7 +65,7 @@ export const sharethroughAdapterSpec = { req.user = nullish(firstPartyData.user, {}); if (!req.user.ext) req.user.ext = {}; - req.user.ext.eids = createEidsArray(deepAccess(bidRequests[0], 'userId')) || []; + req.user.ext.eids = bidRequests[0].userIdAsEids || []; if (bidderRequest.gdprConsent) { const gdprApplies = bidderRequest.gdprConsent.gdprApplies === true; diff --git a/modules/shinezBidAdapter.js b/modules/shinezBidAdapter.js index 0ce2eed6479..f93736894f5 100644 --- a/modules/shinezBidAdapter.js +++ b/modules/shinezBidAdapter.js @@ -364,7 +364,7 @@ function generateGeneralParams(generalObject, bidderRequest) { const {syncEnabled, filterSettings} = config.getConfig('userSync') || {}; const {bidderCode} = bidderRequest; const generalBidParams = generalObject.params; - const timeout = config.getConfig('bidderTimeout'); + const timeout = bidderRequest.timeout; // these params are snake_case instead of camelCase to allow backwards compatability on the server. // in the future, these will be converted to camelCase to match our convention. diff --git a/modules/showheroes-bsBidAdapter.js b/modules/showheroes-bsBidAdapter.js index f9ce3a450cf..4fa95e4ba51 100644 --- a/modules/showheroes-bsBidAdapter.js +++ b/modules/showheroes-bsBidAdapter.js @@ -38,7 +38,9 @@ export const spec = { }, buildRequests: function(validBidRequests, bidderRequest) { let adUnits = []; - const pageURL = validBidRequests[0].params.contentPageUrl || bidderRequest.refererInfo.referer; + const pageURL = validBidRequests[0].params.contentPageUrl || + bidderRequest.refererInfo.canonicalUrl || + deepAccess(window, 'location.href'); const isStage = !!validBidRequests[0].params.stage; const isViralize = !!validBidRequests[0].params.unitId; const isOutstream = deepAccess(validBidRequests[0], 'mediaTypes.video.context') === 'outstream'; @@ -50,6 +52,7 @@ export const spec = { const defaultSchain = validBidRequests[0].schain || {}; const consentData = bidderRequest.gdprConsent || {}; + const uspConsent = bidderRequest.uspConsent || ''; const gdprConsent = { apiVersion: consentData.apiVersion || 2, gdprApplies: consentData.gdprApplies || 0, @@ -104,6 +107,7 @@ export const spec = { height: size[1] }; rBid.gdprConsent = gdprConsent; + rBid.uspConsent = uspConsent; } return rBid; @@ -138,6 +142,7 @@ export const spec = { 'bidRequests': adUnits, 'context': { 'gdprConsent': gdprConsent, + 'uspConsent': uspConsent, 'schain': defaultSchain, 'pageURL': QA.pageURL || encodeURIComponent(pageURL) } diff --git a/modules/sigmoidAnalyticsAdapter.js b/modules/sigmoidAnalyticsAdapter.js index dc386813978..18e1e20e3e3 100644 --- a/modules/sigmoidAnalyticsAdapter.js +++ b/modules/sigmoidAnalyticsAdapter.js @@ -6,8 +6,10 @@ import CONSTANTS from '../src/constants.json'; import adapterManager from '../src/adapterManager.js'; import {getStorageManager} from '../src/storageManager.js'; import {generateUUID, logError, logInfo} from '../src/utils.js'; +import {MODULE_TYPE_ANALYTICS} from '../src/activities/modules.js'; -const storage = getStorageManager(); +const MODULE_CODE = 'sigmoid'; +const storage = getStorageManager({moduleType: MODULE_TYPE_ANALYTICS, moduleName: MODULE_CODE}); const url = 'https://kinesis.us-east-1.amazonaws.com/'; const analyticsType = 'endpoint'; @@ -285,7 +287,7 @@ function pushEvent(eventType, args) { adapterManager.registerAnalyticsAdapter({ adapter: sigmoidAdapter, - code: 'sigmoid' + code: MODULE_CODE, }); export default sigmoidAdapter; diff --git a/modules/sirdataRtdProvider.js b/modules/sirdataRtdProvider.js index 369276e7638..40ee3d8b973 100644 --- a/modules/sirdataRtdProvider.js +++ b/modules/sirdataRtdProvider.js @@ -1,12 +1,13 @@ /** * This module adds Sirdata provider to the real time data module + * and now supports Seller Defined Audience * The {@link module:modules/realTimeData} module is required * The module will fetch segments (user-centric) and categories (page-centric) from Sirdata server * The module will automatically handle user's privacy and choice in California (IAB TL CCPA Framework) and in Europe (IAB EU TCF FOR GDPR) * @module modules/sirdataRtdProvider * @requires module:modules/realTimeData */ -import {deepAccess, deepEqual, deepSetValue, isEmpty, logError, mergeDeep} from '../src/utils.js'; +import {deepAccess, deepSetValue, isEmpty, logError, mergeDeep} from '../src/utils.js'; import {submodule} from '../src/hook.js'; import {ajax} from '../src/ajax.js'; import {findIndex} from '../src/polyfill.js'; @@ -16,6 +17,52 @@ import {config} from '../src/config.js'; /** @type {string} */ const MODULE_NAME = 'realTimeData'; const SUBMODULE_NAME = 'SirdataRTDModule'; +const ORTB2_NAME = 'sirdata.com'; + +const partnerIds = { + 'criteo': 27443, + 'openx': 30342, + 'pubmatic': 30345, + 'smaato': 27520, + 'triplelift': 27518, + 'yahoossp': 30339, + 'rubicon': 27452, + 'appnexus': 27446, + 'appnexusAst': 27446, + 'brealtime': 27446, + 'emxdigital': 27446, + 'pagescience': 27446, + 'gourmetads': 33394, + 'matomy': 27446, + 'featureforward': 27446, + 'oftmedia': 27446, + 'districtm': 27446, + 'adasta': 27446, + 'beintoo': 27446, + 'gravity': 27446, + 'msq_classic': 27878, + 'msq_max': 27878, + '366_apx': 27878, + 'mediasquare': 27878, + 'smartadserver': 27440, + 'smart': 27440, + 'proxistore': 27484, + 'ix': 27248, + 'sdRtdForGpt': 27449, + 'smilewanted': 28690, + 'taboola': 33379, + 'ttd': 33382, + 'zeta_global': 33385, + 'teads': 33388, + 'conversant': 33391, + 'improvedigital': 33397, + 'invibes': 33400, + 'sublime': 33403, + 'rtbhouse': 33406, + 'zeta_global_ssp': 33385, +}; + +let CONTEXT_ONLY = true; export function getSegmentsAndCategories(reqBidsConfigObj, onDone, moduleConfig, userConsent) { moduleConfig.params = moduleConfig.params || {}; @@ -46,9 +93,10 @@ export function getSegmentsAndCategories(reqBidsConfigObj, onDone, moduleConfig, if (!sirdataDomain || !gdprApplies || (deepAccess(userConsent, 'gdpr.vendorData.vendor.consents') && userConsent.gdpr.vendorData.vendor.consents[53] && userConsent.gdpr.vendorData.purpose.consents[1] && userConsent.gdpr.vendorData.purpose.consents[4])) { sirdataDomain = 'sddan.com'; sendWithCredentials = true; + CONTEXT_ONLY = false; } - // TODO: is 'page' the right value here? - var actualUrl = moduleConfig.params.actualUrl || getRefererInfo().page; + + var actualUrl = moduleConfig.params.actualUrl || getRefererInfo().stack.pop() || getRefererInfo().page; const url = 'https://kvt.' + sirdataDomain + '/api/v1/public/p/' + moduleConfig.params.partnerId + '/d/' + moduleConfig.params.key + '/s?callback=&gdpr=' + gdprApplies + '&gdpr_consent=' + tcString + (actualUrl ? '&url=' + encodeURIComponent(actualUrl) : ''); @@ -86,39 +134,74 @@ export function getSegmentsAndCategories(reqBidsConfigObj, onDone, moduleConfig, }); } -export function setGlobalOrtb2(ortb2, segments, categories) { +export function setGlobalOrtb2Sda(ortb2Fragments, data, segtaxid, cattaxid) { try { - let addOrtb2 = {}; - let testGlobal = ortb2 || {} - if (!deepAccess(testGlobal, 'user.ext.data.sd_rtd') || !deepEqual(testGlobal.user.ext.data.sd_rtd, segments)) { - deepSetValue(addOrtb2, 'user.ext.data.sd_rtd', segments || {}); + if (!isEmpty(data.segments)) { + applyGlobalOrtb2Sda(ortb2Fragments, 'user', data.segments, segtaxid); } - if (!deepAccess(testGlobal, 'site.ext.data.sd_rtd') || !deepEqual(testGlobal.site.ext.data.sd_rtd, categories)) { - deepSetValue(addOrtb2, 'site.ext.data.sd_rtd', categories || {}); - } - if (!isEmpty(addOrtb2)) { - mergeDeep(ortb2, addOrtb2); + if (!isEmpty(data.categories)) { + applyGlobalOrtb2Sda(ortb2Fragments, 'site', data.categories, cattaxid); } } catch (e) { logError(e) } + return true; +} +export function applyGlobalOrtb2Sda(ortb2Fragments, type, segments, segtaxValue) { + try { + let ortb2Data = [{ + name: ORTB2_NAME, + segment: segments.map((segmentId) => ({ id: segmentId })), + }]; + if (segtaxValue) { + ortb2Data[0].ext = { segtax: segtaxValue }; + } + let ortb2Conf = (type == 'site' ? {site: {content: {data: ortb2Data}}} : {user: {data: ortb2Data}}); + mergeDeep(ortb2Fragments, ortb2Conf); + } catch (e) { + logError(e) + } return true; } -export function setBidderOrtb2(bidderOrtb2, bidder, segments, categories) { +export function setBidderOrtb2Sda(ortb2Fragments, bidder, data, segtaxid, cattaxid) { try { - let addOrtb2 = {}; - let testBidder = bidderOrtb2[bidder]; - if (!deepAccess(testBidder, 'user.ext.data.sd_rtd') || !deepEqual(testBidder.user.ext.data.sd_rtd, segments)) { - deepSetValue(addOrtb2, 'user.ext.data.sd_rtd', segments || {}); + if (!isEmpty(data.segments)) { + applyBidderOrtb2Sda(ortb2Fragments, bidder, 'user', data.segments, segtaxid); } - if (!deepAccess(testBidder, 'site.ext.data.sd_rtd') || !deepEqual(testBidder.site.ext.data.sd_rtd, categories)) { - deepSetValue(addOrtb2, 'site.ext.data.sd_rtd', categories || {}); + if (!isEmpty(data.categories)) { + applyBidderOrtb2Sda(ortb2Fragments, bidder, 'site', data.categories, cattaxid); } - if (!isEmpty(addOrtb2)) { - mergeDeep(bidderOrtb2[bidder], addOrtb2) + } catch (e) { + logError(e) + } + return true; +} + +export function applyBidderOrtb2Sda(ortb2Fragments, bidder, type, segments, segtaxValue) { + try { + let ortb2Data = [{ + name: ORTB2_NAME, + segment: segments.map((segmentId) => ({ id: segmentId })), + }]; + if (segtaxValue) { + ortb2Data[0].ext = { segtax: segtaxValue }; } + let ortb2Conf = (type == 'site' ? {site: {content: {data: ortb2Data}}} : {user: {data: ortb2Data}}); + mergeDeep(ortb2Fragments, {[bidder]: ortb2Conf}); + } catch (e) { + logError(e) + } + return true; +} + +export function setBidderOrtb2(bidderOrtb2Fragments, bidder, path, segments) { + try { + if (isEmpty(segments)) { return; } + let ortb2Conf = {}; + deepSetValue(ortb2Conf, path, segments || {}); + mergeDeep(bidderOrtb2Fragments, {[bidder]: ortb2Conf}); } catch (e) { logError(e) } @@ -137,16 +220,16 @@ export function loadCustomFunction(todo, adUnit, list, data, bid) { return true; } -export function getSegAndCatsArray(data, minScore) { - var sirdataData = {'segments': [], 'categories': []}; +export function getSegAndCatsArray(data, minScore, pid) { + let sirdataData = {'segments': [], 'categories': []}; minScore = minScore && typeof minScore == 'number' ? minScore : 30; try { if (data && data.contextual_categories) { for (let catId in data.contextual_categories) { - if (data.contextual_categories.hasOwnProperty(catId)) { + if (data.contextual_categories.hasOwnProperty(catId) && data.contextual_categories[catId]) { let value = data.contextual_categories[catId]; if (value >= minScore && sirdataData.categories.indexOf(catId) === -1) { - sirdataData.categories.push(catId.toString()); + sirdataData.categories.push((pid ? pid.toString() + 'cc' : '') + catId.toString()); } } } @@ -157,8 +240,11 @@ export function getSegAndCatsArray(data, minScore) { try { if (data && data.segments) { for (let segId in data.segments) { - if (data.segments.hasOwnProperty(segId)) { - sirdataData.segments.push(data.segments[segId].toString()); + if (data.segments.hasOwnProperty(segId) && data.segments[segId]) { + sirdataData.segments.push((pid ? pid.toString() + 'us' : '') + data.segments[segId].toString()); + if (pid && CONTEXT_ONLY) { + sirdataData.categories.push(pid.toString() + 'uc' + data.segments[segId].toString()); + } } } } @@ -168,34 +254,77 @@ export function getSegAndCatsArray(data, minScore) { return sirdataData; } +export function applySdaGetSpecificData(data, sirdataList, biddersParamsExist, minScore, reqBids, bid, moduleConfig, indexFound, bidderIndex, adUnit) { + // only share SDA data if whitelisted + if (!biddersParamsExist || indexFound) { + // SDA Publisher + let sirdataDataForSDA = getSegAndCatsArray(data, minScore, moduleConfig.params.partnerId); + setBidderOrtb2Sda(reqBids.ortb2Fragments?.bidder, bid.bidder, sirdataDataForSDA, data.segtaxid, data.cattaxid); + } + + // always share SDA for curation + let curationId = (indexFound && moduleConfig.params.bidders[bidderIndex].hasOwnProperty('curationId') ? moduleConfig.params.bidders[bidderIndex].curationId : (partnerIds[bid.bidder] ? partnerIds[bid.bidder] : null)); + if (curationId) { + // seller defined audience & bidder specific data + if (data.shared_taxonomy && data.shared_taxonomy[curationId]) { + // Get Bidder Specific Data + let curationData = getSegAndCatsArray(data.shared_taxonomy[curationId], minScore, null); + sirdataList = sirdataList.concat(curationData.segments).concat(curationData.categories); + + // SDA Partners + let curationDataForSDA = getSegAndCatsArray(data.shared_taxonomy[curationId], minScore, curationId); + setBidderOrtb2Sda(reqBids.ortb2Fragments?.bidder, bid.bidder, curationDataForSDA, data.shared_taxonomy[curationId].segtaxid, data.shared_taxonomy[curationId].cattaxid); + } + } + + // Apply custom function or return Bidder Specific Data if publisher is ok + if (sirdataList && sirdataList.length > 0 && (!biddersParamsExist || indexFound)) { + if (indexFound && moduleConfig.params.bidders[bidderIndex].hasOwnProperty('customFunction')) { + return loadCustomFunction(moduleConfig.params.bidders[bidderIndex].customFunction, adUnit, sirdataList, data, bid); + } else { + return sirdataList; + } + } +} + +export function applySdaAndDefaultSpecificData(data, sirdataList, biddersParamsExist, minScore, reqBids, bid, moduleConfig, indexFound, bidderIndex, adUnit) { + let specificData = applySdaGetSpecificData(data, sirdataList, biddersParamsExist, minScore, reqBids, bid, moduleConfig, indexFound, bidderIndex, adUnit); + if (specificData && specificData.length > 0) { + setBidderOrtb2(reqBids.ortb2Fragments?.bidder, bid.bidder, 'user.ext.data', {sd_rtd: specificData}); + } +} + export function addSegmentData(reqBids, data, moduleConfig, onDone) { const adUnits = reqBids.adUnits; moduleConfig = moduleConfig || {}; moduleConfig.params = moduleConfig.params || {}; const globalMinScore = moduleConfig.params.hasOwnProperty('contextualMinRelevancyScore') ? moduleConfig.params.contextualMinRelevancyScore : 30; - var sirdataData = getSegAndCatsArray(data, globalMinScore); + var sirdataData = getSegAndCatsArray(data, globalMinScore, null); const sirdataList = sirdataData.segments.concat(sirdataData.categories); - var sirdataMergedList = []; - var curationData = {'segments': [], 'categories': []}; - var curationId = '1'; const biddersParamsExist = (!!(moduleConfig.params && moduleConfig.params.bidders)); - // Global ortb2 - if (!biddersParamsExist) { - setGlobalOrtb2(reqBids.ortb2Fragments?.global, sirdataData.segments, sirdataData.categories); + // Global ortb2 SDA + if (data.global_taxonomy && !isEmpty(data.global_taxonomy)) { + let globalData = {'segments': [], 'categories': []}; + for (let i in data.global_taxonomy) { + if (!isEmpty(data.global_taxonomy[i])) { + globalData = getSegAndCatsArray(data.global_taxonomy[i], globalMinScore, null); + setGlobalOrtb2Sda(reqBids.ortb2Fragments?.global, globalData, data.global_taxonomy[i].segtaxid, data.global_taxonomy[i].cattaxid); + } + } } // Google targeting if (typeof window.googletag !== 'undefined' && (moduleConfig.params.setGptKeyValues || !moduleConfig.params.hasOwnProperty('setGptKeyValues'))) { try { - // For curation Google is pid 27449 - curationId = (moduleConfig.params.gptCurationId ? moduleConfig.params.gptCurationId : '27449'); - if (data.shared_taxonomy && data.shared_taxonomy[curationId]) { - curationData = getSegAndCatsArray(data.shared_taxonomy[curationId], globalMinScore); + let gptCurationId = (moduleConfig.params.gptCurationId ? moduleConfig.params.gptCurationId : (partnerIds['sdRtdForGpt'] ? partnerIds['sdRtdForGpt'] : null)); + let sirdataMergedList = sirdataList; + if (gptCurationId && data.shared_taxonomy && data.shared_taxonomy[gptCurationId]) { + let gamCurationData = getSegAndCatsArray(data.shared_taxonomy[gptCurationId], globalMinScore, null); + sirdataMergedList = sirdataMergedList.concat(gamCurationData.segments).concat(gamCurationData.categories); } - sirdataMergedList = sirdataList.concat(curationData.segments).concat(curationData.categories); window.googletag.pubads().getSlots().forEach(function (n) { if (typeof n.setTargeting !== 'undefined' && sirdataMergedList && sirdataMergedList.length > 0) { n.setTargeting('sd_rtd', sirdataMergedList); @@ -221,259 +350,108 @@ export function addSegmentData(reqBids, data, moduleConfig, onDone) { }) : false); indexFound = (!!(typeof bidderIndex == 'number' && bidderIndex >= 0)); try { - curationData = {'segments': [], 'categories': []}; - sirdataMergedList = []; - let minScore = (indexFound && moduleConfig.params.bidders[bidderIndex].hasOwnProperty('contextualMinRelevancyScore') ? moduleConfig.params.bidders[bidderIndex].contextualMinRelevancyScore : globalMinScore); + let specificData = null; + + switch (bid.bidder) { + case 'appnexus': + case 'appnexusAst': + case 'brealtime': + case 'emxdigital': + case 'pagescience': + case 'gourmetads': + case 'matomy': + case 'featureforward': + case 'oftmedia': + case 'districtm': + case 'adasta': + case 'beintoo': + case 'gravity': + case 'msq_classic': + case 'msq_max': + case '366_apx': + specificData = applySdaGetSpecificData(data, sirdataList, biddersParamsExist, minScore, reqBids, bid, moduleConfig, indexFound, bidderIndex, adUnit); + if (specificData && specificData.length > 0) { + deepSetValue(bid, 'params.keywords.sd_rtd', specificData); + } + break; - if (!biddersParamsExist || (indexFound && (!moduleConfig.params.bidders[bidderIndex].hasOwnProperty('adUnitCodes') || moduleConfig.params.bidders[bidderIndex].adUnitCodes.indexOf(adUnit.code) !== -1))) { - switch (bid.bidder) { - case 'appnexus': - case 'appnexusAst': - case 'brealtime': - case 'emxdigital': - case 'pagescience': - case 'gourmetads': - case 'matomy': - case 'featureforward': - case 'oftmedia': - case 'districtm': - case 'adasta': - case 'beintoo': - case 'gravity': - case 'msq_classic': - case 'msq_max': - case '366_apx': - // For curation Xandr is pid 27446 - curationId = (indexFound && moduleConfig.params.bidders[bidderIndex].hasOwnProperty('curationId') ? moduleConfig.params.bidders[bidderIndex].curationId : '27446'); - if (data.shared_taxonomy && data.shared_taxonomy[curationId]) { - curationData = getSegAndCatsArray(data.shared_taxonomy[curationId], minScore); - } - sirdataMergedList = sirdataList.concat(curationData.segments).concat(curationData.categories); - if (sirdataMergedList && sirdataMergedList.length > 0) { - if (indexFound && moduleConfig.params.bidders[bidderIndex].hasOwnProperty('customFunction')) { - loadCustomFunction(moduleConfig.params.bidders[bidderIndex].customFunction, adUnit, sirdataMergedList, data, bid); - } else { - deepSetValue(bid, 'params.keywords.sd_rtd', sirdataMergedList); - } - } - break; - - case 'smartadserver': - case 'smart': + case 'smartadserver': + case 'smart': + specificData = applySdaGetSpecificData(data, sirdataList, biddersParamsExist, minScore, reqBids, bid, moduleConfig, indexFound, bidderIndex, adUnit); + if (specificData && specificData.length > 0) { var target = []; if (bid.hasOwnProperty('params') && bid.params.hasOwnProperty('target')) { target.push(bid.params.target); } - // For curation Smart is pid 27440 - curationId = (indexFound && moduleConfig.params.bidders[bidderIndex].hasOwnProperty('curationId') ? moduleConfig.params.bidders[bidderIndex].curationId : '27440'); - if (data.shared_taxonomy && data.shared_taxonomy[curationId]) { - curationData = getSegAndCatsArray(data.shared_taxonomy[curationId], minScore); - } - sirdataMergedList = sirdataList.concat(curationData.segments).concat(curationData.categories); - if (sirdataMergedList && sirdataMergedList.length > 0) { - if (indexFound && moduleConfig.params.bidders[bidderIndex].hasOwnProperty('customFunction')) { - loadCustomFunction(moduleConfig.params.bidders[bidderIndex].customFunction, adUnit, sirdataMergedList, data, bid); - } else { - sirdataMergedList.forEach(function (entry) { - if (target.indexOf('sd_rtd=' + entry) === -1) { - target.push('sd_rtd=' + entry); - } - }); - deepSetValue(bid, 'params.target', target.join(';')); - } - } - break; - - case 'rubicon': - // For curation Magnite is pid 27518 - curationId = (indexFound && moduleConfig.params.bidders[bidderIndex].hasOwnProperty('curationId') ? moduleConfig.params.bidders[bidderIndex].curationId : '27452'); - if (data.shared_taxonomy && data.shared_taxonomy[curationId]) { - curationData = getSegAndCatsArray(data.shared_taxonomy[curationId], minScore); - } - sirdataMergedList = sirdataList.concat(curationData.segments).concat(curationData.categories); - if (sirdataMergedList && sirdataMergedList.length > 0) { - if (indexFound && moduleConfig.params.bidders[bidderIndex].hasOwnProperty('customFunction')) { - loadCustomFunction(moduleConfig.params.bidders[bidderIndex].customFunction, adUnit, sirdataMergedList, data, bid); - } else { - setBidderOrtb2(reqBids.ortb2Fragments?.bidder, bid.bidder, data.segments.concat(curationData.segments), sirdataMergedList); - } - } - break; - - case 'ix': - var ixConfig = config.getConfig('ix.firstPartyData.sd_rtd'); - if (!ixConfig) { - // For curation index is pid 27248 - curationId = (indexFound && moduleConfig.params.bidders[bidderIndex].hasOwnProperty('curationId') ? moduleConfig.params.bidders[bidderIndex].curationId : '27248'); - if (data.shared_taxonomy && data.shared_taxonomy[curationId]) { - curationData = getSegAndCatsArray(data.shared_taxonomy[curationId], minScore); - } - sirdataMergedList = sirdataList.concat(curationData.segments).concat(curationData.categories); - if (sirdataMergedList && sirdataMergedList.length > 0) { - if (indexFound && moduleConfig.params.bidders[bidderIndex].hasOwnProperty('customFunction')) { - loadCustomFunction(moduleConfig.params.bidders[bidderIndex].customFunction, adUnit, sirdataMergedList, data, bid); - } else { - var cappIxCategories = []; - var ixLength = 0; - var ixLimit = (indexFound && moduleConfig.params.bidders[bidderIndex].hasOwnProperty('sizeLimit') ? moduleConfig.params.bidders[bidderIndex].sizeLimit : 1000); - // Push ids For publisher use and for curation if exists but limit size because the bidder uses GET parameters - sirdataMergedList.forEach(function (entry) { - if (ixLength < ixLimit) { - cappIxCategories.push(entry); - ixLength += entry.toString().length; - } - }); - config.setConfig({ix: {firstPartyData: {sd_rtd: cappIxCategories}}}); - } - } - } - break; - - case 'proxistore': - // For curation Proxistore is pid 27484 - curationId = (indexFound && moduleConfig.params.bidders[bidderIndex].hasOwnProperty('curationId') ? moduleConfig.params.bidders[bidderIndex].curationId : '27484'); - if (data.shared_taxonomy && data.shared_taxonomy[curationId]) { - curationData = getSegAndCatsArray(data.shared_taxonomy[curationId], minScore); - } else { - data.shared_taxonomy[curationId] = {contextual_categories: {}}; - } - sirdataMergedList = sirdataList.concat(curationData.segments).concat(curationData.categories); - if (sirdataMergedList && sirdataMergedList.length > 0) { - if (indexFound && moduleConfig.params.bidders[bidderIndex].hasOwnProperty('customFunction')) { - loadCustomFunction(moduleConfig.params.bidders[bidderIndex].customFunction, adUnit, sirdataMergedList, data, bid); - } else { - deepSetValue(bid, 'ortb2.user.ext.data', { - segments: sirdataData.segments.concat(curationData.segments), - contextual_categories: {...data.contextual_categories, ...data.shared_taxonomy[curationId].contextual_categories} - }); - } - } - break; - - case 'criteo': - // For curation Smart is pid 27443 - curationId = (indexFound && moduleConfig.params.bidders[bidderIndex].hasOwnProperty('curationId') ? moduleConfig.params.bidders[bidderIndex].curationId : '27443'); - if (data.shared_taxonomy && data.shared_taxonomy[curationId]) { - curationData = getSegAndCatsArray(data.shared_taxonomy[curationId], minScore); - } - sirdataMergedList = sirdataList.concat(curationData.segments).concat(curationData.categories); - if (sirdataMergedList && sirdataMergedList.length > 0) { - if (indexFound && moduleConfig.params.bidders[bidderIndex].hasOwnProperty('customFunction')) { - loadCustomFunction(moduleConfig.params.bidders[bidderIndex].customFunction, adUnit, sirdataMergedList, data, bid); - } else { - setBidderOrtb2(reqBids.ortb2Fragments?.bidder, bid.bidder, data.segments.concat(curationData.segments), sirdataMergedList); - } - } - break; - - case 'triplelift': - // For curation Triplelift is pid 27518 - curationId = (indexFound && moduleConfig.params.bidders[bidderIndex].hasOwnProperty('curationId') ? moduleConfig.params.bidders[bidderIndex].curationId : '27518'); - if (data.shared_taxonomy && data.shared_taxonomy[curationId]) { - curationData = getSegAndCatsArray(data.shared_taxonomy[curationId], minScore); - } - sirdataMergedList = sirdataList.concat(curationData.segments).concat(curationData.categories); - if (sirdataMergedList && sirdataMergedList.length > 0) { - if (indexFound && moduleConfig.params.bidders[bidderIndex].hasOwnProperty('customFunction')) { - loadCustomFunction(moduleConfig.params.bidders[bidderIndex].customFunction, adUnit, sirdataMergedList, data, bid); - } else { - setBidderOrtb2(reqBids.ortb2Fragments?.bidder, bid.bidder, data.segments.concat(curationData.segments), sirdataMergedList); - } - } - break; - - case 'avct': - case 'avocet': - // For curation Avocet is pid 27522 - curationId = (indexFound && moduleConfig.params.bidders[bidderIndex].hasOwnProperty('curationId') ? moduleConfig.params.bidders[bidderIndex].curationId : '27522'); - if (data.shared_taxonomy && data.shared_taxonomy[curationId]) { - curationData = getSegAndCatsArray(data.shared_taxonomy[curationId], minScore); - } - sirdataMergedList = sirdataList.concat(curationData.segments).concat(curationData.categories); - if (sirdataMergedList && sirdataMergedList.length > 0) { - if (indexFound && moduleConfig.params.bidders[bidderIndex].hasOwnProperty('customFunction')) { - loadCustomFunction(moduleConfig.params.bidders[bidderIndex].customFunction, adUnit, sirdataMergedList, data, bid); - } else { - setBidderOrtb2(reqBids.ortb2Fragments?.bidder, bid.bidder, data.segments.concat(curationData.segments), sirdataMergedList); - } - } - break; - - case 'smaato': - // For curation Smaato is pid 27520 - curationId = (indexFound && moduleConfig.params.bidders[bidderIndex].hasOwnProperty('curationId') ? moduleConfig.params.bidders[bidderIndex].curationId : '27520'); - if (data.shared_taxonomy && data.shared_taxonomy[curationId]) { - curationData = getSegAndCatsArray(data.shared_taxonomy[curationId], minScore); - } - sirdataMergedList = sirdataList.concat(curationData.segments).concat(curationData.categories); - if (sirdataMergedList && sirdataMergedList.length > 0) { - if (indexFound && moduleConfig.params.bidders[bidderIndex].hasOwnProperty('customFunction')) { - loadCustomFunction(moduleConfig.params.bidders[bidderIndex].customFunction, adUnit, sirdataMergedList, data, bid); - } else { - setBidderOrtb2(reqBids.ortb2Fragments?.bidder, bid.bidder, data.segments.concat(curationData.segments), sirdataMergedList); - } - } - break; - - case 'yahoossp': - // For curation Yahoo is pid 30339 - curationId = (indexFound && moduleConfig.params.bidders[bidderIndex].hasOwnProperty('curationId') ? moduleConfig.params.bidders[bidderIndex].curationId : '30339'); - if (data.shared_taxonomy && data.shared_taxonomy[curationId]) { - curationData = getSegAndCatsArray(data.shared_taxonomy[curationId], minScore); - } - sirdataMergedList = sirdataList.concat(curationData.segments).concat(curationData.categories); - if (sirdataMergedList && sirdataMergedList.length > 0) { - if (indexFound && moduleConfig.params.bidders[bidderIndex].hasOwnProperty('customFunction')) { - loadCustomFunction(moduleConfig.params.bidders[bidderIndex].customFunction, adUnit, sirdataMergedList, data, bid); - } else { - setBidderOrtb2(reqBids.ortb2Fragments?.bidder, bid.bidder, data.segments.concat(curationData.segments), sirdataMergedList); - } - } - break; - - case 'openx': - // For curation OpenX is pid 30342 - curationId = (indexFound && moduleConfig.params.bidders[bidderIndex].hasOwnProperty('curationId') ? moduleConfig.params.bidders[bidderIndex].curationId : '30342'); - if (data.shared_taxonomy && data.shared_taxonomy[curationId]) { - curationData = getSegAndCatsArray(data.shared_taxonomy[curationId], minScore); - } - sirdataMergedList = sirdataList.concat(curationData.segments).concat(curationData.categories); - if (sirdataMergedList && sirdataMergedList.length > 0) { - if (indexFound && moduleConfig.params.bidders[bidderIndex].hasOwnProperty('customFunction')) { - loadCustomFunction(moduleConfig.params.bidders[bidderIndex].customFunction, adUnit, sirdataMergedList, data, bid); - } else { - setBidderOrtb2(reqBids.ortb2Fragments?.bidder, bid.bidder, data.segments.concat(curationData.segments), sirdataMergedList); + specificData.forEach(function (entry) { + if (target.indexOf('sd_rtd=' + entry) === -1) { + target.push('sd_rtd=' + entry); } - } - break; - - case 'pubmatic': - // For curation Pubmatic is pid 30345 - curationId = (indexFound && moduleConfig.params.bidders[bidderIndex].hasOwnProperty('curationId') ? moduleConfig.params.bidders[bidderIndex].curationId : '30345'); - if (data.shared_taxonomy && data.shared_taxonomy[curationId]) { - curationData = getSegAndCatsArray(data.shared_taxonomy[curationId], minScore); - } - sirdataMergedList = sirdataList.concat(curationData.segments).concat(curationData.categories); - if (sirdataMergedList && sirdataMergedList.length > 0) { - if (indexFound && moduleConfig.params.bidders[bidderIndex].hasOwnProperty('customFunction')) { - loadCustomFunction(moduleConfig.params.bidders[bidderIndex].customFunction, adUnit, sirdataMergedList, data, bid); - } else { - setBidderOrtb2(reqBids.ortb2Fragments?.bidder, bid.bidder, data.segments.concat(curationData.segments), sirdataMergedList); - } - } - break; - - default: - if (!biddersParamsExist || indexFound) { - if (!deepAccess(bid, 'ortb2.site.ext.data.sd_rtd')) { - deepSetValue(bid, 'ortb2.site.ext.data.sd_rtd', sirdataData.categories); - } - if (!deepAccess(bid, 'ortb2.user.ext.data.sd_rtd')) { - deepSetValue(bid, 'ortb2.user.ext.data.sd_rtd', sirdataData.segments); + }); + deepSetValue(bid, 'params.target', target.join(';')); + } + break; + + case 'ix': + specificData = applySdaGetSpecificData(data, sirdataList, biddersParamsExist, minScore, reqBids, bid, moduleConfig, indexFound, bidderIndex, adUnit); + let ixConfig = config.getConfig('ix.firstPartyData.sd_rtd'); + if (!ixConfig && specificData && specificData.length > 0) { + let cappIxCategories = []; + let ixLength = 0; + let ixLimit = (indexFound && moduleConfig.params.bidders[bidderIndex].hasOwnProperty('sizeLimit') ? moduleConfig.params.bidders[bidderIndex].sizeLimit : 1000); + // Push ids For publisher use and for curation if exists but limit size because the bidder uses GET parameters + specificData.forEach(function (entry) { + if (ixLength < ixLimit) { + cappIxCategories.push(entry); + ixLength += entry.toString().length; } + }); + config.setConfig({ix: {firstPartyData: {sd_rtd: cappIxCategories}}}); + } + break; + + case 'proxistore': + specificData = applySdaGetSpecificData(data, sirdataList, biddersParamsExist, minScore, reqBids, bid, moduleConfig, indexFound, bidderIndex, adUnit); + if (specificData && specificData.length > 0) { + let psCurationId = (indexFound && moduleConfig.params.bidders[bidderIndex].hasOwnProperty('curationId') ? moduleConfig.params.bidders[bidderIndex].curationId : (partnerIds[bid.bidder] ? partnerIds[bid.bidder] : null)); + if (!data.shared_taxonomy || !data.shared_taxonomy[psCurationId]) { + data.shared_taxonomy[psCurationId] = {segments: [], contextual_categories: {}, segtaxid: null, cattaxid: null}; } - } + let psCurationData = getSegAndCatsArray(data.shared_taxonomy[psCurationId], minScore, null); + setBidderOrtb2(reqBids.ortb2Fragments?.bidder, bid.bidder, 'user.ext.data', { + segments: sirdataData.segments.concat(psCurationData.segments), + contextual_categories: {...data.contextual_categories, ...data.shared_taxonomy[psCurationId].contextual_categories} + }); + } + break; + + case 'rubicon': + case 'criteo': + case 'triplelift': + case 'smaato': + case 'yahoossp': + case 'openx': + case 'pubmatic': + case 'smilewanted': + case 'taboola': + case 'ttd': + case 'zeta_global': + case 'zeta_global_ssp': + case 'teads': + case 'conversant': + case 'improvedigital': + case 'invibes': + case 'sublime': + case 'rtbhouse': + case 'mediasquare': + applySdaAndDefaultSpecificData(data, sirdataList, biddersParamsExist, minScore, reqBids, bid, moduleConfig, indexFound, bidderIndex, adUnit); + break; + + default: + if (!biddersParamsExist || (indexFound && (!moduleConfig.params.bidders[bidderIndex].hasOwnProperty('adUnitCodes') || moduleConfig.params.bidders[bidderIndex].adUnitCodes.indexOf(adUnit.code) !== -1))) { + applySdaAndDefaultSpecificData(data, sirdataList, biddersParamsExist, minScore, reqBids, bid, moduleConfig, indexFound, bidderIndex, adUnit); + } } } catch (e) { logError(e); diff --git a/modules/sizeMappingV2.js b/modules/sizeMappingV2.js index 05475b3e143..d212d98f50b 100644 --- a/modules/sizeMappingV2.js +++ b/modules/sizeMappingV2.js @@ -182,7 +182,7 @@ export function checkAdUnitSetupHook(adUnits) { } } - if (mediaTypes.video) { + if (FEATURES.VIDEO && mediaTypes.video) { if (mediaTypes.video.playerSize) { // Ad unit is using 'mediaTypes.video.playerSize' instead of the new property 'sizeConfig'. Apply the old checks! validatedVideo = validatedBanner ? adUnitSetupChecks.validateVideoMediaType(validatedBanner) : adUnitSetupChecks.validateVideoMediaType(adUnit); diff --git a/modules/smaatoBidAdapter.js b/modules/smaatoBidAdapter.js index 18ce56c7a80..c1eb1bb8489 100644 --- a/modules/smaatoBidAdapter.js +++ b/modules/smaatoBidAdapter.js @@ -8,7 +8,7 @@ import CONSTANTS from '../src/constants.json'; const { NATIVE_IMAGE_TYPES } = CONSTANTS; const BIDDER_CODE = 'smaato'; const SMAATO_ENDPOINT = 'https://prebid.ad.smaato.net/oapi/prebid'; -const SMAATO_CLIENT = 'prebid_js_$prebid.version$_1.7' +const SMAATO_CLIENT = 'prebid_js_$prebid.version$_1.8' const CURRENCY = 'USD'; const buildOpenRtbBidRequest = (bidRequest, bidderRequest) => { @@ -63,11 +63,28 @@ const buildOpenRtbBidRequest = (bidRequest, bidderRequest) => { deepSetValue(requestTemplate, 'regs.ext.us_privacy', bidderRequest.uspConsent); } + if (ortb2.regs?.gpp !== undefined) { + deepSetValue(requestTemplate, 'regs.ext.gpp', ortb2.regs.gpp); + deepSetValue(requestTemplate, 'regs.ext.gpp_sid', ortb2.regs.gpp_sid); + } + + if (ortb2.device?.ifa !== undefined) { + deepSetValue(requestTemplate, 'device.ifa', ortb2.device.ifa); + } + + if (ortb2.device?.geo !== undefined) { + deepSetValue(requestTemplate, 'device.geo', ortb2.device.geo); + } + if (deepAccess(bidRequest, 'params.app')) { - const geo = deepAccess(bidRequest, 'params.app.geo'); - deepSetValue(requestTemplate, 'device.geo', geo); - const ifa = deepAccess(bidRequest, 'params.app.ifa'); - deepSetValue(requestTemplate, 'device.ifa', ifa); + if (!deepAccess(requestTemplate, 'device.geo')) { + const geo = deepAccess(bidRequest, 'params.app.geo'); + deepSetValue(requestTemplate, 'device.geo', geo); + } + if (!deepAccess(requestTemplate, 'device.ifa')) { + const ifa = deepAccess(bidRequest, 'params.app.ifa'); + deepSetValue(requestTemplate, 'device.ifa', ifa); + } } const eids = deepAccess(bidRequest, 'userIdAsEids'); diff --git a/modules/smartadserverBidAdapter.js b/modules/smartadserverBidAdapter.js index 6ff0e592542..fd2d6e16463 100644 --- a/modules/smartadserverBidAdapter.js +++ b/modules/smartadserverBidAdapter.js @@ -1,7 +1,6 @@ import { deepAccess, deepClone, logError, isFn, isPlainObject } from '../src/utils.js'; import { BANNER, VIDEO } from '../src/mediaTypes.js'; import { config } from '../src/config.js'; -import { createEidsArray } from './userId/eids.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; const BIDDER_CODE = 'smartadserver'; @@ -13,6 +12,7 @@ export const spec = { gvlid: GVL_ID, aliases: ['smart'], // short code supportedMediaTypes: [BANNER, VIDEO], + /** * Determines whether or not the given bid request is valid. * @@ -131,7 +131,6 @@ export const spec = { */ buildRequests: function (validBidRequests, bidderRequest) { // use bidderRequest.bids[] to get bidder-dependent request info - const adServerCurrency = config.getConfig('currency.adServerCurrency'); const sellerDefinedAudience = deepAccess(bidderRequest, 'ortb2.user.data', config.getAnyConfig('ortb2.user.data')); const sellerDefinedContext = deepAccess(bidderRequest, 'ortb2.site.content.data', config.getAnyConfig('ortb2.site.content.data')); @@ -144,7 +143,6 @@ export const spec = { pageid: bid.params.pageId, formatid: bid.params.formatId, currencyCode: adServerCurrency, - bidfloor: bid.params.bidfloor || spec.getBidFloor(bid, adServerCurrency), targeting: bid.params.target && bid.params.target !== '' ? bid.params.target : undefined, buid: bid.params.buId && bid.params.buId !== '' ? bid.params.buId : undefined, appname: bid.params.appName && bid.params.appName !== '' ? bid.params.appName : undefined, @@ -161,38 +159,53 @@ export const spec = { sdc: sellerDefinedContext }; - if (bidderRequest && bidderRequest.gdprConsent) { - payload.addtl_consent = bidderRequest.gdprConsent.addtlConsent; - payload.gdpr_consent = bidderRequest.gdprConsent.consentString; - payload.gdpr = bidderRequest.gdprConsent.gdprApplies; // we're handling the undefined case server side + if (bidderRequest) { + if (bidderRequest.gdprConsent) { + payload.addtl_consent = bidderRequest.gdprConsent.addtlConsent; + payload.gdpr_consent = bidderRequest.gdprConsent.consentString; + payload.gdpr = bidderRequest.gdprConsent.gdprApplies; // we're handling the undefined case server side + } + + if (bidderRequest.gppConsent) { + payload.gpp = bidderRequest.gppConsent.gppString; + payload.gpp_sid = bidderRequest.gppConsent.applicableSections; + } + + if (bidderRequest.uspConsent) { + payload.us_privacy = bidderRequest.uspConsent; + } } - if (bid && bid.userId) { - payload.eids = createEidsArray(bid.userId); + if (bid && bid.userIdAsEids) { + payload.eids = bid.userIdAsEids; } if (bidderRequest && bidderRequest.uspConsent) { payload.us_privacy = bidderRequest.uspConsent; } - const videoMediaType = deepAccess(bid, 'mediaTypes.video'); const bannerMediaType = deepAccess(bid, 'mediaTypes.banner'); - const isAdUnitContainingVideo = videoMediaType && (videoMediaType.context === 'instream' || videoMediaType.context === 'outstream'); - if (!isAdUnitContainingVideo && bannerMediaType) { - payload.sizes = spec.adaptBannerSizes(bannerMediaType.sizes); - bidRequests.push(spec.createServerRequest(payload, bid.params.domain)); - } else if (isAdUnitContainingVideo && !bannerMediaType) { - spec.fillPayloadForVideoBidRequest(payload, videoMediaType, bid.params.video); - bidRequests.push(spec.createServerRequest(payload, bid.params.domain)); - } else if (isAdUnitContainingVideo && bannerMediaType) { - // If there are video and banner media types in the ad unit, we clone the payload - // to create a specific one for video. - let videoPayload = deepClone(payload); + const videoMediaType = deepAccess(bid, 'mediaTypes.video'); + const isSupportedVideoContext = videoMediaType && (videoMediaType.context === 'instream' || videoMediaType.context === 'outstream'); + + if (bannerMediaType || isSupportedVideoContext) { + let type; + if (bannerMediaType) { + type = BANNER; + payload.sizes = spec.adaptBannerSizes(bannerMediaType.sizes); - spec.fillPayloadForVideoBidRequest(videoPayload, videoMediaType, bid.params.video); - bidRequests.push(spec.createServerRequest(videoPayload, bid.params.domain)); + if (isSupportedVideoContext) { + let videoPayload = deepClone(payload); + spec.fillPayloadForVideoBidRequest(videoPayload, videoMediaType, bid.params.video); + videoPayload.bidfloor = bid.params.bidfloor || spec.getBidFloor(bid, adServerCurrency, VIDEO); + bidRequests.push(spec.createServerRequest(videoPayload, bid.params.domain)); + } + } else { + type = VIDEO; + spec.fillPayloadForVideoBidRequest(payload, videoMediaType, bid.params.video); + } - payload.sizes = spec.adaptBannerSizes(bannerMediaType.sizes); + payload.bidfloor = bid.params.bidfloor || spec.getBidFloor(bid, adServerCurrency, type); bidRequests.push(spec.createServerRequest(payload, bid.params.domain)); } else { bidRequests.push({}); @@ -253,24 +266,21 @@ export const spec = { * * @param {object} bid Bid request object * @param {string} currency Ad server currency + * @param {string} mediaType Bid media type * @return {number} Floor price */ - getBidFloor: function (bid, currency) { + getBidFloor: function (bid, currency, mediaType) { if (!isFn(bid.getFloor)) { return DEFAULT_FLOOR; } const floor = bid.getFloor({ currency: currency || 'USD', - mediaType: '*', + mediaType, size: '*' }); - if (isPlainObject(floor) && !isNaN(floor.floor)) { - return floor.floor; - } - - return DEFAULT_FLOOR; + return isPlainObject(floor) && !isNaN(floor.floor) ? floor.floor : DEFAULT_FLOOR; }, /** diff --git a/modules/smarthubBidAdapter.js b/modules/smarthubBidAdapter.js index 6aab6a8b57e..2889bd5358b 100644 --- a/modules/smarthubBidAdapter.js +++ b/modules/smarthubBidAdapter.js @@ -125,7 +125,7 @@ function buildRequestParams(bidderRequest = {}, placements = []) { coppa: config.getConfig('coppa') === true ? 1 : 0, ccpa: bidderRequest.uspConsent || undefined, gdpr: bidderRequest.gdprConsent || undefined, - tmax: config.getConfig('bidderTimeout') + tmax: bidderRequest.timeout }; } diff --git a/modules/smartxBidAdapter.js b/modules/smartxBidAdapter.js index 452aaafb09b..d91b62729bc 100644 --- a/modules/smartxBidAdapter.js +++ b/modules/smartxBidAdapter.js @@ -10,8 +10,10 @@ import { } from '../src/mediaTypes.js'; const BIDDER_CODE = 'smartx'; const URL = 'https://bid.sxp.smartclip.net/bid/1000'; +const GVLID = 115; export const spec = { code: BIDDER_CODE, + gvlid: GVLID, supportedMediaTypes: [VIDEO], /** * Determines whether or not the given bid request is valid. @@ -76,6 +78,7 @@ export const spec = { const bidfloor = getBidFloor(bid) || 0; const bidfloorcur = getBidIdParameter('bidfloorcur', bid.params) || 'EUR'; const siteId = getBidIdParameter('siteId', bid.params); + const sitekey = getBidIdParameter('sitekey', bid.params); const domain = getBidIdParameter('domain', bid.params); const cat = getBidIdParameter('cat', bid.params) || ['']; let pubcid = null; @@ -191,6 +194,11 @@ export const spec = { } } + // Add sitekey if available + if (sitekey) { + requestPayload.site.content.ext.sitekey = sitekey; + } + // Add common id if available if (pubcid) { userExt.fpc = pubcid; diff --git a/modules/smartyadsBidAdapter.js b/modules/smartyadsBidAdapter.js index e5800e7cad0..89749aed433 100644 --- a/modules/smartyadsBidAdapter.js +++ b/modules/smartyadsBidAdapter.js @@ -17,7 +17,7 @@ function isBidResponseValid(bid) { case BANNER: return Boolean(bid.width && bid.height && bid.ad); case VIDEO: - return Boolean(bid.vastUrl); + return Boolean(bid.vastUrl) || Boolean(bid.vastXml); case NATIVE: return Boolean(bid.native && bid.native.title && bid.native.image && bid.native.impressionTrackers); default: diff --git a/modules/smartyadsBidAdapter.md b/modules/smartyadsBidAdapter.md index f078d905e62..e0d6023a794 100644 --- a/modules/smartyadsBidAdapter.md +++ b/modules/smartyadsBidAdapter.md @@ -10,6 +10,15 @@ Maintainer: supply@smartyads.com Module that connects to SmartyAds' demand sources +# Parameters + +| Name | Scope | Description | Example | +| :------------ | :------- | :------------------------ | :------------------- | +| `sourceid` | required (for prebid.js) | placement ID | "0" | +| `host` | required (for prebid-server) | const value, set to "prebid" | "prebid" | +| `accountid` | required (for prebid-server) | partner ID | "1901" | +| `traffic` | optional (for prebid.js) | Configures the mediaType that should be used. Values can be banner, native or video | "banner" | + # Test Parameters ``` var adUnits = [ diff --git a/modules/smartytechBidAdapter.js b/modules/smartytechBidAdapter.js index 231ca315de8..34cf9285909 100644 --- a/modules/smartytechBidAdapter.js +++ b/modules/smartytechBidAdapter.js @@ -1,29 +1,86 @@ +import {buildUrl, deepAccess} from '../src/utils.js' +import { BANNER, VIDEO } from '../src/mediaTypes.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; -import {buildUrl} from '../src/utils.js' const BIDDER_CODE = 'smartytech'; export const ENDPOINT_PROTOCOL = 'https'; export const ENDPOINT_DOMAIN = 'server.smartytech.io'; -export const ENDPOINT_PATH = '/hb/bidder'; +export const ENDPOINT_PATH = '/hb/v2/bidder'; export const spec = { + supportedMediaTypes: [ BANNER, VIDEO ], code: BIDDER_CODE, isBidRequestValid: function (bidRequest) { - return !!parseInt(bidRequest.params.endpointId); + return ( + !!parseInt(bidRequest.params.endpointId) && + spec._validateBanner(bidRequest) && + spec._validateVideo(bidRequest) + ); + }, + + _validateBanner: function(bidRequest) { + const bannerAdUnit = deepAccess(bidRequest, 'mediaTypes.banner'); + + if (bannerAdUnit === undefined) { + return true; + } + + if (!Array.isArray(bannerAdUnit.sizes)) { + return false; + } + + return true; + }, + + _validateVideo: function(bidRequest) { + const videoAdUnit = deepAccess(bidRequest, 'mediaTypes.video'); + + if (videoAdUnit === undefined) { + return true; + } + + if (!Array.isArray(videoAdUnit.playerSize)) { + return false; + } + + if (!videoAdUnit.context) { + return false; + } + + return true; }, buildRequests: function (validBidRequests, bidderRequest) { const referer = bidderRequest?.refererInfo?.page || window.location.href; const bidRequests = validBidRequests.map((validBidRequest) => { - return { + let video = deepAccess(validBidRequest, 'mediaTypes.video', false); + let banner = deepAccess(validBidRequest, 'mediaTypes.banner', false); + let sizes = validBidRequest.params.sizes; + + let oneRequest = { endpointId: validBidRequest.params.endpointId, adUnitCode: validBidRequest.adUnitCode, - sizes: validBidRequest.sizes, - bidId: validBidRequest.bidId, - referer: referer + referer: referer, + bidId: validBidRequest.bidId }; + + if (video) { + oneRequest.video = video; + + if (sizes) { + oneRequest.video.sizes = sizes; + } + } else if (banner) { + oneRequest.banner = banner; + + if (sizes) { + oneRequest.banner.sizes = sizes; + } + } + + return oneRequest }); let adPartnerRequestUrl = buildUrl({ @@ -55,12 +112,14 @@ export const spec = { bid: validBids.find(b => b.adUnitCode === key), response: responseBody[key] } - }).map(item => spec.adResponse(item.bid.bidId, item.response)); + }).map(item => spec._adResponse(item.bid, item.response)); }, - adResponse: function (requestId, response) { + _adResponse: function (request, response) { const bidObject = { - requestId, + requestId: request.bidId, + adUnitCode: request.adUnitCode, + bidderCode: BIDDER_CODE, ad: response.ad, cpm: response.cpm, width: response.width, @@ -69,7 +128,14 @@ export const spec = { creativeId: response.creativeId, netRevenue: true, currency: response.currency, + mediaType: BANNER } + + if (response.mediaType === VIDEO) { + bidObject.vastXml = response.ad; + bidObject.mediaType = VIDEO; + } + return bidObject; }, diff --git a/modules/smartytechBidAdapter.md b/modules/smartytechBidAdapter.md index dbfc2833c78..9df57ddbde7 100644 --- a/modules/smartytechBidAdapter.md +++ b/modules/smartytechBidAdapter.md @@ -1,44 +1,55 @@ # Overview -Module Name: SmartyTech Bidder Adapter - -Module Type: Bidder Adapter - +``` +Module Name: SmartyTech Bid Adapter +Module Type: Bidder Adapter Maintainer: info@adpartner.pro +``` # Description -You can use this adapter to get a bid from smartytech.io. +Connects to SmartyTech's exchange for bids. -About us : https://smartytech.io +SmartyTech bid adapter supports Banner and Video -# Test Parameters +# Sample Ad Unit: For Publishers +## Sample Banner Ad Unit +``` +var adUnits = [{ + code: '/123123123/prebidjs-banner', + mediaTypes: { + banner: { + sizes: [ + [300, 301], + [300, 250] + ] + } + }, + bids: [{ + bidder: 'smartytech', + params: { + endpointId: 12 + } + }] +}]; +``` -```javascript - var adUnits = [ - { - code: 'div-smartytech-example', - sizes: [[300, 250]], - bids: [ - { - bidder: "smartytech", - params: { - endpointId: 14 - } - } - ] +## Sample Video Ad Unit +``` +var videoAdUnit = { + code: '/123123123/video-vast-banner', + mediaTypes: { + video: { + context: 'instream', + playerSize: [640, 480], + mimes: ['video/mp4'], + } }, - { - code: 'div-smartytech-example-2', - sizes: [[300, 250]], - bids: [ - { - bidder: "smartytech", - params: { - endpointId: 14 - } - } - ] - } -]; + bids: [{ + bidder: 'smartytech', + params: { + endpointId: 12 + } + }] +}; ``` diff --git a/modules/smilewantedBidAdapter.js b/modules/smilewantedBidAdapter.js index f82e7c9258f..fe14c57d641 100644 --- a/modules/smilewantedBidAdapter.js +++ b/modules/smilewantedBidAdapter.js @@ -63,6 +63,11 @@ export const spec = { payload.gdpr_consent = bidderRequest.gdprConsent.consentString; payload.gdpr = bidderRequest.gdprConsent.gdprApplies; // we're handling the undefined case server side } + + if (bid && bid.userIdAsEids) { + payload.eids = bid.userIdAsEids; + } + var payloadString = JSON.stringify(payload); return { method: 'POST', diff --git a/modules/snigelBidAdapter.js b/modules/snigelBidAdapter.js index e0ec10c6ed6..f41fb98d436 100644 --- a/modules/snigelBidAdapter.js +++ b/modules/snigelBidAdapter.js @@ -3,6 +3,7 @@ import {registerBidder} from '../src/adapters/bidderFactory.js'; import {BANNER} from '../src/mediaTypes.js'; import {deepAccess, isArray, isFn, isPlainObject} from '../src/utils.js'; import {hasPurpose1Consent} from '../src/utils/gpdr.js'; +import {getGlobal} from '../src/prebidGlobal.js'; const BIDDER_CODE = 'snigel'; const GVLID = 1076; @@ -33,7 +34,7 @@ export const spec = { test: getTestFlag(), devw: window.innerWidth || document.documentElement.clientWidth || document.body.clientWidth, devh: window.innerHeight || document.documentElement.clientHeight || document.body.clientHeight, - version: $$PREBID_GLOBAL$$.version, + version: getGlobal().version, gdprApplies: gdprApplies, gdprConsentString: gdprApplies === true ? deepAccess(bidderRequest, 'gdprConsent.consentString') : undefined, gdprConsentProv: gdprApplies === true ? deepAccess(bidderRequest, 'gdprConsent.addtlConsent') : undefined, diff --git a/modules/sonobiBidAdapter.js b/modules/sonobiBidAdapter.js index 3c841cc4d8a..6760a3c18ab 100644 --- a/modules/sonobiBidAdapter.js +++ b/modules/sonobiBidAdapter.js @@ -1,9 +1,10 @@ import { registerBidder } from '../src/adapters/bidderFactory.js'; -import { parseSizesInput, logError, generateUUID, isEmpty, deepAccess, logWarn, logMessage, deepClone, getGptSlotInfoForAdUnitCode, isFn, isPlainObject } from '../src/utils.js'; +import { parseSizesInput, logError, generateUUID, isEmpty, deepAccess, logWarn, logMessage, getGptSlotInfoForAdUnitCode, isFn, isPlainObject } from '../src/utils.js'; import { BANNER, VIDEO } from '../src/mediaTypes.js'; import { config } from '../src/config.js'; import { Renderer } from '../src/Renderer.js'; import { userSync } from '../src/userSync.js'; +import { bidderSettings } from '../src/bidderSettings.js'; const BIDDER_CODE = 'sonobi'; const STR_ENDPOINT = 'https://apex.go.sonobi.com/trinity.json'; const PAGEVIEW_ID = generateUUID(); @@ -49,6 +50,7 @@ export const spec = { return true; }, + /** * Make a server request from the list of BidRequests. * @@ -93,7 +95,7 @@ export const spec = { 'lib_name': 'prebid', 'lib_v': '$prebid.version$', 'us': 0, - + 'iqid': bidderSettings.get(BIDDER_CODE, 'storageAllowed') ? JSON.stringify(loadOrCreateFirstPartyData()) : null, }; const fpd = bidderRequest.ortb2; @@ -132,15 +134,6 @@ export const spec = { if (validBidRequests[0].schain) { payload.schain = JSON.stringify(validBidRequests[0].schain); } - if (deepAccess(validBidRequests[0], 'userId') && Object.keys(validBidRequests[0].userId).length > 0) { - const userIds = deepClone(validBidRequests[0].userId); - - if (userIds.id5id) { - userIds.id5id = deepAccess(userIds, 'id5id.uid'); - } - - payload.userid = JSON.stringify(userIds); - } const eids = deepAccess(validBidRequests[0], 'userIdAsEids'); if (Array.isArray(eids) && eids.length > 0) { @@ -295,22 +288,29 @@ function _findBidderRequest(bidderRequests, bidId) { } } +// This function takes all the possible sizes. +// returns string csv. function _validateSize(bid) { - if (deepAccess(bid, 'mediaTypes.video')) { - return ''; // Video bids arent allowed to override sizes via the trinity request + let size = []; + if (deepAccess(bid, 'mediaTypes.video.playerSize')) { + size.push(deepAccess(bid, 'mediaTypes.video.playerSize')) } - - if (bid.params.sizes) { - return parseSizesInput(bid.params.sizes).join(','); + if (deepAccess(bid, 'mediaTypes.video.sizes')) { + size.push(deepAccess(bid, 'mediaTypes.video.sizes')) + } + if (deepAccess(bid, 'params.sizes')) { + size.push(deepAccess(bid, 'params.sizes')); } if (deepAccess(bid, 'mediaTypes.banner.sizes')) { - return parseSizesInput(deepAccess(bid, 'mediaTypes.banner.sizes')).join(','); + size.push(deepAccess(bid, 'mediaTypes.banner.sizes')) } - - // Handle deprecated sizes definition - if (bid.sizes) { - return parseSizesInput(bid.sizes).join(','); + if (deepAccess(bid, 'sizes')) { + size.push(deepAccess(bid, 'sizes')) } + // Pass the 2d sizes array into parseSizeInput to flatten it into an array of x separated sizes. + // Then throw it into Set to uniquify it. + // Then spread it to an array again. Then join it into a csv of sizes. + return [...new Set(parseSizesInput(...size))].join(','); } function _validateSlot(bid) { @@ -390,6 +390,67 @@ export function _getPlatform(context = window) { } return 'desktop'; } +/** + * Check for local storage + * Generate a UUID for the user if one does not exist in local storage + * Store the UUID in local storage for future use + * @return {object} firstPartyData - Data object containing first party information + */ +function loadOrCreateFirstPartyData() { + var localStorageEnabled; + + var FIRST_PARTY_KEY = '_iiq_fdata'; + var tryParse = function (data) { + try { + return JSON.parse(data); + } catch (err) { + return null; + } + }; + var readData = function (key) { + if (hasLocalStorage()) { + return window.localStorage.getItem(key); + } + return null; + }; + var hasLocalStorage = function () { + if (typeof localStorageEnabled != 'undefined') { return localStorageEnabled; } else { + try { + localStorageEnabled = !!window.localStorage; + return localStorageEnabled; + } catch (e) { + localStorageEnabled = false; + } + } + return false; + }; + var generateGUID = function () { + var d = new Date().getTime(); + return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) { + var r = (d + Math.random() * 16) % 16 | 0; + d = Math.floor(d / 16); + return (c === 'x' ? r : (r & 0x3) | 0x8).toString(16); + }); + }; + var storeData = function (key, value) { + try { + if (hasLocalStorage()) { + window.localStorage.setItem(key, value); + } + } catch (error) { + return null; + } + }; + var firstPartyData = tryParse(readData(FIRST_PARTY_KEY)); + if (!firstPartyData || !firstPartyData.pcid) { + var firstPartyId = generateGUID(); + firstPartyData = { pcid: firstPartyId, pcidDate: Date.now() }; + } else if (firstPartyData && !firstPartyData.pcidDate) { + firstPartyData.pcidDate = Date.now(); + } + storeData(FIRST_PARTY_KEY, JSON.stringify(firstPartyData)); + return firstPartyData; +}; function newRenderer(adUnitCode, bid, rendererOptions = {}) { const renderer = Renderer.install({ diff --git a/modules/sovrnBidAdapter.js b/modules/sovrnBidAdapter.js index ab8abb7e2b4..9982c9afa45 100644 --- a/modules/sovrnBidAdapter.js +++ b/modules/sovrnBidAdapter.js @@ -15,7 +15,6 @@ import { BANNER, VIDEO } from '../src/mediaTypes.js' -import {createEidsArray} from './userId/eids.js'; const ORTB_VIDEO_PARAMS = { 'mimes': (value) => Array.isArray(value) && value.length > 0 && value.every(v => typeof v === 'string'), @@ -90,8 +89,8 @@ export const spec = { let criteoId; _each(bidReqs, function (bid) { - if (!eids && bid.userId) { - eids = createEidsArray(bid.userId) + if (!eids && bid.userIdAsEids) { + eids = bid.userIdAsEids; eids.forEach(function (id) { if (id.uids && id.uids[0]) { if (id.source === 'criteo.com') { diff --git a/modules/sspBCBidAdapter.js b/modules/sspBCBidAdapter.js index ffe2bef054c..acfa9fe7945 100644 --- a/modules/sspBCBidAdapter.js +++ b/modules/sspBCBidAdapter.js @@ -10,10 +10,9 @@ const BIDDER_CODE = 'sspBC'; const BIDDER_URL = 'https://ssp.wp.pl/bidder/'; const SYNC_URL = 'https://ssp.wp.pl/bidder/usersync'; const NOTIFY_URL = 'https://ssp.wp.pl/bidder/notify'; -const TRACKER_URL = 'https://bdr.wpcdn.pl/tag/jstracker.js'; const GVLID = 676; const TMAX = 450; -const BIDDER_VERSION = '5.7'; +const BIDDER_VERSION = '5.8'; const DEFAULT_CURRENCY = 'PLN'; const W = window; const { navigator } = W; @@ -23,6 +22,39 @@ const adSizesCalled = {}; const pageView = {}; var consentApiVersion; +/** + * Native asset mapping - we use constant id per type + * id > 10 indicates additional images + */ +var nativeAssetMap = { + title: 0, + cta: 1, + icon: 2, + image: 3, + body: 4, + sponsoredBy: 5 +}; + +/** + * return native asset type, based on asset id + * @param {int} id - native asset id + * @returns {string} asset type + */ +const getNativeAssetType = id => { + // id>10 will always be an image... + if (id > 10) { + return 'image'; + } + + // ...others should be decoded from nativeAssetMap + for (let assetName in nativeAssetMap) { + const assetId = nativeAssetMap[assetName]; + if (assetId === id) { + return assetName; + } + } +} + /** * Get preferred language of browser (i.e. user) * @returns {string} languageCode - ISO language code @@ -42,6 +74,16 @@ const getContentLanguage = () => { } }; +/** + * Get Bid parameters - returns bid params from Object, or 1el array + * @param {*} bidData - bid (bidWon), or array of bids (timeout) + * @returns {object} params object + */ +const unpackParams = (bidParams) => { + const result = isArray(bidParams) ? bidParams[0] : bidParams; + return result || {}; +} + /** * Get bid parameters for notification * @param {*} bidData - bid (bidWon), or array of bids (timeout) @@ -58,8 +100,7 @@ const getNotificationPayload = bidData => { } bids.forEach(bid => { const { adUnitCode, auctionId, cpm, creativeId, meta, params: bidParams, requestId, timeout } = bid; - let params = isArray(bidParams) ? bidParams[0] : bidParams; - params = params || {}; + const params = unpackParams(bidParams); // basic notification data const bidBasicData = { @@ -178,6 +219,46 @@ const applyGdpr = (bidderRequest, ortbRequest) => { } } +/** + * Get highest floorprice for a given adslot + * (sspBC adapter accepts one floor per imp) + * returns floor = 0 if getFloor() is not defined + * + * @param {object} slot bid request adslot + * @returns {float} floorprice + */ +const getHighestFloor = (slot) => { + const currency = getCurrency(); + let result = { floor: 0, currency }; + + if (typeof slot.getFloor === 'function') { + let bannerFloor = 0; + + if (slot.sizes.length) { + bannerFloor = slot.sizes.reduce(function (prev, next) { + const { floor: currentFloor = 0 } = slot.getFloor({ + mediaType: 'banner', + size: next, + currency + }); + return prev > currentFloor ? prev : currentFloor; + }, 0); + } + + const { floor: nativeFloor = 0 } = slot.getFloor({ + mediaType: 'native', currency + }); + + const { floor: videoFloor = 0 } = slot.getFloor({ + mediaType: 'video', currency + }); + + result.floor = Math.max(bannerFloor, nativeFloor, videoFloor); + } + + return result; +}; + /** * Get currency (either default or adserver) * @returns {string} currency name @@ -226,71 +307,111 @@ const mapBanner = slot => { * @param {object} paramValue Native parameter value * @returns {object} native asset object that conforms to ortb native ads spec */ -const mapAsset = (paramName, paramValue) => { - let asset; - switch (paramName) { - case 'title': - asset = { - id: 0, - required: paramValue.required, - title: { len: paramValue.len } - } - break; - case 'cta': - asset = { - id: 1, - required: paramValue.required, - data: { type: 12 } - } - break; - case 'icon': - asset = { - id: 2, - required: paramValue.required, - img: { type: 1, w: paramValue.sizes[0], h: paramValue.sizes[1] } - } - break; - case 'image': - asset = { - id: 3, - required: paramValue.required, - img: { type: 3, w: paramValue.sizes[0], h: paramValue.sizes[1] } - } - break; - case 'body': - asset = { - id: 4, - required: paramValue.required, - data: { type: 2 } - } - break; - case 'sponsoredBy': - asset = { - id: 5, - required: paramValue.required, - data: { type: 1 } - } - break; +var mapAsset = function mapAsset(paramName, paramValue) { + const { required, sizes, wmin, hmin, len } = paramValue; + var id = nativeAssetMap[paramName]; + var assets = []; + + if (id !== undefined) { + switch (paramName) { + case 'title': + assets.push({ + id: id, + required: required, + title: { + len: len + } + }); + break; + + case 'cta': + assets.push({ + id: id, + required: required, + data: { + type: 12 + } + }); + break; + + case 'icon': + assets.push({ + id: id, + required: required, + img: { + type: 1, + w: sizes && sizes[0], + h: sizes && sizes[1] + } + }); + break; + + case 'image': + var hasMultipleImages = sizes && Array.isArray(sizes[0]); + var imageSizes = hasMultipleImages ? sizes : [sizes]; + + for (var i = 0; i < imageSizes.length; i++) { + assets.push({ + id: i > 0 ? 10 + i : id, + required: required, + img: { + type: 3, + w: imageSizes[i][0], + h: imageSizes[i][1], + wmin: wmin, + hmin: hmin + } + }); + } + + break; + + case 'body': + assets.push({ + id: id, + required: required, + data: { + type: 2 + } + }); + break; + + case 'sponsoredBy': + assets.push({ + id: id, + required: required, + data: { + type: 1 + } + }); + break; + } } - return asset; -} + + return assets; +}; /** * @param {object} slot Ad Unit Params by Prebid * @returns {object} native object that conforms to ortb native ads spec */ -const mapNative = slot => { +const mapNative = (slot) => { const native = deepAccess(slot, 'mediaTypes.native'); - let assets; if (native) { - const nativeParams = Object.keys(native); - assets = []; - nativeParams.forEach(par => { - const newAsset = mapAsset(par, native[par]); - if (newAsset) { assets.push(newAsset) }; + var nativeParams = Object.keys(native); + var assets = []; + nativeParams.forEach(function (par) { + var newAssets = mapAsset(par, native[par]); + assets = assets.concat(newAssets); }); + return { + request: JSON.stringify({ + native: { + assets: assets + } + }) + }; } - return assets ? { request: JSON.stringify({ native: { assets } }) } : undefined; } var mapVideo = (slot, videoFromBid) => { @@ -346,41 +467,18 @@ const mapImpression = slot => { const imp = { id: id && siteId ? id.padStart(3, '0') : 'bidid-' + bidId, banner: mapBanner(slot), - native: mapNative(slot), + native: mapNative(slot, bidId), video: mapVideo(slot, video), tagid: adUnitCode, ext, }; // Check floorprices for this imp - const currency = getCurrency(); - if (typeof slot.getFloor === 'function') { - var bannerFloor = 0; - var nativeFloor = 0; - var videoFloor = 0; // sspBC adapter accepts only floor per imp - check for maximum value for requested ad types and sizes - - if (slot.sizes.length) { - bannerFloor = slot.sizes.reduce(function (prev, next) { - var currentFloor = slot.getFloor({ - mediaType: 'banner', - size: next, - currency - }).floor; - return prev > currentFloor ? prev : currentFloor; - }, 0); - } + const { floor, currency } = getHighestFloor(slot); - nativeFloor = slot.getFloor({ - mediaType: 'native', currency - }); - videoFloor = slot.getFloor({ - mediaType: 'video', currency - }); - imp.bidfloor = Math.max(bannerFloor, nativeFloor, videoFloor); - } else { - imp.bidfloor = 0; - } + imp.bidfloor = floor; imp.bidfloorcur = currency; + return imp; } @@ -395,50 +493,51 @@ const isNativeAd = bid => { return bid.admNative || (bid.adm && bid.adm.match(xmlTester)); } -const parseNative = nativeData => { - const result = {}; - nativeData.assets.forEach(asset => { - const id = parseInt(asset.id); - switch (id) { - case 0: - result.title = asset.title.text; - break; - case 1: - result.cta = asset.data.value; - break; - case 2: - result.icon = { - url: asset.img.url, - width: asset.img.w, - height: asset.img.h, - }; - break; - case 3: - result.image = { - url: asset.img.url, - width: asset.img.w, - height: asset.img.h, - }; - break; - case 4: - result.body = asset.data.value; - break; - case 5: - result.sponsoredBy = asset.data.value; - break; +const parseNative = (nativeData) => { + const { link = {}, imptrackers: impressionTrackers, jstracker } = nativeData; + const { url: clickUrl, clicktrackers: clickTrackers = [] } = link; - default: - logWarn('Unrecognized native asset', asset); + const result = { + clickUrl, + clickTrackers, + impressionTrackers, + javascriptTrackers: isArray(jstracker) ? jstracker : jstracker && [jstracker], + }; + + nativeData.assets.forEach(asset => { + const { id, img = {}, title = {}, data = {} } = asset; + const { w: imgWidth, h: imgHeight, url: imgUrl, type: imgType } = img; + const { type: dataType, value: dataValue } = data; + const { text: titleText } = title; + const detectedType = getNativeAssetType(id); + if (titleText) { + result.title = titleText; + } + if (imgUrl) { + // image or icon + const thisImage = { + url: imgUrl, + width: imgWidth, + height: imgHeight, + }; + if (imgType === 3 || detectedType === 'image') { + result.image = thisImage; + } else if (imgType === 1 || detectedType === 'icon') { + result.icon = thisImage; + } + } + if (dataValue) { + // call-to-action, sponsored-by or body + if (dataType === 1 || detectedType === 'sponsoredBy') { + result.sponsoredBy = dataValue; + } else if (dataType === 2 || detectedType === 'body') { + result.body = dataValue; + } else if (dataType === 12 || detectedType === 'cta') { + result.cta = dataValue; + } } }); - result.clickUrl = nativeData.link.url; - result.impressionTrackers = nativeData.imptrackers; - if (isArray(nativeData.jstracker)) { - result.javascriptTrackers = nativeData.jstracker; - } else if (nativeData.jstracker) { - result.javascriptTrackers = [nativeData.jstracker]; - } return result; } @@ -505,10 +604,6 @@ const renderCreative = (site, auctionId, bid, seat, request) => { window.requestPVID = "${pageView.id}"; `; - if (gam) { - adcode += `window.gam = ${JSON.stringify(gam)};`; - } - adcode += ` @@ -550,7 +645,7 @@ const spec = { const payload = { id: bidderRequest.auctionId, site: { - id: siteId, + id: siteId ? `${siteId}` : undefined, publisher: publisherId ? { id: publisherId } : undefined, page, domain, @@ -586,7 +681,7 @@ const spec = { const { bidderRequest } = request; const response = serverResponse.body; const bids = []; - const site = JSON.parse(request.data).site; // get page and referer data from request + let site = JSON.parse(request.data).site; // get page and referer data from request site.sn = response.sn || 'mc_adapter'; // WPM site name (wp_sn) pageView.sn = site.sn; // store site_name (for syncing and notifications) let seat; @@ -597,39 +692,35 @@ const spec = { 'bidid-' prefix indicates oneCode (parameterless) request and response */ response.seatbid.forEach(seatbid => { - let creativeCache; seat = seatbid.seat; seatbid.bid.forEach(serverBid => { // get data from bid response - const { adomain, crid = `mcad_${bidderRequest.auctionId}_${site.slot}`, impid, exp = 300, ext, price, w, h } = serverBid; + const { adomain, crid = `mcad_${bidderRequest.auctionId}_${site.slot}`, impid, exp = 300, ext = {}, price, w, h } = serverBid; const bidRequest = bidderRequest.bids.filter(b => { - const { bidId, params = {} } = b; + const { bidId, params: requestParams = {} } = b; + const params = unpackParams(requestParams); const { id, siteId } = params; const currentBidId = id && siteId ? id : 'bidid-' + bidId; return currentBidId === impid; })[0]; - // get data from linked bidRequest - const { bidId, params } = bidRequest || {}; - - // get slot id for current bid - site.slot = params && params.id; - - if (ext) { - /* - bid response might include ext object containing siteId / slotId, as detected by OneCode - update site / slot data in this case - - ext also might contain publisherId and custom ad label - */ - const { siteid, slotid, pubid, adlabel, cache } = ext; - site.id = siteid || site.id; - site.slot = slotid || site.slot; - site.publisherId = pubid; - site.adLabel = adlabel; - creativeCache = cache; - } + // get bidid from linked bidRequest + const { bidId } = bidRequest || {}; + + // get ext data from bid + const { siteid = site.id, slotid = site.slot, pubid, adlabel, cache: creativeCache, vurls = [] } = ext; + + // update site data + site = { + ...site, + ...{ + id: siteid, + slot: slotid, + publisherId: pubid, + adLabel: adlabel + } + }; if (bidRequest && site.id && !strIncludes(site.id, 'bidid')) { // found a matching request; add this bid @@ -652,6 +743,7 @@ const spec = { pricepl: ext && ext.pricepl, }, netRevenue: true, + vurls, }; // mediaType and ad data for instream / native / banner @@ -671,24 +763,6 @@ const spec = { bid.native = parseNative(nativeData); bid.width = 1; bid.height = 1; - - // append viewability tracker - const jsData = { - rid: bidRequest.auctionId, - crid: bid.creativeId, - adunit: bidRequest.adUnitCode, - url: bid.native.clickUrl, - vendor: seat, - site: site.id, - slot: site.slot, - cpm: bid.cpm.toPrecision(4), - }; - const jsTracker = '`, meta: { - advertiserDomains: (matchedBid.advertiser) ? matchedBid.advertiser : 'n/a' - } - } + advertiserDomains: (matchedBid.advertiser) ? matchedBid.advertiser : 'n/a', + }, + }; if (isVideo(bidRequest, adType)) { - const playersize = getPlayerSize(bidRequest) + const playersize = getPlayerSize(bidRequest); if (playersize) { - bidResponse.width = playersize[0] - bidResponse.height = playersize[1] + bidResponse.width = playersize[0]; + bidResponse.height = playersize[1]; } - bidResponse.mediaType = VIDEO - bidResponse.vastUrl = `${ENDPOINT}/d/${matchedBid.id}/${bidRequest.params.supplyId}/?ts=${timestamp}${extId}${gdprApplies}${gdprConsent}${pvId}${iabContent}` + bidResponse.mediaType = VIDEO; + bidResponse.vastUrl = `${ENDPOINT}/d/${matchedBid.id}/${bidRequest.params.supplyId}/?ts=${timestamp}${extId}${gdprApplies}${gdprConsent}${pvId}${iabContent}`; if (isOutstream(bidRequest)) { const renderer = Renderer.install({ id: bidRequest.bidId, url: OUTSTREAMPLAYER_URL, - loaded: false - }) - renderer.setRender(outstreamRender) - bidResponse.renderer = renderer + loaded: false, + }); + renderer.setRender(outstreamRender); + bidResponse.renderer = renderer; } } if (isNative(bidRequest, adType)) { // there may be publishers still rely on it - const url = `${ENDPOINT}/d/${matchedBid.id}/${bidRequest.params.supplyId}/?ts=${timestamp}${extId}${gdprApplies}${gdprConsent}${pvId}` - bidResponse.adUrl = url - bidResponse.mediaType = NATIVE - const nativeImageAssetObj = find(matchedBid.native.assets, e => e.id === 2) + const url = `${ENDPOINT}/d/${matchedBid.id}/${bidRequest.params.supplyId}/?ts=${timestamp}${extId}${gdprApplies}${gdprConsent}${pvId}`; + bidResponse.adUrl = url; + bidResponse.mediaType = NATIVE; + const nativeImageAssetObj = find(matchedBid.native.assets, e => e.id === 2); const nativeImageAsset = nativeImageAssetObj ? nativeImageAssetObj.img : { url: '', w: 0, h: 0 }; - const nativeTitleAsset = find(matchedBid.native.assets, e => e.id === 1) - const nativeBodyAsset = find(matchedBid.native.assets, e => e.id === 3) + const nativeTitleAsset = find(matchedBid.native.assets, e => e.id === 1); + const nativeBodyAsset = find(matchedBid.native.assets, e => e.id === 3); bidResponse.native = { title: nativeTitleAsset ? nativeTitleAsset.title.text : '', body: nativeBodyAsset ? nativeBodyAsset.data.value : '', @@ -203,10 +204,10 @@ export const spec = { }; } - bidResponses.push(bidResponse) + bidResponses.push(bidResponse); } - }) - return bidResponses + }); + return bidResponses; }, /** @@ -218,13 +219,13 @@ export const spec = { * @param {string} uspConsent Is the US Privacy Consent string. * @return {UserSync[]} The user syncs which should be dropped. */ - getUserSyncs: function (syncOptions, serverResponses, gdprConsent, uspConsent) { + getUserSyncs(syncOptions, serverResponses, gdprConsent, uspConsent) { const syncs = []; if (syncOptions.iframeEnabled) { const params = []; params.push(`ts=${timestamp()}`); - params.push(`type=h`) + params.push(`type=h`); if (gdprConsent && (typeof gdprConsent.gdprApplies === 'boolean')) { params.push(`gdpr=${Number(gdprConsent.gdprApplies)}`); } @@ -233,12 +234,12 @@ export const spec = { } syncs.push({ type: 'iframe', - url: `${ENDPOINT}/d/6846326/766/2x2?${params.join('&')}` + url: `${ENDPOINT}/d/6846326/766/2x2?${params.join('&')}`, }); } return syncs; - } + }, }; /** @@ -248,7 +249,7 @@ export const spec = { * @returns {Boolean} */ function isVideo(format, adtype) { - return deepAccess(format, 'mediaTypes.video') && adtype.toLowerCase() === 'video' + return deepAccess(format, 'mediaTypes.video') && adtype.toLowerCase() === 'video'; } /** @@ -258,7 +259,7 @@ function isVideo(format, adtype) { * @returns {Boolean} */ function isNative(format, adtype) { - return deepAccess(format, 'mediaTypes.native') && adtype.toLowerCase() === 'native' + return deepAccess(format, 'mediaTypes.native') && adtype.toLowerCase() === 'native'; } /** @@ -267,8 +268,8 @@ function isNative(format, adtype) { * @returns {Boolean} */ function isOutstream(format) { - const context = deepAccess(format, 'mediaTypes.video.context') - return (context === 'outstream') + const context = deepAccess(format, 'mediaTypes.video.context'); + return (context === 'outstream'); } /** @@ -277,30 +278,45 @@ function isOutstream(format) { * @returns {Array} */ function getPlayerSize(format) { - const playerSize = deepAccess(format, 'mediaTypes.video.playerSize') - return (playerSize && isArray(playerSize[0])) ? playerSize[0] : playerSize + const playerSize = deepAccess(format, 'mediaTypes.video.playerSize'); + return (playerSize && isArray(playerSize[0])) ? playerSize[0] : playerSize; } /** - * Expands a 'WxH' string as a 2-element [W, H] array + * Expands a 'WxH' string to a 2-element [W, H] array * @param {String} size * @returns {Array} */ function parseSize(size) { - return size.split(DIMENSION_SIGN).map(Number) + return size.split(DIMENSION_SIGN).map(Number); } /** * Creates a string out of an array of eids with source and uid - * @param {Array} eids + * @param {Array.<{source: String, uids: Array.<{id: String, atype: Number, ext: Object}>}>} eids * @returns {String} */ function createUserIdString(eids) { - const str = [] + const str = []; for (let i = 0; i < eids.length; i++) { - str.push(eids[i].source + ':' + eids[i].uids[0].id) + str.push(eids[i].source + ':' + eids[i].uids[0].id); } - return str.join(',') + return str.join(','); +} + +/** + * Creates a string from an array of eids with ID provider and atype if atype exists + * @param {Array.<{source: String, uids: Array.<{id: String, atype: Number, ext: Object}>}>} eids + * @returns {String} idprovider:atype,idprovider2:atype2,... + */ +function createUserIdAtypesString(eids) { + const str = []; + for (let i = 0; i < eids.length; i++) { + if (eids[i].uids[0].atype) { + str.push(eids[i].source + ':' + eids[i].uids[0].atype); + } + } + return str.join(','); } /** @@ -309,18 +325,18 @@ function createUserIdString(eids) { * @returns {String} */ function createQueryString(obj) { - const str = [] + const str = []; for (const p in obj) { if (obj.hasOwnProperty(p)) { - const val = obj[p] + const val = obj[p]; if (p !== 'schain' && p !== 'iab_content') { - str.push(encodeURIComponent(p) + '=' + encodeURIComponent(val)) + str.push(encodeURIComponent(p) + '=' + encodeURIComponent(val)); } else { - str.push(p + '=' + val) + str.push(p + '=' + val); } } } - return str.join('&') + return str.join('&'); } /** @@ -329,15 +345,15 @@ function createQueryString(obj) { * @returns {String} */ function createTargetingString(obj) { - const str = [] + const str = []; for (const p in obj) { if (obj.hasOwnProperty(p)) { - const key = p - const val = obj[p] - str.push(key + '=' + val) + const key = p; + const val = obj[p]; + str.push(key + '=' + val); } } - return str.join('&') + return str.join('&'); } /** @@ -346,13 +362,13 @@ function createTargetingString(obj) { * @returns {String} */ function createSchainString(schain) { - const ver = schain.ver || '' - const complete = (schain.complete === 1 || schain.complete === 0) ? schain.complete : '' - const keys = ['asi', 'sid', 'hp', 'rid', 'name', 'domain', 'ext'] + const ver = schain.ver || ''; + const complete = (schain.complete === 1 || schain.complete === 0) ? schain.complete : ''; + const keys = ['asi', 'sid', 'hp', 'rid', 'name', 'domain', 'ext']; const nodesString = schain.nodes.reduce((acc, node) => { - return acc += `!${keys.map(key => node[key] ? encodeURIComponentWithBangIncluded(node[key]) : '').join(',')}` - }, '') - return `${ver},${complete}${nodesString}` + return acc += `!${keys.map(key => node[key] ? encodeURIComponentWithBangIncluded(node[key]) : '').join(',')}`; + }, ''); + return `${ver},${complete}${nodesString}`; } /** @@ -364,15 +380,15 @@ function createSchainString(schain) { */ function getContentObject(bid) { if (bid.params.iabContent && isPlainObject(bid.params.iabContent)) { - return bid.params.iabContent + return bid.params.iabContent; } const globalContent = deepAccess(bid, 'ortb2.site') ? deepAccess(bid, 'ortb2.site.content') - : deepAccess(bid, 'ortb2.app.content') + : deepAccess(bid, 'ortb2.app.content'); if (globalContent && isPlainObject(globalContent)) { - return globalContent + return globalContent; } - return undefined + return undefined; } /** @@ -385,15 +401,15 @@ function getContentObject(bid) { * @returns {String} */ function createIabContentString(iabContent) { - const arrKeys = ['keywords', 'cat'] - const str = [] + const arrKeys = ['keywords', 'cat']; + const str = []; const transformObjToParam = (obj = {}, extraKey = '') => { for (const key in obj) { if ((arrKeys.indexOf(key) !== -1 && Array.isArray(obj[key]))) { // Array of defined keyword which have to be joined into one value from "key: [value1, value2, value3]" to "key:value1|value2|value3" - str.push(''.concat(key, ':', obj[key].map(node => encodeURIComponent(node)).join('|'))) + str.push(''.concat(key, ':', obj[key].map(node => encodeURIComponent(node)).join('|'))); } else if (typeof obj[key] !== 'object') { - str.push(''.concat(extraKey + key, ':', encodeURIComponent(obj[key]))) + str.push(''.concat(extraKey + key, ':', encodeURIComponent(obj[key]))); } else { // Object has to be further flattened transformObjToParam(obj[key], ''.concat(extraKey, key, '.')); @@ -401,7 +417,7 @@ function createIabContentString(iabContent) { } return str.join(','); }; - return encodeURIComponent(transformObjToParam(iabContent)) + return encodeURIComponent(transformObjToParam(iabContent)); } /** @@ -410,7 +426,7 @@ function createIabContentString(iabContent) { * @returns {String} */ function encodeURIComponentWithBangIncluded(str) { - return encodeURIComponent(str).replace(/!/g, '%21') + return encodeURIComponent(str).replace(/!/g, '%21'); } /** @@ -419,11 +435,11 @@ function encodeURIComponentWithBangIncluded(str) { */ function outstreamRender(bid) { bid.renderer.push(() => { - window.ma_width = bid.width - window.ma_height = bid.height - window.ma_vastUrl = bid.vastUrl - window.ma_container = bid.adUnitCode - window.document.dispatchEvent(new Event('ma-start-event')) + window.ma_width = bid.width; + window.ma_height = bid.height; + window.ma_vastUrl = bid.vastUrl; + window.ma_container = bid.adUnitCode; + window.document.dispatchEvent(new Event('ma-start-event')); }); } @@ -434,33 +450,33 @@ function outstreamRender(bid) { * @returns {string[]} */ function extractSizes(bid) { - const { mediaTypes } = bid // see https://docs.prebid.org/dev-docs/adunit-reference.html#examples - const sizes = [] + const { mediaTypes } = bid; // see https://docs.prebid.org/dev-docs/adunit-reference.html#examples + const sizes = []; if (isPlainObject(mediaTypes)) { - const { [BANNER]: bannerType } = mediaTypes + const { [BANNER]: bannerType } = mediaTypes; // only applies for multi size Adslots -> BANNER if (bannerType && isArray(bannerType.sizes)) { if (isArray(bannerType.sizes[0])) { // multiple sizes given - sizes.push(bannerType.sizes) + sizes.push(bannerType.sizes); } else { // just one size provided as array -> wrap to uniformly flatten later - sizes.push([bannerType.sizes]) + sizes.push([bannerType.sizes]); } } // The bid top level field `sizes` is deprecated and should not be used anymore. Keeping it for compatibility. } else if (isArray(bid.sizes)) { if (isArray(bid.sizes[0])) { - sizes.push(bid.sizes) + sizes.push(bid.sizes); } else { - sizes.push([bid.sizes]) + sizes.push([bid.sizes]); } } /** @type {Set} */ - const deduplicatedSizeStrings = new Set(sizes.flat().map(([width, height]) => width + DIMENSION_SIGN + height)) + const deduplicatedSizeStrings = new Set(sizes.flat().map(([width, height]) => width + DIMENSION_SIGN + height)); - return Array.from(deduplicatedSizeStrings) + return Array.from(deduplicatedSizeStrings); } /** @@ -481,7 +497,7 @@ function getBidFloor(bid, sizes) { const floor = bid.getFloor({ currency: CURRENCY_CODE, mediaType: mediaType !== undefined && spec.supportedMediaTypes.includes(mediaType) ? mediaType : '*', - size: sizes.length !== 1 ? '*' : sizes[0].split(DIMENSION_SIGN) + size: sizes.length !== 1 ? '*' : sizes[0].split(DIMENSION_SIGN), }); if (floor.currency === CURRENCY_CODE) { return (floor.floor * 100).toFixed(0); @@ -489,4 +505,4 @@ function getBidFloor(bid, sizes) { return undefined; } -registerBidder(spec) +registerBidder(spec); diff --git a/modules/yieldmoBidAdapter.js b/modules/yieldmoBidAdapter.js index 64ab8a87eea..526e1911a06 100644 --- a/modules/yieldmoBidAdapter.js +++ b/modules/yieldmoBidAdapter.js @@ -17,7 +17,6 @@ import {BANNER, VIDEO} from '../src/mediaTypes.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import {Renderer} from '../src/Renderer.js'; import {find, includes} from '../src/polyfill.js'; -import {createEidsArray} from './userId/eids.js'; const BIDDER_CODE = 'yieldmo'; const GVLID = 173; @@ -382,10 +381,11 @@ function openRtbRequest(bidRequests, bidderRequest) { const schain = bidRequests[0].schain; let openRtbRequest = { id: bidRequests[0].bidderRequestId, + tmax: bidderRequest.timeout || 400, at: 1, imp: bidRequests.map(bidRequest => openRtbImpression(bidRequest)), site: openRtbSite(bidRequests[0], bidderRequest), - device: openRtbDevice(bidRequests[0]), + device: deepAccess(bidderRequest, 'ortb2.device'), badv: bidRequests[0].params.badv || [], bcat: deepAccess(bidderRequest, 'bcat') || bidRequests[0].params.bcat || [], ext: { @@ -507,17 +507,6 @@ function openRtbSite(bidRequest, bidderRequest) { return result; } -/** - * @return Object OpenRTB's 'device' object - */ -function openRtbDevice(bidRequest) { - const deviceObj = { - ua: navigator.userAgent, - language: (navigator.language || navigator.browserLanguage || navigator.userLanguage || navigator.systemLanguage), - }; - return deviceObj; -} - /** * Updates openRtbRequest with GDPR info from bidderRequest, if present. * @param {Object} openRtbRequest OpenRTB's request to update. @@ -662,8 +651,8 @@ function shortcutProperty(extraCharacters, target, propertyName) { * @return array of eids objects */ function getEids(bidRequest) { - if (deepAccess(bidRequest, 'userId')) { - return createEidsArray(bidRequest.userId) || []; + if (deepAccess(bidRequest, 'userIdAsEids')) { + return bidRequest.userIdAsEids || []; } }; diff --git a/modules/yieldoneBidAdapter.js b/modules/yieldoneBidAdapter.js index d408a0595f9..fd804eed2e7 100644 --- a/modules/yieldoneBidAdapter.js +++ b/modules/yieldoneBidAdapter.js @@ -53,7 +53,7 @@ export const spec = { const bidId = bidRequest.bidId; const transactionId = bidRequest.transactionId; const unitCode = bidRequest.adUnitCode; - const timeout = config.getConfig('bidderTimeout'); + const timeout = bidderRequest.timeout; const language = window.navigator.language; const screenSize = window.screen.width + 'x' + window.screen.height; const payload = { diff --git a/modules/yuktamediaAnalyticsAdapter.js b/modules/yuktamediaAnalyticsAdapter.js index 31c6daae7f6..820e6365a9f 100644 --- a/modules/yuktamediaAnalyticsAdapter.js +++ b/modules/yuktamediaAnalyticsAdapter.js @@ -6,8 +6,11 @@ import CONSTANTS from '../src/constants.json'; import {getStorageManager} from '../src/storageManager.js'; import {getRefererInfo} from '../src/refererDetection.js'; import {includes as strIncludes} from '../src/polyfill.js'; +import {getGlobal} from '../src/prebidGlobal.js'; +import {MODULE_TYPE_ANALYTICS} from '../src/activities/modules.js'; -const storage = getStorageManager(); +const MODULE_CODE = 'yuktamedia'; +const storage = getStorageManager({moduleType: MODULE_TYPE_ANALYTICS, moduleName: MODULE_CODE}); const yuktamediaAnalyticsVersion = 'v3.1.0'; let initOptions; @@ -34,7 +37,7 @@ const _pageInfo = { referer: referer, refererDomain: parseUrl(referer).host, yuktamediaAnalyticsVersion: yuktamediaAnalyticsVersion, - prebidVersion: $$PREBID_GLOBAL$$.version + prebidVersion: getGlobal().version }; function getParameterByName(param) { @@ -260,7 +263,7 @@ yuktamediaAnalyticsAdapter.enableAnalytics = function (config) { adapterManager.registerAnalyticsAdapter({ adapter: yuktamediaAnalyticsAdapter, - code: 'yuktamedia' + code: MODULE_CODE, }); export default yuktamediaAnalyticsAdapter; diff --git a/modules/zeotapIdPlusIdSystem.js b/modules/zeotapIdPlusIdSystem.js index 3437928df4b..4cb827cbdff 100644 --- a/modules/zeotapIdPlusIdSystem.js +++ b/modules/zeotapIdPlusIdSystem.js @@ -6,7 +6,8 @@ */ import { isStr, isPlainObject } from '../src/utils.js'; import {submodule} from '../src/hook.js'; -import { getStorageManager } from '../src/storageManager.js'; +import {getStorageManager} from '../src/storageManager.js'; +import {MODULE_TYPE_UID} from '../src/activities/modules.js'; const ZEOTAP_COOKIE_NAME = 'IDP'; const ZEOTAP_VENDOR_ID = 301; @@ -21,7 +22,7 @@ function readFromLocalStorage() { } export function getStorage() { - return getStorageManager({gvlid: ZEOTAP_VENDOR_ID, moduleName: ZEOTAP_MODULE_NAME}); + return getStorageManager({moduleType: MODULE_TYPE_UID, moduleName: ZEOTAP_MODULE_NAME}); } export const storage = getStorage(); diff --git a/modules/zeta_global_sspBidAdapter.js b/modules/zeta_global_sspBidAdapter.js index 6c5b9783782..531384b9f27 100644 --- a/modules/zeta_global_sspBidAdapter.js +++ b/modules/zeta_global_sspBidAdapter.js @@ -3,9 +3,11 @@ import {registerBidder} from '../src/adapters/bidderFactory.js'; import {BANNER, VIDEO} from '../src/mediaTypes.js'; import {config} from '../src/config.js'; import {parseDomain} from '../src/refererDetection.js'; +import {ajax} from '../src/ajax.js'; const BIDDER_CODE = 'zeta_global_ssp'; const ENDPOINT_URL = 'https://ssp.disqus.com/bid/prebid'; +const TIMEOUT_URL = 'https://ssp.disqus.com/timeout/prebid'; const USER_SYNC_URL_IFRAME = 'https://ssp.disqus.com/sync?type=iframe'; const USER_SYNC_URL_IMAGE = 'https://ssp.disqus.com/sync?type=image'; const DEFAULT_CUR = 'USD'; @@ -75,6 +77,9 @@ export const spec = { id: request.bidId, secure: secure }; + if (params.tagid) { + impData.tagid = params.tagid; + } if (request.mediaTypes) { for (const mediaType in request.mediaTypes) { switch (mediaType) { @@ -90,6 +95,21 @@ export const spec = { if (!impData.banner && !impData.video) { impData.banner = buildBanner(request); } + + if (typeof request.getFloor === 'function') { + const floorInfo = request.getFloor({ + currency: 'USD', + mediaType: impData.video ? 'video' : 'banner', + size: [ impData.video ? impData.video.w : impData.banner.w, impData.video ? impData.video.h : impData.banner.h ] + }); + if (floorInfo && floorInfo.floor) { + impData.bidfloor = floorInfo.floor; + } + } + if (!impData.bidfloor && params.bidfloor) { + impData.bidfloor = params.bidfloor; + } + return impData; }); @@ -129,6 +149,15 @@ export const spec = { deepSetValue(payload, 'regs.ext.us_privacy', bidderRequest.uspConsent); } + // schain + if (validBidRequests[0].schain) { + payload.source = { + ext: { + schain: validBidRequests[0].schain + } + } + } + if (bidderRequest?.timeout) { payload.tmax = bidderRequest.timeout; } @@ -212,6 +241,18 @@ export const spec = { url: USER_SYNC_URL_IMAGE + syncurl }]; } + }, + + onTimeout: function(timeoutData) { + if (timeoutData) { + ajax(TIMEOUT_URL, null, JSON.stringify(timeoutData), { + method: 'POST', + options: { + withCredentials: false, + contentType: 'application/json' + } + }); + } } } diff --git a/modules/zeusPrimeRtdProvider.js b/modules/zeusPrimeRtdProvider.js index 28c46957c50..2a455a14f7b 100644 --- a/modules/zeusPrimeRtdProvider.js +++ b/modules/zeusPrimeRtdProvider.js @@ -1,328 +1,8 @@ -/** - * This module adds Zeus Insights For Publishers (ZIP) provider to the real time data module - * The {@link module:modules/realTimeData} module is required - * - * This module will request the article topics for the current page and add them as page keyvalues - * for the ad requests. - * - * @module modules/zeusInsightsForPublishersRtdProvider - * @requires module:modules/realTimeData - */ - -import { logInfo, logError, logWarn, logMessage } from '../src/utils.js' +import { logWarn } from '../src/utils.js' import { submodule } from '../src/hook.js' -import { ajaxBuilder } from '../src/ajax.js' - -class Logger { - get showDebug() { - if (this._showDebug === true || this._showDebug === false) { - return this._showDebug - } - - return window.zeusPrime?.debug || false - } - set showDebug(shouldShow) { - this._showDebug = shouldShow - } - - get error() { - return logError.bind(this, 'zeusPrimeRtdProvider: ') - } - get warn() { - return logWarn.bind(this, 'zeusPrimeRtdProvider: ') - } - get info() { - return logInfo.bind(this, 'zeusPrimeRtdProvider: ') - } - get debug() { - if (this.showDebug) { - return logMessage.bind(this, 'zeusPrimeRtdProvider: ') - } - - return () => {} - } -} - -var logger = new Logger() - -function loadCommandQueue() { - window.zeusPrime = window.zeusPrime || { cmd: [] } - const queue = [...window.zeusPrime.cmd] - - window.zeusPrime.cmd = [] - window.zeusPrime.cmd.push = (callback) => { - callback(window.zeusPrime) - } - - queue.forEach((callback) => callback(window.zeusPrime)) -} - -function markStatusComplete(key) { - const status = window?.zeusPrime?.status - if (status) { - status[key] = true - } -} - -function createStatus() { - if (window.zeusPrime && !window.zeusPrime.status) { - Object.defineProperty(window.zeusPrime, 'status', { - enumerable: false, - value: { - initComplete: false, - primeKeyValueSet: false, - insightsReqSent: false, - insightsReqReceived: false, - insightsKeyValueSet: false, - scriptComplete: false, - }, - }) - } -} - -function loadPrimeQueryParams() { - try { - const params = new URLSearchParams(window.location.search) - params.forEach((paramValue, paramKey) => { - if (!paramKey.startsWith('zeus_prime_')) { - return - } - - let key = paramKey.replace('zeus_prime_', '') - let value = paramValue.toLowerCase() - - if (value === 'true' || value === '1') { - value = true - } else if (value === 'false' || value === '0') { - value = false - } - - window.zeusPrime[key] = value - }) - } catch (_) {} -} - -const DEFAULT_API = 'https://insights.zeustechnology.com' - -function init(gamId = null, options = {}) { - window.zeusPrime = window.zeusPrime || { cmd: [] } - - window.zeusPrime.gamId = gamId || options.gamId || window.zeusPrime.gamId || undefined - window.zeusPrime.api = DEFAULT_API - window.zeusPrime.hostname = options.hostname || window.location?.hostname || '' - window.zeusPrime.pathname = options.pathname || window.location?.pathname || '' - window.zeusPrime.pageUrl = `${window.zeusPrime.hostname}${window.zeusPrime.pathname}` - window.zeusPrime.pageHash = options.pageHash || null - window.zeusPrime.debug = window.zeusPrime.debug || options.debug === true || false - window.zeusPrime.disabled = window.zeusPrime.disabled || options.disabled === true || false - - loadPrimeQueryParams() - - logger.showDebug = window.zeusPrime.debug - - createStatus() - markStatusComplete('initComplete') -} - -function setTargeting() { - const { gamId, hostname } = window.zeusPrime - - if (typeof gamId !== 'string') { - throw new Error(`window.zeusPrime.gamId must be a string. Received: ${String(gamId)}`) - } - - addKeyValueToGoogletag(`zeus_${gamId}`, hostname) - logger.debug(`Setting zeus_${gamId}=${hostname}`) - markStatusComplete('primeKeyValueSet') -} -function setPrimeAsDisabled() { - addKeyValueToGoogletag('zeus_prime', 'false') - logger.debug('Disabling prime; Setting key-value zeus_prime to false') -} - -function addKeyValueToGoogletag(key, value) { - window.googletag = window.googletag || { cmd: [] } - window.googletag.cmd.push(function () { - window.googletag.pubads().setTargeting(key, value) - }) -} - -function isInsightsPage(pathname = '') { - const NOT_SECTIONS = [ - { - test: /\/search/, - type: 'search', - }, - { - test: /\/author/, - type: 'author', - }, - { - test: /\/event/, - type: 'event', - }, - { - test: /\/homepage/, - type: 'front', - }, - { - test: /^\/?$/, - type: 'front', - }, - ] - - const typeObj = NOT_SECTIONS.find((pg) => pathname.match(pg.test)) - return typeObj === undefined -} - -async function getUrlHash(canonical) { - try { - const buf = await window.crypto.subtle.digest( - 'SHA-1', - new TextEncoder('utf-8').encode(canonical) - ) - const hashed = Array.prototype.map - .call(new Uint8Array(buf), (x) => `00${x.toString(16)}`.slice(-2)) - .join('') - - return hashed - } catch (e) { - logger.error('Failed to load hash', e.message) - logger.debug('Exception', e) - return '' - } -} - -async function sendPrebidRequest(url) { - return new Promise((resolve, reject) => { - const ajax = ajaxBuilder() - ajax(url, { - success: (responseText, response) => { - resolve({ - ...response, - status: response.status, - json: () => JSON.parse(responseText), - }) - }, - - error: (responseText, response) => { - if (!response.status) { - reject(response) - } - - let json = responseText - if (responseText) { - try { - json = JSON.parse(responseText) - } catch (_) { - json = null - } - } - - resolve({ - status: response.status, - json: () => json || null, - responseValue: json, - }) - }, - }) - }) -} - -async function requestTopics() { - const { api, hostname, pageUrl } = window.zeusPrime - - if (!window.zeusPrime.pageHash) { - window.zeusPrime.pageHash = await getUrlHash(pageUrl) - } - - const pageHash = window.zeusPrime.pageHash - const zeusInsightsUrl = `${api}/${hostname}/${pageHash}?article_location=${pageUrl}` - - logger.debug('Requesting topics', zeusInsightsUrl) - try { - markStatusComplete('insightsReqSent') - const response = await sendPrebidRequest(zeusInsightsUrl) - if (response.status === 200) { - logger.debug('topics found') - markStatusComplete('insightsReqReceived') - return await response.json() - } else if ( - response.status === 204 || - response.status < 200 || - (response.status >= 300 && response.status <= 399) - ) { - logger.debug('no topics found') - markStatusComplete('insightsReqReceived') - return null - } else { - logger.error(`Topics request returned error: ${response.status}`) - markStatusComplete('insightsReqReceived') - return null - } - } catch (e) { - logger.error('failed to request topics', e) - return null - } -} - -function setTopicsTargeting(topics = []) { - if (topics.length === 0) { - return - } - - window.googletag = window.googletag || { cmd: [] } - window.googletag.cmd.push(function () { - window.googletag.pubads().setTargeting('zeus_insights', topics) - }) - - markStatusComplete('insightsKeyValueSet') -} - -async function startTopicsRequest() { - if (isInsightsPage(window.zeusPrime.pathname)) { - const response = await requestTopics() - if (response) { - setTopicsTargeting(response?.topics) - } - } else { - logger.debug('This page is not eligible for topics, request will be skipped') - } -} - -async function run(gamId, options = {}) { - logger.showDebug = options.debug || false - - try { - init(gamId, options) - loadCommandQueue() - - if (window.zeusPrime.disabled) { - setPrimeAsDisabled() - } else { - setTargeting() - await startTopicsRequest() - } - } catch (e) { - logger.error('Failed to run.', e.message || e) - } finally { - markStatusComplete('scriptComplete') - } -} - -/** - * @preserve - * Initializes the ZeusPrime RTD Submodule. The config provides the GamID for this - * site that is used to configure Prime. - * @param {object} config The Prebid configuration for this module. - * @param {object} config.params The parameters for this module. - * @param {string} config.params.gamId The Gam ID (or Network Code) in GAM for this site. - */ -function initModule(config) { - const { params } = config || {} - const { gamId, ...rest } = params || {} - run(gamId, rest) +function initModule() { + logWarn('Zeus Prime has been deprecated. This module will be removed in Prebid 8.') } /** diff --git a/modules/zeusPrimeRtdProvider.md b/modules/zeusPrimeRtdProvider.md index f3a6c5018d5..40b44e76f1c 100644 --- a/modules/zeusPrimeRtdProvider.md +++ b/modules/zeusPrimeRtdProvider.md @@ -1,5 +1,8 @@ # Overview +# NOTE: ZEUS PRIME HAS BEEN DEPRECATED! +# THIS MODULE WILL BE REMOVED IN PREBID 8. + Module Name: Zeus Prime RTD Provider Module Type: Rtd Provider Maintainer: support@zeustechnology.com diff --git a/package-lock.json b/package-lock.json index 006b8bcdcab..54442c348e0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "prebid.js", - "version": "7.29.0-pre", + "version": "7.46.0-pre", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "prebid.js", - "version": "7.28.0-pre", + "version": "7.42.0-pre", "license": "Apache-2.0", "dependencies": { "@babel/core": "^7.16.7", @@ -22,7 +22,7 @@ "express": "^4.15.4", "fun-hooks": "^0.9.9", "just-clone": "^1.0.2", - "live-connect-js": "3.0.1" + "live-connect-js": "^5.0.0" }, "devDependencies": { "@babel/eslint-parser": "^7.16.5", @@ -2797,9 +2797,9 @@ } }, "node_modules/@wdio/browserstack-service/node_modules/ua-parser-js": { - "version": "1.0.32", - "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-1.0.32.tgz", - "integrity": "sha512-dXVsz3M4j+5tTiovFVyVqssXBu5HM47//YSOeZ9fQkdDKkfzv2v3PP1jmH6FUyPW+yCSn7aBVK1fGGKNhowdDA==", + "version": "1.0.33", + "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-1.0.33.tgz", + "integrity": "sha512-RqshF7TPTE0XLYAqmjlu5cLLuGdKrNu9O1KLA/qp39QtbZwuzwv1dT46DZSopoUMsYgXpB3Cv8a03FI8b74oFQ==", "dev": true, "funding": [ { @@ -8453,9 +8453,9 @@ } }, "node_modules/devtools/node_modules/ua-parser-js": { - "version": "1.0.32", - "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-1.0.32.tgz", - "integrity": "sha512-dXVsz3M4j+5tTiovFVyVqssXBu5HM47//YSOeZ9fQkdDKkfzv2v3PP1jmH6FUyPW+yCSn7aBVK1fGGKNhowdDA==", + "version": "1.0.33", + "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-1.0.33.tgz", + "integrity": "sha512-RqshF7TPTE0XLYAqmjlu5cLLuGdKrNu9O1KLA/qp39QtbZwuzwv1dT46DZSopoUMsYgXpB3Cv8a03FI8b74oFQ==", "dev": true, "funding": [ { @@ -8534,9 +8534,9 @@ } }, "node_modules/documentation": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/documentation/-/documentation-14.0.0.tgz", - "integrity": "sha512-4AwFzdiseEdtqR0KKLrruIQ5fvh7n5epg47P0ZyOidA5Fes5am+6xjqkDECHPwcv4pxJ6zITaHwzCoGblP0+JQ==", + "version": "14.0.1", + "resolved": "https://registry.npmjs.org/documentation/-/documentation-14.0.1.tgz", + "integrity": "sha512-Y/brACCE3sNnDJPFiWlhXrqGY+NelLYVZShLGse5bT1KdohP4JkPf5T2KNq1YWhIEbDYl/1tebRLC0WYbPQxVw==", "dev": true, "dependencies": { "@babel/core": "^7.18.10", @@ -8548,7 +8548,7 @@ "chokidar": "^3.5.3", "diff": "^5.1.0", "doctrine-temporary-fork": "2.1.0", - "git-url-parse": "^12.0.0", + "git-url-parse": "^13.1.0", "github-slugger": "1.4.0", "glob": "^8.0.3", "globals-docs": "^2.4.1", @@ -11261,22 +11261,22 @@ } }, "node_modules/git-up": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/git-up/-/git-up-6.0.0.tgz", - "integrity": "sha512-6RUFSNd1c/D0xtGnyWN2sxza2bZtZ/EmI9448n6rCZruFwV/ezeEn2fJP7XnUQGwf0RAtd/mmUCbtH6JPYA2SA==", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/git-up/-/git-up-7.0.0.tgz", + "integrity": "sha512-ONdIrbBCFusq1Oy0sC71F5azx8bVkvtZtMJAsv+a6lz5YAmbNnLD6HAB4gptHZVLPR8S2/kVN6Gab7lryq5+lQ==", "dev": true, "dependencies": { "is-ssh": "^1.4.0", - "parse-url": "^7.0.2" + "parse-url": "^8.1.0" } }, "node_modules/git-url-parse": { - "version": "12.0.0", - "resolved": "https://registry.npmjs.org/git-url-parse/-/git-url-parse-12.0.0.tgz", - "integrity": "sha512-I6LMWsxV87vysX1WfsoglXsXg6GjQRKq7+Dgiseo+h0skmp5Hp2rzmcEIRQot9CPA+uzU7x1x7jZdqvTFGnB+Q==", + "version": "13.1.0", + "resolved": "https://registry.npmjs.org/git-url-parse/-/git-url-parse-13.1.0.tgz", + "integrity": "sha512-5FvPJP/70WkIprlUZ33bm4UAaFdjcLkJLpWft1BeZKqwR0uhhNGoKwlUaPtVb4LxCSQ++erHapRak9kWGj+FCA==", "dev": true, "dependencies": { - "git-up": "^6.0.0" + "git-up": "^7.0.0" } }, "node_modules/github-slugger": { @@ -13818,9 +13818,9 @@ } }, "node_modules/http-cache-semantics": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz", - "integrity": "sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz", + "integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==", "dev": true }, "node_modules/http-errors": { @@ -16228,11 +16228,20 @@ "integrity": "sha512-3mk/Zag0+IJxeDrxSgaDPy4zZ3w05PRZeJNnlWhzFz5OkX49J4krc+A8X2d2M69vGMBEX0uyl8M+W+8gH+kBqQ==", "dev": true }, + "node_modules/live-connect-common": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/live-connect-common/-/live-connect-common-1.0.0.tgz", + "integrity": "sha512-LBZsvykcGeVRYI1eqqXrrNZsoBdL2a8cpyrYPIiGAF/CpixbyRbvqGslaFw511lH294QB16J3fYYg21aYuaM2Q==", + "engines": { + "node": ">=8" + } + }, "node_modules/live-connect-js": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/live-connect-js/-/live-connect-js-3.0.1.tgz", - "integrity": "sha512-rwB0IQfuKPJM+I96nLyq8Utr3LkQ7Z/iuq/xKlWDckQRJLYyWkk7F7yaavf/VsjazzLK2dpJeXGijoDkK4Vz8g==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/live-connect-js/-/live-connect-js-5.0.0.tgz", + "integrity": "sha512-Bv0wQQ+/1VU0/YczEpObbWtHbuXwaHGxwg1+Pe7ZlDgBLb334CrqSQvOL1uyZw3//zs+fSO94yYaQzjjkTd5OQ==", "dependencies": { + "live-connect-common": "^1.0.0", "tiny-hashes": "1.0.1" }, "engines": { @@ -19420,24 +19429,21 @@ } }, "node_modules/parse-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/parse-path/-/parse-path-5.0.0.tgz", - "integrity": "sha512-qOpH55/+ZJ4jUu/oLO+ifUKjFPNZGfnPJtzvGzKN/4oLMil5m9OH4VpOj6++9/ytJcfks4kzH2hhi87GL/OU9A==", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/parse-path/-/parse-path-7.0.0.tgz", + "integrity": "sha512-Euf9GG8WT9CdqwuWJGdf3RkUcTBArppHABkO7Lm8IzRQp0e2r/kkFnmhu4TSK30Wcu5rVAZLmfPKSBBi9tWFog==", "dev": true, "dependencies": { "protocols": "^2.0.0" } }, "node_modules/parse-url": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/parse-url/-/parse-url-7.0.2.tgz", - "integrity": "sha512-PqO4Z0eCiQ08Wj6QQmrmp5YTTxpYfONdOEamrtvK63AmzXpcavIVQubGHxOEwiIoDZFb8uDOoQFS0NCcjqIYQg==", + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/parse-url/-/parse-url-8.1.0.tgz", + "integrity": "sha512-xDvOoLU5XRrcOZvnI6b8zA6n9O9ejNk/GExuz1yBuWUGn9KA97GI6HTs6u02wKara1CeVmZhH+0TZFdWScR89w==", "dev": true, "dependencies": { - "is-ssh": "^1.4.0", - "normalize-url": "^6.1.0", - "parse-path": "^5.0.0", - "protocols": "^2.0.1" + "parse-path": "^7.0.0" } }, "node_modules/parseurl": { @@ -22965,9 +22971,9 @@ } }, "node_modules/ua-parser-js": { - "version": "0.7.32", - "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.32.tgz", - "integrity": "sha512-f9BESNVhzlhEFf2CHMSj40NWOjYPl1YKYbrvIr/hFTDEmLq7SRbWvm7FcdcpCYT95zrOhC7gZSxjdnnTpBcwVw==", + "version": "0.7.33", + "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.33.tgz", + "integrity": "sha512-s8ax/CeZdK9R/56Sui0WM6y9OFREJarMRHqLB2EwkovemBxNQ+Bqu8GAsUnVcXKgphb++ghr/B2BZx4mahujPw==", "dev": true, "funding": [ { @@ -24338,9 +24344,9 @@ "dev": true }, "node_modules/webpack": { - "version": "5.74.0", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.74.0.tgz", - "integrity": "sha512-A2InDwnhhGN4LYctJj6M1JEaGL7Luj6LOmyBHjcI8529cm5p6VXiTIW2sn6ffvEAKmveLzvu4jrihwXtPojlAA==", + "version": "5.76.0", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.76.0.tgz", + "integrity": "sha512-l5sOdYBDunyf72HW8dF23rFtWq/7Zgvt/9ftMof71E/yUb1YLOBmTgA2K4vQthB3kotMrSj609txVE0dnr2fjA==", "dev": true, "dependencies": { "@types/eslint-scope": "^3.7.3", @@ -27308,9 +27314,9 @@ } }, "ua-parser-js": { - "version": "1.0.32", - "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-1.0.32.tgz", - "integrity": "sha512-dXVsz3M4j+5tTiovFVyVqssXBu5HM47//YSOeZ9fQkdDKkfzv2v3PP1jmH6FUyPW+yCSn7aBVK1fGGKNhowdDA==", + "version": "1.0.33", + "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-1.0.33.tgz", + "integrity": "sha512-RqshF7TPTE0XLYAqmjlu5cLLuGdKrNu9O1KLA/qp39QtbZwuzwv1dT46DZSopoUMsYgXpB3Cv8a03FI8b74oFQ==", "dev": true }, "uuid": { @@ -31721,9 +31727,9 @@ } }, "ua-parser-js": { - "version": "1.0.32", - "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-1.0.32.tgz", - "integrity": "sha512-dXVsz3M4j+5tTiovFVyVqssXBu5HM47//YSOeZ9fQkdDKkfzv2v3PP1jmH6FUyPW+yCSn7aBVK1fGGKNhowdDA==", + "version": "1.0.33", + "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-1.0.33.tgz", + "integrity": "sha512-RqshF7TPTE0XLYAqmjlu5cLLuGdKrNu9O1KLA/qp39QtbZwuzwv1dT46DZSopoUMsYgXpB3Cv8a03FI8b74oFQ==", "dev": true }, "uuid": { @@ -31782,9 +31788,9 @@ } }, "documentation": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/documentation/-/documentation-14.0.0.tgz", - "integrity": "sha512-4AwFzdiseEdtqR0KKLrruIQ5fvh7n5epg47P0ZyOidA5Fes5am+6xjqkDECHPwcv4pxJ6zITaHwzCoGblP0+JQ==", + "version": "14.0.1", + "resolved": "https://registry.npmjs.org/documentation/-/documentation-14.0.1.tgz", + "integrity": "sha512-Y/brACCE3sNnDJPFiWlhXrqGY+NelLYVZShLGse5bT1KdohP4JkPf5T2KNq1YWhIEbDYl/1tebRLC0WYbPQxVw==", "dev": true, "requires": { "@babel/core": "^7.18.10", @@ -31797,7 +31803,7 @@ "chokidar": "^3.5.3", "diff": "^5.1.0", "doctrine-temporary-fork": "2.1.0", - "git-url-parse": "^12.0.0", + "git-url-parse": "^13.1.0", "github-slugger": "1.4.0", "glob": "^8.0.3", "globals-docs": "^2.4.1", @@ -33988,22 +33994,22 @@ } }, "git-up": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/git-up/-/git-up-6.0.0.tgz", - "integrity": "sha512-6RUFSNd1c/D0xtGnyWN2sxza2bZtZ/EmI9448n6rCZruFwV/ezeEn2fJP7XnUQGwf0RAtd/mmUCbtH6JPYA2SA==", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/git-up/-/git-up-7.0.0.tgz", + "integrity": "sha512-ONdIrbBCFusq1Oy0sC71F5azx8bVkvtZtMJAsv+a6lz5YAmbNnLD6HAB4gptHZVLPR8S2/kVN6Gab7lryq5+lQ==", "dev": true, "requires": { "is-ssh": "^1.4.0", - "parse-url": "^7.0.2" + "parse-url": "^8.1.0" } }, "git-url-parse": { - "version": "12.0.0", - "resolved": "https://registry.npmjs.org/git-url-parse/-/git-url-parse-12.0.0.tgz", - "integrity": "sha512-I6LMWsxV87vysX1WfsoglXsXg6GjQRKq7+Dgiseo+h0skmp5Hp2rzmcEIRQot9CPA+uzU7x1x7jZdqvTFGnB+Q==", + "version": "13.1.0", + "resolved": "https://registry.npmjs.org/git-url-parse/-/git-url-parse-13.1.0.tgz", + "integrity": "sha512-5FvPJP/70WkIprlUZ33bm4UAaFdjcLkJLpWft1BeZKqwR0uhhNGoKwlUaPtVb4LxCSQ++erHapRak9kWGj+FCA==", "dev": true, "requires": { - "git-up": "^6.0.0" + "git-up": "^7.0.0" } }, "github-slugger": { @@ -36048,9 +36054,9 @@ "dev": true }, "http-cache-semantics": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz", - "integrity": "sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz", + "integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==", "dev": true }, "http-errors": { @@ -37867,11 +37873,17 @@ "integrity": "sha512-3mk/Zag0+IJxeDrxSgaDPy4zZ3w05PRZeJNnlWhzFz5OkX49J4krc+A8X2d2M69vGMBEX0uyl8M+W+8gH+kBqQ==", "dev": true }, + "live-connect-common": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/live-connect-common/-/live-connect-common-1.0.0.tgz", + "integrity": "sha512-LBZsvykcGeVRYI1eqqXrrNZsoBdL2a8cpyrYPIiGAF/CpixbyRbvqGslaFw511lH294QB16J3fYYg21aYuaM2Q==" + }, "live-connect-js": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/live-connect-js/-/live-connect-js-3.0.1.tgz", - "integrity": "sha512-rwB0IQfuKPJM+I96nLyq8Utr3LkQ7Z/iuq/xKlWDckQRJLYyWkk7F7yaavf/VsjazzLK2dpJeXGijoDkK4Vz8g==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/live-connect-js/-/live-connect-js-5.0.0.tgz", + "integrity": "sha512-Bv0wQQ+/1VU0/YczEpObbWtHbuXwaHGxwg1+Pe7ZlDgBLb334CrqSQvOL1uyZw3//zs+fSO94yYaQzjjkTd5OQ==", "requires": { + "live-connect-common": "^1.0.0", "tiny-hashes": "1.0.1" } }, @@ -40244,24 +40256,21 @@ "dev": true }, "parse-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/parse-path/-/parse-path-5.0.0.tgz", - "integrity": "sha512-qOpH55/+ZJ4jUu/oLO+ifUKjFPNZGfnPJtzvGzKN/4oLMil5m9OH4VpOj6++9/ytJcfks4kzH2hhi87GL/OU9A==", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/parse-path/-/parse-path-7.0.0.tgz", + "integrity": "sha512-Euf9GG8WT9CdqwuWJGdf3RkUcTBArppHABkO7Lm8IzRQp0e2r/kkFnmhu4TSK30Wcu5rVAZLmfPKSBBi9tWFog==", "dev": true, "requires": { "protocols": "^2.0.0" } }, "parse-url": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/parse-url/-/parse-url-7.0.2.tgz", - "integrity": "sha512-PqO4Z0eCiQ08Wj6QQmrmp5YTTxpYfONdOEamrtvK63AmzXpcavIVQubGHxOEwiIoDZFb8uDOoQFS0NCcjqIYQg==", + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/parse-url/-/parse-url-8.1.0.tgz", + "integrity": "sha512-xDvOoLU5XRrcOZvnI6b8zA6n9O9ejNk/GExuz1yBuWUGn9KA97GI6HTs6u02wKara1CeVmZhH+0TZFdWScR89w==", "dev": true, "requires": { - "is-ssh": "^1.4.0", - "normalize-url": "^6.1.0", - "parse-path": "^5.0.0", - "protocols": "^2.0.1" + "parse-path": "^7.0.0" } }, "parseurl": { @@ -43064,9 +43073,9 @@ } }, "ua-parser-js": { - "version": "0.7.32", - "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.32.tgz", - "integrity": "sha512-f9BESNVhzlhEFf2CHMSj40NWOjYPl1YKYbrvIr/hFTDEmLq7SRbWvm7FcdcpCYT95zrOhC7gZSxjdnnTpBcwVw==", + "version": "0.7.33", + "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.33.tgz", + "integrity": "sha512-s8ax/CeZdK9R/56Sui0WM6y9OFREJarMRHqLB2EwkovemBxNQ+Bqu8GAsUnVcXKgphb++ghr/B2BZx4mahujPw==", "dev": true }, "uglify-js": { @@ -44138,9 +44147,9 @@ "dev": true }, "webpack": { - "version": "5.74.0", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.74.0.tgz", - "integrity": "sha512-A2InDwnhhGN4LYctJj6M1JEaGL7Luj6LOmyBHjcI8529cm5p6VXiTIW2sn6ffvEAKmveLzvu4jrihwXtPojlAA==", + "version": "5.76.0", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.76.0.tgz", + "integrity": "sha512-l5sOdYBDunyf72HW8dF23rFtWq/7Zgvt/9ftMof71E/yUb1YLOBmTgA2K4vQthB3kotMrSj609txVE0dnr2fjA==", "dev": true, "requires": { "@types/eslint-scope": "^3.7.3", diff --git a/package.json b/package.json index 5ee4cb728b1..7ebcb655ed7 100644 --- a/package.json +++ b/package.json @@ -1,9 +1,10 @@ { "name": "prebid.js", - "version": "7.29.0-pre", + "version": "7.46.0-pre", "description": "Header Bidding Management Library", "main": "src/prebid.js", "scripts": { + "serve": "gulp serve", "test": "gulp test", "lint": "gulp lint" }, @@ -28,6 +29,7 @@ "prebid" ], "globalVarName": "pbjs", + "defineGlobal": true, "author": "the prebid.js contributors", "license": "Apache-2.0", "engines": { @@ -130,7 +132,7 @@ "express": "^4.15.4", "fun-hooks": "^0.9.9", "just-clone": "^1.0.2", - "live-connect-js": "3.0.1" + "live-connect-js": "^5.0.0" }, "optionalDependencies": { "fsevents": "^2.3.2" diff --git a/plugins/eslint/validateImports.js b/plugins/eslint/validateImports.js index 37a87fffb50..324b75c4ce7 100644 --- a/plugins/eslint/validateImports.js +++ b/plugins/eslint/validateImports.js @@ -1,11 +1,17 @@ -let path = require('path'); -let _ = require('lodash'); -let resolveFrom = require('resolve-from'); +const path = require('path'); +const _ = require('lodash'); +const resolveFrom = require('resolve-from'); +const MODULES_PATH = path.resolve(__dirname, '../../modules'); + +function isInDirectory(filename, dir) { + const rel = path.relative(dir, filename); + return rel && !rel.startsWith('..') && !path.isAbsolute(rel); +} function flagErrors(context, node, importPath) { let absFileDir = path.dirname(context.getFilename()); - let absImportPath = path.resolve(absFileDir, importPath); + let absImportPath = importPath.startsWith('.') ? path.resolve(absFileDir, importPath) : require.resolve(importPath); try { resolveFrom(absFileDir, importPath); @@ -20,16 +26,9 @@ function flagErrors(context, node, importPath) { ) { context.report(node, `import "${importPath}" not in import whitelist`); } else { - let absModulePath = path.resolve(__dirname, '../../modules'); - - // don't allow import of any files directly within modules folder or index.js files within modules' sub-folders - if ( - path.dirname(absImportPath) === absModulePath || ( - absImportPath.startsWith(absModulePath) && - path.basename(absImportPath) === 'index.js' - ) - ) { - context.report(node, `import "${importPath}" cannot require module entry point`); + // do not allow cross-module imports + if (isInDirectory(absImportPath, MODULES_PATH) && (!isInDirectory(absImportPath, absFileDir) || absFileDir === MODULES_PATH)) { + context.report(node, `import "${importPath}": importing from modules is not allowed`); } // don't allow extension-less local imports diff --git a/plugins/pbjsGlobals.js b/plugins/pbjsGlobals.js index c1a08133e8e..6d1eeb0c57d 100644 --- a/plugins/pbjsGlobals.js +++ b/plugins/pbjsGlobals.js @@ -1,4 +1,3 @@ - let t = require('@babel/core').types; let prebid = require('../package.json'); const path = require('path'); @@ -28,10 +27,12 @@ function getNpmVersion(version) { module.exports = function(api, options) { const pbGlobal = options.globalVarName || prebid.globalVarName; + const defineGlobal = typeof (options.defineGlobal) !== 'undefined' ? options.defineGlobal : prebid.defineGlobal; const features = featureMap(options.disableFeatures); let replace = { '$prebid.version$': prebid.version, '$$PREBID_GLOBAL$$': pbGlobal, + '$$DEFINE_PREBID_GLOBAL$$': defineGlobal, '$$REPO_AND_VERSION$$': `${prebid.repository.url.split('/')[3]}_prebid_${prebid.version}`, '$$PREBID_DIST_URL_BASE$$': options.prebidDistUrlBase || `https://cdn.jsdelivr.net/npm/prebid.js@${getNpmVersion(prebid.version)}/dist/` }; @@ -41,6 +42,12 @@ module.exports = function(api, options) { ]; const PREBID_ROOT = path.resolve(__dirname, '..'); + // on Windows, require paths are not filesystem paths + const SEP_PAT = new RegExp(path.sep.replace(/\\/g, '\\\\'), 'g') + + function relPath(from, toRelToProjectRoot) { + return path.relative(path.dirname(from), path.join(PREBID_ROOT, toRelToProjectRoot)).replace(SEP_PAT, '/'); + } function getModuleName(filename) { const modPath = path.parse(path.relative(PREBID_ROOT, filename)); @@ -63,8 +70,14 @@ module.exports = function(api, options) { Program(path, state) { const modName = getModuleName(state.filename); if (modName != null) { - // append "registration" of module file to $$PREBID_GLOBAL$$.installedModules - path.node.body.push(...api.parse(`window.${pbGlobal}.installedModules.push('${modName}');`, {filename: state.filename}).program.body); + // append "registration" of module file to getGlobal().installedModules + let i = 0; + let registerName; + do { + registerName = `__r${i++}` + } while (path.scope.hasBinding(registerName)) + path.node.body.unshift(...api.parse(`import {registerModule as ${registerName}} from '${relPath(state.filename, 'src/prebidGlobal.js')}';`, {filename: state.filename}).program.body); + path.node.body.push(...api.parse(`${registerName}('${modName}');`, {filename: state.filename}).program.body); } }, StringLiteral(path) { @@ -72,7 +85,7 @@ module.exports = function(api, options) { if (path.node.value.includes(name)) { path.node.value = path.node.value.replace( new RegExp(escapeRegExp(name), 'g'), - replace[name] + replace[name].toString() ); } }); @@ -102,7 +115,7 @@ module.exports = function(api, options) { ); } else { path.replaceWith( - t.Identifier(replace[name]) + t.Identifier(replace[name].toString()) ); } } diff --git a/src/Renderer.js b/src/Renderer.js index 97e37084e89..2f9b2e025cb 100644 --- a/src/Renderer.js +++ b/src/Renderer.js @@ -3,6 +3,9 @@ import { logError, logWarn, logMessage, deepAccess } from './utils.js'; import {find} from './polyfill.js'; +import {getGlobal} from './prebidGlobal.js'; + +const pbjsInstance = getGlobal(); const moduleCode = 'outstream'; /** @@ -19,6 +22,7 @@ export function Renderer(options) { this.config = config; this.handlers = {}; this.id = id; + this.renderNow = renderNow; // a renderer may push to the command queue to delay rendering until the // render function is loaded by loadExternalScript, at which point the the command @@ -107,7 +111,7 @@ Renderer.prototype.process = function() { * @returns {Boolean} */ export function isRendererRequired(renderer) { - return !!(renderer && renderer.url); + return !!(renderer && (renderer.url || renderer.renderNow)); } /** @@ -129,7 +133,7 @@ export function executeRenderer(renderer, bid, doc) { } function isRendererPreferredFromAdUnit(adUnitCode) { - const adUnits = $$PREBID_GLOBAL$$.adUnits; + const adUnits = pbjsInstance.adUnits; const adUnit = find(adUnits, adUnit => { return adUnit.code === adUnitCode; }); diff --git a/src/activities/modules.js b/src/activities/modules.js new file mode 100644 index 00000000000..d140b10387f --- /dev/null +++ b/src/activities/modules.js @@ -0,0 +1,5 @@ +export const MODULE_TYPE_CORE = 'core'; +export const MODULE_TYPE_BIDDER = 'bidder'; +export const MODULE_TYPE_UID = 'userId'; +export const MODULE_TYPE_RTD = 'rtd'; +export const MODULE_TYPE_ANALYTICS = 'analytics'; diff --git a/src/adapterManager.js b/src/adapterManager.js index eb6a74a82a4..45438f59b55 100644 --- a/src/adapterManager.js +++ b/src/adapterManager.js @@ -31,11 +31,12 @@ import {hook} from './hook.js'; import {find, includes} from './polyfill.js'; import {adunitCounter} from './adUnits.js'; import {getRefererInfo} from './refererDetection.js'; -import {GdprConsentHandler, UspConsentHandler} from './consentHandler.js'; +import {GdprConsentHandler, UspConsentHandler, GppConsentHandler, GDPR_GVLIDS} from './consentHandler.js'; import * as events from './events.js'; import CONSTANTS from './constants.json'; import {useMetrics} from './utils/perfMetrics.js'; import {auctionManager} from './auctionManager.js'; +import {MODULE_TYPE_ANALYTICS, MODULE_TYPE_BIDDER} from './activities/modules.js'; export const PARTITIONS = { CLIENT: 'client', @@ -65,15 +66,21 @@ var _analyticsRegistry = {}; function getBids({bidderCode, auctionId, bidderRequestId, adUnits, src, metrics}) { return adUnits.reduce((result, adUnit) => { - result.push(adUnit.bids.filter(bid => bid.bidder === bidderCode) - .reduce((bids, bid) => { - bid = Object.assign({}, bid, getDefinedParams(adUnit, [ - 'nativeParams', - 'nativeOrtbRequest', - 'ortb2Imp', - 'mediaType', - 'renderer' - ])); + const bids = adUnit.bids.filter(bid => bid.bidder === bidderCode); + if (bidderCode == null && bids.length === 0 && adUnit.s2sBid != null) { + bids.push({bidder: null}); + } + result.push( + bids.reduce((bids, bid) => { + bid = Object.assign({}, bid, + {ortb2Imp: mergeDeep({}, adUnit.ortb2Imp, bid.ortb2Imp)}, + getDefinedParams(adUnit, [ + 'nativeParams', + 'nativeOrtbRequest', + 'mediaType', + 'renderer' + ]) + ); const mediaTypes = bid.mediaTypes == null ? adUnit.mediaTypes : bid.mediaTypes @@ -128,9 +135,18 @@ export const filterBidsForAdUnit = hook('sync', _filterBidsForAdUnit, 'filterBid function getAdUnitCopyForPrebidServer(adUnits, s2sConfig) { let adUnitsCopy = deepClone(adUnits); + let hasModuleBids = false; adUnitsCopy.forEach((adUnit) => { // filter out client side bids + const s2sBids = adUnit.bids.filter((b) => b.module === 'pbsBidAdapter' && b.params?.configName === s2sConfig.configName); + if (s2sBids.length === 1) { + adUnit.s2sBid = s2sBids[0]; + hasModuleBids = true; + adUnit.ortb2Imp = mergeDeep({}, adUnit.s2sBid.ortb2Imp, adUnit.ortb2Imp); + } else if (s2sBids.length > 1) { + logWarn('Multiple "module" bids for the same s2s configuration; all will be ignored', s2sBids); + } adUnit.bids = filterBidsForAdUnit(adUnit.bids, s2sConfig) .map((bid) => { bid.bid_id = getUniqueIdentifierStr(); @@ -140,9 +156,9 @@ function getAdUnitCopyForPrebidServer(adUnits, s2sConfig) { // don't send empty requests adUnitsCopy = adUnitsCopy.filter(adUnit => { - return adUnit.bids.length !== 0; + return adUnit.bids.length !== 0 || adUnit.s2sBid != null; }); - return adUnitsCopy; + return {adUnits: adUnitsCopy, hasModuleBids}; } function getAdUnitCopyForClientAdapters(adUnits) { @@ -161,6 +177,7 @@ function getAdUnitCopyForClientAdapters(adUnits) { export let gdprDataHandler = new GdprConsentHandler(); export let uspDataHandler = new UspConsentHandler(); +export let gppDataHandler = new GppConsentHandler(); export let coppaDataHandler = { getCoppa: function() { @@ -243,11 +260,12 @@ adapterManager.makeBidRequests = hook('sync', function (adUnits, auctionStart, a _s2sConfigs.forEach(s2sConfig => { if (s2sConfig && s2sConfig.enabled) { - let adUnitsS2SCopy = getAdUnitCopyForPrebidServer(adUnits, s2sConfig); + let {adUnits: adUnitsS2SCopy, hasModuleBids} = getAdUnitCopyForPrebidServer(adUnits, s2sConfig); // uniquePbsTid is so we know which server to send which bids to during the callBids function let uniquePbsTid = generateUUID(); - serverBidders.forEach(bidderCode => { + + (serverBidders.length === 0 && hasModuleBids ? [null] : serverBidders).forEach(bidderCode => { const bidderRequestId = getUniqueIdentifierStr(); const metrics = auctionMetrics.fork(); const bidderRequest = addOrtb2({ @@ -278,7 +296,7 @@ adapterManager.makeBidRequests = hook('sync', function (adUnits, auctionStart, a bidRequests.forEach(request => { if (request.adUnitsS2SCopy === undefined) { - request.adUnitsS2SCopy = adUnitsS2SCopy.filter(adUnitCopy => adUnitCopy.bids.length > 0); + request.adUnitsS2SCopy = adUnitsS2SCopy.filter(au => au.bids.length > 0 || au.s2sBid != null); } }); } @@ -309,23 +327,16 @@ adapterManager.makeBidRequests = hook('sync', function (adUnits, auctionStart, a } }); - if (gdprDataHandler.getConsentData()) { - bidRequests.forEach(bidRequest => { + bidRequests.forEach(bidRequest => { + if (gdprDataHandler.getConsentData()) { bidRequest['gdprConsent'] = gdprDataHandler.getConsentData(); - }); - } - - if (uspDataHandler.getConsentData()) { - bidRequests.forEach(bidRequest => { + } + if (uspDataHandler.getConsentData()) { bidRequest['uspConsent'] = uspDataHandler.getConsentData(); - }); - } - - bidRequests.forEach(bidRequest => { - config.runWithBidder(bidRequest.bidderCode, () => { - const fledgeEnabledFromConfig = config.getConfig('fledgeEnabled'); - bidRequest['fledgeEnabled'] = navigator.runAdAuction && fledgeEnabledFromConfig - }); + } + if (gppDataHandler.getConsentData()) { + bidRequest['gppConsent'] = gppDataHandler.getConsentData(); + } }); return bidRequests; @@ -442,7 +453,7 @@ adapterManager.callBids = (adUnits, bidRequests, addBidResponse, doneCb, request function getSupportedMediaTypes(bidderCode) { let supportedMediaTypes = []; - if (includes(adapterManager.videoAdapters, bidderCode)) supportedMediaTypes.push('video'); + if (FEATURES.VIDEO && includes(adapterManager.videoAdapters, bidderCode)) supportedMediaTypes.push('video'); if (FEATURES.NATIVE && includes(nativeAdapters, bidderCode)) supportedMediaTypes.push('native'); return supportedMediaTypes; } @@ -453,8 +464,9 @@ adapterManager.registerBidAdapter = function (bidAdapter, bidderCode, {supported if (bidAdapter && bidderCode) { if (typeof bidAdapter.callBids === 'function') { _bidderRegistry[bidderCode] = bidAdapter; + GDPR_GVLIDS.register(MODULE_TYPE_BIDDER, bidderCode, bidAdapter.getSpec?.().gvlid); - if (includes(supportedMediaTypes, 'video')) { + if (FEATURES.VIDEO && includes(supportedMediaTypes, 'video')) { adapterManager.videoAdapters.push(bidderCode); } if (FEATURES.NATIVE && includes(supportedMediaTypes, 'native')) { @@ -522,6 +534,7 @@ adapterManager.registerAnalyticsAdapter = function ({adapter, code, gvlid}) { if (typeof adapter.enableAnalytics === 'function') { adapter.code = code; _analyticsRegistry[code] = { adapter, gvlid }; + GDPR_GVLIDS.register(MODULE_TYPE_ANALYTICS, code, gvlid); } else { logError(`Prebid Error: Analytics adaptor error for analytics "${code}" analytics adapter must implement an enableAnalytics() function`); diff --git a/src/adapters/bidderFactory.js b/src/adapters/bidderFactory.js index 7e66b849783..53d2a4d3ca6 100644 --- a/src/adapters/bidderFactory.js +++ b/src/adapters/bidderFactory.js @@ -1,20 +1,32 @@ import Adapter from '../adapter.js'; import adapterManager from '../adapterManager.js'; -import { config } from '../config.js'; -import { createBid } from '../bidfactory.js'; -import { userSync } from '../userSync.js'; -import { nativeBidIsValid } from '../native.js'; -import { isValidVideoBid } from '../video.js'; +import {config} from '../config.js'; +import {createBid} from '../bidfactory.js'; +import {userSync} from '../userSync.js'; +import {nativeBidIsValid} from '../native.js'; +import {isValidVideoBid} from '../video.js'; import CONSTANTS from '../constants.json'; import * as events from '../events.js'; import {includes} from '../polyfill.js'; -import { ajax } from '../ajax.js'; -import { logWarn, logInfo, logError, parseQueryStringParameters, delayExecution, parseSizesInput, flatten, uniques, timestamp, deepAccess, isArray, isPlainObject } from '../utils.js'; -import { ADPOD } from '../mediaTypes.js'; -import { getHook, hook } from '../hook.js'; -import { getCoreStorageManager } from '../storageManager.js'; +import {ajax} from '../ajax.js'; +import { + deepAccess, + delayExecution, + flatten, + isArray, + isPlainObject, + logError, + logWarn, + parseQueryStringParameters, + parseSizesInput, + timestamp, + uniques +} from '../utils.js'; +import {ADPOD} from '../mediaTypes.js'; +import {getHook, hook} from '../hook.js'; +import {getCoreStorageManager} from '../storageManager.js'; import {auctionManager} from '../auctionManager.js'; -import { bidderSettings } from '../bidderSettings.js'; +import {bidderSettings} from '../bidderSettings.js'; import {useMetrics} from '../utils/perfMetrics.js'; export const storage = getCoreStorageManager('bidderFactory'); @@ -187,7 +199,7 @@ export function registerBidder(spec) { export function newBidder(spec) { return Object.assign(new Adapter(spec.code), { getSpec: function() { - return Object.freeze(spec); + return Object.freeze(Object.assign({}, spec)); }, registerSyncs, callBids: function(bidderRequest, addBidResponse, done, ajax, onTimelyResponse, configEnabledCallback) { @@ -214,7 +226,7 @@ export function newBidder(spec) { done(); config.runWithBidder(spec.code, () => { events.emit(CONSTANTS.EVENTS.BIDDER_DONE, bidderRequest); - registerSyncs(responses, bidderRequest.gdprConsent, bidderRequest.uspConsent); + registerSyncs(responses, bidderRequest.gdprConsent, bidderRequest.uspConsent, bidderRequest.gppConsent); }); } @@ -247,7 +259,9 @@ export function newBidder(spec) { fledgeAuctionConfigs.forEach((fledgeAuctionConfig) => { const bidRequest = bidRequestMap[fledgeAuctionConfig.bidId]; if (bidRequest) { - addComponentAuction(bidRequest, fledgeAuctionConfig); + addComponentAuction(bidRequest.adUnitCode, fledgeAuctionConfig.config); + } else { + logWarn('Received fledge auction configuration for an unknown bidId', fledgeAuctionConfig); } }); }, @@ -295,8 +309,8 @@ export function newBidder(spec) { return false; } - function registerSyncs(responses, gdprConsent, uspConsent) { - registerSyncInner(spec, responses, gdprConsent, uspConsent); + function registerSyncs(responses, gdprConsent, uspConsent, gppConsent) { + registerSyncInner(spec, responses, gdprConsent, uspConsent, gppConsent); } function filterAndWarn(bid) { @@ -448,14 +462,14 @@ export const processBidderRequests = hook('sync', function (spec, bids, bidderRe }) }, 'processBidderRequests') -export const registerSyncInner = hook('async', function(spec, responses, gdprConsent, uspConsent) { +export const registerSyncInner = hook('async', function(spec, responses, gdprConsent, uspConsent, gppConsent) { const aliasSyncEnabled = config.getConfig('userSync.aliasSyncEnabled'); if (spec.getUserSyncs && (aliasSyncEnabled || !adapterManager.aliasRegistry[spec.code])) { let filterConfig = config.getConfig('userSync.filterSettings'); let syncs = spec.getUserSyncs({ iframeEnabled: !!(filterConfig && (filterConfig.iframe || filterConfig.all)), pixelEnabled: !!(filterConfig && (filterConfig.image || filterConfig.all)), - }, responses, gdprConsent, uspConsent); + }, responses, gdprConsent, uspConsent, gppConsent); if (syncs) { if (!Array.isArray(syncs)) { syncs = [syncs]; @@ -463,60 +477,65 @@ export const registerSyncInner = hook('async', function(spec, responses, gdprCon syncs.forEach((sync) => { userSync.registerSync(sync.type, spec.code, sync.url) }); + userSync.bidderDone(spec.code); } } }, 'registerSyncs') -export const addComponentAuction = hook('sync', (_bidRequest, fledgeAuctionConfig) => { - logInfo(`bidderFactory.addComponentAuction`, fledgeAuctionConfig); -}, 'addComponentAuction') +export const addComponentAuction = hook('sync', (adUnitCode, fledgeAuctionConfig) => { +}, 'addComponentAuction'); export function preloadBidderMappingFile(fn, adUnits) { - if (!config.getConfig('adpod.brandCategoryExclusion')) { - return fn.call(this, adUnits); - } - let adPodBidders = adUnits - .filter((adUnit) => deepAccess(adUnit, 'mediaTypes.video.context') === ADPOD) - .map((adUnit) => adUnit.bids.map((bid) => bid.bidder)) - .reduce(flatten, []) - .filter(uniques); - - adPodBidders.forEach(bidder => { - let bidderSpec = adapterManager.getBidAdapter(bidder); - if (bidderSpec.getSpec().getMappingFileInfo) { - let info = bidderSpec.getSpec().getMappingFileInfo(); - let refreshInDays = (info.refreshInDays) ? info.refreshInDays : DEFAULT_REFRESHIN_DAYS; - let key = (info.localStorageKey) ? info.localStorageKey : bidderSpec.getSpec().code; - let mappingData = storage.getDataFromLocalStorage(key); - try { - mappingData = mappingData ? JSON.parse(mappingData) : undefined; - if (!mappingData || timestamp() > mappingData.lastUpdated + refreshInDays * 24 * 60 * 60 * 1000) { - ajax(info.url, - { - success: (response) => { - try { - response = JSON.parse(response); - let mapping = { - lastUpdated: timestamp(), - mapping: response.mapping + if (FEATURES.VIDEO) { + if (!config.getConfig('adpod.brandCategoryExclusion')) { + return fn.call(this, adUnits); + } + + let adPodBidders = adUnits + .filter((adUnit) => deepAccess(adUnit, 'mediaTypes.video.context') === ADPOD) + .map((adUnit) => adUnit.bids.map((bid) => bid.bidder)) + .reduce(flatten, []) + .filter(uniques); + + adPodBidders.forEach(bidder => { + let bidderSpec = adapterManager.getBidAdapter(bidder); + if (bidderSpec.getSpec().getMappingFileInfo) { + let info = bidderSpec.getSpec().getMappingFileInfo(); + let refreshInDays = (info.refreshInDays) ? info.refreshInDays : DEFAULT_REFRESHIN_DAYS; + let key = (info.localStorageKey) ? info.localStorageKey : bidderSpec.getSpec().code; + let mappingData = storage.getDataFromLocalStorage(key); + try { + mappingData = mappingData ? JSON.parse(mappingData) : undefined; + if (!mappingData || timestamp() > mappingData.lastUpdated + refreshInDays * 24 * 60 * 60 * 1000) { + ajax(info.url, + { + success: (response) => { + try { + response = JSON.parse(response); + let mapping = { + lastUpdated: timestamp(), + mapping: response.mapping + } + storage.setDataInLocalStorage(key, JSON.stringify(mapping)); + } catch (error) { + logError(`Failed to parse ${bidder} bidder translation mapping file`); } - storage.setDataInLocalStorage(key, JSON.stringify(mapping)); - } catch (error) { - logError(`Failed to parse ${bidder} bidder translation mapping file`); + }, + error: () => { + logError(`Failed to load ${bidder} bidder translation file`) } }, - error: () => { - logError(`Failed to load ${bidder} bidder translation file`) - } - }, - ); + ); + } + } catch (error) { + logError(`Failed to parse ${bidder} bidder translation mapping file`); } - } catch (error) { - logError(`Failed to parse ${bidder} bidder translation mapping file`); } - } - }); - fn.call(this, adUnits); + }); + fn.call(this, adUnits); + } else { + return fn.call(this, adUnits) + } } getHook('checkAdUnitSetup').before(preloadBidderMappingFile); @@ -599,7 +618,7 @@ export function isValid(adUnitCode, bid, {index = auctionManager.index} = {}) { logError(errorMessage('Native bid missing some required properties.')); return false; } - if (bid.mediaType === 'video' && !isValidVideoBid(bid, {index})) { + if (FEATURES.VIDEO && bid.mediaType === 'video' && !isValidVideoBid(bid, {index})) { logError(errorMessage(`Video bid does not have required vastUrl or renderer property`)); return false; } diff --git a/src/adloader.js b/src/adloader.js index 64408683e9f..f0b7f7f3e8c 100644 --- a/src/adloader.js +++ b/src/adloader.js @@ -20,7 +20,9 @@ const _approvedLoadExternalJSList = [ 'hadron', 'medianet', 'improvedigital', - 'aaxBlockmeter' + 'aaxBlockmeter', + 'confiant', + 'arcspan' ] /** diff --git a/src/auction.js b/src/auction.js index 87397b0dc15..60892e5d7a2 100644 --- a/src/auction.js +++ b/src/auction.js @@ -93,6 +93,8 @@ import CONSTANTS from './constants.json'; import {GreedyPromise} from './utils/promise.js'; import {useMetrics} from './utils/perfMetrics.js'; import {createBid} from './bidfactory.js'; +import {adjustCpm} from './utils/cpm.js'; +import {getGlobal} from './prebidGlobal.js'; const { syncUsers } = userSync; @@ -110,6 +112,8 @@ const outstandingRequests = {}; const sourceInfo = {}; const queuedCalls = []; +const pbjsInstance = getGlobal(); + /** * Clear global state for tests */ @@ -150,11 +154,13 @@ export function newAuction({adUnits, adUnitCodes, callback, cbTimeout, labels, a let _auctionEnd; let _timer; let _auctionStatus; + let _nonBids = []; function addBidRequests(bidderRequests) { _bidderRequests = _bidderRequests.concat(bidderRequests); } function addBidReceived(bidsReceived) { _bidsReceived = _bidsReceived.concat(bidsReceived); } function addBidRejected(bidsRejected) { _bidsRejected = _bidsRejected.concat(bidsRejected); } function addNoBid(noBid) { _noBids = _noBids.concat(noBid); } + function addNonBids(seatnonbids) { _nonBids = _nonBids.concat(seatnonbids); } function getProperties() { return { @@ -171,7 +177,8 @@ export function newAuction({adUnits, adUnitCodes, callback, cbTimeout, labels, a bidsRejected: _bidsRejected, winningBids: _winningBids, timeout: _timeout, - metrics: metrics + metrics: metrics, + seatNonBids: _nonBids }; } @@ -212,7 +219,7 @@ export function newAuction({adUnits, adUnitCodes, callback, cbTimeout, labels, a const bids = _bidsReceived .filter(bind.call(adUnitsFilter, this, adUnitCodes)) .reduce(groupByPlacement, {}); - _callback.apply($$PREBID_GLOBAL$$, [bids, timedOut, _auctionId]); + _callback.apply(pbjsInstance, [bids, timedOut, _auctionId]); _callback = null; } } catch (e) { @@ -368,6 +375,12 @@ export function newAuction({adUnits, adUnitCodes, callback, cbTimeout, labels, a adapterManager.callSetTargetingBidder(bid.adapterCode || bid.bidder, bid); } + events.on(CONSTANTS.EVENTS.SEAT_NON_BID, (event) => { + if (event.auctionId === _auctionId) { + addNonBids(event.seatnonbid) + } + }); + return { addBidReceived, addBidRejected, @@ -386,6 +399,7 @@ export function newAuction({adUnits, adUnitCodes, callback, cbTimeout, labels, a getBidRequests: () => _bidderRequests, getBidsReceived: () => _bidsReceived, getNoBids: () => _noBids, + getNonBids: () => _nonBids, getFPD: () => ortb2Fragments, getMetrics: () => metrics, }; @@ -462,7 +476,7 @@ export function auctionCallbacks(auctionDone, auctionInstance, {index = auctionM handleBidResponse(adUnitCode, bid, (done) => { let bidResponse = getPreparedBidForAuction(bid); - if (bidResponse.mediaType === VIDEO) { + if (FEATURES.VIDEO && bidResponse.mediaType === VIDEO) { tryAddVideoBid(auctionInstance, bidResponse, done); } else { if (FEATURES.NATIVE && bidResponse.native != null && typeof bidResponse.native === 'object') { @@ -609,7 +623,7 @@ const addLegacyFieldsIfNeeded = (bidResponse) => { } } -const storeInCache = (batch) => { +const _storeInCache = (batch) => { store(batch.map(entry => entry.bidResponse), function (error, cacheIds) { cacheIds.forEach((cacheId, i) => { const { auctionInstance, bidResponse, afterBidAdded } = batch[i]; @@ -636,6 +650,8 @@ const storeInCache = (batch) => { }); }; +const storeInCache = FEATURES.VIDEO ? _storeInCache : () => {}; + let batchSize, batchTimeout; config.getConfig('cache', (cacheConfig) => { batchSize = typeof cacheConfig.cache.batchSize === 'number' && cacheConfig.cache.batchSize > 0 @@ -771,7 +787,7 @@ function setupBidTargeting(bidObject) { */ export function getMediaTypeGranularity(mediaType, mediaTypes, mediaTypePriceGranularity) { if (mediaType && mediaTypePriceGranularity) { - if (mediaType === VIDEO) { + if (FEATURES.VIDEO && mediaType === VIDEO) { const context = deepAccess(mediaTypes, `${VIDEO}.context`, 'instream'); if (mediaTypePriceGranularity[`${VIDEO}-${context}`]) { return mediaTypePriceGranularity[`${VIDEO}-${context}`]; @@ -824,7 +840,7 @@ export const getPriceByGranularity = (granularity) => { */ export const getAdvertiserDomain = () => { return (bid) => { - return (bid.meta && bid.meta.advertiserDomains && bid.meta.advertiserDomains.length > 0) ? bid.meta.advertiserDomains[0] : ''; + return (bid.meta && bid.meta.advertiserDomains && bid.meta.advertiserDomains.length > 0) ? [bid.meta.advertiserDomains].flat()[0] : ''; } } @@ -880,7 +896,7 @@ export function getStandardBidderSettings(mediaType, bidderCode) { standardSettings[CONSTANTS.JSON_MAPPING.ADSERVER_TARGETING] = defaultAdserverTargeting(); } - if (mediaType === 'video') { + if (FEATURES.VIDEO && mediaType === 'video') { const adserverTargeting = standardSettings[CONSTANTS.JSON_MAPPING.ADSERVER_TARGETING].slice(); standardSettings[CONSTANTS.JSON_MAPPING.ADSERVER_TARGETING] = adserverTargeting; @@ -903,6 +919,7 @@ export function getStandardBidderSettings(mediaType, bidderCode) { } } } + return standardSettings; } @@ -954,7 +971,7 @@ function setKeys(keyValues, bidderSettings, custBidObj, bidReq) { if ( ((typeof bidderSettings.suppressEmptyKeys !== 'undefined' && bidderSettings.suppressEmptyKeys === true) || - key === CONSTANTS.TARGETING_KEYS.DEAL) && // hb_deal is suppressed automatically if not set + key === CONSTANTS.TARGETING_KEYS.DEAL || key === CONSTANTS.TARGETING_KEYS.ACAT) && // hb_deal & hb_acat are suppressed automatically if not set ( isEmptyStr(value) || value === null || @@ -971,17 +988,7 @@ function setKeys(keyValues, bidderSettings, custBidObj, bidReq) { } export function adjustBids(bid) { - let code = bid.bidderCode; - let bidPriceAdjusted = bid.cpm; - const bidCpmAdjustment = bidderSettings.get(code || null, 'bidCpmAdjustment'); - - if (bidCpmAdjustment && typeof bidCpmAdjustment === 'function') { - try { - bidPriceAdjusted = bidCpmAdjustment(bid.cpm, Object.assign({}, bid)); - } catch (e) { - logError('Error during bid adjustment', 'bidmanager.js', e); - } - } + let bidPriceAdjusted = adjustCpm(bid.cpm, bid); if (bidPriceAdjusted >= 0) { bid.cpm = bidPriceAdjusted; diff --git a/src/consentHandler.js b/src/consentHandler.js index 01470a4b38c..4776a8ece02 100644 --- a/src/consentHandler.js +++ b/src/consentHandler.js @@ -107,3 +107,56 @@ export class GdprConsentHandler extends ConsentHandler { } } } + +export class GppConsentHandler extends ConsentHandler { + getConsentMeta() { + const consentData = this.getConsentData(); + if (consentData && this.generatedTime) { + return { + generatedAt: this.generatedTime, + } + } + } +} + +export function gvlidRegistry() { + const registry = {}; + const flat = {}; + const none = {}; + return { + /** + * Register a module's GVL ID. + * @param {string} moduleType defined in `activities/modules.js` + * @param {string} moduleName + * @param {number} gvlid + */ + register(moduleType, moduleName, gvlid) { + if (gvlid) { + (registry[moduleName] = registry[moduleName] || {})[moduleType] = gvlid; + if (flat.hasOwnProperty(moduleName)) { + if (flat[moduleName] !== gvlid) flat[moduleName] = none; + } else { + flat[moduleName] = gvlid; + } + } + }, + /** + * Get a module's GVL ID(s). + * + * @param {string} moduleName + * @return {{modules: {[moduleType]: number}, gvlid?: number}} an object where: + * `modules` is a map from module type to that module's GVL ID; + * `gvlid` is the single GVL ID for this family of modules (only defined + * if all modules with this name declared the same ID). + */ + get(moduleName) { + const result = {modules: registry[moduleName] || {}}; + if (flat.hasOwnProperty(moduleName) && flat[moduleName] !== none) { + result.gvlid = flat[moduleName]; + } + return result; + } + } +} + +export const GDPR_GVLIDS = gvlidRegistry(); diff --git a/src/constants.json b/src/constants.json index e33d65f3fb1..ef652af1ae5 100644 --- a/src/constants.json +++ b/src/constants.json @@ -31,6 +31,7 @@ "BID_RESPONSE": "bidResponse", "BID_REJECTED": "bidRejected", "NO_BID": "noBid", + "SEAT_NON_BID": "seatNonBid", "BID_WON": "bidWon", "BIDDER_DONE": "bidderDone", "BIDDER_ERROR": "bidderError", diff --git a/src/fpd/enrichment.js b/src/fpd/enrichment.js new file mode 100644 index 00000000000..f812d8435d9 --- /dev/null +++ b/src/fpd/enrichment.js @@ -0,0 +1,131 @@ +import {hook} from '../hook.js'; +import {getRefererInfo, parseDomain} from '../refererDetection.js'; +import {findRootDomain} from './rootDomain.js'; +import {deepSetValue, getDefinedParams, getDNT, getWindowSelf, getWindowTop, mergeDeep} from '../utils.js'; +import {config} from '../config.js'; +import {getHighEntropySUA, getLowEntropySUA} from './sua.js'; +import {GreedyPromise} from '../utils/promise.js'; +import {CLIENT_SECTIONS, clientSectionChecker, hasSection} from './oneClient.js'; + +export const dep = { + getRefererInfo, + findRootDomain, + getWindowTop, + getWindowSelf, + getHighEntropySUA, + getLowEntropySUA, +}; + +const oneClient = clientSectionChecker('FPD') + +/** + * Enrich an ortb2 object with first party data. + * @param {Promise[{}]} fpd: a promise to an ortb2 object. + * @returns: {Promise[{}]}: a promise to an enriched ortb2 object. + */ +export const enrichFPD = hook('sync', (fpd) => { + return GreedyPromise.all([fpd, getSUA().catch(() => null)]) + .then(([ortb2, sua]) => { + const ri = dep.getRefererInfo(); + mergeLegacySetConfigs(ortb2); + Object.entries(ENRICHMENTS).forEach(([section, getEnrichments]) => { + const data = getEnrichments(ortb2, ri); + if (data && Object.keys(data).length > 0) { + ortb2[section] = mergeDeep({}, data, ortb2[section]); + } + }); + if (sua) { + deepSetValue(ortb2, 'device.sua', Object.assign({}, sua, ortb2.device.sua)); + } + ortb2 = oneClient(ortb2); + for (let section of CLIENT_SECTIONS) { + if (hasSection(ortb2, section)) { + ortb2[section] = mergeDeep({}, clientEnrichment(ortb2, ri), ortb2[section]); + break; + } + } + return ortb2; + }); +}); + +function mergeLegacySetConfigs(ortb2) { + // merge in values from "legacy" setConfig({app, site, device}) + // TODO: deprecate these eventually + ['app', 'site', 'device'].forEach(prop => { + const cfg = config.getConfig(prop); + if (cfg != null) { + ortb2[prop] = mergeDeep({}, cfg, ortb2[prop]); + } + }) +} + +function winFallback(fn) { + try { + return fn(dep.getWindowTop()); + } catch (e) { + return fn(dep.getWindowSelf()); + } +} + +function getSUA() { + const hints = config.getConfig('firstPartyData.uaHints'); + return !Array.isArray(hints) || hints.length === 0 + ? GreedyPromise.resolve(dep.getLowEntropySUA()) + : dep.getHighEntropySUA(hints); +} + +function removeUndef(obj) { + return getDefinedParams(obj, Object.keys(obj)) +} + +const ENRICHMENTS = { + site(ortb2, ri) { + if (CLIENT_SECTIONS.filter(p => p !== 'site').some(hasSection.bind(null, ortb2))) { + // do not enrich site if dooh or app are set + return; + } + return removeUndef({ + page: ri.page, + ref: ri.ref, + }); + }, + device() { + return winFallback((win) => { + const w = win.innerWidth || win.document.documentElement.clientWidth || win.document.body.clientWidth; + const h = win.innerHeight || win.document.documentElement.clientHeight || win.document.body.clientHeight; + return { + w, + h, + dnt: getDNT() ? 1 : 0, + ua: win.navigator.userAgent, + language: win.navigator.language.split('-').shift(), + }; + }) + }, + regs() { + const regs = {}; + if (winFallback((win) => win.navigator.globalPrivacyControl)) { + deepSetValue(regs, 'ext.gpc', 1); + } + const coppa = config.getConfig('coppa'); + if (typeof coppa === 'boolean') { + regs.coppa = coppa ? 1 : 0; + } + return regs; + } +}; + +// Enrichment of properties common across dooh, app and site - will be dropped into whatever +// section is appropriate +function clientEnrichment(ortb2, ri) { + const domain = parseDomain(ri.page, {noLeadingWww: true}); + const keywords = winFallback((win) => win.document.querySelector('meta[name=\'keywords\']')) + ?.content?.replace?.(/\s/g, ''); + return removeUndef({ + domain, + keywords, + publisher: removeUndef({ + domain: dep.findRootDomain(domain) + }) + }) +} diff --git a/src/fpd/oneClient.js b/src/fpd/oneClient.js new file mode 100644 index 00000000000..67f53c73bd8 --- /dev/null +++ b/src/fpd/oneClient.js @@ -0,0 +1,26 @@ +import {logWarn} from '../utils.js'; + +// mutually exclusive ORTB sections in order of priority - 'dooh' beats 'app' & 'site' and 'app' beats 'site'; +// if one is set, the others will be removed +export const CLIENT_SECTIONS = ['dooh', 'app', 'site'] + +export function clientSectionChecker(logPrefix) { + return function onlyOneClientSection(ortb2) { + CLIENT_SECTIONS.reduce((found, section) => { + if (hasSection(ortb2, section)) { + if (found != null) { + logWarn(`${logPrefix} specifies both '${found}' and '${section}'; dropping the latter.`) + delete ortb2[section]; + } else { + found = section; + } + } + return found; + }, null); + return ortb2; + } +} + +export function hasSection(ortb2, section) { + return ortb2[section] != null && Object.keys(ortb2[section]).length > 0 +} diff --git a/src/fpd/rootDomain.js b/src/fpd/rootDomain.js new file mode 100644 index 00000000000..4095613672f --- /dev/null +++ b/src/fpd/rootDomain.js @@ -0,0 +1,57 @@ +import {memoize, timestamp} from '../utils.js'; +import {getCoreStorageManager} from '../storageManager.js'; + +export const coreStorage = getCoreStorageManager(); + +/** + * Find the root domain by testing for the topmost domain that will allow setting cookies. + */ + +export const findRootDomain = memoize(function findRootDomain(fullDomain = window.location.host) { + if (!coreStorage.cookiesAreEnabled()) { + return fullDomain; + } + + const domainParts = fullDomain.split('.'); + if (domainParts.length === 2) { + return fullDomain; + } + let rootDomain; + let continueSearching; + let startIndex = -2; + const TEST_COOKIE_NAME = `_rdc${Date.now()}`; + const TEST_COOKIE_VALUE = 'writeable'; + do { + rootDomain = domainParts.slice(startIndex).join('.'); + let expirationDate = new Date(timestamp() + 10 * 1000).toUTCString(); + + // Write a test cookie + coreStorage.setCookie( + TEST_COOKIE_NAME, + TEST_COOKIE_VALUE, + expirationDate, + 'Lax', + rootDomain, + undefined + ); + + // See if the write was successful + const value = coreStorage.getCookie(TEST_COOKIE_NAME, undefined); + if (value === TEST_COOKIE_VALUE) { + continueSearching = false; + // Delete our test cookie + coreStorage.setCookie( + TEST_COOKIE_NAME, + '', + 'Thu, 01 Jan 1970 00:00:01 GMT', + undefined, + rootDomain, + undefined + ); + } else { + startIndex += -1; + continueSearching = Math.abs(startIndex) <= domainParts.length; + } + } while (continueSearching); + return rootDomain; +}); diff --git a/libraries/fpd/sua.js b/src/fpd/sua.js similarity index 96% rename from libraries/fpd/sua.js rename to src/fpd/sua.js index b6a46763514..30b2be4c13b 100644 --- a/libraries/fpd/sua.js +++ b/src/fpd/sua.js @@ -1,5 +1,5 @@ -import {isEmptyStr, isStr, isEmpty} from '../../src/utils.js'; -import {GreedyPromise} from '../../src/utils/promise.js'; +import {isEmptyStr, isStr, isEmpty} from '../utils.js'; +import {GreedyPromise} from '../utils/promise.js'; export const SUA_SOURCE_UNKNOWN = 0; export const SUA_SOURCE_LOW_ENTROPY = 1; diff --git a/src/native.js b/src/native.js index 25f8c38cb30..79a972371da 100644 --- a/src/native.js +++ b/src/native.js @@ -9,8 +9,8 @@ import { isNumber, isPlainObject, logError, - triggerPixel, - pick + pick, + triggerPixel } from './utils.js'; import {includes} from './polyfill.js'; import {auctionManager} from './auctionManager.js'; @@ -385,16 +385,19 @@ export function getNativeTargeting(bid, {index = auctionManager.index} = {}) { return keyValues; } -function assetsMessage(data, adObject, keys) { +function assetsMessage(data, adObject, keys, {index = auctionManager.index} = {}) { const message = { message: 'assetResponse', adId: data.adId, }; + const adUnit = index.getAdUnit(adObject); let nativeResp = adObject.native; if (adObject.native.ortb) { message.ortb = adObject.native.ortb; + } else if (adUnit.mediaTypes?.native?.ortb) { + message.ortb = toOrtbNativeResponse(adObject.native, adUnit.nativeOrtbRequest); } message.assets = []; @@ -698,7 +701,7 @@ export function toOrtbNativeResponse(legacyResponse, ortbRequest) { } Object.keys(legacyResponse).filter(key => !!legacyResponse[key]).forEach(key => { - const value = legacyResponse[key]; + const value = getAssetValue(legacyResponse[key]); switch (key) { // process titles case 'title': @@ -747,7 +750,11 @@ export function toLegacyResponse(ortbResponse, ortbRequest) { if (asset.title) { legacyResponse.title = asset.title.text; } else if (asset.img) { - legacyResponse[requestAsset.img.type === NATIVE_IMAGE_TYPES.MAIN ? 'image' : 'icon'] = asset.img.url; + legacyResponse[requestAsset.img.type === NATIVE_IMAGE_TYPES.MAIN ? 'image' : 'icon'] = { + url: asset.img.url, + width: asset.img.w, + height: asset.img.h + }; } else if (asset.data) { legacyResponse[PREBID_NATIVE_DATA_KEYS_TO_ORTB_INVERSE[NATIVE_ASSET_TYPES_INVERSE[requestAsset.data.type]]] = asset.data.value; } diff --git a/src/prebid.js b/src/prebid.js index 06429b13a72..5070b6b42a4 100644 --- a/src/prebid.js +++ b/src/prebid.js @@ -45,13 +45,14 @@ import {executeRenderer, isRendererRequired} from './Renderer.js'; import {createBid} from './bidfactory.js'; import {storageCallbacks} from './storageManager.js'; import {emitAdRenderFail, emitAdRenderSucceeded} from './adRendering.js'; -import {default as adapterManager, gdprDataHandler, getS2SBidderSet, uspDataHandler} from './adapterManager.js'; +import {default as adapterManager, gdprDataHandler, getS2SBidderSet, gppDataHandler, uspDataHandler} from './adapterManager.js'; import CONSTANTS from './constants.json'; import * as events from './events.js'; import {newMetrics, useMetrics} from './utils/perfMetrics.js'; -import {defer} from './utils/promise.js'; +import {defer, GreedyPromise} from './utils/promise.js'; +import {enrichFPD} from './fpd/enrichment.js'; -const $$PREBID_GLOBAL$$ = getGlobal(); +const pbjsInstance = getGlobal(); const { triggerUserSyncs } = userSync; /* private variables */ @@ -66,22 +67,22 @@ const eventValidators = { loadSession(); /* Public vars */ -$$PREBID_GLOBAL$$.bidderSettings = $$PREBID_GLOBAL$$.bidderSettings || {}; +pbjsInstance.bidderSettings = pbjsInstance.bidderSettings || {}; // let the world know we are loaded -$$PREBID_GLOBAL$$.libLoaded = true; +pbjsInstance.libLoaded = true; // version auto generated from build -$$PREBID_GLOBAL$$.version = 'v$prebid.version$'; +pbjsInstance.version = 'v$prebid.version$'; logInfo('Prebid.js v$prebid.version$ loaded'); -$$PREBID_GLOBAL$$.installedModules = $$PREBID_GLOBAL$$.installedModules || []; +pbjsInstance.installedModules = pbjsInstance.installedModules || []; // create adUnit array -$$PREBID_GLOBAL$$.adUnits = $$PREBID_GLOBAL$$.adUnits || []; +pbjsInstance.adUnits = pbjsInstance.adUnits || []; // Allow publishers who enable user sync override to trigger their sync -$$PREBID_GLOBAL$$.triggerUserSyncs = triggerUserSyncs; +pbjsInstance.triggerUserSyncs = triggerUserSyncs; function checkDefinedPlacement(id) { var adUnitCodes = auctionManager.getBidsRequested().map(bidSet => bidSet.bids.map(bid => bid.adUnitCode)) @@ -224,7 +225,6 @@ function validateAdUnit(adUnit) { export const adUnitSetupChecks = { validateAdUnit, validateBannerMediaType, - validateVideoMediaType, validateSizes }; @@ -232,6 +232,10 @@ if (FEATURES.NATIVE) { Object.assign(adUnitSetupChecks, {validateNativeMediaType}); } +if (FEATURES.VIDEO) { + Object.assign(adUnitSetupChecks, { validateVideoMediaType }); +} + export const checkAdUnitSetup = hook('sync', function (adUnits) { const validatedAdUnits = []; @@ -247,7 +251,7 @@ export const checkAdUnitSetup = hook('sync', function (adUnits) { if (mediaTypes.banner.hasOwnProperty('pos')) validatedBanner = validateAdUnitPos(validatedBanner, 'banner'); } - if (mediaTypes.video) { + if (FEATURES.VIDEO && mediaTypes.video) { validatedVideo = validatedBanner ? validateVideoMediaType(validatedBanner) : validateVideoMediaType(adUnit); if (mediaTypes.video.hasOwnProperty('pos')) validatedVideo = validateAdUnitPos(validatedVideo, 'video'); } @@ -276,12 +280,12 @@ export const checkAdUnitSetup = hook('sync', function (adUnits) { * @alias module:pbjs.getAdserverTargetingForAdUnitCodeStr * @return {Array} returnObj return bids array */ -$$PREBID_GLOBAL$$.getAdserverTargetingForAdUnitCodeStr = function (adunitCode) { +pbjsInstance.getAdserverTargetingForAdUnitCodeStr = function (adunitCode) { logInfo('Invoking $$PREBID_GLOBAL$$.getAdserverTargetingForAdUnitCodeStr', arguments); // call to retrieve bids array if (adunitCode) { - var res = $$PREBID_GLOBAL$$.getAdserverTargetingForAdUnitCode(adunitCode); + var res = pbjsInstance.getAdserverTargetingForAdUnitCode(adunitCode); return transformAdServerTargetingObj(res); } else { logMessage('Need to call getAdserverTargetingForAdUnitCodeStr with adunitCode'); @@ -294,7 +298,7 @@ $$PREBID_GLOBAL$$.getAdserverTargetingForAdUnitCodeStr = function (adunitCode) { * @alias module:pbjs.getHighestUnusedBidResponseForAdUnitCode * @returns {Object} returnObj return bid */ -$$PREBID_GLOBAL$$.getHighestUnusedBidResponseForAdUnitCode = function (adunitCode) { +pbjsInstance.getHighestUnusedBidResponseForAdUnitCode = function (adunitCode) { if (adunitCode) { const bid = auctionManager.getAllBidsForAdUnitCode(adunitCode) .filter(isBidUsable) @@ -311,8 +315,8 @@ $$PREBID_GLOBAL$$.getHighestUnusedBidResponseForAdUnitCode = function (adunitCod * @alias module:pbjs.getAdserverTargetingForAdUnitCode * @returns {Object} returnObj return bids */ -$$PREBID_GLOBAL$$.getAdserverTargetingForAdUnitCode = function (adUnitCode) { - return $$PREBID_GLOBAL$$.getAdserverTargeting(adUnitCode)[adUnitCode]; +pbjsInstance.getAdserverTargetingForAdUnitCode = function (adUnitCode) { + return pbjsInstance.getAdserverTargeting(adUnitCode)[adUnitCode]; }; /** @@ -321,7 +325,7 @@ $$PREBID_GLOBAL$$.getAdserverTargetingForAdUnitCode = function (adUnitCode) { * @alias module:pbjs.getAdserverTargeting */ -$$PREBID_GLOBAL$$.getAdserverTargeting = function (adUnitCode) { +pbjsInstance.getAdserverTargeting = function (adUnitCode) { logInfo('Invoking $$PREBID_GLOBAL$$.getAdserverTargeting', arguments); return targeting.getAllTargeting(adUnitCode); }; @@ -335,11 +339,12 @@ function getConsentMetadata() { return { gdpr: gdprDataHandler.getConsentMeta(), usp: uspDataHandler.getConsentMeta(), + gpp: gppDataHandler.getConsentMeta(), coppa: !!(config.getConfig('coppa')) } } -$$PREBID_GLOBAL$$.getConsentMetadata = function () { +pbjsInstance.getConsentMetadata = function () { logInfo('Invoking $$PREBID_GLOBAL$$.getConsentMetadata'); return getConsentMetadata(); }; @@ -370,7 +375,7 @@ function getBids(type) { * @return {Object} map | object that contains the bidRequests */ -$$PREBID_GLOBAL$$.getNoBids = function () { +pbjsInstance.getNoBids = function () { logInfo('Invoking $$PREBID_GLOBAL$$.getNoBids', arguments); return getBids('getNoBids'); }; @@ -382,7 +387,7 @@ $$PREBID_GLOBAL$$.getNoBids = function () { * @return {Object} bidResponse object */ -$$PREBID_GLOBAL$$.getNoBidsForAdUnitCode = function (adUnitCode) { +pbjsInstance.getNoBidsForAdUnitCode = function (adUnitCode) { const bids = auctionManager.getNoBids().filter(bid => bid.adUnitCode === adUnitCode); return { bids }; }; @@ -393,7 +398,7 @@ $$PREBID_GLOBAL$$.getNoBidsForAdUnitCode = function (adUnitCode) { * @return {Object} map | object that contains the bidResponses */ -$$PREBID_GLOBAL$$.getBidResponses = function () { +pbjsInstance.getBidResponses = function () { logInfo('Invoking $$PREBID_GLOBAL$$.getBidResponses', arguments); return getBids('getBidsReceived'); }; @@ -405,7 +410,7 @@ $$PREBID_GLOBAL$$.getBidResponses = function () { * @return {Object} bidResponse object */ -$$PREBID_GLOBAL$$.getBidResponsesForAdUnitCode = function (adUnitCode) { +pbjsInstance.getBidResponsesForAdUnitCode = function (adUnitCode) { const bids = auctionManager.getBidsReceived().filter(bid => bid.adUnitCode === adUnitCode); return { bids }; }; @@ -416,7 +421,7 @@ $$PREBID_GLOBAL$$.getBidResponsesForAdUnitCode = function (adUnitCode) { * @param {function(object)} customSlotMatching gets a GoogleTag slot and returns a filter function for adUnitCode, so you can decide to match on either eg. return slot => { return adUnitCode => { return slot.getSlotElementId() === 'myFavoriteDivId'; } }; * @alias module:pbjs.setTargetingForGPTAsync */ -$$PREBID_GLOBAL$$.setTargetingForGPTAsync = function (adUnit, customSlotMatching) { +pbjsInstance.setTargetingForGPTAsync = function (adUnit, customSlotMatching) { logInfo('Invoking $$PREBID_GLOBAL$$.setTargetingForGPTAsync', arguments); if (!isGptPubadsDefined()) { logError('window.googletag is not defined on the page'); @@ -449,7 +454,7 @@ $$PREBID_GLOBAL$$.setTargetingForGPTAsync = function (adUnit, customSlotMatching * @param {(string|string[])} adUnitCode adUnitCode or array of adUnitCodes * @alias module:pbjs.setTargetingForAst */ -$$PREBID_GLOBAL$$.setTargetingForAst = function (adUnitCodes) { +pbjsInstance.setTargetingForAst = function (adUnitCodes) { logInfo('Invoking $$PREBID_GLOBAL$$.setTargetingForAn', arguments); if (!targeting.isApntagDefined()) { logError('window.apntag is not defined on the page'); @@ -482,7 +487,7 @@ function reinjectNodeIfRemoved(node, doc, tagName) { * @param {string} id bid id to locate the ad * @alias module:pbjs.renderAd */ -$$PREBID_GLOBAL$$.renderAd = hook('async', function (doc, id, options) { +pbjsInstance.renderAd = hook('async', function (doc, id, options) { logInfo('Invoking $$PREBID_GLOBAL$$.renderAd', arguments); logMessage('Calling renderAd with adId :' + id); @@ -528,12 +533,14 @@ $$PREBID_GLOBAL$$.renderAd = hook('async', function (doc, id, options) { const {height, width, ad, mediaType, adUrl, renderer} = bid; // video module - const adUnitCode = bid.adUnitCode; - const adUnit = $$PREBID_GLOBAL$$.adUnits.filter(adUnit => adUnit.code === adUnitCode); - const videoModule = $$PREBID_GLOBAL$$.videoModule; - if (adUnit.video && videoModule) { - videoModule.renderBid(adUnit.video.divId, bid); - return; + if (FEATURES.VIDEO) { + const adUnitCode = bid.adUnitCode; + const adUnit = pbjsInstance.adUnits.filter(adUnit => adUnit.code === adUnitCode); + const videoModule = pbjsInstance.videoModule; + if (adUnit.video && videoModule) { + videoModule.renderBid(adUnit.video.divId, bid); + return; + } } if (!doc) { @@ -587,11 +594,11 @@ $$PREBID_GLOBAL$$.renderAd = hook('async', function (doc, id, options) { * @param {string| Array} adUnitCode the adUnitCode(s) to remove * @alias module:pbjs.removeAdUnit */ -$$PREBID_GLOBAL$$.removeAdUnit = function (adUnitCode) { +pbjsInstance.removeAdUnit = function (adUnitCode) { logInfo('Invoking $$PREBID_GLOBAL$$.removeAdUnit', arguments); if (!adUnitCode) { - $$PREBID_GLOBAL$$.adUnits = []; + pbjsInstance.adUnits = []; return; } @@ -604,9 +611,9 @@ $$PREBID_GLOBAL$$.removeAdUnit = function (adUnitCode) { } adUnitCodes.forEach((adUnitCode) => { - for (let i = $$PREBID_GLOBAL$$.adUnits.length - 1; i >= 0; i--) { - if ($$PREBID_GLOBAL$$.adUnits[i].code === adUnitCode) { - $$PREBID_GLOBAL$$.adUnits.splice(i, 1); + for (let i = pbjsInstance.adUnits.length - 1; i >= 0; i--) { + if (pbjsInstance.adUnits[i].code === adUnitCode) { + pbjsInstance.adUnits.splice(i, 1); } } }); @@ -622,16 +629,26 @@ $$PREBID_GLOBAL$$.removeAdUnit = function (adUnitCode) { * @param {String} requestOptions.auctionId * @alias module:pbjs.requestBids */ -$$PREBID_GLOBAL$$.requestBids = (function() { +pbjsInstance.requestBids = (function() { const delegate = hook('async', function ({ bidsBackHandler, timeout, adUnits, adUnitCodes, labels, auctionId, ttlBuffer, ortb2, metrics, defer } = {}) { events.emit(REQUEST_BIDS); const cbTimeout = timeout || config.getConfig('bidderTimeout'); logInfo('Invoking $$PREBID_GLOBAL$$.requestBids', arguments); + if (adUnitCodes && adUnitCodes.length) { + // if specific adUnitCodes supplied filter adUnits for those codes + adUnits = adUnits.filter(unit => includes(adUnitCodes, unit.code)); + } else { + // otherwise derive adUnitCodes from adUnits + adUnitCodes = adUnits && adUnits.map(unit => unit.code); + } const ortb2Fragments = { global: mergeDeep({}, config.getAnyConfig('ortb2') || {}, ortb2 || {}), bidder: Object.fromEntries(Object.entries(config.getBidderConfig()).map(([bidder, cfg]) => [bidder, cfg.ortb2]).filter(([_, ortb2]) => ortb2 != null)) } - return startAuction({bidsBackHandler, timeout: cbTimeout, adUnits, adUnitCodes, labels, auctionId, ttlBuffer, ortb2Fragments, metrics, defer}); + return enrichFPD(GreedyPromise.resolve(ortb2Fragments.global)).then(global => { + ortb2Fragments.global = global; + return startAuction({bidsBackHandler, timeout: cbTimeout, adUnits, adUnitCodes, labels, auctionId, ttlBuffer, ortb2Fragments, metrics, defer}); + }) }, 'requestBids'); return wrapHook(delegate, function requestBids(req = {}) { @@ -641,7 +658,7 @@ $$PREBID_GLOBAL$$.requestBids = (function() { // if the request does not specify adUnits, clone the global adUnit array; // otherwise, if the caller goes on to use addAdUnits/removeAdUnits, any asynchronous logic // in any hook might see their effects. - let adUnits = req.adUnits || $$PREBID_GLOBAL$$.adUnits; + let adUnits = req.adUnits || pbjsInstance.adUnits; req.adUnits = (isArray(adUnits) ? adUnits.slice() : [adUnits]); req.metrics = newMetrics(); @@ -656,14 +673,6 @@ export const startAuction = hook('async', function ({ bidsBackHandler, timeout: const s2sBidders = getS2SBidderSet(config.getConfig('s2sConfig') || []); adUnits = useMetrics(metrics).measureTime('requestBids.validate', () => checkAdUnitSetup(adUnits)); - if (adUnitCodes && adUnitCodes.length) { - // if specific adUnitCodes supplied filter adUnits for those codes - adUnits = adUnits.filter(unit => includes(adUnitCodes, unit.code)); - } else { - // otherwise derive adUnitCodes from adUnits - adUnitCodes = adUnits && adUnits.map(unit => unit.code); - } - function auctionDone(bids, timedOut, auctionId) { if (typeof bidsBackHandler === 'function') { try { @@ -757,7 +766,7 @@ export function executeCallbacks(fn, reqBidsConfigObj) { } // This hook will execute all storage callbacks which were registered before gdpr enforcement hook was added. Some bidders, user id modules use storage functions when module is parsed but gdpr enforcement hook is not added at that stage as setConfig callbacks are yet to be called. Hence for such calls we execute all the stored callbacks just before requestBids. At this hook point we will know for sure that gdprEnforcement module is added or not -$$PREBID_GLOBAL$$.requestBids.before(executeCallbacks, 49); +pbjsInstance.requestBids.before(executeCallbacks, 49); /** * @@ -765,9 +774,9 @@ $$PREBID_GLOBAL$$.requestBids.before(executeCallbacks, 49); * @param {Array|Object} adUnitArr Array of adUnits or single adUnit Object. * @alias module:pbjs.addAdUnits */ -$$PREBID_GLOBAL$$.addAdUnits = function (adUnitArr) { +pbjsInstance.addAdUnits = function (adUnitArr) { logInfo('Invoking $$PREBID_GLOBAL$$.addAdUnits', arguments); - $$PREBID_GLOBAL$$.adUnits.push.apply($$PREBID_GLOBAL$$.adUnits, isArray(adUnitArr) ? adUnitArr : [adUnitArr]); + pbjsInstance.adUnits.push.apply(pbjsInstance.adUnits, isArray(adUnitArr) ? adUnitArr : [adUnitArr]); // emit event events.emit(ADD_AD_UNITS); }; @@ -788,7 +797,7 @@ $$PREBID_GLOBAL$$.addAdUnits = function (adUnitArr) { * * Currently `bidWon` is the only event that accepts an `id` parameter. */ -$$PREBID_GLOBAL$$.onEvent = function (event, handler, id) { +pbjsInstance.onEvent = function (event, handler, id) { logInfo('Invoking $$PREBID_GLOBAL$$.onEvent', arguments); if (!isFn(handler)) { logError('The event handler provided is not a function and was not set on event "' + event + '".'); @@ -809,7 +818,7 @@ $$PREBID_GLOBAL$$.onEvent = function (event, handler, id) { * @param {string} id an identifier in the context of the event (see `$$PREBID_GLOBAL$$.onEvent`) * @alias module:pbjs.offEvent */ -$$PREBID_GLOBAL$$.offEvent = function (event, handler, id) { +pbjsInstance.offEvent = function (event, handler, id) { logInfo('Invoking $$PREBID_GLOBAL$$.offEvent', arguments); if (id && !eventValidators[event].call(null, id)) { return; @@ -823,7 +832,7 @@ $$PREBID_GLOBAL$$.offEvent = function (event, handler, id) { * * @alias module:pbjs.getEvents */ -$$PREBID_GLOBAL$$.getEvents = function () { +pbjsInstance.getEvents = function () { logInfo('Invoking $$PREBID_GLOBAL$$.getEvents'); return events.getEvents(); }; @@ -834,7 +843,7 @@ $$PREBID_GLOBAL$$.getEvents = function () { * @param {string} bidderCode [description] * @alias module:pbjs.registerBidAdapter */ -$$PREBID_GLOBAL$$.registerBidAdapter = function (bidderAdaptor, bidderCode) { +pbjsInstance.registerBidAdapter = function (bidderAdaptor, bidderCode) { logInfo('Invoking $$PREBID_GLOBAL$$.registerBidAdapter', arguments); try { adapterManager.registerBidAdapter(bidderAdaptor(), bidderCode); @@ -848,7 +857,7 @@ $$PREBID_GLOBAL$$.registerBidAdapter = function (bidderAdaptor, bidderCode) { * @param {Object} options [description] * @alias module:pbjs.registerAnalyticsAdapter */ -$$PREBID_GLOBAL$$.registerAnalyticsAdapter = function (options) { +pbjsInstance.registerAnalyticsAdapter = function (options) { logInfo('Invoking $$PREBID_GLOBAL$$.registerAnalyticsAdapter', arguments); try { adapterManager.registerAnalyticsAdapter(options); @@ -863,7 +872,7 @@ $$PREBID_GLOBAL$$.registerAnalyticsAdapter = function (options) { * @alias module:pbjs.createBid * @return {Object} bidResponse [description] */ -$$PREBID_GLOBAL$$.createBid = function (statusCode) { +pbjsInstance.createBid = function (statusCode) { logInfo('Invoking $$PREBID_GLOBAL$$.createBid', arguments); return createBid(statusCode); }; @@ -895,14 +904,14 @@ const enableAnalyticsCb = hook('async', function (config) { } }, 'enableAnalyticsCb'); -$$PREBID_GLOBAL$$.enableAnalytics = function (config) { +pbjsInstance.enableAnalytics = function (config) { enableAnalyticsCallbacks.push(enableAnalyticsCb.bind(this, config)); }; /** * @alias module:pbjs.aliasBidder */ -$$PREBID_GLOBAL$$.aliasBidder = function (bidderCode, alias, options) { +pbjsInstance.aliasBidder = function (bidderCode, alias, options) { logInfo('Invoking $$PREBID_GLOBAL$$.aliasBidder', arguments); if (bidderCode && alias) { adapterManager.aliasBidAdapter(bidderCode, alias, options); @@ -911,6 +920,14 @@ $$PREBID_GLOBAL$$.aliasBidder = function (bidderCode, alias, options) { } }; +/** + * @alias module:pbjs.aliasRegistry + */ +pbjsInstance.aliasRegistry = adapterManager.aliasRegistry; +config.getConfig('aliasRegistry', config => { + if (config.aliasRegistry === 'private') delete pbjsInstance.aliasRegistry; +}); + /** * The bid response object returned by an external bidder adapter during the auction. * @typedef {Object} AdapterBidResponse @@ -950,7 +967,7 @@ $$PREBID_GLOBAL$$.aliasBidder = function (bidderCode, alias, options) { * Get all of the bids that have been rendered. Useful for [troubleshooting your integration](http://prebid.org/dev-docs/prebid-troubleshooting-guide.html). * @return {Array} A list of bids that have been rendered. */ -$$PREBID_GLOBAL$$.getAllWinningBids = function () { +pbjsInstance.getAllWinningBids = function () { return auctionManager.getAllWinningBids(); }; @@ -958,7 +975,7 @@ $$PREBID_GLOBAL$$.getAllWinningBids = function () { * Get all of the bids that have won their respective auctions. * @return {Array} A list of bids that have won their respective auctions. */ -$$PREBID_GLOBAL$$.getAllPrebidWinningBids = function () { +pbjsInstance.getAllPrebidWinningBids = function () { return auctionManager.getBidsReceived() .filter(bid => bid.status === CONSTANTS.BID_STATUS.BID_TARGETING_SET); }; @@ -970,46 +987,48 @@ $$PREBID_GLOBAL$$.getAllPrebidWinningBids = function () { * @alias module:pbjs.getHighestCpmBids * @return {Array} array containing highest cpm bid object(s) */ -$$PREBID_GLOBAL$$.getHighestCpmBids = function (adUnitCode) { +pbjsInstance.getHighestCpmBids = function (adUnitCode) { return targeting.getWinningBids(adUnitCode); }; -/** - * Mark the winning bid as used, should only be used in conjunction with video - * @typedef {Object} MarkBidRequest - * @property {string} adUnitCode The ad unit code - * @property {string} adId The id representing the ad we want to mark - * - * @alias module:pbjs.markWinningBidAsUsed - */ -$$PREBID_GLOBAL$$.markWinningBidAsUsed = function (markBidRequest) { - let bids = []; - - if (markBidRequest.adUnitCode && markBidRequest.adId) { - bids = auctionManager.getBidsReceived() - .filter(bid => bid.adId === markBidRequest.adId && bid.adUnitCode === markBidRequest.adUnitCode); - } else if (markBidRequest.adUnitCode) { - bids = targeting.getWinningBids(markBidRequest.adUnitCode); - } else if (markBidRequest.adId) { - bids = auctionManager.getBidsReceived().filter(bid => bid.adId === markBidRequest.adId); - } else { - logWarn('Improper use of markWinningBidAsUsed. It needs an adUnitCode or an adId to function.'); - } +if (FEATURES.VIDEO) { + /** + * Mark the winning bid as used, should only be used in conjunction with video + * @typedef {Object} MarkBidRequest + * @property {string} adUnitCode The ad unit code + * @property {string} adId The id representing the ad we want to mark + * + * @alias module:pbjs.markWinningBidAsUsed + */ + pbjsInstance.markWinningBidAsUsed = function (markBidRequest) { + let bids = []; + + if (markBidRequest.adUnitCode && markBidRequest.adId) { + bids = auctionManager.getBidsReceived() + .filter(bid => bid.adId === markBidRequest.adId && bid.adUnitCode === markBidRequest.adUnitCode); + } else if (markBidRequest.adUnitCode) { + bids = targeting.getWinningBids(markBidRequest.adUnitCode); + } else if (markBidRequest.adId) { + bids = auctionManager.getBidsReceived().filter(bid => bid.adId === markBidRequest.adId); + } else { + logWarn('Improper use of markWinningBidAsUsed. It needs an adUnitCode or an adId to function.'); + } - if (bids.length > 0) { - bids[0].status = CONSTANTS.BID_STATUS.RENDERED; + if (bids.length > 0) { + bids[0].status = CONSTANTS.BID_STATUS.RENDERED; + } } -}; +} /** * Get Prebid config options * @param {Object} options * @alias module:pbjs.getConfig */ -$$PREBID_GLOBAL$$.getConfig = config.getAnyConfig; -$$PREBID_GLOBAL$$.readConfig = config.readAnyConfig; -$$PREBID_GLOBAL$$.mergeConfig = config.mergeConfig; -$$PREBID_GLOBAL$$.mergeBidderConfig = config.mergeBidderConfig; +pbjsInstance.getConfig = config.getAnyConfig; +pbjsInstance.readConfig = config.readAnyConfig; +pbjsInstance.mergeConfig = config.mergeConfig; +pbjsInstance.mergeBidderConfig = config.mergeBidderConfig; /** * Set Prebid config options. @@ -1017,10 +1036,10 @@ $$PREBID_GLOBAL$$.mergeBidderConfig = config.mergeBidderConfig; * * @param {Object} options Global Prebid configuration object. Must be JSON - no JavaScript functions are allowed. */ -$$PREBID_GLOBAL$$.setConfig = config.setConfig; -$$PREBID_GLOBAL$$.setBidderConfig = config.setBidderConfig; +pbjsInstance.setConfig = config.setConfig; +pbjsInstance.setBidderConfig = config.setBidderConfig; -$$PREBID_GLOBAL$$.que.push(() => listenMessagesFromCreative()); +pbjsInstance.que.push(() => listenMessagesFromCreative()); /** * This queue lets users load Prebid asynchronously, but run functions the same way regardless of whether it gets loaded @@ -1042,7 +1061,7 @@ $$PREBID_GLOBAL$$.que.push(() => listenMessagesFromCreative()); * the Prebid script has been fully loaded. * @alias module:pbjs.cmd.push */ -$$PREBID_GLOBAL$$.cmd.push = function (command) { +pbjsInstance.cmd.push = function (command) { if (typeof command === 'function') { try { command.call(); @@ -1054,7 +1073,7 @@ $$PREBID_GLOBAL$$.cmd.push = function (command) { } }; -$$PREBID_GLOBAL$$.que.push = $$PREBID_GLOBAL$$.cmd.push; +pbjsInstance.que.push = pbjsInstance.cmd.push; function processQueue(queue) { queue.forEach(function (cmd) { @@ -1072,10 +1091,10 @@ function processQueue(queue) { /** * @alias module:pbjs.processQueue */ -$$PREBID_GLOBAL$$.processQueue = function () { +pbjsInstance.processQueue = function () { hook.ready(); - processQueue($$PREBID_GLOBAL$$.que); - processQueue($$PREBID_GLOBAL$$.cmd); + processQueue(pbjsInstance.que); + processQueue(pbjsInstance.cmd); }; -export default $$PREBID_GLOBAL$$; +export default pbjsInstance; diff --git a/src/prebidGlobal.js b/src/prebidGlobal.js index 5eed4b3670f..4cbc3e10ad1 100644 --- a/src/prebidGlobal.js +++ b/src/prebidGlobal.js @@ -1,13 +1,21 @@ // if $$PREBID_GLOBAL$$ already exists in global document scope, use it, if not, create the object // global defination should happen BEFORE imports to avoid global undefined errors. -window.$$PREBID_GLOBAL$$ = (window.$$PREBID_GLOBAL$$ || {}); -window.$$PREBID_GLOBAL$$.cmd = window.$$PREBID_GLOBAL$$.cmd || []; -window.$$PREBID_GLOBAL$$.que = window.$$PREBID_GLOBAL$$.que || []; +/* global $$DEFINE_PREBID_GLOBAL$$ */ +const scope = !$$DEFINE_PREBID_GLOBAL$$ ? {} : window; +const global = scope.$$PREBID_GLOBAL$$ = scope.$$PREBID_GLOBAL$$ || {}; +global.cmd = global.cmd || []; +global.que = global.que || []; // create a pbjs global pointer -window._pbjsGlobals = window._pbjsGlobals || []; -window._pbjsGlobals.push('$$PREBID_GLOBAL$$'); +if (scope === window) { + scope._pbjsGlobals = scope._pbjsGlobals || []; + scope._pbjsGlobals.push('$$PREBID_GLOBAL$$'); +} export function getGlobal() { - return window.$$PREBID_GLOBAL$$; + return global; +} + +export function registerModule(name) { + global.installedModules.push(name); } diff --git a/src/refererDetection.js b/src/refererDetection.js index e0cb15522cc..93ebf085dd5 100644 --- a/src/refererDetection.js +++ b/src/refererDetection.js @@ -51,6 +51,26 @@ export function parseDomain(url, {noLeadingWww = false, noPort = false} = {}) { return url; } +/** + * This function returns canonical URL which refers to an HTML link element, with the attribute of rel="canonical", found in the element of your webpage + * + * @param {Object} doc document + * @returns {string|null} + */ +function getCanonicalUrl(doc) { + try { + const element = doc.querySelector("link[rel='canonical']"); + + if (element !== null) { + return element.href; + } + } catch (e) { + // Ignore error + } + + return null; +} + /** * @param {Window} win Window * @returns {Function} @@ -75,26 +95,6 @@ export function detectReferer(win) { } } - /** - * This function returns canonical URL which refers to an HTML link element, with the attribute of rel="canonical", found in the element of your webpage - * - * @param {Object} doc document - * @returns {string|null} - */ - function getCanonicalUrl(doc) { - try { - const element = doc.querySelector("link[rel='canonical']"); - - if (element !== null) { - return element.href; - } - } catch (e) { - // Ignore error - } - - return null; - } - // TODO: the meaning of "reachedTop" seems to be intentionally ambiguous - best to leave them out of // the typedef for now. (for example, unit tests enforce that "reachedTop" should be false in some situations where we // happily provide a location for the top). @@ -260,7 +260,25 @@ export function detectReferer(win) { return refererInfo; } +// cache result of fn (= referer info) as long as: +// - we are the top window +// - canonical URL tag and window location have not changed +export function cacheWithLocation(fn, win = window) { + if (win.top !== win) return fn; + let canonical, href, value; + return function () { + const newCanonical = getCanonicalUrl(win.document); + const newHref = win.location.href; + if (canonical !== newCanonical || newHref !== href) { + canonical = newCanonical; + href = newHref; + value = fn(); + } + return value; + } +} + /** * @type {function(): refererInfo} */ -export const getRefererInfo = detectReferer(window); +export const getRefererInfo = cacheWithLocation(detectReferer(window)); diff --git a/src/secureCreatives.js b/src/secureCreatives.js index 82d331988fc..c719bc191f2 100644 --- a/src/secureCreatives.js +++ b/src/secureCreatives.js @@ -73,7 +73,7 @@ function handleRenderRequest(reply, data, adObject) { if (adObject == null) { emitAdRenderFail({ reason: constants.AD_RENDER_FAILED_REASON.CANNOT_FIND_AD, - message: `Cannot find ad '${data.adId}' for cross-origin render request`, + message: `Cannot find ad for cross-origin render request: '${data.adId}'`, id: data.adId }); return; @@ -111,7 +111,7 @@ function handleNativeRequest(reply, data, adObject) { // adId: '%%PATTERN:hb_adid%%' // }), '*'); if (adObject == null) { - logError(`Cannot find ad '${data.adId}' for x-origin event request`); + logError(`Cannot find ad for x-origin event request: '${data.adId}'`); return; } diff --git a/src/storageManager.js b/src/storageManager.js index eaf35603c60..0248237fbc4 100644 --- a/src/storageManager.js +++ b/src/storageManager.js @@ -1,46 +1,30 @@ import {hook} from './hook.js'; -import {hasDeviceAccess, checkCookieSupport, logError, logInfo, isPlainObject} from './utils.js'; +import {checkCookieSupport, hasDeviceAccess, logError, logInfo} from './utils.js'; import {bidderSettings as defaultBidderSettings} from './bidderSettings.js'; -import {VENDORLESS_GVLID} from './consentHandler.js'; +import {MODULE_TYPE_BIDDER, MODULE_TYPE_CORE} from './activities/modules.js'; -const moduleTypeWhiteList = ['core', 'prebid-module']; +export const STORAGE_TYPE_LOCALSTORAGE = 'html5'; +export const STORAGE_TYPE_COOKIES = 'cookie'; export let storageCallbacks = []; -/** - * Storage options - * @typedef {Object} storageOptions - * @property {Number=} gvlid - Vendor id - * @property {string} moduleName? - Module name - * @property {string=} bidderCode? - Bidder code - * @property {string=} moduleType - Module type, value can be anyone of core or prebid-module +/* + * Storage manager constructor. Consumers should prefer one of `getStorageManager` or `getCoreStorageManager`. */ - -/** - * Returns list of storage related functions with gvlid, module name and module type in its scope. - * All three argument are optional here. Below shows the usage of of these - * - GVL Id: Pass GVL id if you are a vendor - * - Bidder code: All bid adapters need to pass bidderCode - * - Module name: All other modules need to pass module name - * - Module type: Some modules may need these functions but are not vendor. e.g prebid core files in src and modules like currency. - * @param {storageOptions} options - */ -export function newStorageManager({gvlid, moduleName, bidderCode, moduleType} = {}, {bidderSettings = defaultBidderSettings} = {}) { - function isBidderAllowed() { - if (bidderCode == null) { +export function newStorageManager({moduleName, moduleType} = {}, {bidderSettings = defaultBidderSettings} = {}) { + function isBidderAllowed(storageType) { + if (moduleType !== MODULE_TYPE_BIDDER) { return true; } - const storageAllowed = bidderSettings.get(bidderCode, 'storageAllowed'); - return storageAllowed == null ? false : storageAllowed; - } - - if (moduleTypeWhiteList.includes(moduleType)) { - gvlid = gvlid || VENDORLESS_GVLID; + const storageAllowed = bidderSettings.get(moduleName, 'storageAllowed'); + if (!storageAllowed || storageAllowed === true) return !!storageAllowed; + if (Array.isArray(storageAllowed)) return storageAllowed.some((e) => e === storageType); + return storageAllowed === storageType; } - function isValid(cb) { - if (!isBidderAllowed()) { - logInfo(`bidderSettings denied access to device storage for bidder '${bidderCode}'`); + function isValid(cb, storageType) { + if (!isBidderAllowed(storageType)) { + logInfo(`bidderSettings denied access to device storage for bidder '${moduleName}'`); const result = {valid: false}; return cb(result); } else { @@ -48,7 +32,7 @@ export function newStorageManager({gvlid, moduleName, bidderCode, moduleType} = let hookDetails = { hasEnforcementHook: false } - validateStorageEnforcement(gvlid, bidderCode || moduleName, hookDetails, function(result) { + validateStorageEnforcement(moduleType, moduleName, hookDetails, function(result) { if (result && result.hasEnforcementHook) { value = cb(result); } else { @@ -63,6 +47,17 @@ export function newStorageManager({gvlid, moduleName, bidderCode, moduleType} = } } + function schedule(operation, storageType, done) { + if (done && typeof done === 'function') { + storageCallbacks.push(function() { + let result = isValid(operation, storageType); + done(result); + }); + } else { + return isValid(operation, storageType); + } + } + /** * @param {string} key * @param {string} value @@ -83,14 +78,7 @@ export function newStorageManager({gvlid, moduleName, bidderCode, moduleType} = document.cookie = `${key}=${encodeURIComponent(value)}${expiresPortion}; path=/${domainPortion}${sameSite ? `; SameSite=${sameSite}` : ''}${secure}`; } } - if (done && typeof done === 'function') { - storageCallbacks.push(function() { - let result = isValid(cb); - done(result); - }); - } else { - return isValid(cb); - } + return schedule(cb, STORAGE_TYPE_COOKIES, done); }; /** @@ -105,14 +93,7 @@ export function newStorageManager({gvlid, moduleName, bidderCode, moduleType} = } return null; } - if (done && typeof done === 'function') { - storageCallbacks.push(function() { - let result = isValid(cb); - done(result); - }); - } else { - return isValid(cb); - } + return schedule(cb, STORAGE_TYPE_COOKIES, done); }; /** @@ -133,14 +114,7 @@ export function newStorageManager({gvlid, moduleName, bidderCode, moduleType} = } return false; } - if (done && typeof done === 'function') { - storageCallbacks.push(function() { - let result = isValid(cb); - done(result); - }); - } else { - return isValid(cb); - } + return schedule(cb, STORAGE_TYPE_LOCALSTORAGE, done); } /** @@ -153,14 +127,7 @@ export function newStorageManager({gvlid, moduleName, bidderCode, moduleType} = } return false; } - if (done && typeof done === 'function') { - storageCallbacks.push(function() { - let result = isValid(cb); - done(result); - }); - } else { - return isValid(cb); - } + return schedule(cb, STORAGE_TYPE_COOKIES, done); } /** @@ -173,14 +140,7 @@ export function newStorageManager({gvlid, moduleName, bidderCode, moduleType} = window.localStorage.setItem(key, value); } } - if (done && typeof done === 'function') { - storageCallbacks.push(function() { - let result = isValid(cb); - done(result); - }); - } else { - return isValid(cb); - } + return schedule(cb, STORAGE_TYPE_LOCALSTORAGE, done); } /** @@ -194,14 +154,7 @@ export function newStorageManager({gvlid, moduleName, bidderCode, moduleType} = } return null; } - if (done && typeof done === 'function') { - storageCallbacks.push(function() { - let result = isValid(cb); - done(result); - }); - } else { - return isValid(cb); - } + return schedule(cb, STORAGE_TYPE_LOCALSTORAGE, done); } /** @@ -213,14 +166,7 @@ export function newStorageManager({gvlid, moduleName, bidderCode, moduleType} = window.localStorage.removeItem(key); } } - if (done && typeof done === 'function') { - storageCallbacks.push(function() { - let result = isValid(cb); - done(result); - }); - } else { - return isValid(cb); - } + return schedule(cb, STORAGE_TYPE_LOCALSTORAGE, done); } /** @@ -237,14 +183,7 @@ export function newStorageManager({gvlid, moduleName, bidderCode, moduleType} = } return false; } - if (done && typeof done === 'function') { - storageCallbacks.push(function() { - let result = isValid(cb); - done(result); - }); - } else { - return isValid(cb); - } + return schedule(cb, STORAGE_TYPE_LOCALSTORAGE, done); } /** @@ -273,14 +212,7 @@ export function newStorageManager({gvlid, moduleName, bidderCode, moduleType} = } } - if (done && typeof done === 'function') { - storageCallbacks.push(function() { - let result = isValid(cb); - done(result); - }); - } else { - return isValid(cb); - } + return schedule(cb, STORAGE_TYPE_COOKIES, done); } return { @@ -299,31 +231,38 @@ export function newStorageManager({gvlid, moduleName, bidderCode, moduleType} = /** * This hook validates the storage enforcement if gdprEnforcement module is included */ -export const validateStorageEnforcement = hook('async', function(gvlid, moduleName, hookDetails, callback) { +export const validateStorageEnforcement = hook('async', function(moduleType, moduleName, hookDetails, callback) { callback(hookDetails); }, 'validateStorageEnforcement'); /** - * This function returns storage functions to access cookies and localstorage. This function will bypass the gdpr enforcement requirement. Prebid as a software needs to use storage in some scenarios and is not a vendor so GDPR enforcement rules does not apply on Prebid. - * @param {string} moduleName Module name + * Get a storage manager for a particular module. + * + * Either bidderCode or a combination of moduleType + moduleName must be provided. The former is a shorthand + * for `{moduleType: 'bidder', moduleName: bidderCode}`. + * */ -export function getCoreStorageManager(moduleName) { - return newStorageManager({moduleName: moduleName, moduleType: 'core'}); +export function getStorageManager({moduleType, moduleName, bidderCode} = {}) { + function err() { + throw new Error(`Invalid invocation for getStorageManager: must set either bidderCode, or moduleType + moduleName`) + } + if (bidderCode) { + if ((moduleType && moduleType !== MODULE_TYPE_BIDDER) || moduleName) err() + moduleType = MODULE_TYPE_BIDDER; + moduleName = bidderCode; + } else if (!moduleName || !moduleType) { + err() + } + return newStorageManager({moduleType, moduleName}); } /** - * Note: Core modules or Prebid modules like Currency, SizeMapping should use getCoreStorageManager - * This function returns storage functions to access cookies and localstorage. Bidders and User id modules should import this and use it in their module if needed. - * Bid adapters should always provide `bidderCode`. GVL ID and Module name are optional param but gvl id is needed for when gdpr enforcement module is used. - * @param {Number=} gvlid? Vendor id - required for proper GDPR integration - * @param {string=} bidderCode? - required for bid adapters - * @param {string=} moduleName? module name + * Get a storage manager for "core" (vendorless, or first-party) modules. Shorthand for `getStorageManager({moduleName, moduleType: 'core'})`. + * + * @param {string} moduleName Module name */ -export function getStorageManager({gvlid, moduleName, bidderCode} = {}) { - if (arguments.length > 1 || (arguments.length > 0 && !isPlainObject(arguments[0]))) { - throw new Error('Invalid invocation for getStorageManager') - } - return newStorageManager({gvlid, moduleName, bidderCode}); +export function getCoreStorageManager(moduleName) { + return newStorageManager({moduleName: moduleName, moduleType: MODULE_TYPE_CORE}); } export function resetData() { diff --git a/src/userSync.js b/src/userSync.js index dec8f650930..ed3cbb5d5f6 100644 --- a/src/userSync.js +++ b/src/userSync.js @@ -109,10 +109,7 @@ export function newUserSync(userSyncDependencies) { // Randomize the order of the pixels before firing // This is to avoid giving any bidder who has registered multiple syncs // any preferential treatment and balancing them out - shuffle(queue).forEach((sync) => { - fn(sync); - hasFiredBidder.add(sync[0]); - }); + shuffle(queue).forEach(fn); } /** @@ -212,6 +209,12 @@ export function newUserSync(userSyncDependencies) { numAdapterBids = incrementAdapterBids(numAdapterBids, bidder); }; + /** + * Mark a bidder as done with its user syncs - no more will be accepted from them in this session. + * @param {string} bidderCode + */ + publicApi.bidderDone = hasFiredBidder.add.bind(hasFiredBidder); + /** * @function shouldBidderBeBlocked * @summary Check filterSettings logic to determine if the bidder should be prevented from registering their userSync tracker diff --git a/src/utils.js b/src/utils.js index 869e1007841..61cd136b850 100644 --- a/src/utils.js +++ b/src/utils.js @@ -3,6 +3,7 @@ import clone from 'just-clone'; import {find, includes} from './polyfill.js'; import CONSTANTS from './constants.json'; import {GreedyPromise} from './utils/promise.js'; +import {getGlobal} from './prebidGlobal.js'; export { default as deepAccess } from 'dlv/index.js'; export { dset as deepSetValue } from 'dset'; @@ -21,6 +22,8 @@ let consoleErrorExists = Boolean(consoleExists && window.console.error); let eventEmitter; +const pbjsInstance = getGlobal(); + export function _setEventEmitter(emitFn) { // called from events.js - this hoop is to avoid circular imports eventEmitter = emitFn; @@ -707,10 +710,10 @@ export function getKeyByValue(obj, value) { } } -export function getBidderCodes(adUnits = $$PREBID_GLOBAL$$.adUnits) { +export function getBidderCodes(adUnits = pbjsInstance.adUnits) { // this could memoize adUnits return adUnits.map(unit => unit.bids.map(bid => bid.bidder) - .reduce(flatten, [])).reduce(flatten, []).filter(uniques); + .reduce(flatten, [])).reduce(flatten, []).filter((bidder) => typeof bidder !== 'undefined').filter(uniques); } export function isGptPubadsDefined() { @@ -904,7 +907,7 @@ export function isValidMediaTypes(mediaTypes) { return false; } - if (mediaTypes.video && mediaTypes.video.context) { + if (FEATURES.VIDEO && mediaTypes.video && mediaTypes.video.context) { return includes(SUPPORTED_STREAM_TYPES, mediaTypes.video.context); } diff --git a/src/utils/cpm.js b/src/utils/cpm.js new file mode 100644 index 00000000000..07113e7c944 --- /dev/null +++ b/src/utils/cpm.js @@ -0,0 +1,17 @@ +import {auctionManager} from '../auctionManager.js'; +import {bidderSettings} from '../bidderSettings.js'; +import {logError} from '../utils.js'; + +export function adjustCpm(cpm, bidResponse, bidRequest, {index = auctionManager.index, bs = bidderSettings} = {}) { + bidRequest = bidRequest || index.getBidRequest(bidResponse); + const bidCpmAdjustment = bs.get(bidResponse?.bidderCode || bidRequest?.bidder, 'bidCpmAdjustment'); + + if (bidCpmAdjustment && typeof bidCpmAdjustment === 'function') { + try { + return bidCpmAdjustment(cpm, Object.assign({}, bidResponse), bidRequest); + } catch (e) { + logError('Error during bid adjustment', e); + } + } + return cpm; +} diff --git a/src/utils/promise.js b/src/utils/promise.js index 97c64a96f8b..0cf0a47eb8e 100644 --- a/src/utils/promise.js +++ b/src/utils/promise.js @@ -1,15 +1,12 @@ -import {getGlobal} from '../prebidGlobal.js'; - const SUCCESS = 0; const FAIL = 1; /** * A version of Promise that runs callbacks synchronously when it can (i.e. after it's been fulfilled or rejected). */ -export class GreedyPromise extends (getGlobal().Promise || Promise) { +export class GreedyPromise { #result; #callbacks; - #parent = null; /** * Convenience wrapper for setTimeout; takes care of returning an already fulfilled GreedyPromise when the delay is zero. @@ -24,53 +21,33 @@ export class GreedyPromise extends (getGlobal().Promise || Promise) { } constructor(resolver) { + if (typeof resolver !== 'function') { + throw new Error('resolver not a function'); + } const result = []; const callbacks = []; - function handler(type, resolveFn) { + let [resolve, reject] = [SUCCESS, FAIL].map((type) => { return function (value) { - if (!result.length) { + if (type === SUCCESS && typeof value?.then === 'function') { + value.then(resolve, reject); + } else if (!result.length) { result.push(type, value); while (callbacks.length) callbacks.shift()(); - resolveFn(value); } } + }); + try { + resolver(resolve, reject); + } catch (e) { + reject(e); } - super( - typeof resolver !== 'function' - ? resolver // let super throw an error - : (resolve, reject) => { - const rejectHandler = handler(FAIL, reject); - const resolveHandler = (() => { - const done = handler(SUCCESS, resolve); - return value => - typeof value?.then === 'function' ? value.then(done, rejectHandler) : done(value); - })(); - try { - resolver(resolveHandler, rejectHandler); - } catch (e) { - rejectHandler(e); - } - } - ); this.#result = result; this.#callbacks = callbacks; } + then(onSuccess, onError) { - if (typeof onError === 'function') { - // if an error handler is provided, attach a dummy error handler to super, - // and do the same for all promises without an error handler that precede this one in a chain. - // This is to avoid unhandled rejection events / warnings for errors that were, in fact, handled; - // since we are not using super's callback mechanisms we need to make it aware of this separately. - let node = this; - while (node) { - super.then.call(node, null, () => null); - const next = node.#parent; - node.#parent = null; // since we attached a handler already, we are no longer interested in what will happen later in the chain - node = next; - } - } const result = this.#result; - const res = new GreedyPromise((resolve, reject) => { + return new this.constructor((resolve, reject) => { const continuation = () => { let value = result[1]; let [handler, resolveFn] = result[0] === SUCCESS ? [onSuccess, resolve] : [onError, reject]; @@ -87,8 +64,58 @@ export class GreedyPromise extends (getGlobal().Promise || Promise) { } result.length ? continuation() : this.#callbacks.push(continuation); }); - res.#parent = this; - return res; + } + + catch(onError) { + return this.then(null, onError); + } + + finally(onFinally) { + let val; + return this.then( + (v) => { val = v; return onFinally(); }, + (e) => { val = this.constructor.reject(e); return onFinally() } + ).then(() => val); + } + + static #collect(promises, collector, done) { + let cnt = promises.length; + function clt() { + collector.apply(this, arguments); + if (--cnt <= 0 && done) done(); + } + promises.length === 0 && done ? done() : promises.forEach((p, i) => this.resolve(p).then( + (val) => clt(true, val, i), + (err) => clt(false, err, i) + )); + } + + static race(promises) { + return new this((resolve, reject) => { + this.#collect(promises, (success, result) => success ? resolve(result) : reject(result)); + }) + } + + static all(promises) { + return new this((resolve, reject) => { + let res = []; + this.#collect(promises, (success, val, i) => success ? res[i] = val : reject(val), () => resolve(res)); + }) + } + + static allSettled(promises) { + return new this((resolve) => { + let res = []; + this.#collect(promises, (success, val, i) => res[i] = success ? {status: 'fulfilled', value: val} : {status: 'rejected', reason: val}, () => resolve(res)) + }) + } + + static resolve(value) { + return new this(resolve => resolve(value)) + } + + static reject(error) { + return new this((resolve, reject) => reject(error)) } } diff --git a/src/videoCache.js b/src/videoCache.js index f69a20f0139..88fc27625fd 100644 --- a/src/videoCache.js +++ b/src/videoCache.js @@ -9,8 +9,8 @@ * This trickery helps integrate with ad servers, which set character limits on request params. */ -import { ajax } from './ajax.js'; -import { config } from './config.js'; +import {ajaxBuilder} from './ajax.js'; +import {config} from './config.js'; import {auctionManager} from './auctionManager.js'; /** @@ -142,11 +142,11 @@ function shimStorageCallback(done) { * @param {videoCacheStoreCallback} [done] An optional callback which should be executed after * the data has been stored in the cache. */ -export function store(bids, done) { +export function store(bids, done, getAjax = ajaxBuilder) { const requestData = { puts: bids.map(toStorageRequest) }; - + const ajax = getAjax(config.getConfig('cache.timeout')); ajax(config.getConfig('cache.url'), shimStorageCallback(done), JSON.stringify(requestData), { contentType: 'text/plain', withCredentials: true diff --git a/test/helpers/fpd.js b/test/helpers/fpd.js new file mode 100644 index 00000000000..89755f26541 --- /dev/null +++ b/test/helpers/fpd.js @@ -0,0 +1,71 @@ +import {dep, enrichFPD} from 'src/fpd/enrichment.js'; +import {GreedyPromise} from '../../src/utils/promise.js'; +import {deepClone} from '../../src/utils.js'; +import {gdprDataHandler, uspDataHandler} from '../../src/adapterManager.js'; + +export function mockFpdEnrichments(sandbox, overrides = {}) { + overrides = Object.assign({}, { + // override window getters, required for ChromeHeadless, apparently it sees window.self !== window + getWindowTop() { + return window + }, + getWindowSelf() { + return window + }, + getHighEntropySUA() { + return GreedyPromise.resolve() + } + }, overrides) + Object.entries(overrides) + .filter(([k]) => dep[k]) + .forEach(([k, v]) => { + sandbox.stub(dep, k).callsFake(v); + }); + Object.entries({ + gdprConsent: gdprDataHandler, + uspConsent: uspDataHandler, + }).forEach(([ovKey, handler]) => { + const v = overrides[ovKey]; + if (v) { + sandbox.stub(handler, 'getConsentData').callsFake(v); + } + }) +} + +export function addFPDEnrichments(ortb2 = {}, overrides) { + const sandbox = sinon.sandbox.create(); + mockFpdEnrichments(sandbox, overrides) + return enrichFPD(GreedyPromise.resolve(deepClone(ortb2))).finally(() => sandbox.restore()); +} + +export const syncAddFPDEnrichments = synchronize(addFPDEnrichments); + +export function addFPDToBidderRequest(bidderRequest, overrides) { + overrides = Object.assign({}, { + getRefererInfo() { + return bidderRequest.refererInfo || {}; + }, + gdprConsent() { + return bidderRequest.gdprConsent; + }, + uspConsent() { + return bidderRequest.uspConsent; + } + }, overrides); + return addFPDEnrichments(bidderRequest.ortb2 || {}, overrides).then(ortb2 => { + return { + ...bidderRequest, + ortb2 + } + }); +} + +export const syncAddFPDToBidderRequest = synchronize(addFPDToBidderRequest); + +function synchronize(fn) { + return function () { + let result; + fn.apply(this, arguments).then(res => { result = res }); + return result; + } +} diff --git a/test/helpers/hookSetup.js b/test/helpers/hookSetup.js new file mode 100644 index 00000000000..2de35bb1dd4 --- /dev/null +++ b/test/helpers/hookSetup.js @@ -0,0 +1,5 @@ +import {hook} from '../../src/hook.js'; + +before(() => { + hook.ready(); +}); diff --git a/test/mocks/timers.js b/test/mocks/timers.js new file mode 100644 index 00000000000..6efd798c881 --- /dev/null +++ b/test/mocks/timers.js @@ -0,0 +1,84 @@ +/* + * Provides wrappers for timers to allow easy cancelling and/or awaiting of outstanding timers. + * This helps avoid functionality leaking from one test to the next. + */ + +let wrappersActive = false; + +export function configureTimerInterceptors(debugLog = function() {}, generateStackTraces = false) { + if (wrappersActive) throw new Error(`Timer wrappers are already in place.`); + wrappersActive = true; + let theseWrappersActive = true; + + let originalSetTimeout = setTimeout, originalSetInterval = setInterval, originalClearTimeout = clearTimeout, originalClearInterval = clearInterval; + + let timerId = -1; + let timers = []; + + const waitOnTimersResolves = []; + function checkWaits() { + if (timers.length === 0) waitOnTimersResolves.forEach((r) => r()); + } + const waitAllActiveTimers = () => timers.length === 0 ? Promise.resolve() : new Promise((resolve) => waitOnTimersResolves.push(resolve)); + const clearAllActiveTimers = () => timers.forEach((timer) => timer.type === 'timeout' ? clearTimeout(timer.handle) : clearInterval(timer.handle)); + + const generateInterceptor = (type, originalFunctionWrapper) => (fn, delay, ...args) => { + timerId++; + debugLog(`Setting wrapped timeout ${timerId} for ${delay ?? 0}`); + const info = { timerId, type }; + if (generateStackTraces) { + try { + throw new Error(); + } catch (ex) { + info.stack = ex.stack; + } + } + info.handle = originalFunctionWrapper(info, fn, delay, ...args); + timers.push(info); + return info.handle; + }; + const setTimeoutInterceptor = generateInterceptor('timeout', (info, fn, delay, ...args) => originalSetTimeout(() => { + try { + debugLog(`Running timeout ${info.timerId}`); + fn(...args); + } finally { + const infoIndex = timers.indexOf(info); + if (infoIndex > -1) timers.splice(infoIndex, 1); + checkWaits(); + } + }, delay)); + + const setIntervalInterceptor = generateInterceptor('interval', (info, fn, interval, ...args) => originalSetInterval(() => { + debugLog(`Running interval ${info.timerId}`); + fn(...args); + }, interval)); + + const generateClearInterceptor = (type, originalClearFunction) => (handle) => { + originalClearFunction(handle); + const infoIndex = timers.findIndex((i) => i.handle === handle && i.type === type); + if (infoIndex > -1) timers.splice(infoIndex, 1); + checkWaits(); + } + const clearTimeoutInterceptor = generateClearInterceptor('timeout', originalClearTimeout); + const clearIntervalInterceptor = generateClearInterceptor('interval', originalClearInterval); + + setTimeout = setTimeoutInterceptor; + setInterval = setIntervalInterceptor; + clearTimeout = clearTimeoutInterceptor; + clearInterval = clearIntervalInterceptor; + + return { + waitAllActiveTimers, + clearAllActiveTimers, + timers, + restore: () => { + if (theseWrappersActive) { + theseWrappersActive = false; + setTimeout = originalSetTimeout; + setInterval = originalSetInterval; + clearTimeout = originalClearTimeout; + clearInterval = originalClearInterval; + } + } + } +} diff --git a/test/spec/auctionmanager_spec.js b/test/spec/auctionmanager_spec.js index e1ecf801aa3..3e5a39e5ee5 100644 --- a/test/spec/auctionmanager_spec.js +++ b/test/spec/auctionmanager_spec.js @@ -232,22 +232,33 @@ describe('auctionmanager.js', function () { assert.deepEqual(response, expected); }); - it('No bidder level configuration defined - default for video', function () { - config.setConfig({ - cache: { - url: 'https://prebid.adnxs.com/pbc/v1/cache' - } - }); - $$PREBID_GLOBAL$$.bidderSettings = {}; - let videoBid = utils.deepClone(bid); - videoBid.mediaType = 'video'; - videoBid.videoCacheKey = 'abc123def'; - - let expected = getDefaultExpected(videoBid); - let response = getKeyValueTargetingPairs(videoBid.bidderCode, videoBid); + it('should suppress acat if undefined', function () { + const noAcatBid = deepClone(DEFAULT_BID); + noAcatBid.meta.primaryCatId = '' + let expected = getDefaultExpected(noAcatBid); + delete expected.hb_acat; + let response = getKeyValueTargetingPairs(noAcatBid.bidderCode, noAcatBid); assert.deepEqual(response, expected); }); + if (FEATURES.VIDEO) { + it('No bidder level configuration defined - default for video', function () { + config.setConfig({ + cache: { + url: 'https://prebid.adnxs.com/pbc/v1/cache' + } + }); + $$PREBID_GLOBAL$$.bidderSettings = {}; + let videoBid = utils.deepClone(bid); + videoBid.mediaType = 'video'; + videoBid.videoCacheKey = 'abc123def'; + + let expected = getDefaultExpected(videoBid); + let response = getKeyValueTargetingPairs(videoBid.bidderCode, videoBid); + assert.deepEqual(response, expected); + }); + } + it('Custom configuration for all bidders', function () { $$PREBID_GLOBAL$$.bidderSettings = { @@ -311,17 +322,18 @@ describe('auctionmanager.js', function () { assert.deepEqual(response, expected); }); - it('Custom configuration for all bidders with video bid', function () { - config.setConfig({ - cache: { - url: 'https://prebid.adnxs.com/pbc/v1/cache' - } - }); - let videoBid = utils.deepClone(bid); - videoBid.mediaType = 'video'; - videoBid.videoCacheKey = 'abc123def'; + if (FEATURES.VIDEO) { + it('Custom configuration for all bidders with video bid', function () { + config.setConfig({ + cache: { + url: 'https://prebid.adnxs.com/pbc/v1/cache' + } + }); + let videoBid = utils.deepClone(bid); + videoBid.mediaType = 'video'; + videoBid.videoCacheKey = 'abc123def'; - $$PREBID_GLOBAL$$.bidderSettings = + $$PREBID_GLOBAL$$.bidderSettings = { standard: { adserverTargeting: [ @@ -387,11 +399,12 @@ describe('auctionmanager.js', function () { } }; - let expected = getDefaultExpected(videoBid); + let expected = getDefaultExpected(videoBid); - let response = getKeyValueTargetingPairs(videoBid.bidderCode, videoBid); - assert.deepEqual(response, expected); - }); + let response = getKeyValueTargetingPairs(videoBid.bidderCode, videoBid); + assert.deepEqual(response, expected); + }); + } it('Custom configuration for one bidder', function () { $$PREBID_GLOBAL$$.bidderSettings = @@ -759,6 +772,20 @@ describe('auctionmanager.js', function () { sinon.assert.calledWith(stubMakeBidRequests, ...anyArgs.slice(0, 5).concat([sinon.match.same(ortb2Fragments)])); sinon.assert.calledWith(stubCallAdapters, ...anyArgs.slice(0, 7).concat([sinon.match.same(ortb2Fragments)])); }); + + it('correctly adds nonbids when they are emitted', () => { + const ortb2Fragments = { + global: {}, + bidder: {} + } + const auction = auctionManager.createAuction({adUnits, ortb2Fragments}); + expect(auction.getNonBids()[0]).to.equal(undefined); + events.emit(CONSTANTS.EVENTS.SEAT_NON_BID, { + auctionId: auction.getAuctionId(), + seatnonbid: ['test'] + }); + expect(auction.getNonBids()[0]).to.equal('test'); + }); }); describe('addBidResponse #1', function () { @@ -1376,8 +1403,8 @@ describe('auctionmanager.js', function () { assert.equal(addedBid.native.title, 'Sample title') assert.equal(addedBid.native.sponsoredBy, 'Sample sponsoredBy') assert.equal(addedBid.native.clickUrl, 'http://www.click.com') - assert.equal(addedBid.native.image, 'https://www.example.com/image.png') - assert.equal(addedBid.native.icon, 'https://www.example.com/icon.png') + assert.equal(addedBid.native.image.url, 'https://www.example.com/image.png') + assert.equal(addedBid.native.icon.url, 'https://www.example.com/icon.png') assert.equal(addedBid.native.impressionTrackers[0], 'http://www.imptracker.com') assert.equal(addedBid.native.javascriptTrackers, '') }); @@ -1385,63 +1412,65 @@ describe('auctionmanager.js', function () { } describe('getMediaTypeGranularity', function () { - it('video', function () { - let mediaTypes = { video: {id: '1'} }; - - // mediaType is video and video.context is undefined - expect(getMediaTypeGranularity('video', mediaTypes, { - banner: 'low', - video: 'medium' - })).to.equal('medium'); - - expect(getMediaTypeGranularity('video', {}, { - banner: 'low', - video: 'medium' - })).to.equal('medium'); - `` - expect(getMediaTypeGranularity('video', undefined, { - banner: 'low', - video: 'medium' - })).to.equal('medium'); - - // also when mediaTypes.video is undefined - mediaTypes = { banner: {} }; - expect(getMediaTypeGranularity('video', mediaTypes, { - banner: 'low', - video: 'medium' - })).to.equal('medium'); - - // also when mediaTypes is undefined - expect(getMediaTypeGranularity('video', {}, { - banner: 'low', - video: 'medium' - })).to.equal('medium'); - }); + if (FEATURES.VIDEO) { + it('video', function () { + let mediaTypes = { video: {id: '1'} }; + + // mediaType is video and video.context is undefined + expect(getMediaTypeGranularity('video', mediaTypes, { + banner: 'low', + video: 'medium' + })).to.equal('medium'); + + expect(getMediaTypeGranularity('video', {}, { + banner: 'low', + video: 'medium' + })).to.equal('medium'); + `` + expect(getMediaTypeGranularity('video', undefined, { + banner: 'low', + video: 'medium' + })).to.equal('medium'); + + // also when mediaTypes.video is undefined + mediaTypes = { banner: {} }; + expect(getMediaTypeGranularity('video', mediaTypes, { + banner: 'low', + video: 'medium' + })).to.equal('medium'); + + // also when mediaTypes is undefined + expect(getMediaTypeGranularity('video', {}, { + banner: 'low', + video: 'medium' + })).to.equal('medium'); + }); - it('video-outstream', function () { - let mediaTypes = { video: { context: 'outstream' } }; + it('video-outstream', function () { + let mediaTypes = { video: { context: 'outstream' } }; - expect(getMediaTypeGranularity('video', mediaTypes, { - 'banner': 'low', 'video': 'medium', 'video-outstream': 'high' - })).to.equal('high'); - }); + expect(getMediaTypeGranularity('video', mediaTypes, { + 'banner': 'low', 'video': 'medium', 'video-outstream': 'high' + })).to.equal('high'); + }); - it('video-instream', function () { - let mediaTypes = { video: { context: 'instream' } }; + it('video-instream', function () { + let mediaTypes = { video: { context: 'instream' } }; - expect(getMediaTypeGranularity('video', mediaTypes, { - banner: 'low', video: 'medium', 'video-instream': 'high' - })).to.equal('high'); + expect(getMediaTypeGranularity('video', mediaTypes, { + banner: 'low', video: 'medium', 'video-instream': 'high' + })).to.equal('high'); - // fall back to video if video-instream not found - expect(getMediaTypeGranularity('video', mediaTypes, { - banner: 'low', video: 'medium' - })).to.equal('medium'); + // fall back to video if video-instream not found + expect(getMediaTypeGranularity('video', mediaTypes, { + banner: 'low', video: 'medium' + })).to.equal('medium'); - expect(getMediaTypeGranularity('video', {mediaTypes: {banner: {}}}, { - banner: 'low', video: 'medium' - })).to.equal('medium'); - }); + expect(getMediaTypeGranularity('video', {mediaTypes: {banner: {}}}, { + banner: 'low', video: 'medium' + })).to.equal('medium'); + }); + } it('native', function () { expect(getMediaTypeGranularity('native', {native: {}}, { @@ -1543,34 +1572,36 @@ describe('auctionmanager.js', function () { }); }) - it('should call auction done after prebid cache is complete for mediaType video', function() { - bids[0].mediaType = 'video'; - let bids1 = [mockBid({ bidderCode: BIDDER_CODE1 })]; + if (FEATURES.VIDEO) { + it('should call auction done after prebid cache is complete for mediaType video', function() { + bids[0].mediaType = 'video'; + let bids1 = [mockBid({ bidderCode: BIDDER_CODE1 })]; - let opts = { - mediaType: { - video: { - context: 'instream', - playerSize: [640, 480], - }, - } - }; - bidRequests = [ - mockBidRequest(bids[0], opts), - mockBidRequest(bids1[0], { adUnitCode: ADUNIT_CODE1 }), - ]; + let opts = { + mediaType: { + video: { + context: 'instream', + playerSize: [640, 480], + }, + } + }; + bidRequests = [ + mockBidRequest(bids[0], opts), + mockBidRequest(bids1[0], { adUnitCode: ADUNIT_CODE1 }), + ]; - let cbs = auctionCallbacks(doneSpy, auction); - cbs.addBidResponse.call(bidRequests[0], ADUNIT_CODE, bids[0]); - cbs.adapterDone.call(bidRequests[0]); - cbs.addBidResponse.call(bidRequests[1], ADUNIT_CODE1, bids1[0]); - cbs.adapterDone.call(bidRequests[1]); - assert.equal(doneSpy.callCount, 0); - const uuid = 'c488b101-af3e-4a99-b538-00423e5a3371'; - const responseBody = `{"responses":[{"uuid":"${uuid}"}]}`; - server.requests[0].respond(200, { 'Content-Type': 'application/json' }, responseBody); - assert.equal(doneSpy.callCount, 1); - }); + let cbs = auctionCallbacks(doneSpy, auction); + cbs.addBidResponse.call(bidRequests[0], ADUNIT_CODE, bids[0]); + cbs.adapterDone.call(bidRequests[0]); + cbs.addBidResponse.call(bidRequests[1], ADUNIT_CODE1, bids1[0]); + cbs.adapterDone.call(bidRequests[1]); + assert.equal(doneSpy.callCount, 0); + const uuid = 'c488b101-af3e-4a99-b538-00423e5a3371'; + const responseBody = `{"responses":[{"uuid":"${uuid}"}]}`; + server.requests[0].respond(200, { 'Content-Type': 'application/json' }, responseBody); + assert.equal(doneSpy.callCount, 1); + }); + } it('should convert cpm to number', () => { auction.addBidReceived = sinon.spy(); diff --git a/test/spec/fpd/enrichment_spec.js b/test/spec/fpd/enrichment_spec.js new file mode 100644 index 00000000000..328846ca081 --- /dev/null +++ b/test/spec/fpd/enrichment_spec.js @@ -0,0 +1,320 @@ +import {dep, enrichFPD} from '../../../src/fpd/enrichment.js'; +import {hook} from '../../../src/hook.js'; +import {expect} from 'chai/index.mjs'; +import {config} from 'src/config.js'; +import * as utils from 'src/utils.js'; +import {CLIENT_SECTIONS} from '../../../src/fpd/oneClient.js'; + +describe('FPD enrichment', () => { + let sandbox; + before(() => { + hook.ready(); + }); + beforeEach(() => { + sandbox = sinon.sandbox.create(); + }); + afterEach(() => { + sandbox.restore(); + config.resetConfig(); + }); + + function fpd(ortb2 = {}) { + return enrichFPD(Promise.resolve(ortb2)); + } + + function mockWindow() { + return { + innerHeight: 1, + innerWidth: 1, + navigator: { + language: '' + }, + document: { + querySelector: sinon.stub() + } + }; + } + + function testWindows(getWindow, fn) { + Object.entries({ + 'top': ['getWindowTop', 'getWindowSelf'], + 'self': ['getWindowSelf', 'getWindowTop'] + }).forEach(([t, [winWorks, winThrows]]) => { + describe(`using window.${t}`, () => { + beforeEach(() => { + sandbox.stub(dep, winWorks).callsFake(getWindow); + sandbox.stub(dep, winThrows).throws(new Error()); + }); + fn(); + }); + }); + } + + CLIENT_SECTIONS.forEach(section => { + describe(`${section}, when set`, () => { + const ORTB2 = {[section]: {ext: {}}} + + it('sets domain and publisher.domain', () => { + const refererInfo = { + page: 'www.example.com', + }; + sandbox.stub(dep, 'getRefererInfo').callsFake(() => refererInfo); + sandbox.stub(dep, 'findRootDomain').callsFake((dom) => `publisher.${dom}`); + return fpd(ORTB2).then(ortb2 => { + sinon.assert.match(ortb2[section], { + domain: 'example.com', + publisher: { + domain: 'publisher.example.com' + } + }); + }); + }) + + describe('keywords', () => { + let metaTag; + beforeEach(() => { + metaTag = document.createElement('meta'); + metaTag.name = 'keywords'; + metaTag.content = 'kw1, kw2'; + document.head.appendChild(metaTag); + }); + afterEach(() => { + document.head.removeChild(metaTag); + }); + + testWindows(() => window, () => { + it(`sets kewwords from meta tag`, () => { + return fpd(ORTB2).then(ortb2 => { + expect(ortb2[section].keywords).to.eql('kw1,kw2'); + }); + }); + }); + }); + + it('should not set keywords if meta tag is not present', () => { + return fpd(ORTB2).then(ortb2 => { + expect(ortb2[section].hasOwnProperty('keywords')).to.be.false; + }); + }); + }) + }) + + describe('site', () => { + describe('when mixed with app/dooh', () => { + beforeEach(() => { + sinon.stub(utils, 'logWarn'); + }); + + afterEach(() => { + utils.logWarn.restore(); + }); + + ['dooh', 'app'].forEach(prop => { + it(`should not be set when ${prop} is set`, () => { + return fpd({[prop]: {foo: 'bar'}}).then(ortb2 => { + expect(ortb2.site).to.not.exist; + sinon.assert.notCalled(utils.logWarn); // make sure we don't generate "both site and app are set" warnings + }) + }) + }) + }) + + it('sets page, ref', () => { + const refererInfo = { + page: 'www.example.com', + ref: 'referrer.com' + }; + sandbox.stub(dep, 'getRefererInfo').callsFake(() => refererInfo); + return fpd().then(ortb2 => { + sinon.assert.match(ortb2.site, { + page: 'www.example.com', + ref: 'referrer.com', + }); + }); + }); + + it('respects pub-provided fpd', () => { + return fpd({ + site: { + publisher: { + domain: 'pub.com' + } + } + }).then(ortb2 => { + expect(ortb2.site.publisher.domain).to.eql('pub.com'); + }); + }); + + it('respects config set through setConfig({site})', () => { + sandbox.stub(dep, 'getRefererInfo').callsFake(() => ({ + page: 'www.example.com', + ref: 'referrer.com', + })); + config.setConfig({ + site: { + ref: 'override.com', + priority: 'lower' + } + }); + return fpd({site: {priority: 'highest'}}).then(ortb2 => { + sinon.assert.match(ortb2.site, { + page: 'www.example.com', + ref: 'override.com', + priority: 'highest' + }) + }) + }) + }); + + describe('device', () => { + let win; + beforeEach(() => { + win = mockWindow(); + }); + testWindows(() => win, () => { + it('sets w/h', () => { + win.innerHeight = 123; + win.innerWidth = 321; + return fpd().then(ortb2 => { + sinon.assert.match(ortb2.device, { + w: 321, + h: 123, + }); + }); + }); + + it('sets ua', () => { + win.navigator.userAgent = 'mock-ua'; + return fpd().then(ortb2 => { + expect(ortb2.device.ua).to.eql('mock-ua'); + }) + }); + + it('sets language', () => { + win.navigator.language = 'lang-ignored'; + return fpd().then(ortb2 => { + expect(ortb2.device.language).to.eql('lang'); + }) + }); + + it('respects setConfig({device})', () => { + win.navigator.userAgent = 'ua'; + win.navigator.language = 'lang'; + config.setConfig({ + device: { + language: 'override', + priority: 'lower' + } + }); + return fpd({device: {priority: 'highest'}}).then(ortb2 => { + sinon.assert.match(ortb2.device, { + language: 'override', + priority: 'highest', + ua: 'ua' + }) + }) + }) + }); + }); + + describe('app', () => { + it('respects setConfig({app})', () => { + config.setConfig({ + app: { + priority: 'lower', + prop: 'value' + } + }); + return fpd({app: {priority: 'highest'}}).then(ortb2 => { + sinon.assert.match(ortb2.app, { + priority: 'highest', + prop: 'value' + }) + }) + }) + }) + + describe('regs', () => { + describe('gpc', () => { + let win; + beforeEach(() => { + win = mockWindow(); + }); + testWindows(() => win, () => { + it('is set if globalPrivacyControl is set', () => { + win.navigator.globalPrivacyControl = true; + return fpd().then(ortb2 => { + expect(ortb2.regs.ext.gpc).to.eql(1); + }); + }); + + it('is not set otherwise', () => { + return fpd().then(ortb2 => { + expect(ortb2.regs?.ext?.gpc).to.not.exist; + }) + }) + }); + }) + describe('coppa', () => { + [[true, 1], [false, 0]].forEach(([cfgVal, regVal]) => { + it(`is set to ${regVal} if config = ${cfgVal}`, () => { + config.setConfig({coppa: cfgVal}); + return fpd().then(ortb2 => { + expect(ortb2.regs.coppa).to.eql(regVal); + }) + }); + }) + + it('is not set if not configured', () => { + return fpd().then(ortb2 => { + expect(ortb2.regs?.coppa).to.not.exist; + }) + }) + }); + }); + + describe('sua', () => { + it('does not set device.sua if resolved sua is null', () => { + sandbox.stub(dep, 'getHighEntropySUA').returns(Promise.resolve()) + return fpd().then(ortb2 => { + expect(ortb2.device.sua).to.not.exist; + }) + }); + it('uses low entropy values if uaHints is []', () => { + sandbox.stub(dep, 'getLowEntropySUA').callsFake(() => ({mock: 'sua'})); + config.setConfig({ + firstPartyData: { + uaHints: [], + } + }) + return fpd().then(ortb2 => { + expect(ortb2.device.sua).to.eql({mock: 'sua'}); + }) + }); + it('uses high entropy values otherwise', () => { + sandbox.stub(dep, 'getHighEntropySUA').callsFake((hints) => Promise.resolve({hints})); + config.setConfig({ + firstPartyData: { + uaHints: ['h1', 'h2'] + } + }); + return fpd().then(ortb2 => { + expect(ortb2.device.sua).to.eql({hints: ['h1', 'h2']}) + }) + }); + }); + + it('leaves only one of app, site, dooh', () => { + return fpd({ + app: {p: 'val'}, + site: {p: 'val'}, + dooh: {p: 'val'} + }).then(ortb2 => { + expect(ortb2.app).to.not.exist; + expect(ortb2.site).to.not.exist; + sinon.assert.match(ortb2.dooh, { + p: 'val' + }) + }); + }) +}); diff --git a/test/spec/fpd/gdpr_spec.js b/test/spec/fpd/gdpr_spec.js new file mode 100644 index 00000000000..8fc04815112 --- /dev/null +++ b/test/spec/fpd/gdpr_spec.js @@ -0,0 +1,47 @@ +import {gdprDataHandler} from '../../../src/adapterManager.js'; +import {enrichFPDHook} from '../../../modules/consentManagement.js'; + +describe('GDPR FPD enrichment', () => { + let sandbox, consent; + beforeEach(() => { + consent = null; + sandbox = sinon.sandbox.create(); + sandbox.stub(gdprDataHandler, 'getConsentData').callsFake(() => consent); + }); + afterEach(() => { + sandbox.restore(); + }) + + function callHook() { + let result; + enrichFPDHook((res) => { result = res }, Promise.resolve({})); + return result; + } + + it('sets gdpr properties from gdprDataHandler', () => { + consent = {gdprApplies: true, consentString: 'consent'}; + return callHook().then(ortb2 => { + expect(ortb2.regs.ext.gdpr).to.eql(1); + expect(ortb2.user.ext.consent).to.eql('consent'); + }) + }); + + it('does not set it if missing', () => { + return callHook().then((ortb2) => { + expect(ortb2).to.eql({}); + }) + }); + + it('sets user.ext.consent, but not regs.ext.gdpr, if gpdrApplies is not a boolean', () => { + consent = {consentString: 'mock-consent'}; + return callHook().then(ortb2 => { + expect(ortb2).to.eql({ + user: { + ext: { + consent: 'mock-consent' + } + } + }) + }) + }); +}); diff --git a/test/spec/fpd/oneClient.js b/test/spec/fpd/oneClient.js new file mode 100644 index 00000000000..4ecde8d8a38 --- /dev/null +++ b/test/spec/fpd/oneClient.js @@ -0,0 +1,24 @@ +import {clientSectionChecker} from '../../../src/fpd/oneClient.js'; + +describe('onlyOneClientSection', () => { + const oneClient = clientSectionChecker(); + [ + [['app'], 'app'], + [['site'], 'site'], + [['dooh'], 'dooh'], + [['app', 'site'], 'app'], + [['dooh', 'app', 'site'], 'dooh'], + [['dooh', 'site'], 'dooh'] + ].forEach(([sections, winner]) => { + it(`should leave only ${winner} in request when it contains ${sections.join(', ')}`, () => { + const req = Object.fromEntries(sections.map(s => [s, {foo: 'bar'}])); + oneClient(req); + expect(Object.keys(req)).to.eql([winner]); + }) + }); + it('should not choke if none of the sections are in the request', () => { + const req = {}; + oneClient(req); + expect(req).to.eql({}); + }); +}); diff --git a/test/spec/fpd/rootDomain_spec.js b/test/spec/fpd/rootDomain_spec.js new file mode 100644 index 00000000000..008ef749edc --- /dev/null +++ b/test/spec/fpd/rootDomain_spec.js @@ -0,0 +1,61 @@ +import {expect} from 'chai/index.js'; +import {findRootDomain, coreStorage} from 'src/fpd/rootDomain.js'; + +describe('findRootDomain', function () { + let sandbox, cookies, rejectDomain; + + beforeEach(function () { + findRootDomain.clear(); + cookies = {}; + rejectDomain = ''; + sandbox = sinon.createSandbox(); + sandbox.stub(coreStorage, 'cookiesAreEnabled').returns(true); + sandbox.stub(coreStorage, 'setCookie').callsFake((cookie, value, expiration, sameSite, domain) => { + if (rejectDomain !== domain) { + if (new Date(expiration) <= Date.now()) { + delete cookies[cookie]; + } else { + cookies[cookie] = value; + } + } + }) + sandbox.stub(coreStorage, 'getCookie').callsFake((cookie) => { + return cookies[cookie] + }); + }); + + afterEach(function () { + sandbox.restore(); + }); + + after(() => { + findRootDomain.clear(); + }) + + it('should just find the root domain', function () { + rejectDomain = 'co.uk'; + var domain = findRootDomain('sub.realdomain.co.uk'); + expect(domain).to.be.eq('realdomain.co.uk'); + expect(cookies).to.eql({}); + }); + + it('should find the full domain when no subdomain is present', function () { + rejectDomain = 'co.uk'; + var domain = findRootDomain('realdomain.co.uk'); + expect(domain).to.be.eq('realdomain.co.uk'); + expect(cookies).to.eql({}); + }); + + it('should return domain as-is if cookies are disabled', () => { + coreStorage.cookiesAreEnabled.returns(false); + expect(findRootDomain('sub.example.com')).to.eql('sub.example.com'); + sinon.assert.notCalled(coreStorage.setCookie); + }); + + it('should memoize default value', () => { + const domain = findRootDomain(); + expect(domain.length > 0).to.be.true; + expect(findRootDomain()).to.eql(domain); + sinon.assert.calledOnce(coreStorage.getCookie); + }); +}); diff --git a/test/spec/fpd/sua_spec.js b/test/spec/fpd/sua_spec.js index 121922fa78d..431f47268d3 100644 --- a/test/spec/fpd/sua_spec.js +++ b/test/spec/fpd/sua_spec.js @@ -6,7 +6,7 @@ import { SUA_SOURCE_UNKNOWN, suaFromUAData, uaDataToSUA -} from '../../../libraries/fpd/sua.js'; +} from '../../../src/fpd/sua.js'; describe('uaDataToSUA', () => { Object.entries({ diff --git a/test/spec/fpd/usp_spec.js b/test/spec/fpd/usp_spec.js new file mode 100644 index 00000000000..f616b086ffa --- /dev/null +++ b/test/spec/fpd/usp_spec.js @@ -0,0 +1,36 @@ +import {enrichFPDHook} from '../../../modules/consentManagementUsp.js'; +import {uspDataHandler} from '../../../src/adapterManager.js'; + +describe('FPD enrichment USP', () => { + let sandbox, consent; + beforeEach(() => { + consent = null; + sandbox = sinon.sandbox.create(); + sandbox.stub(uspDataHandler, 'getConsentData').callsFake(() => consent); + }); + + afterEach(() => { + sandbox.restore(); + }); + + function callHook() { + let result; + enrichFPDHook((res) => { + result = res; + }, Promise.resolve({})); + return result; + } + + it('sets regs.ext.us_privacy from uspDataHandler', () => { + consent = '1NN'; + return callHook().then(ortb2 => { + expect(ortb2.regs.ext.us_privacy).to.eql('1NN'); + }) + }); + + it('does not set if missing', () => { + return callHook().then(ortb2 => { + expect(ortb2).to.eql({}); + }) + }); +}); diff --git a/test/spec/libraries/domainOverrideToRootDomain/index_spec.js b/test/spec/libraries/domainOverrideToRootDomain/index_spec.js new file mode 100644 index 00000000000..b490d80fd40 --- /dev/null +++ b/test/spec/libraries/domainOverrideToRootDomain/index_spec.js @@ -0,0 +1,77 @@ +import {domainOverrideToRootDomain} from 'libraries/domainOverrideToRootDomain/index.js'; +import {getStorageManager} from 'src/storageManager.js'; +import {MODULE_TYPE_UID} from '../../../../src/activities/modules'; + +const storage = getStorageManager({ moduleName: 'test', moduleType: MODULE_TYPE_UID }); +const domainOverride = domainOverrideToRootDomain(storage, 'test'); + +describe('domainOverride', () => { + let sandbox, domain, cookies, rejectCookiesFor; + let setCookieStub; + + beforeEach(() => { + sandbox = sinon.createSandbox(); + sandbox.stub(document, 'domain').get(() => domain); + cookies = {}; + sandbox.stub(storage, 'getCookie').callsFake((key) => cookies[key]); + rejectCookiesFor = null; + setCookieStub = sandbox.stub(storage, 'setCookie').callsFake((key, value, expires, sameSite, domain) => { + if (domain !== rejectCookiesFor) { + if (expires != null) { + expires = new Date(expires); + } + if (expires == null || expires > Date.now()) { + cookies[key] = value; + } else { + delete cookies[key]; + } + } + }); + }); + + afterEach(() => sandbox.restore()) + + it('test cookies include the module name', () => { + domain = 'greatpublisher.com' + rejectCookiesFor = 'greatpublisher.com' + + // stub Date.now() to return a constant value + sandbox.stub(Date, 'now').returns(1234567890) + + const randomName = `adapterV${(Math.random() * 1e8).toString(16)}` + const localDomainOverride = domainOverrideToRootDomain(storage, randomName) + + const time = Date.now(); + localDomainOverride(); + + sandbox.assert.callCount(setCookieStub, 2) + sandbox.assert.calledWith(setCookieStub, `_gd${time}_${randomName}`, '1', undefined, undefined, 'greatpublisher.com') + }); + + it('will return the root domain when given a subdomain', () => { + const test_domains = [ + 'deeply.nested.subdomain.for.greatpublisher.com', + 'greatpublisher.com', + 'subdomain.greatpublisher.com', + 'a-subdomain.greatpublisher.com', + ]; + + test_domains.forEach((testDomain) => { + domain = testDomain + rejectCookiesFor = 'com' + expect(domainOverride()).to.equal('greatpublisher.com'); + }); + }); + + it(`If we can't set cookies on the root domain, we'll return the subdomain`, () => { + domain = 'subdomain.greatpublisher.com' + rejectCookiesFor = 'greatpublisher.com' + expect(domainOverride()).to.equal('subdomain.greatpublisher.com'); + }); + + it('Will return undefined if we can\'t set cookies on the root domain or the subdomain', () => { + domain = 'subdomain.greatpublisher.com' + rejectCookiesFor = 'subdomain.greatpublisher.com' + expect(domainOverride()).to.equal(undefined); + }); +}); diff --git a/test/spec/modules/33acrossBidAdapter_spec.js b/test/spec/modules/33acrossBidAdapter_spec.js index 2680544d00b..3b3c05660df 100644 --- a/test/spec/modules/33acrossBidAdapter_spec.js +++ b/test/spec/modules/33acrossBidAdapter_spec.js @@ -218,6 +218,14 @@ describe('33acrossBidAdapter:', function () { return this; }; + this.withReferer = referer => { + Object.assign(ttxRequest.site, { + ref: referer + }); + + return this; + }; + this.withSchain = schain => { Object.assign(ttxRequest, { source: { @@ -1187,26 +1195,51 @@ describe('33acrossBidAdapter:', function () { }); }); - context('when referer value is available', function() { - it('returns corresponding server requests with site.page set', function() { - const bidderRequest = { - refererInfo: { - page: 'http://foo.com/bar' - } - }; + context('when refererInfo values are available', function() { + context('when refererInfo.page is defined', function() { + it('returns corresponding server requests with site.page set', function() { + const bidderRequest = { + refererInfo: { + page: 'http://foo.com/bar' + } + }; - const ttxRequest = new TtxRequestBuilder() - .withBanner() - .withProduct() - .withPageUrl('http://foo.com/bar') - .build(); - const serverRequest = new ServerRequestBuilder() - .withData(ttxRequest) - .build(); + const ttxRequest = new TtxRequestBuilder() + .withBanner() + .withProduct() + .withPageUrl('http://foo.com/bar') + .build(); + const serverRequest = new ServerRequestBuilder() + .withData(ttxRequest) + .build(); - const [ builtServerRequest ] = spec.buildRequests(bidRequests, bidderRequest); + const [ builtServerRequest ] = spec.buildRequests(bidRequests, bidderRequest); - validateBuiltServerRequest(builtServerRequest, serverRequest); + validateBuiltServerRequest(builtServerRequest, serverRequest); + }); + }); + + context('when refererInfo.ref is defined', function() { + it('returns corresponding server requests with site.ref set', function() { + const bidderRequest = { + refererInfo: { + ref: 'google.com' + } + }; + + const ttxRequest = new TtxRequestBuilder() + .withBanner() + .withProduct() + .withReferer('google.com') + .build(); + const serverRequest = new ServerRequestBuilder() + .withData(ttxRequest) + .build(); + + const [ builtServerRequest ] = spec.buildRequests(bidRequests, bidderRequest); + + validateBuiltServerRequest(builtServerRequest, serverRequest); + }); }); }); @@ -1246,7 +1279,7 @@ describe('33acrossBidAdapter:', function () { }); context('when referer value is not available', function() { - it('returns corresponding server requests without site.page set', function() { + it('returns corresponding server requests without site.page and site.ref set', function() { const bidderRequest = { refererInfo: {} }; diff --git a/test/spec/modules/33acrossIdSystem_spec.js b/test/spec/modules/33acrossIdSystem_spec.js index 765b320f925..5070d2b8845 100644 --- a/test/spec/modules/33acrossIdSystem_spec.js +++ b/test/spec/modules/33acrossIdSystem_spec.js @@ -282,7 +282,7 @@ describe('33acrossIdSystem', () => { error: 'foo' })); - expect(logErrorSpy.calledOnceWithExactly(`${thirthyThreeAcrossIdSubmodule.name}: Unsuccessful response`)).to.be.true; + expect(logErrorSpy.calledOnceWithExactly(`${thirthyThreeAcrossIdSubmodule.name}: Unsuccessful response foo`)).to.be.true; logErrorSpy.restore(); }); diff --git a/test/spec/modules/ViouslyBidAdapter_spec.js b/test/spec/modules/ViouslyBidAdapter_spec.js new file mode 100644 index 00000000000..f8cd686581c --- /dev/null +++ b/test/spec/modules/ViouslyBidAdapter_spec.js @@ -0,0 +1,474 @@ +import {expect} from 'chai'; + +import { deepClone, mergeDeep } from 'src/utils'; +import { BANNER, VIDEO } from 'src/mediaTypes'; +import { createEidsArray } from 'modules/userId/eids.js'; + +import {spec as adapter} from 'modules/viouslyBidAdapter'; + +import sinon from 'sinon'; +import { config } from 'src/config.js'; + +const CURRENCY = 'EUR'; +const TTL = 60; +const HTTP_METHOD = 'POST'; +const REQUEST_URL = 'https://bidder.viously.com/bid'; + +const VALID_BID_BANNER = { + bidder: 'viously', + bidId: '5e6f7g8h', + adUnitCode: 'id-5678', + params: { + pid: '123e4567-e89b-12d3-a456-426614174002' + }, + mediaTypes: { + banner: { + sizes: [300, 50], + pos: 1 + } + } +}; + +const VALID_BID_VIDEO = { + bidder: 'viously', + bidId: '5e6f7g8h', + adUnitCode: 'id-5678', + params: { + pid: '123e4567-e89b-12d3-a456-426614174001' + }, + mediaTypes: { + video: { + playerSize: [640, 360], + context: 'instream', + playbackmethod: [1, 2, 3, 4] + } + } +}; + +const VALID_REQUEST_BANNER = { + method: HTTP_METHOD, + url: REQUEST_URL, + data: { + pid: '123e4567-e89b-12d3-a456-426614174002', + currency_code: CURRENCY, + placements: [ + { + id: 'id-5678', + bid_id: '5e6f7g8h', + sizes: ['300x50'], + type: BANNER, + position: 1 + } + ] + } +}; + +const VALID_REQUEST_VIDEO = { + method: HTTP_METHOD, + url: REQUEST_URL, + data: { + pid: '123e4567-e89b-12d3-a456-426614174001', + currency_code: CURRENCY, + placements: [ + { + id: 'id-5678', + bid_id: '5e6f7g8h', + type: VIDEO, + video_params: { + context: 'instream', + playbackmethod: [1, 2, 3, 4], + size: ['640x360'] + } + } + ] + } +}; + +const VALID_GDPR = { + gdprApplies: true, + apiVersion: 2, + consentString: 'abcdefgh', + addtlConsent: '1~12345678', + vendorData: { + purpose: { + consents: { + 1: true + } + } + } +}; +const US_PRIVACY = '1YNN'; + +describe('ViouslyAdapter', function () { + describe('isBidRequestValid', function () { + describe('Check method return', function () { + it('should return true', function () { + expect(adapter.isBidRequestValid(VALID_BID_BANNER)).to.equal(true); + expect(adapter.isBidRequestValid(VALID_BID_VIDEO)).to.equal(true); + }); + + it('should return true for banner with no pos', function () { + let newBid = deepClone(VALID_BID_BANNER); + let newRequest = deepClone(VALID_REQUEST_BANNER); + + delete newBid.mediaTypes.banner.pos; + newRequest.data.placements[0].position = 0; + + expect(adapter.buildRequests([newBid])).to.deep.equal(newRequest); + }); + + it('should return false because the banner size is missing', function () { + let wrongBid = deepClone(VALID_BID_BANNER); + + wrongBid.mediaTypes.banner.sizes = '123456'; + expect(adapter.isBidRequestValid(wrongBid)).to.equal(false); + + delete wrongBid.mediaTypes.banner.sizes; + expect(adapter.isBidRequestValid(wrongBid)).to.equal(false); + }); + + it('should return false because the pid is missing', function () { + let wrongBid = deepClone(VALID_BID_VIDEO); + delete wrongBid.params.pid; + + expect(adapter.isBidRequestValid(wrongBid)).to.equal(false); + }); + + it('should return false because the video context parameter is missing', function () { + let wrongBid = deepClone(VALID_BID_VIDEO); + + delete wrongBid.mediaTypes.video.context; + expect(adapter.isBidRequestValid(wrongBid)).to.equal(false); + }); + }); + }); + + describe('buildRequests', function () { + describe('Check method return', function () { + it('should return the right formatted banner requests', function() { + expect(adapter.buildRequests([VALID_BID_BANNER])).to.deep.equal(VALID_REQUEST_BANNER); + }); + + it('should return the right formatted video requests', function() { + expect(adapter.buildRequests([VALID_BID_VIDEO])).to.deep.equal(VALID_REQUEST_VIDEO); + }); + + it('should return the right formatted request with the referer info', function() { + let bidderRequest = { + refererInfo: { + page: 'https://www.example.com/test' + } + }; + + let requests = mergeDeep(deepClone(VALID_REQUEST_VIDEO), { + data: { + domain: 'www.example.com', + page_domain: 'https://www.example.com/test' + } + }); + + expect(adapter.buildRequests([VALID_BID_VIDEO], bidderRequest)).to.deep.equal(requests); + }); + + it('should return the right formatted request with the referer info from config', function() { + /** Mock the config.getConfig method */ + sinon.stub(config, 'getConfig') + .withArgs('pageUrl') + .returns('https://www.example.com/page'); + + let requests = mergeDeep(deepClone(VALID_REQUEST_VIDEO), { + data: { + domain: 'www.example.com', + page_domain: 'https://www.example.com/page' + } + }); + + expect(adapter.buildRequests([VALID_BID_VIDEO])).to.deep.equal(requests); + + config.getConfig.restore(); + }); + + it('should return the right formatted request with GDPR Consent info', function() { + let bidderRequest = { + gdprConsent: VALID_GDPR + }; + + let requests = mergeDeep(deepClone(VALID_REQUEST_VIDEO), { + data: { + gdpr: true, + gdpr_consent: 'abcdefgh', + addtl_consent: '1~12345678' + } + }); + + expect(adapter.buildRequests([VALID_BID_VIDEO], bidderRequest)).to.deep.equal(requests); + }); + + it('should return the right formatted request with US Privacy info', function() { + let bidderRequest = { + uspConsent: US_PRIVACY + }; + + let requests = mergeDeep(deepClone(VALID_REQUEST_VIDEO), { + data: { + us_privacy: US_PRIVACY + } + }); + + expect(adapter.buildRequests([VALID_BID_VIDEO], bidderRequest)).to.deep.equal(requests); + }); + + // TODO: Supply chain + it('should return the right formatted request with Supply Chain info', function() { + let schain = { + 'ver': '1.0', + 'complete': 1, + 'nodes': [ + { + 'asi': 'test1.com', + 'sid': '00001', + 'hp': 1 + }, + { + 'asi': 'test2-2.com', + 'sid': '00002', + 'hp': 2 + } + ] + }; + + let bid = mergeDeep(deepClone(VALID_BID_VIDEO), { + schain: schain + }); + + let requests = mergeDeep(deepClone(VALID_REQUEST_VIDEO), { + data: { + schain: schain + } + }); + + expect(adapter.buildRequests([bid])).to.deep.equal(requests); + }); + + it('should return the right formatted request with User Ids info', function() { + let userIds = { + idl_env: '1234-5678-9012-3456', // Liveramp + netId: 'testnetid123', // NetId + IDP: 'userIDP000', // IDP + fabrickId: 'fabrickId9000', // FabrickId + uid2: { id: 'testuid2' } // UID 2.0 + }; + + let bid = mergeDeep(deepClone(VALID_BID_VIDEO), { + userIds: userIds + }, { + userIdAsEids: createEidsArray(userIds) + }); + + let requests = mergeDeep(deepClone(VALID_REQUEST_VIDEO), { + data: { + users_uid: createEidsArray(userIds) + } + }); + + expect(adapter.buildRequests([bid])).to.deep.equal(requests); + }); + + it('should return the right formatted request with endpoint test', function() { + let endpoint = 'https://bid-test.viously.com/prebid'; + + let bid = mergeDeep(deepClone(VALID_BID_VIDEO), { + params: { + endpoint: endpoint + } + }); + + let requests = mergeDeep(deepClone(VALID_REQUEST_VIDEO)); + + requests.url = endpoint; + + expect(adapter.buildRequests([bid])).to.deep.equal(requests); + }); + + // TODO: Floor + }); + }); + + describe('interpretResponse', function() { + describe('Check method return', function () { + it('should return the right formatted response', function() { + let response = { + body: { + ads: [ + { + bid: false, + id: 'id-0157324f-bee4-5390-a14c-47a7da3eb73c-0', + bid_id: '1234' + }, + { + bid: true, + creative_id: '2468', + id: 'id-0157324f-bee4-5390-a14c-47a7da3eb73c-1', + bid_id: '5678', + cpm: 8, + ad: 'vast xml', + ad_url: 'http://www.example.com/vast', + type: 'video', + size: '640x480', + nurl: [ + 'win.domain.com' + ] + }, + { + bid: true, + creative_id: '1357', + id: 'id-0157324f-bee4-5390-a14c-47a7da3eb73c-2', + bid_id: '9101112', + cpm: 1.5, + ad: 'html content', + type: 'banner', + size: '300x50', + nurl: [ + 'win.domain2.com', + 'win.domain3.com' + ] + }, + { + bid: true, + creative_id: '1469', + id: 'id-0157324f-bee4-5390-a14c-47a7da3eb73c-3', + bid_id: '2570', + cpm: 4, + ad: 'vast xml', + type: 'video', + size: '640x480', + } + ] + } + }; + let requests = { + data: { + placements: [ + { + id: 'id-0157324f-bee4-5390-a14c-47a7da3eb73c-0', + bid_id: '1234' + }, + { + id: 'id-0157324f-bee4-5390-a14c-47a7da3eb73c-1', + bid_id: '5678' + }, + { + id: 'id-0157324f-bee4-5390-a14c-47a7da3eb73c-2', + bid_id: '9101112' + }, + { + id: 'id-0157324f-bee4-5390-a14c-47a7da3eb73c-3', + bid_id: '2570' + } + ] + } + }; + + let formattedReponse = [ + { + requestId: '5678', + id: 'id-0157324f-bee4-5390-a14c-47a7da3eb73c-1', + cpm: 8, + width: '640', + height: '480', + creativeId: '2468', + currency: CURRENCY, + netRevenue: true, + ttl: TTL, + mediaType: 'video', + meta: {}, + vastUrl: 'http://www.example.com/vast', + nurl: [ + 'win.domain.com' + ] + }, + { + requestId: '9101112', + id: 'id-0157324f-bee4-5390-a14c-47a7da3eb73c-2', + cpm: 1.5, + width: '300', + height: '50', + creativeId: '1357', + currency: CURRENCY, + netRevenue: true, + ttl: TTL, + mediaType: 'banner', + meta: {}, + ad: 'html content', + nurl: [ + 'win.domain2.com', + 'win.domain3.com' + ] + }, + { + requestId: '2570', + id: 'id-0157324f-bee4-5390-a14c-47a7da3eb73c-3', + cpm: 4, + width: '640', + height: '480', + creativeId: '1469', + currency: CURRENCY, + netRevenue: true, + ttl: TTL, + mediaType: 'video', + meta: {}, + vastXml: 'vast xml', + nurl: [] + } + ]; + + expect(adapter.interpretResponse(response, requests)).to.deep.equal(formattedReponse); + }); + }); + }); + + describe('onBidWon', function() { + describe('Check methods succeed', function () { + it('should not throw error', function() { + let bids = [ + { + requestId: '5678', + id: 'id-0157324f-bee4-5390-a14c-47a7da3eb73c-1', + cpm: 8, + width: '640', + height: '480', + creativeId: '2468', + currency: CURRENCY, + netRevenue: true, + ttl: TTL, + mediaType: 'video', + meta: {}, + vastUrl: 'http://www.example.com/vast', + nurl: [ + 'win.domain.com' + ] + }, + { + requestId: '2570', + id: 'id-0157324f-bee4-5390-a14c-47a7da3eb73c-3', + cpm: 4, + width: '640', + height: '480', + creativeId: '1469', + currency: CURRENCY, + netRevenue: true, + ttl: TTL, + mediaType: 'video', + meta: {}, + vastXml: 'vast xml', + nurl: [] + } + ]; + + bids.forEach(function(bid) { + expect(adapter.onBidWon.bind(adapter, bid)).to.not.throw(); + }); + }); + }); + }); +}); diff --git a/test/spec/modules/acuityAdsBidAdapter_spec.js b/test/spec/modules/acuityAdsBidAdapter_spec.js index 18ea574c1ce..05c59036ff3 100644 --- a/test/spec/modules/acuityAdsBidAdapter_spec.js +++ b/test/spec/modules/acuityAdsBidAdapter_spec.js @@ -76,7 +76,8 @@ describe('AcuityAdsBidAdapter', function () { gdprConsent: 'COvFyGBOvFyGBAbAAAENAPCAAOAAAAAAAAAAAEEUACCKAAA.IFoEUQQgAIQwgIwQABAEAAAAOIAACAIAAAAQAIAgEAACEAAAAAgAQBAAAAAAAGBAAgAAAAAAAFAAECAAAgAAQARAEQAAAAAJAAIAAgAAAYQEAAAQmAgBC3ZAYzUw', refererInfo: { referer: 'https://test.com' - } + }, + timeout: 500 }; describe('isBidRequestValid', function () { diff --git a/test/spec/modules/adagioBidAdapter_spec.js b/test/spec/modules/adagioBidAdapter_spec.js index 436d481c4a1..759b16a81bb 100644 --- a/test/spec/modules/adagioBidAdapter_spec.js +++ b/test/spec/modules/adagioBidAdapter_spec.js @@ -17,6 +17,7 @@ import * as utils from '../../../src/utils.js'; import { config } from '../../../src/config.js'; import { NATIVE } from '../../../src/mediaTypes.js'; import { executeRenderer } from '../../../src/Renderer.js'; +import { userSync } from '../../../src/userSync.js'; const BidRequestBuilder = function BidRequestBuilder(options) { const defaults = { @@ -149,10 +150,8 @@ describe('Adagio bid adapter', () => { site: { ext: { data: { - environment: 'desktop', pagetype: 'abc', - category: ['cat1', 'cat2', 'cat3'], - subcategory: [] + category: ['cat1', 'cat2', 'cat3'] } } } @@ -167,17 +166,11 @@ describe('Adagio bid adapter', () => { return utils.deepAccess(config, key); }); - setExtraParam(bid, 'environment'); - expect(bid.params.environment).to.equal('desktop'); - setExtraParam(bid, 'pagetype') expect(bid.params.pagetype).to.equal('article'); setExtraParam(bid, 'category'); expect(bid.params.category).to.equal('cat1'); // Only the first value is kept - - setExtraParam(bid, 'subcategory'); - expect(bid.params.subcategory).to.be.undefined; }); it('should use the adUnit param unit if defined', function() { @@ -274,7 +267,8 @@ describe('Adagio bid adapter', () => { 'schain', 'prebidVersion', 'featuresVersion', - 'data' + 'data', + 'usIfr' ]; it('groups requests by organizationId', function() { @@ -353,6 +347,76 @@ describe('Adagio bid adapter', () => { expect(requests[0].data.adUnits[0].features.url).to.not.exist; }); + it('should force split keyword param into a string', function() { + const bid01 = new BidRequestBuilder().withParams({ + splitKeyword: 1234 + }).build(); + const bid02 = new BidRequestBuilder().withParams({ + splitKeyword: ['1234'] + }).build(); + const bidderRequest = new BidderRequestBuilder().build(); + + const requests = spec.buildRequests([bid01, bid02], bidderRequest); + + expect(requests).to.have.lengthOf(1); + expect(requests[0].data).to.have.all.keys(expectedDataKeys); + expect(requests[0].data.adUnits[0].params).to.exist; + expect(requests[0].data.adUnits[0].params.splitKeyword).to.exist; + expect(requests[0].data.adUnits[0].params.splitKeyword).to.equal('1234'); + expect(requests[0].data.adUnits[1].params.splitKeyword).to.not.exist; + }); + + it('should force key and value from data layer param into a string', function() { + const bid01 = new BidRequestBuilder().withParams({ + dataLayer: { + 1234: 'dlparam', + goodkey: 1234, + objectvalue: { + random: 'result' + }, + arrayvalue: ['1234'] + } + }).build(); + + const bid02 = new BidRequestBuilder().withParams({ + dataLayer: 'a random string' + }).build(); + + const bid03 = new BidRequestBuilder().withParams({ + dataLayer: 1234 + }).build(); + + const bid04 = new BidRequestBuilder().withParams({ + dataLayer: ['an array'] + }).build(); + + const bidderRequest = new BidderRequestBuilder().build(); + + const requests = spec.buildRequests([bid01, bid02, bid03, bid04], bidderRequest); + + expect(requests).to.have.lengthOf(1); + expect(requests[0].data).to.have.all.keys(expectedDataKeys); + expect(requests[0].data.adUnits[0].params).to.exist; + expect(requests[0].data.adUnits[0].params.dataLayer).to.not.exist; + expect(requests[0].data.adUnits[0].params.dl).to.exist; + expect(requests[0].data.adUnits[0].params.dl['1234']).to.equal('dlparam'); + expect(requests[0].data.adUnits[0].params.dl.goodkey).to.equal('1234'); + expect(requests[0].data.adUnits[0].params.dl.objectvalue).to.not.exist; + expect(requests[0].data.adUnits[0].params.dl.arrayvalue).to.not.exist; + + expect(requests[0].data.adUnits[1].params).to.exist; + expect(requests[0].data.adUnits[1].params.dl).to.not.exist; + expect(requests[0].data.adUnits[1].params.dataLayer).to.not.exist; + + expect(requests[0].data.adUnits[2].params).to.exist; + expect(requests[0].data.adUnits[2].params.dl).to.not.exist; + expect(requests[0].data.adUnits[2].params.dataLayer).to.not.exist; + + expect(requests[0].data.adUnits[3].params).to.exist; + expect(requests[0].data.adUnits[3].params.dl).to.not.exist; + expect(requests[0].data.adUnits[3].params.dataLayer).to.not.exist; + }); + describe('With video mediatype', function() { context('Outstream video', function() { it('should logWarn if user does not set renderer.backupOnly: true', function() { @@ -389,8 +453,8 @@ describe('Adagio bid adapter', () => { context: 'outstream', playerSize: [[300, 250]], mimes: ['video/mp4'], - api: 5, // will be removed because invalid - playbackmethod: [7], // will be removed because invalid + api: 'val', // will be removed because invalid + playbackmethod: ['val'], // will be removed because invalid } }, }).withParams({ @@ -569,15 +633,13 @@ describe('Adagio bid adapter', () => { it('should send the Coppa "required" flag set to "1" in the request', function () { const bidderRequest = new BidderRequestBuilder().build(); - sinon.stub(config, 'getConfig') - .withArgs('coppa') - .returns(true); + sandbox.stub(config, 'getConfig') + .withArgs('userSync').returns({ syncEnabled: true }) + .withArgs('coppa').returns(true); const requests = spec.buildRequests([bid01], bidderRequest); expect(requests[0].data.regs.coppa.required).to.equal(1); - - config.getConfig.restore(); }); }); @@ -622,32 +684,26 @@ describe('Adagio bid adapter', () => { }); describe('with userID modules', function() { - const userId = { - pubcid: '01EAJWWNEPN3CYMM5N8M5VXY22', - unsuported: '666' - }; + const userIdAsEids = [{ + 'source': 'pubcid.org', + 'uids': [ + { + 'atype': 1, + 'id': '01EAJWWNEPN3CYMM5N8M5VXY22' + } + ] + }]; it('should send "user.eids" in the request for Prebid.js supported modules only', function() { const bid01 = new BidRequestBuilder({ - userId + userIdAsEids }).withParams().build(); const bidderRequest = new BidderRequestBuilder().build(); const requests = spec.buildRequests([bid01], bidderRequest); - const expected = [{ - source: 'pubcid.org', - uids: [ - { - atype: 1, - id: '01EAJWWNEPN3CYMM5N8M5VXY22' - } - ] - }]; - - expect(requests[0].data.user.eids).to.have.lengthOf(1); - expect(requests[0].data.user.eids).to.deep.equal(expected); + expect(requests[0].data.user.eids).to.deep.equal(userIdAsEids); }); it('should send an empty "user.eids" array in the request if userId module is unsupported', function() { @@ -744,6 +800,46 @@ describe('Adagio bid adapter', () => { expect(requests[0].data.adUnits[0].mediaTypes.video.floor).to.be.undefined; }); }); + + describe('with user-sync iframe enabled', function () { + const bid01 = new BidRequestBuilder().withParams().build(); + + it('should send the UsIfr flag set to "true" in the request', function () { + const bidderRequest = new BidderRequestBuilder().build(); + + sandbox.stub(config, 'getConfig') + .withArgs('userSync') + .returns({ syncEnabled: true }); + + sandbox.stub(userSync, 'canBidderRegisterSync') + .withArgs('iframe', 'adagio') + .returns(true); + + const requests = spec.buildRequests([bid01], bidderRequest); + + expect(requests[0].data.usIfr).to.equal(true); + }); + }); + + describe('with user-sync iframe disabled', function () { + const bid01 = new BidRequestBuilder().withParams().build(); + + it('should send the UsIfr flag set to "false" in the request', function () { + const bidderRequest = new BidderRequestBuilder().build(); + + sandbox.stub(config, 'getConfig') + .withArgs('userSync') + .returns({ syncEnabled: true }); + + sandbox.stub(userSync, 'canBidderRegisterSync') + .withArgs('iframe', 'adagio') + .returns(false); + + const requests = spec.buildRequests([bid01], bidderRequest); + + expect(requests[0].data.usIfr).to.equal(false); + }); + }); }); describe('interpretResponse()', function() { @@ -784,8 +880,6 @@ describe('Adagio bid adapter', () => { adUnitElementId: 'gpt-adunit-code', pagetype: 'ARTICLE', category: 'NEWS', - subcategory: 'SPORT', - environment: 'desktop', supportIObs: true }, adUnitCode: 'adunit-code', @@ -833,8 +927,6 @@ describe('Adagio bid adapter', () => { site: 'SITE-NAME', pagetype: 'ARTICLE', category: 'NEWS', - subcategory: 'SPORT', - environment: 'desktop', aDomain: ['advertiser.com'], mediaType: 'banner', meta: { @@ -868,8 +960,6 @@ describe('Adagio bid adapter', () => { site: 'SITE-NAME', pagetype: 'ARTICLE', category: 'NEWS', - subcategory: 'SPORT', - environment: 'desktop', aDomain: ['advertiser.com'], mediaType: 'banner', meta: { @@ -1392,19 +1482,6 @@ describe('Adagio bid adapter', () => { }); describe.skip('optional params auto detection', function() { - it('should auto detect environment', function() { - const getDeviceStub = sandbox.stub(_features, 'getDevice'); - - getDeviceStub.returns(5); - expect(adagio.autoDetectEnvironment()).to.eq('tablet'); - - getDeviceStub.returns(4); - expect(adagio.autoDetectEnvironment()).to.eq('mobile'); - - getDeviceStub.returns(2); - expect(adagio.autoDetectEnvironment()).to.eq('desktop'); - }); - it('should auto detect adUnitElementId when GPT is used', function() { sandbox.stub(utils, 'getGptSlotInfoForAdUnitCode').withArgs('banner').returns({divId: 'gpt-banner'}); expect(adagio.autoDetectAdUnitElementId('banner')).to.eq('gpt-banner'); diff --git a/test/spec/modules/adfBidAdapter_spec.js b/test/spec/modules/adfBidAdapter_spec.js index eb560bb1bae..574f559e994 100644 --- a/test/spec/modules/adfBidAdapter_spec.js +++ b/test/spec/modules/adfBidAdapter_spec.js @@ -481,6 +481,17 @@ describe('Adf adapter', function () { nativeParams: { title: { required: true, len: 140 } }, + nativeOrtbRequest: { + assets: [ + { + required: 1, + id: 0, + title: { + len: 140 + } + } + ] + }, mediaTypes: { banner: { sizes: [[100, 100], [200, 300]] @@ -547,6 +558,57 @@ describe('Adf adapter', function () { describe('native', function () { describe('assets', function () { + it('should use nativeOrtbRequest instead of nativeParams or mediaTypes', function () { + let validBidRequests = [{ + bidId: 'bidId', + params: { mid: 1000 }, + nativeParams: { + title: { required: true, len: 200 }, + image: { required: true, sizes: [150, 150] }, + icon: { required: false, sizes: [150, 150] }, + body: { required: false, len: 1140 }, + sponsoredBy: { required: true }, + cta: { required: false }, + clickUrl: { required: false }, + ortb: { + ver: '1.2', + assets: [] + } + }, + mediaTypes: { + native: { + title: { required: true, len: 140 }, + image: { required: true, sizes: [150, 50] }, + icon: { required: false, sizes: [50, 50] }, + body: { required: false, len: 140 }, + sponsoredBy: { required: true }, + cta: { required: false }, + clickUrl: { required: false } + } + }, + nativeOrtbRequest: { + assets: [ + { required: 1, title: { len: 200 } }, + { required: 1, img: { type: 3, w: 170, h: 70 } }, + { required: 0, img: { type: 1, w: 70, h: 70 } }, + { required: 0, data: { type: 2, len: 150 } }, + { required: 1, data: { type: 1 } }, + { required: 0, data: { type: 12 } }, + ] + } + }]; + + let assets = JSON.parse(spec.buildRequests(validBidRequests, { refererInfo: { page: 'page' } }).data).imp[0].native.request.assets; + assert.ok(assets[0].title); + assert.equal(assets[0].title.len, 200); + assert.deepEqual(assets[1].img, { type: 3, w: 170, h: 70 }); + assert.deepEqual(assets[2].img, { type: 1, w: 70, h: 70 }); + assert.deepEqual(assets[3].data, { type: 2, len: 150 }); + assert.deepEqual(assets[4].data, { type: 1 }); + assert.deepEqual(assets[5].data, { type: 12 }); + assert.ok(!assets[6]); + }); + it('should set correct asset id', function () { let validBidRequests = [{ bidId: 'bidId', @@ -555,14 +617,46 @@ describe('Adf adapter', function () { title: { required: true, len: 140 }, image: { required: false, wmin: 836, hmin: 627, w: 325, h: 300, mimes: ['image/jpg', 'image/gif'] }, body: { len: 140 } + }, + nativeOrtbRequest: { + assets: [ + { + id: 0, + required: 1, + title: { + len: 140 + } + }, + { + id: 1, + required: 0, + img: { + type: 3, + wmin: 836, + hmin: 627, + w: 325, + h: 300, + mimes: [ 'image/jpg', 'image/gif' ] + } + }, + { + id: 2, + data: { + type: 2, + len: 140 + } + } + ] } }]; + let assets = JSON.parse(spec.buildRequests(validBidRequests, { refererInfo: { page: 'page' } }).data).imp[0].native.request.assets; assert.equal(assets[0].id, 0); - assert.equal(assets[1].id, 3); - assert.equal(assets[2].id, 4); + assert.equal(assets[1].id, 1); + assert.equal(assets[2].id, 2); }); + it('should add required key if it is necessary', function () { let validBidRequests = [{ bidId: 'bidId', @@ -572,9 +666,16 @@ describe('Adf adapter', function () { image: { required: false, wmin: 836, hmin: 627, w: 325, h: 300, mimes: ['image/jpg', 'image/gif'] }, body: { len: 140 }, sponsoredBy: { required: true, len: 140 } + }, + nativeOrtbRequest: { + assets: [ + { required: 1, title: { len: 140 } }, + { required: 0, img: { type: 3, wmin: 836, hmin: 627, w: 325, h: 300, mimes: ['image/jpg', 'image/gif'] } }, + { data: { type: 2, len: 140 } }, + { required: 1, data: { type: 1, len: 140 } } + ] } }]; - let assets = JSON.parse(spec.buildRequests(validBidRequests, { refererInfo: { page: 'page' } }).data).imp[0].native.request.assets; assert.equal(assets[0].required, 1); @@ -593,8 +694,17 @@ describe('Adf adapter', function () { icon: { required: false, sizes: [50, 50] }, body: { required: false, len: 140 }, sponsoredBy: { required: true }, - cta: { required: false }, - clickUrl: { required: false } + cta: { required: false } + }, + nativeOrtbRequest: { + assets: [ + { required: 1, title: { len: 140 } }, + { required: 1, img: { type: 3, w: 150, h: 50 } }, + { required: 0, img: { type: 1, w: 50, h: 50 } }, + { required: 0, data: { type: 2, len: 140 } }, + { required: 1, data: { type: 1 } }, + { required: 0, data: { type: 12 } }, + ] } }]; @@ -609,25 +719,6 @@ describe('Adf adapter', function () { assert.ok(!assets[6]); }); - describe('icon/image sizing', function () { - it('should flatten sizes and utilise first pair', function () { - const validBidRequests = [{ - bidId: 'bidId', - params: { mid: 1000 }, - nativeParams: { - image: { - sizes: [[200, 300], [100, 200]] - }, - } - }]; - - let assets = JSON.parse(spec.buildRequests(validBidRequests, { refererInfo: { page: 'page' } }).data).imp[0].native.request.assets; - assert.ok(assets[0].img); - assert.equal(assets[0].img.w, 200); - assert.equal(assets[0].img.h, 300); - }); - }); - it('should utilise aspect_ratios', function () { const validBidRequests = [{ bidId: 'bidId', @@ -647,6 +738,12 @@ describe('Adf adapter', function () { ratio_width: 2 }] } + }, + nativeOrtbRequest: { + assets: [ + { img: { type: 3, wmin: 100, ext: { aspectratios: ['1:3'] } } }, + { img: { type: 1, wmin: 10, ext: { aspectratios: ['2:5'] } } } + ] } }]; @@ -671,6 +768,14 @@ describe('Adf adapter', function () { icon: { aspect_ratios: [] } + }, + nativeOrtbRequest: { + request: { + assets: [ + { img: {} }, + { img: {} } + ] + } } }]; @@ -689,13 +794,18 @@ describe('Adf adapter', function () { ratio_width: 1 }] } + }, + nativeOrtbRequest: { + assets: [ + { img: { type: 3, ext: { aspectratios: ['3:1'] } } } + ] } }]; let assets = JSON.parse(spec.buildRequests(validBidRequests, { refererInfo: { page: 'page' } }).data).imp[0].native.request.assets; assert.ok(assets[0].img); - assert.equal(assets[0].img.wmin, 0); - assert.equal(assets[0].img.hmin, 0); + assert.ok(!assets[0].img.wmin); + assert.ok(!assets[0].img.hmin); assert.ok(!assets[1]); }); }); @@ -717,7 +827,7 @@ describe('Adf adapter', function () { let serverResponse = { body: { seatbid: [{ - bid: [{impid: '1', native: {ver: '1.1', link: { url: 'link' }, assets: [{id: 1, title: {text: 'Asset title text'}}]}}] + bid: [{impid: '1', native: {ver: '1.1', link: { url: 'link' }, assets: [{id: 0, title: {text: 'Asset title text'}}]}}] }, { bid: [{impid: '2', native: {ver: '1.1', link: { url: 'link' }, assets: [{id: 1, data: {value: 'Asset title text'}}]}}] }] @@ -729,19 +839,23 @@ describe('Adf adapter', function () { { bidId: 'bidId1', params: { mid: 1000 }, - nativeParams: { - title: { required: true, len: 140 }, - image: { required: false, wmin: 836, hmin: 627, w: 325, h: 300, mimes: ['image/jpg', 'image/gif'] }, - body: { len: 140 } + nativeOrtbRequest: { + assets: [ + { id: 0, required: 1, title: { len: 140 } }, + { id: 1, required: 0, img: { type: 3, wmin: 836, hmin: 627, ext: { aspectratios: ['6:5'] } } }, + { id: 2, required: 0, data: { type: 2 } } + ] } }, { bidId: 'bidId2', params: { mid: 1000 }, - nativeParams: { - title: { required: true, len: 140 }, - image: { required: false, wmin: 836, hmin: 627, w: 325, h: 300, mimes: ['image/jpg', 'image/gif'] }, - body: { len: 140 } + nativeOrtbRequest: { + assets: [ + { id: 0, required: 1, title: { len: 140 } }, + { id: 1, required: 0, img: { type: 3, wmin: 836, hmin: 627, ext: { aspectratios: ['6:5'] } } }, + { id: 2, required: 0, data: { type: 2 } } + ] } } ] @@ -756,11 +870,11 @@ describe('Adf adapter', function () { body: { seatbid: [{ bid: [ - {impid: '1', native: {ver: '1.1', link: { url: 'link1' }, assets: [{id: 1, title: {text: 'Asset title text'}}]}}, + {impid: '1', native: {ver: '1.1', link: { url: 'link1' }, assets: [{id: 0, title: {text: 'Asset title text'}}]}}, {impid: '4', native: {ver: '1.1', link: { url: 'link4' }, assets: [{id: 1, title: {text: 'Asset title text'}}]}} ] }, { - bid: [{impid: '2', native: {ver: '1.1', link: { url: 'link2' }, assets: [{id: 1, data: {value: 'Asset title text'}}]}}] + bid: [{impid: '2', native: {ver: '1.1', link: { url: 'link2' }, assets: [{id: 0, data: {value: 'Asset title text'}}]}}] }] } }; @@ -770,45 +884,53 @@ describe('Adf adapter', function () { { bidId: 'bidId1', params: { mid: 1000 }, - nativeParams: { - title: { required: true, len: 140 }, - image: { required: false, wmin: 836, hmin: 627, w: 325, h: 300, mimes: ['image/jpg', 'image/gif'] }, - body: { len: 140 } + nativeOrtbRequest: { + assets: [ + { id: 0, required: 1, title: { len: 140 } }, + { id: 1, required: 0, img: { type: 3, wmin: 836, hmin: 627, ext: { aspectratios: ['6:5'] } } }, + { id: 2, required: 0, data: { type: 2 } } + ] } }, { bidId: 'bidId2', params: { mid: 1000 }, - nativeParams: { - title: { required: true, len: 140 }, - image: { required: false, wmin: 836, hmin: 627, w: 325, h: 300, mimes: ['image/jpg', 'image/gif'] }, - body: { len: 140 } + nativeOrtbRequest: { + assets: [ + { id: 0, required: 1, title: { len: 140 } }, + { id: 1, required: 0, img: { type: 3, wmin: 836, hmin: 627, ext: { aspectratios: ['6:5'] } } }, + { id: 2, required: 0, data: { type: 2 } } + ] } }, { bidId: 'bidId3', params: { mid: 1000 }, - nativeParams: { - title: { required: true, len: 140 }, - image: { required: false, wmin: 836, hmin: 627, w: 325, h: 300, mimes: ['image/jpg', 'image/gif'] }, - body: { len: 140 } + nativeOrtbRequest: { + assets: [ + { id: 0, required: 1, title: { len: 140 } }, + { id: 1, required: 0, img: { type: 3, wmin: 836, hmin: 627, ext: { aspectratios: ['6:5'] } } }, + { id: 2, required: 0, data: { type: 2 } } + ] } }, { bidId: 'bidId4', params: { mid: 1000 }, - nativeParams: { - title: { required: true, len: 140 }, - image: { required: false, wmin: 836, hmin: 627, w: 325, h: 300, mimes: ['image/jpg', 'image/gif'] }, - body: { len: 140 } + nativeOrtbRequest: { + assets: [ + { id: 0, required: 1, title: { len: 140 } }, + { id: 1, required: 0, img: { type: 3, wmin: 836, hmin: 627, ext: { aspectratios: ['6:5'] } } }, + { id: 2, required: 0, data: { type: 2 } } + ] } } ] }; bids = spec.interpretResponse(serverResponse, bidRequest).map(bid => { - const { requestId, native: { clickUrl } } = bid; - return [ requestId, clickUrl ]; + const { requestId, native: { ortb: { link: { url } } } } = bid; + return [ requestId, url ]; }); assert.equal(bids.length, 3); @@ -850,6 +972,34 @@ describe('Adf adapter', function () { { bidId: 'bidId1', params: { mid: 1000 }, + nativeOrtbRequest: { + assets: [ + { + id: 0, + required: 1, + title: { + len: 140 + } + }, { + id: 1, + required: 1, + img: { + type: 3, + wmin: 836, + hmin: 627, + ext: { + aspectratios: ['6:5'] + } + } + }, { + id: 2, + required: 0, + data: { + type: 2 + } + } + ] + }, nativeParams: { title: { required: true, len: 140 }, image: { required: false, wmin: 836, hmin: 627, w: 325, h: 300, mimes: ['image/jpg', 'image/gif'] }, @@ -876,56 +1026,45 @@ describe('Adf adapter', function () { const bid = [ { impid: '1', - price: 93.1231, - crid: '12312312', native: { - assets: [ - { - data: null, - id: 0, - img: null, - required: 0, - title: {text: 'title', len: null}, - video: null - }, { - data: null, - id: 2, - img: {type: null, url: 'test.url.com/Files/58345/308185.jpg?bv=1', w: 30, h: 10}, - required: 0, - title: null, - video: null - }, { - data: null, - id: 3, - img: {type: null, url: 'test.url.com/Files/58345/308200.jpg?bv=1', w: 100, h: 100}, - required: 0, - title: null, - video: null - }, { - data: {type: null, len: null, value: 'body'}, - id: 4, - img: null, - required: 0, - title: null, - video: null - }, { - data: {type: null, len: null, value: 'cta'}, - id: 1, - img: null, - required: 0, - title: null, - video: null - }, { - data: {type: null, len: null, value: 'sponsoredBy'}, - id: 5, - img: null, - required: 0, - title: null, - video: null + ver: '1.1', + assets: [{ + id: 1, + required: 0, + title: { + text: 'FLS Native' + } + }, { + id: 3, + required: 0, + data: { + value: 'Adform' } - ], - link: { url: 'clickUrl', clicktrackers: ['clickTracker1', 'clickTracker2'] }, - imptrackers: ['imptrackers url1', 'imptrackers url2'], + }, { + id: 2, + required: 0, + data: { + value: 'Native banner. WOW.' + } + }, { + id: 4, + required: 0, + data: { + value: 'Oho' + } + }, { + id: 5, + required: 0, + img: { url: 'test.url.com/Files/58345/308185.jpg?bv=1', w: 30, h: 10 } + }, { + id: 0, + required: 0, + img: { url: 'test.url.com/Files/58345/308200.jpg?bv=1', w: 300, h: 300 } + }], + link: { + url: 'clickUrl', clicktrackers: [ 'clickTracker1', 'clickTracker2' ] + }, + imptrackers: ['imptracker url1', 'imptracker url2'], jstracker: 'jstracker' } } @@ -940,24 +1079,66 @@ describe('Adf adapter', function () { }; let bidRequest = { data: {}, - bids: [{ bidId: 'bidId1' }] + bids: [{ + bidId: 'bidId1', + nativeOrtbRequest: { + ver: '1.2', + assets: [{ + id: 0, + required: 1, + img: { + type: 3, + wmin: 200, + hmin: 166, + ext: { + aspectratios: ['6:5'] + } + } + }, { + id: 1, + required: 1, + title: { + len: 150 + } + }, { + id: 2, + required: 0, + data: { + type: 2 + } + }, { + id: 3, + required: 1, + data: { + type: 1 + } + }, { + id: 4, + required: 1, + data: { + type: 12 + } + }, { + id: 5, + required: 0, + img: { + type: 1, + wmin: 10, + hmin: 10, + ext: { + aspectratios: ['1:1'] + } + } + }] + }, + }] }; const result = spec.interpretResponse(serverResponse, bidRequest)[0].native; const native = bid[0].native; const assets = native.assets; - assert.deepEqual({ - clickUrl: native.link.url, - clickTrackers: native.link.clicktrackers, - impressionTrackers: native.imptrackers, - javascriptTrackers: [ native.jstracker ], - title: assets[0].title.text, - icon: {url: assets[1].img.url, width: assets[1].img.w, height: assets[1].img.h}, - image: {url: assets[2].img.url, width: assets[2].img.w, height: assets[2].img.h}, - body: assets[3].data.value, - cta: assets[4].data.value, - sponsoredBy: assets[5].data.value - }, result); + + assert.deepEqual(result, {ortb: native}); }); it('should return empty when there is no bids in response', function () { const serverResponse = { diff --git a/test/spec/modules/adlooxAdServerVideo_spec.js b/test/spec/modules/adlooxAdServerVideo_spec.js index 170982a51bd..ba9261e1aba 100644 --- a/test/spec/modules/adlooxAdServerVideo_spec.js +++ b/test/spec/modules/adlooxAdServerVideo_spec.js @@ -235,6 +235,7 @@ describe('Adloox Ad Server Video', function () { it('should fetch, retry on withoutCredentials, follow and return a wrapped blob that expires', function (done) { BID.responseTimestamp = utils.timestamp(); BID.ttl = 30; + this.timeout(5000) const clock = sandbox.useFakeTimers(BID.responseTimestamp); diff --git a/test/spec/modules/adlooxRtdProvider_spec.js b/test/spec/modules/adlooxRtdProvider_spec.js index 236e053e58c..5b99789981f 100644 --- a/test/spec/modules/adlooxRtdProvider_spec.js +++ b/test/spec/modules/adlooxRtdProvider_spec.js @@ -1,5 +1,6 @@ import adapterManager from 'src/adapterManager.js'; import analyticsAdapter from 'modules/adlooxAnalyticsAdapter.js'; +import {auctionManager} from 'src/auctionManager.js'; import { config as _config } from 'src/config.js'; import { expect } from 'chai'; import * as events from 'src/events.js'; @@ -75,14 +76,6 @@ describe('Adloox RTD Provider', function () { done(); }); - it('should reject non-string config.params.api_origin', function (done) { - const ret = rtdProvider.init({ params: { api_origin: null } }); - - expect(ret).is.false; - - done(); - }); - it('should reject less than one config.params.imps', function (done) { const ret = rtdProvider.init({ params: { imps: 0 } }); @@ -147,31 +140,37 @@ describe('Adloox RTD Provider', function () { }); let server = null; - let __config = null, CONFIG = null; - let getConfigStub, setConfigStub; + let CONFIG = null; beforeEach(function () { server = sinon.createFakeServer(); - __config = {}; CONFIG = utils.deepClone(config); - getConfigStub = sinon.stub(_config, 'getConfig').callsFake(function (path) { - return utils.deepAccess(__config, path); - }); - setConfigStub = sinon.stub(_config, 'setConfig').callsFake(function (obj) { - utils.mergeDeep(__config, obj); - }); }); afterEach(function () { - setConfigStub.restore(); - getConfigStub.restore(); - getConfigStub = setConfigStub = undefined; CONFIG = null; - __config = null; server.restore(); server = null; }); it('should fetch segments', function (done) { - const req = {}; + const req = { + adUnitCodes: [ adUnit.code ], + ortb2Fragments: { + global: { + site: { + ext: { + data: { + } + } + }, + user: { + ext: { + data: { + } + } + } + } + } + }; const adUnitWithSegments = utils.deepClone(adUnit); const getGlobalStub = sinon.stub(prebidGlobal, 'getGlobal').returns({ adUnits: [ adUnitWithSegments ] @@ -201,119 +200,28 @@ describe('Adloox RTD Provider', function () { }); it('should set ad server targeting', function (done) { - utils.deepSetValue(__config, 'ortb2.site.ext.data.adloox_rtd.ok', true); - const adUnitWithSegments = utils.deepClone(adUnit); utils.deepSetValue(adUnitWithSegments, 'ortb2Imp.ext.data.adloox_rtd.dis', [ 50, 60 ]); const getGlobalStub = sinon.stub(prebidGlobal, 'getGlobal').returns({ adUnits: [ adUnitWithSegments ] }); - const targetingData = rtdProvider.getTargetingData([ adUnitWithSegments.code ], CONFIG, null, { - getFPD: () => ({ - global: __config.ortb2 - }) + const auction = { adUnits: [ adUnitWithSegments ] }; + const getAuctionStub = sinon.stub(auctionManager.index, 'getAuction').returns({ + adUnits: [ adUnitWithSegments ], + getFPD: () => { return { global: { site: { ext: { data: { adloox_rtd: { ok: true } } } } } } } }); + + const targetingData = rtdProvider.getTargetingData([ adUnitWithSegments.code ], CONFIG, null, auction); expect(Object.keys(targetingData).length).is.equal(1); expect(Object.keys(targetingData[adUnit.code]).length).is.equal(2); expect(targetingData[adUnit.code].adl_ok).is.equal(1); expect(targetingData[adUnit.code].adl_dis.length).is.equal(2); + getAuctionStub.restore(); getGlobalStub.restore(); done(); }); }); - - describe('measure atf', function () { - const adUnitCopy = utils.deepClone(adUnit); - - const ratio = 0.38; - const [ [width, height] ] = utils.getAdUnitSizes(adUnitCopy); - - before(function () { - adapterManager.enableAnalytics({ - provider: analyticsAdapterName, - options: analyticsOptions - }); - expect(analyticsAdapter.context).is.not.null; - }); - - after(function () { - analyticsAdapter.disableAnalytics(); - expect(analyticsAdapter.context).is.null; - }); - - it(`should return ${ratio} for same-origin`, function (done) { - const el = document.createElement('div'); - el.setAttribute('id', adUnitCopy.code); - - const offset = height * ratio; - const elStub = sinon.stub(el, 'getBoundingClientRect').returns({ - top: 0 - (height - offset), - bottom: height - offset, - left: 0, - right: width - }); - - const querySelectorStub = sinon.stub(document, 'querySelector'); - querySelectorStub.withArgs(`#${adUnitCopy.code}`).returns(el); - - rtdProvider.atf(adUnitCopy, function(x) { - expect(x).is.equal(ratio); - - querySelectorStub.restore(); - elStub.restore(); - - done(); - }); - }); - - ('IntersectionObserver' in window ? it : it.skip)(`should return ${ratio} for cross-origin`, function (done) { - const frameElementStub = sinon.stub(window, 'frameElement').value(null); - - const el = document.createElement('div'); - el.setAttribute('id', adUnitCopy.code); - - const elStub = sinon.stub(el, 'getBoundingClientRect').returns({ - top: 0, - bottom: height, - left: 0, - right: width - }); - - const querySelectorStub = sinon.stub(document, 'querySelector'); - querySelectorStub.withArgs(`#${adUnitCopy.code}`).returns(el); - - let intersectionObserverStubFn = null; - const intersectionObserverStub = sinon.stub(window, 'IntersectionObserver').callsFake((fn) => { - intersectionObserverStubFn = fn; - return { - observe: (element) => { - expect(element).is.equal(el); - - intersectionObserverStubFn([{ - target: element, - intersectionRect: { width, height }, - intersectionRatio: ratio - }]); - }, - unobserve: (element) => { - expect(element).is.equal(el); - } - } - }); - - rtdProvider.atf(adUnitCopy, function(x) { - expect(x).is.equal(ratio); - - intersectionObserverStub.restore(); - querySelectorStub.restore(); - elStub.restore(); - frameElementStub.restore(); - - done(); - }); - }); - }); }); diff --git a/test/spec/modules/admaruBidAdapter_spec.js b/test/spec/modules/admaruBidAdapter_spec.js index a45ddae108f..813a4ed8b29 100644 --- a/test/spec/modules/admaruBidAdapter_spec.js +++ b/test/spec/modules/admaruBidAdapter_spec.js @@ -121,4 +121,52 @@ describe('Admaru Adapter', function () { expect(result.length).to.equal(0); }); }); + + describe('getUserSyncs()', () => { + it('should return iframe user sync if iframe sync is enabled', () => { + const syncs = spec.getUserSyncs( + { + pixelEnabled: true, + iframeEnabled: true, + }, + null + ); + + expect(syncs).to.deep.equal([ + { + type: 'iframe', + url: 'https://p2.admaru.net/UserSync/sync', + }, + ]); + }); + + it('should return image syncs if they are enabled and iframe is disabled', () => { + const syncs = spec.getUserSyncs( + { + pixelEnabled: true, + iframeEnabled: false, + }, + null + ); + + expect(syncs).to.deep.equal([ + { + type: 'image', + url: 'https://p2.admaru.net/UserSync/sync', + }, + ]); + }); + + it('should not return user syncs if syncs are disabled', () => { + const syncs = spec.getUserSyncs( + { + pixelEnabled: false, + iframeEnabled: false, + }, + null + ); + + expect(syncs).to.deep.equal([]); + }); + }); }); diff --git a/test/spec/modules/admediaBidAdapter_spec.js b/test/spec/modules/admediaBidAdapter_spec.js new file mode 100644 index 00000000000..a04e288311a --- /dev/null +++ b/test/spec/modules/admediaBidAdapter_spec.js @@ -0,0 +1,130 @@ +import {assert, expect} from 'chai'; +import {spec} from 'modules/admediaBidAdapter.js'; +import {newBidder} from 'src/adapters/bidderFactory.js'; +import * as utils from 'src/utils.js'; + +const ENDPOINT_URL = 'https://prebid.admedia.com/bidder/'; + +describe('admediaBidAdapter', function () { + const adapter = newBidder(spec); + describe('isBidRequestValid', function () { + let bid = { + adUnitCode: 'adunit-code', + bidder: 'admedia', + bidId: 'g7ghhs78', + mediaTypes: {banner: {sizes: [[300, 250]]}}, + params: { + placementId: '782332', + aid: '86858', + }, + refererInfo: { + page: 'https://test.com' + } + }; + it('should return true where required params found', function () { + expect(spec.isBidRequestValid(bid)).to.equal(true); + }); + }); + describe('buildRequests', function () { + let bidRequests = [ + { + adUnitCode: 'adunit-code', + bidder: 'admedia', + bidId: 'g7ghhs78', + mediaTypes: {banner: {sizes: [[300, 250]]}}, + params: { + placementId: '782332', + aid: '86858' + }, + refererInfo: { + page: 'https://test.com' + } + } + ]; + + let bidderRequests = { + refererInfo: { + page: 'https://test.com', + } + }; + + const request = spec.buildRequests(bidRequests, bidderRequests); + it('sends bid request via POST', function () { + expect(request[0].method).to.equal('POST'); + }); + }); + + describe('interpretResponse', function () { + let bidRequest = { + method: 'POST', + url: ENDPOINT_URL, + data: { + 'id': '782332', + 'aid': '86858', + 'tags': [ + { + 'sizes': [ + '300x250' + ], + 'id': '782332', + 'aid': '86858' + } + ], + 'bidId': '2556388472b168', + 'referer': 'https%3A%2F%test.com' + } + }; + let serverResponse = { + body: + { + 'tags': [ + { + 'requestId': '2b8bf2ac497ae', + 'ad': "", + 'width': 300, + 'height': 250, + 'cpm': 0.71, + 'currency': 'USD', + 'ttl': 200, + 'creativeId': 128, + 'netRevenue': true, + 'meta': { + 'advertiserDomains': [ + 'https://www.test.com' + ] + } + } + ] + } + + }; + it('should get the correct bid response', function () { + let expectedResponse = + { + 'tags': [ + { + 'requestId': '2b8bf2ac497ae', + 'ad': "", + 'width': 300, + 'height': 250, + 'cpm': 0.71, + 'currency': 'USD', + 'ttl': 200, + 'creativeId': 128, + 'netRevenue': true, + 'meta': { + 'advertiserDomains': [ + 'https://www.test.com' + ] + } + } + ] + } + let result = spec.interpretResponse(serverResponse, bidRequest); + expect(result).to.be.an('array').that.is.not.empty; + expect(Object.keys(result[0])).to.have.members( + Object.keys(expectedResponse.tags[0]) + ); + }); + }); +}); diff --git a/test/spec/modules/admixerBidAdapter_spec.js b/test/spec/modules/admixerBidAdapter_spec.js index 228b87ae4d5..26caaf5b70f 100644 --- a/test/spec/modules/admixerBidAdapter_spec.js +++ b/test/spec/modules/admixerBidAdapter_spec.js @@ -4,8 +4,10 @@ import {newBidder} from 'src/adapters/bidderFactory.js'; import {config} from '../../../src/config.js'; const BIDDER_CODE = 'admixer'; +const BIDDER_CODE_ADX = 'admixeradx'; const ENDPOINT_URL = 'https://inv-nets.admixer.net/prebid.1.2.aspx'; const ENDPOINT_URL_CUSTOM = 'https://custom.admixer.net/prebid.aspx'; +const ENDPOINT_URL_ADX = 'https://inv-nets.admixer.net/adxprebid.1.2.aspx'; const ZONE_ID = '2eb6bd58-865c-47ce-af7f-a918108c3fd2'; describe('AdmixerAdapter', function () { @@ -16,18 +18,22 @@ describe('AdmixerAdapter', function () { expect(adapter.callBids).to.be.exist.and.to.be.a('function'); }); }); + // inv-nets.admixer.net/adxprebid.1.2.aspx describe('isBidRequestValid', function () { let bid = { - 'bidder': BIDDER_CODE, - 'params': { - 'zone': ZONE_ID + bidder: BIDDER_CODE, + params: { + zone: ZONE_ID, }, - 'adUnitCode': 'adunit-code', - 'sizes': [[300, 250], [300, 600]], - 'bidId': '30b31c1838de1e', - 'bidderRequestId': '22edbae2733bf6', - 'auctionId': '1d1a030790a475', + adUnitCode: 'adunit-code', + sizes: [ + [300, 250], + [300, 600], + ], + bidId: '30b31c1838de1e', + bidderRequestId: '22edbae2733bf6', + auctionId: '1d1a030790a475', }; it('should return true when required params found', function () { @@ -38,7 +44,7 @@ describe('AdmixerAdapter', function () { let bid = Object.assign({}, bid); delete bid.params; bid.params = { - 'placementId': 0 + placementId: 0, }; expect(spec.isBidRequestValid(bid)).to.equal(false); }); @@ -47,22 +53,25 @@ describe('AdmixerAdapter', function () { describe('buildRequests', function () { let validRequest = [ { - 'bidder': BIDDER_CODE, - 'params': { - 'zone': ZONE_ID + bidder: BIDDER_CODE, + params: { + zone: ZONE_ID, }, - 'adUnitCode': 'adunit-code', - 'sizes': [[300, 250], [300, 600]], - 'bidId': '30b31c1838de1e', - 'bidderRequestId': '22edbae2733bf6', - 'auctionId': '1d1a030790a475', - } + adUnitCode: 'adunit-code', + sizes: [ + [300, 250], + [300, 600], + ], + bidId: '30b31c1838de1e', + bidderRequestId: '22edbae2733bf6', + auctionId: '1d1a030790a475', + }, ]; let bidderRequest = { bidderCode: BIDDER_CODE, refererInfo: { - page: 'https://example.com' - } + page: 'https://example.com', + }, }; it('should add referrer and imp to be equal bidRequest', function () { @@ -81,49 +90,122 @@ describe('AdmixerAdapter', function () { it('sends bid request to CUSTOM_ENDPOINT via GET', function () { config.setBidderConfig({ bidders: [BIDDER_CODE], // one or more bidders - config: {[BIDDER_CODE]: {endpoint_url: ENDPOINT_URL_CUSTOM}} + config: { [BIDDER_CODE]: { endpoint_url: ENDPOINT_URL_CUSTOM } }, }); - const request = config.runWithBidder(BIDDER_CODE, () => spec.buildRequests(validRequest, bidderRequest)); + const request = config.runWithBidder(BIDDER_CODE, () => + spec.buildRequests(validRequest, bidderRequest) + ); expect(request.url).to.equal(ENDPOINT_URL_CUSTOM); expect(request.method).to.equal('POST'); }); }); + describe('buildRequestsAdmixerADX', function () { + let validRequest = [ + { + bidder: BIDDER_CODE_ADX, + params: { + zone: ZONE_ID, + }, + adUnitCode: 'adunit-code', + sizes: [ + [300, 250], + [300, 600], + ], + bidId: '30b31c1838de1e', + bidderRequestId: '22edbae2733bf6', + auctionId: '1d1a030790a475', + }, + ]; + let bidderRequest = { + bidderCode: BIDDER_CODE_ADX, + refererInfo: { + page: 'https://example.com', + }, + }; + + it('sends bid request to ADX ENDPOINT', function () { + const request = spec.buildRequests(validRequest, bidderRequest); + expect(request.url).to.equal(ENDPOINT_URL_ADX); + expect(request.method).to.equal('POST'); + }); + }); + + describe('checkFloorGetting', function () { + let validRequest = [ + { + bidder: BIDDER_CODE, + params: { + zone: ZONE_ID, + }, + adUnitCode: 'adunit-code', + sizes: [[300, 250]], + bidId: '30b31c1838de1e', + bidderRequestId: '22edbae2733bf6', + auctionId: '1d1a030790a475', + }, + ]; + let bidderRequest = { + bidderCode: BIDDER_CODE, + refererInfo: { + page: 'https://example.com', + }, + }; + it('gets floor', function () { + bidderRequest.getFloor = () => { + return { floor: 0.6 }; + }; + const request = spec.buildRequests(validRequest, bidderRequest); + const payload = request.data; + expect(payload.bidFloor).to.deep.equal(0.6); + }); + }); + describe('interpretResponse', function () { let response = { body: { - ads: [{ - 'currency': 'USD', - 'cpm': 6.210000, - 'ad': '
ad
', - 'width': 300, - 'height': 600, - 'creativeId': 'ccca3e5e-0c54-4761-9667-771322fbdffc', - 'ttl': 360, - 'netRevenue': false, - 'requestId': '5e4e763b6bc60b', - 'dealId': 'asd123', - 'meta': {'advertiserId': 123, 'networkId': 123, 'advertiserDomains': ['test.com']} - }] - } + ads: [ + { + currency: 'USD', + cpm: 6.21, + ad: '
ad
', + width: 300, + height: 600, + creativeId: 'ccca3e5e-0c54-4761-9667-771322fbdffc', + ttl: 360, + netRevenue: false, + requestId: '5e4e763b6bc60b', + dealId: 'asd123', + meta: { + advertiserId: 123, + networkId: 123, + advertiserDomains: ['test.com'], + }, + }, + ], + }, }; it('should get correct bid response', function () { const ads = response.body.ads; let expectedResponse = [ { - 'requestId': ads[0].requestId, - 'cpm': ads[0].cpm, - 'creativeId': ads[0].creativeId, - 'width': ads[0].width, - 'height': ads[0].height, - 'ad': ads[0].ad, - 'currency': ads[0].currency, - 'netRevenue': ads[0].netRevenue, - 'ttl': ads[0].ttl, - 'dealId': ads[0].dealId, - 'meta': {'advertiserId': 123, 'networkId': 123, 'advertiserDomains': ['test.com']} - } + requestId: ads[0].requestId, + cpm: ads[0].cpm, + creativeId: ads[0].creativeId, + width: ads[0].width, + height: ads[0].height, + ad: ads[0].ad, + currency: ads[0].currency, + netRevenue: ads[0].netRevenue, + ttl: ads[0].ttl, + dealId: ads[0].dealId, + meta: { + advertiserId: 123, + networkId: 123, + advertiserDomains: ['test.com'], + }, + }, ]; let result = spec.interpretResponse(response); @@ -141,18 +223,16 @@ describe('AdmixerAdapter', function () { describe('getUserSyncs', function () { let imgUrl = 'https://example.com/img1'; let frmUrl = 'https://example.com/frm2'; - let responses = [{ - body: { - cm: { - pixels: [ - imgUrl - ], - iframes: [ - frmUrl - ], - } - } - }]; + let responses = [ + { + body: { + cm: { + pixels: [imgUrl], + iframes: [frmUrl], + }, + }, + }, + ]; it('Returns valid values', function () { let userSyncAll = spec.getUserSyncs({pixelEnabled: true, iframeEnabled: true}, responses); diff --git a/test/spec/modules/admixerIdSystem_spec.js b/test/spec/modules/admixerIdSystem_spec.js index 18107b780db..753b1e3c2d5 100644 --- a/test/spec/modules/admixerIdSystem_spec.js +++ b/test/spec/modules/admixerIdSystem_spec.js @@ -1,9 +1,6 @@ import {admixerIdSubmodule} from 'modules/admixerIdSystem.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 = '4D393FAC-B6BB-4E19-8396-0A4813607316'; const getIdParams = {params: {pid: pid}}; diff --git a/test/spec/modules/adnuntiusBidAdapter_spec.js b/test/spec/modules/adnuntiusBidAdapter_spec.js index 2d5ea630f0f..150e013af98 100644 --- a/test/spec/modules/adnuntiusBidAdapter_spec.js +++ b/test/spec/modules/adnuntiusBidAdapter_spec.js @@ -13,21 +13,21 @@ describe('adnuntiusBidAdapter', function () { const meta = [{ key: 'usi', value: usi }] before(() => { - const storage = getStorageManager({ gvlid: GVLID, moduleName: 'adnuntius' }) - storage.setDataInLocalStorage('adn.metaData', JSON.stringify(meta)) - }); - - beforeEach(function () { $$PREBID_GLOBAL$$.bidderSettings = { adnuntius: { storageAllowed: true } }; + const storage = getStorageManager({ bidderCode: 'adnuntius' }) + storage.setDataInLocalStorage('adn.metaData', JSON.stringify(meta)) + }); + + after(() => { + $$PREBID_GLOBAL$$.bidderSettings = {}; }); afterEach(function () { config.resetConfig(); - $$PREBID_GLOBAL$$.bidderSettings = {}; }); const tzo = new Date().getTimezoneOffset(); @@ -71,6 +71,30 @@ describe('adnuntiusBidAdapter', function () { } ] + // const nativeBidderRequest = [ + // { + // bidId: '123', + // bidder: 'adnuntius', + // params: { + // auId: '8b6bc', + // network: 'adnuntius', + // }, + // mediaTypes: { + // native: { + // title: { + // required: true + // }, + // image: { + // required: true + // }, + // body: { + // required: true + // } + // } + // }, + // } + // ] + const singleBidRequest = { bid: [ { @@ -83,6 +107,10 @@ describe('adnuntiusBidAdapter', function () { bid: videoBidderRequest } + // const nativeBidRequest = { + // bid: nativeBidderRequest + // } + const serverResponse = { body: { 'adUnits': [ @@ -209,6 +237,83 @@ describe('adnuntiusBidAdapter', function () { ] } } + // const serverNativeResponse = { + // body: { + // 'adUnits': [ + // { + // 'auId': '000000000008b6bc', + // 'targetId': '123', + // 'html': '

hi!

', + // 'matchedAdCount': 1, + // 'responseId': 'adn-rsp-1460129238', + // 'ads': [ + // { + // 'destinationUrlEsc': 'https%3A%2F%2Fdelivery.adnuntius.com%2Fc%2F52AHNuxCqxB_Y9ZP9ERWkMBPCOha4zuV3aKn5cog5jsAAAAQCtjQz9kbGWD4nuZy3q6HaHGLB4-k_fySWECIOOmHKY6iokgHNFH-U57ew_-1QHlKnFr2NT8y4QK1oU5HxnDLbYPz-GmQ3C2JyxLGpKmIb-P-3bm7HYPEreNjPdhjRG51A8NGuc4huUhns7nEUejHuOjOHE5sV1zfYxCRWRx9wPDN9EUCC7KN%3Fct%3D2501%26r%3Dhttp%253A%252F%252Fgoogle.com', + // 'assets': { + // 'image': { + // 'cdnId': 'https://assets.adnuntius.com/K9rfXC6wJvgVuy4Fbt5P8oEEGXme9ZaP8BNDzz3OMGQ.jpg', + // 'width': '300', + // 'height': '250' + // } + // }, + // 'text': { + // 'body': { + // 'content': 'Testing Native ad from Adnuntius', + // 'length': '32', + // 'minLength': '0', + // 'maxLength': '100' + // }, + // 'title': { + // 'content': 'Native Ad', + // 'length': '9', + // 'minLength': '5', + // 'maxLength': '100' + // } + // }, + // 'clickUrl': 'https://delivery.adnuntius.com/c/52AHNuxCqxB_Y9ZP9ERWkMBPCOha4zuV3aKn5cog5jsAAAAQCtjQz9kbGWD4nuZy3q6HaHGLB4-k_fySWECIOOmHKY6iokgHNFH-U57ew_-1QHlKnFr2NT8y4QK1oU5HxnDLbYPz-GmQ3C2JyxLGpKmIb-P-3bm7HYPEreNjPdhjRG51A8NGuc4huUhns7nEUejHuOjOHE5sV1zfYxCRWRx9wPDN9EUCC7KN', + // 'urls': { + // 'destination': 'https://delivery.adnuntius.com/c/52AHNuxCqxB_Y9ZP9ERWkMBPCOha4zuV3aKn5cog5jsAAAAQCtjQz9kbGWD4nuZy3q6HaHGLB4-k_fySWECIOOmHKY6iokgHNFH-U57ew_-1QHlKnFr2NT8y4QK1oU5HxnDLbYPz-GmQ3C2JyxLGpKmIb-P-3bm7HYPEreNjPdhjRG51A8NGuc4huUhns7nEUejHuOjOHE5sV1zfYxCRWRx9wPDN9EUCC7KN?ct=2501&r=http%3A%2F%2Fgoogle.com' + // }, + // 'urlsEsc': { + // 'destination': 'https%3A%2F%2Fdelivery.adnuntius.com%2Fc%2F52AHNuxCqxB_Y9ZP9ERWkMBPCOha4zuV3aKn5cog5jsAAAAQCtjQz9kbGWD4nuZy3q6HaHGLB4-k_fySWECIOOmHKY6iokgHNFH-U57ew_-1QHlKnFr2NT8y4QK1oU5HxnDLbYPz-GmQ3C2JyxLGpKmIb-P-3bm7HYPEreNjPdhjRG51A8NGuc4huUhns7nEUejHuOjOHE5sV1zfYxCRWRx9wPDN9EUCC7KN%3Fct%3D2501%26r%3Dhttp%253A%252F%252Fgoogle.com' + // }, + // 'destinationUrls': { + // 'destination': 'http://google.com' + // }, + // 'cpm': { 'amount': 5.0, 'currency': 'NOK' }, + // 'bid': { 'amount': 0.005, 'currency': 'NOK' }, + // 'cost': { 'amount': 0.005, 'currency': 'NOK' }, + // 'impressionTrackingUrls': [], + // 'impressionTrackingUrlsEsc': [], + // 'adId': 'adn-id-1347343135', + // 'selectedColumn': '0', + // 'selectedColumnPosition': '0', + // 'renderedPixel': 'https://delivery.adnuntius.com/b/52AHNuxCqxB_Y9ZP9ERWkMBPCOha4zuV3aKn5cog5jsAAAAQCtjQz9kbGWD4nuZy3q6HaHGLB4-k_fySWECIOOmHKY6iokgHNFH-U57ew_-1QHlKnFr2NT8y4QK1oU5HxnDLbYPz-GmQ3C2JyxLGpKmIb-P-3bm7HYPEreNjPdhjRG51A8NGuc4huUhns7nEUejHuOjOHE5sV1zfYxCRWRx9wPDN9EUCC7KN.html', + // 'renderedPixelEsc': 'https%3A%2F%2Fdelivery.adnuntius.com%2Fb%2F52AHNuxCqxB_Y9ZP9ERWkMBPCOha4zuV3aKn5cog5jsAAAAQCtjQz9kbGWD4nuZy3q6HaHGLB4-k_fySWECIOOmHKY6iokgHNFH-U57ew_-1QHlKnFr2NT8y4QK1oU5HxnDLbYPz-GmQ3C2JyxLGpKmIb-P-3bm7HYPEreNjPdhjRG51A8NGuc4huUhns7nEUejHuOjOHE5sV1zfYxCRWRx9wPDN9EUCC7KN.html', + // 'visibleUrl': 'https://delivery.adnuntius.com/s?rt=52AHNuxCqxB_Y9ZP9ERWkMBPCOha4zuV3aKn5cog5jsAAAAQCtjQz9kbGWD4nuZy3q6HaHGLB4-k_fySWECIOOmHKY6iokgHNFH-U57ew_-1QHlKnFr2NT8y4QK1oU5HxnDLbYPz-GmQ3C2JyxLGpKmIb-P-3bm7HYPEreNjPdhjRG51A8NGuc4huUhns7nEUejHuOjOHE5sV1zfYxCRWRx9wPDN9EUCC7KN', + // 'visibleUrlEsc': 'https%3A%2F%2Fdelivery.adnuntius.com%2Fs%3Frt%3D52AHNuxCqxB_Y9ZP9ERWkMBPCOha4zuV3aKn5cog5jsAAAAQCtjQz9kbGWD4nuZy3q6HaHGLB4-k_fySWECIOOmHKY6iokgHNFH-U57ew_-1QHlKnFr2NT8y4QK1oU5HxnDLbYPz-GmQ3C2JyxLGpKmIb-P-3bm7HYPEreNjPdhjRG51A8NGuc4huUhns7nEUejHuOjOHE5sV1zfYxCRWRx9wPDN9EUCC7KN', + // 'viewUrl': 'https://delivery.adnuntius.com/v?rt=52AHNuxCqxB_Y9ZP9ERWkMBPCOha4zuV3aKn5cog5jsAAAAQCtjQz9kbGWD4nuZy3q6HaHGLB4-k_fySWECIOOmHKY6iokgHNFH-U57ew_-1QHlKnFr2NT8y4QK1oU5HxnDLbYPz-GmQ3C2JyxLGpKmIb-P-3bm7HYPEreNjPdhjRG51A8NGuc4huUhns7nEUejHuOjOHE5sV1zfYxCRWRx9wPDN9EUCC7KN', + // 'viewUrlEsc': 'https%3A%2F%2Fdelivery.adnuntius.com%2Fv%3Frt%3D52AHNuxCqxB_Y9ZP9ERWkMBPCOha4zuV3aKn5cog5jsAAAAQCtjQz9kbGWD4nuZy3q6HaHGLB4-k_fySWECIOOmHKY6iokgHNFH-U57ew_-1QHlKnFr2NT8y4QK1oU5HxnDLbYPz-GmQ3C2JyxLGpKmIb-P-3bm7HYPEreNjPdhjRG51A8NGuc4huUhns7nEUejHuOjOHE5sV1zfYxCRWRx9wPDN9EUCC7KN', + // 'rt': '52AHNuxCqxB_Y9ZP9ERWkMBPCOha4zuV3aKn5cog5jsAAAAQCtjQz9kbGWD4nuZy3q6HaHGLB4-k_fySWECIOOmHKY6iokgHNFH-U57ew_-1QHlKnFr2NT8y4QK1oU5HxnDLbYPz-GmQ3C2JyxLGpKmIb-P-3bm7HYPEreNjPdhjRG51A8NGuc4huUhns7nEUejHuOjOHE5sV1zfYxCRWRx9wPDN9EUCC7KN', + // 'creativeWidth': '980', + // 'creativeHeight': '120', + // 'creativeId': 'wgkq587vgtpchsx1', + // 'lineItemId': 'scyjdyv3mzgdsnpf', + // 'layoutId': 'sw6gtws2rdj1kwby', + // 'layoutName': 'Responsive image' + // }, + + // ] + // }, + // { + // 'auId': '000000000008b6bc', + // 'targetId': '456', + // 'matchedAdCount': 0, + // 'responseId': 'adn-rsp-1460129238', + // } + // ] + // } + // } describe('inherited functions', function () { it('exists and is a function', function () { @@ -426,4 +531,21 @@ describe('adnuntiusBidAdapter', function () { expect(interpretedResponse[0].vastXml).to.equal(serverVideoResponse.body.adUnits[0].vastXml); }); }); + // describe('interpretNativeResponse', function () { + // it('should return valid response when passed valid server response', function () { + // const interpretedResponse = spec.interpretResponse(serverNativeResponse, nativeBidRequest); + // const ad = serverNativeResponse.body.adUnits[0].ads[0] + // expect(interpretedResponse).to.have.lengthOf(1); + // expect(interpretedResponse[0].cpm).to.equal(ad.cpm.amount); + // expect(interpretedResponse[0].width).to.equal(Number(ad.creativeWidth)); + // expect(interpretedResponse[0].height).to.equal(Number(ad.creativeHeight)); + // expect(interpretedResponse[0].creativeId).to.equal(ad.creativeId); + // expect(interpretedResponse[0].currency).to.equal(ad.bid.currency); + // expect(interpretedResponse[0].netRevenue).to.equal(false); + // expect(interpretedResponse[0].meta).to.have.property('advertiserDomains'); + // expect(interpretedResponse[0].meta.advertiserDomains).to.have.lengthOf(1); + // expect(interpretedResponse[0].meta.advertiserDomains[0]).to.equal('google.com'); + // expect(interpretedResponse[0].native.body).to.equal(serverNativeResponse.body.adUnits[0].ads[0].text.body.content); + // }); + // }); }); diff --git a/test/spec/modules/adrinoBidAdapter_spec.js b/test/spec/modules/adrinoBidAdapter_spec.js index 78cea8da9ac..577dd3e9164 100644 --- a/test/spec/modules/adrinoBidAdapter_spec.js +++ b/test/spec/modules/adrinoBidAdapter_spec.js @@ -86,16 +86,16 @@ describe('adrinoBidAdapter', function () { ); expect(result.length).to.equal(1); expect(result[0].method).to.equal('POST'); - expect(result[0].url).to.equal('https://stg-prebid-bidder.adrino.io/bidder/bid/'); - expect(result[0].data.bidId).to.equal('12345678901234'); - expect(result[0].data.placementHash).to.equal('abcdef123456'); - expect(result[0].data.referer).to.equal('http://example.com/'); - expect(result[0].data.userAgent).to.equal(navigator.userAgent); - expect(result[0].data).to.have.property('nativeParams'); - expect(result[0].data).not.to.have.property('gdprConsent'); - expect(result[0].data).to.have.property('userId'); - expect(result[0].data.userId.criteoId).to.equal('2xqi3F94aHdwWnM3'); - expect(result[0].data.userId.pubcid).to.equal('3ec0b202-7697'); + expect(result[0].url).to.equal('https://stg-prebid-bidder.adrino.io/bidder/bids/'); + expect(result[0].data[0].bidId).to.equal('12345678901234'); + expect(result[0].data[0].placementHash).to.equal('abcdef123456'); + expect(result[0].data[0].referer).to.equal('http://example.com/'); + expect(result[0].data[0].userAgent).to.equal(navigator.userAgent); + expect(result[0].data[0]).to.have.property('nativeParams'); + expect(result[0].data[0]).not.to.have.property('gdprConsent'); + expect(result[0].data[0]).to.have.property('userId'); + expect(result[0].data[0].userId.criteoId).to.equal('2xqi3F94aHdwWnM3'); + expect(result[0].data[0].userId.pubcid).to.equal('3ec0b202-7697'); }); it('should build the request correctly with gdpr', function () { @@ -105,16 +105,16 @@ describe('adrinoBidAdapter', function () { ); expect(result.length).to.equal(1); expect(result[0].method).to.equal('POST'); - expect(result[0].url).to.equal('https://prd-prebid-bidder.adrino.io/bidder/bid/'); - expect(result[0].data.bidId).to.equal('12345678901234'); - expect(result[0].data.placementHash).to.equal('abcdef123456'); - expect(result[0].data.referer).to.equal('http://example.com/'); - expect(result[0].data.userAgent).to.equal(navigator.userAgent); - expect(result[0].data).to.have.property('nativeParams'); - expect(result[0].data).to.have.property('gdprConsent'); - expect(result[0].data).to.have.property('userId'); - expect(result[0].data.userId.criteoId).to.equal('2xqi3F94aHdwWnM3'); - expect(result[0].data.userId.pubcid).to.equal('3ec0b202-7697'); + expect(result[0].url).to.equal('https://prd-prebid-bidder.adrino.io/bidder/bids/'); + expect(result[0].data[0].bidId).to.equal('12345678901234'); + expect(result[0].data[0].placementHash).to.equal('abcdef123456'); + expect(result[0].data[0].referer).to.equal('http://example.com/'); + expect(result[0].data[0].userAgent).to.equal(navigator.userAgent); + expect(result[0].data[0]).to.have.property('nativeParams'); + expect(result[0].data[0]).to.have.property('gdprConsent'); + expect(result[0].data[0]).to.have.property('userId'); + expect(result[0].data[0].userId.criteoId).to.equal('2xqi3F94aHdwWnM3'); + expect(result[0].data[0].userId.pubcid).to.equal('3ec0b202-7697'); }); it('should build the request correctly without gdpr', function () { @@ -124,22 +124,22 @@ describe('adrinoBidAdapter', function () { ); expect(result.length).to.equal(1); expect(result[0].method).to.equal('POST'); - expect(result[0].url).to.equal('https://prd-prebid-bidder.adrino.io/bidder/bid/'); - expect(result[0].data.bidId).to.equal('12345678901234'); - expect(result[0].data.placementHash).to.equal('abcdef123456'); - expect(result[0].data.referer).to.equal('http://example.com/'); - expect(result[0].data.userAgent).to.equal(navigator.userAgent); - expect(result[0].data).to.have.property('nativeParams'); - expect(result[0].data).not.to.have.property('gdprConsent'); - expect(result[0].data).to.have.property('userId'); - expect(result[0].data.userId.criteoId).to.equal('2xqi3F94aHdwWnM3'); - expect(result[0].data.userId.pubcid).to.equal('3ec0b202-7697'); + expect(result[0].url).to.equal('https://prd-prebid-bidder.adrino.io/bidder/bids/'); + expect(result[0].data[0].bidId).to.equal('12345678901234'); + expect(result[0].data[0].placementHash).to.equal('abcdef123456'); + expect(result[0].data[0].referer).to.equal('http://example.com/'); + expect(result[0].data[0].userAgent).to.equal(navigator.userAgent); + expect(result[0].data[0]).to.have.property('nativeParams'); + expect(result[0].data[0]).not.to.have.property('gdprConsent'); + expect(result[0].data[0]).to.have.property('userId'); + expect(result[0].data[0].userId.criteoId).to.equal('2xqi3F94aHdwWnM3'); + expect(result[0].data[0].userId.pubcid).to.equal('3ec0b202-7697'); }); }); describe('interpretResponse', function () { it('should interpret the response correctly', function () { - const response = { + const response1 = { requestId: '31662c69728811', mediaType: 'native', cpm: 0.53, @@ -167,13 +167,44 @@ describe('adrinoBidAdapter', function () { } }; + const response2 = { + requestId: '31662c69728812', + mediaType: 'native', + cpm: 0.77, + currency: 'PLN', + creativeId: '859120', + netRevenue: true, + ttl: 600, + width: 1, + height: 1, + noAd: false, + testAd: false, + native: { + title: 'Ad Title', + body: 'Ad Body', + image: { + url: 'http://emisja.contentstream.pl/_/getImageII/?vid=17180728299&typ=cs_300_150&element=IMAGE&scale=1&prefix=adart&nc=1643878278955', + height: 150, + width: 300 + }, + clickUrl: 'http://emisja.contentstream.pl/_/ctr2/?u=https%3A%2F%2Fonline.efortuna.pl%2Fpage%3Fkey%3Dej0xMzUzMTM1NiZsPTE1Mjc1MzY1JnA9NTMyOTA%253D&e=znU3tABN8K4N391dmUxYfte5G9tBaDXELJVo1_-kvaTJH2XwWRw77fmfL2YjcEmrbqRQ3M0GcJ0vPWcLtZlsrf8dWrAEHNoZKAC6JMnZF_65IYhTPbQIJ-zn3ac9TU7gEZftFKksH1al7rMuieleVv9r6_DtrOk_oZcYAe4rMRQM-TiWvivJRPBchAAblE0cqyG7rCunJFpal43sxlYm4GvcBJaYHzErn5PXjEzNbd3xHjkdiap-xU9y6BbfkUZ1xIMS8QZLvwNrTXMFCSfSRN2tgVfEj7KyGdLCITHSaFtuIKT2iW2pxC7f2RtPHnzsEPXH0SgAfhA3OxZ5jkQjOZy0PsO7MiCv3sJai5ezUAOjFgayU91ZhI0Y9r2YpB1tTGIjnO23wot8PvRENlThHQ%3D%3D&ref=https%3A%2F%2Fbox.adrino.cloud%2Ftmielcarz%2Fadrino_prebid%2Ftest_page3.html%3Fpbjs_debug%3Dtrue', + privacyLink: 'https://adrino.pl/wp-content/uploads/2021/01/POLITYKA-PRYWATNOS%CC%81CI-Adrino-Mobile.pdf', + impressionTrackers: [ + 'https://prd-impression-tracker-producer.adrino.io/impression/eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJ7XCJpbXByZXNzaW9uSWRcIjpcIjMxNjYyYzY5NzI4ODExXCIsXCJkYXRlXCI6WzIwMjIsMiwzXSxcInBsYWNlbWVudEhhc2hcIjpcIjk0NTVjMDQxYzlkMTI1ZmIwNDE4MWVhMGVlZTJmMmFlXCIsXCJjYW1wYWlnbklkXCI6MTc5MjUsXCJhZHZlcnRpc2VtZW50SWRcIjo5MjA3OSxcInZpc3VhbGlzYXRpb25JZFwiOjg1OTExNSxcImNwbVwiOjUzLjB9IiwiZXhwIjoxNjQzOTE2MjUxLCJpYXQiOjE2NDM5MTU2NTF9.0Y_HvInGl6Xo5xP6rDLC8lzQRGvy-wKe0blk1o8ebWyVRFiUY1JGLUeE0k3sCsPNxgdHAv-o6EcbogpUuqlMJA' + ] + } + }; + const serverResponse = { - body: response + body: { bidResponses: [response1, response2] } }; const result = spec.interpretResponse(serverResponse, {}); - expect(result.length).to.equal(1); - expect(result[0]).to.equal(response); + expect(result.length).to.equal(2); + expect(result[0]).to.equal(response1); + expect(result[0].requestId).to.equal('31662c69728811'); + expect(result[1]).to.equal(response2); + expect(result[1].requestId).to.equal('31662c69728812'); }); it('should return empty array of responses', function () { diff --git a/test/spec/modules/adsinteractiveBidAdapter_spec.js b/test/spec/modules/adsinteractiveBidAdapter_spec.js new file mode 100644 index 00000000000..d0f90bd71de --- /dev/null +++ b/test/spec/modules/adsinteractiveBidAdapter_spec.js @@ -0,0 +1,207 @@ +import { expect } from 'chai'; +import { spec } from 'modules/adsinteractiveBidAdapter.js'; +import { newBidder } from 'src/adapters/bidderFactory.js'; + +describe('adsinteractiveBidAdapter', function () { + let bid = { + ortb2: { + site: { + page: 'http://test.com', + domain: 'test.com', + publisher: { + domain: 'test.com', + }, + }, + }, + bidder: 'adsinteractive', + sizes: [[300, 250]], + bidId: '32469kja92389', + params: { + adUnit: 'example_adunit_1', + }, + } + + const bidderRequest = { + refererInfo: { + isAmp: 0 + } } + + describe('build requests', () => { + it('sends bid request to ENDPOINT via POST', function () { + const request = spec.buildRequests([ + { + ortb2: { + site: { + page: 'http://test.com', + domain: 'test.com', + publisher: { + domain: 'test.com', + }, + }, + }, + bidder: 'adsinteractive', + sizes: [[300, 250]], + bidId: '32469kja92389', + params: { + adUnit: 'example_adunit_1', + }, + }, + ], bidderRequest); + expect(request[0].method).to.equal('POST'); + }); + it('sends bid request to adsinteractive endpoint', function () { + const request = spec.buildRequests([ + { + ortb2: { + site: { + page: 'http://test.com', + domain: 'test.com', + publisher: { + domain: 'test.com', + }, + }, + }, + bidder: 'adsinteractive', + sizes: [[300, 250]], + bidId: '32469kja92389', + params: { + adUnit: 'example_adunit_1', + }, + }, + ], bidderRequest); + expect(request[0].url).to.equal('https://pb.adsinteractive.com/prebid'); + }); + }); + + describe('inherited functions', () => { + const adapter = newBidder(spec); + it('exists and is a function', () => { + expect(adapter.callBids).to.exist.and.to.be.a('function'); + }); + }); + + describe('isBidRequestValid', function () { + it('should return true when necessary information is found', function () { + expect(spec.isBidRequestValid(bid)).to.be.true; + }); + + it('should return false when necessary information is not found', function () { + // empty bid + expect(spec.isBidRequestValid({ bidId: '', params: {} })).to.be.false; + + // empty bidId + bid.bidId = ''; + expect(spec.isBidRequestValid(bid)).to.be.false; + + // empty adUnit + bid.bidId = '32469kja92389'; + bid.params.adUnit = ''; + expect(spec.isBidRequestValid(bid)).to.be.false; + }); + + it('returns false when bidder not set to "adsinteractive"', function () { + const invalidBid = { + bidder: 'newyork', + sizes: [[300, 250]], + bidId: '32469kja92389', + params: { + adUnit: 'example_adunit_1', + }, + }; + expect(spec.isBidRequestValid(invalidBid)).to.be.false; + }); + + it('returns false when adUnit is not set in params', function () { + const invalidBid = { + bidder: 'adsinteractive', + sizes: [[300, 250]], + bidId: '32469kja92389', + params: {}, + }; + + expect(spec.isBidRequestValid(invalidBid)).to.be.false; + }); + }); + describe('interpretResponse', function () { + let serverResponse; + let bidRequest = { data: { id: 'adsinteractiverequest-9320' } }; + + beforeEach(function () { + serverResponse = { + body: { + id: '239823rhaldf822', + seatbid: [ + { + bid: [ + { + id: 'fae50ca1-3f69-4b34-bf6d-b2eb0ae3376b', + impid: 'example_adunit_1', + price: 0.49, + netRevenue: true, + ttl: 1000, + meta: {advertiserDomains: []}, + adm: '', + crid: '932048jda99cr', + h: 250, + w: 300, + }, + ], + seat: 'adsinteractive', + }, + ], + cur: 'USD', + }, + }; + }); + + it('validate_response_params', function () { + const newResponse = spec.interpretResponse(serverResponse, bidRequest); + expect(newResponse[0].id).to.be.equal( + 'fae50ca1-3f69-4b34-bf6d-b2eb0ae3376b' + ); + expect(newResponse[0].requestId).to.be.equal( + 'adsinteractiverequest-9320' + ); + expect(newResponse[0].cpm).to.be.equal(0.49); + expect(newResponse[0].width).to.be.equal(300); + expect(newResponse[0].height).to.be.equal(250); + expect(newResponse[0].currency).to.be.equal('USD'); + expect(newResponse[0].ad).to.be.equal( + '' + ); + }); + + it('should correctly reorder the server response', function () { + const newResponse = spec.interpretResponse(serverResponse, bidRequest); + expect(newResponse.length).to.be.equal(1); + expect(newResponse[0]).to.deep.equal({ + id: 'fae50ca1-3f69-4b34-bf6d-b2eb0ae3376b', + requestId: 'adsinteractiverequest-9320', + cpm: 0.49, + netRevenue: true, + ttl: 1000, + width: 300, + height: 250, + meta: {advertiserDomains: []}, + creativeId: '932048jda99cr', + currency: 'USD', + ad: '', + }); + }); + + it('should not add responses if the cpm is 0 or null', function () { + serverResponse.body.seatbid[0].bid[0].price = 0; + let response = spec.interpretResponse(serverResponse, bidRequest); + expect(response).to.deep.equal([]); + + serverResponse.body.seatbid[0].bid[0].price = null; + response = spec.interpretResponse(serverResponse, bidRequest); + expect(response).to.deep.equal([]); + }); + it('should add responses if the cpm is valid', function () { + serverResponse.body.seatbid[0].bid[0].price = 0.5; + let response = spec.interpretResponse(serverResponse, bidRequest); + expect(response).to.not.deep.equal([]); + }); + }); +}); diff --git a/test/spec/modules/adtelligentBidAdapter_spec.js b/test/spec/modules/adtelligentBidAdapter_spec.js index d0ef69ccf08..c57e51c44fc 100644 --- a/test/spec/modules/adtelligentBidAdapter_spec.js +++ b/test/spec/modules/adtelligentBidAdapter_spec.js @@ -11,16 +11,17 @@ const EXPECTED_ENDPOINTS = [ 'https://ghb.adtelligent.com/v2/auction/' ]; const aliasEP = { - appaloosa: 'https://ghb.hb.appaloosa.media/v2/auction/', - appaloosa_publisherSuffix: 'https://ghb.hb.appaloosa.media/v2/auction/', - onefiftytwomedia: 'https://ghb.ads.152media.com/v2/auction/', - navelix: 'https://ghb.hb.navelix.com/v2/auction/', - bidsxchange: 'https://ghb.hbd.bidsxchange.com/v2/auction/', - streamkey: 'https://ghb.hb.streamkey.net/v2/auction/', - janet: 'https://ghb.bidder.jmgads.com/v2/auction/', - pgam: 'https://ghb.pgamssp.com/v2/auction/', - ocm: 'https://ghb.cenarius.orangeclickmedia.com/v2/auction/', - vidcrunchllc: 'https://ghb.platform.vidcrunch.com/v2/auction/', + 'appaloosa': 'https://ghb.hb.appaloosa.media/v2/auction/', + 'appaloosa_publisherSuffix': 'https://ghb.hb.appaloosa.media/v2/auction/', + 'onefiftytwomedia': 'https://ghb.ads.152media.com/v2/auction/', + 'navelix': 'https://ghb.hb.navelix.com/v2/auction/', + 'bidsxchange': 'https://ghb.hbd.bidsxchange.com/v2/auction/', + 'streamkey': 'https://ghb.hb.streamkey.net/v2/auction/', + 'janet': 'https://ghb.bidder.jmgads.com/v2/auction/', + 'pgam': 'https://ghb.pgamssp.com/v2/auction/', + 'ocm': 'https://ghb.cenarius.orangeclickmedia.com/v2/auction/', + 'vidcrunchllc': 'https://ghb.platform.vidcrunch.com/v2/auction/', + '9dotsmedia': 'https://ghb.platform.audiodots.com/v2/auction/', }; const DEFAULT_ADATPER_REQ = { bidderCode: 'adtelligent' }; diff --git a/test/spec/modules/aduptechBidAdapter_spec.js b/test/spec/modules/aduptechBidAdapter_spec.js index bbc4e554f7e..8dbdbbfeab5 100644 --- a/test/spec/modules/aduptechBidAdapter_spec.js +++ b/test/spec/modules/aduptechBidAdapter_spec.js @@ -182,6 +182,54 @@ describe('AduptechBidAdapter', () => { }); }); + describe('getFloor', () => { + let bidRequest; + + beforeEach(() => { + bidRequest = { + getFloor: sinon.stub() + }; + }); + + it('should handle empty or invalid bidRequest', () => { + expect(internal.getFloor(null)).to.be.null; + expect(internal.getFloor({})).to.be.null; + expect(internal.getFloor({ getFloor: 'foo' })).to.be.null; + }); + + it('should detect floor via getFloor()', () => { + const result = { + floor: 1.11, + currency: 'USD' + }; + + const options = { + mediaType: BANNER, + size: '*' + } + + bidRequest.getFloor.returns(result); + + expect(internal.getFloor(bidRequest, options)).to.deep.equal(result); + expect(bidRequest.getFloor.calledOnceWith(options)).to.be.true; + }); + + it('should handle empty, invalid or faulty getFloor() results', () => { + bidRequest.getFloor + .onCall(0).returns({}) + .onCall(1).returns({ floor: 'foo' }) + .onCall(2).returns('bar') + .onCall(3).throws(new Error('baz')); + + expect(internal.getFloor(bidRequest, {})).to.be.null; + expect(internal.getFloor(bidRequest, {})).to.be.null; + expect(internal.getFloor(bidRequest, {})).to.be.null; + expect(internal.getFloor(bidRequest, {})).to.be.null; + + expect(bidRequest.getFloor.callCount).to.equal(4); + }); + }); + describe('groupBidRequestsByPublisher', () => { it('should handle empty bidRequests', () => { expect(internal.groupBidRequestsByPublisher(null)).to.deep.equal({}); @@ -541,34 +589,35 @@ describe('AduptechBidAdapter', () => { } }; - const getFloorResponse = { - currency: 'USD', - floor: '1.23' - }; - - const validBidRequests = [ - { - bidId: 'bidId1', - adUnitCode: 'adUnitCode1', - transactionId: 'transactionId1', - mediaTypes: { - banner: { - sizes: [[100, 200], [300, 400]] - } - }, - params: { - publisher: 'publisher1', - placement: 'placement1' + const bidRequest = { + bidId: 'bidId1', + adUnitCode: 'adUnitCode1', + transactionId: 'transactionId1', + mediaTypes: { + banner: { + sizes: [[100, 200], [300, 400]] }, - getFloor: () => { - return getFloorResponse + native: { + image: { + required: true + }, } - } - ]; + }, + params: { + publisher: 'publisher1', + placement: 'placement1' + }, + getFloor: sinon.stub() + .onCall(0).returns({ floor: 1.11, currency: 'USD' }) + .onCall(1).returns({ floor: 2.22, currency: 'EUR' }) + .onCall(2).returns({ floor: 3.33, currency: 'USD' }) + .onCall(3).returns({ floor: 4.44, currency: 'GBP' }) + .onCall(4).returns({ floor: 5.55, currency: 'EUR' }) + }; - expect(spec.buildRequests(validBidRequests, bidderRequest)).to.deep.equal([ + expect(spec.buildRequests([bidRequest], bidderRequest)).to.deep.equal([ { - url: internal.buildEndpointUrl(validBidRequests[0].params.publisher), + url: internal.buildEndpointUrl(bidRequest.params.publisher), method: ENDPOINT_METHOD, data: { auctionId: bidderRequest.auctionId, @@ -580,17 +629,39 @@ describe('AduptechBidAdapter', () => { }, imp: [ { - bidId: validBidRequests[0].bidId, - transactionId: validBidRequests[0].transactionId, - adUnitCode: validBidRequests[0].adUnitCode, - params: validBidRequests[0].params, - banner: validBidRequests[0].mediaTypes.banner, - floorPrice: getFloorResponse.floor + bidId: bidRequest.bidId, + transactionId: bidRequest.transactionId, + adUnitCode: bidRequest.adUnitCode, + params: bidRequest.params, + banner: { + sizes: [ + [100, 200, 1.11, 'USD'], + [300, 400, 2.22, 'EUR'], + ], + floorPrice: 3.33, + floorCurrency: 'USD' + }, + native: { + image: { + required: true + }, + floorPrice: 4.44, + floorCurrency: 'GBP' + }, + floorPrice: 5.55, + floorCurrency: 'EUR' } ] } } ]); + + expect(bidRequest.getFloor.callCount).to.equal(5); + expect(bidRequest.getFloor.getCall(0).calledWith({ mediaType: BANNER, size: bidRequest.mediaTypes.banner.sizes[0] })).to.be.true; + expect(bidRequest.getFloor.getCall(1).calledWith({ mediaType: BANNER, size: bidRequest.mediaTypes.banner.sizes[1] })).to.be.true; + expect(bidRequest.getFloor.getCall(2).calledWith({ mediaType: BANNER, size: '*' })).to.be.true; + expect(bidRequest.getFloor.getCall(3).calledWith({ mediaType: NATIVE, size: '*' })).to.be.true; + expect(bidRequest.getFloor.getCall(4).calledWith({ mediaType: '*', size: '*' })).to.be.true; }); }); diff --git a/test/spec/modules/adyoulikeBidAdapter_spec.js b/test/spec/modules/adyoulikeBidAdapter_spec.js index e6a153d501a..c69d31d1bbd 100644 --- a/test/spec/modules/adyoulikeBidAdapter_spec.js +++ b/test/spec/modules/adyoulikeBidAdapter_spec.js @@ -672,10 +672,18 @@ describe('Adyoulike Adapter', function () { 'auctionId': '1d1a030790a475', 'bidderRequestId': '22edbae2733bf6', 'timeout': 3000, - 'userId': { - pubcid: '01EAJWWNEPN3CYMM5N8M5VXY22', - unsuported: '666' - } + 'userIdAsEids': + [ + { + 'source': 'pubcid.org', + 'uids': [ + { + 'atype': 1, + 'id': '01EAJWWNEPN3CYMM5N8M5VXY22' + } + ] + } + ] }; bidderRequest.bids = bidRequestWithSinglePlacement; @@ -684,13 +692,7 @@ describe('Adyoulike Adapter', function () { const payload = JSON.parse(request.data); expect(payload.userId).to.exist; - expect(payload.userId).to.deep.equal([{ - 'source': 'pubcid.org', - 'uids': [{ - 'atype': 1, - 'id': '01EAJWWNEPN3CYMM5N8M5VXY22' - }] - }]); + expect(payload.userId).to.deep.equal(bidderRequest.userIdAsEids); }); it('sends bid request to endpoint with single placement', function () { @@ -722,6 +724,7 @@ describe('Adyoulike Adapter', function () { expect(payload.Version).to.equal('1.0'); expect(payload.Bids['bid_id_0'].PlacementID).to.be.equal('placement_0'); expect(payload.PageRefreshed).to.equal(false); + expect(payload.pbjs_version).to.equal('$prebid.version$'); expect(payload.Bids['bid_id_0'].TransactionID).to.be.equal('bid_id_0_transaction_id'); }); @@ -736,6 +739,7 @@ describe('Adyoulike Adapter', function () { expect(payload.Version).to.equal('1.0'); expect(payload.Bids['bid_id_0'].PlacementID).to.be.equal('placement_0'); expect(payload.PageRefreshed).to.equal(false); + expect(payload.pbjs_version).to.equal('$prebid.version$'); expect(payload.Bids['bid_id_0'].TransactionID).to.be.equal('bid_id_0_transaction_id'); }); @@ -758,6 +762,7 @@ describe('Adyoulike Adapter', function () { expect(payload.Bids['bid_id_1'].TransactionID).to.be.equal('bid_id_1_transaction_id'); expect(payload.Bids['bid_id_3'].TransactionID).to.be.equal('bid_id_3_transaction_id'); expect(payload.PageRefreshed).to.equal(false); + expect(payload.pbjs_version).to.equal('$prebid.version$'); }); it('sends bid request to endpoint setted by parameters', function () { diff --git a/test/spec/modules/aidemBidAdapter_spec.js b/test/spec/modules/aidemBidAdapter_spec.js index 472b226ab8a..6a875feb2a9 100644 --- a/test/spec/modules/aidemBidAdapter_spec.js +++ b/test/spec/modules/aidemBidAdapter_spec.js @@ -13,7 +13,7 @@ const VALID_BIDS = [ params: { siteId: '301491', publisherId: '3021491', - placementId: 13144370, + placementId: '13144370', }, mediaTypes: { banner: { @@ -26,7 +26,7 @@ const VALID_BIDS = [ params: { siteId: '301491', publisherId: '3021491', - placementId: 13144370, + placementId: '13144370', }, mediaTypes: { video: { @@ -110,7 +110,7 @@ const INVALID_BIDS = [ }, params: { siteId: '301491', - placementId: 13144370, + placementId: '13144370', }, }, { @@ -126,7 +126,7 @@ const INVALID_BIDS = [ }, params: { siteId: '301491', - placementId: 13144370, + placementId: '13144370', }, }, { @@ -143,7 +143,7 @@ const INVALID_BIDS = [ }, params: { siteId: '301491', - placementId: 13144370, + placementId: '13144370', video: { size: [480, 40] } @@ -167,8 +167,8 @@ const DEFAULT_VALID_BANNER_REQUESTS = [ } }, params: { - siteId: 1, - placementId: 13144370 + siteId: '1', + placementId: '13144370' }, src: 'client', transactionId: '54a58774-7a41-494e-9aaf-fa7b79164f0c' @@ -192,8 +192,8 @@ const DEFAULT_VALID_VIDEO_REQUESTS = [ } }, params: { - siteId: 1, - placementId: 13144370 + siteId: '1', + placementId: '13144370' }, src: 'client', transactionId: '54a58774-7a41-494e-9aaf-fa7b79164f0c' @@ -276,7 +276,7 @@ const SERVER_RESPONSE_VIDEO = { }, } -const WIN_NOTICE = { +const WIN_NOTICE_WEB = { 'adId': '3a20ee5dc78c1e', 'adUnitCode': 'div-gpt-ad-1460505748561-0', 'creativeId': '24277955', @@ -289,14 +289,87 @@ const WIN_NOTICE = { 'hb_size': '300x250', 'hb_source': 'client', 'hb_format': 'banner', - 'hb_adomain': 'tenutabene.it' + 'hb_adomain': 'example.com' }, + 'auctionId': '85864730-6cbc-4e56-bc3c-a4a6596dca5b', 'currency': [ 'USD' ], 'mediaType': 'banner', + 'meta': { + 'advertiserDomains': [ + 'cloudflare.com' + ], + 'ext': {} + }, + 'size': '300x250', + 'params': [ + { + 'placementId': '13144370', + 'siteId': '23434', + 'publisherId': '7689670753' + } + ], + 'width': 300, + 'height': 250, + 'status': 'rendered', + 'transactionId': 'ce089116-4251-45c3-bdbb-3a03cb13816b', + 'ttl': 300, + 'requestTimestamp': 1666796241007, + 'responseTimestamp': 1666796241021, + metrics: { + getMetrics() { + return { + + } + } + } +} + +const WIN_NOTICE_APP = { + 'adId': '3a20ee5dc78c1e', + 'adUnitCode': 'div-gpt-ad-1460505748561-0', + 'creativeId': '24277955', + 'cpm': 1, + 'netRevenue': false, + 'adserverTargeting': { + 'hb_bidder': 'aidem', + 'hb_adid': '3a20ee5dc78c1e', + 'hb_pb': '1.00', + 'hb_size': '300x250', + 'hb_source': 'client', + 'hb_format': 'banner', + 'hb_adomain': 'example.com' + }, + + 'auctionId': '85864730-6cbc-4e56-bc3c-a4a6596dca5b', + 'currency': [ + 'USD' + ], + 'mediaType': 'banner', + 'meta': { + 'advertiserDomains': [ + 'cloudflare.com' + ], + 'ext': { + 'app': { + 'app_bundle': '{{APP_BUNDLE}}', + 'app_id': '{{APP_ID}}', + 'app_name': '{{APP_NAME}}', + 'app_store_url': '{{APP_STORE_URL}}', + 'inventory_source': '{{INVENTORY_SOURCE}}' + } + } + }, 'size': '300x250', + 'params': [ + { + 'placementId': '13144370', + 'siteId': '23434', + 'publisherId': '7689670753' + } + ], 'width': 300, 'height': 250, 'status': 'rendered', @@ -365,6 +438,62 @@ describe('Aidem adapter', () => { deepSetValue(validVideoRequest.params, 'video.size', [640, 480]) expect(spec.isBidRequestValid(validVideoRequest)).to.be.true }); + + it('BANNER: should return true if rateLimit is 1', function () { + // spec.isBidRequestValid() + const validBannerRequest = utils.deepClone(VALID_BIDS[0]) + deepSetValue(validBannerRequest.params, 'rateLimit', 1) + expect(spec.isBidRequestValid(validBannerRequest)).to.be.true + }); + + it('BANNER: should return false if rateLimit is 0', function () { + // spec.isBidRequestValid() + const validBannerRequest = utils.deepClone(VALID_BIDS[0]) + deepSetValue(validBannerRequest.params, 'rateLimit', 0) + expect(spec.isBidRequestValid(validBannerRequest)).to.be.false + }); + + it('BANNER: should return false if rateLimit is not between 0 and 1', function () { + // spec.isBidRequestValid() + const validBannerRequest = utils.deepClone(VALID_BIDS[0]) + deepSetValue(validBannerRequest.params, 'rateLimit', 1.2) + expect(spec.isBidRequestValid(validBannerRequest)).to.be.false + }); + + it('BANNER: should return false if rateLimit is not a number', function () { + // spec.isBidRequestValid() + const validBannerRequest = utils.deepClone(VALID_BIDS[0]) + deepSetValue(validBannerRequest.params, 'rateLimit', '0.5') + expect(spec.isBidRequestValid(validBannerRequest)).to.be.false + }); + + it('VIDEO: should return true if rateLimit is 1', function () { + // spec.isBidRequestValid() + const validVideoRequest = utils.deepClone(VALID_BIDS[1]) + deepSetValue(validVideoRequest.params, 'rateLimit', 1) + expect(spec.isBidRequestValid(validVideoRequest)).to.be.true + }); + + it('VIDEO: should return false if rateLimit is 0', function () { + // spec.isBidRequestValid() + const validVideoRequest = utils.deepClone(VALID_BIDS[1]) + deepSetValue(validVideoRequest.params, 'rateLimit', 0) + expect(spec.isBidRequestValid(validVideoRequest)).to.be.false + }); + + it('VIDEO: should return false if rateLimit is not between 0 and 1', function () { + // spec.isBidRequestValid() + const validBannerRequest = utils.deepClone(VALID_BIDS[1]) + deepSetValue(validBannerRequest.params, 'rateLimit', 1.2) + expect(spec.isBidRequestValid(validBannerRequest)).to.be.false + }); + + it('VIDEO: should return false if rateLimit is not a number', function () { + // spec.isBidRequestValid() + const validBannerRequest = utils.deepClone(VALID_BIDS[1]) + deepSetValue(validBannerRequest.params, 'rateLimit', '0.5') + expect(spec.isBidRequestValid(validBannerRequest)).to.be.false + }); }); describe('buildRequests', () => { @@ -385,7 +514,7 @@ describe('Aidem adapter', () => { expect(payload.imp).to.be.a('array').that.has.lengthOf(DEFAULT_VALID_BANNER_REQUESTS.length) expect(payload.imp[0]).to.be.a('object').that.has.all.keys( - 'banner', 'id', 'mediatype', 'imp_ext', 'tid' + 'banner', 'id', 'mediatype', 'imp_ext', 'tid', 'tagid' ) expect(payload.imp[0].banner).to.be.a('object').that.has.all.keys( 'format', 'topframe' @@ -401,7 +530,7 @@ describe('Aidem adapter', () => { expect(payload.imp).to.be.a('array').that.has.lengthOf(DEFAULT_VALID_VIDEO_REQUESTS.length) expect(payload.imp[0]).to.be.a('object').that.has.all.keys( - 'video', 'id', 'mediatype', 'imp_ext', 'tid' + 'video', 'id', 'mediatype', 'imp_ext', 'tid', 'tagid' ) expect(payload.imp[0].video).to.be.a('object').that.has.all.keys( 'format', 'mimes', 'minDuration', 'maxDuration', 'protocols' @@ -421,6 +550,14 @@ describe('Aidem adapter', () => { 'value', 'currency' ) }); + + it('should hav wpar keys in environment object', function () { + const requests = spec.buildRequests(DEFAULT_VALID_VIDEO_REQUESTS, VALID_BIDDER_REQUEST); + const payload = JSON.parse(requests.data) + expect(payload).to.have.property('environment') + expect(payload.environment).to.be.a('object').that.have.property('wpar') + expect(payload.environment.wpar).to.be.a('object').that.has.keys('innerWidth', 'innerHeight') + }); }) describe('interpretResponse', () => { @@ -474,11 +611,13 @@ describe('Aidem adapter', () => { expect(spec.onBidWon).to.exist.and.to.be.a('function') }); - it(`should send a valid bid won notice`, function () { - spec.onBidWon(WIN_NOTICE); - // server.respondWith('POST', WIN_EVENT_URL, [ - // 400, {'Content-Type': 'application/json'}, ) - // ]); + it(`should send a valid bid won notice from web environment`, function () { + spec.onBidWon(WIN_NOTICE_WEB); + expect(server.requests.length).to.equal(1); + }); + + it(`should send a valid bid won notice from app environment`, function () { + spec.onBidWon(WIN_NOTICE_APP); expect(server.requests.length).to.equal(1); }); }); diff --git a/test/spec/modules/airgridRtdProvider_spec.js b/test/spec/modules/airgridRtdProvider_spec.js index 5b1df6f9d4c..c587ef1a133 100644 --- a/test/spec/modules/airgridRtdProvider_spec.js +++ b/test/spec/modules/airgridRtdProvider_spec.js @@ -110,12 +110,18 @@ describe('airgrid RTD Submodule', function () { .withArgs(agRTD.AG_AUDIENCE_IDS_KEY) .returns(JSON.stringify(MATCHED_AUDIENCES)); const audiences = agRTD.getMatchedAudiencesFromStorage(); - const bidderOrtb2 = agRTD.getAudiencesAsBidderOrtb2(RTD_CONFIG.dataProviders[0], audiences); - const bidders = RTD_CONFIG.dataProviders[0].params.bidders; + agRTD.setAudiencesAsBidderOrtb2(RTD_CONFIG.dataProviders[0], audiences); + + const bidderConfig = config.getBidderConfig() + const bidders = RTD_CONFIG.dataProviders[0].params.bidders + const bidderOrtb2 = bidderConfig + Object.keys(bidderOrtb2).forEach((bidder) => { if (bidders.indexOf(bidder) === -1) return; - expect(deepAccess(bidderOrtb2[bidder], 'ortb2.user.ext.data.airgrid')).to.eql(MATCHED_AUDIENCES); + MATCHED_AUDIENCES.forEach((audience) => { + expect(deepAccess(bidderOrtb2[bidder], 'ortb2.site.keywords')).to.contain(audience); + }) }); }); diff --git a/test/spec/modules/ajaBidAdapter_spec.js b/test/spec/modules/ajaBidAdapter_spec.js index a2095d52857..7cf5698f7d4 100644 --- a/test/spec/modules/ajaBidAdapter_spec.js +++ b/test/spec/modules/ajaBidAdapter_spec.js @@ -45,6 +45,26 @@ describe('AjaAdapter', function () { bidId: '30b31c1838de1e', bidderRequestId: '22edbae2733bf6', auctionId: '1d1a030790a475', + ortb2: { + device: { + sua: { + source: 2, + platform: { + brand: 'Android', + version: ['8', '0', '0'] + }, + browsers: [ + {brand: 'Not_A Brand', version: ['99', '0', '0', '0']}, + {brand: 'Google Chrome', version: ['109', '0', '5414', '119']}, + {brand: 'Chromium', version: ['109', '0', '5414', '119']} + ], + mobile: 1, + model: 'SM-G955U', + bitness: '64', + architecture: '' + } + } + } } ]; @@ -58,7 +78,7 @@ describe('AjaAdapter', function () { const requests = spec.buildRequests(bidRequests, bidderRequest); expect(requests[0].url).to.equal(ENDPOINT); expect(requests[0].method).to.equal('GET'); - expect(requests[0].data).to.equal('asi=123456&skt=5&prebid_id=30b31c1838de1e&prebid_ver=$prebid.version$&page_url=https%3A%2F%2Fhoge.com&'); + expect(requests[0].data).to.equal('asi=123456&skt=5&prebid_id=30b31c1838de1e&prebid_ver=$prebid.version$&page_url=https%3A%2F%2Fhoge.com&sua=%7B%22source%22%3A2%2C%22platform%22%3A%7B%22brand%22%3A%22Android%22%2C%22version%22%3A%5B%228%22%2C%220%22%2C%220%22%5D%7D%2C%22browsers%22%3A%5B%7B%22brand%22%3A%22Not_A%20Brand%22%2C%22version%22%3A%5B%2299%22%2C%220%22%2C%220%22%2C%220%22%5D%7D%2C%7B%22brand%22%3A%22Google%20Chrome%22%2C%22version%22%3A%5B%22109%22%2C%220%22%2C%225414%22%2C%22119%22%5D%7D%2C%7B%22brand%22%3A%22Chromium%22%2C%22version%22%3A%5B%22109%22%2C%220%22%2C%225414%22%2C%22119%22%5D%7D%5D%2C%22mobile%22%3A1%2C%22model%22%3A%22SM-G955U%22%2C%22bitness%22%3A%2264%22%2C%22architecture%22%3A%22%22%7D&'); }); }); diff --git a/test/spec/modules/alkimiBidAdapter_spec.js b/test/spec/modules/alkimiBidAdapter_spec.js index 1ae9bb56df4..a396e5b8139 100644 --- a/test/spec/modules/alkimiBidAdapter_spec.js +++ b/test/spec/modules/alkimiBidAdapter_spec.js @@ -108,7 +108,7 @@ describe('alkimiBidAdapter', function () { describe('buildRequests', function () { let bidRequests = [REQUEST] - const bidderRequest = spec.buildRequests(bidRequests, { + let requestData = { auctionId: '123', refererInfo: { page: 'http://test.com/path.html' @@ -119,7 +119,8 @@ describe('alkimiBidAdapter', function () { gdprApplies: true }, uspConsent: 'uspConsent' - }) + } + const bidderRequest = spec.buildRequests(bidRequests, requestData) it('should return a properly formatted request with eids defined', function () { expect(bidderRequest.data.eids).to.deep.equal(REQUEST.userIdAsEids) @@ -138,8 +139,8 @@ describe('alkimiBidAdapter', function () { expect(bidderRequest.method).to.equal('POST') expect(bidderRequest.data.requestId).to.equal('123') expect(bidderRequest.data.referer).to.equal('http://test.com/path.html') - expect(bidderRequest.data.schain).to.deep.contains({ver: '1.0', complete: 1, nodes: [{asi: 'alkimi-onboarding.com', sid: '00001', hp: 1}]}) - expect(bidderRequest.data.signRequest.bids).to.deep.contains({ token: 'e64782a4-8e68-4c38-965b-80ccf115d46f', pos: 7, bidFloor: 0.1, width: 300, height: 250, impMediaType: 'Banner', adUnitCode: 'bannerAdUnitCode' }) + expect(bidderRequest.data.schain).to.deep.contains({ ver: '1.0', complete: 1, nodes: [{ asi: 'alkimi-onboarding.com', sid: '00001', hp: 1 }] }) + expect(bidderRequest.data.signRequest.bids).to.deep.contains({ token: 'e64782a4-8e68-4c38-965b-80ccf115d46f', pos: 7, bidFloor: 0.1, sizes: [{width: 300, height: 250}], playerSizes: [], impMediaTypes: ['Banner'], adUnitCode: 'bannerAdUnitCode' }) expect(bidderRequest.data.signRequest.randomUUID).to.equal(undefined) expect(bidderRequest.data.bidIds).to.deep.contains('456') expect(bidderRequest.data.signature).to.equal(undefined) @@ -147,6 +148,17 @@ describe('alkimiBidAdapter', function () { expect(bidderRequest.options.contentType).to.equal('application/json') expect(bidderRequest.url).to.equal(ENDPOINT) }) + + it('sends bidFloor when configured', () => { + const requestWithFloor = Object.assign({}, REQUEST); + requestWithFloor.getFloor = function (arg) { + if (arg.currency === 'USD' && arg.mediaType === 'banner' && JSON.stringify(arg.size) === JSON.stringify([300, 250])) { + return { currency: 'USD', floor: 0.3 } + } + } + const bidderRequestFloor = spec.buildRequests([requestWithFloor], requestData); + expect(bidderRequestFloor.data.signRequest.bids[0].bidFloor).to.be.equal(0.3); + }); }) describe('interpretResponse', function () { diff --git a/test/spec/modules/amxBidAdapter_spec.js b/test/spec/modules/amxBidAdapter_spec.js index d71e9ab5cba..6f69e57d8bc 100644 --- a/test/spec/modules/amxBidAdapter_spec.js +++ b/test/spec/modules/amxBidAdapter_spec.js @@ -2,6 +2,7 @@ import { expect } from 'chai'; import { spec } from 'modules/amxBidAdapter.js'; import { createEidsArray } from 'modules/userId/eids.js'; import { BANNER, VIDEO } from 'src/mediaTypes.js'; +import { config } from 'src/config.js'; import * as utils from 'src/utils.js'; const sampleRequestId = '82c91e127a9b93e'; @@ -34,9 +35,17 @@ const sampleBidderRequest = { consentString: utils.getUniqueIdentifierStr(), vendorData: {}, }, + gppConsent: { + gppString: 'example', + applicableSections: 'example' + }, auctionId: utils.getUniqueIdentifierStr(), uspConsent: '1YYY', refererInfo: { + reachedTop: true, + numIframes: 10, + stack: ['https://www.prebid.org'], + canonicalUrl: 'https://prebid.org', location: 'https://www.prebid.org', site: 'prebid.org', topmostLocation: 'https://www.prebid.org', @@ -195,9 +204,12 @@ describe('AmxBidAdapter', () => { }); }); describe('getUserSync', () => { - it('will only sync from valid server responses', () => { + it('Will perform an iframe sync even if there is no server response..', () => { const syncs = spec.getUserSyncs({ iframeEnabled: true }); - expect(syncs).to.eql([]); + expect(syncs).to.eql([{ + type: 'iframe', + url: 'https://prebid.a-mo.net/isyn?gdpr_consent=&gdpr=0&us_privacy=&gpp=&gpp_sid=' + }]); }); it('will return valid syncs from a server response', () => { @@ -260,6 +272,15 @@ describe('AmxBidAdapter', () => { expect(data.tm).to.equal(true); }); + it('will attach additional referrer info data', () => { + const { data } = spec.buildRequests([sampleBidRequestBase], sampleBidderRequest); + expect(data.ri.r).to.equal(sampleBidderRequest.refererInfo.topmostLocation); + expect(data.ri.t).to.equal(sampleBidderRequest.refererInfo.reachedTop); + expect(data.ri.l).to.equal(sampleBidderRequest.refererInfo.numIframes); + expect(data.ri.s).to.equal(sampleBidderRequest.refererInfo.stack); + expect(data.ri.c).to.equal(sampleBidderRequest.refererInfo.canonicalUrl); + }); + it('if prebid is in an iframe, will use the frame url as domain, if the topmost is not avialable', () => { const { data } = spec.buildRequests([sampleBidRequestBase], { ...sampleBidderRequest, @@ -286,7 +307,7 @@ describe('AmxBidAdapter', () => { expect(data.re).to.equal('http://search-traffic-source.com'); }); - it('handles referer data and GDPR, USP Consent, COPPA', () => { + it('handles GDPR, USP Consent, COPPA, and GPP', () => { const { data } = spec.buildRequests( [sampleBidRequestBase], sampleBidderRequest @@ -295,6 +316,7 @@ describe('AmxBidAdapter', () => { expect(data.gs).to.equal(sampleBidderRequest.gdprConsent.gdprApplies); expect(data.gc).to.equal(sampleBidderRequest.gdprConsent.consentString); expect(data.usp).to.equal(sampleBidderRequest.uspConsent); + expect(data.gpp).to.equal(sampleBidderRequest.gppConsent); expect(data.cpp).to.equal(0); }); @@ -316,6 +338,73 @@ describe('AmxBidAdapter', () => { expect(data.bwc).to.equal(bidderWinsCount); expect(data.trc).to.equal(0); }); + + it('will attach sync configuration', () => { + const request = () => spec.buildRequests( + [sampleBidRequestBase], + sampleBidderRequest + ); + + const setConfig = (filterSettings) => + config.setConfig({ + userSync: { + syncsPerBidder: 2, + syncDelay: 2300, + syncEnabled: true, + filterSettings, + } + }); + + const test = (filterSettings) => { + setConfig(filterSettings); + return request().data.sync; + } + + const base = { d: 2300, l: 2, e: true }; + + const tests = [[ + undefined, + { ...base, t: 0 } + ], [{ + image: { + bidders: '*', + filter: 'include' + }, + iframe: { + bidders: '*', + filter: 'include' + } + }, { ...base, t: 3 }], [{ + image: { + bidders: ['amx'], + }, + iframe: { + bidders: '*', + filter: 'include' + } + }, { ...base, t: 3 }], [{ + image: { + bidders: ['other'], + }, + iframe: { + bidders: '*' + } + }, { ...base, t: 2 }], [{ + image: { + bidders: ['amx'] + }, + iframe: { + bidders: ['amx'], + filter: 'exclude' + } + }, { ...base, t: 1 }]] + + for (let i = 0, l = tests.length; i < l; i++) { + const [result, expected] = tests[i]; + expect(test(result), `input: ${JSON.stringify(result)}`).to.deep.equal(expected); + } + }); + it('will forward first-party data', () => { const { data } = spec.buildRequests( [sampleBidRequestBase], @@ -509,7 +598,14 @@ describe('AmxBidAdapter', () => { before(() => { _Image = window.Image; window.Image = class FakeImage { + _src = ''; + + get src() { + return this._src; + } + set src(value) { + this._src = value; firedPixels.push(value); } }; diff --git a/test/spec/modules/amxIdSystem_spec.js b/test/spec/modules/amxIdSystem_spec.js index dea79e87baa..c1ae2c791d5 100644 --- a/test/spec/modules/amxIdSystem_spec.js +++ b/test/spec/modules/amxIdSystem_spec.js @@ -1,4 +1,4 @@ -import { amxIdSubmodule } from 'modules/amxIdSystem.js'; +import { amxIdSubmodule, storage } from 'modules/amxIdSystem.js'; import { server } from 'test/mocks/xhr.js'; import * as utils from 'src/utils.js'; @@ -48,38 +48,17 @@ describe('validateConfig', () => { logErrorSpy.restore(); }); - it('should return undefined if config.storage is not present', () => { + it('should allow configuration with no storage', () => { expect( amxIdSubmodule.getId( { ...config, - storage: null, + storage: undefined }, null, null ) - ).to.equal(undefined); - - expect(logErrorSpy.calledOnce).to.be.true; - expect(logErrorSpy.lastCall.lastArg).to.contain('storage is required'); - }); - - it('should return undefined if config.storage.type !== "html5"', () => { - expect( - amxIdSubmodule.getId( - { - ...config, - storage: { - type: 'cookie', - }, - }, - null, - null - ) - ).to.equal(undefined); - - expect(logErrorSpy.calledOnce).to.be.true; - expect(logErrorSpy.lastCall.lastArg).to.contain('cookie'); + ).to.not.equal(undefined); }); it('should return undefined if expires > 30', () => { @@ -111,10 +90,18 @@ describe('getId', () => { }); it('should call the sync endpoint and accept a valid response', () => { + storage.setDataInLocalStorage('__amuidpb', TEST_ID); + const { callback } = amxIdSubmodule.getId(config, null, null); callback(spy); const [request] = server.requests; + expect(request.withCredentials).to.be.true + expect(request.requestHeaders['Content-Type']).to.match(/text\/plain/) + + const { search } = utils.parseUrl(request.url); + expect(search.av).to.equal(amxIdSubmodule.version); + expect(search.am).to.equal(TEST_ID); expect(request.method).to.equal('GET'); request.respond( @@ -187,7 +174,7 @@ describe('getId', () => { ); const [, secondRequest] = server.requests; - expect(secondRequest.url).to.be.equal(intermediateValue); + expect(secondRequest.url).to.match(new RegExp(`^${intermediateValue}\?`)); secondRequest.respond( 200, {}, diff --git a/test/spec/modules/aolBidAdapter_spec.js b/test/spec/modules/aolBidAdapter_spec.js index 57ce37145f4..92246f76c7a 100644 --- a/test/spec/modules/aolBidAdapter_spec.js +++ b/test/spec/modules/aolBidAdapter_spec.js @@ -84,7 +84,7 @@ describe('AolAdapter', function () { 'admixer.net': '100', 'adserver.org': '200', 'adtelligent.com': '300', - 'amxrtb.com': '500', + 'amxdt.net': '500', 'audigent.com': '600', 'britepool.com': '700', 'criteo.com': '800', @@ -110,7 +110,7 @@ describe('AolAdapter', function () { const USER_ID_DATA = { admixerId: SUPPORTED_USER_ID_SOURCES['admixer.net'], adtelligentId: SUPPORTED_USER_ID_SOURCES['adtelligent.com'], - amxId: SUPPORTED_USER_ID_SOURCES['amxrtb.com'], + amxId: SUPPORTED_USER_ID_SOURCES['amxdt.net'], britepoolid: SUPPORTED_USER_ID_SOURCES['britepool.com'], criteoId: SUPPORTED_USER_ID_SOURCES['criteo.com'], connectid: SUPPORTED_USER_ID_SOURCES['verizonmedia.com'], @@ -488,6 +488,20 @@ describe('AolAdapter', function () { expect(request.url).to.contain(NEXAGE_URL + 'dcn=2c9d2b50015c5ce9db6aeeed8b9500d6&pos=header'); }); + it('should return One Mobile url with configured GPP data', function () { + let bidRequest = createCustomBidRequest({ + params: getNexageGetBidParams() + }); + bidRequest.ortb2 = { + regs: { + gpp: 'testgpp', + gpp_sid: [8] + } + } + let [request] = spec.buildRequests(bidRequest.bids, bidRequest); + expect(request.url).to.contain('gpp=testgpp&gpp_sid=8'); + }); + it('should return One Mobile url with cmd=bid option', function () { let bidRequest = createCustomBidRequest({ params: getNexageGetBidParams() @@ -794,6 +808,14 @@ describe('AolAdapter', function () { 'euconsent=test-consent;gdpr=1;us_privacy=test-usp-consent;'); }); + it('should return formatted gpp privacy params when formatConsentData returns GPP data', function () { + formatConsentDataStub.returns({ + gpp: 'gppstring', + gpp_sid: [6, 7] + }); + expect(spec.formatMarketplaceDynamicParams()).to.be.equal('gpp=gppstring;gpp_sid=6%2C7;'); + }); + it('should return formatted params when formatKeyValues returns data', function () { formatKeyValuesStub.returns({ param1: 'val1', diff --git a/test/spec/modules/appnexusBidAdapter_spec.js b/test/spec/modules/appnexusBidAdapter_spec.js index 5cd189da9a1..9e88e2875c7 100644 --- a/test/spec/modules/appnexusBidAdapter_spec.js +++ b/test/spec/modules/appnexusBidAdapter_spec.js @@ -36,14 +36,31 @@ describe('AppNexusAdapter', function () { }); it('should return true when required params found', function () { - let bid = Object.assign({}, bid); - delete bid.params; - bid.params = { + let bid1 = deepClone(bid); + bid1.params = { + 'placement_id': 123423 + } + expect(spec.isBidRequestValid(bid1)).to.equal(true); + }); + + it('should return true when required params found', function () { + let bid1 = deepClone(bid); + bid1.params = { 'member': '1234', 'invCode': 'ABCD' }; - expect(spec.isBidRequestValid(bid)).to.equal(true); + expect(spec.isBidRequestValid(bid1)).to.equal(true); + }); + + it('should return true when required params found', function () { + let bid1 = deepClone(bid); + bid1.params = { + 'member': '1234', + 'inv_code': 'ABCD' + }; + + expect(spec.isBidRequestValid(bid1)).to.equal(true); }); it('should return false when required params are not passed', function () { @@ -54,6 +71,15 @@ describe('AppNexusAdapter', function () { }; expect(spec.isBidRequestValid(bid)).to.equal(false); }); + + it('should return false when required params are not passed', function () { + let bid = Object.assign({}, bid); + delete bid.params; + bid.params = { + 'placement_id': 0 + }; + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); }); describe('buildRequests', function () { @@ -101,6 +127,24 @@ describe('AppNexusAdapter', function () { expect(payload.tags[0].private_sizes).to.deep.equal([{ width: 300, height: 250 }]); }); + it('should parse out private sizes', function () { + let bidRequest = Object.assign({}, + bidRequests[0], + { + params: { + placementId: '10433394', + private_sizes: [300, 250] + } + } + ); + + const request = spec.buildRequests([bidRequest]); + const payload = JSON.parse(request.data); + + expect(payload.tags[0].private_sizes).to.exist; + expect(payload.tags[0].private_sizes).to.deep.equal([{ width: 300, height: 250 }]); + }); + it('should add position in request', function () { // set from bid.params let bidRequest = deepClone(bidRequests[0]); @@ -168,6 +212,24 @@ describe('AppNexusAdapter', function () { expect(payload.publisher_id).to.deep.equal(1231234); }); + it('should add publisher_id in request', function () { + let bidRequest = Object.assign({}, + bidRequests[0], + { + params: { + placement_id: '10433394', + publisher_id: '1231234' + } + }); + const request = spec.buildRequests([bidRequest]); + const payload = JSON.parse(request.data); + + expect(payload.tags[0].publisher_id).to.exist; + expect(payload.tags[0].publisher_id).to.deep.equal(1231234); + expect(payload.publisher_id).to.exist; + expect(payload.publisher_id).to.deep.equal(1231234); + }); + it('should add source and verison to the tag', function () { const request = spec.buildRequests(bidRequests); const payload = JSON.parse(request.data); @@ -189,7 +251,7 @@ describe('AppNexusAdapter', function () { bids: [{ bidder: 'appnexus', params: { - placementId: '10433394' + placement_id: '10433394' } }], transactionId: '04f2659e-c005-4eb1-a57c-fa93145e3843' @@ -351,7 +413,7 @@ describe('AppNexusAdapter', function () { bidRequests[0], { params: { - placementId: '10433394', + placement_id: '10433394', user: { externalUid: '123', segments: [123, { id: 987, value: 876 }], @@ -407,7 +469,7 @@ describe('AppNexusAdapter', function () { // 2 -> reserve is defined, getFloor not defined > reserve is used bidRequest.params = { - 'placementId': '10433394', + 'placement_id': '10433394', 'reserve': 0.5 }; request = spec.buildRequests([bidRequest]); @@ -428,7 +490,7 @@ describe('AppNexusAdapter', function () { let bidRequest = Object.assign({}, bidRequests[0], { - params: { placementId: '14542875' } + params: { placement_id: '14542875' } }, { mediaTypes: { @@ -461,7 +523,7 @@ describe('AppNexusAdapter', function () { let bidRequest = Object.assign({}, bidRequests[0], { - params: { placementId: '14542875' } + params: { placement_id: '14542875' } }, { mediaTypes: { @@ -484,7 +546,7 @@ describe('AppNexusAdapter', function () { let bidRequest = Object.assign({}, bidRequests[0], { - params: { placementId: '14542875' } + params: { placement_id: '14542875' } }, { mediaTypes: { @@ -526,7 +588,7 @@ describe('AppNexusAdapter', function () { let bidRequest = Object.assign({}, bidRequests[0], { - params: { placementId: '14542875' } + params: { placement_id: '14542875' } }, { mediaTypes: { @@ -557,7 +619,7 @@ describe('AppNexusAdapter', function () { let bidRequest = Object.assign({}, bidRequests[0], { - params: { placementId: '14542875' } + params: { placement_id: '14542875' } }, { mediaTypes: { @@ -585,7 +647,7 @@ describe('AppNexusAdapter', function () { let bidRequest = Object.assign({}, bidRequests[0], { - params: { placementId: '14542875' } + params: { placement_id: '14542875' } }, { mediaTypes: { @@ -610,7 +672,7 @@ describe('AppNexusAdapter', function () { mediaType: 'banner', params: { sizes: [[300, 250], [300, 600]], - placementId: 13144370 + placement_id: 13144370 } } ); @@ -786,7 +848,7 @@ describe('AppNexusAdapter', function () { bidRequests[0], { params: { - placementId: '10433394', + placement_id: '10433394', keywords: { single: 'val', singleArr: ['val'], @@ -931,6 +993,23 @@ describe('AppNexusAdapter', function () { expect(payload.tags[0].use_pmt_rule).to.equal(true); }); + it('should add payment rules to the request', function () { + let bidRequest = Object.assign({}, + bidRequests[0], + { + params: { + placement_id: '10433394', + use_payment_rule: true + } + } + ); + + const request = spec.buildRequests([bidRequest]); + const payload = JSON.parse(request.data); + + expect(payload.tags[0].use_pmt_rule).to.equal(true); + }); + it('should add gpid to the request', function () { let testGpid = '/12345/my-gpt-tag-0'; let bidRequest = deepClone(bidRequests[0]); @@ -985,6 +1064,52 @@ describe('AppNexusAdapter', function () { expect(payload.us_privacy).to.exist.and.to.equal(consentString); }); + it('should add gpp information to the request via bidderRequest.gppConsent', function () { + let consentString = 'abc1234'; + let bidderRequest = { + 'bidderCode': 'appnexus', + 'auctionId': '1d1a030790a475', + 'bidderRequestId': '22edbae2733bf6', + 'timeout': 3000, + 'gppConsent': { + 'gppString': consentString, + 'applicableSections': [8] + } + }; + bidderRequest.bids = bidRequests; + + const request = spec.buildRequests(bidRequests, bidderRequest); + const payload = JSON.parse(request.data); + + expect(payload.privacy).to.exist; + expect(payload.privacy.gpp).to.equal(consentString); + expect(payload.privacy.gpp_sid).to.deep.equal([8]); + }); + + it('should add gpp information to the request via bidderRequest.ortb2.regs', function () { + let consentString = 'abc1234'; + let bidderRequest = { + 'bidderCode': 'appnexus', + 'auctionId': '1d1a030790a475', + 'bidderRequestId': '22edbae2733bf6', + 'timeout': 3000, + 'ortb2': { + 'regs': { + 'gpp': consentString, + 'gpp_sid': [7] + } + } + }; + bidderRequest.bids = bidRequests; + + const request = spec.buildRequests(bidRequests, bidderRequest); + const payload = JSON.parse(request.data); + + expect(payload.privacy).to.exist; + expect(payload.privacy.gpp).to.equal(consentString); + expect(payload.privacy.gpp_sid).to.deep.equal([7]); + }); + it('supports sending hybrid mobile app parameters', function () { let appRequest = Object.assign({}, bidRequests[0], @@ -1193,9 +1318,33 @@ describe('AppNexusAdapter', function () { source: 'puburl2.com', uids: [{ id: 'pubid2' + }, { + id: 'pubid2-123' }] }] - } + }, + userIdAsEids: [{ + source: 'adserver.org', + uids: [{ id: 'sample-userid' }] + }, { + source: 'criteo.com', + uids: [{ id: 'sample-criteo-userid' }] + }, { + source: 'netid.de', + uids: [{ id: 'sample-netId-userid' }] + }, { + source: 'liveramp.com', + uids: [{ id: 'sample-idl-userid' }] + }, { + source: 'uidapi.com', + uids: [{ id: 'sample-uid2-value' }] + }, { + source: 'puburl.com', + uids: [{ id: 'pubid1' }] + }, { + source: 'puburl2.com', + uids: [{ id: 'pubid2' }, { id: 'pubid2-123' }] + }] }); const request = spec.buildRequests([bidRequest]); @@ -1236,6 +1385,10 @@ describe('AppNexusAdapter', function () { source: 'puburl2.com', id: 'pubid2' }); + expect(payload.eids).to.deep.include({ + source: 'puburl2.com', + id: 'pubid2-123' + }); }); it('should populate iab_support object at the root level if omid support is detected', function () { @@ -1584,7 +1737,10 @@ describe('AppNexusAdapter', function () { 'phone': '1234567890', 'address': '28 W 23rd St, New York, NY 10010', 'privacy_link': 'https://appnexus.com/?url=privacy_url', - 'javascriptTrackers': '' + 'javascriptTrackers': '', + 'video': { + 'content': '' + } }; let bidderRequest = { bids: [{ @@ -1598,6 +1754,7 @@ describe('AppNexusAdapter', function () { expect(result[0].native.body).to.equal('Cool description great stuff'); expect(result[0].native.cta).to.equal('Do it'); expect(result[0].native.image.url).to.equal('https://cdn.adnxs.com/img.png'); + expect(result[0].native.video.content).to.equal(''); }); } @@ -1685,7 +1842,7 @@ describe('AppNexusAdapter', function () { it('should add advertiserDomains', function () { let responseAdvertiserId = deepClone(response); - responseAdvertiserId.tags[0].ads[0].adomain = ['123']; + responseAdvertiserId.tags[0].ads[0].adomain = '123'; let bidderRequest = { bids: [{ @@ -1695,7 +1852,7 @@ describe('AppNexusAdapter', function () { } let result = spec.interpretResponse({ body: responseAdvertiserId }, { bidderRequest }); expect(Object.keys(result[0].meta)).to.include.members(['advertiserDomains']); - expect(Object.keys(result[0].meta.advertiserDomains)).to.deep.equal([]); + expect(result[0].meta.advertiserDomains).to.deep.equal(['123']); }); }); diff --git a/test/spec/modules/appushBidAdapter_spec.js b/test/spec/modules/appushBidAdapter_spec.js new file mode 100644 index 00000000000..e6af98c0f33 --- /dev/null +++ b/test/spec/modules/appushBidAdapter_spec.js @@ -0,0 +1,373 @@ +import { expect } from 'chai'; +import { spec } from '../../../modules/appushBidAdapter.js'; +import { BANNER, VIDEO, NATIVE } from '../../../src/mediaTypes.js'; +import { getUniqueIdentifierStr } from '../../../src/utils.js'; + +const bidder = 'appush' + +describe('AppushBidAdapter', function () { + const bids = [ + { + bidId: getUniqueIdentifierStr(), + bidder: bidder, + mediaTypes: { + [BANNER]: { + sizes: [[300, 250]] + } + }, + params: { + placementId: 'testBanner', + } + }, + { + bidId: getUniqueIdentifierStr(), + bidder: bidder, + mediaTypes: { + [VIDEO]: { + playerSize: [[300, 300]], + minduration: 5, + maxduration: 60 + } + }, + params: { + placementId: 'testVideo', + } + }, + { + bidId: getUniqueIdentifierStr(), + bidder: bidder, + mediaTypes: { + [NATIVE]: { + native: { + title: { + required: true + }, + body: { + required: true + }, + icon: { + required: true, + size: [64, 64] + } + } + } + }, + params: { + placementId: 'testNative', + } + } + ]; + + const invalidBid = { + bidId: getUniqueIdentifierStr(), + bidder: bidder, + mediaTypes: { + [BANNER]: { + sizes: [[300, 250]] + } + }, + params: { + + } + } + + const bidderRequest = { + uspConsent: '1---', + gdprConsent: 'COvFyGBOvFyGBAbAAAENAPCAAOAAAAAAAAAAAEEUACCKAAA.IFoEUQQgAIQwgIwQABAEAAAAOIAACAIAAAAQAIAgEAACEAAAAAgAQBAAAAAAAGBAAgAAAAAAAFAAECAAAgAAQARAEQAAAAAJAAIAAgAAAYQEAAAQmAgBC3ZAYzUw', + refererInfo: { + referer: 'https://test.com' + }, + timeout: 500 + }; + + describe('isBidRequestValid', function () { + it('Should return true if there are bidId, params and key parameters present', function () { + expect(spec.isBidRequestValid(bids[0])).to.be.true; + }); + it('Should return false if at least one of parameters is not present', function () { + expect(spec.isBidRequestValid(invalidBid)).to.be.false; + }); + }); + + describe('buildRequests', function () { + let serverRequest = spec.buildRequests(bids, bidderRequest); + + it('Creates a ServerRequest object with method, URL and data', function () { + expect(serverRequest).to.exist; + expect(serverRequest.method).to.exist; + expect(serverRequest.url).to.exist; + expect(serverRequest.data).to.exist; + }); + + it('Returns POST method', function () { + expect(serverRequest.method).to.equal('POST'); + }); + + it('Returns valid URL', function () { + expect(serverRequest.url).to.equal('https://hb.appush.com/pbjs'); + }); + + it('Returns general data valid', function () { + let data = serverRequest.data; + expect(data).to.be.an('object'); + expect(data).to.have.all.keys('deviceWidth', + 'deviceHeight', + 'language', + 'secure', + 'host', + 'page', + 'placements', + 'coppa', + 'ccpa', + 'gdpr', + 'tmax' + ); + expect(data.deviceWidth).to.be.a('number'); + expect(data.deviceHeight).to.be.a('number'); + expect(data.language).to.be.a('string'); + expect(data.secure).to.be.within(0, 1); + expect(data.host).to.be.a('string'); + expect(data.page).to.be.a('string'); + expect(data.coppa).to.be.a('number'); + expect(data.gdpr).to.be.a('string'); + expect(data.ccpa).to.be.a('string'); + expect(data.tmax).to.be.a('number'); + expect(data.placements).to.have.lengthOf(3); + }); + + it('Returns valid placements', function () { + const { placements } = serverRequest.data; + for (let i = 0, len = placements.length; i < len; i++) { + const placement = placements[i]; + expect(placement.placementId).to.be.oneOf(['testBanner', 'testVideo', 'testNative']); + expect(placement.adFormat).to.be.oneOf([BANNER, VIDEO, NATIVE]); + expect(placement.bidId).to.be.a('string'); + expect(placement.schain).to.be.an('object'); + expect(placement.bidfloor).to.exist.and.to.equal(0); + expect(placement.type).to.exist.and.to.equal('publisher'); + + if (placement.adFormat === BANNER) { + expect(placement.sizes).to.be.an('array'); + } + switch (placement.adFormat) { + case BANNER: + expect(placement.sizes).to.be.an('array'); + break; + case VIDEO: + expect(placement.playerSize).to.be.an('array'); + expect(placement.minduration).to.be.an('number'); + expect(placement.maxduration).to.be.an('number'); + break; + case NATIVE: + expect(placement.native).to.be.an('object'); + break; + } + } + }); + + it('Returns data with gdprConsent and without uspConsent', function () { + delete bidderRequest.uspConsent; + serverRequest = spec.buildRequests(bids, bidderRequest); + let data = serverRequest.data; + expect(data.gdpr).to.exist; + expect(data.gdpr).to.be.a('string'); + expect(data.gdpr).to.equal(bidderRequest.gdprConsent); + expect(data.ccpa).to.not.exist; + delete bidderRequest.gdprConsent; + }); + + it('Returns data with uspConsent and without gdprConsent', function () { + bidderRequest.uspConsent = '1---'; + delete bidderRequest.gdprConsent; + serverRequest = spec.buildRequests(bids, bidderRequest); + let data = serverRequest.data; + expect(data.ccpa).to.exist; + expect(data.ccpa).to.be.a('string'); + expect(data.ccpa).to.equal(bidderRequest.uspConsent); + expect(data.gdpr).to.not.exist; + }); + + it('Returns empty data if no valid requests are passed', function () { + serverRequest = spec.buildRequests([], bidderRequest); + let data = serverRequest.data; + expect(data.placements).to.be.an('array').that.is.empty; + }); + }); + + describe('interpretResponse', function () { + it('Should interpret banner response', function () { + const banner = { + body: [{ + mediaType: 'banner', + width: 300, + height: 250, + cpm: 0.4, + ad: 'Test', + requestId: '23fhj33i987f', + ttl: 120, + creativeId: '2', + netRevenue: true, + currency: 'USD', + dealId: '1', + meta: { + advertiserDomains: ['google.com'], + advertiserId: 1234 + } + }] + }; + let bannerResponses = spec.interpretResponse(banner); + expect(bannerResponses).to.be.an('array').that.is.not.empty; + let dataItem = bannerResponses[0]; + expect(dataItem).to.have.all.keys('requestId', 'cpm', 'width', 'height', 'ad', 'ttl', 'creativeId', + 'netRevenue', 'currency', 'dealId', 'mediaType', 'meta'); + expect(dataItem.requestId).to.equal(banner.body[0].requestId); + expect(dataItem.cpm).to.equal(banner.body[0].cpm); + expect(dataItem.width).to.equal(banner.body[0].width); + expect(dataItem.height).to.equal(banner.body[0].height); + expect(dataItem.ad).to.equal(banner.body[0].ad); + expect(dataItem.ttl).to.equal(banner.body[0].ttl); + expect(dataItem.creativeId).to.equal(banner.body[0].creativeId); + expect(dataItem.netRevenue).to.be.true; + expect(dataItem.currency).to.equal(banner.body[0].currency); + expect(dataItem.meta).to.be.an('object').that.has.any.key('advertiserDomains'); + }); + it('Should interpret video response', function () { + const video = { + body: [{ + vastUrl: 'test.com', + mediaType: 'video', + cpm: 0.5, + requestId: '23fhj33i987f', + ttl: 120, + creativeId: '2', + netRevenue: true, + currency: 'USD', + dealId: '1', + meta: { + advertiserDomains: ['google.com'], + advertiserId: 1234 + } + }] + }; + let videoResponses = spec.interpretResponse(video); + expect(videoResponses).to.be.an('array').that.is.not.empty; + + let dataItem = videoResponses[0]; + expect(dataItem).to.have.all.keys('requestId', 'cpm', 'vastUrl', 'ttl', 'creativeId', + 'netRevenue', 'currency', 'dealId', 'mediaType', 'meta'); + expect(dataItem.requestId).to.equal('23fhj33i987f'); + expect(dataItem.cpm).to.equal(0.5); + expect(dataItem.vastUrl).to.equal('test.com'); + expect(dataItem.ttl).to.equal(120); + expect(dataItem.creativeId).to.equal('2'); + expect(dataItem.netRevenue).to.be.true; + expect(dataItem.currency).to.equal('USD'); + expect(dataItem.meta).to.be.an('object').that.has.any.key('advertiserDomains'); + }); + it('Should interpret native response', function () { + const native = { + body: [{ + mediaType: 'native', + native: { + clickUrl: 'test.com', + title: 'Test', + image: 'test.com', + impressionTrackers: ['test.com'], + }, + ttl: 120, + cpm: 0.4, + requestId: '23fhj33i987f', + creativeId: '2', + netRevenue: true, + currency: 'USD', + meta: { + advertiserDomains: ['google.com'], + advertiserId: 1234 + } + }] + }; + let nativeResponses = spec.interpretResponse(native); + expect(nativeResponses).to.be.an('array').that.is.not.empty; + + let dataItem = nativeResponses[0]; + expect(dataItem).to.have.keys('requestId', 'cpm', 'ttl', 'creativeId', 'netRevenue', 'currency', 'mediaType', 'native', 'meta'); + expect(dataItem.native).to.have.keys('clickUrl', 'impressionTrackers', 'title', 'image') + expect(dataItem.requestId).to.equal('23fhj33i987f'); + expect(dataItem.cpm).to.equal(0.4); + expect(dataItem.native.clickUrl).to.equal('test.com'); + expect(dataItem.native.title).to.equal('Test'); + expect(dataItem.native.image).to.equal('test.com'); + expect(dataItem.native.impressionTrackers).to.be.an('array').that.is.not.empty; + expect(dataItem.native.impressionTrackers[0]).to.equal('test.com'); + expect(dataItem.ttl).to.equal(120); + expect(dataItem.creativeId).to.equal('2'); + expect(dataItem.netRevenue).to.be.true; + expect(dataItem.currency).to.equal('USD'); + expect(dataItem.meta).to.be.an('object').that.has.any.key('advertiserDomains'); + }); + it('Should return an empty array if invalid banner response is passed', function () { + const invBanner = { + body: [{ + width: 300, + cpm: 0.4, + ad: 'Test', + requestId: '23fhj33i987f', + ttl: 120, + creativeId: '2', + netRevenue: true, + currency: 'USD', + dealId: '1' + }] + }; + + let serverResponses = spec.interpretResponse(invBanner); + expect(serverResponses).to.be.an('array').that.is.empty; + }); + it('Should return an empty array if invalid video response is passed', function () { + const invVideo = { + body: [{ + mediaType: 'video', + cpm: 0.5, + requestId: '23fhj33i987f', + ttl: 120, + creativeId: '2', + netRevenue: true, + currency: 'USD', + dealId: '1' + }] + }; + let serverResponses = spec.interpretResponse(invVideo); + expect(serverResponses).to.be.an('array').that.is.empty; + }); + it('Should return an empty array if invalid native response is passed', function () { + const invNative = { + body: [{ + mediaType: 'native', + clickUrl: 'test.com', + title: 'Test', + impressionTrackers: ['test.com'], + ttl: 120, + requestId: '23fhj33i987f', + creativeId: '2', + netRevenue: true, + currency: 'USD', + }] + }; + let serverResponses = spec.interpretResponse(invNative); + expect(serverResponses).to.be.an('array').that.is.empty; + }); + it('Should return an empty array if invalid response is passed', function () { + const invalid = { + body: [{ + ttl: 120, + creativeId: '2', + netRevenue: true, + currency: 'USD', + dealId: '1' + }] + }; + let serverResponses = spec.interpretResponse(invalid); + expect(serverResponses).to.be.an('array').that.is.empty; + }); + }); +}); diff --git a/test/spec/modules/arcspanRtdProvider_spec.js b/test/spec/modules/arcspanRtdProvider_spec.js new file mode 100644 index 00000000000..c75075d8e05 --- /dev/null +++ b/test/spec/modules/arcspanRtdProvider_spec.js @@ -0,0 +1,187 @@ +import { arcspanSubmodule } from 'modules/arcspanRtdProvider.js'; +import { expect } from 'chai'; +import { loadExternalScript } from 'src/adloader.js'; + +describe('arcspanRtdProvider', function () { + describe('init', function () { + afterEach(function () { + window.arcobj1 = undefined; + window.arcobj2 = undefined; + }); + + it('successfully initializes with a valid silo ID', function () { + expect(arcspanSubmodule.init(getGoodConfig())).to.equal(true); + expect(loadExternalScript.called).to.be.ok; + expect(loadExternalScript.args[0][0]).to.deep.equal('https://silo13.p7cloud.net/as.js'); + loadExternalScript.resetHistory(); + }); + + it('fails to initialize with a missing silo ID', function () { + expect(arcspanSubmodule.init(getBadConfig())).to.equal(false); + expect(loadExternalScript.called).to.be.not.ok; + loadExternalScript.resetHistory(); + }); + + it('drops localhost script for test silo', function () { + expect(arcspanSubmodule.init(getTestConfig())).to.equal(true); + expect(loadExternalScript.called).to.be.ok; + expect(loadExternalScript.args[0][0]).to.deep.equal('https://localhost:8080/as.js'); + loadExternalScript.resetHistory(); + }); + }); + + describe('alterBidRequests', function () { + afterEach(function () { + window.arcobj1 = undefined; + window.arcobj2 = undefined; + }); + + it('alters the bid request 1', function () { + setIAB({ + raw: { + images: [ + 'Religion & Spirituality', + 'Medical Health>Substance Abuse', + 'Religion & Spirituality>Astrology', + 'Medical Health', + 'Events & Attractions', + ], + }, + codes: { + images: ['IAB23-10', 'IAB7', 'IAB7-42', 'IAB15-1'], + }, + newcodes: { + images: ['150', '453', '311', '456', '286'], + }, + }); + + var reqBidsConfigObj = {}; + reqBidsConfigObj.ortb2Fragments = {}; + reqBidsConfigObj.ortb2Fragments.global = {}; + arcspanSubmodule.getBidRequestData(reqBidsConfigObj, function () { + expect(reqBidsConfigObj.ortb2Fragments.global.site.name).to.equal( + 'arcspan' + ); + expect(reqBidsConfigObj.ortb2Fragments.global.site.keywords).to.equal( + 'Religion & Spirituality,Medical Health>Substance Abuse,Religion & Spirituality>Astrology,Medical Health,Events & Attractions' + ); + expect(reqBidsConfigObj.ortb2Fragments.global.site.content.data[0].ext.segtax).to.equal(6); + expect(reqBidsConfigObj.ortb2Fragments.global.site.cat).to.eql([ + 'IAB23_10', + 'IAB7', + 'IAB7_42', + 'IAB15_1', + ]); + expect(reqBidsConfigObj.ortb2Fragments.global.site.sectioncat).to.eql([ + 'IAB23_10', + 'IAB7', + 'IAB7_42', + 'IAB15_1' + ]); + expect(reqBidsConfigObj.ortb2Fragments.global.site.pagecat).to.eql([ + 'IAB23_10', + 'IAB7', + 'IAB7_42', + 'IAB15_1', + ]); + expect(reqBidsConfigObj.ortb2Fragments.global.site.content.data[0].segment).to.eql([ + { id: '150' }, + { id: '453' }, + { id: '311' }, + { id: '456' }, + { id: '286' } + ]); + }); + }); + + it('alters the bid request 2', function () { + setIAB({ + raw: { text: ['Sports', 'Sports>Soccer'] }, + codes: { text: ['IAB17', 'IAB17-44'] }, + newcodes: { text: ['483', '533'] }, + }); + + var reqBidsConfigObj = {}; + reqBidsConfigObj.ortb2Fragments = {}; + reqBidsConfigObj.ortb2Fragments.global = {}; + arcspanSubmodule.getBidRequestData(reqBidsConfigObj, function () { + expect(reqBidsConfigObj.ortb2Fragments.global.site.name).to.equal( + 'arcspan' + ); + expect(reqBidsConfigObj.ortb2Fragments.global.site.keywords).to.equal( + 'Sports,Sports>Soccer' + ); + expect(reqBidsConfigObj.ortb2Fragments.global.site.content.data[0].ext.segtax).to.equal(6); + expect(reqBidsConfigObj.ortb2Fragments.global.site.cat).to.eql([ + 'IAB17', + 'IAB17_44', + ]); + expect(reqBidsConfigObj.ortb2Fragments.global.site.sectioncat).to.eql([ + 'IAB17', + 'IAB17_44' + ]); + expect(reqBidsConfigObj.ortb2Fragments.global.site.pagecat).to.eql([ + 'IAB17', + 'IAB17_44', + ]); + expect(reqBidsConfigObj.ortb2Fragments.global.site.content.data[0].segment).to.eql([ + { id: '483' }, + { id: '533' } + ]); + }); + }); + }); +}); + +function getGoodConfig() { + return { + name: 'arcspan', + waitForIt: true, + params: { + silo: 13, + }, + }; +} + +function getBadConfig() { + return { + name: 'arcspan', + waitForIt: true, + params: { + notasilo: 1, + }, + }; +} + +function getTestConfig() { + return { + name: 'arcspan', + waitForIt: true, + params: { + silo: 'test', + }, + }; +} + +function setIAB(vjson) { + window.arcobj2 = {}; + window.arcobj2.cat = 0; + if (typeof vjson.codes != 'undefined') { + window.arcobj2.cat = 1; + if (typeof vjson.codes.images != 'undefined') { + vjson.codes.images.forEach(function f(e, i) { + vjson.codes.images[i] = e.replace('-', '_'); + }); + } + if (typeof vjson.codes.text != 'undefined') { + vjson.codes.text.forEach(function f(e, i) { + vjson.codes.text[i] = e.replace('-', '_'); + }); + } + window.arcobj2.sampled = 1; + window.arcobj1 = {}; + window.arcobj1.page_iab_codes = vjson.codes; + window.arcobj1.page_iab = vjson.raw; + window.arcobj1.page_iab_newcodes = vjson.newcodes; + } +} diff --git a/test/spec/modules/atsAnalyticsAdapter_spec.js b/test/spec/modules/atsAnalyticsAdapter_spec.js index cae90a19223..2316f96ec8e 100644 --- a/test/spec/modules/atsAnalyticsAdapter_spec.js +++ b/test/spec/modules/atsAnalyticsAdapter_spec.js @@ -3,14 +3,14 @@ import { expect } from 'chai'; import adapterManager from 'src/adapterManager.js'; import {server} from '../../mocks/xhr.js'; import {parseBrowser} from '../../../modules/atsAnalyticsAdapter.js'; -import {getStorageManager} from '../../../src/storageManager.js'; +import {getCoreStorageManager, getStorageManager} from '../../../src/storageManager.js'; import {analyticsUrl} from '../../../modules/atsAnalyticsAdapter.js'; let utils = require('src/utils'); let events = require('src/events'); let constants = require('src/constants.json'); -export const storage = getStorageManager(); +const storage = getCoreStorageManager(); let sandbox; let clock; let now = new Date(); diff --git a/test/spec/modules/audiencerunBidAdapter_spec.js b/test/spec/modules/audiencerunBidAdapter_spec.js index 57de8bdb0df..5c736345068 100644 --- a/test/spec/modules/audiencerunBidAdapter_spec.js +++ b/test/spec/modules/audiencerunBidAdapter_spec.js @@ -218,16 +218,7 @@ describe('AudienceRun bid adapter tests', function () { it('should add userid eids information to the request', function () { const bid = Object.assign({}, bidRequest); - bid.userId = { - pubcid: '01EAJWWNEPN3CYMM5N8M5VXY22', - unsuported: '666', - } - - const request = spec.buildRequests([bid]); - const payload = JSON.parse(request.data); - - expect(payload.userId).to.exist; - expect(payload.userId).to.deep.equal([ + bid.userIdAsEids = [ { source: 'pubcid.org', uids: [ @@ -237,7 +228,13 @@ describe('AudienceRun bid adapter tests', function () { }, ], }, - ]); + ]; + + const request = spec.buildRequests([bid]); + const payload = JSON.parse(request.data); + + expect(payload.userId).to.exist; + expect(payload.userId).to.deep.equal(bid.userIdAsEids); }); it('should add schain object if available', function() { diff --git a/test/spec/modules/bedigitechBidAdapter_spec.js b/test/spec/modules/bedigitechBidAdapter_spec.js new file mode 100644 index 00000000000..4bd335b7bbd --- /dev/null +++ b/test/spec/modules/bedigitechBidAdapter_spec.js @@ -0,0 +1,145 @@ +import { expect } from 'chai'; +import { spec } from 'modules/bedigitechBidAdapter.js'; +import { newBidder } from 'src/adapters/bidderFactory.js'; +import {BANNER} from 'src/mediaTypes.js'; + +describe('BedigitechAdapter', function () { + const adapter = newBidder(spec); + + describe('inherited functions', function () { + it('exists and is a function', function () { + expect(adapter.callBids).to.exist.and.to.be.a('function'); + }); + }); + + describe('isBidRequestValid', function () { + const bid = { + bidder: 'bedigitech', + adUnitCode: 'adunit-code', + mediaTypes: { + banner: { + sizes: [[300, 250], [300, 600]] + } + }, + bidId: '30b31c1838de1e', + bidderRequestId: '22edbae2733bf6', + auctionId: '1d1a030790a475', + params: { + placementId: 1234, + }, + }; + + it('should return true when required params found', function () { + expect(spec.isBidRequestValid(bid)).to.equal(true); + }); + + it('should return false when required params are not passed', function () { + const bid = Object.assign({}, bid); + delete bid.params; + bid.params = { + 'masterId': 0 + }; + + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + }); + + describe('buildRequests', function () { + const bidRequests = [{ + bidder: 'bedigitech', + adUnitCode: 'adunit-code', + mediaTypes: { + banner: { + sizes: [[300, 250], [300, 600]] + } + }, + bidId: '30b31c1838de1e', + bidderRequestId: '22edbae2733bf6', + auctionId: '1d1a030790a475', + params: { + placementId: 1234, + }, + }]; + + it('sends bid request to url via GET', function () { + const request = spec.buildRequests(bidRequests)[0]; + expect(request.method).to.equal('GET'); + expect(request.url).to.equal('https://bedigitalhb.s3.amazonaws.com/hb.js'); + }); + + it('should attach pid to url', function () { + const request = spec.buildRequests(bidRequests)[0]; + expect(request.data.pid).to.equal(1234); + }); + }); + + describe('interpretResponse', function () { + const response = { + 'body': [ + { + 'id': 'bedigitechMyidfdfdf', + 'requestId': 'gshgfshdfgdfsd', + 'cpm': '5.0', + 'ad': '%3C!--%20Creative%20--%3E', + 'width': 300, + 'height': 250, + 'currency': 'USD', + 'ttl': 300, + 'creativeId': '0af345b42983cc4bc0' + } + ], + 'headers': { + 'get': function() {} + } + }; + + const bidRequest = { + bidder: 'bedigitech', + adUnitCode: 'adunit-code', + mediaTypes: { + banner: { + sizes: [[300, 250], [300, 600]] + } + }, + bidId: '30b31c1838de1e', + bidderRequestId: '22edbae2733bf6', + auctionId: '1d1a030790a475', + params: { + placementId: 1234, + }, + }; + + it('should get correct bid response', function () { + const expectedResponse = [ + { + 'id': 'bedigitechMyidfdfdf', + 'requestId': 'gshgfshdfgdfsd', + 'cpm': '5.0', + 'ad': '%3C!--%20Creative%20--%3E', + 'width': 300, + 'height': 250, + 'currency': 'USD', + 'ttl': 300, + 'creativeId': '0af345b42983cc4bc0', + 'meta': { + 'mediaType': BANNER, + }, + } + ]; + + const result = spec.interpretResponse(response, bidRequest); + expect(result).to.have.lengthOf(1); + let resultKeys = Object.keys(result[0]); + expect(resultKeys.sort()).to.deep.equal(Object.keys(expectedResponse[0]).sort()); + resultKeys.forEach(function(k) { + if (k === 'ad') { + expect(result[0][k]).to.match(/$/); + } else if (k === 'meta') { + expect(result[0][k]).to.deep.equal(expectedResponse[0][k]); + } else { + expect(result[0][k]).to.equal(expectedResponse[0][k]); + } + }); + }); + }); +}); diff --git a/test/spec/modules/beyondmediaBidAdapter_spec.js b/test/spec/modules/beyondmediaBidAdapter_spec.js index a4c1125fa5f..751b3ae1098 100644 --- a/test/spec/modules/beyondmediaBidAdapter_spec.js +++ b/test/spec/modules/beyondmediaBidAdapter_spec.js @@ -76,7 +76,8 @@ describe('AndBeyondMediaBidAdapter', function () { gdprConsent: 'COvFyGBOvFyGBAbAAAENAPCAAOAAAAAAAAAAAEEUACCKAAA.IFoEUQQgAIQwgIwQABAEAAAAOIAACAIAAAAQAIAgEAACEAAAAAgAQBAAAAAAAGBAAgAAAAAAAFAAECAAAgAAQARAEQAAAAAJAAIAAgAAAYQEAAAQmAgBC3ZAYzUw', refererInfo: { referer: 'https://test.com' - } + }, + timeout: 500 }; describe('isBidRequestValid', function () { diff --git a/test/spec/modules/bliinkBidAdapter_spec.js b/test/spec/modules/bliinkBidAdapter_spec.js index b8994b86847..8e96bd76940 100644 --- a/test/spec/modules/bliinkBidAdapter_spec.js +++ b/test/spec/modules/bliinkBidAdapter_spec.js @@ -130,7 +130,7 @@ const getConfigCreative = () => { creativeId: '34567erty', width: 300, height: 250, - ttl: 3600, + ttl: 300, netRevenue: true, } } @@ -145,7 +145,7 @@ const getConfigCreativeVideo = (isNoVast) => { requestId: '6a204ce130280d', width: 300, height: 250, - ttl: 3600, + ttl: 300, netRevenue: true, } } @@ -395,7 +395,7 @@ const testsInterpretResponse = [ mediaType: 'video', netRevenue: true, requestId: '2def0c5b2a7f6e', - ttl: 3600, + ttl: 300, vastXml, vastUrl: 'data:text/xml;charset=utf-8;base64,' + btoa(vastXml.replace(/\\"/g, '"')) }] @@ -421,7 +421,7 @@ const testsInterpretResponse = [ mediaType: 'banner', netRevenue: true, requestId: '2def0c5b2a7f6e', - ttl: 3600, + ttl: 300, width: 300 }] }, @@ -505,7 +505,7 @@ const testsBuildBid = [ netRevenue: true, vastXml: getConfigCreativeVideo().vastXml, vastUrl: 'data:text/xml;charset=utf-8;base64,' + btoa(getConfigCreativeVideo().vastXml.replace(/\\"/g, '"')), - ttl: 3600, + ttl: 300, } }, { @@ -535,7 +535,7 @@ const testsBuildBid = [ netRevenue: true, vastXml: getConfigCreativeVideo().vastXml, vastUrl: 'data:text/xml;charset=utf-8;base64,' + btoa(getConfigCreativeVideo().vastXml.replace(/\\"/g, '"')), - ttl: 3600, + ttl: 300, } }, { @@ -552,7 +552,7 @@ const testsBuildBid = [ height: 250, creativeId: getConfigCreative().creativeId, ad: getConfigBannerBid().creative.banner.adm, - ttl: 3600, + ttl: 300, netRevenue: true, } } diff --git a/test/spec/modules/bluebillywigBidAdapter_spec.js b/test/spec/modules/bluebillywigBidAdapter_spec.js index 0a9c6b30c94..0826acc7f29 100644 --- a/test/spec/modules/bluebillywigBidAdapter_spec.js +++ b/test/spec/modules/bluebillywigBidAdapter_spec.js @@ -535,10 +535,8 @@ describe('BlueBillywigAdapter', () => { }); it('should set user ids when present', () => { - const userId = { tdid: 123 }; - const newBaseValidBidRequests = deepClone(baseValidBidRequests); - newBaseValidBidRequests[0].userId = { criteoId: 'sample-userid' }; + newBaseValidBidRequests[0].userIdAsEids = [ {} ]; const request = spec.buildRequests(newBaseValidBidRequests, validBidderRequest); const payload = JSON.parse(request.data); diff --git a/test/spec/modules/brightcomSSPBidAdapter_spec.js b/test/spec/modules/brightcomSSPBidAdapter_spec.js new file mode 100644 index 00000000000..6f35a7a290b --- /dev/null +++ b/test/spec/modules/brightcomSSPBidAdapter_spec.js @@ -0,0 +1,411 @@ +import { expect } from 'chai'; +import * as utils from 'src/utils.js'; +import { spec } from 'modules/brightcomSSPBidAdapter'; +import { newBidder } from 'src/adapters/bidderFactory.js'; +import {config} from '../../../src/config'; + +const URL = 'https://rt.marphezis.com/hb'; + +describe('brightcomSSPBidAdapter', function() { + const adapter = newBidder(spec); + let element, win; + let bidRequests; + let sandbox; + + beforeEach(function() { + element = { + x: 0, + y: 0, + + width: 0, + height: 0, + + getBoundingClientRect: () => { + return { + width: element.width, + height: element.height, + + left: element.x, + top: element.y, + right: element.x + element.width, + bottom: element.y + element.height + }; + } + }; + win = { + document: { + visibilityState: 'visible' + }, + + innerWidth: 800, + innerHeight: 600 + }; + bidRequests = [{ + 'bidder': 'bcmssp', + 'params': { + 'publisherId': 1234567 + }, + 'adUnitCode': 'adunit-code', + 'mediaTypes': { + 'banner': { + 'sizes': [[300, 250], [300, 600]] + } + }, + 'bidId': '5fb26ac22bde4', + 'bidderRequestId': '4bf93aeb730cb9', + 'auctionId': 'ffe9a1f7-7b67-4bda-a8e0-9ee5dc9f442e', + 'schain': { + 'ver': '1.0', + 'complete': 1, + 'nodes': [ + { + 'asi': 'exchange1.com', + 'sid': '1234', + 'hp': 1, + 'rid': 'bid-request-1', + 'name': 'publisher', + 'domain': 'publisher.com' + } + ] + }, + }]; + + sandbox = sinon.sandbox.create(); + sandbox.stub(document, 'getElementById').withArgs('adunit-code').returns(element); + sandbox.stub(utils, 'getWindowTop').returns(win); + sandbox.stub(utils, 'getWindowSelf').returns(win); + }); + + afterEach(function() { + sandbox.restore(); + }); + + describe('isBidRequestValid', function () { + let bid = { + 'bidder': 'bcmssp', + 'params': { + 'publisherId': 1234567 + }, + 'adUnitCode': 'adunit-code', + 'mediaTypes': { + 'banner': { + 'sizes': [[300, 250], [300, 600]] + } + }, + 'bidId': '5fb26ac22bde4', + 'bidderRequestId': '4bf93aeb730cb9', + 'auctionId': 'ffe9a1f7-7b67-4bda-a8e0-9ee5dc9f442e', + }; + + it('should return true when required params found', function () { + expect(spec.isBidRequestValid(bid)).to.equal(true); + }); + + it('should return false when publisherId not passed correctly', function () { + bid.params.publisherId = undefined; + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + + it('should return false when require params are not passed', function () { + let bid = Object.assign({}, bid); + bid.params = {}; + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + }); + + describe('buildRequests', function () { + it('sends bid request to our endpoint via POST', function () { + const request = spec.buildRequests(bidRequests); + expect(request.method).to.equal('POST'); + }); + + it('request url should match our endpoint url', function () { + const request = spec.buildRequests(bidRequests); + expect(request.url).to.equal(URL); + }); + + it('sets the proper banner object', function() { + const request = spec.buildRequests(bidRequests); + const payload = JSON.parse(request.data); + expect(payload.imp[0].banner.format).to.deep.equal([{w: 300, h: 250}, {w: 300, h: 600}]); + }); + + it('accepts a single array as a size', function() { + bidRequests[0].mediaTypes.banner.sizes = [300, 250]; + const request = spec.buildRequests(bidRequests); + const payload = JSON.parse(request.data); + expect(payload.imp[0].banner.format).to.deep.equal([{w: 300, h: 250}]); + }); + + it('sends bidfloor param if present', function () { + bidRequests[0].params.bidFloor = 0.05; + const request = spec.buildRequests(bidRequests); + const payload = JSON.parse(request.data); + expect(payload.imp[0].bidfloor).to.equal(0.05); + }); + + it('sends tagid', function () { + const request = spec.buildRequests(bidRequests); + const payload = JSON.parse(request.data); + expect(payload.imp[0].tagid).to.equal('adunit-code'); + }); + + it('sends publisher id', function () { + const request = spec.buildRequests(bidRequests); + const payload = JSON.parse(request.data); + expect(payload.site.publisher.id).to.equal(1234567); + }); + + it('sends gdpr info if exists', function () { + const consentString = 'BOJ8RZsOJ8RZsABAB8AAAAAZ+A=='; + const bidderRequest = { + 'bidderCode': 'bcmssp', + 'auctionId': '1d1a030790a437', + 'bidderRequestId': '22edbae2744bf5', + 'timeout': 3000, + gdprConsent: { + consentString: consentString, + gdprApplies: true + }, + refererInfo: { + page: 'http://example.com/page.html', + domain: 'example.com', + } + }; + bidderRequest.bids = bidRequests; + + const data = JSON.parse(spec.buildRequests(bidRequests, bidderRequest).data); + + expect(data.regs.ext.gdpr).to.exist.and.to.be.a('number'); + expect(data.regs.ext.gdpr).to.equal(1); + expect(data.user.ext.consent).to.exist.and.to.be.a('string'); + expect(data.user.ext.consent).to.equal(consentString); + }); + + it('sends us_privacy', function () { + const bidderRequest = { + uspConsent: '1YYY' + }; + const data = JSON.parse(spec.buildRequests(bidRequests, bidderRequest).data) + + expect(data.regs).to.not.equal(null); + expect(data.regs.ext).to.not.equal(null); + expect(data.regs.ext.us_privacy).to.equal('1YYY'); + }); + + it('sends coppa', function () { + sandbox.stub(config, 'getConfig').withArgs('coppa').returns(true); + + const data = JSON.parse(spec.buildRequests(bidRequests).data) + expect(data.regs).to.not.be.undefined; + expect(data.regs.coppa).to.equal(1); + }); + + it('sends schain', function () { + const data = JSON.parse(spec.buildRequests(bidRequests).data); + expect(data).to.not.be.undefined; + expect(data.source).to.not.be.undefined; + expect(data.source.ext).to.not.be.undefined; + expect(data.source.ext.schain).to.not.be.undefined; + expect(data.source.ext.schain.complete).to.equal(1); + expect(data.source.ext.schain.ver).to.equal('1.0'); + expect(data.source.ext.schain.nodes).to.not.be.undefined; + expect(data.source.ext.schain.nodes).to.lengthOf(1); + expect(data.source.ext.schain.nodes[0].asi).to.equal('exchange1.com'); + expect(data.source.ext.schain.nodes[0].sid).to.equal('1234'); + expect(data.source.ext.schain.nodes[0].hp).to.equal(1); + expect(data.source.ext.schain.nodes[0].rid).to.equal('bid-request-1'); + expect(data.source.ext.schain.nodes[0].name).to.equal('publisher'); + expect(data.source.ext.schain.nodes[0].domain).to.equal('publisher.com'); + }); + + it('sends user eid parameters', function () { + bidRequests[0].userIdAsEids = [{ + source: 'pubcid.org', + uids: [{ + id: 'userid_pubcid' + }] + }, { + source: 'adserver.org', + uids: [{ + id: 'userid_ttd', + ext: { + rtiPartner: 'TDID' + } + }] + } + ]; + + const data = JSON.parse(spec.buildRequests(bidRequests).data); + + expect(data.user).to.not.be.undefined; + expect(data.user.ext).to.not.be.undefined; + expect(data.user.ext.eids).to.not.be.undefined; + expect(data.user.ext.eids).to.deep.equal(bidRequests[0].userIdAsEids); + }); + + it('sends user id parameters', function () { + const userId = { + sharedid: { + id: '01*******', + third: '01E*******' + } + }; + + bidRequests[0].userId = userId; + + const data = JSON.parse(spec.buildRequests(bidRequests).data); + expect(data.user).to.not.be.undefined; + expect(data.user.ext).to.not.be.undefined; + expect(data.user.ext.ids).is.deep.equal(userId); + }); + + context('when element is fully in view', function() { + it('returns 100', function() { + Object.assign(element, { width: 600, height: 400 }); + const request = spec.buildRequests(bidRequests); + const payload = JSON.parse(request.data); + expect(payload.imp[0].banner.ext.viewability).to.equal(100); + }); + }); + + context('when element is out of view', function() { + it('returns 0', function() { + Object.assign(element, { x: -300, y: 0, width: 207, height: 320 }); + const request = spec.buildRequests(bidRequests); + const payload = JSON.parse(request.data); + expect(payload.imp[0].banner.ext.viewability).to.equal(0); + }); + }); + + context('when element is partially in view', function() { + it('returns percentage', function() { + Object.assign(element, { width: 800, height: 800 }); + const request = spec.buildRequests(bidRequests); + const payload = JSON.parse(request.data); + expect(payload.imp[0].banner.ext.viewability).to.equal(75); + }); + }); + + context('when width or height of the element is zero', function() { + it('try to use alternative values', function() { + Object.assign(element, { width: 0, height: 0 }); + bidRequests[0].mediaTypes.banner.sizes = [[800, 2400]]; + const request = spec.buildRequests(bidRequests); + const payload = JSON.parse(request.data); + expect(payload.imp[0].banner.ext.viewability).to.equal(25); + }); + }); + + context('when nested iframes', function() { + it('returns \'na\'', function() { + Object.assign(element, { width: 600, height: 400 }); + + utils.getWindowTop.restore(); + utils.getWindowSelf.restore(); + sandbox.stub(utils, 'getWindowTop').returns(win); + sandbox.stub(utils, 'getWindowSelf').returns({}); + + const request = spec.buildRequests(bidRequests); + const payload = JSON.parse(request.data); + expect(payload.imp[0].banner.ext.viewability).to.equal('na'); + }); + }); + + context('when tab is inactive', function() { + it('returns 0', function() { + Object.assign(element, { width: 600, height: 400 }); + + utils.getWindowTop.restore(); + win.document.visibilityState = 'hidden'; + sandbox.stub(utils, 'getWindowTop').returns(win); + + const request = spec.buildRequests(bidRequests); + const payload = JSON.parse(request.data); + expect(payload.imp[0].banner.ext.viewability).to.equal(0); + }); + }); + }); + + describe('interpretResponse', function () { + let response; + beforeEach(function () { + response = { + body: { + 'id': '37386aade21a71', + 'seatbid': [{ + 'bid': [{ + 'id': '376874781', + 'impid': '283a9f4cd2415d', + 'price': 0.35743275, + 'nurl': '', + 'adm': '', + 'w': 300, + 'h': 250, + 'adomain': ['example.com'] + }] + }] + } + }; + }); + + it('should get the correct bid response', function () { + let expectedResponse = [{ + 'requestId': '283a9f4cd2415d', + 'cpm': 0.35743275, + 'width': 300, + 'height': 250, + 'creativeId': '376874781', + 'currency': 'USD', + 'netRevenue': true, + 'mediaType': 'banner', + 'ad': `
`, + 'ttl': 60, + 'meta': { + 'advertiserDomains': ['example.com'] + } + }]; + + let result = spec.interpretResponse(response); + expect(result[0]).to.deep.equal(expectedResponse[0]); + }); + + it('crid should default to the bid id if not on the response', function () { + let expectedResponse = [{ + 'requestId': '283a9f4cd2415d', + 'cpm': 0.35743275, + 'width': 300, + 'height': 250, + 'creativeId': response.body.seatbid[0].bid[0].id, + 'currency': 'USD', + 'netRevenue': true, + 'mediaType': 'banner', + 'ad': `
`, + 'ttl': 60, + 'meta': { + 'advertiserDomains': ['example.com'] + } + }]; + + let result = spec.interpretResponse(response); + expect(result[0]).to.deep.equal(expectedResponse[0]); + }); + + it('handles empty bid response', function () { + let response = { + body: '' + }; + let result = spec.interpretResponse(response); + expect(result.length).to.equal(0); + }); + }); + + describe('getUserSyncs ', () => { + let syncOptions = {iframeEnabled: true, pixelEnabled: true}; + + it('should not return', () => { + let returnStatement = spec.getUserSyncs(syncOptions, []); + expect(returnStatement).to.be.empty; + }); + }); +}); diff --git a/test/spec/modules/browsiBidAdapter_spec.js b/test/spec/modules/browsiBidAdapter_spec.js new file mode 100644 index 00000000000..8892396adac --- /dev/null +++ b/test/spec/modules/browsiBidAdapter_spec.js @@ -0,0 +1,209 @@ +import {ENDPOINT, spec} from 'modules/browsiBidAdapter.js'; +import {config} from 'src/config.js'; +import {VIDEO, BANNER} from 'src/mediaTypes.js'; + +const {expect} = require('chai'); +const DATA = 'brwvidtag'; +const ADAPTER = '__bad'; + +describe('browsi Bid Adapter Test', function () { + describe('isBidRequestValid', function () { + let mediaTypes; + let bid; + beforeEach(function () { + mediaTypes = {}; + mediaTypes[VIDEO] = {}; + bid = { + 'params': { + 'pubId': '1234567', + 'tagId': '1' + }, + 'mediaTypes': mediaTypes + }; + }); + it('should return true when required params found', function () { + expect(spec.isBidRequestValid(bid)).to.equal(true); + }); + it('should return false when missing pubId', function () { + delete bid.params.pubId; + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + it('should return false when missing tagId', function () { + delete bid.params.tagId; + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + it('should return false when missing params', function () { + delete bid.params; + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + it('should return false when params have invalid type', function () { + bid.params.tagId = 1; + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + it('should return false when video mediaType is missing', function () { + delete bid.mediaTypes[VIDEO]; + bid.mediaTypes[BANNER] = {} + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + }); + + describe('buildRequests', function () { + let bidRequest; + let bidderRequest; + beforeEach(function () { + window[DATA] = {} + window[DATA][ADAPTER] = {index: 0}; + bidRequest = [ + { + 'params': { + 'pubId': 'browiPubId1', + 'tagId': '2' + }, + 'adUnitCode': 'adUnitCode1', + 'auctionId': 'auctionId1', + 'sizes': [640, 480], + 'bidId': '12345678', + 'requestId': '1234567-3456-4562-7689-98765434A', + 'transactionId': '1234567-3456-4562-7689-98765434B', + 'schain': {}, + 'mediaTypes': {video: {playerSize: [640, 480]}} + } + ]; + bidderRequest = { + 'bidderRequestId': 'bidderRequestId1', + 'refererInfo': { + 'canonicalUrl': null, + 'page': 'https://browsi.com', + 'domain': 'browsi.com', + 'ref': null, + 'numIframes': 0, + 'reachedTop': true, + 'isAmp': false, + 'stack': ['https://browsi.com'] + }, + 'gdprConsent': { + consentString: 'BOtmiBKOtmiBKABABAENAFAAAAACeAAA', + gdprApplies: true + }, + 'uspConsent': '1YYY' + }; + }); + afterEach(function() { + window[DATA] = undefined; + config.resetConfig(); + }); + it('should return an array of requests with single request', function () { + const requests = spec.buildRequests(bidRequest, bidderRequest); + expect(requests.length).to.equal(1); + const request = requests[0]; + const inputRequest = bidRequest[0]; + const requestToExpect = { + method: 'POST', + url: ENDPOINT, + data: { + requestId: bidderRequest.bidderRequestId, + bidId: inputRequest.bidId, + timeout: 3000, + baData: window[DATA][ADAPTER], + referer: bidderRequest.refererInfo.page, + gdpr: bidderRequest.gdprConsent, + ccpa: bidderRequest.uspConsent, + sizes: inputRequest.sizes, + video: {playerSize: [640, 480]}, + aUCode: inputRequest.adUnitCode, + aID: inputRequest.auctionId, + tID: inputRequest.transactionId, + schain: inputRequest.schain, + params: inputRequest.params + } + } + assert.deepEqual(request, requestToExpect); + }); + it('should pass on timeout in bidderRequest', function() { + bidderRequest.timeout = 8000; + const requests = spec.buildRequests(bidRequest, bidderRequest); + expect(requests[0].data.timeout).to.equal(8000); + }); + it('should pass timeout in config', function() { + config.setConfig({'bidderTimeout': 6000}); + const requests = spec.buildRequests(bidRequest, bidderRequest); + expect(requests[0].data.timeout).to.equal(6000); + }); + }); + + describe('interpretResponse', function () { + let bidRequest = { + 'url': ENDPOINT, + 'data': { + 'bidId': 'bidId1', + } + }; + let serverResponse = {}; + serverResponse.body = { + bidId: 'bidId1', + w: 300, + h: 250, + vXml: 'vastXml', + vUrl: 'vastUrl', + cpm: 1, + cur: 'USD', + ttl: 10000, + someExtraParams: 8, + } + + it('should return a valid response', function () { + const bidResponses = spec.interpretResponse(serverResponse, bidRequest); + expect(bidResponses.length).to.equal(1); + const actualBidResponse = bidResponses[0]; + const expectedBidResponse = { + requestId: bidRequest.data.bidId, + bidderCode: 'browsi', + bidId: 'bidId1', + width: 300, + height: 250, + vastXml: 'vastXml', + vastUrl: 'vastUrl', + cpm: 1, + currency: 'USD', + ttl: 10000, + someExtraParams: 8, + mediaType: VIDEO + }; + + assert.deepEqual(actualBidResponse, expectedBidResponse); + }); + }); + + describe('getUserSyncs', function () { + const bidResponse = { + userSyncs: [ + {url: 'syncUrl1', type: 'image'}, + {url: 'http://syncUrl2', type: 'iframe'} + ] + } + let serverResponse = [ + {body: bidResponse} + ]; + it('should return iframe type userSync', function () { + let userSyncs = spec.getUserSyncs({iframeEnabled: true, pixelEnabled: false}, serverResponse[0]); + expect(userSyncs.length).to.equal(1); + let userSync = userSyncs[0]; + expect(userSync.url).to.equal('http://syncUrl2'); + expect(userSync.type).to.equal('iframe'); + }); + it('should return image type userSyncs', function () { + let userSyncs = spec.getUserSyncs({iframeEnabled: false, pixelEnabled: true}, serverResponse[0]); + let userSync = userSyncs[0]; + expect(userSync.url).to.equal('http://syncUrl1'); + expect(userSync.type).to.equal('image'); + }); + it('should handle multiple server responses', function () { + let userSyncs = spec.getUserSyncs({iframeEnabled: false, pixelEnabled: true}, serverResponse); + expect(userSyncs.length).to.equal(1); + }); + it('should return empty userSyncs', function () { + let userSyncs = spec.getUserSyncs({iframeEnabled: false, pixelEnabled: false}, serverResponse); + expect(userSyncs.length).to.equal(0); + }); + }); +}); diff --git a/test/spec/modules/cleanmedianetBidAdapter_spec.js b/test/spec/modules/cleanmedianetBidAdapter_spec.js index 8c2ac34350b..a19cdf7fd35 100644 --- a/test/spec/modules/cleanmedianetBidAdapter_spec.js +++ b/test/spec/modules/cleanmedianetBidAdapter_spec.js @@ -2,96 +2,295 @@ import {expect} from 'chai'; import {spec, helper} from 'modules/cleanmedianetBidAdapter.js'; import * as utils from 'src/utils.js'; import {newBidder} from '../../../src/adapters/bidderFactory.js'; +import {deepClone} from 'src/utils'; const supplyPartnerId = '123'; const adapter = newBidder(spec); -describe('CleanmedianetAdapter', function () { - describe('Is String start with search ', function () { - it('check if a string started with', function () { - expect(helper.startsWith('cleanmediaads.com', 'cleanmediaads')).to.equal( - true - ); +const TTL = 360; + +describe('CleanmedianetAdapter', () => { + let schainConfig, + bidRequest, + bannerBidRequest, + videoBidRequest, + rtbResponse, + videoResponse, + gdprConsent; + + beforeEach(() => { + schainConfig = { + 'ver': '1.0', + 'complete': 1, + 'nodes': [ + { + 'asi': 'indirectseller.com', + 'sid': '00001', + 'hp': 1 + }, + + { + 'asi': 'indirectseller-2.com', + 'sid': '00002', + 'hp': 2 + } + ] + }; + + bidRequest = { + 'adUnitCode': 'adunit-code', + 'auctionId': '1d1a030790a475', + 'mediaTypes': { + banner: {} + }, + 'params': { + 'supplyPartnerId': supplyPartnerId + }, + 'sizes': [[300, 250], [300, 600]], + 'transactionId': 'a123456789', + refererInfo: {referer: 'http://examplereferer.com'}, + gdprConsent: { + consentString: 'some string', + gdprApplies: true + }, + schain: schainConfig, + uspConsent: 'cleanmediaCCPA' + }; + + bannerBidRequest = { + 'adUnitCode': 'adunit-code', + 'auctionId': '1d1a030790a475', + 'mediaTypes': { + banner: {} + }, + 'params': { + 'supplyPartnerId': supplyPartnerId + }, + 'sizes': [[300, 250], [300, 600]], + 'transactionId': 'a123456789', + 'bidId': '111', + refererInfo: {referer: 'http://examplereferer.com'} + }; + + videoBidRequest = { + 'adUnitCode': 'adunit-code', + 'auctionId': '1d1a030790a475', + 'mediaTypes': { + video: {} + }, + 'params': { + 'supplyPartnerId': supplyPartnerId + }, + 'sizes': [[300, 250], [300, 600]], + 'transactionId': 'a123456789', + 'bidId': '111', + refererInfo: {referer: 'http://examplereferer.com'} + }; + + rtbResponse = { + 'id': 'imp_5b05b9fde4b09084267a556f', + 'bidid': 'imp_5b05b9fde4b09084267a556f', + 'cur': 'USD', + 'ext': { + 'utrk': [ + {'type': 'iframe', 'url': '//bidder.cleanmediaads.com/user/sync/1?gdpr=[GDPR]&consent=[CONSENT]&usp=[US_PRIVACY]'}, + {'type': 'image', 'url': '//bidder.cleanmediaads.com/user/sync/2'} + ] + }, + 'seatbid': [ + { + 'seat': 'seat1', + 'group': 0, + 'bid': [ + { + 'id': '0', + 'impid': '1', + 'price': 2.016, + 'adid': '579ef31bfa788b9d2000d562', + 'nurl': 'https://bidder.cleanmediaads.com/pix/monitoring/win_notice/imp_5b05b9fde4b09084267a556f/im.gif?r=imp_5b05b9fde4b09084267a556f&i=1&a=579ef31bfa788b9d2000d562&b=0', + 'adm': '', + 'adomain': ['aaa.com'], + 'cid': '579ef268fa788b9d2000d55c', + 'crid': '579ef31bfa788b9d2000d562', + 'attr': [], + 'h': 600, + 'w': 120, + 'ext': { + 'vast_url': 'http://my.vast.com', + 'utrk': [ + {'type': 'iframe', 'url': '//p.partner1.io/user/sync/1'} + ] + } + } + ] + }, + { + 'seat': 'seat2', + 'group': 0, + 'bid': [ + { + 'id': '1', + 'impid': '1', + 'price': 3, + 'adid': '542jlhdfd2112jnjf3x', + 'nurl': 'https://bidder.cleanmediaads.com/pix/monitoring/win_notice/imp_5b05b9fde4b09084267a556f/im.gif?r=imp_5b05b9fde4b09084267a556f&i=1&a=579ef31bfa788b9d2000d562&b=0', + 'adm': ' ', + 'adomain': ['bbb.com'], + 'cid': 'fgdlwjh2498ydjhg1', + 'crid': 'kjh34297ydh2133d', + 'attr': [], + 'h': 250, + 'w': 300, + 'ext': { + 'utrk': [ + {'type': 'image', 'url': '//p.partner2.io/user/sync/1'} + ] + } + } + ] + } + ] + }; + + videoResponse = { + 'id': '64f32497-b2f7-48ec-9205-35fc39894d44', + 'bidid': 'imp_5c24924de4b0d106447af333', + 'cur': 'USD', + 'seatbid': [ + { + 'seat': '3668', + 'group': 0, + 'bid': [ + { + 'id': 'gb_1', + 'impid': 'afbb5852-7cea-4a81-aa9a-a41aab505c23', + 'price': 5.0, + 'adid': '1274', + 'nurl': 'https://bidder.cleanmediaads.com/pix/1275/win_notice/imp_5c24924de4b0d106447af333/im.gif?r=imp_5c24924de4b0d106447af333&i=afbb5852-7cea-4a81-aa9a-a41aab505c23&a=1274&b=gb_1', + 'adomain': [], + 'adm': '\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n', + 'cid': '3668', + 'crid': '1274', + 'cat': [], + 'attr': [], + 'h': 250, + 'w': 300, + 'ext': { + 'vast_url': 'https://bidder.cleanmediaads.com/pix/1275/vast_o/imp_5c24924de4b0d106447af333/im.xml?r=imp_5c24924de4b0d106447af333&i=afbb5852-7cea-4a81-aa9a-a41aab505c23&a=1274&b=gb_1&w=300&h=250&vatu=aHR0cHM6Ly9zdGF0aWMuZ2FtYmlkLmlvL2RlbW8vdmFzdC54bWw&vwarv', + 'imptrackers': [ + 'https://bidder.cleanmediaads.com/pix/1275/imp/imp_5c24924de4b0d106447af333/im.gif?r=imp_5c24924de4b0d106447af333&i=afbb5852-7cea-4a81-aa9a-a41aab505c23&a=1274&b=gb_1'] + } + } + ] + } + ], + 'ext': { + 'utrk': [{ + 'type': 'image', + 'url': 'https://bidder.cleanmediaads.com/pix/1275/scm?cb=1545900621675&gdpr=[GDPR]&consent=[CONSENT]&us_privacy=[US_PRIVACY]' + }] + } + }; + + gdprConsent = { + gdprApplies: true, + consentString: 'consent string' + }; + }); + + describe('Get top Frame', () => { + it('check if you are in the top frame', () => { + expect(helper.getTopFrame()).to.equal(0); + }); + }); + + describe('Is String start with search', () => { + it('check if a string started with', () => { + expect(helper.startsWith('cleanmedia.net', 'clea')).to.equal(true); }); }); - describe('inherited functions', function() { - it('exists and is a function', function() { + describe('inherited functions', () => { + it('exists and is a function', () => { expect(adapter.callBids).to.exist.and.to.be.a('function'); }); }); - describe('isBidRequestValid', function() { - it('should validate supply-partner ID', function() { - expect(spec.isBidRequestValid({ params: {} })).to.equal(false); - expect( - spec.isBidRequestValid({ params: { supplyPartnerId: 123 } }) - ).to.equal(false); - expect( - spec.isBidRequestValid({ params: { supplyPartnerId: '123' } }) - ).to.equal(true); + describe('isBidRequestValid', () => { + it('should validate supply-partner ID', () => { + expect(spec.isBidRequestValid({params: {}})).to.equal(false); + expect(spec.isBidRequestValid({params: {supplyPartnerId: 123}})).to.equal(false); + expect(spec.isBidRequestValid({params: {supplyPartnerId: '123'}})).to.equal(true); + }); + + it('should validate RTB endpoint', () => { + expect(spec.isBidRequestValid({params: {supplyPartnerId: '123'}})).to.equal(true); // RTB endpoint has a default + expect(spec.isBidRequestValid({params: {supplyPartnerId: '123', rtbEndpoint: 123}})).to.equal(false); + expect(spec.isBidRequestValid({ + params: { + supplyPartnerId: '123', + rtbEndpoint: 'https://some.url.com' + } + })).to.equal(true); + }); + + it('should validate bid floor', () => { + expect(spec.isBidRequestValid({params: {supplyPartnerId: '123'}})).to.equal(true); // bidfloor has a default + expect(spec.isBidRequestValid({params: {supplyPartnerId: '123', bidfloor: '123'}})).to.equal(false); + expect(spec.isBidRequestValid({params: {supplyPartnerId: '123', bidfloor: 0.1}})).to.equal(true); + + const getFloorResponse = {currency: 'USD', floor: 5}; + let testBidRequest = deepClone(bidRequest); + let request = spec.buildRequests([testBidRequest], bidRequest)[0]; + + // 1. getBidFloor not exist AND bidfloor not exist - return 0 + let payload = request.data; + expect(payload.imp[0].bidfloor).to.exist.and.equal(0); + + // 2. getBidFloor not exist AND bidfloor exist - use bidfloor property + testBidRequest = deepClone(bidRequest); + testBidRequest.params = { + 'bidfloor': 0.3 + }; + request = spec.buildRequests([testBidRequest], bidRequest)[0]; + payload = request.data; + expect(payload.imp[0].bidfloor).to.exist.and.to.equal(0.3) + + // 3. getBidFloor exist AND bidfloor not exist - use getFloor method + testBidRequest = deepClone(bidRequest); + testBidRequest.getFloor = () => getFloorResponse; + request = spec.buildRequests([testBidRequest], bidRequest)[0]; + payload = request.data; + expect(payload.imp[0].bidfloor).to.exist.and.to.equal(5) + + // 4. getBidFloor exist AND bidfloor exist -> use getFloor method + testBidRequest = deepClone(bidRequest); + testBidRequest.getFloor = () => getFloorResponse; + testBidRequest.params = { + 'bidfloor': 0.3 + }; + request = spec.buildRequests([testBidRequest], bidRequest)[0]; + payload = request.data; + expect(payload.imp[0].bidfloor).to.exist.and.to.equal(5) }); - it('should validate adpos', function() { - expect( - spec.isBidRequestValid({ params: { supplyPartnerId: '123' } }) - ).to.equal(true); // adpos has a default - expect( - spec.isBidRequestValid({ - params: { supplyPartnerId: '123', adpos: '123' } - }) - ).to.equal(false); - expect( - spec.isBidRequestValid({ - params: { supplyPartnerId: '123', adpos: 0.1 } - }) - ).to.equal(true); + it('should validate adpos', () => { + expect(spec.isBidRequestValid({params: {supplyPartnerId: '123'}})).to.equal(true); // adpos has a default + expect(spec.isBidRequestValid({params: {supplyPartnerId: '123', adpos: '123'}})).to.equal(false); + expect(spec.isBidRequestValid({params: {supplyPartnerId: '123', adpos: 0.1}})).to.equal(true); }); - it('should validate instl', function() { - expect( - spec.isBidRequestValid({ params: { supplyPartnerId: '123' } }) - ).to.equal(true); // adpos has a default - expect( - spec.isBidRequestValid({ - params: { supplyPartnerId: '123', instl: '123' } - }) - ).to.equal(false); - expect( - spec.isBidRequestValid({ - params: { supplyPartnerId: '123', instl: -1 } - }) - ).to.equal(false); - expect( - spec.isBidRequestValid({ params: { supplyPartnerId: '123', instl: 0 } }) - ).to.equal(true); - expect( - spec.isBidRequestValid({ params: { supplyPartnerId: '123', instl: 1 } }) - ).to.equal(true); - expect( - spec.isBidRequestValid({ params: { supplyPartnerId: '123', instl: 2 } }) - ).to.equal(false); + it('should validate instl', () => { + expect(spec.isBidRequestValid({params: {supplyPartnerId: '123'}})).to.equal(true); // adpos has a default + expect(spec.isBidRequestValid({params: {supplyPartnerId: '123', instl: '123'}})).to.equal(false); + expect(spec.isBidRequestValid({params: {supplyPartnerId: '123', instl: -1}})).to.equal(false); + expect(spec.isBidRequestValid({params: {supplyPartnerId: '123', instl: 0}})).to.equal(true); + expect(spec.isBidRequestValid({params: {supplyPartnerId: '123', instl: 1}})).to.equal(true); + expect(spec.isBidRequestValid({params: {supplyPartnerId: '123', instl: 2}})).to.equal(false); }); }); - describe('buildRequests', function() { - const bidRequest = { - adUnitCode: 'adunit-code', - auctionId: '1d1a030790a475', - mediaTypes: { - banner: {} - }, - params: { - supplyPartnerId: supplyPartnerId - }, - sizes: [[300, 250], [300, 600]], - transactionId: 'a123456789', - refererInfo: { referer: 'https://examplereferer.com', domain: 'examplereferer.com' }, - gdprConsent: { - consentString: 'some string', - gdprApplies: true - } - }; - it('returns an array', function() { + describe('buildRequests', () => { + it('returns an array', () => { let response; response = spec.buildRequests([]); expect(Array.isArray(response)).to.equal(true); @@ -99,58 +298,61 @@ describe('CleanmedianetAdapter', function () { response = spec.buildRequests([bidRequest], bidRequest); expect(Array.isArray(response)).to.equal(true); expect(response.length).to.equal(1); - const adUnit1 = Object.assign({}, utils.deepClone(bidRequest), { - auctionId: '1', - adUnitCode: 'a' - }); - const adUnit2 = Object.assign({}, utils.deepClone(bidRequest), { - auctionId: '1', - adUnitCode: 'b' - }); + const adUnit1 = Object.assign({}, utils.deepClone(bidRequest), {auctionId: '1', adUnitCode: 'a'}); + const adUnit2 = Object.assign({}, utils.deepClone(bidRequest), {auctionId: '1', adUnitCode: 'b'}); response = spec.buildRequests([adUnit1, adUnit2], bidRequest); expect(Array.isArray(response)).to.equal(true); expect(response.length).to.equal(2); }); - it('builds request correctly', function() { + it('targets correct endpoint', () => { + let response; + response = spec.buildRequests([bidRequest], bidRequest)[0]; + expect(response.method).to.equal('POST'); + expect(response.url).to.match(new RegExp(`^https://bidder\\.cleanmediaads\\.com/r/${supplyPartnerId}/bidr\\?rformat=open_rtb&reqformat=rtb_json&bidder=prebid$`, 'g')); + expect(response.data.id).to.equal(bidRequest.auctionId); + const bidRequestWithEndpoint = utils.deepClone(bidRequest); + bidRequestWithEndpoint.params.rtbEndpoint = 'https://bidder.cleanmediaads.com/a12'; + response = spec.buildRequests([bidRequestWithEndpoint], bidRequest)[0]; + expect(response.url).to.match(new RegExp(`^https://bidder\\.cleanmediaads\\.com/a12/r/${supplyPartnerId}/bidr\\?rformat=open_rtb&reqformat=rtb_json&bidder=prebid$`, 'g')); + }); + + it('builds request correctly', () => { let bidRequest2 = utils.deepClone(bidRequest); Object.assign(bidRequest2.refererInfo, { - page: 'https://www.test.com/page.html', - domain: 'test.com', - ref: 'https://referer.com' + page: 'http://www.test.com/page.html', + domain: 'www.test.com', + ref: 'http://referrer.com' }) - let response = spec.buildRequests([bidRequest], bidRequest2)[0]; - expect(response.data.site.domain).to.equal('test.com'); - expect(response.data.site.page).to.equal('https://www.test.com/page.html'); - expect(response.data.site.ref).to.equal('https://referer.com'); + + expect(response.data.site.domain).to.equal('www.test.com'); + expect(response.data.site.page).to.equal('http://www.test.com/page.html'); + expect(response.data.site.ref).to.equal('http://referrer.com'); expect(response.data.imp.length).to.equal(1); expect(response.data.imp[0].id).to.equal(bidRequest.transactionId); expect(response.data.imp[0].instl).to.equal(0); expect(response.data.imp[0].tagid).to.equal(bidRequest.adUnitCode); expect(response.data.imp[0].bidfloor).to.equal(0); expect(response.data.imp[0].bidfloorcur).to.equal('USD'); + expect(response.data.regs.ext.us_privacy).to.equal('cleanmediaCCPA');// USP/CCPAs + expect(response.data.source.ext.schain).to.deep.equal(bidRequest2.schain); + const bidRequestWithInstlEquals1 = utils.deepClone(bidRequest); bidRequestWithInstlEquals1.params.instl = 1; - response = spec.buildRequests( - [bidRequestWithInstlEquals1], - bidRequest2 - )[0]; - expect(response.data.imp[0].instl).to.equal( - bidRequestWithInstlEquals1.params.instl - ); + response = spec.buildRequests([bidRequestWithInstlEquals1], bidRequest2)[0]; + expect(response.data.imp[0].instl).to.equal(bidRequestWithInstlEquals1.params.instl); const bidRequestWithInstlEquals0 = utils.deepClone(bidRequest); bidRequestWithInstlEquals0.params.instl = 1; - response = spec.buildRequests( - [bidRequestWithInstlEquals0], - bidRequest2 - )[0]; - expect(response.data.imp[0].instl).to.equal( - bidRequestWithInstlEquals0.params.instl - ); + response = spec.buildRequests([bidRequestWithInstlEquals0], bidRequest2)[0]; + expect(response.data.imp[0].instl).to.equal(bidRequestWithInstlEquals0.params.instl); + const bidRequestWithBidfloorEquals1 = utils.deepClone(bidRequest); + bidRequestWithBidfloorEquals1.params.bidfloor = 1; + response = spec.buildRequests([bidRequestWithBidfloorEquals1], bidRequest2)[0]; + expect(response.data.imp[0].bidfloor).to.equal(bidRequestWithBidfloorEquals1.params.bidfloor); }); - it('builds request banner object correctly', function() { + it('builds request banner object correctly', () => { let response; const bidRequestWithBanner = utils.deepClone(bidRequest); bidRequestWithBanner.mediaTypes = { @@ -159,54 +361,80 @@ describe('CleanmedianetAdapter', function () { } }; response = spec.buildRequests([bidRequestWithBanner], bidRequest)[0]; - expect(response.data.imp[0].banner.w).to.equal( - bidRequestWithBanner.mediaTypes.banner.sizes[0][0] - ); - expect(response.data.imp[0].banner.h).to.equal( - bidRequestWithBanner.mediaTypes.banner.sizes[0][1] - ); + expect(response.data.imp[0].banner.w).to.equal(bidRequestWithBanner.mediaTypes.banner.sizes[0][0]); + expect(response.data.imp[0].banner.h).to.equal(bidRequestWithBanner.mediaTypes.banner.sizes[0][1]); expect(response.data.imp[0].banner.pos).to.equal(0); + expect(response.data.imp[0].banner.topframe).to.equal(0); const bidRequestWithPosEquals1 = utils.deepClone(bidRequestWithBanner); bidRequestWithPosEquals1.params.pos = 1; response = spec.buildRequests([bidRequestWithPosEquals1], bidRequest)[0]; - expect(response.data.imp[0].banner.pos).to.equal( - bidRequestWithPosEquals1.params.pos - ); + expect(response.data.imp[0].banner.pos).to.equal(bidRequestWithPosEquals1.params.pos); }); - it('builds request video object correctly', function() { + it('builds request video object correctly', () => { let response; const bidRequestWithVideo = utils.deepClone(bidRequest); + + bidRequestWithVideo.params.video = { + placement: 1, + minduration: 1, + } + bidRequestWithVideo.mediaTypes = { video: { - sizes: [[300, 250], [120, 600]] + playerSize: [[302, 252]], + mimes: ['video/mpeg'], + playbackmethod: 1, + startdelay: 1, } }; response = spec.buildRequests([bidRequestWithVideo], bidRequest)[0]; - expect(response.data.imp[0].video.w).to.equal( - bidRequestWithVideo.mediaTypes.video.sizes[0][0] - ); - expect(response.data.imp[0].video.h).to.equal( - bidRequestWithVideo.mediaTypes.video.sizes[0][1] - ); + expect(response.data.imp[0].video.w).to.equal(bidRequestWithVideo.mediaTypes.video.playerSize[0][0]); + expect(response.data.imp[0].video.h).to.equal(bidRequestWithVideo.mediaTypes.video.playerSize[0][1]); expect(response.data.imp[0].video.pos).to.equal(0); + + expect(response.data.imp[0].video.mimes).to.equal(bidRequestWithVideo.mediaTypes.video.mimes); + expect(response.data.imp[0].video.skip).to.not.exist; + expect(response.data.imp[0].video.placement).to.equal(1); + expect(response.data.imp[0].video.minduration).to.equal(1); + expect(response.data.imp[0].video.playbackmethod).to.equal(1); + expect(response.data.imp[0].video.startdelay).to.equal(1); + + bidRequestWithVideo.mediaTypes = { + video: { + playerSize: [302, 252], + mimes: ['video/mpeg'], + skip: 1, + placement: 1, + minduration: 1, + playbackmethod: 1, + startdelay: 1, + }, + }; + const bidRequestWithPosEquals1 = utils.deepClone(bidRequestWithVideo); + expect(response.data.imp[0].video.w).to.equal(bidRequestWithVideo.mediaTypes.video.playerSize[0]); + expect(response.data.imp[0].video.h).to.equal(bidRequestWithVideo.mediaTypes.video.playerSize[1]); + bidRequestWithPosEquals1.params.pos = 1; response = spec.buildRequests([bidRequestWithPosEquals1], bidRequest)[0]; - expect(response.data.imp[0].video.pos).to.equal( - bidRequestWithPosEquals1.params.pos - ); + expect(response.data.imp[0].video.pos).to.equal(bidRequestWithPosEquals1.params.pos); }); - it('builds request video object correctly with context', function() { - let response; + it('builds request video object correctly with context', () => { const bidRequestWithVideo = utils.deepClone(bidRequest); bidRequestWithVideo.mediaTypes = { video: { - context: 'instream' + context: 'instream', + mimes: ['video/mpeg'], + skip: 1, + placement: 1, + minduration: 1, + playbackmethod: 1, + startdelay: 1, } }; - response = spec.buildRequests([bidRequestWithVideo], bidRequest)[0]; + let response = spec.buildRequests([bidRequestWithVideo], bidRequest)[0]; expect(response.data.imp[0].video.ext.context).to.equal('instream'); bidRequestWithVideo.mediaTypes.video.context = 'outstream'; @@ -220,358 +448,185 @@ describe('CleanmedianetAdapter', function () { response = spec.buildRequests([bidRequestWithPosEquals2], bidRequest)[0]; expect(response.data.imp[0].video.ext.context).to.equal(null); }); - it('builds request video object correctly with multi-dimensions size array', function () { - let bidRequestWithVideo = utils.deepClone(bidRequest); + + it('builds request video object correctly with multi-dimensions size array', () => { + let response; + const bidRequestWithVideo = utils.deepClone(bidRequest); bidRequestWithVideo.mediaTypes.video = { playerSize: [[304, 254], [305, 255]], - context: 'instream' + context: 'instream', + mimes: ['video/mpeg'], + skip: 1, + placement: 1, + minduration: 1, + playbackmethod: 1, + startdelay: 1, }; - let response = spec.buildRequests([bidRequestWithVideo], bidRequest)[0]; - expect(response.data.imp[1].video.w).to.equal(304); - expect(response.data.imp[1].video.h).to.equal(254); + response = spec.buildRequests([bidRequestWithVideo], bidRequest)[0]; + expect(response.data.imp[1].video.ext.context).to.equal('instream'); + bidRequestWithVideo.mediaTypes.video.context = 'outstream'; - bidRequestWithVideo = utils.deepClone(bidRequest); - bidRequestWithVideo.mediaTypes.video = { - playerSize: [304, 254] - }; + const bidRequestWithPosEquals1 = utils.deepClone(bidRequestWithVideo); + bidRequestWithPosEquals1.mediaTypes.video.context = 'outstream'; + response = spec.buildRequests([bidRequestWithPosEquals1], bidRequest)[0]; + expect(response.data.imp[1].video.ext.context).to.equal('outstream'); - response = spec.buildRequests([bidRequestWithVideo], bidRequest)[0]; - expect(response.data.imp[1].video.w).to.equal(304); - expect(response.data.imp[1].video.h).to.equal(254); + const bidRequestWithPosEquals2 = utils.deepClone(bidRequestWithVideo); + bidRequestWithPosEquals2.mediaTypes.video.context = null; + response = spec.buildRequests([bidRequestWithPosEquals2], bidRequest)[0]; + expect(response.data.imp[1].video.ext.context).to.equal(null); }); - it('builds request with gdpr consent', function() { + it('builds request with gdpr consent', () => { let response = spec.buildRequests([bidRequest], bidRequest)[0]; + + expect(response.data.ext.gdpr_consent).to.not.equal(null).and.not.equal(undefined); expect(response.data.ext).to.have.property('gdpr_consent'); - expect(response.data.ext.gdpr_consent.consent_string).to.equal( - 'some string' - ); + expect(response.data.ext.gdpr_consent.consent_string).to.equal('some string'); expect(response.data.ext.gdpr_consent.consent_required).to.equal(true); - }); - }); - - describe('interpretResponse', function() { - const bannerBidRequest = { - adUnitCode: 'adunit-code', - auctionId: '1d1a030790a475', - mediaTypes: { - banner: {} - }, - params: { - supplyPartnerId: supplyPartnerId - }, - sizes: [[300, 250], [300, 600]], - transactionId: 'a123456789', - bidId: '111', - refererInfo: { referer: 'https://examplereferer.com' } - }; - const videoBidRequest = { - adUnitCode: 'adunit-code', - auctionId: '1d1a030790a475', - mediaTypes: { - video: {} - }, - params: { - supplyPartnerId: supplyPartnerId - }, - sizes: [[300, 250], [300, 600]], - transactionId: 'a123456789', - bidId: '111', - refererInfo: { referer: 'https://examplereferer.com' } - }; + expect(response.data.regs.ext.gdpr).to.not.equal(null).and.not.equal(undefined); + expect(response.data.user.ext.consent).to.equal('some string'); + }); - const rtbResponse = { - id: 'imp_5b05b9fde4b09084267a556f', - bidid: 'imp_5b05b9fde4b09084267a556f', - cur: 'USD', - ext: { - utrk: [ - { type: 'iframe', url: '//bidder.cleanmediaads.com/user/sync/1' }, - { type: 'image', url: '//bidder.cleanmediaads.com/user/sync/2' } - ] - }, - seatbid: [ - { - seat: 'seat1', - group: 0, - bid: [ - { - id: '0', - impid: '1', - price: 2.016, - adid: '579ef31bfa788b9d2000d562', - nurl: - 'https://bidder.cleanmediaads.com/pix/monitoring/win_notice/imp_5b05b9fde4b09084267a556f/im.gif?r=imp_5b05b9fde4b09084267a556f&i=1&a=579ef31bfa788b9d2000d562&b=0', - adm: - '', - adomain: ['aaa.com'], - cid: '579ef268fa788b9d2000d55c', - crid: '579ef31bfa788b9d2000d562', - attr: [], - h: 600, - w: 120, - ext: { - vast_url: 'https://my.vast.com', - utrk: [{ type: 'iframe', url: '//p.partner1.io/user/sync/1' }] - } - } - ] - }, - { - seat: 'seat2', - group: 0, - bid: [ - { - id: '1', - impid: '1', - price: 3, - adid: '542jlhdfd2112jnjf3x', - nurl: - 'https://bidder.cleanmediaads.com/pix/monitoring/win_notice/imp_5b05b9fde4b09084267a556f/im.gif?r=imp_5b05b9fde4b09084267a556f&i=1&a=579ef31bfa788b9d2000d562&b=0', - adm: - ' ', - adomain: ['bbb.com'], - cid: 'fgdlwjh2498ydjhg1', - crid: 'kjh34297ydh2133d', - attr: [], - h: 250, - w: 300, - ext: { - utrk: [{ type: 'image', url: '//p.partner2.io/user/sync/1' }] - } - } - ] - } - ] - }; + it('build request with ID5 Id', () => { + const bidRequestClone = utils.deepClone(bidRequest); + bidRequestClone.userId = {}; + bidRequestClone.userId.id5id = { uid: 'id5-user-id' }; + let request = spec.buildRequests([bidRequestClone], bidRequestClone)[0]; + expect(request.data.user.ext.eids).to.deep.equal([{ + 'source': 'id5-sync.com', + 'uids': [{ + 'id': 'id5-user-id', + 'ext': { + 'rtiPartner': 'ID5ID' + } + }] + }]); + }); - it('returns an empty array on missing response', function() { - let response; + it('build request with unified Id', () => { + const bidRequestClone = utils.deepClone(bidRequest); + bidRequestClone.userId = {}; + bidRequestClone.userId.tdid = 'tdid-user-id'; + let request = spec.buildRequests([bidRequestClone], bidRequestClone)[0]; + expect(request.data.user.ext.eids).to.deep.equal([{ + 'source': 'adserver.org', + 'uids': [{ + 'id': 'tdid-user-id', + 'ext': { + 'rtiPartner': 'TDID' + } + }] + }]); + }); + }); - response = spec.interpretResponse(undefined, { - bidRequest: bannerBidRequest - }); + describe('interpretResponse', () => { + it('returns an empty array on missing response', () => { + let response = spec.interpretResponse(undefined, {bidRequest: bannerBidRequest}); expect(Array.isArray(response)).to.equal(true); expect(response.length).to.equal(0); - response = spec.interpretResponse({}, { bidRequest: bannerBidRequest }); + response = spec.interpretResponse({}, {bidRequest: bannerBidRequest}); expect(Array.isArray(response)).to.equal(true); expect(response.length).to.equal(0); }); - it('aggregates banner bids from all seat bids', function() { - const response = spec.interpretResponse( - { body: rtbResponse }, - { bidRequest: bannerBidRequest } - ); + it('aggregates banner bids from all seat bids', () => { + const response = spec.interpretResponse({body: rtbResponse}, {bidRequest: bannerBidRequest}); expect(Array.isArray(response)).to.equal(true); expect(response.length).to.equal(1); - const ad0 = response[0]; expect(ad0.requestId).to.equal(bannerBidRequest.bidId); expect(ad0.cpm).to.equal(rtbResponse.seatbid[1].bid[0].price); expect(ad0.width).to.equal(rtbResponse.seatbid[1].bid[0].w); expect(ad0.height).to.equal(rtbResponse.seatbid[1].bid[0].h); - expect(ad0.ttl).to.equal(360); + expect(ad0.ttl).to.equal(TTL); expect(ad0.creativeId).to.equal(rtbResponse.seatbid[1].bid[0].crid); expect(ad0.netRevenue).to.equal(true); - expect(ad0.currency).to.equal( - rtbResponse.seatbid[1].bid[0].cur || rtbResponse.cur || 'USD' - ); + expect(ad0.currency).to.equal(rtbResponse.seatbid[1].bid[0].cur || rtbResponse.cur || 'USD'); expect(ad0.ad).to.equal(rtbResponse.seatbid[1].bid[0].adm); expect(ad0.vastXml).to.be.an('undefined'); expect(ad0.vastUrl).to.be.an('undefined'); + expect(ad0.meta.advertiserDomains).to.be.equal(rtbResponse.seatbid[1].bid[0].adomain); }); - it('aggregates video bids from all seat bids', function() { - const response = spec.interpretResponse( - { body: rtbResponse }, - { bidRequest: videoBidRequest } - ); + it('aggregates video bids from all seat bids', () => { + const response = spec.interpretResponse({body: rtbResponse}, {bidRequest: videoBidRequest}); expect(Array.isArray(response)).to.equal(true); expect(response.length).to.equal(1); - const ad0 = response[0]; expect(ad0.requestId).to.equal(videoBidRequest.bidId); expect(ad0.cpm).to.equal(rtbResponse.seatbid[0].bid[0].price); expect(ad0.width).to.equal(rtbResponse.seatbid[0].bid[0].w); expect(ad0.height).to.equal(rtbResponse.seatbid[0].bid[0].h); - expect(ad0.ttl).to.equal(360); + expect(ad0.ttl).to.equal(TTL); expect(ad0.creativeId).to.equal(rtbResponse.seatbid[0].bid[0].crid); expect(ad0.netRevenue).to.equal(true); - expect(ad0.currency).to.equal( - rtbResponse.seatbid[0].bid[0].cur || rtbResponse.cur || 'USD' - ); + expect(ad0.currency).to.equal(rtbResponse.seatbid[0].bid[0].cur || rtbResponse.cur || 'USD'); expect(ad0.ad).to.be.an('undefined'); expect(ad0.vastXml).to.equal(rtbResponse.seatbid[0].bid[0].adm); expect(ad0.vastUrl).to.equal(rtbResponse.seatbid[0].bid[0].ext.vast_url); }); - it('aggregates user-sync pixels', function() { - const response = spec.getUserSyncs({}, [{ body: rtbResponse }]); + it('aggregates user-sync pixels', () => { + const response = spec.getUserSyncs({}, [{body: rtbResponse}]); expect(Array.isArray(response)).to.equal(true); expect(response.length).to.equal(4); expect(response[0].type).to.equal(rtbResponse.ext.utrk[0].type); - expect(response[0].url).to.equal( - rtbResponse.ext.utrk[0].url + '?gc=missing' - ); + expect(response[0].url).to.equal('//bidder.cleanmediaads.com/user/sync/1?gdpr=0&consent=&usp='); expect(response[1].type).to.equal(rtbResponse.ext.utrk[1].type); - expect(response[1].url).to.equal( - rtbResponse.ext.utrk[1].url + '?gc=missing' - ); - expect(response[2].type).to.equal( - rtbResponse.seatbid[0].bid[0].ext.utrk[0].type - ); - expect(response[2].url).to.equal( - rtbResponse.seatbid[0].bid[0].ext.utrk[0].url + '?gc=missing' - ); - expect(response[3].type).to.equal( - rtbResponse.seatbid[1].bid[0].ext.utrk[0].type - ); - expect(response[3].url).to.equal( - rtbResponse.seatbid[1].bid[0].ext.utrk[0].url + '?gc=missing' - ); + expect(response[1].url).to.equal('//bidder.cleanmediaads.com/user/sync/2'); + expect(response[2].type).to.equal(rtbResponse.seatbid[0].bid[0].ext.utrk[0].type); + expect(response[2].url).to.equal('//p.partner1.io/user/sync/1'); + expect(response[3].type).to.equal(rtbResponse.seatbid[1].bid[0].ext.utrk[0].type); + expect(response[3].url).to.equal('//p.partner2.io/user/sync/1'); }); - it('supports configuring outstream renderers', function() { - const videoResponse = { - id: '64f32497-b2f7-48ec-9205-35fc39894d44', - bidid: 'imp_5c24924de4b0d106447af333', - cur: 'USD', - seatbid: [ - { - seat: '3668', - group: 0, - bid: [ - { - id: 'gb_1', - impid: 'afbb5852-7cea-4a81-aa9a-a41aab505c23', - price: 5.0, - adid: '1274', - nurl: - 'https://bidder.cleanmediaads.com/pix/1275/win_notice/imp_5c24924de4b0d106447af333/im.gif?r=imp_5c24924de4b0d106447af333&i=afbb5852-7cea-4a81-aa9a-a41aab505c23&a=1274&b=gb_1', - adomain: [], - adm: - '\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n', - cid: '3668', - crid: '1274', - cat: [], - attr: [], - h: 250, - w: 300, - ext: { - vast_url: - 'https://bidder.cleanmediaads.com/pix/1275/vast_o/imp_5c24924de4b0d106447af333/im.xml?r=imp_5c24924de4b0d106447af333&i=afbb5852-7cea-4a81-aa9a-a41aab505c23&a=1274&b=gb_1&w=300&h=250&vatu=aHR0cHM6Ly9zdGF0aWMuZ2FtYmlkLmlvL2RlbW8vdmFzdC54bWw&vwarv', - imptrackers: [ - 'https://bidder.cleanmediaads.com/pix/1275/imp/imp_5c24924de4b0d106447af333/im.gif?r=imp_5c24924de4b0d106447af333&i=afbb5852-7cea-4a81-aa9a-a41aab505c23&a=1274&b=gb_1' - ] - } - } - ] - } - ], - ext: { - utrk: [ - { - type: 'image', - url: - 'https://bidder.cleanmediaads.com/pix/1275/scm?cb=1545900621675' - } - ] - } - }; + it('supports configuring outstream renderers', () => { const videoRequest = utils.deepClone(videoBidRequest); videoRequest.mediaTypes.video.context = 'outstream'; - const result = spec.interpretResponse( - { body: videoResponse }, - { bidRequest: videoRequest } - ); + const result = spec.interpretResponse({body: videoResponse}, {bidRequest: videoRequest}); expect(result[0].renderer).to.not.equal(undefined); }); - it('validates in/existing of gdpr consent', function() { - let videoResponse = { - id: '64f32497-b2f7-48ec-9205-35fc39894d44', - bidid: 'imp_5c24924de4b0d106447af333', - cur: 'USD', - seatbid: [ - { - seat: '3668', - group: 0, - bid: [ - { - id: 'gb_1', - impid: 'afbb5852-7cea-4a81-aa9a-a41aab505c23', - price: 5.0, - adid: '1274', - nurl: - 'https://bidder.cleanmediaads.com/pix/1275/win_notice/imp_5c24924de4b0d106447af333/im.gif?r=imp_5c24924de4b0d106447af333&i=afbb5852-7cea-4a81-aa9a-a41aab505c23&a=1274&b=gb_1', - adomain: [], - adm: - '\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n', - cid: '3668', - crid: '1274', - cat: [], - attr: [], - h: 250, - w: 300, - ext: { - vast_url: - 'https://bidder.cleanmediaads.com/pix/1275/vast_o/imp_5c24924de4b0d106447af333/im.xml?r=imp_5c24924de4b0d106447af333&i=afbb5852-7cea-4a81-aa9a-a41aab505c23&a=1274&b=gb_1&w=300&h=250&vatu=aHR0cHM6Ly9zdGF0aWMuZ2FtYmlkLmlvL2RlbW8vdmFzdC54bWw&vwarv', - imptrackers: [ - 'https://bidder.cleanmediaads.com/pix/1275/imp/imp_5c24924de4b0d106447af333/im.gif?r=imp_5c24924de4b0d106447af333&i=afbb5852-7cea-4a81-aa9a-a41aab505c23&a=1274&b=gb_1' - ] - } - } - ] - } - ], - ext: { - utrk: [ - { - type: 'image', - url: - 'https://bidder.cleanmediaads.com/pix/1275/scm?cb=1545900621675' - } - ] - } - }; - let gdprConsent = { - gdprApplies: true, - consentString: 'consent string' - }; - let result = spec.getUserSyncs( - {}, - [{ body: videoResponse }], - gdprConsent - ); + it('validates in/existing of gdpr consent', () => { + let result = spec.getUserSyncs({}, [{body: videoResponse}], gdprConsent, 'cleanmediaCCPA'); expect(result).to.be.an('array'); expect(result.length).to.equal(1); expect(result[0].type).to.equal('image'); - expect(result[0].url).to.equal( - 'https://bidder.cleanmediaads.com/pix/1275/scm?cb=1545900621675&gc=consent%20string' - ); + expect(result[0].url).to.equal('https://bidder.cleanmediaads.com/pix/1275/scm?cb=1545900621675&gdpr=1&consent=consent%20string&us_privacy=cleanmediaCCPA'); gdprConsent.gdprApplies = false; - result = spec.getUserSyncs({}, [{ body: videoResponse }], gdprConsent); + result = spec.getUserSyncs({}, [{body: videoResponse}], gdprConsent, 'cleanmediaCCPA'); expect(result).to.be.an('array'); expect(result.length).to.equal(1); expect(result[0].type).to.equal('image'); - expect(result[0].url).to.equal( - 'https://bidder.cleanmediaads.com/pix/1275/scm?cb=1545900621675&gc=missing' - ); + expect(result[0].url).to.equal('https://bidder.cleanmediaads.com/pix/1275/scm?cb=1545900621675&gdpr=0&consent=&us_privacy=cleanmediaCCPA'); - videoResponse.ext.utrk[0].url = - 'https://bidder.cleanmediaads.com/pix/1275/scm'; - result = spec.getUserSyncs({}, [{ body: videoResponse }], gdprConsent); + videoResponse.ext.utrk[0].url = 'https://bidder.cleanmediaads.com/pix/1275/scm'; + result = spec.getUserSyncs({}, [{body: videoResponse}], gdprConsent); + expect(result).to.be.an('array'); + expect(result.length).to.equal(1); + expect(result[0].type).to.equal('image'); + expect(result[0].url).to.equal('https://bidder.cleanmediaads.com/pix/1275/scm'); + }); + + it('validates existence of gdpr, gdpr consent and usp consent', () => { + let result = spec.getUserSyncs({}, [{body: videoResponse}], gdprConsent, 'cleanmediaCCPA'); + expect(result).to.be.an('array'); + expect(result.length).to.equal(1); + expect(result[0].type).to.equal('image'); + expect(result[0].url).to.equal('https://bidder.cleanmediaads.com/pix/1275/scm?cb=1545900621675&gdpr=1&consent=consent%20string&us_privacy=cleanmediaCCPA'); + + gdprConsent.gdprApplies = false; + result = spec.getUserSyncs({}, [{body: videoResponse}], gdprConsent, ''); expect(result).to.be.an('array'); expect(result.length).to.equal(1); expect(result[0].type).to.equal('image'); - expect(result[0].url).to.equal( - 'https://bidder.cleanmediaads.com/pix/1275/scm?gc=missing' - ); + expect(result[0].url).to.equal('https://bidder.cleanmediaads.com/pix/1275/scm?cb=1545900621675&gdpr=0&consent=&us_privacy='); }); }); }); diff --git a/test/spec/modules/cointrafficBidAdapter_spec.js b/test/spec/modules/cointrafficBidAdapter_spec.js index 24570de5083..79775f7b135 100644 --- a/test/spec/modules/cointrafficBidAdapter_spec.js +++ b/test/spec/modules/cointrafficBidAdapter_spec.js @@ -1,13 +1,15 @@ +import sinon from 'sinon' import { expect } from 'chai'; import { spec } from 'modules/cointrafficBidAdapter.js'; import { config } from 'src/config.js' import * as utils from 'src/utils.js' -const ENDPOINT_URL = 'https://apps-pbd.ctengine.io/pb/tmp'; +const ENDPOINT_URL = 'https://apps-pbd.ctraffic.io/pb/tmp'; describe('cointrafficBidAdapter', function () { describe('isBidRequestValid', function () { - let bid = { + /** @type {BidRequest} */ + let bidRequest = { bidder: 'cointraffic', params: { placementId: 'testPlacementId' @@ -22,11 +24,12 @@ describe('cointrafficBidAdapter', function () { }; it('should return true where required params found', function () { - expect(spec.isBidRequestValid(bid)).to.equal(true); + expect(spec.isBidRequestValid(bidRequest)).to.equal(true); }); }); describe('buildRequests', function () { + /** @type {BidRequest[]} */ let bidRequests = [ { bidder: 'cointraffic', @@ -56,7 +59,8 @@ describe('cointrafficBidAdapter', function () { } ]; - let bidderRequests = { + /** @type {BidderRequest} */ + let bidderRequest = { refererInfo: { numIframes: 0, reachedTop: true, @@ -68,7 +72,7 @@ describe('cointrafficBidAdapter', function () { }; it('replaces currency with EUR if there is no currency provided', function () { - const request = spec.buildRequests(bidRequests, bidderRequests); + const request = spec.buildRequests(bidRequests, bidderRequest); expect(request[0].data.currency).to.equal('EUR'); expect(request[1].data.currency).to.equal('EUR'); @@ -79,7 +83,7 @@ describe('cointrafficBidAdapter', function () { arg => arg === 'currency.bidderCurrencyDefault.cointraffic' ? 'USD' : 'EUR' ); - const request = spec.buildRequests(bidRequests, bidderRequests); + const request = spec.buildRequests(bidRequests, bidderRequest); expect(request[0].data.currency).to.equal('USD'); expect(request[1].data.currency).to.equal('USD'); @@ -93,7 +97,7 @@ describe('cointrafficBidAdapter', function () { arg => arg === 'currency.bidderCurrencyDefault.cointraffic' ? 'BTC' : 'EUR' ); - const request = spec.buildRequests(bidRequests, bidderRequests); + const request = spec.buildRequests(bidRequests, bidderRequest); expect(request[0]).to.undefined; expect(request[1]).to.undefined; @@ -103,14 +107,14 @@ describe('cointrafficBidAdapter', function () { }); it('sends bid request to our endpoint via POST', function () { - const request = spec.buildRequests(bidRequests, bidderRequests); + const request = spec.buildRequests(bidRequests, bidderRequest); expect(request[0].method).to.equal('POST'); expect(request[1].method).to.equal('POST'); }); it('attaches source and version to endpoint URL as query params', function () { - const request = spec.buildRequests(bidRequests, bidderRequests); + const request = spec.buildRequests(bidRequests, bidderRequest); expect(request[0].url).to.equal(ENDPOINT_URL); expect(request[1].url).to.equal(ENDPOINT_URL); @@ -119,6 +123,7 @@ describe('cointrafficBidAdapter', function () { describe('interpretResponse', function () { it('should get the correct bid response', function () { + /** @type {BidRequest[]} */ let bidRequest = [{ method: 'POST', url: ENDPOINT_URL, @@ -142,7 +147,7 @@ describe('cointrafficBidAdapter', function () { height: 250, creativeId: 'creativeId12345', ttl: 90, - ad: '

I am an ad

', + ad: '

I am an ad

', mediaType: 'banner', adomain: ['test.com'] } @@ -157,7 +162,7 @@ describe('cointrafficBidAdapter', function () { height: 250, creativeId: 'creativeId12345', ttl: 90, - ad: '

I am an ad

', + ad: '

I am an ad

', meta: { mediaType: 'banner', advertiserDomains: [ @@ -170,7 +175,58 @@ describe('cointrafficBidAdapter', function () { expect(Object.keys(result)).to.deep.equal(Object.keys(expectedResponse)); }); + it('should get the correct bid response without advertiser domains specified', function () { + /** @type {BidRequest[]} */ + let bidRequest = [{ + method: 'POST', + url: ENDPOINT_URL, + data: { + placementId: 'testPlacementId', + device: 'desktop', + currency: 'EUR', + sizes: ['300x250'], + bidId: 'bidId12345', + referer: 'www.example.com' + } + }]; + + let serverResponse = { + body: { + requestId: 'bidId12345', + cpm: 3.9, + currency: 'EUR', + netRevenue: true, + width: 300, + height: 250, + creativeId: 'creativeId12345', + ttl: 90, + ad: '

I am an ad

', + mediaType: 'banner', + } + }; + + let expectedResponse = [{ + requestId: 'bidId12345', + cpm: 3.9, + currency: 'EUR', + netRevenue: true, + width: 300, + height: 250, + creativeId: 'creativeId12345', + ttl: 90, + ad: '

I am an ad

', + meta: { + mediaType: 'banner', + advertiserDomains: [] + } + }]; + + let result = spec.interpretResponse(serverResponse, bidRequest[0]); + expect(Object.keys(result)).to.deep.equal(Object.keys(expectedResponse)); + }); + it('should get the correct bid response with different currency', function () { + /** @type {BidRequest[]} */ let bidRequest = [{ method: 'POST', url: ENDPOINT_URL, @@ -194,7 +250,7 @@ describe('cointrafficBidAdapter', function () { height: 250, creativeId: 'creativeId12345', ttl: 90, - ad: '

I am an ad

', + ad: '

I am an ad

', mediaType: 'banner', adomain: ['test.com'] } @@ -209,7 +265,7 @@ describe('cointrafficBidAdapter', function () { height: 250, creativeId: 'creativeId12345', ttl: 90, - ad: '

I am an ad

', + ad: '

I am an ad

', meta: { mediaType: 'banner', advertiserDomains: [ @@ -227,6 +283,7 @@ describe('cointrafficBidAdapter', function () { }); it('should get empty bid response requested currency is not available', function () { + /** @type {BidRequest[]} */ let bidRequest = [{ method: 'POST', url: ENDPOINT_URL, @@ -253,6 +310,7 @@ describe('cointrafficBidAdapter', function () { }); it('should get empty bid response if no server response', function () { + /** @type {BidRequest[]} */ let bidRequest = [{ method: 'POST', url: ENDPOINT_URL, diff --git a/test/spec/modules/colossussspBidAdapter_spec.js b/test/spec/modules/colossussspBidAdapter_spec.js index 71e94f0da32..adb9137d892 100644 --- a/test/spec/modules/colossussspBidAdapter_spec.js +++ b/test/spec/modules/colossussspBidAdapter_spec.js @@ -56,6 +56,93 @@ describe('ColossussspAdapter', function () { referer: 'http://www.example.com', reachedTop: true, }, + ortb2: { + app: { + name: 'myappname', + keywords: 'power tools, drills', + content: { + data: [ + { + name: 'www.dataprovider1.com', + ext: { + segtax: 6 + }, + segment: [ + { + id: '687' + }, + { + id: '123' + } + ] + }, + { + name: 'www.dataprovider1.com', + ext: { + segtax: 7 + }, + segment: [ + { + id: '456' + }, + { + id: '789' + } + ] + } + ] + } + }, + site: { + name: 'example', + domain: 'page.example.com', + cat: ['IAB2'], + sectioncat: ['IAB2-2'], + pagecat: ['IAB2-2'], + page: 'https://page.example.com/here.html', + ref: 'https://ref.example.com', + keywords: 'power tools, drills', + search: 'drill', + content: { + userrating: '4', + data: [{ + name: 'www.dataprovider1.com', + ext: { + segtax: 7, + cids: ['iris_c73g5jq96mwso4d8'] + }, + segment: [ + { id: '687' }, + { id: '123' } + ] + }] + }, + ext: { + data: { + pageType: 'article', + category: 'repair' + } + } + }, + user: { + yob: 1985, + gender: 'm', + keywords: 'a,b', + data: [{ + name: 'dataprovider.com', + ext: { segtax: 4 }, + segment: [ + { id: '1' } + ] + }], + ext: { + data: { + registered: true, + interests: ['cars'] + } + } + } + }, bids: [bid] } @@ -91,7 +178,7 @@ describe('ColossussspAdapter', function () { it('Returns valid data if array of bids is valid', function () { let data = serverRequest.data; expect(data).to.be.an('object'); - expect(data).to.have.all.keys('deviceWidth', 'deviceHeight', 'language', 'secure', 'host', 'page', 'placements', 'ccpa', 'gdpr_consent', 'gdpr_require'); + expect(data).to.have.all.keys('deviceWidth', 'deviceHeight', 'language', 'secure', 'host', 'page', 'placements', 'ccpa', 'gdpr_consent', 'gdpr_require', 'userObj', 'siteObj', 'appObj'); expect(data.deviceWidth).to.be.a('number'); expect(data.deviceHeight).to.be.a('number'); expect(data.language).to.be.a('string'); @@ -132,7 +219,7 @@ describe('ColossussspAdapter', function () { let data = serverRequest.data; expect(data).to.be.an('object'); - expect(data).to.have.all.keys('deviceWidth', 'deviceHeight', 'language', 'secure', 'host', 'page', 'placements', 'ccpa', 'gdpr_consent', 'gdpr_require'); + expect(data).to.have.all.keys('deviceWidth', 'deviceHeight', 'language', 'secure', 'host', 'page', 'placements', 'ccpa', 'gdpr_consent', 'gdpr_require', 'userObj', 'siteObj', 'appObj'); expect(data.deviceWidth).to.be.a('number'); expect(data.deviceHeight).to.be.a('number'); expect(data.language).to.be.a('string'); diff --git a/test/spec/modules/compassBidAdapter_spec.js b/test/spec/modules/compassBidAdapter_spec.js index 28021c4f7c0..6a761e63ea1 100644 --- a/test/spec/modules/compassBidAdapter_spec.js +++ b/test/spec/modules/compassBidAdapter_spec.js @@ -76,7 +76,8 @@ describe('CompassBidAdapter', function () { gdprConsent: 'COvFyGBOvFyGBAbAAAENAPCAAOAAAAAAAAAAAEEUACCKAAA.IFoEUQQgAIQwgIwQABAEAAAAOIAACAIAAAAQAIAgEAACEAAAAAgAQBAAAAAAAGBAAgAAAAAAAFAAECAAAgAAQARAEQAAAAAJAAIAAgAAAYQEAAAQmAgBC3ZAYzUw', refererInfo: { referer: 'https://test.com' - } + }, + timeout: 500 }; describe('isBidRequestValid', function () { diff --git a/test/spec/modules/concertBidAdapter_spec.js b/test/spec/modules/concertBidAdapter_spec.js index 00626472ecb..f5c807b4703 100644 --- a/test/spec/modules/concertBidAdapter_spec.js +++ b/test/spec/modules/concertBidAdapter_spec.js @@ -57,8 +57,9 @@ describe('ConcertAdapter', function () { refererInfo: { page: 'https://www.google.com' }, - uspConsent: '1YYY', - gdprConsent: {} + uspConsent: '1YN-', + gdprConsent: {}, + gppConsent: {} }; bidResponse = { @@ -111,7 +112,7 @@ describe('ConcertAdapter', function () { expect(payload).to.have.property('meta'); expect(payload).to.have.property('slots'); - const metaRequiredFields = ['prebidVersion', 'pageUrl', 'screen', 'debug', 'uid', 'optedOut', 'adapterVersion', 'uspConsent', 'gdprConsent']; + const metaRequiredFields = ['prebidVersion', 'pageUrl', 'screen', 'debug', 'uid', 'optedOut', 'adapterVersion', 'uspConsent', 'gdprConsent', 'gppConsent']; const slotsRequiredFields = ['name', 'bidId', 'transactionId', 'sizes', 'partnerId', 'slotType']; metaRequiredFields.forEach(function(field) { @@ -138,17 +139,20 @@ describe('ConcertAdapter', function () { expect(payload.meta.uid).to.not.equal(false); }); + it('should not generate uid if USP consent disallows', function() { + storage.removeDataFromLocalStorage('c_nap'); + const request = spec.buildRequests(bidRequests, { ...bidRequest, uspConsent: '1YY' }); + const payload = JSON.parse(request.data); + + expect(payload.meta.uid).to.equal(false); + }); + it('should use sharedid if it exists', function() { storage.removeDataFromLocalStorage('c_nap'); - const request = spec.buildRequests(bidRequests, { - ...bidRequest, - userId: { - _sharedid: { - id: '123abc' - } - } - }); + const bidRequestsWithSharedId = [{ ...bidRequests[0], userId: { sharedid: { id: '123abc' } } }] + const request = spec.buildRequests(bidRequestsWithSharedId, bidRequest); const payload = JSON.parse(request.data); + expect(payload.meta.uid).to.equal('123abc'); }) @@ -213,82 +217,4 @@ describe('ConcertAdapter', function () { expect(bids).to.have.lengthOf(0); }); }); - - describe('spec.getUserSyncs', function() { - it('should not register syncs when iframe is not enabled', function() { - const opts = { - iframeEnabled: false - } - const sync = spec.getUserSyncs(opts, [], bidRequest.gdprConsent, bidRequest.uspConsent); - expect(sync).to.have.lengthOf(0); - }); - - it('should not register syncs when the user has opted out', function() { - const opts = { - iframeEnabled: true - }; - storage.setDataInLocalStorage('c_nap', 'true'); - - const sync = spec.getUserSyncs(opts, [], bidRequest.gdprConsent, bidRequest.uspConsent); - expect(sync).to.have.lengthOf(0); - }); - - it('should set gdprApplies flag to 1 if the user is in area where GDPR applies', function() { - const opts = { - iframeEnabled: true - }; - storage.removeDataFromLocalStorage('c_nap'); - - bidRequest.gdprConsent = { - gdprApplies: true - }; - - const sync = spec.getUserSyncs(opts, [], bidRequest.gdprConsent, bidRequest.uspConsent); - expect(sync[0].url).to.have.string('gdpr_applies=1'); - }); - - it('should set gdprApplies flag to 1 if the user is in area where GDPR applies', function() { - const opts = { - iframeEnabled: true - }; - storage.removeDataFromLocalStorage('c_nap'); - - bidRequest.gdprConsent = { - gdprApplies: false - }; - - const sync = spec.getUserSyncs(opts, [], bidRequest.gdprConsent, bidRequest.uspConsent); - expect(sync[0].url).to.have.string('gdpr_applies=0'); - }); - - it('should set gdpr consent param with the user\'s choices on consent', function() { - const opts = { - iframeEnabled: true - }; - storage.removeDataFromLocalStorage('c_nap'); - - bidRequest.gdprConsent = { - gdprApplies: false, - consentString: 'BOJ/P2HOJ/P2HABABMAAAAAZ+A==' - }; - - const sync = spec.getUserSyncs(opts, [], bidRequest.gdprConsent, bidRequest.uspConsent); - expect(sync[0].url).to.have.string('gdpr_consent=BOJ/P2HOJ/P2HABABMAAAAAZ+A=='); - }); - - it('should set ccpa consent param with the user\'s choices on consent', function() { - const opts = { - iframeEnabled: true - }; - storage.removeDataFromLocalStorage('c_nap'); - - bidRequest.gdprConsent = { - gdprApplies: false, - uspConsent: '1YYY' - }; - - const sync = spec.getUserSyncs(opts, [], bidRequest.gdprConsent, bidRequest.uspConsent); - expect(sync[0].url).to.have.string('usp_consent=1YY'); - }); - }); }); diff --git a/test/spec/modules/confiantRtdProvider_spec.js b/test/spec/modules/confiantRtdProvider_spec.js new file mode 100644 index 00000000000..987aca2e020 --- /dev/null +++ b/test/spec/modules/confiantRtdProvider_spec.js @@ -0,0 +1,107 @@ +import * as utils from '../../../src/utils.js'; +import * as hook from '../../../src/hook.js' +import * as events from '../../../src/events.js'; +import CONSTANTS from '../../../src/constants.json'; + +import confiantModule from '../../../modules/confiantRtdProvider.js'; + +const { + injectConfigScript, + setupPage, + subscribeToConfiantCommFrame, + registerConfiantSubmodule +} = confiantModule; + +describe('Confiant RTD module', function () { + describe('setupPage()', function() { + it('should return false if propertId is not present in config', function() { + expect(setupPage({})).to.be.false; + expect(setupPage({ params: {} })).to.be.false; + expect(setupPage({ params: { propertyId: '' } })).to.be.false; + }); + + it('should return true if expected parameters are present', function() { + expect(setupPage({ params: { propertyId: 'clientId' } })).to.be.true; + }); + }); + + describe('Module initialization', function() { + let insertElementStub; + beforeEach(function() { + insertElementStub = sinon.stub(utils, 'insertElement'); + }); + afterEach(function() { + utils.insertElement.restore(); + }); + + it('should subscribe to rovided Window object', function () { + const mockWindow = { addEventListener: sinon.spy() }; + + subscribeToConfiantCommFrame(mockWindow); + + sinon.assert.calledOnce(mockWindow.addEventListener); + }); + + it('should fire BillableEvent as a result for message in comm window', function() { + let listenerCallback; + const mockWindow = { addEventListener: (a, cb) => (listenerCallback = cb) }; + let billableEventsCounter = 0; + + events.on(CONSTANTS.EVENTS.BILLABLE_EVENT, (e) => { + if (e.vendor === 'confiant') { + billableEventsCounter++; + expect(e.type).to.equal('impression'); + expect(e.billingId).to.be.a('number'); + expect(e.auctionId).to.equal('auctionId'); + expect(e.transactionId).to.equal('transactionId'); + } + }); + + subscribeToConfiantCommFrame(mockWindow); + listenerCallback({ + data: { + auctionId: 'auctionId', + transactionId: 'transactionId' + } + }); + listenerCallback({ + data: { + auctionId: 'auctionId', + transactionId: 'transactionId' + } + }); + + expect(billableEventsCounter).to.equal(2); + }); + }); + + describe('Sumbodule execution', function() { + let submoduleStub; + let insertElementStub; + beforeEach(function () { + submoduleStub = sinon.stub(hook, 'submodule'); + insertElementStub = sinon.stub(utils, 'insertElement'); + }); + afterEach(function () { + utils.insertElement.restore(); + submoduleStub.restore(); + }); + + function initModule() { + registerConfiantSubmodule(); + + expect(submoduleStub.calledOnceWith('realTimeData')).to.equal(true); + + const registeredSubmoduleDefinition = submoduleStub.getCall(0).args[1]; + expect(registeredSubmoduleDefinition).to.be.an('object'); + expect(registeredSubmoduleDefinition).to.have.own.property('name', 'confiant'); + expect(registeredSubmoduleDefinition).to.have.own.property('init').that.is.a('function'); + + return registeredSubmoduleDefinition; + } + + it('should register Confiant submodule', function () { + initModule(); + }); + }); +}); diff --git a/test/spec/modules/connectIdSystem_spec.js b/test/spec/modules/connectIdSystem_spec.js index b2193f350ce..72acfa38e0a 100644 --- a/test/spec/modules/connectIdSystem_spec.js +++ b/test/spec/modules/connectIdSystem_spec.js @@ -1,6 +1,10 @@ import {expect} from 'chai'; -import {parseQS} from 'src/utils.js'; -import {connectIdSubmodule} from 'modules/connectIdSystem.js'; +import {connectIdSubmodule, storage} from 'modules/connectIdSystem.js'; +import {server} from '../../mocks/xhr'; +import {parseQS, parseUrl} from 'src/utils.js'; +import {uspDataHandler} from 'src/adapterManager.js'; + +const TEST_SERVER_URL = 'http://localhost:9876/'; describe('Yahoo ConnectID Submodule', () => { const HASHED_EMAIL = '6bda6f2fa268bf0438b5423a9861a2cedaa5dec163c03f743cfe05c08a8397b2'; @@ -8,6 +12,8 @@ describe('Yahoo ConnectID Submodule', () => { const PIXEL_ID = '1234'; const PROD_ENDPOINT = `https://ups.analytics.yahoo.com/ups/${PIXEL_ID}/fed`; const OVERRIDE_ENDPOINT = 'https://foo/bar'; + const STORAGE_KEY = 'connectId'; + const USP_DATA = '1YYY'; it('should have the correct module name declared', () => { expect(connectIdSubmodule.name).to.equal('connectId'); @@ -20,259 +26,440 @@ describe('Yahoo ConnectID Submodule', () => { describe('getId()', () => { let ajaxStub; let getAjaxFnStub; + let getCookieStub; + let setCookieStub; + let getLocalStorageStub; + let setLocalStorageStub; + let cookiesEnabledStub; + let localStorageEnabledStub; + let removeLocalStorageDataStub; + let uspConsentDataStub; + let consentData; beforeEach(() => { ajaxStub = sinon.stub(); getAjaxFnStub = sinon.stub(connectIdSubmodule, 'getAjaxFn'); getAjaxFnStub.returns(ajaxStub); + getCookieStub = sinon.stub(storage, 'getCookie'); + setCookieStub = sinon.stub(storage, 'setCookie'); + cookiesEnabledStub = sinon.stub(storage, 'cookiesAreEnabled'); + getLocalStorageStub = sinon.stub(storage, 'getDataFromLocalStorage'); + setLocalStorageStub = sinon.stub(storage, 'setDataInLocalStorage'); + localStorageEnabledStub = sinon.stub(storage, 'localStorageIsEnabled'); + removeLocalStorageDataStub = sinon.stub(storage, 'removeDataFromLocalStorage'); + uspConsentDataStub = sinon.stub(uspDataHandler, 'getConsentData'); + + cookiesEnabledStub.returns(true); + localStorageEnabledStub.returns(true); + uspConsentDataStub.returns(USP_DATA); consentData = { - gdpr: { - gdprApplies: 1, - consentString: 'GDPR_CONSENT_STRING' - }, - uspConsent: 'USP_CONSENT_STRING' + gdprApplies: 1, + consentString: 'GDPR_CONSENT_STRING' }; }); afterEach(() => { getAjaxFnStub.restore(); + getCookieStub.restore(); + setCookieStub.restore(); + getLocalStorageStub.restore(); + setLocalStorageStub.restore(); + cookiesEnabledStub.restore(); + localStorageEnabledStub.restore(); + removeLocalStorageDataStub.restore(); + uspConsentDataStub.restore(); }); function invokeGetIdAPI(configParams, consentData) { let result = connectIdSubmodule.getId({ params: configParams }, consentData); - if (typeof result === 'object') { + if (typeof result === 'object' && result.callback) { result.callback(sinon.stub()); } return result; } - it('returns undefined if he, pixelId and puid params are not passed', () => { - expect(invokeGetIdAPI({}, consentData)).to.be.undefined; - expect(ajaxStub.callCount).to.equal(0); - }); - - it('returns undefined if the pixelId param is not passed', () => { - expect(invokeGetIdAPI({ - he: HASHED_EMAIL, - puid: PUBLISHER_USER_ID - }, consentData)).to.be.undefined; - expect(ajaxStub.callCount).to.equal(0); - }); - - it('returns undefined if the pixelId param is passed, but the he and puid param are not passed', () => { - expect(invokeGetIdAPI({ - pixelId: PIXEL_ID - }, consentData)).to.be.undefined; - expect(ajaxStub.callCount).to.equal(0); - }); - - it('returns an object with the callback function if the endpoint override param and the he params are passed', () => { - let result = invokeGetIdAPI({ - he: HASHED_EMAIL, - endpoint: OVERRIDE_ENDPOINT - }, consentData); - expect(result).to.be.an('object').that.has.all.keys('callback'); - expect(result.callback).to.be.a('function'); - }); - - it('returns an object with the callback function if the endpoint override param and the puid params are passed', () => { - let result = invokeGetIdAPI({ - puid: PUBLISHER_USER_ID, - endpoint: OVERRIDE_ENDPOINT - }, consentData); - expect(result).to.be.an('object').that.has.all.keys('callback'); - expect(result.callback).to.be.a('function'); - }); - - it('returns an object with the callback function if the endpoint override param and the puid and he params are passed', () => { - let result = invokeGetIdAPI({ - he: HASHED_EMAIL, - puid: PUBLISHER_USER_ID, - endpoint: OVERRIDE_ENDPOINT - }, consentData); - expect(result).to.be.an('object').that.has.all.keys('callback'); - expect(result.callback).to.be.a('function'); - }); - - it('returns an object with the callback function if the pixelId and he params are passed', () => { - let result = invokeGetIdAPI({ - he: HASHED_EMAIL, - pixelId: PIXEL_ID - }, consentData); - expect(result).to.be.an('object').that.has.all.keys('callback'); - expect(result.callback).to.be.a('function'); - }); - - it('returns an object with the callback function if the pixelId and puid params are passed', () => { - let result = invokeGetIdAPI({ - puid: PUBLISHER_USER_ID, - pixelId: PIXEL_ID - }, consentData); - expect(result).to.be.an('object').that.has.all.keys('callback'); - expect(result.callback).to.be.a('function'); - }); - - it('returns an object with the callback function if the pixelId, he and puid params are passed', () => { - let result = invokeGetIdAPI({ - he: HASHED_EMAIL, - puid: PUBLISHER_USER_ID, - pixelId: PIXEL_ID - }, consentData); - expect(result).to.be.an('object').that.has.all.keys('callback'); - expect(result.callback).to.be.a('function'); - }); - - it('returns an undefined if the Yahoo specific opt-out key is present in local storage', () => { - localStorage.setItem('connectIdOptOut', '1'); - expect(invokeGetIdAPI({ - he: HASHED_EMAIL, - pixelId: PIXEL_ID - }, consentData)).to.be.undefined; - localStorage.removeItem('connectIdOptOut'); - }); - - it('returns an object with the callback function if the correct params are passed and Yahoo opt-out value is not "1"', () => { - localStorage.setItem('connectIdOptOut', 'true'); - let result = invokeGetIdAPI({ - he: HASHED_EMAIL, - pixelId: PIXEL_ID - }, consentData); - expect(result).to.be.an('object').that.has.all.keys('callback'); - expect(result.callback).to.be.a('function'); - localStorage.removeItem('connectIdOptOut'); - }); - - it('Makes an ajax GET request to the production API endpoint with pixelId and he query params', () => { - invokeGetIdAPI({ - he: HASHED_EMAIL, - pixelId: PIXEL_ID - }, consentData); - - const expectedParams = { - he: HASHED_EMAIL, - pixelId: PIXEL_ID, - '1p': '0', - gdpr: '1', - gdpr_consent: consentData.gdpr.consentString, - us_privacy: consentData.uspConsent - }; - const requestQueryParams = parseQS(ajaxStub.firstCall.args[0].split('?')[1]); - - expect(ajaxStub.firstCall.args[0].indexOf(`${PROD_ENDPOINT}?`)).to.equal(0); - expect(requestQueryParams).to.deep.equal(expectedParams); - expect(ajaxStub.firstCall.args[3]).to.deep.equal({method: 'GET', withCredentials: true}); - }); - - it('Makes an ajax GET request to the production API endpoint with pixelId and puid query params', () => { - invokeGetIdAPI({ - puid: PUBLISHER_USER_ID, - pixelId: PIXEL_ID - }, consentData); - - const expectedParams = { - puid: PUBLISHER_USER_ID, - pixelId: PIXEL_ID, - '1p': '0', - gdpr: '1', - gdpr_consent: consentData.gdpr.consentString, - us_privacy: consentData.uspConsent - }; - const requestQueryParams = parseQS(ajaxStub.firstCall.args[0].split('?')[1]); - - expect(ajaxStub.firstCall.args[0].indexOf(`${PROD_ENDPOINT}?`)).to.equal(0); - expect(requestQueryParams).to.deep.equal(expectedParams); - expect(ajaxStub.firstCall.args[3]).to.deep.equal({method: 'GET', withCredentials: true}); - }); - - it('Makes an ajax GET request to the production API endpoint with pixelId, puid and he query params', () => { - invokeGetIdAPI({ - puid: PUBLISHER_USER_ID, - he: HASHED_EMAIL, - pixelId: PIXEL_ID - }, consentData); - - const expectedParams = { - puid: PUBLISHER_USER_ID, - he: HASHED_EMAIL, - pixelId: PIXEL_ID, - '1p': '0', - gdpr: '1', - gdpr_consent: consentData.gdpr.consentString, - us_privacy: consentData.uspConsent - }; - const requestQueryParams = parseQS(ajaxStub.firstCall.args[0].split('?')[1]); - - expect(ajaxStub.firstCall.args[0].indexOf(`${PROD_ENDPOINT}?`)).to.equal(0); - expect(requestQueryParams).to.deep.equal(expectedParams); - expect(ajaxStub.firstCall.args[3]).to.deep.equal({method: 'GET', withCredentials: true}); - }); - - it('Makes an ajax GET request to the specified override API endpoint with query params', () => { - invokeGetIdAPI({ - he: HASHED_EMAIL, - endpoint: OVERRIDE_ENDPOINT - }, consentData); - - const expectedParams = { - he: HASHED_EMAIL, - '1p': '0', - gdpr: '1', - gdpr_consent: consentData.gdpr.consentString, - us_privacy: consentData.uspConsent - }; - const requestQueryParams = parseQS(ajaxStub.firstCall.args[0].split('?')[1]); - - expect(ajaxStub.firstCall.args[0].indexOf(`${OVERRIDE_ENDPOINT}?`)).to.equal(0); - expect(requestQueryParams).to.deep.equal(expectedParams); - expect(ajaxStub.firstCall.args[3]).to.deep.equal({method: 'GET', withCredentials: true}); - }); - - it('sets the callbacks param of the ajax function call correctly', () => { - invokeGetIdAPI({ - he: HASHED_EMAIL, - pixelId: PIXEL_ID, - }, consentData); + describe('Low level storage functionality', () => { + const storageTestCases = [ + { + detail: 'cookie data over local storage data', + cookie: '{"connectId":"foo"}', + localStorage: {connectId: 'bar'}, + expected: {connectId: 'foo'} + }, + { + detail: 'cookie data if only cookie data exists', + cookie: '{"connectId":"foo"}', + localStorage: undefined, + expected: {connectId: 'foo'} + }, + { + detail: 'local storage data if only it local storage data exists', + cookie: undefined, + localStorage: {connectId: 'bar'}, + expected: {connectId: 'bar'} + }, + { + detail: 'undefined when both cookie and local storage are empty', + cookie: undefined, + localStorage: undefined, + expected: undefined + } + ] - expect(ajaxStub.firstCall.args[1]).to.be.an('object').that.has.all.keys(['success', 'error']); - }); + storageTestCases.forEach(testCase => it(`getId() should return ${testCase.detail}`, function () { + getCookieStub.withArgs(STORAGE_KEY).returns(testCase.cookie); + getLocalStorageStub.withArgs(STORAGE_KEY).returns(testCase.localStorage); - it('sets GDPR consent data flag correctly when call is under GDPR jurisdiction.', () => { - invokeGetIdAPI({ - he: HASHED_EMAIL, - pixelId: PIXEL_ID, - }, consentData); + const result = invokeGetIdAPI({ + he: HASHED_EMAIL, + endpoint: OVERRIDE_ENDPOINT + }, consentData); - const requestQueryParams = parseQS(ajaxStub.firstCall.args[0].split('?')[1]); - expect(requestQueryParams.gdpr).to.equal('1'); - expect(requestQueryParams.gdpr_consent).to.equal(consentData.gdpr.consentString); - }); + expect(result.id).to.be.deep.equal(testCase.expected ? testCase.expected : undefined); + })); + }) - it('sets GDPR consent data flag correctly when call is NOT under GDPR jurisdiction.', () => { - consentData.gdpr.gdprApplies = false; + describe('with invalid module configuration', () => { + it('returns undefined if he, pixelId and puid params are not passed', () => { + expect(invokeGetIdAPI({}, consentData)).to.be.undefined; + expect(ajaxStub.callCount).to.equal(0); + }); - invokeGetIdAPI({ - he: HASHED_EMAIL, - pixelId: PIXEL_ID, - }, consentData); + it('returns undefined if the pixelId param is not passed', () => { + expect(invokeGetIdAPI({ + he: HASHED_EMAIL, + puid: PUBLISHER_USER_ID + }, consentData)).to.be.undefined; + expect(ajaxStub.callCount).to.equal(0); + }); - const requestQueryParams = parseQS(ajaxStub.firstCall.args[0].split('?')[1]); - expect(requestQueryParams.gdpr).to.equal('0'); - expect(requestQueryParams.gdpr_consent).to.equal(''); + it('returns undefined if the pixelId param is passed, but the he and puid param are not passed', () => { + expect(invokeGetIdAPI({ + pixelId: PIXEL_ID + }, consentData)).to.be.undefined; + expect(ajaxStub.callCount).to.equal(0); + }); }); - [1, '1', true].forEach(firstPartyParamValue => { - it(`sets 1p payload property to '1' for a config value of ${firstPartyParamValue}`, () => { - invokeGetIdAPI({ - '1p': firstPartyParamValue, - he: HASHED_EMAIL, - pixelId: PIXEL_ID, - }, consentData); + describe('with valid module configuration', () => { + describe('when data is in client storage', () => { + it('returns an object with the stored ID from cookies for valid module configuration', () => { + const cookieData = {connectId: 'foobar'}; + getCookieStub.withArgs(STORAGE_KEY).returns(JSON.stringify(cookieData)); + let result = invokeGetIdAPI({ + he: HASHED_EMAIL, + pixelId: PIXEL_ID + }, consentData); + expect(result).to.be.an('object').that.has.all.keys('id'); + expect(result.id).to.deep.equal(cookieData); + }); + + it('returns an object with the stored ID from localStorage for valid module configuration', () => { + const localStorageData = {connectId: 'foobarbaz'}; + getLocalStorageStub.withArgs(STORAGE_KEY).returns(localStorageData); + let result = invokeGetIdAPI({ + he: HASHED_EMAIL, + pixelId: PIXEL_ID + }, consentData); + expect(result).to.be.an('object').that.has.all.keys('id'); + expect(result.id).to.deep.equal(localStorageData); + }); + + it('deletes local storage data when expiry has passed', () => { + const localStorageData = {connectId: 'foobarbaz', __expires: Date.now() - 10000}; + getLocalStorageStub.withArgs(STORAGE_KEY).returns(localStorageData); + let result = invokeGetIdAPI({ + he: HASHED_EMAIL, + pixelId: PIXEL_ID + }, consentData); + expect(removeLocalStorageDataStub.calledOnce).to.be.true; + expect(removeLocalStorageDataStub.firstCall.args[0]).to.equal(STORAGE_KEY); + expect(result.id).to.equal(undefined); + expect(result.callback).to.be.a('function'); + }); + + it('will not delete local storage data when expiry has not passed', () => { + const localStorageData = {connectId: 'foobarbaz', __expires: Date.now() + 10000}; + getLocalStorageStub.withArgs(STORAGE_KEY).returns(localStorageData); + let result = invokeGetIdAPI({ + he: HASHED_EMAIL, + pixelId: PIXEL_ID + }, consentData); + expect(removeLocalStorageDataStub.calledOnce).to.be.false; + expect(result.id).to.deep.equal(localStorageData); + }); + }); - const requestQueryParams = parseQS(ajaxStub.firstCall.args[0].split('?')[1]); - expect(requestQueryParams['1p']).to.equal('1'); + describe('when no data in client storage', () => { + it('returns an object with the callback function if the endpoint override param and the he params are passed', () => { + let result = invokeGetIdAPI({ + he: HASHED_EMAIL, + endpoint: OVERRIDE_ENDPOINT + }, consentData); + expect(result).to.be.an('object').that.has.all.keys('callback'); + expect(result.callback).to.be.a('function'); + }); + + it('returns an object with the callback function if the endpoint override param and the puid params are passed', () => { + let result = invokeGetIdAPI({ + puid: PUBLISHER_USER_ID, + endpoint: OVERRIDE_ENDPOINT + }, consentData); + expect(result).to.be.an('object').that.has.all.keys('callback'); + expect(result.callback).to.be.a('function'); + }); + + it('returns an object with the callback function if the endpoint override param and the puid and he params are passed', () => { + let result = invokeGetIdAPI({ + he: HASHED_EMAIL, + puid: PUBLISHER_USER_ID, + endpoint: OVERRIDE_ENDPOINT + }, consentData); + expect(result).to.be.an('object').that.has.all.keys('callback'); + expect(result.callback).to.be.a('function'); + }); + + it('returns an object with the callback function if the pixelId and he params are passed', () => { + let result = invokeGetIdAPI({ + he: HASHED_EMAIL, + pixelId: PIXEL_ID + }, consentData); + expect(result).to.be.an('object').that.has.all.keys('callback'); + expect(result.callback).to.be.a('function'); + }); + + it('returns an object with the callback function if the pixelId and puid params are passed', () => { + let result = invokeGetIdAPI({ + puid: PUBLISHER_USER_ID, + pixelId: PIXEL_ID + }, consentData); + expect(result).to.be.an('object').that.has.all.keys('callback'); + expect(result.callback).to.be.a('function'); + }); + + it('returns an object with the callback function if the pixelId, he and puid params are passed', () => { + let result = invokeGetIdAPI({ + he: HASHED_EMAIL, + puid: PUBLISHER_USER_ID, + pixelId: PIXEL_ID + }, consentData); + expect(result).to.be.an('object').that.has.all.keys('callback'); + expect(result.callback).to.be.a('function'); + }); + + it('returns an undefined if the Yahoo specific opt-out key is present in local storage', () => { + localStorage.setItem('connectIdOptOut', '1'); + expect(invokeGetIdAPI({ + he: HASHED_EMAIL, + pixelId: PIXEL_ID + }, consentData)).to.be.undefined; + localStorage.removeItem('connectIdOptOut'); + }); + + it('returns an object with the callback function if the correct params are passed and Yahoo opt-out value is not "1"', () => { + localStorage.setItem('connectIdOptOut', 'true'); + let result = invokeGetIdAPI({ + he: HASHED_EMAIL, + pixelId: PIXEL_ID + }, consentData); + expect(result).to.be.an('object').that.has.all.keys('callback'); + expect(result.callback).to.be.a('function'); + localStorage.removeItem('connectIdOptOut'); + }); + + it('Makes an ajax GET request to the production API endpoint with pixelId and he query params', () => { + invokeGetIdAPI({ + he: HASHED_EMAIL, + pixelId: PIXEL_ID + }, consentData); + + const expectedParams = { + he: HASHED_EMAIL, + pixelId: PIXEL_ID, + '1p': '0', + gdpr: '1', + gdpr_consent: consentData.consentString, + v: '1', + url: TEST_SERVER_URL, + us_privacy: USP_DATA + }; + const requestQueryParams = parseQS(ajaxStub.firstCall.args[0].split('?')[1]); + + expect(ajaxStub.firstCall.args[0].indexOf(`${PROD_ENDPOINT}?`)).to.equal(0); + expect(requestQueryParams).to.deep.equal(expectedParams); + expect(ajaxStub.firstCall.args[3]).to.deep.equal({method: 'GET', withCredentials: true}); + }); + + it('Makes an ajax GET request to the production API endpoint with pixelId and puid query params', () => { + invokeGetIdAPI({ + puid: PUBLISHER_USER_ID, + pixelId: PIXEL_ID + }, consentData); + + const expectedParams = { + v: '1', + '1p': '0', + gdpr: '1', + puid: PUBLISHER_USER_ID, + pixelId: PIXEL_ID, + gdpr_consent: consentData.consentString, + url: TEST_SERVER_URL, + us_privacy: USP_DATA + }; + const requestQueryParams = parseQS(ajaxStub.firstCall.args[0].split('?')[1]); + + expect(ajaxStub.firstCall.args[0].indexOf(`${PROD_ENDPOINT}?`)).to.equal(0); + expect(requestQueryParams).to.deep.equal(expectedParams); + expect(ajaxStub.firstCall.args[3]).to.deep.equal({method: 'GET', withCredentials: true}); + }); + + it('Makes an ajax GET request to the production API endpoint with pixelId, puid and he query params', () => { + invokeGetIdAPI({ + puid: PUBLISHER_USER_ID, + he: HASHED_EMAIL, + pixelId: PIXEL_ID + }, consentData); + + const expectedParams = { + puid: PUBLISHER_USER_ID, + he: HASHED_EMAIL, + pixelId: PIXEL_ID, + '1p': '0', + gdpr: '1', + gdpr_consent: consentData.consentString, + v: '1', + url: TEST_SERVER_URL, + us_privacy: USP_DATA + }; + const requestQueryParams = parseQS(ajaxStub.firstCall.args[0].split('?')[1]); + + expect(ajaxStub.firstCall.args[0].indexOf(`${PROD_ENDPOINT}?`)).to.equal(0); + expect(requestQueryParams).to.deep.equal(expectedParams); + expect(ajaxStub.firstCall.args[3]).to.deep.equal({method: 'GET', withCredentials: true}); + }); + + it('Makes an ajax GET request to the specified override API endpoint with query params', () => { + invokeGetIdAPI({ + he: HASHED_EMAIL, + endpoint: OVERRIDE_ENDPOINT + }, consentData); + + const expectedParams = { + he: HASHED_EMAIL, + '1p': '0', + gdpr: '1', + gdpr_consent: consentData.consentString, + v: '1', + url: TEST_SERVER_URL, + us_privacy: USP_DATA + }; + const requestQueryParams = parseQS(ajaxStub.firstCall.args[0].split('?')[1]); + + expect(ajaxStub.firstCall.args[0].indexOf(`${OVERRIDE_ENDPOINT}?`)).to.equal(0); + expect(requestQueryParams).to.deep.equal(expectedParams); + expect(ajaxStub.firstCall.args[3]).to.deep.equal({method: 'GET', withCredentials: true}); + }); + + it('sets the callbacks param of the ajax function call correctly', () => { + invokeGetIdAPI({ + he: HASHED_EMAIL, + pixelId: PIXEL_ID, + }, consentData); + + expect(ajaxStub.firstCall.args[1]).to.be.an('object').that.has.all.keys(['success', 'error']); + }); + + it('sets GDPR consent data flag correctly when call is under GDPR jurisdiction.', () => { + invokeGetIdAPI({ + he: HASHED_EMAIL, + pixelId: PIXEL_ID, + }, consentData); + + const requestQueryParams = parseQS(ajaxStub.firstCall.args[0].split('?')[1]); + expect(requestQueryParams.gdpr).to.equal('1'); + expect(requestQueryParams.gdpr_consent).to.equal(consentData.consentString); + }); + + it('sets GDPR consent data flag correctly when call is NOT under GDPR jurisdiction.', () => { + consentData.gdprApplies = false; + + invokeGetIdAPI({ + he: HASHED_EMAIL, + pixelId: PIXEL_ID, + }, consentData); + + const requestQueryParams = parseQS(ajaxStub.firstCall.args[0].split('?')[1]); + expect(requestQueryParams.gdpr).to.equal('0'); + expect(requestQueryParams.gdpr_consent).to.equal(''); + }); + + [1, '1', true].forEach(firstPartyParamValue => { + it(`sets 1p payload property to '1' for a config value of ${firstPartyParamValue}`, () => { + invokeGetIdAPI({ + '1p': firstPartyParamValue, + he: HASHED_EMAIL, + pixelId: PIXEL_ID, + }, consentData); + + const requestQueryParams = parseQS(ajaxStub.firstCall.args[0].split('?')[1]); + expect(requestQueryParams['1p']).to.equal('1'); + }); + }); + + it('stores the result in client cookie storage', () => { + const dateNowStub = sinon.stub(Date, 'now'); + dateNowStub.returns(0); + getAjaxFnStub.restore(); + const upsResponse = {connectid: 'foobarbaz'}; + const expiryDelta = new Date(60 * 60 * 24 * 14 * 1000); + invokeGetIdAPI({ + puid: PUBLISHER_USER_ID, + pixelId: PIXEL_ID + }, consentData); + let request = server.requests[0]; + request.respond( + 200, + {'Content-Type': 'application/json'}, + JSON.stringify(upsResponse) + ); + expect(setCookieStub.calledOnce).to.be.true; + expect(setCookieStub.firstCall.args[0]).to.equal(STORAGE_KEY); + expect(setCookieStub.firstCall.args[1]).to.equal(JSON.stringify(upsResponse)); + expect(setCookieStub.firstCall.args[2]).to.equal(expiryDelta.toUTCString()); + expect(setCookieStub.firstCall.args[3]).to.equal(null); + const cookieDomain = parseUrl(TEST_SERVER_URL).hostname; + expect(setCookieStub.firstCall.args[4]).to.equal(`.${cookieDomain}`); + dateNowStub.restore(); + }); + + it('stores the result in localStorage if cookies are not permitted', () => { + getAjaxFnStub.restore(); + cookiesEnabledStub.returns(false); + const dateNowStub = sinon.stub(Date, 'now'); + dateNowStub.returns(0); + const upsResponse = {connectid: 'barfoo'}; + const expectedStoredData = { + connectid: 'barfoo', + __expires: 60 * 60 * 24 * 14 * 1000 + }; + invokeGetIdAPI({ + puid: PUBLISHER_USER_ID, + pixelId: PIXEL_ID + }, consentData); + let request = server.requests[0]; + request.respond( + 200, + {'Content-Type': 'application/json'}, + JSON.stringify(upsResponse) + ); + expect(setLocalStorageStub.calledOnce).to.be.true; + expect(setLocalStorageStub.firstCall.args[0]).to.equal(STORAGE_KEY); + expect(setLocalStorageStub.firstCall.args[1]).to.deep.equal(expectedStoredData); + dateNowStub.restore(); + }); }); }); }); @@ -294,6 +481,13 @@ describe('Yahoo ConnectID Submodule', () => { payload: { connectid: '4567' } + }, + { + key: 'connectId', + expected: '9924', + payload: { + connectId: '9924' + } }]; VALID_API_RESPONSES.forEach(responseData => { it('should return a newly constructed object with the connect ID for a payload with ${responseData.key} key(s)', () => { @@ -336,19 +530,15 @@ describe('Yahoo ConnectID Submodule', () => { })).to.be.false; }); - it('should return false if consent data.gdpr.applies is false', () => { + it('should return false if consent consentData.applies is false', () => { expect(connectIdSubmodule.isEUConsentRequired({ - gdpr: { - gdprApplies: false - } + gdprApplies: false })).to.be.false; }); it('should return true if consent data.gdpr.applies is true', () => { expect(connectIdSubmodule.isEUConsentRequired({ - gdpr: { - gdprApplies: true - } + gdprApplies: true })).to.be.true; }); }); diff --git a/test/spec/modules/connectadBidAdapter_spec.js b/test/spec/modules/connectadBidAdapter_spec.js index 7a70c4bacdb..d8dfcb0ce98 100644 --- a/test/spec/modules/connectadBidAdapter_spec.js +++ b/test/spec/modules/connectadBidAdapter_spec.js @@ -46,9 +46,17 @@ describe('ConnectAd Adapter', function () { auctionId: 'e76cbb58-f3e1-4ad9-9f4c-718c1919d0df', bidderRequestId: '1c56ad30b9b8ca8', transactionId: 'e76cbb58-f3e1-4ad9-9f4c-718c1919d0df', - userId: { - tdid: '123456' - } + userIdAsEids: [ + { + 'source': 'pubcid.org', + 'uids': [ + { + 'atype': 1, + 'id': '123456' + } + ] + } + ] }]; bidderRequest = { diff --git a/test/spec/modules/consentManagementGpp_spec.js b/test/spec/modules/consentManagementGpp_spec.js new file mode 100644 index 00000000000..1170f418caf --- /dev/null +++ b/test/spec/modules/consentManagementGpp_spec.js @@ -0,0 +1,577 @@ +import { setConsentConfig, requestBidsHook, resetConsentData, userCMP, consentTimeout, staticConsentData } from 'modules/consentManagementGpp.js'; +import { gppDataHandler } from 'src/adapterManager.js'; +import * as utils from 'src/utils.js'; +import { config } from 'src/config.js'; +import 'src/prebid.js'; + +let expect = require('chai').expect; + +describe('consentManagementGpp', function () { + describe('setConsentConfig tests:', function () { + describe('empty setConsentConfig value', function () { + beforeEach(function () { + sinon.stub(utils, 'logInfo'); + sinon.stub(utils, 'logWarn'); + }); + + afterEach(function () { + utils.logInfo.restore(); + utils.logWarn.restore(); + config.resetConfig(); + resetConsentData(); + }); + + it('should use system default values', function () { + setConsentConfig({ + gpp: {} + }); + expect(userCMP).to.be.equal('iab'); + expect(consentTimeout).to.be.equal(10000); + sinon.assert.callCount(utils.logInfo, 3); + }); + + it('should exit consent manager if config is not an object', function () { + setConsentConfig(''); + expect(userCMP).to.be.undefined; + sinon.assert.calledOnce(utils.logWarn); + }); + + it('should exit consentManagement module if config is "undefined"', function () { + setConsentConfig(undefined); + expect(userCMP).to.be.undefined; + sinon.assert.calledOnce(utils.logWarn); + }); + + it('should not produce any consent metadata', function () { + setConsentConfig(undefined) + let consentMetadata = gppDataHandler.getConsentMeta(); + expect(consentMetadata).to.be.undefined; + sinon.assert.calledOnce(utils.logWarn); + }) + + it('should immediately look up consent data', () => { + setConsentConfig({ + gpp: { + cmpApi: 'invalid' + } + }); + expect(gppDataHandler.ready).to.be.true; + }) + }); + + describe('valid setConsentConfig value', function () { + afterEach(function () { + config.resetConfig(); + }); + + it('results in all user settings overriding system defaults', function () { + let allConfig = { + gpp: { + cmpApi: 'iab', + timeout: 7500 + } + }; + + setConsentConfig(allConfig); + expect(userCMP).to.be.equal('iab'); + expect(consentTimeout).to.be.equal(7500); + }); + + it('should recognize config.gpp, with default cmpApi and timeout', function () { + setConsentConfig({ + gpp: {} + }); + + expect(userCMP).to.be.equal('iab'); + expect(consentTimeout).to.be.equal(10000); + }); + + it('should enable gppDataHandler', () => { + setConsentConfig({ + gpp: {} + }); + expect(gppDataHandler.enabled).to.be.true; + }); + }); + + describe('static consent string setConsentConfig value', () => { + afterEach(() => { + config.resetConfig(); + }); + + it('results in user settings overriding system defaults for v2 spec', () => { + let staticConfig = { + gpp: { + cmpApi: 'static', + timeout: 7500, + consentData: { + applicableSections: [7], + gppString: 'ABCDEFG1234', + gppVersion: 1, + sectionId: 3, + sectionList: [] + } + } + }; + + setConsentConfig(staticConfig); + expect(userCMP).to.be.equal('static'); + expect(consentTimeout).to.be.equal(0); // should always return without a timeout when config is used + const consent = gppDataHandler.getConsentData(); + expect(consent.gppString).to.eql(staticConfig.gpp.consentData.gppString); + expect(consent.fullGppData).to.eql(staticConfig.gpp.consentData); + expect(staticConsentData).to.be.equal(staticConfig.gpp.consentData); + }); + }); + }); + + describe('requestBidsHook tests:', function () { + let goodConfig = { + gpp: { + cmpApi: 'iab', + timeout: 7500, + } + }; + + const staticConfig = { + gpp: { + cmpApi: 'static', + timeout: 7500, + consentData: { + gppString: 'abc12345', + applicableSections: [7] + } + } + } + + let didHookReturn; + + beforeEach(resetConsentData); + after(resetConsentData); + + describe('error checks:', function () { + beforeEach(function () { + didHookReturn = false; + sinon.stub(utils, 'logWarn'); + sinon.stub(utils, 'logError'); + }); + + afterEach(function () { + utils.logWarn.restore(); + utils.logError.restore(); + config.resetConfig(); + }); + + it('should throw a warning and return to hooked function when an unknown CMP framework ID is used', function () { + let badCMPConfig = { + gpp: { + cmpApi: 'bad' + } + }; + setConsentConfig(badCMPConfig); + expect(userCMP).to.be.equal(badCMPConfig.gpp.cmpApi); + + requestBidsHook(() => { + didHookReturn = true; + }, {}); + let consent = gppDataHandler.getConsentData(); + + sinon.assert.calledOnce(utils.logWarn); + expect(didHookReturn).to.be.true; + expect(consent).to.be.null; + }); + + it('should call gppDataHandler.setConsentData() when unknown CMP api is used', () => { + setConsentConfig({ + gpp: { + cmpApi: 'invalid' + } + }); + let hookRan = false; + requestBidsHook(() => { + hookRan = true; + }, {}); + expect(hookRan).to.be.true; + expect(gppDataHandler.ready).to.be.true; + }) + + it('should throw proper errors when CMP is not found', function () { + setConsentConfig(goodConfig); + + requestBidsHook(() => { + didHookReturn = true; + }, {}); + let consent = gppDataHandler.getConsentData(); + // throw 2 errors; one for no bidsBackHandler and for CMP not being found (this is an error due to gdpr config) + sinon.assert.calledTwice(utils.logError); + expect(didHookReturn).to.be.false; + expect(consent).to.be.null; + expect(gppDataHandler.ready).to.be.true; + }); + + it('should not trip when adUnits have no size', () => { + setConsentConfig(staticConfig); + let ran = false; + requestBidsHook(() => { + ran = true; + }, { + adUnits: [{ + code: 'test', + mediaTypes: { + video: {} + } + }] + }); + return gppDataHandler.promise.then(() => { + expect(ran).to.be.true; + }); + }); + + it('should continue the auction immediately, without consent data, if timeout is 0', (done) => { + setConsentConfig({ + gpp: { + cmpApi: 'iab', + timeout: 0 + } + }); + window.__gpp = function () {}; + try { + requestBidsHook(() => { + const consent = gppDataHandler.getConsentData(); + expect(consent.applicableSections).to.deep.equal([]); + expect(consent.gppString).to.be.undefined; + done(); + }, {}) + } finally { + delete window.__gpp; + } + }); + }); + + describe('already known consentData:', function () { + let cmpStub = sinon.stub(); + + function mockCMP(cmpResponse) { + return function (...args) { + if (args[0] === 'addEventListener') { + args[1](({ + eventName: 'sectionChange' + })); + } else if (args[0] === 'getGPPData') { + return cmpResponse; + } + } + } + + beforeEach(function () { + didHookReturn = false; + window.__gpp = function () {}; + }); + + afterEach(function () { + config.resetConfig(); + cmpStub.restore(); + delete window.__gpp; + resetConsentData(); + }); + + it('should bypass CMP and simply use previously stored consentData', function () { + let testConsentData = { + applicableSections: [7], + gppString: 'xyz', + }; + + cmpStub = sinon.stub(window, '__gpp').callsFake(mockCMP(testConsentData)); + setConsentConfig(goodConfig); + requestBidsHook(() => {}, {}); + cmpStub.reset(); + + requestBidsHook(() => { + didHookReturn = true; + }, {}); + let consent = gppDataHandler.getConsentData(); + + expect(didHookReturn).to.be.true; + expect(consent.gppString).to.equal(testConsentData.gppString); + expect(consent.applicableSections).to.deep.equal(testConsentData.applicableSections); + sinon.assert.notCalled(cmpStub); + }); + }); + + describe('iframe tests', function () { + let cmpPostMessageCb = () => {}; + let stringifyResponse; + + function createIFrameMarker(frameName) { + let ifr = document.createElement('iframe'); + ifr.width = 0; + ifr.height = 0; + ifr.name = frameName; + document.body.appendChild(ifr); + return ifr; + } + + function creatCmpMessageHandler(prefix, returnEvtValue, returnGPPValue) { + return function (event) { + if (event && event.data) { + let data = event.data; + if (data[`${prefix}Call`]) { + let callId = data[`${prefix}Call`].callId; + let response; + if (data[`${prefix}Call`].command === 'addEventListener') { + response = { + [`${prefix}Return`]: { + callId, + returnValue: returnEvtValue, + success: true + } + } + } else if (data[`${prefix}Call`].command === 'getGPPData') { + response = { + [`${prefix}Return`]: { + callId, + returnValue: returnGPPValue, + success: true + } + } + } + event.source.postMessage(stringifyResponse ? JSON.stringify(response) : response, '*'); + } + } + } + } + + function testIFramedPage(testName, messageFormatString, tarConsentString, tarSections) { + it(`should return the consent string from a postmessage + addEventListener response - ${testName}`, (done) => { + stringifyResponse = messageFormatString; + setConsentConfig(goodConfig); + requestBidsHook(() => { + let consent = gppDataHandler.getConsentData(); + sinon.assert.notCalled(utils.logError); + expect(consent.gppString).to.equal(tarConsentString); + expect(consent.applicableSections).to.deep.equal(tarSections); + done(); + }, {}); + }); + } + + beforeEach(function () { + sinon.stub(utils, 'logError'); + sinon.stub(utils, 'logWarn'); + }); + + afterEach(function () { + utils.logError.restore(); + utils.logWarn.restore(); + config.resetConfig(); + resetConsentData(); + }); + + describe('v2 CMP workflow for iframe pages:', function () { + stringifyResponse = false; + let ifr2 = null; + + beforeEach(function () { + ifr2 = createIFrameMarker('__gppLocator'); + cmpPostMessageCb = creatCmpMessageHandler('__gpp', { + eventName: 'sectionChange' + }, { + gppString: 'abc12345234', + applicableSections: [7] + }); + window.addEventListener('message', cmpPostMessageCb, false); + }); + + afterEach(function () { + delete window.__gpp; // deletes the local copy made by the postMessage CMP call function + document.body.removeChild(ifr2); + window.removeEventListener('message', cmpPostMessageCb); + }); + + testIFramedPage('with/JSON response', false, 'abc12345234', [7]); + testIFramedPage('with/String response', true, 'abc12345234', [7]); + }); + }); + + describe('direct calls to CMP API tests', function () { + let cmpStub = sinon.stub(); + + beforeEach(function () { + didHookReturn = false; + sinon.stub(utils, 'logError'); + sinon.stub(utils, 'logWarn'); + }); + + afterEach(function () { + config.resetConfig(); + cmpStub.restore(); + utils.logError.restore(); + utils.logWarn.restore(); + resetConsentData(); + }); + + describe('v2 CMP workflow for normal pages:', function () { + beforeEach(function () { + window.__gpp = function () {}; + }); + + afterEach(function () { + delete window.__gpp; + }); + + it('performs lookup check and stores consentData for a valid existing user', function () { + let testConsentData = { + gppString: 'abc12345234', + applicableSections: [7] + }; + cmpStub = sinon.stub(window, '__gpp').callsFake((...args) => { + if (args[0] === 'addEventListener') { + args[1]({ + eventName: 'sectionChange' + }); + } else if (args[0] === 'getGPPData') { + return testConsentData; + } + }); + + setConsentConfig(goodConfig); + + requestBidsHook(() => { + didHookReturn = true; + }, {}); + let consent = gppDataHandler.getConsentData(); + sinon.assert.notCalled(utils.logError); + expect(didHookReturn).to.be.true; + expect(consent.gppString).to.equal(testConsentData.gppString); + expect(consent.applicableSections).to.deep.equal(testConsentData.applicableSections); + }); + + it('produces gdpr metadata', function () { + let testConsentData = { + gppString: 'abc12345234', + applicableSections: [7] + }; + cmpStub = sinon.stub(window, '__gpp').callsFake((...args) => { + if (args[0] === 'addEventListener') { + args[1]({ + eventName: 'sectionChange' + }); + } else if (args[0] === 'getGPPData') { + return testConsentData; + } + }); + + setConsentConfig(goodConfig); + + requestBidsHook(() => { + didHookReturn = true; + }, {}); + let consentMeta = gppDataHandler.getConsentMeta(); + sinon.assert.notCalled(utils.logError); + expect(consentMeta.generatedAt).to.be.above(1644367751709); + }); + + it('throws an error when processCmpData check fails + does not call requestBids callback', function () { + let testConsentData = {}; + let bidsBackHandlerReturn = false; + + cmpStub = sinon.stub(window, '__gpp').callsFake((...args) => { + if (args[0] === 'addEventListener') { + args[1]({ + eventName: 'sectionChange' + }); + } else if (args[0] === 'getGPPData') { + return testConsentData; + } + }); + + setConsentConfig(goodConfig); + + sinon.assert.notCalled(utils.logWarn); + sinon.assert.notCalled(utils.logError); + + [utils.logWarn, utils.logError].forEach((stub) => stub.reset()); + + requestBidsHook(() => { + didHookReturn = true; + }, { + bidsBackHandler: () => bidsBackHandlerReturn = true + }); + let consent = gppDataHandler.getConsentData(); + + sinon.assert.calledOnce(utils.logError); + sinon.assert.notCalled(utils.logWarn); + expect(didHookReturn).to.be.false; + expect(bidsBackHandlerReturn).to.be.true; + expect(consent).to.be.null; + expect(gppDataHandler.ready).to.be.true; + }); + + describe('when proper consent is not available', () => { + let gppStub; + + function runAuction() { + setConsentConfig({ + gpp: { + cmpApi: 'iab', + timeout: 10, + } + }); + return new Promise((resolve, reject) => { + requestBidsHook(() => { + didHookReturn = true; + }, {}); + setTimeout(() => didHookReturn ? resolve() : reject(new Error('Auction did not run')), 20); + }) + } + + function mockGppCmp(gppdata) { + gppStub.callsFake((api, cb) => { + if (api === 'addEventListener') { + // eslint-disable-next-line standard/no-callback-literal + cb({ + pingData: { + cmpStatus: 'loaded' + } + }, true); + } + if (api === 'getGPPData') { + return gppdata; + } + }); + } + + beforeEach(() => { + gppStub = sinon.stub(window, '__gpp'); + }); + + afterEach(() => { + gppStub.restore(); + }) + + it('should continue auction with null consent when CMP is unresponsive', () => { + return runAuction().then(() => { + const consent = gppDataHandler.getConsentData(); + expect(consent.applicableSections).to.deep.equal([]); + expect(consent.gppString).to.be.undefined; + expect(gppDataHandler.ready).to.be.true; + }); + }); + + it('should use consent provided by events other than sectionChange', () => { + mockGppCmp({ + gppString: 'mock-consent-string', + applicableSections: [7] + }); + return runAuction().then(() => { + const consent = gppDataHandler.getConsentData(); + expect(consent.applicableSections).to.deep.equal([7]); + expect(consent.gppString).to.equal('mock-consent-string'); + expect(gppDataHandler.ready).to.be.true; + }); + }); + }); + }); + }); + }); +}); diff --git a/test/spec/modules/consentManagementUsp_spec.js b/test/spec/modules/consentManagementUsp_spec.js index f9c3cd5890e..e98486754ab 100644 --- a/test/spec/modules/consentManagementUsp_spec.js +++ b/test/spec/modules/consentManagementUsp_spec.js @@ -8,7 +8,7 @@ import { } from 'modules/consentManagementUsp.js'; import * as utils from 'src/utils.js'; import { config } from 'src/config.js'; -import adapterManager, {uspDataHandler} from 'src/adapterManager.js'; +import adapterManager, {gdprDataHandler, uspDataHandler} from 'src/adapterManager.js'; import 'src/prebid.js'; import {defer} from '../../../src/utils/promise.js'; @@ -508,7 +508,20 @@ describe('consentManagement', function () { sinon.assert.notCalled(adapterManager.callDataDeletionRequest); listener(); sinon.assert.calledOnce(adapterManager.callDataDeletionRequest); - }) + }); + + it('does not fail if CMP does not support registerDeletion', () => { + sandbox.stub(window, '__uspapi').callsFake((cmd, _, cb) => { + if (cmd === 'registerDeletion') { + throw new Error('CMP not compliant'); + } else if (cmd === 'getUSPData') { + // eslint-disable-next-line standard/no-callback-literal + cb({uspString: 'string'}, true); + } + }); + setConsentConfig(goodConfig); + expect(uspDataHandler.getConsentData()).to.eql('string'); + }); }); }); }); diff --git a/test/spec/modules/consentManagement_spec.js b/test/spec/modules/consentManagement_spec.js index 47a2e2ab3d9..2cd3d011e1d 100644 --- a/test/spec/modules/consentManagement_spec.js +++ b/test/spec/modules/consentManagement_spec.js @@ -1,4 +1,15 @@ -import { setConsentConfig, requestBidsHook, resetConsentData, userCMP, consentTimeout, staticConsentData, gdprScope } from 'modules/consentManagement.js'; +import { + setConsentConfig, + requestBidsHook, + resetConsentData, + userCMP, + consentTimeout, + actionTimeout, + staticConsentData, + gdprScope, + loadConsentData, + setActionTimeout +} from 'modules/consentManagement.js'; import { gdprDataHandler } from 'src/adapterManager.js'; import * as utils from 'src/utils.js'; import { config } from 'src/config.js'; @@ -135,16 +146,17 @@ describe('consentManagement', function () { }); describe('static consent string setConsentConfig value', () => { - afterEach(() => { - config.resetConfig(); - }); + Object.entries({ + 'getTCData': (cfg) => ({getTCData: cfg}), + 'consent data directly': (cfg) => cfg, + }).forEach(([t, packageCfg]) => { + describe(`using ${t}`, () => { + afterEach(() => { + config.resetConfig(); + }); - it('results in user settings overriding system defaults for v2 spec', () => { - let staticConfig = { - cmpApi: 'static', - timeout: 7500, - consentData: { - getTCData: { + it('results in user settings overriding system defaults for v2 spec', () => { + const consentData = { 'tcString': 'COuqj-POu90rDBcBkBENAZCgAPzAAAPAACiQFwwBAABAA1ADEAbQC4YAYAAgAxAG0A', 'cmpId': 92, 'cmpVersion': 100, @@ -207,18 +219,22 @@ describe('consentManagement', function () { 'legitimateInterests': {} } } - } - } - }; + }; - setConsentConfig(staticConfig); - expect(userCMP).to.be.equal('static'); - expect(consentTimeout).to.be.equal(0); // should always return without a timeout when config is used - expect(gdprScope).to.be.equal(false); - const consent = gdprDataHandler.getConsentData(); - expect(consent.consentString).to.eql(staticConfig.consentData.getTCData.tcString); - expect(consent.vendorData).to.eql(staticConfig.consentData.getTCData); - expect(staticConsentData).to.be.equal(staticConfig.consentData); + setConsentConfig({ + cmpApi: 'static', + timeout: 7500, + consentData: packageCfg(consentData) + }); + expect(userCMP).to.be.equal('static'); + expect(consentTimeout).to.be.equal(0); // should always return without a timeout when config is used + expect(gdprScope).to.be.equal(false); + const consent = gdprDataHandler.getConsentData(); + expect(consent.consentString).to.eql(consentData.tcString); + expect(consent.vendorData).to.eql(consentData); + expect(staticConsentData).to.be.equal(consentData); + }); + }); }); }); }); @@ -232,9 +248,7 @@ describe('consentManagement', function () { const staticConfig = { cmpApi: 'static', timeout: 7500, - consentData: { - getTCData: {} - } + consentData: {} } let didHookReturn; @@ -674,6 +688,46 @@ describe('consentManagement', function () { }); }); + it('should timeout after actionTimeout from the first CMP event', (done) => { + mockTcfEvent({ + eventStatus: 'cmpuishown', + tcString: 'mock-consent-string', + vendorData: {} + }); + setConsentConfig({ + timeout: 1000, + actionTimeout: 100, + cmpApi: 'iab', + defaultGdprScope: true + }); + let hookRan = false; + requestBidsHook(() => { + hookRan = true; + }, {}); + setTimeout(() => { + expect(hookRan).to.be.true; + done(); + }, 200) + }); + + it('should still pick up consent data when actionTimeout is 0', (done) => { + mockTcfEvent({ + eventStatus: 'tcloaded', + tcString: 'mock-consent-string', + vendorData: {} + }); + setConsentConfig({ + timeout: 1000, + actionTimeout: 0, + cmpApi: 'iab', + defaultGdprScope: true + }); + requestBidsHook(() => { + expect(gdprDataHandler.getConsentData().consentString).to.eql('mock-consent-string'); + done(); + }, {}) + }) + Object.entries({ 'null': null, 'empty': '', diff --git a/test/spec/modules/consumableBidAdapter_spec.js b/test/spec/modules/consumableBidAdapter_spec.js index d1b310624a6..556dce447b9 100644 --- a/test/spec/modules/consumableBidAdapter_spec.js +++ b/test/spec/modules/consumableBidAdapter_spec.js @@ -625,6 +625,52 @@ describe('Consumable BidAdapter', function () { expect(opts.length).to.equal(1); }); + it('should return a sync url if iframe syncs are enabled and GDPR applies', function () { + let gdprConsent = { + consentString: 'GDPR_CONSENT_STRING', + gdprApplies: true, + } + let opts = spec.getUserSyncs(syncOptions, [AD_SERVER_RESPONSE], gdprConsent); + + expect(opts.length).to.equal(1); + expect(opts[0].url).to.equal('https://sync.serverbid.com/ss/730181.html?gdpr=1&gdpr_consent=GDPR_CONSENT_STRING'); + }) + + it('should return a sync url if iframe syncs are enabled and GDPR is undefined', function () { + let gdprConsent = { + consentString: 'GDPR_CONSENT_STRING', + gdprApplies: undefined, + } + let opts = spec.getUserSyncs(syncOptions, [AD_SERVER_RESPONSE], gdprConsent); + + expect(opts.length).to.equal(1); + expect(opts[0].url).to.equal('https://sync.serverbid.com/ss/730181.html?gdpr=0&gdpr_consent=GDPR_CONSENT_STRING'); + }) + + it('should return a sync url if iframe syncs are enabled and USP applies', function () { + let uspConsent = { + consentString: 'USP_CONSENT_STRING', + } + let opts = spec.getUserSyncs(syncOptions, [AD_SERVER_RESPONSE], {}, uspConsent); + + expect(opts.length).to.equal(1); + expect(opts[0].url).to.equal('https://sync.serverbid.com/ss/730181.html?us_privacy=USP_CONSENT_STRING'); + }) + + it('should return a sync url if iframe syncs are enabled, GDPR and USP applies', function () { + let gdprConsent = { + consentString: 'GDPR_CONSENT_STRING', + gdprApplies: true, + } + let uspConsent = { + consentString: 'USP_CONSENT_STRING', + } + let opts = spec.getUserSyncs(syncOptions, [AD_SERVER_RESPONSE], gdprConsent, uspConsent); + + expect(opts.length).to.equal(1); + expect(opts[0].url).to.equal('https://sync.serverbid.com/ss/730181.html?gdpr=1&gdpr_consent=GDPR_CONSENT_STRING&us_privacy=USP_CONSENT_STRING'); + }) + it('should return a sync url if pixel syncs are enabled and some are returned from the server', function () { let syncOptions = {'pixelEnabled': true}; let opts = spec.getUserSyncs(syncOptions, [AD_SERVER_RESPONSE]); diff --git a/test/spec/modules/contentexchangeBidAdapter_spec.js b/test/spec/modules/contentexchangeBidAdapter_spec.js index 368ca8d9e3f..1b3dc4f19c9 100644 --- a/test/spec/modules/contentexchangeBidAdapter_spec.js +++ b/test/spec/modules/contentexchangeBidAdapter_spec.js @@ -79,7 +79,8 @@ describe('ContentexchangeBidAdapter', function () { gdprConsent: 'COvFyGBOvFyGBAbAAAENAPCAAOAAAAAAAAAAAEEUACCKAAA.IFoEUQQgAIQwgIwQABAEAAAAOIAACAIAAAAQAIAgEAACEAAAAAgAQBAAAAAAAGBAAgAAAAAAAFAAECAAAgAAQARAEQAAAAAJAAIAAgAAAYQEAAAQmAgBC3ZAYzUw', refererInfo: { referer: 'https://test.com' - } + }, + timeout: 500 }; describe('isBidRequestValid', function () { diff --git a/test/spec/modules/conversantAnalyticsAdapter_spec.js b/test/spec/modules/conversantAnalyticsAdapter_spec.js index 48b68d09bc7..ce134f7f6af 100644 --- a/test/spec/modules/conversantAnalyticsAdapter_spec.js +++ b/test/spec/modules/conversantAnalyticsAdapter_spec.js @@ -12,6 +12,7 @@ describe('Conversant analytics adapter tests', function() { let sandbox; // sinon sandbox to make restoring all stubbed objects easier let xhr; // xhr stub from sinon for capturing data sent via ajax let clock; // clock stub from sinon to mock our cache cleanup interval + let logInfoStub; const PREBID_VERSION = '1.2'; const SITE_ID = 108060; @@ -21,14 +22,16 @@ describe('Conversant analytics adapter tests', function() { const VALID_CONFIGURATION = { options: { - site_id: SITE_ID + site_id: SITE_ID, + send_error_data: true } }; const VALID_ALWAYS_SAMPLE_CONFIG = { options: { site_id: SITE_ID, - cnvr_sampling: 1 + cnvr_sampling: 1, + send_error_data: true } }; @@ -45,25 +48,32 @@ describe('Conversant analytics adapter tests', function() { }; sandbox.stub(prebidGlobal, 'getGlobal').returns(getGlobalStub); // getGlobal does not seem to be available in testing so need to mock it clock = sandbox.useFakeTimers(DATESTAMP); // to use sinon fake timers they MUST be created before the interval/timeout is created in the code you are testing. + + logInfoStub = sandbox.stub(utils, 'logInfo');/* .callsFake((arg, arg1, arg2) => { //debugging stuff + console.log(arg); + if (arg1) console.log(arg1); + if (arg2) console.log(arg2); + }); */ + + conversantAnalytics.enableAnalytics(VALID_ALWAYS_SAMPLE_CONFIG); }); afterEach(function () { sandbox.restore(); requests = []; // clean up any requests in our ajax request capture array. + conversantAnalytics.disableAnalytics(); }); describe('Initialization Tests', function() { it('should log error if site id is not passed', function() { sandbox.stub(utils, 'logError'); - + conversantAnalytics.disableAnalytics(); conversantAnalytics.enableAnalytics(); expect(utils.logError.calledWith(CNVR_CONSTANTS.LOG_PREFIX + 'siteId is required.')).to.be.true; - conversantAnalytics.disableAnalytics(); }); it('should not log error if valid config is passed', function() { sandbox.stub(utils, 'logError'); - sandbox.stub(utils, 'logInfo'); conversantAnalytics.enableAnalytics(VALID_CONFIGURATION); expect(utils.logError.called).to.equal(false); @@ -78,7 +88,6 @@ describe('Conversant analytics adapter tests', function() { CNVR_CONSTANTS.LOG_PREFIX + 'Global sample rate set to 1' ) ).to.be.true; - conversantAnalytics.disableAnalytics(); }); it('should sample when sampling set to 1', function() { @@ -86,17 +95,16 @@ describe('Conversant analytics adapter tests', function() { conversantAnalytics.enableAnalytics(VALID_ALWAYS_SAMPLE_CONFIG); expect(utils.logError.called).to.equal(false); expect(cnvrHelper.doSample).to.equal(true); - conversantAnalytics.disableAnalytics(); }); it('should NOT sample when sampling set to 0', function() { sandbox.stub(utils, 'logError'); const NEVER_SAMPLE_CONFIG = utils.deepClone(VALID_ALWAYS_SAMPLE_CONFIG); NEVER_SAMPLE_CONFIG['options'].cnvr_sampling = 0; + conversantAnalytics.disableAnalytics(); conversantAnalytics.enableAnalytics(NEVER_SAMPLE_CONFIG); expect(utils.logError.called).to.equal(false); expect(cnvrHelper.doSample).to.equal(false); - conversantAnalytics.disableAnalytics(); }); }); @@ -113,14 +121,19 @@ describe('Conversant analytics adapter tests', function() { cnvrHelper.auctionIdTimestampCache['keep'] = {timeReceived: DATESTAMP + 1}; cnvrHelper.auctionIdTimestampCache['delete'] = {timeReceived: DATESTAMP - CNVR_CONSTANTS.MAX_MILLISECONDS_IN_CACHE}; + cnvrHelper.bidderErrorCache['keep'] = {timeReceived: DATESTAMP + 1, errors: []}; + cnvrHelper.bidderErrorCache['delete'] = {timeReceived: DATESTAMP - CNVR_CONSTANTS.MAX_MILLISECONDS_IN_CACHE, errors: []}; + expect(Object.keys(cnvrHelper.adIdLookup)).to.have.lengthOf(2); expect(Object.keys(cnvrHelper.timeoutCache)).to.have.lengthOf(2); expect(Object.keys(cnvrHelper.auctionIdTimestampCache)).to.have.lengthOf(2); + expect(Object.keys(cnvrHelper.bidderErrorCache)).to.have.lengthOf(2); clock.tick(CNVR_CONSTANTS.CACHE_CLEANUP_TIME_IN_MILLIS); expect(Object.keys(cnvrHelper.adIdLookup)).to.have.lengthOf(1); expect(Object.keys(cnvrHelper.timeoutCache)).to.have.lengthOf(1); expect(Object.keys(cnvrHelper.auctionIdTimestampCache)).to.have.lengthOf(1); + expect(Object.keys(cnvrHelper.bidderErrorCache)).to.have.lengthOf(1); conversantAnalytics.disableAnalytics(); @@ -128,6 +141,7 @@ describe('Conversant analytics adapter tests', function() { expect(Object.keys(cnvrHelper.adIdLookup)).to.have.lengthOf(0); expect(Object.keys(cnvrHelper.timeoutCache)).to.have.lengthOf(0); expect(Object.keys(cnvrHelper.auctionIdTimestampCache)).to.have.lengthOf(0); + expect(Object.keys(cnvrHelper.bidderErrorCache)).to.have.lengthOf(0); }); it('createBid() should return correct object', function() { @@ -173,6 +187,7 @@ describe('Conversant analytics adapter tests', function() { let payload = cnvrHelper.createPayload(REQUEST_TYPE, AUCTION_ID, myDate); expect(payload).to.deep.equal({ + bidderErrors: [], cnvrSampleRate: 1, globalSampleRate: 1, requestType: REQUEST_TYPE, @@ -184,8 +199,6 @@ describe('Conversant analytics adapter tests', function() { }, adUnits: {} }); - - conversantAnalytics.disableAnalytics(); }); it('keyExistsAndIsObject() should return correct data', function() { @@ -236,6 +249,68 @@ describe('Conversant analytics adapter tests', function() { expect(cnvrHelper.getSampleRate(obj, 'not_a_key', DEFAULT_VAL)).to.equal(DEFAULT_VAL); expect(cnvrHelper.getSampleRate(obj, 'too_small', DEFAULT_VAL)).to.equal(0); }); + + it('getPageUrl() should return correct data', function() { + let url = cnvrHelper.getPageUrl(); + expect(url.length).to.be.above(1); + }); + + it('sendErrorData() should send data via ajax', function() { + const error = { + stack: 'foobar', + message: 'foobar message' + }; + const eventType = 'fooType'; + + expect(requests).to.have.lengthOf(0); + cnvrHelper.sendErrorData(eventType, error); + expect(requests).to.have.lengthOf(1); + + expect(requests[0].url).to.contain('cvx/event/prebidanalyticerrors'); + const data = JSON.parse(requests[0].requestBody); + expect(data.event).to.be.equal(eventType); + expect(data.siteId).to.be.equal(SITE_ID); + expect(data.message).to.not.be.undefined; + expect(data.prebidVersion).to.not.be.undefined; + expect(data.userAgent).to.not.be.undefined; + expect(data.url).to.not.be.undefined; + }); + + it('Should not send data when error logging disabled', function() { + const error = { + stack: 'foobar', + message: 'foobar message' + }; + const eventType = 'fooType'; + + conversantAnalytics.disableAnalytics(); + conversantAnalytics.enableAnalytics({ + options: { + site_id: SITE_ID, + cnvr_sampling: 1, + send_error_data: false + } + }); + expect(cnvrHelper.doSendErrorData).to.be.false; + + expect(requests).to.have.lengthOf(0); + cnvrHelper.sendErrorData(eventType, error); + expect(requests).to.have.lengthOf(0); + + conversantAnalytics.disableAnalytics(); + conversantAnalytics.enableAnalytics({ + options: { + site_id: SITE_ID, + cnvr_sampling: 1, + send_error_data: 0 + } + }); + expect(cnvrHelper.doSendErrorData).to.be.false; + + expect(requests).to.have.lengthOf(0); + cnvrHelper.sendErrorData(eventType, error); + expect(requests).to.have.lengthOf(0); + }); }); describe('Bid Timeout Event Tests', function() { @@ -251,14 +326,6 @@ describe('Conversant analytics adapter tests', function() { 'auctionId': 'afbd6e0b-e45b-46ab-87bf-c0bac0cb8881' }]; - beforeEach(function () { - conversantAnalytics.enableAnalytics(VALID_ALWAYS_SAMPLE_CONFIG); - }); - - afterEach(function () { - conversantAnalytics.disableAnalytics(); - }); - it('should put both items in timeout cache', function() { expect(Object.keys(cnvrHelper.timeoutCache)).to.have.lengthOf(0); events.emit(constants.EVENTS.BID_TIMEOUT, BID_TIMEOUT_PAYLOAD); @@ -284,14 +351,6 @@ describe('Conversant analytics adapter tests', function() { message: 'value' }; - beforeEach(function () { - conversantAnalytics.enableAnalytics(VALID_ALWAYS_SAMPLE_CONFIG); - }); - - afterEach(function () { - conversantAnalytics.disableAnalytics(); - }); - it('should empty adIdLookup and send data', function() { cnvrHelper.adIdLookup[RENDER_FAILED_PAYLOAD.adId] = { bidderCode: 'bidderCode', @@ -323,9 +382,18 @@ describe('Conversant analytics adapter tests', function() { expect(Object.keys(cnvrHelper.adIdLookup)).to.have.lengthOf(1); events.emit(constants.EVENTS.AD_RENDER_FAILED, RENDER_FAILED_PAYLOAD_NO_ADID); - expect(requests).to.have.lengthOf(0); + expect(requests).to.have.lengthOf(1); expect(Object.keys(cnvrHelper.adIdLookup)).to.have.lengthOf(1); // same object in cache as before... no change expect(cnvrHelper.adIdLookup[RENDER_FAILED_PAYLOAD.adId]).to.not.be.undefined; + + expect(requests[0].url).to.contain('cvx/event/prebidanalyticerrors'); + const data = JSON.parse(requests[0].requestBody); + expect(data.event).to.be.equal(constants.EVENTS.AD_RENDER_FAILED); + expect(data.siteId).to.be.equal(SITE_ID); + expect(data.message).to.not.be.undefined; + expect(data.prebidVersion).to.not.be.undefined; + expect(data.userAgent).to.not.be.undefined; + expect(data.url).to.not.be.undefined; }); it('should not send data if bad data in lookup', function() { @@ -338,7 +406,16 @@ describe('Conversant analytics adapter tests', function() { expect(Object.keys(cnvrHelper.adIdLookup)).to.have.lengthOf(1); events.emit(constants.EVENTS.AD_RENDER_FAILED, RENDER_FAILED_PAYLOAD); expect(Object.keys(cnvrHelper.adIdLookup)).to.have.lengthOf(0); // object should be removed but no call made to send data - expect(requests).to.have.lengthOf(0); + expect(requests).to.have.lengthOf(1); + + expect(requests[0].url).to.contain('cvx/event/prebidanalyticerrors'); + const data = JSON.parse(requests[0].requestBody); + expect(data.event).to.be.equal(constants.EVENTS.AD_RENDER_FAILED); + expect(data.siteId).to.be.equal(SITE_ID); + expect(data.message).to.not.be.undefined; + expect(data.prebidVersion).to.not.be.undefined; + expect(data.userAgent).to.not.be.undefined; + expect(data.url).to.not.be.undefined; }); }); @@ -414,20 +491,22 @@ describe('Conversant analytics adapter tests', function() { ] }; - beforeEach(function () { - conversantAnalytics.enableAnalytics(VALID_ALWAYS_SAMPLE_CONFIG); - }); - - afterEach(function () { - conversantAnalytics.disableAnalytics(); - }); - it('should not send data or put a record in adIdLookup when bad data provided', function() { expect(requests).to.have.lengthOf(0); expect(Object.keys(cnvrHelper.adIdLookup)).to.have.lengthOf(0); events.emit(constants.EVENTS.BID_WON, BAD_BID_WON_ARGS); - expect(requests).to.have.lengthOf(0); + expect(requests).to.have.lengthOf(1); expect(Object.keys(cnvrHelper.adIdLookup)).to.have.lengthOf(0); + + // check for error event + expect(requests[0].url).to.contain('cvx/event/prebidanalyticerrors'); + const data = JSON.parse(requests[0].requestBody); + expect(data.event).to.be.equal(constants.EVENTS.BID_WON); + expect(data.siteId).to.be.equal(SITE_ID); + expect(data.message).to.not.be.undefined; + expect(data.prebidVersion).to.not.be.undefined; + expect(data.userAgent).to.not.be.undefined; + expect(data.url).to.not.be.undefined; }); it('should send data and put a record in adIdLookup', function() { @@ -834,14 +913,6 @@ describe('Conversant analytics adapter tests', function() { timeout: 3000 }; - beforeEach(function () { - conversantAnalytics.enableAnalytics(VALID_ALWAYS_SAMPLE_CONFIG); - }); - - afterEach(function () { - conversantAnalytics.disableAnalytics(); - }); - it('should not do anything when auction id doesnt exist', function() { sandbox.stub(utils, 'logError'); @@ -849,30 +920,48 @@ describe('Conversant analytics adapter tests', function() { delete BAD_ARGS.auctionId; expect(requests).to.have.lengthOf(0); events.emit(constants.EVENTS.AUCTION_END, BAD_ARGS); - expect(requests).to.have.lengthOf(0); - expect( - utils.logError.calledWith( - CNVR_CONSTANTS.LOG_PREFIX + 'onAuctionEnd(): No auctionId in args supplied so unable to process event.' - ) - ).to.be.true; + expect(requests).to.have.lengthOf(1); + + // check for error event + expect(requests[0].url).to.contain('cvx/event/prebidanalyticerrors'); + const data = JSON.parse(requests[0].requestBody); + expect(data.event).to.be.equal(constants.EVENTS.AUCTION_END); + expect(data.siteId).to.be.equal(SITE_ID); + expect(data.message).to.not.be.undefined; + expect(data.prebidVersion).to.not.be.undefined; + expect(data.userAgent).to.not.be.undefined; + expect(data.url).to.not.be.undefined; }); it('should send the expected data', function() { sandbox.stub(utils, 'logError'); - sandbox.stub(utils, 'logWarn'); /* .callsFake((arg, arg1, arg2) => { //debugging stuff - console.error(arg); - if (arg1) console.error(arg1); - if (arg2) console.error(arg2); - }); */ + sandbox.stub(utils, 'logWarn'); + expect(requests).to.have.lengthOf(0); const AUCTION_ID = AUCTION_END_PAYLOAD.auctionId; const AD_UNIT_CODE = AUCTION_END_PAYLOAD.adUnits[0].code; const AD_UNIT_CODE_NATIVE = AUCTION_END_PAYLOAD.adUnits[2].code; const timeoutKey = cnvrHelper.getLookupKey(AUCTION_ID, AD_UNIT_CODE, 'appnexus'); + const URL = 'some url'; + cnvrHelper.bidderErrorCache[AUCTION_ID] = { + errors: [{ + status: 500, + message: 'error msg', + bidderCode: 'bidderCode', + url: URL, + }, { + status: 501, + message: 'error msg1', + bidderCode: 'bidderCode1', + url: URL, + }], + timeReceived: Date.now() + }; cnvrHelper.timeoutCache[timeoutKey] = { timeReceived: Date.now() }; expect(Object.keys(cnvrHelper.timeoutCache)).to.have.lengthOf(1); expect(utils.logError.called).to.equal(false); expect(Object.keys(cnvrHelper.auctionIdTimestampCache)).to.have.lengthOf(0); + expect(Object.keys(cnvrHelper.bidderErrorCache)).to.have.lengthOf(1); events.emit(constants.EVENTS.AUCTION_END, AUCTION_END_PAYLOAD); expect(utils.logError.called).to.equal(false); @@ -958,6 +1047,69 @@ describe('Conversant analytics adapter tests', function() { expect(apnNativeBid.adSize.w).to.be.undefined; expect(apnNativeBid.adSize.h).to.be.undefined; expect(apnNativeBid.mediaType).to.equal('native'); + + expect(Object.keys(data.bidderErrors)).to.have.lengthOf(2); + expect(data.bidderErrors[0].status).to.equal(500); + expect(data.bidderErrors[0].url).to.equal(URL); + expect(data.bidderErrors[0].message).to.not.be.undefined; + expect(data.bidderErrors[0].bidderCode).to.not.be.undefined; + + expect(data.bidderErrors[1].status).to.equal(501); + expect(data.bidderErrors[1].url).to.equal(URL); + expect(data.bidderErrors[1].message).to.not.be.undefined; + expect(data.bidderErrors[1].bidderCode).to.not.be.undefined; + + expect(Object.keys(cnvrHelper.bidderErrorCache)).to.have.lengthOf(0); + }); + }); + + describe('Bidder Error Tests', function() { + // https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest + const XHR_ERROR_MOCK = { + status: 500, + statusText: 'Internal Server Error' + }; + + // https://docs.prebid.org/dev-docs/bidder-adaptor.html#registering-on-bidder-error + const MOCK_BID_REQUEST = { + auctionId: 'b06c5141-fe8f-4cdf-9d7d-54415490a917', + auctionStart: 1579746300522, + bidderCode: 'myBidderCode', + bidderRequestId: '15246a574e859f', + bids: [{}], + gdprConsent: {consentString: 'BOtmiBKOtmiBKABABAENAFAAAAACeAAA', vendorData: {}, gdprApplies: true}, + refererInfo: { + canonicalUrl: null, + page: 'http://mypage.org?pbjs_debug=true', + domain: 'mypage.org', + ref: null, + numIframes: 0, + reachedTop: true, + isAmp: false, + stack: ['http://mypage.org?pbjs_debug=true'] + } + }; + + it('should record error when bidder_error called', function() { + let warnStub = sandbox.stub(utils, 'logWarn'); + expect(requests).to.have.lengthOf(0); + expect(Object.keys(cnvrHelper.bidderErrorCache)).to.have.lengthOf(0); + expect(warnStub.calledOnce).to.be.false; + + events.emit(constants.EVENTS.BIDDER_ERROR, {'error': XHR_ERROR_MOCK, 'bidderRequest': MOCK_BID_REQUEST}); + expect(Object.keys(cnvrHelper.bidderErrorCache)).to.have.lengthOf(1); + expect(warnStub.calledOnce).to.be.true; + + let errorObj = cnvrHelper.bidderErrorCache[MOCK_BID_REQUEST.auctionId]; + expect(errorObj.errors).to.have.lengthOf(1); + expect(errorObj.errors[0].status).to.equal(XHR_ERROR_MOCK.status); + expect(errorObj.errors[0].message).to.equal(XHR_ERROR_MOCK.statusText); + expect(errorObj.errors[0].bidderCode).to.equal(MOCK_BID_REQUEST.bidderCode); + expect(errorObj.errors[0].url).to.not.be.undefined; + + events.emit(constants.EVENTS.BIDDER_ERROR, {'error': XHR_ERROR_MOCK, 'bidderRequest': MOCK_BID_REQUEST}); + errorObj = cnvrHelper.bidderErrorCache[MOCK_BID_REQUEST.auctionId]; + expect(errorObj.errors).to.have.lengthOf(2); }); }); }); diff --git a/test/spec/modules/conversantBidAdapter_spec.js b/test/spec/modules/conversantBidAdapter_spec.js index c63dc8f9c3b..261414f336d 100644 --- a/test/spec/modules/conversantBidAdapter_spec.js +++ b/test/spec/modules/conversantBidAdapter_spec.js @@ -389,6 +389,14 @@ describe('Conversant adapter tests', function() { expect(payload.device).to.have.property('ua', navigator.userAgent); expect(payload).to.not.have.property('user'); // there should be no user by default + expect(payload).to.not.have.property('tmax'); // there should be no user by default + }); + + it('Verify timeout', () => { + const bidderRequest = { timeout: 9999 }; + const request = spec.buildRequests(bidRequests, bidderRequest); + const payload = request.data; + expect(payload.tmax).equals(bidderRequest.timeout); }); it('Verify first party data', () => { diff --git a/test/spec/modules/cpexIdSystem_spec.js b/test/spec/modules/cpexIdSystem_spec.js index e1320d82a6a..6e004c9f8ca 100644 --- a/test/spec/modules/cpexIdSystem_spec.js +++ b/test/spec/modules/cpexIdSystem_spec.js @@ -1,6 +1,6 @@ -import { cpexIdSubmodule, storage } from 'modules/cpexIdSystem.js'; +import { czechAdIdSubmodule, storage } from 'modules/czechAdIdSystem.js'; -describe('cpexId module', function () { +describe('czechAdId module', function () { let getCookieStub; beforeEach(function (done) { @@ -16,23 +16,23 @@ describe('cpexId module', function () { describe('getId()', function () { it('should return the uid when it exists in cookie', function () { - getCookieStub.withArgs('czaid').returns('cpexIdTest'); - const id = cpexIdSubmodule.getId(); - expect(id).to.be.deep.equal({ id: 'cpexIdTest' }); + getCookieStub.withArgs('czaid').returns('czechAdIdTest'); + const id = czechAdIdSubmodule.getId(); + expect(id).to.be.deep.equal({ id: 'czechAdIdTest' }); }); cookieTestCasesForEmpty.forEach(testCase => it('should not return the uid when it doesnt exist in cookie', function () { getCookieStub.withArgs('czaid').returns(testCase); - const id = cpexIdSubmodule.getId(); + const id = czechAdIdSubmodule.getId(); expect(id).to.be.undefined; })); }); describe('decode()', function () { it('should return the uid when it exists in cookie', function () { - getCookieStub.withArgs('czaid').returns('cpexIdTest'); - const decoded = cpexIdSubmodule.decode(); - expect(decoded).to.be.deep.equal({ cpexId: 'cpexIdTest' }); + getCookieStub.withArgs('czaid').returns('czechAdIdTest'); + const decoded = czechAdIdSubmodule.decode(); + expect(decoded).to.be.deep.equal({ czechAdId: 'czechAdIdTest' }); }); }); }); diff --git a/test/spec/modules/criteoBidAdapter_spec.js b/test/spec/modules/criteoBidAdapter_spec.js index c54453bd7fc..36d04d4b2e0 100755 --- a/test/spec/modules/criteoBidAdapter_spec.js +++ b/test/spec/modules/criteoBidAdapter_spec.js @@ -7,12 +7,9 @@ import { ADAPTER_VERSION, canFastBid, getFastBidUrl, FAST_BID_VERSION_CURRENT } from 'modules/criteoBidAdapter.js'; -import { createBid } from 'src/bidfactory.js'; -import CONSTANTS from 'src/constants.json'; import * as utils from 'src/utils.js'; import * as refererDetection from 'src/refererDetection.js'; import { config } from '../../../src/config.js'; -import * as storageManager from 'src/storageManager.js'; import { BANNER, NATIVE, VIDEO } from '../../../src/mediaTypes.js'; describe('The Criteo bidding adapter', function () { @@ -128,19 +125,13 @@ describe('The Criteo bidding adapter', function () { it('forwards ids from cookies', function () { const cookieData = { 'cto_bundle': 'a', - 'cto_sid': 'b', - 'cto_lwid': 'c', - 'cto_idcpy': 'd', - 'cto_optout': 'e' + 'cto_optout': 'b' }; const expectedHashWithCookieData = { ...expectedHash, ...{ bundle: cookieData['cto_bundle'], - localWebId: cookieData['cto_lwid'], - secureIdCookie: cookieData['cto_sid'], - uid: cookieData['cto_idcpy'], optoutCookie: cookieData['cto_optout'] } }; @@ -158,19 +149,13 @@ describe('The Criteo bidding adapter', function () { it('forwards ids from local storage', function () { const localStorageData = { 'cto_bundle': 'a', - 'cto_sid': 'b', - 'cto_lwid': 'c', - 'cto_idcpy': 'd', - 'cto_optout': 'e' + 'cto_optout': 'b' }; const expectedHashWithLocalStorageData = { ...expectedHash, ...{ bundle: localStorageData['cto_bundle'], - localWebId: localStorageData['cto_lwid'], - secureIdCookie: localStorageData['cto_sid'], - uid: localStorageData['cto_idcpy'], optoutCookie: localStorageData['cto_optout'] } }; @@ -898,6 +883,67 @@ describe('The Criteo bidding adapter', function () { expect(request.data.user.uspIab).to.equal('1YNY'); }); + it('should properly build a request with device sua field', function () { + const sua = {} + const bidRequests = [ + { + bidder: 'criteo', + adUnitCode: 'bid-123', + transactionId: 'transaction-123', + mediaTypes: { + banner: { + sizes: [[728, 90]] + } + }, + params: { + zoneId: 123, + }, + }, + ]; + const bidderRequest = { + timeout: 3000, + uspConsent: '1YNY', + ortb2: { + device: { + sua: sua + } + } + }; + + const request = spec.buildRequests(bidRequests, bidderRequest); + expect(request.data.user.ext.sua).to.not.be.null; + expect(request.data.user.ext.sua).to.equal(sua); + }); + + it('should properly build a request with gpp consent field', function () { + const bidRequests = [ + { + bidder: 'criteo', + adUnitCode: 'bid-123', + transactionId: 'transaction-123', + mediaTypes: { + banner: { + sizes: [[728, 90]] + } + }, + params: { + zoneId: 123, + }, + }, + ]; + const ortb2 = { + regs: { + gpp: 'gpp_consent_string', + gpp_sid: [0, 1, 2] + } + }; + + const request = spec.buildRequests(bidRequests, { ...bidderRequest, ortb2 }); + expect(request.data.regs).to.not.be.null; + expect(request.data.regs.gpp).to.equal('gpp_consent_string'); + expect(request.data.regs.gpp_sid).to.deep.equal([0, 1, 2]); + }); + it('should properly build a request with schain object', function () { const expectedSchain = { someProperty: 'someValue' @@ -1156,6 +1202,18 @@ describe('The Criteo bidding adapter', function () { it('should properly build a request with first party data', function () { const siteData = { keywords: ['power tools'], + content: { + data: [{ + name: 'some_provider', + ext: { + segtax: 3 + }, + segment: [ + { 'id': '1001' }, + { 'id': '1002' } + ] + }] + }, ext: { data: { pageType: 'article' @@ -1164,6 +1222,16 @@ describe('The Criteo bidding adapter', function () { }; const userData = { gender: 'M', + data: [{ + name: 'some_provider', + ext: { + segtax: 3 + }, + segment: [ + { 'id': '1001' }, + { 'id': '1002' } + ] + }], ext: { data: { registered: true @@ -1203,7 +1271,8 @@ describe('The Criteo bidding adapter', function () { const request = spec.buildRequests(bidRequests, { ...bidderRequest, ortb2 }); expect(request.data.publisher.ext).to.deep.equal({ data: { pageType: 'article' } }); - expect(request.data.user.ext).to.deep.equal({ data: { registered: true } }); + expect(request.data.user).to.deep.equal(userData); + expect(request.data.site).to.deep.equal(siteData); expect(request.data.slots[0].ext).to.deep.equal({ bidfloor: 0.75, data: { @@ -1438,7 +1507,7 @@ describe('The Criteo bidding adapter', function () { creativecode: 'test-crId', width: 728, height: 90, - dealCode: 'myDealCode', + deal: 'myDealCode', adomain: ['criteo.com'], }], }, diff --git a/test/spec/modules/criteoIdSystem_spec.js b/test/spec/modules/criteoIdSystem_spec.js index 76d5222c8b2..508a22e8baa 100644 --- a/test/spec/modules/criteoIdSystem_spec.js +++ b/test/spec/modules/criteoIdSystem_spec.js @@ -185,6 +185,44 @@ describe('CriteoId module', function () { expect(setLocalStorageStub.calledWith('cto_pixel_test', 'ok')).to.be.true; }); + it('should call sync pixels and use error handler', function () { + const expirationTs = new Date(nowTimestamp + cookiesMaxAge).toString(); + + const result = criteoIdSubmodule.getId(); + result.callback((id) => { + }); + + const response = { + pixels: [ + { + pixelUrl: 'pixelUrlWithBundle', + writeBundleInStorage: true, + bundlePropertyName: 'abc', + storageKeyName: 'cto_pixel_test' + } + ] + }; + + server.requests[0].respond( + 200, + { 'Content-Type': 'application/json' }, + JSON.stringify(response) + ); + + server.requests[1].respond( + 500, + { 'Content-Type': 'application/json' }, + JSON.stringify({ + abc: 'ok' + }) + ); + + expect(triggerPixelStub.called).to.be.false; + expect(setCookieStub.calledWith('cto_pixel_test', 'ok', expirationTs, null, '.com')).to.be.false; + expect(setCookieStub.calledWith('cto_pixel_test', 'ok', expirationTs, null, '.testdev.com')).to.be.false; + expect(setLocalStorageStub.calledWith('cto_pixel_test', 'ok')).to.be.false; + }); + gdprConsentTestCases.forEach(testCase => it('should call user sync url with the gdprConsent', function () { let callBackSpy = sinon.spy(); let result = criteoIdSubmodule.getId(undefined, testCase.consentData); diff --git a/test/spec/modules/cwireBidAdapter_spec.js b/test/spec/modules/cwireBidAdapter_spec.js index f116b184b8c..88c54212aff 100644 --- a/test/spec/modules/cwireBidAdapter_spec.js +++ b/test/spec/modules/cwireBidAdapter_spec.js @@ -1,382 +1,296 @@ -import { expect } from 'chai'; -import * as utils from '../../../src/utils.js'; -import { config } from '../../../src/config.js'; -import { - spec, - CW_PAGE_VIEW_ID, - ENDPOINT_URL, - RENDERER_URL, -} from '../../../modules/cwireBidAdapter.js'; -import * as prebidGlobal from 'src/prebidGlobal.js'; - -// ------------------------------------ -// Bid Request Builder -// ------------------------------------ - -const BID_DEFAULTS = { - request: { - bidder: 'cwire', - auctionId: 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee', - transactionId: 'txaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee', - bidId: 'bid123445', - bidderRequestId: 'brid12345', - code: 'original-div', - }, - params: { - placementId: 123456, - pageId: 777, - }, - sizes: [[300, 250], [1, 1]], -}; - -const BidderRequestBuilder = function BidderRequestBuilder(options) { - const defaults = { - bidderCode: 'cwire', - auctionId: BID_DEFAULTS.request.auctionId, - bidderRequestId: BID_DEFAULTS.request.bidderRequestId, - transactionId: BID_DEFAULTS.request.transactionId, - timeout: 3000, - }; - - const request = { - ...defaults, - ...options - }; - - this.build = () => request; -}; - -const BidRequestBuilder = function BidRequestBuilder(options, deleteKeys) { - const defaults = JSON.parse(JSON.stringify(BID_DEFAULTS)); - - const request = { - ...defaults.request, - ...options - }; - - if (request && utils.isArray(deleteKeys)) { - deleteKeys.forEach((k) => { - delete request[k]; - }) - } - - this.withParams = (options, deleteKeys) => { - request.params = { - ...defaults.params, - ...options - }; - if (request && utils.isArray(deleteKeys)) { - deleteKeys.forEach((k) => { - delete request.params[k]; - }) - } - return this; - }; - - this.build = () => request; -}; +import {expect} from 'chai'; +import {newBidder} from '../../../src/adapters/bidderFactory'; +import {BID_ENDPOINT, spec, storage} from '../../../modules/cwireBidAdapter'; +import {deepClone, logInfo} from '../../../src/utils'; +import * as utils from 'src/utils.js'; +import {sandbox, stub} from 'sinon'; +import {config} from '../../../src/config'; describe('C-WIRE bid adapter', () => { - let sandbox; - - beforeEach(() => { - sandbox = sinon.createSandbox(); + config.setConfig({debug: true}); + const adapter = newBidder(spec); + let bidRequests = [ + { + 'bidder': 'cwire', + 'params': { + 'pageId': '4057', + 'placementId': 'ad-slot-bla' + }, + 'adUnitCode': 'adunit-code', + 'sizes': [[300, 250], [300, 600]], + 'bidId': '30b31c1838de1e', + 'bidderRequestId': '22edbae2733bf6', + 'auctionId': '1d1a030790a475', + 'transactionId': '04f2659e-c005-4eb1-a57c-fa93145e3843' + } + ]; + const response = { + body: { + 'cwid': '2ef90743-7936-4a82-8acf-e73382a64e94', + 'hash': '17112D98BBF55D3A', + 'bids': [{ + 'html': '

Hello world

', + 'cpm': 100, + 'currency': 'CHF', + 'dimensions': [1, 1], + 'netRevenue': true, + 'creativeId': '3454', + 'requestId': '2c634d4ca5ccfb', + 'placementId': 177, + 'transactionId': 'b4b32618-1350-4828-b6f0-fbb5c329e9a4', + 'ttl': 360 + }] + } + } + describe('inherited functions', function () { + it('exists and is a function', function () { + expect(adapter.callBids).to.exist.and.to.be.a('function'); + expect(spec.isBidRequestValid).to.exist.and.to.be.a('function'); + expect(spec.buildRequests).to.exist.and.to.be.a('function'); + expect(spec.interpretResponse).to.exist.and.to.be.a('function'); + }); }); - - afterEach(() => { - sandbox.restore(); - config.resetConfig(); + describe('buildRequests', function () { + it('sends bid request to ENDPOINT via POST', function () { + const request = spec.buildRequests(bidRequests); + expect(request.url).to.equal(BID_ENDPOINT); + expect(request.method).to.equal('POST'); + }); }); + describe('buildRequests with given creative', function () { + let utilsStub; - // START TESTING - describe('C-WIRE - isBidRequestValid', function () { - it('should return true when required params found', function () { - const bid01 = new BidRequestBuilder().withParams().build(); - expect(spec.isBidRequestValid(bid01)).to.equal(true); + before(function () { + utilsStub = stub(utils, 'getParameterByName').callsFake(function () { + return 'str-str' + }); }); - it('should fail if there is no placementId', function () { - const bid01 = new BidRequestBuilder().withParams().build(); - delete bid01.params.placementId - expect(spec.isBidRequestValid(bid01)).to.equal(false); + after(function () { + utilsStub.restore(); }); - it('should fail if invalid placementId type', function () { - const bid01 = new BidRequestBuilder().withParams().build(); - delete bid01.params.placementId; - bid01.placementId = '322'; - expect(spec.isBidRequestValid(bid01)).to.equal(false); + it('should add creativeId if url parameter given', function () { + // set from bid.params + let bidRequest = deepClone(bidRequests[0]); + + const request = spec.buildRequests([bidRequest]); + const payload = JSON.parse(request.data); + expect(payload.cwcreative).to.exist; + expect(payload.cwcreative).to.deep.equal('str-str'); + }); + }) + + describe('buildRequests reads adUnit offsetWidth and offsetHeight', function () { + before(function () { + const documentStub = sandbox.stub(document, 'getElementById'); + documentStub.withArgs(`${bidRequests[0].adUnitCode}`).returns({ + offsetWidth: 200, + offsetHeight: 250 + }); }); + it('width and height should be set', function () { + let bidRequest = deepClone(bidRequests[0]); - it('should fail if there is no pageId', function () { - const bid01 = new BidRequestBuilder().withParams().build(); - delete bid01.params.pageId - expect(spec.isBidRequestValid(bid01)).to.equal(false); + const request = spec.buildRequests([bidRequest]); + const payload = JSON.parse(request.data); + const el = document.getElementById(`${bidRequest.adUnitCode}`) + + logInfo(JSON.stringify(payload)) + + expect(el).to.exist; + expect(payload.slots[0].cwExt.dimensions.width).to.equal(200); + expect(payload.slots[0].cwExt.dimensions.height).to.equal(250); + expect(payload.slots[0].cwExt.style.maxHeight).to.not.exist; + expect(payload.slots[0].cwExt.style.maxWidth).to.not.exist; + }); + after(function () { + sandbox.restore() + }); + }); + describe('buildRequests reads style attributes', function () { + before(function () { + const documentStub = sandbox.stub(document, 'getElementById'); + documentStub.withArgs(`${bidRequests[0].adUnitCode}`).returns({ + style: { + maxWidth: '400px', + maxHeight: '350px', + } + }); }); + it('css maxWidth should be set', function () { + let bidRequest = deepClone(bidRequests[0]); + + const request = spec.buildRequests([bidRequest]); + const payload = JSON.parse(request.data); + const el = document.getElementById(`${bidRequest.adUnitCode}`) + + logInfo(JSON.stringify(payload)) - it('should fail if invalid pageId type', function () { - const bid01 = new BidRequestBuilder().withParams().build(); - delete bid01.params.pageId; - bid01.params.pageId = '3320'; - expect(spec.isBidRequestValid(bid01)).to.equal(false); + expect(el).to.exist; + expect(payload.slots[0].cwExt.style.maxWidth).to.eq('400px'); + !expect(payload.slots[0].cwExt.style.maxHeight).to.eq('350px'); }); + after(function () { + sandbox.restore() + }); + }); - it('should fail if cwcreative of type number', function () { - const bid01 = new BidRequestBuilder().withParams().build(); - delete bid01.params.cwcreative; - bid01.params.cwcreative = 3320; - expect(spec.isBidRequestValid(bid01)).to.equal(false); + describe('buildRequests reads feature flags', function () { + before(function () { + sandbox.stub(utils, 'getParameterByName').callsFake(function () { + return 'feature1,feature2' + }); }); - it('should pass with valid cwcreative of type string', function () { - const bid01 = new BidRequestBuilder().withParams().build(); - bid01.params.cwcreative = 'i-am-a-string'; - expect(spec.isBidRequestValid(bid01)).to.equal(true); + it('read from url parameter', function () { + let bidRequest = deepClone(bidRequests[0]); + + const request = spec.buildRequests([bidRequest]); + const payload = JSON.parse(request.data); + + logInfo(JSON.stringify(payload)) + + expect(payload.featureFlags).to.exist; + expect(payload.featureFlags).to.include.members(['feature1', 'feature2']); + }); + after(function () { + sandbox.restore() }); }); - describe('C-WIRE - buildRequests()', function () { - it('creates a valid request', function () { - const bid01 = new BidRequestBuilder({ - mediaTypes: { - banner: { - sizes: [[1, 1]], - } - } - }).withParams({ - cwcreative: '54321', - cwapikey: 'xxx-xxx-yyy-zzz-uuid', - refgroups: 'group_1', - }).build(); - - const bidderRequest01 = new BidderRequestBuilder().build(); - - const requests = spec.buildRequests([bid01], bidderRequest01); - - expect(requests.data.slots.length).to.equal(1); - expect(requests.data.cwid).to.be.null; - expect(requests.data.cwid).to.be.null; - expect(requests.data.slots[0].sizes[0]).to.equal('1x1'); - expect(requests.data.cwcreative).to.equal('54321'); - expect(requests.data.cwapikey).to.equal('xxx-xxx-yyy-zzz-uuid'); - expect(requests.data.refgroups[0]).to.equal('group_1'); + describe('buildRequests reads cwgroups flag', function () { + before(function () { + sandbox.stub(utils, 'getParameterByName').callsFake(function () { + return 'group1,group2' + }); }); - it('creates a valid request - read debug params from second bid', function () { - const bid01 = new BidRequestBuilder().withParams().build(); + it('read from url parameter', function () { + let bidRequest = deepClone(bidRequests[0]); - const bid02 = new BidRequestBuilder({ - mediaTypes: { - banner: { - sizes: [[1, 1]], - } - } - }).withParams({ - cwcreative: '1234', - cwapikey: 'api_key_5', - refgroups: 'group_5', - }).build(); - - const bidderRequest01 = new BidderRequestBuilder().build(); - - const requests = spec.buildRequests([bid01, bid02], bidderRequest01); - - expect(requests.data.slots.length).to.equal(2); - expect(requests.data.cwid).to.be.null; - expect(requests.data.cwid).to.be.null; - expect(requests.data.cwcreative).to.equal('1234'); - expect(requests.data.cwapikey).to.equal('api_key_5'); - expect(requests.data.refgroups[0]).to.equal('group_5'); + const request = spec.buildRequests([bidRequest]); + const payload = JSON.parse(request.data); + + logInfo(JSON.stringify(payload)) + + expect(payload.refgroups).to.exist; + expect(payload.refgroups).to.include.members(['group1', 'group2']); + }); + after(function () { + sandbox.restore() }); + }) - it('creates a valid request - read debug params from first bid, ignore second', function () { - const bid01 = new BidRequestBuilder() - .withParams({ - cwcreative: '33', - cwapikey: 'api_key_33', - refgroups: 'group_33', - }).build(); - - const bid02 = new BidRequestBuilder() - .withParams({ - cwcreative: '1234', - cwapikey: 'api_key_5', - refgroups: 'group_5', - }).build(); - - const bidderRequest01 = new BidderRequestBuilder().build(); - - const requests = spec.buildRequests([bid01, bid02], bidderRequest01); - - expect(requests.data.slots.length).to.equal(2); - expect(requests.data.cwid).to.be.null; - expect(requests.data.cwid).to.be.null; - expect(requests.data.cwcreative).to.equal('33'); - expect(requests.data.cwapikey).to.equal('api_key_33'); - expect(requests.data.refgroups[0]).to.equal('group_33'); + describe('buildRequests reads debug flag', function () { + before(function () { + sandbox.stub(utils, 'getParameterByName').callsFake(function () { + return 'true' + }); }); - it('creates a valid request - read debug params from 3 different slots', function () { - const bid01 = new BidRequestBuilder() - .withParams({ - cwcreative: '33', - }).build(); - - const bid02 = new BidRequestBuilder() - .withParams({ - cwapikey: 'api_key_5', - }).build(); - - const bid03 = new BidRequestBuilder() - .withParams({ - refgroups: 'group_5', - }).build(); - const bidderRequest01 = new BidderRequestBuilder().build(); - - const requests = spec.buildRequests([bid01, bid02, bid03], bidderRequest01); - - expect(requests.data.slots.length).to.equal(3); - expect(requests.data.cwid).to.be.null; - expect(requests.data.cwid).to.be.null; - expect(requests.data.cwcreative).to.equal('33'); - expect(requests.data.cwapikey).to.equal('api_key_5'); - expect(requests.data.refgroups[0]).to.equal('group_5'); + it('read from url parameter', function () { + let bidRequest = deepClone(bidRequests[0]); + + const request = spec.buildRequests([bidRequest]); + const payload = JSON.parse(request.data); + + logInfo(JSON.stringify(payload)) + + expect(payload.debug).to.exist; + expect(payload.debug).to.equal(true); + }); + after(function () { + sandbox.restore() }); + }) - it('creates a valid request - config is overriden by URL params', function () { - // for whatever reason stub for getWindowLocation does not work - // so this was the closest way to test for get params - const params = sandbox.stub(utils, 'getParameterByName'); - params.withArgs('cwgroups').returns('group_2'); - params.withArgs('cwcreative').returns('654321'); - - const bid01 = new BidRequestBuilder({ - mediaTypes: { - banner: { - sizes: [[1, 1]], - } - } - }).withParams({ - cwcreative: '54321', - cwapikey: 'xxx-xxx-yyy-zzz', - refgroups: 'group_1', - }).build(); - - const bidderRequest01 = new BidderRequestBuilder().build(); - - const requests = spec.buildRequests([bid01], bidderRequest01); - - expect(requests.data.slots.length).to.equal(1); - expect(requests.data.cwid).to.be.null; - expect(requests.data.cwid).to.be.null; - expect(requests.data.slots[0].sizes[0]).to.equal('1x1'); - expect(requests.data.cwcreative).to.equal('654321'); - expect(requests.data.cwapikey).to.equal('xxx-xxx-yyy-zzz'); - expect(requests.data.refgroups[0]).to.equal('group_2'); + describe('buildRequests reads cw_id from Localstorage', function () { + before(function () { + sandbox.stub(storage, 'localStorageIsEnabled').callsFake(() => true); + sandbox.stub(storage, 'setDataInLocalStorage'); + sandbox.stub(storage, 'getDataFromLocalStorage').callsFake((key) => 'taerfagerg'); }); - it('creates a valid request - if params are not set, null or empty array are sent to the API', function () { - const bid01 = new BidRequestBuilder({ - mediaTypes: { - banner: { - sizes: [[1, 1]], - } - } - }).withParams().build(); + it('cw_id is set', function () { + let bidRequest = deepClone(bidRequests[0]); - const bidderRequest01 = new BidderRequestBuilder().build(); + const request = spec.buildRequests([bidRequest]); + const payload = JSON.parse(request.data); - const requests = spec.buildRequests([bid01], bidderRequest01); + logInfo(JSON.stringify(payload)) - expect(requests.data.slots.length).to.equal(1); - expect(requests.data.cwid).to.be.null; - expect(requests.data.cwid).to.be.null; - expect(requests.data.slots[0].sizes[0]).to.equal('1x1'); - expect(requests.data.cwcreative).to.equal(null); - expect(requests.data.cwapikey).to.equal(null); - expect(requests.data.refgroups.length).to.equal(0); + expect(payload.cwid).to.exist; + expect(payload.cwid).to.equal('taerfagerg'); }); - }); + after(function () { + sandbox.restore() + }); + }) - describe('C-WIRE - interpretResponse()', function () { - const serverResponse = { - body: { - bids: [{ - html: '

AD CONTENT

', - currency: 'CHF', - cpm: 43.37, - dimensions: [1, 1], - netRevenue: true, - creativeId: '1337', - requestId: BID_DEFAULTS.request.bidId, - ttl: 3500, - }], - } - }; - - const expectedResponse = [{ - ad: JSON.parse(JSON.stringify(serverResponse.body.bids[0].html)), - bidderCode: BID_DEFAULTS.request.bidder, - cpm: JSON.parse(JSON.stringify(serverResponse.body.bids[0].cpm)), - creativeId: JSON.parse(JSON.stringify(serverResponse.body.bids[0].creativeId)), - currency: JSON.parse(JSON.stringify(serverResponse.body.bids[0].currency)), - height: JSON.parse(JSON.stringify(serverResponse.body.bids[0].dimensions[0])), - width: JSON.parse(JSON.stringify(serverResponse.body.bids[0].dimensions[1])), - netRevenue: JSON.parse(JSON.stringify(serverResponse.body.bids[0].netRevenue)), - requestId: JSON.parse(JSON.stringify(serverResponse.body.bids[0].requestId)), - ttl: JSON.parse(JSON.stringify(serverResponse.body.bids[0].ttl)), - meta: { - advertiserDomains: [], - }, - mediaType: 'banner', - }] - - it('correctly parses response', function () { - const bid01 = new BidRequestBuilder({ - mediaTypes: { - banner: { - sizes: [[1, 1]], - } - } - }).withParams().build(); + describe('buildRequests maps flattens params for legacy compat', function () { + before(function () { + const documentStub = sandbox.stub(document, 'getElementById'); + documentStub.withArgs(`${bidRequests[0].adUnitCode}`).returns({}); + }); + it('pageId flattened', function () { + let bidRequest = deepClone(bidRequests[0]); - const bidderRequest01 = new BidderRequestBuilder().build(); - const requests = spec.buildRequests([bid01], bidderRequest01); + const request = spec.buildRequests([bidRequest]); + const payload = JSON.parse(request.data); - const response = spec.interpretResponse(serverResponse, requests); - expect(response).to.deep.equal(expectedResponse); + logInfo(JSON.stringify(payload)) + + expect(payload.slots[0].pageId).to.exist; + }); + after(function () { + sandbox.restore() }); + }) - it('attaches renderer', function () { - const bid01 = new BidRequestBuilder({ - mediaTypes: { - video: { - playerSize: [[640, 480]], - context: 'outstream', - } - } - }).withParams().build(); - const bidderRequest01 = new BidderRequestBuilder().build(); + describe('pageId and placementId are required params', function () { + it('invalid request', function () { + let bidRequest = deepClone(bidRequests[0]); + delete bidRequest.params + + const valid = spec.isBidRequestValid(bidRequest); + expect(valid).to.be.false; + }) - const _serverResponse = utils.deepClone(serverResponse); - _serverResponse.body.bids[0].vastXml = ''; + it('valid request', function () { + let bidRequest = deepClone(bidRequests[0]); + bidRequest.params.pageId = 42 + bidRequest.params.placementId = 42 - const _expectedResponse = utils.deepClone(expectedResponse); - _expectedResponse[0].mediaType = 'video'; - _expectedResponse[0].videoScript = JSON.parse(JSON.stringify(_serverResponse.body.bids[0].html)); - _expectedResponse[0].vastXml = JSON.parse(JSON.stringify(_serverResponse.body.bids[0].vastXml)); - delete _expectedResponse[0].ad; + const valid = spec.isBidRequestValid(bidRequest); + expect(valid).to.be.true; + }) - const requests = spec.buildRequests([bid01], bidderRequest01); - expect(requests.data.slots[0].sizes).to.deep.equal(['640x480']); + it('cwcreative must be of type string', function () { + let bidRequest = deepClone(bidRequests[0]); + bidRequest.params.pageId = 42 + bidRequest.params.placementId = 42 - const response = spec.interpretResponse(_serverResponse, requests); - expect(response[0].renderer).to.exist; - expect(response[0].renderer.url).to.equals(RENDERER_URL); - expect(response[0].renderer.loaded).to.equals(false); + const valid = spec.isBidRequestValid(bidRequest); + expect(valid).to.be.true; + }) - delete response[0].renderer; - expect(response).to.deep.equal(_expectedResponse); - }); + it('build request adds pageId', function () { + let bidRequest = deepClone(bidRequests[0]); + + const request = spec.buildRequests([bidRequest]); + const payload = JSON.parse(request.data); + + expect(payload.slots[0].pageId).to.exist; + }) + }); + + describe('process serverResponse', function () { + it('html to ad mapping', function () { + let bidResponse = deepClone(response); + const bids = spec.interpretResponse(bidResponse, {}); + + expect(bids[0].ad).to.exist; + }) }); }); diff --git a/test/spec/modules/datablocksBidAdapter_spec.js b/test/spec/modules/datablocksBidAdapter_spec.js index 8e203510b10..537b82b0e2e 100644 --- a/test/spec/modules/datablocksBidAdapter_spec.js +++ b/test/spec/modules/datablocksBidAdapter_spec.js @@ -2,7 +2,6 @@ import { expect } from 'chai'; import { spec } from '../../../modules/datablocksBidAdapter.js'; import { BotClientTests } from '../../../modules/datablocksBidAdapter.js'; import { getStorageManager } from '../../../src/storageManager.js'; -export let storage = getStorageManager(); const bid = { bidId: '2dd581a2b6281d', diff --git a/test/spec/modules/dfpAdServerVideo_spec.js b/test/spec/modules/dfpAdServerVideo_spec.js index 943b7cc0162..89485adf28b 100644 --- a/test/spec/modules/dfpAdServerVideo_spec.js +++ b/test/spec/modules/dfpAdServerVideo_spec.js @@ -1,7 +1,7 @@ import { expect } from 'chai'; import parse from 'url-parse'; -import { buildDfpVideoUrl, buildAdpodVideoUrl } from 'modules/dfpAdServerVideo.js'; +import {buildDfpVideoUrl, buildAdpodVideoUrl, dep} from 'modules/dfpAdServerVideo.js'; import adUnit from 'test/fixtures/video/adUnit.json'; import * as utils from 'src/utils.js'; import { config } from 'src/config.js'; @@ -13,6 +13,7 @@ import { server } from 'test/mocks/xhr.js'; import * as adServer from 'src/adserver.js'; import {deepClone} from 'src/utils.js'; import {hook} from '../../../src/hook.js'; +import {getRefererInfo} from '../../../src/refererDetection.js'; const bid = { videoCacheKey: 'abc', @@ -27,6 +28,40 @@ describe('The DFP video support module', function () { hook.ready(); }); + let sandbox; + + beforeEach(() => { + sandbox = sinon.sandbox.create(); + }); + + afterEach(() => { + sandbox.restore(); + }); + + Object.entries({ + params: { + params: { + 'iu': 'my/adUnit' + } + }, + url: { + url: 'https://some-example-url.com' + } + }).forEach(([t, options]) => { + describe(`when using ${t}`, () => { + it('should use page location as default for description_url', () => { + sandbox.stub(dep, 'ri').callsFake(() => ({page: 'example.com'})); + + const url = parse(buildDfpVideoUrl(Object.assign({ + adUnit: adUnit, + bid: bid, + }, options))); + const prm = utils.parseQS(url.query); + expect(prm.description_url).to.eql('example.com'); + }) + }) + }) + it('should make a legal request URL when given the required params', function () { const url = parse(buildDfpVideoUrl({ adUnit: adUnit, @@ -63,9 +98,6 @@ describe('The DFP video support module', function () { })); expect(url.host).to.equal('video.adserver.example'); - - const queryObject = utils.parseQS(url.query); - expect(queryObject.description_url).to.equal('vastUrl.example'); }); it('requires a params object or url', function () { diff --git a/test/spec/modules/dspxBidAdapter_spec.js b/test/spec/modules/dspxBidAdapter_spec.js index 2869385d7e7..841fc087613 100644 --- a/test/spec/modules/dspxBidAdapter_spec.js +++ b/test/spec/modules/dspxBidAdapter_spec.js @@ -1,6 +1,7 @@ import { expect } from 'chai'; import { spec } from 'modules/dspxBidAdapter.js'; import { newBidder } from 'src/adapters/bidderFactory.js'; +import { deepClone } from '../../../src/utils'; const ENDPOINT_URL = 'https://buyer.dspx.tv/request/'; const ENDPOINT_URL_DEV = 'https://dcbuyer.dspx.tv/request/'; @@ -65,7 +66,34 @@ describe('dspxAdapter', function () { 'adUnitCode': 'testDiv1', 'userId': { 'netId': '123', - 'uid2': '456' + 'uid2': {'id': '456'}, + 'pubcid': 'e09ab6a3-ae74-4f01-b2e8-81b141d6dc61', + 'id5id': { + 'uid': 'ID5-ZHMOcvSShIBZiIth_yYh9odjNFxVEmMQ_i5TArPfWw!ID5*dtrjfV5mPLasyya5TW2IE9oVzQZwx7xRPGyAYS4hcWkAAOoxoFef4bIoREpQys8x', + 'ext': { + 'linkType': 2 + } + }, + 'sharedid': { + 'id': '01EXPPGZ9C8NKG1MTXVHV98505', + 'third': '01EXPPGZ9C8NKG1MTXVHV98505' + } + }, + 'crumbs': { + 'pubcid': 'e09ab6a3-ae74-4f01-b2e8-81b141d6dc61' + }, + 'schain': { + 'ver': '1.0', + 'complete': 1, + 'nodes': [ + { + 'asi': 'example.com', + 'sid': '0', + 'hp': 1, + 'rid': 'bidrequestid', + 'domain': 'example.com' + } + ] } }, { @@ -111,7 +139,10 @@ describe('dspxAdapter', function () { 'mediaTypes': { 'video': { 'playerSize': [640, 480], - 'context': 'instream' + 'context': 'instream', + 'protocols': [1, 2], + 'playbackmethod': [2], + 'skip': 1 }, 'banner': { 'sizes': [ @@ -135,14 +166,53 @@ describe('dspxAdapter', function () { 'mediaTypes': { 'video': { 'playerSize': [640, 480], - 'context': 'instream' + 'context': 'instream', + 'protocols': [1, 2], + 'playbackmethod': [2], + 'skip': 1, + 'renderer': { + url: 'example.com/videoRenderer.js', + render: function (bid) { alert('test'); } + } } }, 'bidId': '30b31c1838de1e41', 'bidderRequestId': '22edbae2733bf67', 'auctionId': '1d1a030790a478', 'adUnitCode': 'testDiv4' - } + }, + { + 'bidder': 'dspx', + 'params': { + 'placement': '101', + 'devMode': true, + 'dev': { + 'endpoint': 'http://localhost', + 'placement': '107', + 'pfilter': {'test': 1} + } + }, + 'mediaTypes': { + 'video': { + 'playerSize': [640, 480], + 'context': 'instream', + 'mimes': ['video/mp4'], + 'protocols': [1, 2], + 'playbackmethod': [2], + 'skip': 1 + }, + 'banner': { + 'sizes': [ + [300, 250] + ] + } + }, + + 'bidId': '30b31c1838de1e4', + 'bidderRequestId': '22edbae2733bf67', + 'auctionId': '1d1a030790a478', + 'adUnitCode': 'testDiv3' + }, ]; @@ -163,7 +233,7 @@ describe('dspxAdapter', function () { expect(request1.method).to.equal('GET'); expect(request1.url).to.equal(ENDPOINT_URL); let data = request1.data.replace(/rnd=\d+\&/g, '').replace(/ref=.*\&bid/g, 'bid').replace(/pbver=.*?&/g, 'pbver=test&'); - expect(data).to.equal('_f=auto&alternative=prebid_js&inventory_item_id=6682&srw=300&srh=250&idt=100&bid_id=30b31c1838de1e1&pbver=test&pfilter%5Bfloorprice%5D=1000000&pfilter%5Bprivate_auction%5D=0&pfilter%5Bgeo%5D%5Bcountry%5D=DE&pfilter%5Bgdpr_consent%5D=BOJ%2FP2HOJ%2FP2HABABMAAAAAZ%2BA%3D%3D&pfilter%5Bgdpr%5D=true&bcat=IAB2%2CIAB4&dvt=desktop&did_netid=123&did_uid2=456&auctionId=1d1a030790a475&pbcode=testDiv1&media_types%5Bbanner%5D=300x250'); + expect(data).to.equal('_f=auto&alternative=prebid_js&inventory_item_id=6682&srw=300&srh=250&idt=100&bid_id=30b31c1838de1e1&pbver=test&pfilter%5Bfloorprice%5D=1000000&pfilter%5Bprivate_auction%5D=0&pfilter%5Bgeo%5D%5Bcountry%5D=DE&pfilter%5Bgdpr_consent%5D=BOJ%2FP2HOJ%2FP2HABABMAAAAAZ%2BA%3D%3D&pfilter%5Bgdpr%5D=true&bcat=IAB2%2CIAB4&dvt=desktop&did_netid=123&did_id5=ID5-ZHMOcvSShIBZiIth_yYh9odjNFxVEmMQ_i5TArPfWw!ID5*dtrjfV5mPLasyya5TW2IE9oVzQZwx7xRPGyAYS4hcWkAAOoxoFef4bIoREpQys8x&did_id5_linktype=2&did_uid2=456&did_sharedid=01EXPPGZ9C8NKG1MTXVHV98505&did_pubcid=e09ab6a3-ae74-4f01-b2e8-81b141d6dc61&did_cpubcid=e09ab6a3-ae74-4f01-b2e8-81b141d6dc61&schain%5Bver%5D=1.0&schain%5Bcomplete%5D=1&schain%5Bnodes%5D%5B0%5D%5Basi%5D=example.com&schain%5Bnodes%5D%5B0%5D%5Bsid%5D=0&schain%5Bnodes%5D%5B0%5D%5Bhp%5D=1&schain%5Bnodes%5D%5B0%5D%5Brid%5D=bidrequestid&schain%5Bnodes%5D%5B0%5D%5Bdomain%5D=example.com&auctionId=1d1a030790a475&pbcode=testDiv1&media_types%5Bbanner%5D=300x250'); }); var request2 = spec.buildRequests([bidRequests[1]], bidderRequest)[0]; @@ -193,14 +263,61 @@ describe('dspxAdapter', function () { expect(request4.method).to.equal('GET'); expect(request4.url).to.equal(ENDPOINT_URL_DEV); let data = request4.data.replace(/rnd=\d+\&/g, '').replace(/ref=.*\&bid/g, 'bid').replace(/pbver=.*?&/g, 'pbver=test&'); - expect(data).to.equal('_f=auto&alternative=prebid_js&inventory_item_id=101&srw=300&srh=250&idt=100&bid_id=30b31c1838de1e4&pbver=test&prebidDevMode=1&auctionId=1d1a030790a478&pbcode=testDiv3&media_types%5Bvideo%5D=640x480&media_types%5Bbanner%5D=300x250'); + expect(data).to.equal('_f=auto&alternative=prebid_js&inventory_item_id=101&srw=300&srh=250&idt=100&bid_id=30b31c1838de1e4&pbver=test&prebidDevMode=1&auctionId=1d1a030790a478&pbcode=testDiv3&media_types%5Bvideo%5D=640x480&media_types%5Bbanner%5D=300x250&vctx=instream&vpl%5Bprotocols%5D%5B0%5D=1&vpl%5Bprotocols%5D%5B1%5D=2&vpl%5Bplaybackmethod%5D%5B0%5D=2&vpl%5Bskip%5D=1'); }); var request5 = spec.buildRequests([bidRequests[4]], bidderRequestWithoutGdpr)[0]; it('sends bid video request to our endpoint via GET', function () { expect(request5.method).to.equal('GET'); let data = request5.data.replace(/rnd=\d+\&/g, '').replace(/ref=.*\&bid/g, 'bid').replace(/pbver=.*?&/g, 'pbver=test&'); - expect(data).to.equal('_f=auto&alternative=prebid_js&inventory_item_id=101&srw=640&srh=480&idt=100&bid_id=30b31c1838de1e41&pbver=test&vf=vast4&prebidDevMode=1&auctionId=1d1a030790a478&pbcode=testDiv4&media_types%5Bvideo%5D=640x480'); + expect(data).to.equal('_f=auto&alternative=prebid_js&inventory_item_id=101&srw=640&srh=480&idt=100&bid_id=30b31c1838de1e41&pbver=test&prebidDevMode=1&auctionId=1d1a030790a478&pbcode=testDiv4&media_types%5Bvideo%5D=640x480&vctx=instream&vf=vast4&vpl%5Bprotocols%5D%5B0%5D=1&vpl%5Bprotocols%5D%5B1%5D=2&vpl%5Bplaybackmethod%5D%5B0%5D=2&vpl%5Bskip%5D=1'); + }); + + var request6 = spec.buildRequests([bidRequests[5]], bidderRequestWithoutGdpr)[0]; + it('sends bid request without gdprConsent to our DEV endpoint with overriden DEV params via GET', function () { + expect(request6.method).to.equal('GET'); + expect(request6.url).to.equal('http://localhost'); + let data = request6.data.replace(/rnd=\d+\&/g, '').replace(/ref=.*\&bid/g, 'bid').replace(/pbver=.*?&/g, 'pbver=test&'); + expect(data).to.equal('_f=auto&alternative=prebid_js&inventory_item_id=107&srw=300&srh=250&idt=100&bid_id=30b31c1838de1e4&pbver=test&pfilter%5Btest%5D=1&prebidDevMode=1&auctionId=1d1a030790a478&pbcode=testDiv3&media_types%5Bvideo%5D=640x480&media_types%5Bbanner%5D=300x250&vctx=instream&vpl%5Bmimes%5D%5B0%5D=video%2Fmp4&vpl%5Bprotocols%5D%5B0%5D=1&vpl%5Bprotocols%5D%5B1%5D=2&vpl%5Bplaybackmethod%5D%5B0%5D=2&vpl%5Bskip%5D=1'); + }); + + // bidfloor tests + const getFloorResponse = {currency: 'EUR', floor: 5}; + let testBidRequest = deepClone(bidRequests[1]); + let floorRequest = spec.buildRequests([testBidRequest], bidderRequestWithoutGdpr)[0]; + + // 1. getBidFloor not exist AND bidfloor not exist - no floorprice in request + it('bidfloor is not exists in request', function () { + expect(floorRequest.data).to.not.contain('floorprice'); + }); + + // 2. getBidFloor not exist AND pfilter.floorprice exist - use pfilter.floorprice property + it('bidfloor is equal 0.5', function () { + testBidRequest = deepClone(bidRequests[0]); + testBidRequest.params.pfilter = { + 'floorprice': 0.5 + }; + floorRequest = spec.buildRequests([testBidRequest], bidderRequestWithoutGdpr)[0]; + expect(floorRequest.data).to.contain('floorprice%5D=0.5'); + }); + + // 3. getBidFloor exist AND pfilter.floorprice not exist - use getFloor method + it('bidfloor is equal 5', function () { + testBidRequest = deepClone(bidRequests[1]); + testBidRequest.getFloor = () => getFloorResponse; + floorRequest = spec.buildRequests([testBidRequest], bidderRequestWithoutGdpr)[0]; + expect(floorRequest.data).to.contain('floorprice%5D=5'); + }); + + // 4. getBidFloor exist AND pfilter.floorprice exist -> use getFloor method + it('bidfloor is equal 0.35', function () { + testBidRequest = deepClone(bidRequests[0]); + testBidRequest.getFloor = () => getFloorResponse; + testBidRequest.params.pfilter = { + 'floorprice': 0.35 + }; + floorRequest = spec.buildRequests([testBidRequest], bidderRequestWithoutGdpr)[0]; + expect(floorRequest.data).to.contain('floorprice%5D=0.35'); }); }); @@ -212,7 +329,7 @@ describe('dspxAdapter', function () { 'width': '300', 'height': '250', 'type': 'sspHTML', - 'tag': '', + 'adTag': '', 'requestId': '220ed41385952a', 'currency': 'EUR', 'ttl': 60, @@ -233,7 +350,25 @@ describe('dspxAdapter', function () { 'currency': 'EUR', 'ttl': 60, 'netRevenue': true, - 'zone': '6682' + 'zone': '6682', + 'renderer': {id: 1, url: '//player.example.com', options: {}} + } + }; + let serverVideoResponseVastUrl = { + 'body': { + 'cpm': 5000000, + 'crid': 100500, + 'width': '300', + 'height': '250', + 'requestId': '220ed41385952a', + 'type': 'vast2', + 'currency': 'EUR', + 'ttl': 60, + 'netRevenue': true, + 'zone': '6682', + 'vastUrl': 'https://local/vasturl1', + 'videoCacheKey': 'cache_123', + 'bid_appendix': {'someField': 'someValue'} } }; @@ -246,10 +381,10 @@ describe('dspxAdapter', function () { dealId: '', currency: 'EUR', netRevenue: true, - ttl: 300, + ttl: 60, type: 'sspHTML', ad: '', - meta: {advertiserDomains: ['bdomain']} + meta: {advertiserDomains: ['bdomain']}, }, { requestId: '23beaa6af6cdde', cpm: 0.5, @@ -263,7 +398,24 @@ describe('dspxAdapter', function () { type: 'vast2', vastXml: '{"reason":7001,"status":"accepted"}', mediaType: 'video', - meta: {advertiserDomains: []} + meta: {advertiserDomains: []}, + renderer: {} + }, { + requestId: '23beaa6af6cdde', + cpm: 0.5, + width: 0, + height: 0, + creativeId: 100500, + dealId: '', + currency: 'EUR', + netRevenue: true, + ttl: 60, + type: 'vast2', + vastUrl: 'https://local/vasturl1', + videoCacheKey: 'cache_123', + mediaType: 'video', + meta: {advertiserDomains: []}, + someField: 'someValue' }]; it('should get the correct bid response by display ad', function () { @@ -287,7 +439,7 @@ describe('dspxAdapter', function () { 'mediaTypes': { 'video': { 'playerSize': [640, 480], - 'context': 'instream' + 'context': 'outstream' } }, 'data': { @@ -299,6 +451,25 @@ describe('dspxAdapter', function () { expect(result[0].meta.advertiserDomains.length).to.equal(0); }); + it('should get the correct dspx video bid response by display ad (vastUrl)', function () { + let bidRequest = [{ + 'method': 'GET', + 'url': ENDPOINT_URL, + 'mediaTypes': { + 'video': { + 'playerSize': [640, 480], + 'context': 'instream' + } + }, + 'data': { + 'bid_id': '30b31c1838de1e' + } + }]; + let result = spec.interpretResponse(serverVideoResponseVastUrl, bidRequest[0]); + expect(Object.keys(result[0])).to.have.members(Object.keys(expectedResponse[2])); + expect(result[0].meta.advertiserDomains.length).to.equal(0); + }); + it('handles empty bid response', function () { let response = { body: {} diff --git a/test/spec/modules/eids_spec.js b/test/spec/modules/eids_spec.js index 78c8f2b3148..e5e779479cc 100644 --- a/test/spec/modules/eids_spec.js +++ b/test/spec/modules/eids_spec.js @@ -172,6 +172,36 @@ describe('eids array generation for known sub-modules', function() { }); }); + it('bidswitch', function() { + const userId = { + bidswitch: {'id': 'sample_id'} + }; + const newEids = createEidsArray(userId); + expect(newEids.length).to.equal(1); + expect(newEids[0]).to.deep.equal({ + source: 'bidswitch.net', + uids: [{ + id: 'sample_id', + atype: 3 + }] + }); + }); + + it('medianet', function() { + const userId = { + medianet: {'id': 'sample_id'} + }; + const newEids = createEidsArray(userId); + expect(newEids.length).to.equal(1); + expect(newEids[0]).to.deep.equal({ + source: 'media.net', + uids: [{ + id: 'sample_id', + atype: 3 + }] + }); + }); + it('liveIntentId; getValue call and NO ext', function() { const userId = { lipb: { @@ -392,7 +422,7 @@ describe('eids array generation for known sub-modules', function() { const [eid] = createEidsArray(userId); expect(eid).to.deep.equal({ - source: 'amxrtb.com', + source: 'amxdt.net', uids: [{ atype: 1, id, @@ -432,9 +462,9 @@ describe('eids array generation for known sub-modules', function() { }); }); - it('cpexId', () => { + it('czechAdId', () => { const id = 'some-random-id-value' - const userId = { cpexId: id }; + const userId = { czechAdId: id }; const [eid] = createEidsArray(userId); expect(eid).to.deep.equal({ source: 'czechadid.cz', diff --git a/test/spec/modules/emx_digitalBidAdapter_spec.js b/test/spec/modules/emx_digitalBidAdapter_spec.js index d99318b5ddc..d80d0f3e875 100644 --- a/test/spec/modules/emx_digitalBidAdapter_spec.js +++ b/test/spec/modules/emx_digitalBidAdapter_spec.js @@ -707,7 +707,7 @@ describe('emx_digital Adapter', function () { })); }); - it('returns valid advertiser domain', function () { + it('returns valid advertiser domains', function () { const bidResponse = utils.deepClone(serverResponse); let result = spec.interpretResponse({body: bidResponse}); expect(result[0].meta.advertiserDomains).to.deep.equal(expectedResponse[0].meta.advertiserDomains); @@ -724,6 +724,7 @@ describe('emx_digital Adapter', function () { expect(syncs).to.not.be.an('undefined'); expect(syncs).to.have.lengthOf(1); expect(syncs[0].type).to.equal('iframe'); + expect(syncs[0].url).to.equal('https://biddr.brealtime.com/check.html') }); it('should pass gdpr params', function () { @@ -734,6 +735,34 @@ describe('emx_digital Adapter', function () { expect(syncs).to.have.lengthOf(1); expect(syncs[0].type).to.equal('iframe'); expect(syncs[0].url).to.contains('gdpr=0'); + expect(syncs[0].url).to.equal('https://biddr.brealtime.com/check.html?gdpr=0&gdpr_consent=test') + }); + + it('should pass us_privacy string', function () { + let syncs = spec.getUserSyncs({ iframeEnabled: true }, {}, {}, { + consentString: 'test', + }); + expect(syncs).to.not.be.an('undefined'); + expect(syncs).to.have.lengthOf(1); + expect(syncs[0].type).to.equal('iframe'); + expect(syncs[0].url).to.contains('usp=test'); + }); + + it('should pass us_privacy and gdpr strings', function () { + let syncs = spec.getUserSyncs({ iframeEnabled: true }, {}, + { + gdprApplies: true, + consentString: 'test' + }, + { + consentString: 'test' + }); + expect(syncs).to.not.be.an('undefined'); + expect(syncs).to.have.lengthOf(1); + expect(syncs[0].type).to.equal('iframe'); + expect(syncs[0].url).to.contains('gdpr=1'); + expect(syncs[0].url).to.contains('usp=test'); + expect(syncs[0].url).to.equal('https://biddr.brealtime.com/check.html?gdpr=1&gdpr_consent=test&usp=test') }); }); }); diff --git a/test/spec/modules/enrichmentFpdModule_spec.js b/test/spec/modules/enrichmentFpdModule_spec.js index 38853f1528c..e69de29bb2d 100644 --- a/test/spec/modules/enrichmentFpdModule_spec.js +++ b/test/spec/modules/enrichmentFpdModule_spec.js @@ -1,159 +0,0 @@ -import { expect } from 'chai'; -import {config} from 'src/config.js'; -import { getRefererInfo } from 'src/refererDetection.js'; -import {processFpd, coreStorage, resetEnrichments} from 'modules/enrichmentFpdModule.js'; -import * as enrichmentModule from 'modules/enrichmentFpdModule.js'; -import {GreedyPromise} from '../../../src/utils/promise.js'; - -describe('the first party data enrichment module', function() { - let width; - let widthStub; - let height; - let heightStub; - let querySelectorStub; - let coreStorageStub; - let canonical; - let keywords; - let lowEntropySuaStub; - let highEntropySuaStub; - - before(function() { - canonical = document.createElement('link'); - canonical.rel = 'canonical'; - keywords = document.createElement('meta'); - keywords.name = 'keywords'; - }); - - beforeEach(function() { - resetEnrichments(); - querySelectorStub = sinon.stub(window.top.document, 'querySelector'); - querySelectorStub.withArgs("link[rel='canonical']").returns(canonical); - querySelectorStub.withArgs("meta[name='keywords']").returns(keywords); - widthStub = sinon.stub(window.top, 'innerWidth').get(function() { - return width; - }); - heightStub = sinon.stub(window.top, 'innerHeight').get(function() { - return height; - }); - coreStorageStub = sinon.stub(coreStorage, 'getCookie'); - coreStorageStub - .onFirstCall() - .returns(null) // co.uk - .onSecondCall() - .returns('writeable'); // domain.co.uk - lowEntropySuaStub = sinon.stub(enrichmentModule.sua, 'le').callsFake(() => null); - highEntropySuaStub = sinon.stub(enrichmentModule.sua, 'he').callsFake(() => GreedyPromise.resolve()); - }); - - afterEach(function() { - widthStub.restore(); - heightStub.restore(); - querySelectorStub.restore(); - coreStorageStub.restore(); - canonical = document.createElement('link'); - canonical.rel = 'canonical'; - keywords = document.createElement('meta'); - keywords.name = 'keywords'; - lowEntropySuaStub.restore(); - highEntropySuaStub.restore(); - }); - - function syncProcessFpd(fpdConf, ortb2Fragments) { - let result; - processFpd(fpdConf, ortb2Fragments).then((res) => { result = res; }); - return result; - }; - - it('adds ref and device values', function() { - width = 800; - height = 500; - - let validated = syncProcessFpd({}, {}).global; - - const {ref, page, domain} = getRefererInfo(); - expect(validated.site.ref).to.equal(ref || undefined); - expect(validated.site.page).to.equal(page || undefined) - expect(validated.site.domain).to.equal(domain || undefined) - expect(validated.device).to.deep.equal({ w: 800, h: 500 }); - expect(validated.site.keywords).to.be.undefined; - }); - - it('adds page domain values if pageUrl url exists', function() { - config.setConfig({'pageUrl': 'https://www.subdomain.domain.co.uk/path?query=12345'}); - width = 800; - height = 500; - - let validated = syncProcessFpd({}, {}).global; - - expect(validated.site.ref).to.equal(getRefererInfo().ref || undefined); - expect(validated.site.page).to.equal('https://www.subdomain.domain.co.uk/path?query=12345'); - expect(validated.site.domain).to.equal('subdomain.domain.co.uk'); - expect(validated.site.publisher.domain).to.equal('domain.co.uk'); - expect(validated.device).to.deep.equal({ w: 800, h: 500 }); - expect(validated.site.keywords).to.be.undefined; - }); - - it('adds keyword value if keyword meta content exists', function() { - width = 800; - height = 500; - keywords.content = 'value1,value2,value3'; - - let validated = syncProcessFpd({}, {}).global; - - expect(validated.site.keywords).to.equal('value1,value2,value3'); - }); - - it('does not overwrite existing data from getConfig ortb2', function() { - width = 800; - height = 500; - - let validated = syncProcessFpd({}, {global: {device: {w: 1200, h: 700}, site: {ref: 'https://someUrl.com', page: 'test.com'}}}).global; - - expect(validated.site.ref).to.equal('https://someUrl.com'); - expect(validated.site.page).to.equal('test.com'); - expect(validated.device).to.deep.equal({ w: 1200, h: 700 }); - expect(validated.site.keywords).to.be.undefined; - }); - - it('does not run enrichments again on the second call', () => { - width = 1; - height = 2; - const first = syncProcessFpd({}, {}).global; - width = 3; - const second = syncProcessFpd({}, {}).global; - expect(first).to.eql(second); - }); - - describe('device.sua', () => { - it('does not set device.sua if resolved sua is null', () => { - const sua = syncProcessFpd({}, {}).global.device?.sua; - expect(sua).to.not.exist; - }); - it('uses low entropy values if uaHints is []', () => { - lowEntropySuaStub.callsFake(() => ({mock: 'sua'})); - const sua = syncProcessFpd({uaHints: []}, {}).global.device.sua; - expect(sua).to.eql({mock: 'sua'}); - }); - it('uses high entropy values otherwise', () => { - highEntropySuaStub.callsFake((hints) => GreedyPromise.resolve({hints})); - const sua = syncProcessFpd({uaHints: ['h1', 'h2']}, {}).global.device.sua; - expect(sua).to.eql({hints: ['h1', 'h2']}); - }) - }) - - it('should store a reference to gpc witin ortb2.regs.ext if it has been enabled', function() { - let validated; - width = 800; - height = 500; - - validated = syncProcessFpd({}, {}).global; - expect(validated.regs).to.equal(undefined); - - resetEnrichments(); - - const globalPrivacyControlStub = sinon.stub(window, 'navigator').value({globalPrivacyControl: true}); - validated = syncProcessFpd({}, {}).global; - expect(validated.regs.ext.gpc).to.equal(1); - globalPrivacyControlStub.restore(); - }); -}); diff --git a/test/spec/modules/eplanningBidAdapter_spec.js b/test/spec/modules/eplanningBidAdapter_spec.js index cb8393a29b8..1a6cfd7afe4 100644 --- a/test/spec/modules/eplanningBidAdapter_spec.js +++ b/test/spec/modules/eplanningBidAdapter_spec.js @@ -729,11 +729,46 @@ describe('E-Planning Adapter', function () { expect(ur).to.equal(bidderRequest.refererInfo.page); }); + it('should return ur parameter without params query string when current window url length is greater than 255', function () { + let bidderRequestParams = bidderRequest; + + bidderRequestParams.refererInfo.page = refererUrl + '?param=' + 'x'.repeat(255); + const ur = spec.buildRequests(bidRequests, bidderRequest).data.ur; + expect(ur).to.equal(refererUrl); + }); + + it('should return ur parameter with a length of 255 when url length is greater than 255', function () { + let bidderRequestParams = bidderRequest; + let url_255_characters = 'https://localhost/abc' + '/subse'.repeat(39); + let refererUrl = url_255_characters + '/ext'.repeat(5) + '?param=' + 'x'.repeat(15); + + bidderRequestParams.refererInfo.page = refererUrl; + const ur = spec.buildRequests(bidRequests, bidderRequest).data.ur; + expect(ur).to.equal(url_255_characters); + }); + it('should return fr parameter when there is a referrer', function () { const request = spec.buildRequests(bidRequests, bidderRequest); const dataRequest = request.data; expect(dataRequest.fr).to.equal(refererUrl); }); + it('should return fr parameter without params query string when ref length is greater than 255', function () { + let bidderRequestParams = bidderRequest; + + bidderRequestParams.refererInfo.ref = refererUrl + '?param=' + 'x'.repeat(255); + const fr = spec.buildRequests(bidRequests, bidderRequest).data.fr; + expect(fr).to.equal(refererUrl); + }); + + it('should return fr parameter with a length of 255 when url length is greater than 255', function () { + let bidderRequestParams = bidderRequest; + let url_255_characters = 'https://localhost/abc' + '/subse'.repeat(39); + let refererUrl = url_255_characters + '/ext'.repeat(5) + '?param=' + 'x'.repeat(15); + + bidderRequestParams.refererInfo.ref = refererUrl; + const fr = spec.buildRequests(bidRequests, bidderRequest).data.fr; + expect(fr).to.equal(url_255_characters); + }); it('should return crs parameter with document charset', function () { let expected; diff --git a/test/spec/modules/feedadBidAdapter_spec.js b/test/spec/modules/feedadBidAdapter_spec.js index 6db0b88a9bf..5789361d2a1 100644 --- a/test/spec/modules/feedadBidAdapter_spec.js +++ b/test/spec/modules/feedadBidAdapter_spec.js @@ -4,6 +4,7 @@ import {BANNER, NATIVE, VIDEO} from '../../../src/mediaTypes.js'; import {server} from 'test/mocks/xhr.js'; const CODE = 'feedad'; +const EXPECTED_ADAPTER_VERSION = '1.0.6'; describe('FeedAdAdapter', function () { describe('Public API', function () { @@ -300,6 +301,20 @@ describe('FeedAdAdapter', function () { expect(result.data.gdprApplies).to.equal(request.gdprConsent.gdprApplies); expect(result.data.consentIabTcf).to.equal(request.gdprConsent.consentString); }); + it('should include adapter and prebid version', function () { + let bid = { + code: 'feedad', + mediaTypes: { + banner: { + sizes: [[320, 250]] + } + }, + params: {clientToken: 'clientToken', placementId: 'placement-id'} + }; + let result = spec.buildRequests([bid], bidderRequest); + expect(result.data.bids[0].params.prebid_adapter_version).to.equal(EXPECTED_ADAPTER_VERSION); + expect(result.data.bids[0].params.prebid_sdk_version).to.equal('$prebid.version$'); + }); }); describe('interpretResponse', function () { @@ -358,75 +373,67 @@ describe('FeedAdAdapter', function () { const pixelSync2 = {type: 'image', url: 'the pixel url 2'}; const iFrameSync1 = {type: 'iframe', url: 'the iFrame url 1'}; const iFrameSync2 = {type: 'iframe', url: 'the iFrame url 2'}; - - it('should pass through the syncs out of the extension fields of the server response', function () { - const serverResponse = [{ + const response1 = { + body: [{ ext: { pixels: [pixelSync1, pixelSync2], - iframes: [iFrameSync1, iFrameSync2], + iframes: [iFrameSync1] + }, + }] + }; + const response2 = { + body: [{ + ext: { + pixels: [pixelSync1], + iframes: [iFrameSync1], } - }]; - const result = spec.getUserSyncs({iframeEnabled: true, pixelEnabled: true}, serverResponse) + }, { + ext: { + pixels: [pixelSync2], + iframes: [iFrameSync2], + } + }] + }; + it('should pass through the syncs out of the extension fields of the server response', function () { + const result = spec.getUserSyncs({iframeEnabled: true, pixelEnabled: true}, [response1]) expect(result).to.deep.equal([ pixelSync1, pixelSync2, iFrameSync1, - iFrameSync2, ]); }); it('should concat the syncs of all responses', function () { - const serverResponse = [{ - ext: { - pixels: [pixelSync1], - iframes: [iFrameSync2], - }, - ad: 'ad html', - cpm: 100 - }, { - ext: { - iframes: [iFrameSync1], - } - }, { - ext: { - pixels: [pixelSync2], - } - }]; - const result = spec.getUserSyncs({iframeEnabled: true, pixelEnabled: true}, serverResponse); + const result = spec.getUserSyncs({iframeEnabled: true, pixelEnabled: true}, [response1, response2]); expect(result).to.deep.equal([ pixelSync1, + pixelSync2, + iFrameSync1, iFrameSync2, + ]); + }); + + it('should concat the syncs of all bids', function () { + const result = spec.getUserSyncs({iframeEnabled: true, pixelEnabled: true}, [response2]); + expect(result).to.deep.equal([ + pixelSync1, iFrameSync1, pixelSync2, + iFrameSync2, ]); }); it('should filter out duplicates', function () { - const serverResponse = [{ - ext: { - pixels: [pixelSync1, pixelSync1], - iframes: [iFrameSync2, iFrameSync2], - } - }, { - ext: { - iframes: [iFrameSync2, iFrameSync2], - } - }]; - const result = spec.getUserSyncs({iframeEnabled: true, pixelEnabled: true}, serverResponse); + const result = spec.getUserSyncs({iframeEnabled: true, pixelEnabled: true}, [response1, response1]); expect(result).to.deep.equal([ pixelSync1, - iFrameSync2, + pixelSync2, + iFrameSync1, ]); }); it('should not include iFrame syncs if the option is disabled', function () { - const serverResponse = [{ - ext: { - pixels: [pixelSync1, pixelSync2], - iframes: [iFrameSync1, iFrameSync2], - } - }]; - const result = spec.getUserSyncs({iframeEnabled: false, pixelEnabled: true}, serverResponse); + const result = spec.getUserSyncs({iframeEnabled: false, pixelEnabled: true}, [response1]); expect(result).to.deep.equal([ pixelSync1, pixelSync2, @@ -434,27 +441,36 @@ describe('FeedAdAdapter', function () { }); it('should not include pixel syncs if the option is disabled', function () { - const serverResponse = [{ - ext: { - pixels: [pixelSync1, pixelSync2], - iframes: [iFrameSync1, iFrameSync2], - } - }]; - const result = spec.getUserSyncs({iframeEnabled: true, pixelEnabled: false}, serverResponse); + const result = spec.getUserSyncs({iframeEnabled: true, pixelEnabled: false}, [response1]); expect(result).to.deep.equal([ iFrameSync1, - iFrameSync2, ]); }); it('should not include any syncs if the sync options are disabled or missing', function () { - const serverResponse = [{ - ext: { - pixels: [pixelSync1, pixelSync2], - iframes: [iFrameSync1, iFrameSync2], - } - }]; - const result = spec.getUserSyncs({iframeEnabled: false, pixelEnabled: false}, serverResponse); + const result = spec.getUserSyncs({iframeEnabled: false, pixelEnabled: false}, [response1]); + expect(result).to.deep.equal([]); + }); + + it('should handle empty responses', function () { + const result = spec.getUserSyncs({iframeEnabled: true, pixelEnabled: true}, []) + expect(result).to.deep.equal([]); + }); + + it('should not throw if the server response is weird', function () { + const responses = [ + {body: null}, + {body: 'null'}, + {body: 1234}, + {body: {}}, + {body: [{}, 123]}, + ]; + expect(() => spec.getUserSyncs({iframeEnabled: true, pixelEnabled: true}, responses)).to.not.throw(); + }); + + it('should return empty array if the body extension is null', function () { + const response = {body: [{ext: null}]}; + const result = spec.getUserSyncs({iframeEnabled: true, pixelEnabled: true}, [response]); expect(result).to.deep.equal([]); }); }); @@ -592,7 +608,8 @@ describe('FeedAdAdapter', function () { prebid_bid_id: bidId, prebid_transaction_id: transactionId, referer, - sdk_version: '1.0.3' + prebid_adapter_version: EXPECTED_ADAPTER_VERSION, + prebid_sdk_version: '$prebid.version$', }; subject(data); expect(server.requests.length).to.equal(1); diff --git a/test/spec/modules/fledge_spec.js b/test/spec/modules/fledge_spec.js index 2094ab42438..a81ff05596e 100644 --- a/test/spec/modules/fledge_spec.js +++ b/test/spec/modules/fledge_spec.js @@ -2,6 +2,13 @@ import { expect } from 'chai'; import * as fledge from 'modules/fledgeForGpt.js'; +import {config} from '../../../src/config.js'; +import adapterManager from '../../../src/adapterManager.js'; +import * as utils from '../../../src/utils.js'; +import {hook} from '../../../src/hook.js'; +import 'modules/appnexusBidAdapter.js'; +import 'modules/rubiconBidAdapter.js'; +import {parseExtPrebidFledge, setImpExtAe, setResponseFledgeConfigs} from 'modules/fledgeForGpt.js'; const CODE = 'sampleBidder'; const AD_UNIT_CODE = 'mock/placement'; @@ -29,9 +36,173 @@ describe('fledgeForGpt module', function() { nextFnSpy = sinon.spy(); }); - it('should call next() when a proper bidrequest and fledgeAuctionConfig are provided', function() { - fledge.addComponentAuctionHook(nextFnSpy, bidRequest, fledgeAuctionConfig); + it('should call next() when a proper adUnitCode and fledgeAuctionConfig are provided', function() { + fledge.addComponentAuctionHook(nextFnSpy, bidRequest.adUnitCode, fledgeAuctionConfig); expect(nextFnSpy.called).to.be.true; }); }); }); + +describe('fledgeEnabled', function () { + const navProps = Object.fromEntries(['runAdAuction', 'joinAdInterestGroup'].map(p => [p, navigator[p]])) + + before(function () { + // navigator.runAdAuction & co may not exist, so we can't stub it normally with + // sinon.stub(navigator, 'runAdAuction') or something + Object.keys(navProps).forEach(p => { navigator[p] = sinon.stub() }); + hook.ready(); + }); + + after(function() { + Object.entries(navProps).forEach(([p, orig]) => navigator[p] = orig); + }) + + afterEach(function () { + config.resetConfig(); + }); + + it('should set fledgeEnabled correctly per bidder', function () { + config.setConfig({bidderSequence: 'fixed'}) + config.setBidderConfig({ + bidders: ['appnexus'], + config: { + fledgeEnabled: true, + } + }); + + const adUnits = [{ + 'code': '/19968336/header-bid-tag1', + 'mediaTypes': { + 'banner': { + 'sizes': [[728, 90]] + }, + }, + 'bids': [ + { + 'bidder': 'appnexus', + }, + { + 'bidder': 'rubicon', + }, + ] + }]; + + const bidRequests = adapterManager.makeBidRequests( + adUnits, + Date.now(), + utils.getUniqueIdentifierStr(), + function callback() {}, + [] + ); + + expect(bidRequests[0].bids[0].bidder).equals('appnexus'); + expect(bidRequests[0].fledgeEnabled).to.be.true; + + expect(bidRequests[1].bids[0].bidder).equals('rubicon'); + expect(bidRequests[1].fledgeEnabled).to.be.undefined; + }); +}); + +describe('ortb processors for fledge', () => { + describe('imp.ext.ae', () => { + it('should be removed if fledge is not enabled', () => { + const imp = {ext: {ae: 1}}; + setImpExtAe(imp, {}, {bidderRequest: {}}); + expect(imp.ext.ae).to.not.exist; + }) + it('should be left intact if fledge is enabled', () => { + const imp = {ext: {ae: false}}; + setImpExtAe(imp, {}, {bidderRequest: {fledgeEnabled: true}}); + expect(imp.ext.ae).to.equal(false); + }); + }); + describe('parseExtPrebidFledge', () => { + function packageConfigs(configs) { + return { + ext: { + prebid: { + fledge: { + auctionconfigs: configs + } + } + } + } + } + + function generateImpCtx(fledgeFlags) { + return Object.fromEntries(Object.entries(fledgeFlags).map(([impid, fledgeEnabled]) => [impid, {imp: {ext: {ae: fledgeEnabled}}}])); + } + + function generateCfg(impid, ...ids) { + return ids.map((id) => ({impid, config: {id}})); + } + + function extractResult(ctx) { + return Object.fromEntries( + Object.entries(ctx) + .map(([impid, ctx]) => [impid, ctx.fledgeConfigs?.map(cfg => cfg.config.id)]) + .filter(([_, val]) => val != null) + ); + } + + it('should collect fledge configs by imp', () => { + const ctx = { + impContext: generateImpCtx({e1: 1, e2: 1, d1: 0}) + }; + const resp = packageConfigs( + generateCfg('e1', 1, 2, 3) + .concat(generateCfg('e2', 4) + .concat(generateCfg('d1', 5, 6))) + ); + parseExtPrebidFledge({}, resp, ctx); + expect(extractResult(ctx.impContext)).to.eql({ + e1: [1, 2, 3], + e2: [4], + }); + }); + it('should not choke if fledge config references unknown imp', () => { + const ctx = {impContext: generateImpCtx({i: 1})}; + const resp = packageConfigs(generateCfg('unknown', 1)); + parseExtPrebidFledge({}, resp, ctx); + expect(extractResult(ctx.impContext)).to.eql({}); + }); + }); + describe('setResponseFledgeConfigs', () => { + it('should set fledgeAuctionConfigs paired with their corresponding bid id', () => { + const ctx = { + impContext: { + 1: { + bidRequest: {bidId: 'bid1'}, + fledgeConfigs: [{config: {id: 1}}, {config: {id: 2}}] + }, + 2: { + bidRequest: {bidId: 'bid2'}, + fledgeConfigs: [{config: {id: 3}}] + }, + 3: { + bidRequest: {bidId: 'bid3'} + } + } + }; + const resp = {}; + setResponseFledgeConfigs(resp, {}, ctx); + expect(resp.fledgeAuctionConfigs).to.eql([ + {bidId: 'bid1', config: {id: 1}}, + {bidId: 'bid1', config: {id: 2}}, + {bidId: 'bid2', config: {id: 3}}, + ]); + }); + it('should not set fledgeAuctionConfigs if none exist', () => { + const resp = {}; + setResponseFledgeConfigs(resp, {}, { + impContext: { + 1: { + fledgeConfigs: [] + }, + 2: {} + } + }); + expect(resp).to.eql({}); + }); + }); +}); diff --git a/test/spec/modules/fluctBidAdapter_spec.js b/test/spec/modules/fluctBidAdapter_spec.js index 92a4b42f6f8..e9681c05314 100644 --- a/test/spec/modules/fluctBidAdapter_spec.js +++ b/test/spec/modules/fluctBidAdapter_spec.js @@ -54,6 +54,16 @@ describe('fluctAdapter', function () { }); describe('buildRequests', function () { + let sb; + + beforeEach(function () { + sb = sinon.sandbox.create(); + }); + + afterEach(function () { + sb.restore(); + }); + const bidRequests = [{ bidder: 'fluct', params: { @@ -70,7 +80,7 @@ describe('fluctAdapter', function () { }]; const bidderRequest = { refererInfo: { - referer: 'http://example.com' + page: 'http://example.com' } }; @@ -84,6 +94,11 @@ describe('fluctAdapter', function () { expect(request.url).to.equal('https://hb.adingo.jp/prebid?dfpUnitCode=%2F100000%2Funit_code&tagId=10000%3A100000001&groupId=1000000002'); }); + it('includes data.page by default', function () { + const request = spec.buildRequests(bidRequests, bidderRequest)[0]; + expect(request.data.page).to.eql('http://example.com'); + }); + it('includes data.user.eids = [] by default', function () { const request = spec.buildRequests(bidRequests, bidderRequest)[0]; expect(request.data.user.eids).to.eql([]); @@ -99,6 +114,11 @@ describe('fluctAdapter', function () { expect(request.data.schain).to.eql(undefined); }); + it('includes no data.regs by default', function () { + const request = spec.buildRequests(bidRequests, bidderRequest)[0]; + expect(request.data.regs).to.eql(undefined); + }); + it('includes filtered user.eids if any exist', function () { const bidRequests2 = bidRequests.map( (bidReq) => Object.assign(bidReq, { @@ -211,6 +231,44 @@ describe('fluctAdapter', function () { ] }); }); + + it('includes data.regs.gdpr if bidderRequest.gdprConsent exists', function () { + const request = spec.buildRequests( + bidRequests, + Object.assign({}, bidderRequest, { + gdprConsent: { + consentString: 'gdpr-consent-string', + gdprApplies: true, + }, + }), + )[0]; + expect(request.data.regs.gdpr).to.eql({ + consent: 'gdpr-consent-string', + gdprApplies: 1, + }); + }); + + it('includes data.regs.us_privacy if bidderRequest.uspConsent exists', function () { + const request = spec.buildRequests( + bidRequests, + Object.assign({}, bidderRequest, { + uspConsent: 'usp-consent-string', + }), + )[0]; + expect(request.data.regs.us_privacy).to.eql({ + consent: 'usp-consent-string', + }); + }); + + it('includes data.regs.coppa if config.getConfig("coppa") is true', function () { + const cfg = { + coppa: true, + }; + sb.stub(config, 'getConfig').callsFake(key => cfg[key]); + + const request = spec.buildRequests(bidRequests, bidderRequest)[0]; + expect(request.data.regs.coppa).to.eql(1); + }); }); describe('interpretResponse', function() { @@ -247,9 +305,15 @@ describe('fluctAdapter', function () { adm: '', burl: 'https://i.adingo.jp/?test=1&et=hb&bidid=237f4d1a293f99', crid: 'test_creative', - adomain: ['test_adomain'] + adomain: ['test_adomain'], }] - }] + }], + usersyncs: [ + { + 'type': 'image', + 'url': 'https://cs.adingo.jp/sync', + }, + ], } }; @@ -338,4 +402,71 @@ describe('fluctAdapter', function () { expect(spec.interpretResponse({})).to.be.empty; }); }); + + describe('getUserSyncs', function () { + const syncOptions = {}; + const serverResponse = { + body: { + usersyncs: [ + { + type: 'image', + url: 'https://cs.adingo.jp/foo', + }, + { + type: 'image', + url: 'https://cs.adingo.jp/bar', + }, + { + type: 'iframe', + url: 'https://cs.adingo.jp/buz', + }, + ], + }, + }; + + it('returns no user syncs if syncOption.pixelEnabled !== true and syncOption.iframeEnabled !== true', function () { + const actual = spec.getUserSyncs( + syncOptions, + [serverResponse], + ); + + expect(actual).to.eql([]); + }); + + it('returns user syncs if syncOption.pixelEnabled === true', function () { + const actual = spec.getUserSyncs( + Object.assign({}, syncOptions, { + pixelEnabled: true, + }), + [serverResponse], + ); + + expect(actual).to.eql([ + { + type: 'image', + url: 'https://cs.adingo.jp/foo', + }, + { + type: 'image', + url: 'https://cs.adingo.jp/bar', + }, + ]); + }); + + it('returns user syncs if syncOption.iframeEnabled === true', function () { + const actual = spec.getUserSyncs( + Object.assign({}, syncOptions, { + iframeEnabled: true, + }), + [serverResponse], + ); + + expect(actual).to.eql([ + { + type: 'iframe', + url: 'https://cs.adingo.jp/buz', + }, + ]); + }); + }); }); diff --git a/test/spec/modules/fpdModule_spec.js b/test/spec/modules/fpdModule_spec.js index 7d41fbb55e1..4536b304f9d 100644 --- a/test/spec/modules/fpdModule_spec.js +++ b/test/spec/modules/fpdModule_spec.js @@ -1,16 +1,14 @@ import {expect} from 'chai'; import {config} from 'src/config.js'; -import {getRefererInfo} from 'src/refererDetection.js'; -import {processFpd, registerSubmodules, startAuctionHook, reset} from 'modules/fpdModule/index.js'; -import * as enrichmentModule from 'modules/enrichmentFpdModule.js'; -import * as validationModule from 'modules/validationFpdModule/index.js'; -import {resetEnrichments} from 'modules/enrichmentFpdModule.js'; -import {GreedyPromise} from '../../../src/utils/promise.js'; +import {registerSubmodules, reset, startAuctionHook} from 'modules/fpdModule/index.js'; describe('the first party data module', function () { afterEach(function () { config.resetConfig(); }); + after(() => { + reset(); + }) describe('startAuctionHook', () => { const mockFpd = { @@ -50,282 +48,4 @@ describe('the first party data module', function () { }); }); }); - - describe('first party data intitializing', function () { - let width; - let widthStub; - let height; - let heightStub; - let querySelectorStub; - let canonical; - let keywords; - let lowEntropySuaStub; - let highEntropySuaStub; - - before(function() { - reset(); - registerSubmodules(enrichmentModule); - registerSubmodules(validationModule); - - canonical = document.createElement('link'); - canonical.rel = 'canonical'; - keywords = document.createElement('meta'); - keywords.name = 'keywords'; - }); - - beforeEach(function() { - resetEnrichments(); - querySelectorStub = sinon.stub(window.top.document, 'querySelector'); - querySelectorStub.withArgs("link[rel='canonical']").returns(canonical); - querySelectorStub.withArgs("meta[name='keywords']").returns(keywords); - widthStub = sinon.stub(window.top, 'innerWidth').get(function () { - return width; - }); - heightStub = sinon.stub(window.top, 'innerHeight').get(function () { - return height; - }); - lowEntropySuaStub = sinon.stub(enrichmentModule.sua, 'le').callsFake(() => null); - highEntropySuaStub = sinon.stub(enrichmentModule.sua, 'he').callsFake(() => GreedyPromise.resolve()); - }); - - afterEach(function() { - widthStub.restore(); - heightStub.restore(); - querySelectorStub.restore(); - canonical = document.createElement('link'); - canonical.rel = 'canonical'; - keywords = document.createElement('meta'); - keywords.name = 'keywords'; - lowEntropySuaStub.restore(); - highEntropySuaStub.restore(); - }); - - it('filters ortb2 data that is set', function () { - const global = { - user: { - data: {}, - gender: 'f', - age: 45 - }, - site: { - content: { - data: [{ - segment: { - test: 1 - }, - name: 'foo' - }, { - segment: [{ - id: 'test' - }, { - id: 3 - }], - name: 'bar' - }] - } - }, - device: { - w: 1, - h: 1 - } - }; - - config.setConfig({'pageUrl': 'https://www.domain.com/path?query=12345'}); - width = 1120; - height = 750; - - return processFpd({global}).then(({global: validated}) => { - expect(validated.site.ref).to.equal(getRefererInfo().ref || undefined); - expect(validated.site.page).to.equal('https://www.domain.com/path?query=12345'); - expect(validated.site.domain).to.equal('domain.com'); - expect(validated.site.content.data).to.deep.equal([{segment: [{id: 'test'}], name: 'bar'}]); - expect(validated.user.data).to.be.undefined; - expect(validated.device).to.deep.to.equal({w: 1, h: 1}); - expect(validated.site.keywords).to.be.undefined; - }); - }); - - it('should not overwrite existing data with default settings', function () { - const global = { - site: { - ref: 'https://referer.com' - } - }; - - return processFpd({global}).then(({global: validated}) => { - expect(validated.site.ref).to.equal('https://referer.com'); - }); - }); - - it('should allow overwrite default data with setConfig', function () { - const global = { - site: { - ref: 'https://referer.com' - } - }; - - return processFpd({global}).then(({global: validated}) => { - expect(validated.site.ref).to.equal('https://referer.com'); - }); - }); - - it('should filter all data', function () { - let global = { - imp: [], - site: { - name: 123, - domain: 456, - page: 789, - ref: 987, - keywords: ['keywords'], - search: 654, - cat: 'cat', - sectioncat: 'sectioncat', - pagecat: 'pagecat', - content: { - data: [{ - name: 1, - segment: [] - }] - } - }, - user: { - yob: 'twenty', - gender: 0, - keywords: ['foobar'], - data: ['test'] - }, - device: [800, 450], - cur: { - adServerCurrency: 'USD' - } - }; - config.setConfig({'firstPartyData': {skipEnrichments: true}}); - return processFpd({global}).then(({global: validated}) => { - expect(validated).to.deep.equal({}); - }); - }); - - it('should add enrichments but not alter any arbitrary ortb2 data', function () { - let global = { - site: { - ext: { - data: { - inventory: ['value1'] - } - } - }, - user: { - ext: { - data: { - visitor: ['value2'] - } - } - }, - cur: ['USD'] - }; - return processFpd({global}).then(({global: validated}) => { - expect(validated.site.ref).to.equal(getRefererInfo().referer); - expect(validated.site.ext.data).to.deep.equal({inventory: ['value1']}); - expect(validated.user.ext.data).to.deep.equal({visitor: ['value2']}); - expect(validated.cur).to.deep.equal(['USD']); - }) - }); - - it('should filter bidderConfig data', function () { - let bidder = { - bidderA: { - site: { - keywords: 'other', - ref: 'https://domain.com' - }, - user: { - keywords: 'test', - data: [{ - segment: [{id: 4}], - name: 't' - }] - } - } - }; - - return processFpd({bidder}).then(({bidder: validated}) => { - expect(validated.bidderA).to.not.be.undefined; - expect(validated.bidderA.user.data).to.be.undefined; - expect(validated.bidderA.user.keywords).to.equal('test'); - expect(validated.bidderA.site.keywords).to.equal('other'); - expect(validated.bidderA.site.ref).to.equal('https://domain.com'); - }) - }); - - it('should not filter bidderConfig data as it is valid', function () { - let bidder = { - bidderA: { - site: { - keywords: 'other', - ref: 'https://domain.com' - }, - user: { - keywords: 'test', - data: [{ - segment: [{id: 'data1_id'}], - name: 'data1' - }] - } - } - }; - - return processFpd({bidder}).then(({bidder: validated}) => { - expect(validated.bidderA).to.not.be.undefined; - expect(validated.bidderA.user.data).to.deep.equal([{segment: [{id: 'data1_id'}], name: 'data1'}]); - expect(validated.bidderA.user.keywords).to.equal('test'); - expect(validated.bidderA.site.keywords).to.equal('other'); - expect(validated.bidderA.site.ref).to.equal('https://domain.com'); - }); - }); - - it('should not set default values if skipEnrichments is turned on', function () { - config.setConfig({'firstPartyData': {skipEnrichments: true}}); - - let global = { - site: { - keywords: 'other' - }, - user: { - keywords: 'test', - data: [{ - segment: [{id: 'data1_id'}], - name: 'data1' - }] - } - }; - - return processFpd({global}).then(({global: validated}) => { - expect(validated.device).to.be.undefined; - expect(validated.site.ref).to.be.undefined; - expect(validated.site.page).to.be.undefined; - expect(validated.site.domain).to.be.undefined; - }); - }); - - it('should not validate ortb2 data if skipValidations is turned on', function () { - config.setConfig({'firstPartyData': {skipValidations: true}}); - - let global = { - site: { - keywords: 'other' - }, - user: { - keywords: 'test', - data: [{ - segment: [{id: 'nonfiltered'}] - }] - } - }; - - return processFpd({global}).then(({global: validated}) => { - expect(validated.user.data).to.deep.equal([{segment: [{id: 'nonfiltered'}]}]); - }); - }); - }); }); diff --git a/test/spec/modules/freewheel-sspBidAdapter_spec.js b/test/spec/modules/freewheel-sspBidAdapter_spec.js index 7ef576fc7ec..123981825dc 100644 --- a/test/spec/modules/freewheel-sspBidAdapter_spec.js +++ b/test/spec/modules/freewheel-sspBidAdapter_spec.js @@ -1,8 +1,10 @@ import { expect } from 'chai'; import { spec } from 'modules/freewheel-sspBidAdapter.js'; import { newBidder } from 'src/adapters/bidderFactory.js'; +import { createEidsArray } from 'modules/userId/eids.js'; const ENDPOINT = '//ads.stickyadstv.com/www/delivery/swfIndex.php'; +const PREBID_VERSION = '$prebid.version$'; describe('freewheelSSP BidAdapter Test', () => { const adapter = newBidder(spec); @@ -84,7 +86,8 @@ describe('freewheelSSP BidAdapter Test', () => { { 'bidder': 'freewheel-ssp', 'params': { - 'zoneId': '277225' + 'zoneId': '277225', + 'bidfloor': 2.00, }, 'adUnitCode': 'adunit-code', 'mediaTypes': { @@ -114,21 +117,63 @@ describe('freewheelSSP BidAdapter Test', () => { } ]; + it('should get bidfloor value from params if no getFloor method', () => { + const request = spec.buildRequests(bidRequests); + const payload = request[0].data; + expect(payload._fw_bidfloor).to.equal(2.00); + expect(payload._fw_bidfloorcur).to.deep.equal('USD'); + }); + + it('should get bidfloor value from getFloor method if available', () => { + const bidRequest = bidRequests[0]; + bidRequest.getFloor = () => ({ currency: 'USD', floor: 1.16 }); + const request = spec.buildRequests(bidRequests); + const payload = request[0].data; + expect(payload._fw_bidfloor).to.equal(1.16); + expect(payload._fw_bidfloorcur).to.deep.equal('USD'); + }); + + it('should pass 3rd party IDs with the request when present', function () { + const bidRequest = bidRequests[0]; + bidRequest.userIdAsEids = createEidsArray({ + tdid: 'TTD_ID_FROM_USER_ID_MODULE', + admixerId: 'admixerId_FROM_USER_ID_MODULE', + adtelligentId: 'adtelligentId_FROM_USER_ID_MODULE' + }); + const request = spec.buildRequests(bidRequests); + const payload = request[0].data; + expect(payload._fw_prebid_3p_UID).to.deep.equal(JSON.stringify([ + {source: 'adserver.org', uids: [{id: 'TTD_ID_FROM_USER_ID_MODULE', atype: 1, ext: {rtiPartner: 'TDID'}}]}, + {source: 'admixer.net', uids: [{id: 'admixerId_FROM_USER_ID_MODULE', atype: 3}]}, + {source: 'adtelligent.com', uids: [{id: 'adtelligentId_FROM_USER_ID_MODULE', atype: 3}]}, + ])); + }); + + it('should return empty bidFloorCurrency when bidfloor <= 0', () => { + const bidRequest = bidRequests[0]; + bidRequest.getFloor = () => ({ currency: 'USD', floor: -1 }); + const request = spec.buildRequests(bidRequests); + const payload = request[0].data; + expect(payload._fw_bidfloor).to.equal(0); + expect(payload._fw_bidfloorcur).to.deep.equal(''); + }); + it('should add parameters to the tag', () => { const request = spec.buildRequests(bidRequests); const payload = request[0].data; expect(payload.reqType).to.equal('AdsSetup'); - expect(payload.protocolVersion).to.equal('2.0'); + expect(payload.protocolVersion).to.equal('4.2'); expect(payload.zoneId).to.equal('277225'); expect(payload.componentId).to.equal('prebid'); expect(payload.componentSubId).to.equal('mustang'); expect(payload.playerSize).to.equal('300x600'); + expect(payload.pbjs_version).to.equal(PREBID_VERSION); }); it('should return a properly formatted request with schain defined', function () { const request = spec.buildRequests(bidRequests); const payload = request[0].data; - expect(payload.schain).to.deep.equal(bidRequests[0].schain) + expect(payload.schain).to.deep.equal('{\"ver\":\"1.0\",\"complete\":1,\"nodes\":[{\"asi\":\"example.com\",\"sid\":\"0\",\"hp\":1,\"rid\":\"bidrequestid\",\"domain\":\"example.com\"}]}'); }); it('sends bid request to ENDPOINT via GET', () => { @@ -144,7 +189,7 @@ describe('freewheelSSP BidAdapter Test', () => { const request = spec.buildRequests(bidRequests, bidderRequest); const payload = request[0].data; expect(payload.reqType).to.equal('AdsSetup'); - expect(payload.protocolVersion).to.equal('2.0'); + expect(payload.protocolVersion).to.equal('4.2'); expect(payload.zoneId).to.equal('277225'); expect(payload.componentId).to.equal('prebid'); expect(payload.componentSubId).to.equal('mustang'); @@ -164,7 +209,7 @@ describe('freewheelSSP BidAdapter Test', () => { const request = spec.buildRequests(bidRequests, bidderRequest); const payload = request[0].data; expect(payload.reqType).to.equal('AdsSetup'); - expect(payload.protocolVersion).to.equal('2.0'); + expect(payload.protocolVersion).to.equal('4.2'); expect(payload.zoneId).to.equal('277225'); expect(payload.componentId).to.equal('prebid'); expect(payload.componentSubId).to.equal('mustang'); @@ -207,11 +252,18 @@ describe('freewheelSSP BidAdapter Test', () => { } ]; + it('should return context and placement with default values', () => { + const request = spec.buildRequests(bidRequests); + const payload = request[0].data; + expect(payload.video_context).to.equal('instream'); ; + expect(payload.video_placement).to.equal(1); + }); + it('should add parameters to the tag', () => { const request = spec.buildRequests(bidRequests); const payload = request[0].data; expect(payload.reqType).to.equal('AdsSetup'); - expect(payload.protocolVersion).to.equal('2.0'); + expect(payload.protocolVersion).to.equal('4.2'); expect(payload.zoneId).to.equal('277225'); expect(payload.componentId).to.equal('prebid'); expect(payload.componentSubId).to.equal('mustang'); @@ -231,7 +283,7 @@ describe('freewheelSSP BidAdapter Test', () => { const request = spec.buildRequests(bidRequests, bidderRequest); const payload = request[0].data; expect(payload.reqType).to.equal('AdsSetup'); - expect(payload.protocolVersion).to.equal('2.0'); + expect(payload.protocolVersion).to.equal('4.2'); expect(payload.zoneId).to.equal('277225'); expect(payload.componentId).to.equal('prebid'); expect(payload.componentSubId).to.equal('mustang'); @@ -251,7 +303,7 @@ describe('freewheelSSP BidAdapter Test', () => { const request = spec.buildRequests(bidRequests, bidderRequest); const payload = request[0].data; expect(payload.reqType).to.equal('AdsSetup'); - expect(payload.protocolVersion).to.equal('2.0'); + expect(payload.protocolVersion).to.equal('4.2'); expect(payload.zoneId).to.equal('277225'); expect(payload.componentId).to.equal('prebid'); expect(payload.componentSubId).to.equal('mustang'); @@ -274,6 +326,36 @@ describe('freewheelSSP BidAdapter Test', () => { }); }) + describe('buildRequestsForVideoWithContextAndPlacement', () => { + let bidRequests = [ + { + 'bidder': 'freewheel-ssp', + 'params': { + 'zoneId': '277225' + }, + 'adUnitCode': 'adunit-code', + 'mediaTypes': { + 'video': { + 'context': 'outstream', + 'placement': 2, + 'playerSize': [300, 600], + } + }, + 'sizes': [[300, 250], [300, 600]], + 'bidId': '30b31c1838de1e', + 'bidderRequestId': '22edbae2733bf6', + 'auctionId': '1d1a030790a475', + } + ]; + + it('should return input context and placement', () => { + const request = spec.buildRequests(bidRequests); + const payload = request[0].data; + expect(payload.video_context).to.equal('outstream'); ; + expect(payload.video_placement).to.equal(2); + }); + }) + describe('interpretResponseForBanner', () => { let bidRequests = [ { @@ -337,7 +419,7 @@ describe('freewheelSSP BidAdapter Test', () => { } ]; - let response = '' + + let response = '' + '' + ' ' + ' Adswizz' + @@ -422,7 +504,7 @@ describe('freewheelSSP BidAdapter Test', () => { it('handles nobid responses', () => { var request = spec.buildRequests(formattedBidRequests); - let response = ''; + let response = ''; let result = spec.interpretResponse(response, request[0]); expect(result.length).to.equal(0); @@ -503,7 +585,7 @@ describe('freewheelSSP BidAdapter Test', () => { } ]; - let response = '' + + let response = '' + '' + ' ' + ' Adswizz' + @@ -599,7 +681,7 @@ describe('freewheelSSP BidAdapter Test', () => { it('handles nobid responses', () => { var request = spec.buildRequests(formattedBidRequests); - let response = ''; + let response = ''; let result = spec.interpretResponse(response, request[0]); expect(result.length).to.equal(0); diff --git a/test/spec/modules/gdprEnforcement_spec.js b/test/spec/modules/gdprEnforcement_spec.js index 8d58990bb66..941f2b3c8df 100644 --- a/test/spec/modules/gdprEnforcement_spec.js +++ b/test/spec/modules/gdprEnforcement_spec.js @@ -1,26 +1,33 @@ import { deviceAccessHook, - setEnforcementConfig, - userSyncHook, - userIdHook, - makeBidRequestsHook, - validateRules, + enableAnalyticsHook, enforcementRules, + getGvlid, + getGvlidFromAnalyticsAdapter, + makeBidRequestsHook, purpose1Rule, purpose2Rule, - enableAnalyticsHook, - getGvlid, - internal, STRICT_STORAGE_ENFORCEMENT + setEnforcementConfig, + STRICT_STORAGE_ENFORCEMENT, + userIdHook, + userSyncHook, + validateRules } from 'modules/gdprEnforcement.js'; -import { config } from 'src/config.js'; -import adapterManager, { gdprDataHandler } from 'src/adapterManager.js'; +import {config} from 'src/config.js'; +import adapterManager, {gdprDataHandler} from 'src/adapterManager.js'; import * as utils from 'src/utils.js'; -import { validateStorageEnforcement } from 'src/storageManager.js'; +import { + MODULE_TYPE_ANALYTICS, + MODULE_TYPE_BIDDER, + MODULE_TYPE_CORE, + MODULE_TYPE_UID +} from '../../../src/activities/modules.js'; import * as events from 'src/events.js'; import 'modules/appnexusBidAdapter.js'; // some tests expect this to be in the adapter registry -import 'src/prebid.js' +import 'src/prebid.js'; import {hook} from '../../../src/hook.js'; -import {VENDORLESS_GVLID} from '../../../src/consentHandler.js'; +import {GDPR_GVLIDS, VENDORLESS_GVLID} from '../../../src/consentHandler.js'; +import {validateStorageEnforcement} from '../../../src/storageManager.js'; describe('gdpr enforcement', function () { let nextFnSpy; @@ -100,6 +107,7 @@ describe('gdpr enforcement', function () { } } }; + let gvlids; before(() => { hook.ready(); @@ -111,31 +119,28 @@ describe('gdpr enforcement', function () { adapterManager.makeBidRequests.getHooks({ hook: makeBidRequestsHook }).remove(); }) - describe('deviceAccessHook', function () { - let adapterManagerStub; + beforeEach(() => { + gvlids = {}; + sinon.stub(GDPR_GVLIDS, 'get').callsFake((name) => ({gvlid: gvlids[name], modules: {}})); + }); - function getBidderSpec(gvlid) { - return { - getSpec: () => { - return { - gvlid - } - } - } - } + afterEach(() => { + GDPR_GVLIDS.get.restore(); + }); + describe('deviceAccessHook', function () { beforeEach(function () { nextFnSpy = sinon.spy(); gdprDataHandlerStub = sinon.stub(gdprDataHandler, 'getConsentData'); logWarnSpy = sinon.spy(utils, 'logWarn'); - adapterManagerStub = sinon.stub(adapterManager, 'getBidAdapter'); }); + afterEach(function () { config.resetConfig(); gdprDataHandler.getConsentData.restore(); logWarnSpy.restore(); - adapterManagerStub.restore(); }); + it('should not allow device access when device access flag is set to false', function () { config.setConfig({ deviceAccess: false, @@ -161,8 +166,10 @@ describe('gdpr enforcement', function () { }); it('should only check for consent for vendor exceptions when enforcePurpose and enforceVendor are false', function () { - adapterManagerStub.withArgs('appnexus').returns(getBidderSpec(1)); - adapterManagerStub.withArgs('rubicon').returns(getBidderSpec(5)); + Object.assign(gvlids, { + appnexus: 1, + rubicon: 5 + }); setEnforcementConfig({ gdpr: { rules: [{ @@ -179,14 +186,16 @@ describe('gdpr enforcement', function () { consentData.apiVersion = 2; gdprDataHandlerStub.returns(consentData); - deviceAccessHook(nextFnSpy, 1, 'appnexus'); - deviceAccessHook(nextFnSpy, 5, 'rubicon'); + deviceAccessHook(nextFnSpy, MODULE_TYPE_BIDDER, 'appnexus'); + deviceAccessHook(nextFnSpy, MODULE_TYPE_BIDDER, 'rubicon'); expect(logWarnSpy.callCount).to.equal(0); }); it('should check consent for all vendors when enforcePurpose and enforceVendor are true', function () { - adapterManagerStub.withArgs('appnexus').returns(getBidderSpec(1)); - adapterManagerStub.withArgs('rubicon').returns(getBidderSpec(3)); + Object.assign(gvlids, { + appnexus: 1, + rubicon: 3 + }); setEnforcementConfig({ gdpr: { rules: [{ @@ -202,13 +211,13 @@ describe('gdpr enforcement', function () { consentData.apiVersion = 2; gdprDataHandlerStub.returns(consentData); - deviceAccessHook(nextFnSpy, 1, 'appnexus'); - deviceAccessHook(nextFnSpy, 3, 'rubicon'); + deviceAccessHook(nextFnSpy, MODULE_TYPE_BIDDER, 'appnexus'); + deviceAccessHook(nextFnSpy, MODULE_TYPE_BIDDER, 'rubicon'); expect(logWarnSpy.callCount).to.equal(1); }); it('should allow device access when gdprApplies is false and hasDeviceAccess flag is true', function () { - adapterManagerStub.withArgs('appnexus').returns(getBidderSpec(1)); + gvlids.appnexus = 1; setEnforcementConfig({ gdpr: { rules: [{ @@ -225,13 +234,13 @@ describe('gdpr enforcement', function () { consentData.apiVersion = 2; gdprDataHandlerStub.returns(consentData); - deviceAccessHook(nextFnSpy, 1, 'appnexus'); + deviceAccessHook(nextFnSpy, MODULE_TYPE_BIDDER, 'appnexus'); expect(nextFnSpy.calledOnce).to.equal(true); let result = { hasEnforcementHook: true, valid: true } - sinon.assert.calledWith(nextFnSpy, 1, 'appnexus', result); + sinon.assert.calledWith(nextFnSpy, MODULE_TYPE_BIDDER, 'appnexus', result); }); it('should use gvlMapping set by publisher', function() { @@ -256,13 +265,13 @@ describe('gdpr enforcement', function () { consentData.apiVersion = 2; gdprDataHandlerStub.returns(consentData); - deviceAccessHook(nextFnSpy, 1, 'appnexus'); + deviceAccessHook(nextFnSpy, MODULE_TYPE_BIDDER, 'appnexus'); expect(nextFnSpy.calledOnce).to.equal(true); let result = { hasEnforcementHook: true, valid: true } - sinon.assert.calledWith(nextFnSpy, 4, 'appnexus', result); + sinon.assert.calledWith(nextFnSpy, MODULE_TYPE_BIDDER, 'appnexus', result); config.resetConfig(); }); @@ -291,13 +300,13 @@ describe('gdpr enforcement', function () { consentData.apiVersion = 2; gdprDataHandlerStub.returns(consentData); - deviceAccessHook(nextFnSpy, 1, 'appnexus'); + deviceAccessHook(nextFnSpy, MODULE_TYPE_BIDDER, 'appnexus'); expect(nextFnSpy.calledOnce).to.equal(true); let result = { hasEnforcementHook: true, valid: true } - sinon.assert.calledWith(nextFnSpy, 4, 'appnexus', result); + sinon.assert.calledWith(nextFnSpy, MODULE_TYPE_BIDDER, 'appnexus', result); config.resetConfig(); curBidderStub.restore(); }); @@ -310,9 +319,9 @@ describe('gdpr enforcement', function () { } gdprDataHandlerStub.returns(consentData); const validate = sinon.stub().callsFake(() => false); - deviceAccessHook(nextFnSpy, VENDORLESS_GVLID, 'mockModule', undefined, {validate}); + deviceAccessHook(nextFnSpy, MODULE_TYPE_CORE, 'mockModule', undefined, {validate}); sinon.assert.callCount(validate, 0); - sinon.assert.calledWith(nextFnSpy, VENDORLESS_GVLID, 'mockModule', {hasEnforcementHook: true, valid: true}); + sinon.assert.calledWith(nextFnSpy, MODULE_TYPE_CORE, 'mockModule', {hasEnforcementHook: true, valid: true}); }) }); @@ -354,23 +363,11 @@ describe('gdpr enforcement', function () { gdprDataHandlerStub.returns(consentData); curBidderStub.returns('sampleBidder1'); - adapterManagerStub.withArgs('sampleBidder1').returns({ - getSpec: function () { - return { - 'gvlid': 1 - } - } - }); + gvlids.sampleBidder1 = 1; userSyncHook(nextFnSpy); curBidderStub.returns('sampleBidder2'); - adapterManagerStub.withArgs('sampleBidder2').returns({ - getSpec: function () { - return { - 'gvlid': 3 - } - } - }); + gvlids.sampleBidder2 = 3; userSyncHook(nextFnSpy); expect(nextFnSpy.calledTwice).to.equal(true); }); @@ -393,23 +390,11 @@ describe('gdpr enforcement', function () { gdprDataHandlerStub.returns(consentData); curBidderStub.returns('sampleBidder1'); - adapterManagerStub.withArgs('sampleBidder1').returns({ - getSpec: function () { - return { - 'gvlid': 1 - } - } - }); + gvlids.sampleBidder1 = 1; userSyncHook(nextFnSpy); curBidderStub.returns('sampleBidder2'); - adapterManagerStub.withArgs('sampleBidder2').returns({ - getSpec: function () { - return { - 'gvlid': 3 - } - } - }); + gvlids.sampleBidder2 = 3; userSyncHook(nextFnSpy); expect(nextFnSpy.calledOnce).to.equal(true); expect(logWarnSpy.callCount).to.equal(1); @@ -433,23 +418,11 @@ describe('gdpr enforcement', function () { gdprDataHandlerStub.returns(consentData); curBidderStub.returns('sampleBidder1'); - adapterManagerStub.withArgs('sampleBidder1').returns({ - getSpec: function () { - return { - 'gvlid': 1 - } - } - }); + gvlids.sampleBidder1 = 1; userSyncHook(nextFnSpy); curBidderStub.returns('sampleBidder2'); - adapterManagerStub.withArgs('sampleBidder2').returns({ - getSpec: function () { - return { - 'gvlid': 3 - } - } - }); + gvlids.sampleBidder2 = 3; userSyncHook(nextFnSpy); expect(nextFnSpy.calledTwice).to.equal(true); expect(logWarnSpy.callCount).to.equal(0); @@ -486,6 +459,7 @@ describe('gdpr enforcement', function () { name: 'sampleUserId' } }] + gvlids.sampleUserId = 1; userIdHook(nextFnSpy, submodules, consentData); // Should pass back hasValidated flag since version 2 const args = nextFnSpy.getCalls()[0].args; @@ -501,6 +475,7 @@ describe('gdpr enforcement', function () { name: 'sampleUserId' } }]; + gvlids.sampleUserId = 1; let consentData = null; userIdHook(nextFnSpy, submodules, consentData); // Should not pass back hasValidated flag since version 2 @@ -537,6 +512,10 @@ describe('gdpr enforcement', function () { name: 'sampleUserId1' } }] + Object.assign(gvlids, { + sampleUserId: 1, + sampleUserId1: 3 + }); userIdHook(nextFnSpy, submodules, consentData); expect(logWarnSpy.callCount).to.equal(1); let expectedSubmodules = [{ @@ -602,20 +581,9 @@ describe('gdpr enforcement', function () { consentData.gdprApplies = true; gdprDataHandlerStub.returns(consentData); - adapterManagerStub.withArgs('bidder_1').returns({ - getSpec: function () { - return { 'gvlid': 4 } - } - }); - adapterManagerStub.withArgs('bidder_2').returns({ - getSpec: function () { - return { 'gvlid': 5 } - } - }); - adapterManagerStub.withArgs('bidder_3').returns({ - getSpec: function () { - return { 'gvlid': undefined } - } + Object.assign(gvlids, { + bidder_1: 4, + bidder_2: 5, }); makeBidRequestsHook(nextFnSpy, MOCK_AD_UNITS, []); @@ -660,21 +628,10 @@ describe('gdpr enforcement', function () { consentData.gdprApplies = true; gdprDataHandlerStub.returns(consentData); - adapterManagerStub.withArgs('bidder_1').returns({ - getSpec: function () { - return { 'gvlid': 4 } - } - }); - adapterManagerStub.withArgs('bidder_2').returns({ - getSpec: function () { - return { 'gvlid': 5 } - } - }); - adapterManagerStub.withArgs('bidder_3').returns({ - getSpec: function () { - return { 'gvlid': undefined } - } - }); + Object.assign(gvlids, { + bidder_1: 4, + bidder_2: 5, + }) makeBidRequestsHook(nextFnSpy, MOCK_AD_UNITS, []); @@ -771,9 +728,11 @@ describe('gdpr enforcement', function () { consentData.gdprApplies = true; gdprDataHandlerStub.returns(consentData); - adapterManagerStub.withArgs('analyticsAdapter_A').returns({ gvlid: 3 }); - adapterManagerStub.withArgs('analyticsAdapter_B').returns({ gvlid: 5 }); - adapterManagerStub.withArgs('analyticsAdapter_C').returns({ gvlid: 1 }); + Object.assign(gvlids, { + analyticsAdapter_A: 3, + analyticsAdapter_B: 5, + analyticsAdapter_C: 1 + }); enableAnalyticsHook(nextFnSpy, MOCK_ANALYTICS_ADAPTER_CONFIG); @@ -1142,13 +1101,13 @@ describe('gdpr enforcement', function () { }); describe('getGvlid', function() { - let getGvlidForBidAdapterStub; - let getGvlidForUserIdModuleStub; - let getGvlidForAnalyticsAdapterStub; + const MOCK_MODULE = 'moduleA'; + let entry; + beforeEach(function() { - getGvlidForBidAdapterStub = sandbox.stub(internal, 'getGvlidForBidAdapter'); - getGvlidForUserIdModuleStub = sandbox.stub(internal, 'getGvlidForUserIdModule'); - getGvlidForAnalyticsAdapterStub = sandbox.stub(internal, 'getGvlidForAnalyticsAdapter'); + entry = {modules: {}}; + GDPR_GVLIDS.get.reset(); + GDPR_GVLIDS.get.callsFake((mod) => mod === MOCK_MODULE ? entry : {modules: {}}); }); it('should return "null" if called without passing any argument', function() { @@ -1156,46 +1115,63 @@ describe('gdpr enforcement', function () { expect(gvlid).to.equal(null); }); - it('should return "null" if GVL ID is not defined for any of these modules: Bid adapter, UserId submodule and Analytics adapter', function() { - getGvlidForBidAdapterStub.withArgs('moduleA').returns(null); - getGvlidForUserIdModuleStub.withArgs('moduleA').returns(null); - getGvlidForAnalyticsAdapterStub.withArgs('moduleA').returns(null); - - const gvlid = getGvlid('moduleA'); + it('should return "null" if no GVL ID was registered', function() { + const gvlid = getGvlid('type', MOCK_MODULE); expect(gvlid).to.equal(null); }); - it('should return the GVL ID from gvlMapping if it is defined in setConfig', function() { - config.setConfig({ - gvlMapping: { - moduleA: 1 - } - }); - - // Actual GVL ID for moduleA is 2, as defined on its the bidAdapter.js file. - getGvlidForBidAdapterStub.withArgs('moduleA').returns(2); - - const gvlid = getGvlid('moduleA'); - expect(gvlid).to.equal(1); - }); - - it('should return the GVL ID by calling getGvlidForBidAdapter -> getGvlidForUserIdModule -> getGvlidForAnalyticsAdapter in sequence', function() { - getGvlidForBidAdapterStub.withArgs('moduleA').returns(null); - getGvlidForUserIdModuleStub.withArgs('moduleA').returns(null); - getGvlidForAnalyticsAdapterStub.withArgs('moduleA').returns(7); + it('should return null if the wrong GVL ID was registered', () => { + entry = {gvlid: 123}; + expect(getGvlid('type', 'someOtherModule')).to.equal(null); + }) - expect(getGvlid('moduleA')).to.equal(7); - }); + Object.entries({ + 'without fallback': null, + 'with fallback': () => 'shouldBeIgnored' + }).forEach(([t, fallbackFn]) => { + describe(t, () => { + it('should return the GVL ID from gvlMapping if it is defined in setConfig', function() { + config.setConfig({ + gvlMapping: { + [MOCK_MODULE]: 1 + } + }); + + entry = {gvlid: 2}; + + const gvlid = getGvlid('type', MOCK_MODULE, fallbackFn); + expect(gvlid).to.equal(1); + }); + + it('should return the GVL ID that was registered', function() { + entry = {gvlid: 7}; + expect(getGvlid('type', MOCK_MODULE, fallbackFn)).to.equal(7); + }); + + it('should return VENDORLESS_GVLID for core modules', () => { + entry = {gvlid: 123}; + expect(getGvlid(MODULE_TYPE_CORE, MOCK_MODULE, fallbackFn)).to.equal(VENDORLESS_GVLID); + }); + + describe('multiple GVL IDs are found', () => { + it('should use bidder over others', () => { + entry = {modules: {[MODULE_TYPE_BIDDER]: 123, [MODULE_TYPE_UID]: 321}}; + expect(getGvlid(MODULE_TYPE_UID, MOCK_MODULE, fallbackFn)).to.equal(123); + }); + it('should use uid over analytics', () => { + entry = {modules: {[MODULE_TYPE_UID]: 123, [MODULE_TYPE_ANALYTICS]: 321}}; + expect(getGvlid(MODULE_TYPE_ANALYTICS, MOCK_MODULE, fallbackFn)).to.equal(123); + }) + }) + }) + }) - it('should pass extra arguments to analytics\' getGvlid', () => { - getGvlidForAnalyticsAdapterStub.withArgs('analytics').returns(321); - const cfg = {some: 'args'}; - getGvlid('analytics', cfg); - sinon.assert.calledWith(getGvlidForAnalyticsAdapterStub, 'analytics', cfg); + it('should use fallbackFn if no other lookup produces a gvl id', () => { + expect(getGvlid('type', MOCK_MODULE, () => 321)).to.equal(321); }); }); - describe('getGvlidForAnalyticsAdapter', () => { + describe('getGvlidFromAnalyticsConfig', () => { let getAnalyticsAdapter, adapter, adapterEntry; beforeEach(() => { @@ -1207,26 +1183,20 @@ describe('gdpr enforcement', function () { getAnalyticsAdapter.withArgs('analytics').returns(adapterEntry); }); - it('should return gvlid from adapterManager if defined', () => { - adapterEntry.gvlid = 123; - adapter.gvlid = 321 - expect(internal.getGvlidForAnalyticsAdapter('analytics')).to.equal(123); - }); - it('should return gvlid from adapter if defined', () => { adapter.gvlid = 321; - expect(internal.getGvlidForAnalyticsAdapter('analytics')).to.equal(321); + expect(getGvlidFromAnalyticsAdapter('analytics')).to.equal(321); }); it('should invoke adapter.gvlid if it\'s a function', () => { adapter.gvlid = (cfg) => cfg.k const cfg = {k: 231}; - expect(internal.getGvlidForAnalyticsAdapter('analytics', cfg)).to.eql(231); + expect(getGvlidFromAnalyticsAdapter('analytics', cfg)).to.eql(231); }); it('should not choke if adapter gvlid fn throws', () => { adapter.gvlid = () => { throw new Error(); }; - expect(internal.getGvlidForAnalyticsAdapter('analytics')).to.not.be.ok; + expect(getGvlidFromAnalyticsAdapter('analytics')).to.not.be.ok; }); }); }) diff --git a/test/spec/modules/globalsunBidAdapter_spec.js b/test/spec/modules/globalsunBidAdapter_spec.js new file mode 100644 index 00000000000..0d17c25363d --- /dev/null +++ b/test/spec/modules/globalsunBidAdapter_spec.js @@ -0,0 +1,399 @@ +import { expect } from 'chai'; +import { spec } from '../../../modules/globalsunBidAdapter.js'; +import { BANNER, VIDEO, NATIVE } from '../../../src/mediaTypes.js'; +import { getUniqueIdentifierStr } from '../../../src/utils.js'; + +const bidder = 'globalsun' + +describe('GlobalsunBidAdapter', function () { + const bids = [ + { + bidId: getUniqueIdentifierStr(), + bidder: bidder, + mediaTypes: { + [BANNER]: { + sizes: [[300, 250]] + } + }, + params: { + placementId: 'testBanner', + } + }, + { + bidId: getUniqueIdentifierStr(), + bidder: bidder, + mediaTypes: { + [VIDEO]: { + playerSize: [[300, 300]], + minduration: 5, + maxduration: 60 + } + }, + params: { + placementId: 'testVideo', + } + }, + { + bidId: getUniqueIdentifierStr(), + bidder: bidder, + mediaTypes: { + [NATIVE]: { + native: { + title: { + required: true + }, + body: { + required: true + }, + icon: { + required: true, + size: [64, 64] + } + } + } + }, + params: { + placementId: 'testNative', + } + } + ]; + + const invalidBid = { + bidId: getUniqueIdentifierStr(), + bidder: bidder, + mediaTypes: { + [BANNER]: { + sizes: [[300, 250]] + } + }, + params: { + + } + } + + const bidderRequest = { + uspConsent: '1---', + gdprConsent: 'COvFyGBOvFyGBAbAAAENAPCAAOAAAAAAAAAAAEEUACCKAAA.IFoEUQQgAIQwgIwQABAEAAAAOIAACAIAAAAQAIAgEAACEAAAAAgAQBAAAAAAAGBAAgAAAAAAAFAAECAAAgAAQARAEQAAAAAJAAIAAgAAAYQEAAAQmAgBC3ZAYzUw', + refererInfo: { + referer: 'https://test.com' + }, + timeout: 500 + }; + + describe('isBidRequestValid', function () { + it('Should return true if there are bidId, params and key parameters present', function () { + expect(spec.isBidRequestValid(bids[0])).to.be.true; + }); + it('Should return false if at least one of parameters is not present', function () { + expect(spec.isBidRequestValid(invalidBid)).to.be.false; + }); + }); + + describe('buildRequests', function () { + let serverRequest = spec.buildRequests(bids, bidderRequest); + + it('Creates a ServerRequest object with method, URL and data', function () { + expect(serverRequest).to.exist; + expect(serverRequest.method).to.exist; + expect(serverRequest.url).to.exist; + expect(serverRequest.data).to.exist; + }); + + it('Returns POST method', function () { + expect(serverRequest.method).to.equal('POST'); + }); + + it('Returns valid URL', function () { + expect(serverRequest.url).to.equal('https://endpoint.globalsun.io/pbjs'); + }); + + it('Returns general data valid', function () { + let data = serverRequest.data; + expect(data).to.be.an('object'); + expect(data).to.have.all.keys('deviceWidth', + 'deviceHeight', + 'language', + 'secure', + 'host', + 'page', + 'placements', + 'coppa', + 'ccpa', + 'gdpr', + 'tmax' + ); + expect(data.deviceWidth).to.be.a('number'); + expect(data.deviceHeight).to.be.a('number'); + expect(data.language).to.be.a('string'); + expect(data.secure).to.be.within(0, 1); + expect(data.host).to.be.a('string'); + expect(data.page).to.be.a('string'); + expect(data.coppa).to.be.a('number'); + expect(data.gdpr).to.be.a('string'); + expect(data.ccpa).to.be.a('string'); + expect(data.tmax).to.be.a('number'); + expect(data.placements).to.have.lengthOf(3); + }); + + it('Returns valid placements', function () { + const { placements } = serverRequest.data; + for (let i = 0, len = placements.length; i < len; i++) { + const placement = placements[i]; + expect(placement.placementId).to.be.oneOf(['testBanner', 'testVideo', 'testNative']); + expect(placement.adFormat).to.be.oneOf([BANNER, VIDEO, NATIVE]); + expect(placement.bidId).to.be.a('string'); + expect(placement.schain).to.be.an('object'); + expect(placement.bidfloor).to.exist.and.to.equal(0); + expect(placement.type).to.exist.and.to.equal('publisher'); + + if (placement.adFormat === BANNER) { + expect(placement.sizes).to.be.an('array'); + } + switch (placement.adFormat) { + case BANNER: + expect(placement.sizes).to.be.an('array'); + break; + case VIDEO: + expect(placement.playerSize).to.be.an('array'); + expect(placement.minduration).to.be.an('number'); + expect(placement.maxduration).to.be.an('number'); + break; + case NATIVE: + expect(placement.native).to.be.an('object'); + break; + } + } + }); + + it('Returns data with gdprConsent and without uspConsent', function () { + delete bidderRequest.uspConsent; + serverRequest = spec.buildRequests(bids, bidderRequest); + let data = serverRequest.data; + expect(data.gdpr).to.exist; + expect(data.gdpr).to.be.a('string'); + expect(data.gdpr).to.equal(bidderRequest.gdprConsent); + expect(data.ccpa).to.not.exist; + delete bidderRequest.gdprConsent; + }); + + it('Returns data with uspConsent and without gdprConsent', function () { + bidderRequest.uspConsent = '1---'; + delete bidderRequest.gdprConsent; + serverRequest = spec.buildRequests(bids, bidderRequest); + let data = serverRequest.data; + expect(data.ccpa).to.exist; + expect(data.ccpa).to.be.a('string'); + expect(data.ccpa).to.equal(bidderRequest.uspConsent); + expect(data.gdpr).to.not.exist; + }); + + it('Returns empty data if no valid requests are passed', function () { + serverRequest = spec.buildRequests([], bidderRequest); + let data = serverRequest.data; + expect(data.placements).to.be.an('array').that.is.empty; + }); + }); + + describe('interpretResponse', function () { + it('Should interpret banner response', function () { + const banner = { + body: [{ + mediaType: 'banner', + width: 300, + height: 250, + cpm: 0.4, + ad: 'Test', + requestId: '23fhj33i987f', + ttl: 120, + creativeId: '2', + netRevenue: true, + currency: 'USD', + dealId: '1', + meta: { + advertiserDomains: ['google.com'], + advertiserId: 1234 + } + }] + }; + let bannerResponses = spec.interpretResponse(banner); + expect(bannerResponses).to.be.an('array').that.is.not.empty; + let dataItem = bannerResponses[0]; + expect(dataItem).to.have.all.keys('requestId', 'cpm', 'width', 'height', 'ad', 'ttl', 'creativeId', + 'netRevenue', 'currency', 'dealId', 'mediaType', 'meta'); + expect(dataItem.requestId).to.equal(banner.body[0].requestId); + expect(dataItem.cpm).to.equal(banner.body[0].cpm); + expect(dataItem.width).to.equal(banner.body[0].width); + expect(dataItem.height).to.equal(banner.body[0].height); + expect(dataItem.ad).to.equal(banner.body[0].ad); + expect(dataItem.ttl).to.equal(banner.body[0].ttl); + expect(dataItem.creativeId).to.equal(banner.body[0].creativeId); + expect(dataItem.netRevenue).to.be.true; + expect(dataItem.currency).to.equal(banner.body[0].currency); + expect(dataItem.meta).to.be.an('object').that.has.any.key('advertiserDomains'); + }); + it('Should interpret video response', function () { + const video = { + body: [{ + vastUrl: 'test.com', + mediaType: 'video', + cpm: 0.5, + requestId: '23fhj33i987f', + ttl: 120, + creativeId: '2', + netRevenue: true, + currency: 'USD', + dealId: '1', + meta: { + advertiserDomains: ['google.com'], + advertiserId: 1234 + } + }] + }; + let videoResponses = spec.interpretResponse(video); + expect(videoResponses).to.be.an('array').that.is.not.empty; + + let dataItem = videoResponses[0]; + expect(dataItem).to.have.all.keys('requestId', 'cpm', 'vastUrl', 'ttl', 'creativeId', + 'netRevenue', 'currency', 'dealId', 'mediaType', 'meta'); + expect(dataItem.requestId).to.equal('23fhj33i987f'); + expect(dataItem.cpm).to.equal(0.5); + expect(dataItem.vastUrl).to.equal('test.com'); + expect(dataItem.ttl).to.equal(120); + expect(dataItem.creativeId).to.equal('2'); + expect(dataItem.netRevenue).to.be.true; + expect(dataItem.currency).to.equal('USD'); + expect(dataItem.meta).to.be.an('object').that.has.any.key('advertiserDomains'); + }); + it('Should interpret native response', function () { + const native = { + body: [{ + mediaType: 'native', + native: { + clickUrl: 'test.com', + title: 'Test', + image: 'test.com', + impressionTrackers: ['test.com'], + }, + ttl: 120, + cpm: 0.4, + requestId: '23fhj33i987f', + creativeId: '2', + netRevenue: true, + currency: 'USD', + meta: { + advertiserDomains: ['google.com'], + advertiserId: 1234 + } + }] + }; + let nativeResponses = spec.interpretResponse(native); + expect(nativeResponses).to.be.an('array').that.is.not.empty; + + let dataItem = nativeResponses[0]; + expect(dataItem).to.have.keys('requestId', 'cpm', 'ttl', 'creativeId', 'netRevenue', 'currency', 'mediaType', 'native', 'meta'); + expect(dataItem.native).to.have.keys('clickUrl', 'impressionTrackers', 'title', 'image') + expect(dataItem.requestId).to.equal('23fhj33i987f'); + expect(dataItem.cpm).to.equal(0.4); + expect(dataItem.native.clickUrl).to.equal('test.com'); + expect(dataItem.native.title).to.equal('Test'); + expect(dataItem.native.image).to.equal('test.com'); + expect(dataItem.native.impressionTrackers).to.be.an('array').that.is.not.empty; + expect(dataItem.native.impressionTrackers[0]).to.equal('test.com'); + expect(dataItem.ttl).to.equal(120); + expect(dataItem.creativeId).to.equal('2'); + expect(dataItem.netRevenue).to.be.true; + expect(dataItem.currency).to.equal('USD'); + expect(dataItem.meta).to.be.an('object').that.has.any.key('advertiserDomains'); + }); + it('Should return an empty array if invalid banner response is passed', function () { + const invBanner = { + body: [{ + width: 300, + cpm: 0.4, + ad: 'Test', + requestId: '23fhj33i987f', + ttl: 120, + creativeId: '2', + netRevenue: true, + currency: 'USD', + dealId: '1' + }] + }; + + let serverResponses = spec.interpretResponse(invBanner); + expect(serverResponses).to.be.an('array').that.is.empty; + }); + it('Should return an empty array if invalid video response is passed', function () { + const invVideo = { + body: [{ + mediaType: 'video', + cpm: 0.5, + requestId: '23fhj33i987f', + ttl: 120, + creativeId: '2', + netRevenue: true, + currency: 'USD', + dealId: '1' + }] + }; + let serverResponses = spec.interpretResponse(invVideo); + expect(serverResponses).to.be.an('array').that.is.empty; + }); + it('Should return an empty array if invalid native response is passed', function () { + const invNative = { + body: [{ + mediaType: 'native', + clickUrl: 'test.com', + title: 'Test', + impressionTrackers: ['test.com'], + ttl: 120, + requestId: '23fhj33i987f', + creativeId: '2', + netRevenue: true, + currency: 'USD', + }] + }; + let serverResponses = spec.interpretResponse(invNative); + expect(serverResponses).to.be.an('array').that.is.empty; + }); + it('Should return an empty array if invalid response is passed', function () { + const invalid = { + body: [{ + ttl: 120, + creativeId: '2', + netRevenue: true, + currency: 'USD', + dealId: '1' + }] + }; + let serverResponses = spec.interpretResponse(invalid); + expect(serverResponses).to.be.an('array').that.is.empty; + }); + }); + + describe('getUserSyncs', function() { + it('Should return array of objects with proper sync config , include GDPR', function() { + const syncData = spec.getUserSyncs({}, {}, { + consentString: 'ALL', + gdprApplies: true, + }, {}); + expect(syncData).to.be.an('array').which.is.not.empty; + expect(syncData[0]).to.be.an('object') + expect(syncData[0].type).to.be.a('string') + expect(syncData[0].type).to.equal('image') + expect(syncData[0].url).to.be.a('string') + expect(syncData[0].url).to.equal('https://cs.globalsun.io/image?pbjs=1&gdpr=1&gdpr_consent=ALL&coppa=0') + }); + it('Should return array of objects with proper sync config , include CCPA', function() { + const syncData = spec.getUserSyncs({}, {}, {}, { + consentString: '1---' + }); + expect(syncData).to.be.an('array').which.is.not.empty; + expect(syncData[0]).to.be.an('object') + expect(syncData[0].type).to.be.a('string') + expect(syncData[0].type).to.equal('image') + expect(syncData[0].url).to.be.a('string') + expect(syncData[0].url).to.equal('https://cs.globalsun.io/image?pbjs=1&ccpa_consent=1---&coppa=0') + }); + }); +}); diff --git a/test/spec/modules/greenbidsAnalyticsAdapter_spec.js b/test/spec/modules/greenbidsAnalyticsAdapter_spec.js new file mode 100644 index 00000000000..b3cbf4c47c2 --- /dev/null +++ b/test/spec/modules/greenbidsAnalyticsAdapter_spec.js @@ -0,0 +1,418 @@ +import { + greenbidsAnalyticsAdapter, parseBidderCode, + ANALYTICS_VERSION, BIDDER_STATUS +} from 'modules/greenbidsAnalyticsAdapter.js'; + +import {expect} from 'chai'; +import sinon from 'sinon'; + +const events = require('src/events'); +const constants = require('src/constants.json'); + +const pbuid = 'pbuid-AA778D8A796AEA7A0843E2BBEB677766'; +const auctionId = 'b0b39610-b941-4659-a87c-de9f62d3e13e'; + +describe('Greenbids Prebid AnalyticsAdapter Testing', function () { + describe('event tracking and message cache manager', function () { + beforeEach(function () { + const configOptions = { + pbuid: pbuid, + sampling: 0, + }; + + greenbidsAnalyticsAdapter.enableAnalytics({ + provider: 'greenbidsAnalytics', + options: configOptions + }); + }); + + afterEach(function () { + greenbidsAnalyticsAdapter.disableAnalytics(); + }); + + describe('#parseBidderCode()', function() { + it('should get lower case bidder code from bidderCode field value', function() { + const receivedBids = [ + { + auctionId: auctionId, + adUnitCode: 'adunit_1', + bidder: 'greenbids', + bidderCode: 'GREENBIDS', + requestId: 'a1b2c3d4', + timeToRespond: 72, + cpm: 0.1, + currency: 'USD', + ad: 'fake ad1' + }, + ]; + const result = parseBidderCode(receivedBids[0]); + expect(result).to.equal('greenbids'); + }); + it('should get lower case bidder code from bidder field value as bidderCode field is missing', function() { + const receivedBids = [ + { + auctionId: auctionId, + adUnitCode: 'adunit_1', + bidder: 'greenbids', + bidderCode: '', + requestId: 'a1b2c3d4', + timeToRespond: 72, + cpm: 0.1, + currency: 'USD', + ad: 'fake ad1' + }, + ]; + const result = parseBidderCode(receivedBids[0]); + expect(result).to.equal('greenbids'); + }); + }); + + describe('#getCachedAuction()', function() { + const existing = {timeoutBids: [{}]}; + greenbidsAnalyticsAdapter.cachedAuctions['test_auction_id'] = existing; + + it('should get the existing cached object if it exists', function() { + const result = greenbidsAnalyticsAdapter.getCachedAuction('test_auction_id'); + + expect(result).to.equal(existing); + }); + + it('should create a new object and store it in the cache on cache miss', function() { + const result = greenbidsAnalyticsAdapter.getCachedAuction('no_such_id'); + + expect(result).to.deep.include({ + timeoutBids: [], + }); + }); + }); + + describe('when formatting JSON payload sent to backend', function() { + const receivedBids = [ + { + auctionId: auctionId, + adUnitCode: 'adunit-1', + bidder: 'greenbids', + bidderCode: 'greenbids', + requestId: 'a1b2c3d4', + timeToRespond: 72, + cpm: 0.1, + currency: 'USD', + ad: 'fake ad1' + }, + { + auctionId: auctionId, + adUnitCode: 'adunit-1', + bidder: 'greenbidsx', + bidderCode: 'greenbidsx', + requestId: 'b2c3d4e5', + timeToRespond: 100, + cpm: 0.08, + currency: 'USD', + ad: 'fake ad2' + }, + { + auctionId: auctionId, + adUnitCode: 'adunit-2', + bidder: 'greenbids', + bidderCode: 'greenbids', + requestId: 'c3d4e5f6', + timeToRespond: 120, + cpm: 0.09, + currency: 'USD', + ad: 'fake ad3' + }, + ]; + const noBids = [ + { + auctionId: auctionId, + adUnitCode: 'adunit-2', + bidder: 'greenbids', + bidderCode: 'greenbids', + bidId: 'a1b2c3d4', + } + ]; + const timeoutBids = [ + { + auctionId: auctionId, + adUnitCode: 'adunit-2', + bidder: 'greenbids', + bidderCode: 'greenbids', + bidId: '00123d4c', + } + ]; + function assertHavingRequiredMessageFields(message) { + expect(message).to.include({ + version: ANALYTICS_VERSION, + auctionId: auctionId, + pbuid: pbuid, + referrer: window.location.href, + sampling: 0, + prebid: '$prebid.version$', + }); + } + + describe('#createCommonMessage', function() { + it('should correctly serialize some common fields', function() { + const message = greenbidsAnalyticsAdapter.createCommonMessage(auctionId); + + assertHavingRequiredMessageFields(message); + }); + }); + + describe('#serializeBidResponse', function() { + it('should handle BID properly with timeout false and hasBid true', function() { + const result = greenbidsAnalyticsAdapter.serializeBidResponse(receivedBids[0], BIDDER_STATUS.BID); + + expect(result).to.include({ + bidder: 'greenbids', + isTimeout: false, + hasBid: true, + }); + }); + + it('should handle NO_BID properly and set hasBid to false', function() { + const result = greenbidsAnalyticsAdapter.serializeBidResponse(noBids[0], BIDDER_STATUS.NO_BID); + + expect(result).to.include({ + bidder: 'greenbids', + isTimeout: false, + hasBid: false, + }); + }); + + it('should handle TIMEOUT properly and set isTimeout to true', function() { + const result = greenbidsAnalyticsAdapter.serializeBidResponse(noBids[0], BIDDER_STATUS.TIMEOUT); + + expect(result).to.include({ + bidder: 'greenbids', + isTimeout: true, + hasBid: false, + }); + }); + }); + + describe('#addBidResponseToMessage()', function() { + it('should add a bid response in the output message, grouped by adunit_id and bidder', function() { + const message = { + adUnits: [ + { + code: 'adunit-2', + bidders: [] + } + ] + }; + greenbidsAnalyticsAdapter.addBidResponseToMessage(message, noBids[0], BIDDER_STATUS.NO_BID); + + expect(message.adUnits[0]).to.deep.include({ + code: 'adunit-2', + bidders: [ + { + bidder: 'greenbids', + isTimeout: false, + hasBid: false, + } + ] + }); + }); + }); + + describe('#createBidMessage()', function() { + it('should format auction message sent to the backend', function() { + const args = { + auctionId: auctionId, + timestamp: 1234567890, + timeout: 3000, + auctionEnd: 1234567990, + adUnitCodes: ['adunit-1', 'adunit-2'], + adUnits: [ + { + code: 'adunit-1', + mediaTypes: { + banner: { + sizes: [[300, 250], [300, 600]] + }, + } + }, + { + code: 'adunit-2', + mediaTypes: { + banner: { + sizes: [[300, 250], [300, 600]] + }, + video: { + context: 'instream', + mimes: ['video/mp4'], + playerSize: [[640, 480]], + skip: 1, + protocols: [1, 2, 3, 4] + }, + } + }, + ], + bidsReceived: receivedBids, + noBids: noBids + }; + + const result = greenbidsAnalyticsAdapter.createBidMessage(args, timeoutBids); + + assertHavingRequiredMessageFields(result); + expect(result).to.deep.include({ + auctionElapsed: 100, + adUnits: [ + { + code: 'adunit-1', + mediaTypes: { + banner: { + sizes: [[300, 250], [300, 600]] + } + }, + bidders: [ + { + bidder: 'greenbids', + isTimeout: false, + hasBid: true + }, + { + bidder: 'greenbidsx', + isTimeout: false, + hasBid: true + } + ] + }, + { + code: 'adunit-2', + mediaTypes: { + banner: { + sizes: [[300, 250], [300, 600]] + }, + video: { + context: 'instream', + mimes: ['video/mp4'], + playerSize: [[640, 480]], + skip: 1, + protocols: [1, 2, 3, 4] + } + }, + bidders: [ + { + bidder: 'greenbids', + isTimeout: true, + hasBid: true + } + ] + } + ], + }); + }); + }); + + describe('#handleBidTimeout()', function() { + it('should cached the timeout bid as BID_TIMEOUT event was triggered', function() { + greenbidsAnalyticsAdapter.cachedAuctions['test_timeout_auction_id'] = { 'timeoutBids': [] }; + const args = [{ + auctionId: 'test_timeout_auction_id', + timestamp: 1234567890, + timeout: 3000, + auctionEnd: 1234567990, + bidsReceived: receivedBids, + noBids: noBids + }]; + + greenbidsAnalyticsAdapter.handleBidTimeout(args); + const result = greenbidsAnalyticsAdapter.getCachedAuction('test_timeout_auction_id'); + expect(result).to.deep.include({ + timeoutBids: [{ + auctionId: 'test_timeout_auction_id', + timestamp: 1234567890, + timeout: 3000, + auctionEnd: 1234567990, + bidsReceived: receivedBids, + noBids: noBids + }] + }); + }); + }); + }); + }); + + describe('greenbids Analytics Adapter track handler ', function () { + const configOptions = { + pbuid: pbuid, + sampling: 1, + }; + + beforeEach(function () { + sinon.stub(events, 'getEvents').returns([]); + greenbidsAnalyticsAdapter.enableAnalytics({ + provider: 'greenbidsAnalytics', + options: configOptions + }); + }); + + afterEach(function () { + greenbidsAnalyticsAdapter.disableAnalytics(); + events.getEvents.restore(); + }); + + it('should call handleBidTimeout as BID_TIMEOUT trigger event', function() { + sinon.spy(greenbidsAnalyticsAdapter, 'handleBidTimeout'); + events.emit(constants.EVENTS.BID_TIMEOUT, {}); + sinon.assert.callCount(greenbidsAnalyticsAdapter.handleBidTimeout, 1); + greenbidsAnalyticsAdapter.handleBidTimeout.restore(); + }); + + it('should call handleAuctionEnd as AUCTION_END trigger event', function() { + sinon.spy(greenbidsAnalyticsAdapter, 'handleAuctionEnd'); + events.emit(constants.EVENTS.AUCTION_END, {}); + sinon.assert.callCount(greenbidsAnalyticsAdapter.handleAuctionEnd, 1); + greenbidsAnalyticsAdapter.handleAuctionEnd.restore(); + }); + }); + + describe('enableAnalytics and config parser', function () { + const configOptions = { + pbuid: pbuid, + sampling: 0, + }; + + beforeEach(function () { + greenbidsAnalyticsAdapter.enableAnalytics({ + provider: 'greenbidsAnalytics', + options: configOptions + }); + }); + + afterEach(function () { + greenbidsAnalyticsAdapter.disableAnalytics(); + }); + + it('should parse config correctly with optional values', function () { + expect(greenbidsAnalyticsAdapter.getAnalyticsOptions().options).to.deep.equal(configOptions); + expect(greenbidsAnalyticsAdapter.getAnalyticsOptions().pbuid).to.equal(configOptions.pbuid); + expect(greenbidsAnalyticsAdapter.getAnalyticsOptions().sampled).to.equal(false); + }); + + it('should not enable Analytics when pbuid is missing', function () { + const configOptions = { + options: { + } + }; + const validConfig = greenbidsAnalyticsAdapter.initConfig(configOptions); + expect(validConfig).to.equal(false); + }); + it('should fall back to default value when sampling factor is not number', function () { + const configOptions = { + options: { + pbuid: pbuid, + sampling: 'string', + } + }; + greenbidsAnalyticsAdapter.enableAnalytics({ + provider: 'greenbidsAnalytics', + options: configOptions + }); + + expect(greenbidsAnalyticsAdapter.getAnalyticsOptions().sampled).to.equal(false); + }); + }); +}); diff --git a/test/spec/modules/gridBidAdapter_spec.js b/test/spec/modules/gridBidAdapter_spec.js index 4b93c287fee..1cafdf6134f 100644 --- a/test/spec/modules/gridBidAdapter_spec.js +++ b/test/spec/modules/gridBidAdapter_spec.js @@ -575,6 +575,30 @@ describe('TheMediaGrid Adapter', function () { expect(payload.regs.ext).to.have.property('us_privacy', '1YNN'); }); + it('should add gpp information to the request via bidderRequest.gppConsent', function () { + let consentString = 'abc1234'; + const gppBidderRequest = Object.assign({gppConsent: {gppString: consentString, applicableSections: [8]}}, bidderRequest); + + const [request] = spec.buildRequests(bidRequests, gppBidderRequest); + const payload = JSON.parse(request.data); + + expect(payload.regs).to.exist; + expect(payload.regs.gpp).to.equal(consentString); + expect(payload.regs.gpp_sid).to.deep.equal([8]); + }); + + it('should add gpp information to the request via bidderRequest.ortb2.regs.gpp', function () { + let consentString = 'abc1234'; + const gppBidderRequest = Object.assign({ortb2: {regs: {gpp: consentString, gpp_sid: [8]}}}, bidderRequest); + + const [request] = spec.buildRequests(bidRequests, gppBidderRequest); + const payload = JSON.parse(request.data); + + expect(payload.regs).to.exist; + expect(payload.regs.gpp).to.equal(consentString); + expect(payload.regs.gpp_sid).to.deep.equal([8]); + }); + it('if userId is present payload must have user.ext param with right keys', function () { const eids = [ { @@ -667,14 +691,6 @@ describe('TheMediaGrid Adapter', function () { const [request] = spec.buildRequests(bidRequestsWithJwTargeting, bidderRequest); expect(request.data).to.be.an('string'); const payload = parseRequest(request.data); - expect(payload).to.have.property('user'); - expect(payload.user.data).to.deep.equal([{ - name: 'iow_labs_pub_data', - segment: [ - {name: 'jwpseg', value: jsSegments[0]}, - {name: 'jwpseg', value: jsSegments[1]} - ] - }]); expect(payload).to.have.property('site'); expect(payload.site.content).to.deep.equal(jsContent); }); @@ -793,7 +809,7 @@ describe('TheMediaGrid Adapter', function () { expect(payload.site.content.data).to.deep.equal(contentData); }); - it('should have right value in user.data when jwpsegments are present', function () { + it('should have right value in user.data', function () { const userData = [ { name: 'someName', @@ -823,13 +839,7 @@ describe('TheMediaGrid Adapter', function () { }); const [request] = spec.buildRequests([bidRequestsWithJwTargeting], {...bidderRequest, ortb2}); const payload = parseRequest(request.data); - expect(payload.user.data).to.deep.equal([{ - name: 'iow_labs_pub_data', - segment: [ - {name: 'jwpseg', value: jsSegments[0]}, - {name: 'jwpseg', value: jsSegments[1]} - ] - }, ...userData]); + expect(payload.user.data).to.deep.equal(userData); }); it('should have site.content.id filled from config ortb2.site.content.id', function () { @@ -840,24 +850,6 @@ describe('TheMediaGrid Adapter', function () { expect(payload.site.content.id).to.equal(contentId); }); - it('should be right tmax when timeout in config is less then timeout in bidderRequest', function() { - const getConfigStub = sinon.stub(config, 'getConfig').callsFake( - arg => arg === 'bidderTimeout' ? 2000 : null); - const [request] = spec.buildRequests([bidRequests[0]], bidderRequest); - expect(request.data).to.be.an('string'); - const payload = parseRequest(request.data); - expect(payload.tmax).to.equal(2000); - getConfigStub.restore(); - }); - it('should be right tmax when timeout in bidderRequest is less then timeout in config', function() { - const getConfigStub = sinon.stub(config, 'getConfig').callsFake( - arg => arg === 'bidderTimeout' ? 5000 : null); - const [request] = spec.buildRequests([bidRequests[0]], bidderRequest); - expect(request.data).to.be.an('string'); - const payload = parseRequest(request.data); - expect(payload.tmax).to.equal(3000); - getConfigStub.restore(); - }); it('should contain regs.coppa if coppa is true in config', function () { const getConfigStub = sinon.stub(config, 'getConfig').callsFake( arg => arg === 'coppa' ? true : null); @@ -881,14 +873,17 @@ describe('TheMediaGrid Adapter', function () { } }, { ext: { + gpid: '/222222/slot', data: { adserver: { name: 'ad_server_name', - adslot: '/222222/slot' - }, - pbadslot: '/222222/slot' + } } } + }, { + ext: { + gpid: '/333333/slot' + } }]; const bidRequestsWithOrtb2Imp = bidRequests.slice(0, 3).map((bid, ind) => { return Object.assign(ortb2Imp[ind] ? { ortb2Imp: ortb2Imp[ind] } : {}, bid); @@ -904,10 +899,11 @@ describe('TheMediaGrid Adapter', function () { expect(payload.imp[1].ext).to.deep.equal({ divid: bidRequests[1].adUnitCode, data: ortb2Imp[1].ext.data, - gpid: ortb2Imp[1].ext.data.adserver.adslot + gpid: ortb2Imp[1].ext.gpid }); expect(payload.imp[2].ext).to.deep.equal({ - divid: bidRequests[2].adUnitCode + divid: bidRequests[2].adUnitCode, + gpid: ortb2Imp[2].ext.gpid }); }); diff --git a/test/spec/modules/gridNMBidAdapter_spec.js b/test/spec/modules/gridNMBidAdapter_spec.js index b400ef3394d..e4f06a451d2 100644 --- a/test/spec/modules/gridNMBidAdapter_spec.js +++ b/test/spec/modules/gridNMBidAdapter_spec.js @@ -245,14 +245,6 @@ describe('TheMediaGridNM Adapter', function () { requests.forEach((req, i) => { const payload = req.data; expect(req).to.have.property('data'); - expect(payload).to.have.property('user'); - expect(payload.user.data).to.deep.equal([{ - name: 'iow_labs_pub_data', - segment: [ - {name: 'jwpseg', value: jsSegments[0]}, - {name: 'jwpseg', value: jsSegments[1]} - ] - }]); expect(payload).to.have.property('site'); expect(payload.site.content).to.deep.equal(jsContent); }); diff --git a/test/spec/modules/growthCodeIdSystem_spec.js b/test/spec/modules/growthCodeIdSystem_spec.js index dce995d25e0..97083047d4e 100644 --- a/test/spec/modules/growthCodeIdSystem_spec.js +++ b/test/spec/modules/growthCodeIdSystem_spec.js @@ -4,12 +4,13 @@ import { server } from 'test/mocks/xhr.js'; import { uspDataHandler } from 'src/adapterManager.js'; import {expect} from 'chai'; import {getStorageManager} from '../../../src/storageManager.js'; +import {MODULE_TYPE_UID} from '../../../src/activities/modules.js'; const GCID_EXPIRY = 45; const MODULE_NAME = 'growthCodeId'; const SHAREDID = 'fe9c5c89-7d56-4666-976d-e07e73b3b664'; -export const storage = getStorageManager({ gvlid: undefined, moduleName: MODULE_NAME }); +const storage = getStorageManager({ moduleType: MODULE_TYPE_UID, moduleName: MODULE_NAME }); const getIdParams = {params: { pid: 'TEST01', diff --git a/test/spec/modules/gumgumBidAdapter_spec.js b/test/spec/modules/gumgumBidAdapter_spec.js index a2dacb16b73..71f356a83ae 100644 --- a/test/spec/modules/gumgumBidAdapter_spec.js +++ b/test/spec/modules/gumgumBidAdapter_spec.js @@ -485,6 +485,46 @@ describe('gumgumAdapter', function () { const bidRequest = spec.buildRequests(bidRequests, fakeBidRequest)[0]; expect(bidRequest.data).to.not.include.any.keys('gdprConsent') }); + it('should add gpp parameters if gppConsent is present', function () { + const gppConsent = { gppString: 'DBACNYA~CPXxRfAPXxRfAAfKABENB-CgAAAAAAAAAAYgAAAAAAAA~1YNN', applicableSections: [7] } + const fakeBidRequest = { gppConsent: gppConsent }; + const bidRequest = spec.buildRequests(bidRequests, fakeBidRequest)[0]; + expect(bidRequest.data.gppConsent).to.exist; + expect(bidRequest.data.gppConsent.gppString).to.equal(gppConsent.gppString); + expect(bidRequest.data.gppConsent.gpp_sid).to.equal(gppConsent.applicableSections); + expect(bidRequest.data.gppConsent.gppString).to.eq('DBACNYA~CPXxRfAPXxRfAAfKABENB-CgAAAAAAAAAAYgAAAAAAAA~1YNN'); + }); + it('should handle ortb2 parameters', function () { + const ortb2 = { + regs: { + gpp: 'DBACNYA~CPXxRfAPXxRfAAfKABENB-CgAAAAAAAAAAYgAAAAAAAA~1YNN', + gpp_sid: [7] + } + } + const fakeBidRequest = { gppConsent: ortb2 }; + const bidRequest = spec.buildRequests(bidRequests, fakeBidRequest)[0]; + expect(bidRequest.data.gppConsent.gppString).to.eq(fakeBidRequest[0]) + }); + it('should handle gppConsent is present but values are undefined case', function () { + const gppConsent = { gppString: undefined, applicableSections: undefined } + const fakeBidRequest = { gppConsent: gppConsent }; + const bidRequest = spec.buildRequests(bidRequests, fakeBidRequest)[0]; + expect(bidRequest.data.gppConsent).to.exist; + expect(bidRequest.data.gppConsent.gppString).to.equal(undefined); + expect(bidRequest.data.gppConsent.gpp_sid).to.equal(undefined); + }); + it('should handle ortb2 undefined parameters', function () { + const ortb2 = { + regs: { + gpp: undefined, + gpp_sid: undefined + } + } + const fakeBidRequest = { gppConsent: ortb2 }; + const bidRequest = spec.buildRequests(bidRequests, fakeBidRequest)[0]; + expect(bidRequest.data.gppConsent.gppString).to.eq(undefined) + expect(bidRequest.data.gppConsent.gpp_sid).to.eq(undefined) + }); it('should not set coppa parameter if coppa config is set to false', function () { config.setConfig({ coppa: false diff --git a/test/spec/modules/hadronIdSystem_spec.js b/test/spec/modules/hadronIdSystem_spec.js index ca9eadc7fd4..c998ef2cf14 100644 --- a/test/spec/modules/hadronIdSystem_spec.js +++ b/test/spec/modules/hadronIdSystem_spec.js @@ -24,7 +24,7 @@ describe('HadronIdSystem', function () { const request = server.requests[0]; expect(request.url).to.eq(`https://id.hadron.ad.gt/api/v1/pbhid?partner_id=0&_it=prebid`); request.respond(200, { 'Content-Type': 'application/json' }, JSON.stringify({ hadronId: 'testHadronId1' })); - expect(callbackSpy.lastCall.lastArg).to.deep.equal({hadronId: 'testHadronId1'}); + expect(callbackSpy.lastCall.lastArg).to.deep.equal({ id: { hadronId: 'testHadronId1' } }); }); it('gets a cached hadronid', function() { @@ -33,10 +33,8 @@ describe('HadronIdSystem', function () { }; getDataFromLocalStorageStub.withArgs('auHadronId').returns('tstCachedHadronId1'); - const callbackSpy = sinon.spy(); - const callback = hadronIdSubmodule.getId(config).callback; - callback(callbackSpy); - expect(callbackSpy.lastCall.lastArg).to.deep.equal({hadronId: 'tstCachedHadronId1'}); + const result = hadronIdSubmodule.getId(config); + expect(result).to.deep.equal({ id: { hadronId: 'tstCachedHadronId1' } }); }); it('allows configurable id url', function() { @@ -51,7 +49,7 @@ describe('HadronIdSystem', function () { const request = server.requests[0]; expect(request.url).to.eq('https://hadronid.publync.com?partner_id=0&_it=prebid'); request.respond(200, { 'Content-Type': 'application/json' }, JSON.stringify({ hadronId: 'testHadronId1' })); - expect(callbackSpy.lastCall.lastArg).to.deep.equal({hadronId: 'testHadronId1'}); + expect(callbackSpy.lastCall.lastArg).to.deep.equal({ id: { hadronId: 'testHadronId1' } }); }); }); }); diff --git a/test/spec/modules/holidBidAdapter_spec.js b/test/spec/modules/holidBidAdapter_spec.js new file mode 100644 index 00000000000..e55befd213a --- /dev/null +++ b/test/spec/modules/holidBidAdapter_spec.js @@ -0,0 +1,194 @@ +import { expect } from 'chai' +import { spec } from 'modules/holidBidAdapter.js' + +describe('holidBidAdapterTests', () => { + const bidRequestData = { + bidder: 'holid', + adUnitCode: 'test-div', + bidId: 'bid-id', + auctionId: 'test-id', + params: { adUnitID: '12345' }, + mediaTypes: { banner: {} }, + sizes: [[300, 250]], + ortb2: { + site: { + publisher: { + domain: 'https://foo.bar', + } + }, + regs: { + gdpr: 1, + }, + user: { + ext: { + consent: 'G4ll0p1ng_Un1c0rn5', + } + }, + device: { + h: 410, + w: 1860, + } + } + } + + describe('isBidRequestValid', () => { + const bid = JSON.parse(JSON.stringify(bidRequestData)) + + it('should return true', () => { + expect(spec.isBidRequestValid(bid)).to.equal(true) + }) + + it('should return false when required params are not passed', () => { + const bid = JSON.parse(JSON.stringify(bidRequestData)) + delete bid.params.adUnitID + + expect(spec.isBidRequestValid(bid)).to.equal(false) + }) + }) + + describe('buildRequests', () => { + const bid = JSON.parse(JSON.stringify(bidRequestData)) + const request = spec.buildRequests([bid], bid) + const payload = JSON.parse(request[0].data) + + it('should include ext in imp', () => { + expect(payload.imp[0].ext).to.exist + expect(payload.imp[0].ext).to.deep.equal({ + prebid: { storedrequest: { id: '12345' } }, + }) + }) + + it('should include banner format in imp', () => { + expect(payload.imp[0].banner).to.exist + expect(payload.imp[0].banner).to.deep.equal({ + format: [{ w: 300, h: 250 }], + }) + }) + + it('should include ortb2 first party data', () => { + expect(payload.device.w).to.equal(1860) + expect(payload.device.h).to.equal(410) + expect(payload.user.ext.consent).to.equal('G4ll0p1ng_Un1c0rn5') + expect(payload.regs.gdpr).to.equal(1) + }) + }) + + describe('interpretResponse', () => { + const serverResponse = { + body: { + id: 'test-id', + cur: 'USD', + seatbid: [ + { + bid: [ + { + id: 'testbidid', + price: 0.4, + adm: 'test-ad', + adid: 789456, + crid: 1234, + w: 300, + h: 250, + }, + ], + }, + ], + }, + } + + const interpretedResponse = spec.interpretResponse( + serverResponse, + bidRequestData + ) + + it('should interpret response', () => { + expect(interpretedResponse[0].requestId).to.equal(bidRequestData.bidId) + expect(interpretedResponse[0].cpm).to.equal( + serverResponse.body.seatbid[0].bid[0].price + ) + expect(interpretedResponse[0].ad).to.equal( + serverResponse.body.seatbid[0].bid[0].adm + ) + expect(interpretedResponse[0].creativeId).to.equal( + serverResponse.body.seatbid[0].bid[0].crid + ) + expect(interpretedResponse[0].width).to.equal( + serverResponse.body.seatbid[0].bid[0].w + ) + expect(interpretedResponse[0].height).to.equal( + serverResponse.body.seatbid[0].bid[0].h + ) + expect(interpretedResponse[0].currency).to.equal(serverResponse.body.cur) + }) + }) + + describe('getUserSyncs', () => { + it('should return user sync', () => { + const optionsType = { + iframeEnabled: true, + pixelEnabled: true, + } + const serverResponse = [ + { + body: { + ext: { + responsetimemillis: { + 'test seat 1': 2, + 'test seat 2': 1, + }, + }, + }, + }, + ] + const gdprConsent = { + gdprApplies: 1, + consentString: 'dkj49Sjmfjuj34as:12jaf90123hufabidfy9u23brfpoig', + } + const uspConsent = 'mkjvbiniwot4827obfoy8sdg8203gb' + const expectedUserSyncs = [ + { + type: 'iframe', + url: 'https://null.holid.io/sync.html?bidders=%5B%22test%20seat%201%22%2C%22test%20seat%202%22%5D&gdpr=1&gdpr_consent=dkj49Sjmfjuj34as:12jaf90123hufabidfy9u23brfpoig&usp_consent=mkjvbiniwot4827obfoy8sdg8203gb&type=iframe', + }, + ] + + const userSyncs = spec.getUserSyncs( + optionsType, + serverResponse, + gdprConsent, + uspConsent + ) + + expect(userSyncs).to.deep.equal(expectedUserSyncs) + }) + + it('should return empty user syncs when responsetimemillis is not defined', () => { + const optionsType = { + iframeEnabled: true, + pixelEnabled: true, + } + const serverResponse = [ + { + body: { + ext: {}, + }, + }, + ] + const gdprConsent = { + gdprApplies: 1, + consentString: 'dkj49Sjmfjuj34as:12jaf90123hufabidfy9u23brfpoig', + } + const uspConsent = 'mkjvbiniwot4827obfoy8sdg8203gb' + const expectedUserSyncs = [] + + const userSyncs = spec.getUserSyncs( + optionsType, + serverResponse, + gdprConsent, + uspConsent + ) + + expect(userSyncs).to.deep.equal(expectedUserSyncs) + }) + }) +}) diff --git a/test/spec/modules/id5IdSystem_spec.js b/test/spec/modules/id5IdSystem_spec.js index 8c0f8ad9cf3..51954f76356 100644 --- a/test/spec/modules/id5IdSystem_spec.js +++ b/test/spec/modules/id5IdSystem_spec.js @@ -52,6 +52,22 @@ describe('ID5 ID System', function () { 'signature': ID5_RESPONSE_SIGNATURE, 'link_type': ID5_RESPONSE_LINK_TYPE }; + const ALLOWED_ID5_VENDOR_DATA = { + purpose: { + consents: { + 1: true + } + }, + vendor: { + consents: { + 131: true + } + } + } + + const HEADERS_CONTENT_TYPE_JSON = { + 'Content-Type': 'application/json' + } function getId5FetchConfig(storageName = ID5_STORAGE_NAME, storageType = 'html5') { return { @@ -136,7 +152,7 @@ describe('ID5 ID System', function () { } respondWithConfigAndExpectNext(configRequest, config = ID5_API_CONFIG) { - configRequest.respond(200, {'Content-Type': 'application/json'}, JSON.stringify(config)); + configRequest.respond(200, HEADERS_CONTENT_TYPE_JSON, JSON.stringify(config)); return this.expectNextRequest() } @@ -205,8 +221,39 @@ describe('ID5 ID System', function () { }); }); + describe('Check for valid consent', function() { + const dataConsentVals = [ + [{purpose: {consents: {1: false}}}, {vendor: {consents: {131: true}}}, ' no purpose consent'], + [{purpose: {consents: {1: true}}}, {vendor: {consents: {131: false}}}, ' no vendor consent'], + [{purpose: {consents: {1: false}}}, {vendor: {consents: {131: false}}}, ' no purpose and vendor consent'], + [{purpose: {consents: undefined}}, {vendor: {consents: {131: true}}}, ' undefined purpose consent'], + [{purpose: {consents: {1: false}}}, {vendor: {consents: undefined}}], ' undefined vendor consent', + [undefined, {vendor: {consents: {131: true}}}, ' undefined purpose'], + [{purpose: {consents: {1: true}}}, {vendor: undefined}, ' undefined vendor'], + [{purpose: {consents: {1: true}}}, {vendor: {consents: {31: true}}}, ' incorrect vendor consent'] + ]; + + dataConsentVals.forEach(function([purposeConsent, vendorConsent, caseName]) { + it('should fail with invalid consent because of ' + caseName, function() { + let dataConsent = { + gdprApplies: true, + consentString: 'consentString', + vendorData: { + purposeConsent, vendorConsent + } + } + expect(id5IdSubmodule.getId(config)).is.eq(undefined); + expect(id5IdSubmodule.getId(config, dataConsent)).is.eq(undefined); + + let cacheIdObject = 'cacheIdObject'; + expect(id5IdSubmodule.extendId(config)).is.eq(undefined); + expect(id5IdSubmodule.extendId(config, dataConsent, cacheIdObject)).is.eq(cacheIdObject); + }); + }); + }); + describe('Xhr Requests from getId()', function () { - const responseHeader = {'Content-Type': 'application/json'}; + const responseHeader = HEADERS_CONTENT_TYPE_JSON beforeEach(function () { }); @@ -248,7 +295,8 @@ describe('ID5 ID System', function () { let xhrServerMock = new XhrServerMock(sinon.createFakeServer()) let consentData = { gdprApplies: true, - consentString: 'consentString' + consentString: 'consentString', + vendorData: ALLOWED_ID5_VENDOR_DATA } let submoduleResponse = callSubmoduleGetId(getId5FetchConfig(), consentData, undefined); @@ -297,7 +345,8 @@ describe('ID5 ID System', function () { let xhrServerMock = new XhrServerMock(sinon.createFakeServer()) let consentData = { gdprApplies: true, - consentString: 'consentString' + consentString: 'consentString', + vendorData: ALLOWED_ID5_VENDOR_DATA } let submoduleResponse = callSubmoduleGetId(getId5FetchConfig(), consentData, undefined); @@ -674,6 +723,40 @@ describe('ID5 ID System', function () { }) }); + describe('Local storage', () => { + let sandbox; + beforeEach(() => { + sandbox = sinon.sandbox.create(); + sandbox.stub(storage, 'localStorageIsEnabled'); + }); + afterEach(() => { + sandbox.restore(); + }); + [ + [true, 1], + [false, 0] + ].forEach(function ([isEnabled, expectedValue]) { + it(`should check localStorage availability and log in request. Available=${isEnabled}`, () => { + let xhrServerMock = new XhrServerMock(sinon.createFakeServer()) + let config = getId5FetchConfig(); + let submoduleResponse = callSubmoduleGetId(config, undefined, undefined); + storage.localStorageIsEnabled.callsFake(() => isEnabled) + + return xhrServerMock.expectFetchRequest() + .then(fetchRequest => { + let requestBody = JSON.parse(fetchRequest.requestBody); + expect(requestBody.localStorage).is.eq(expectedValue); + + fetchRequest.respond(200, HEADERS_CONTENT_TYPE_JSON, JSON.stringify(ID5_JSON_RESPONSE)); + return submoduleResponse + }) + .then(submoduleResponse => { + expect(submoduleResponse).is.deep.equal(ID5_JSON_RESPONSE); + }); + }) + }) + }); + describe('Request Bids Hook', function () { let adUnits; let sandbox; @@ -797,8 +880,7 @@ describe('ID5 ID System', function () { expect(requestBody.s).is.eq(ID5_STORED_SIGNATURE); expect(requestBody.nbPage).is.eq(2); expect(getNbFromCache(ID5_TEST_PARTNER_ID)).is.eq(2); - const responseHeader = {'Content-Type': 'application/json'}; - request.respond(200, responseHeader, JSON.stringify(ID5_JSON_RESPONSE)); + request.respond(200, HEADERS_CONTENT_TYPE_JSON, JSON.stringify(ID5_JSON_RESPONSE)); return new Promise(function (resolve) { (function waitForCondition() { diff --git a/test/spec/modules/identityLinkIdSystem_spec.js b/test/spec/modules/identityLinkIdSystem_spec.js index a31270c86c7..52e9f9171d6 100644 --- a/test/spec/modules/identityLinkIdSystem_spec.js +++ b/test/spec/modules/identityLinkIdSystem_spec.js @@ -1,9 +1,9 @@ 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'; +import {getCoreStorageManager} from '../../../src/storageManager.js'; -export const storage = getStorageManager(); +const storage = getCoreStorageManager(); const pid = '14'; let defaultConfigParams; diff --git a/test/spec/modules/improvedigitalBidAdapter_spec.js b/test/spec/modules/improvedigitalBidAdapter_spec.js index b3b01dc93b5..1c3bd3197d0 100644 --- a/test/spec/modules/improvedigitalBidAdapter_spec.js +++ b/test/spec/modules/improvedigitalBidAdapter_spec.js @@ -1,9 +1,9 @@ -import { expect } from 'chai'; +import {expect} from 'chai'; import {CONVERTER, spec} from 'modules/improvedigitalBidAdapter.js'; -import { config } from 'src/config.js'; -import { deepClone } from 'src/utils.js'; +import {config} from 'src/config.js'; +import {deepClone} from 'src/utils.js'; import {BANNER, NATIVE, VIDEO} from '../../../src/mediaTypes'; -import { deepSetValue } from '../../../src/utils'; +import {deepSetValue} from '../../../src/utils'; // load modules that register ORTB processors import 'src/prebid.js'; import 'modules/currency.js'; @@ -13,8 +13,9 @@ import 'modules/priceFloors.js'; import 'modules/consentManagement.js'; import 'modules/consentManagementUsp.js'; import 'modules/schain.js'; -import {decorateAdUnitsWithNativeParams, toLegacyResponse} from '../../../src/native.js'; -import {createEidsArray} from '../../../modules/userId/eids.js'; +import {decorateAdUnitsWithNativeParams} from '../../../src/native.js'; +import {syncAddFPDToBidderRequest} from '../../helpers/fpd.js'; +import {hook} from '../../../src/hook.js'; describe('Improve Digital Adapter Tests', function () { const METHOD = 'POST'; @@ -168,6 +169,10 @@ describe('Improve Digital Adapter Tests', function () { return bidRequests; } + before(() => { + hook.ready(); + }); + describe('isBidRequestValid', function () { it('should return false when no bid', function () { expect(spec.isBidRequestValid()).to.equal(false); @@ -220,7 +225,7 @@ describe('Improve Digital Adapter Tests', function () { it('should make a well-formed request objects', function () { getConfigStub = sinon.stub(config, 'getConfig'); getConfigStub.withArgs('improvedigital.usePrebidSizes').returns(true); - const request = spec.buildRequests([simpleBidRequest], bidderRequest)[0]; + const request = spec.buildRequests([simpleBidRequest], syncAddFPDToBidderRequest(bidderRequest))[0]; expect(request).to.be.an('object'); expect(request.method).to.equal(METHOD); expect(request.url).to.equal(AD_SERVER_URL); @@ -272,12 +277,14 @@ describe('Improve Digital Adapter Tests', function () { placementId: 1053688, } }, - video: { - placement: OUTSTREAM_TYPE, - w: 640, - h: 480, - mimes: ['video/mp4'], - }, + ...(FEATURES.VIDEO && { + video: { + placement: OUTSTREAM_TYPE, + w: 640, + h: 480, + mimes: ['video/mp4'], + } + }), banner: { format: [ {w: 300, h: 250}, @@ -390,7 +397,7 @@ describe('Improve Digital Adapter Tests', function () { it('should add GDPR consent string', function () { const bidRequest = Object.assign({}, simpleBidRequest); - const payload = JSON.parse(spec.buildRequests([bidRequest], bidderRequestGdpr)[0].data); + const payload = JSON.parse(spec.buildRequests([bidRequest], syncAddFPDToBidderRequest(bidderRequestGdpr))[0].data); expect(payload.regs.ext.gdpr).to.exist.and.to.equal(1); expect(payload.user.ext.consent).to.equal('CONSENT'); expect(payload.user.ext.ConsentedProvidersSettings).to.not.exist; @@ -401,13 +408,13 @@ describe('Improve Digital Adapter Tests', function () { const bidderRequestGdprEmptyAddtl = deepClone(bidderRequestGdpr); bidderRequestGdprEmptyAddtl.gdprConsent.addtlConsent = '1~'; const bidRequest = Object.assign({}, simpleBidRequest); - const payload = JSON.parse(spec.buildRequests([bidRequest], bidderRequestGdprEmptyAddtl)[0].data); + const payload = JSON.parse(spec.buildRequests([bidRequest], syncAddFPDToBidderRequest(bidderRequestGdprEmptyAddtl))[0].data); expect(payload.user.ext.consented_providers_settings).to.not.exist; }); it('should add ConsentedProvidersSettings when extend mode enabled', function () { const bidRequest = deepClone(extendBidRequest); - const payload = JSON.parse(spec.buildRequests([bidRequest], bidderRequestGdpr)[0].data); + const payload = JSON.parse(spec.buildRequests([bidRequest], syncAddFPDToBidderRequest(bidderRequestGdpr))[0].data); expect(payload.regs.ext.gdpr).to.exist.and.to.equal(1); expect(payload.user.ext.consent).to.equal('CONSENT'); expect(payload.user.ext.ConsentedProvidersSettings.consented_providers).to.exist.and.to.equal('1~1.35.41.101'); @@ -416,7 +423,7 @@ describe('Improve Digital Adapter Tests', function () { it('should add CCPA consent string', function () { const bidRequest = Object.assign({}, simpleBidRequest); - const request = spec.buildRequests([bidRequest], {...bidderRequest, ...{ uspConsent: '1YYY' }}); + const request = spec.buildRequests([bidRequest], syncAddFPDToBidderRequest({...bidderRequest, ...{ uspConsent: '1YYY' }})); const payload = JSON.parse(request[0].data); expect(payload.regs.ext.us_privacy).to.equal('1YYY'); }); @@ -425,17 +432,17 @@ describe('Improve Digital Adapter Tests', function () { getConfigStub = sinon.stub(config, 'getConfig'); getConfigStub.withArgs('coppa').returns(true); let bidRequest = Object.assign({}, simpleBidRequest); - let payload = JSON.parse(spec.buildRequests([bidRequest], bidderRequestGdpr)[0].data); + let payload = JSON.parse(spec.buildRequests([bidRequest], syncAddFPDToBidderRequest(bidderRequestGdpr))[0].data); expect(payload.regs.coppa).to.equal(1); getConfigStub.withArgs('coppa').returns(false); bidRequest = Object.assign({}, simpleBidRequest); - payload = JSON.parse(spec.buildRequests([bidRequest], bidderRequestGdpr)[0].data); + payload = JSON.parse(spec.buildRequests([bidRequest], syncAddFPDToBidderRequest(bidderRequestGdpr))[0].data); expect(payload.regs.coppa).to.equal(0); }); it('should add referrer', function () { const bidRequest = Object.assign({}, simpleBidRequest); - const request = spec.buildRequests([bidRequest], bidderRequestReferrer)[0]; + const request = spec.buildRequests([bidRequest], syncAddFPDToBidderRequest(bidderRequestReferrer))[0]; const payload = JSON.parse(request.data); expect(payload.site.page).to.equal('https://blah.com/test.html'); }); @@ -462,102 +469,104 @@ describe('Improve Digital Adapter Tests', function () { expect(payload.imp[0].video).to.not.exist; }); - it('should add correct placement value for instream and outstream video', function () { - let bidRequest = deepClone(simpleBidRequest); - let payload = JSON.parse(spec.buildRequests([bidRequest], bidderRequest)[0].data); - expect(payload.imp[0].video).to.not.exist; + if (FEATURES.VIDEO) { + it('should add correct placement value for instream and outstream video', function () { + let bidRequest = deepClone(simpleBidRequest); + let payload = JSON.parse(spec.buildRequests([bidRequest], bidderRequest)[0].data); + expect(payload.imp[0].video).to.not.exist; - bidRequest = deepClone(simpleBidRequest); - bidRequest.mediaTypes = { - video: { - context: 'instream', - playerSize: [640, 480] - } - }; - payload = JSON.parse(spec.buildRequests([bidRequest], bidderRequest)[0].data); - expect(payload.imp[0].video.placement).to.exist.and.equal(1); - bidRequest.mediaTypes.video.context = 'outstream'; - payload = JSON.parse(spec.buildRequests([bidRequest], bidderRequest)[0].data); - expect(payload.imp[0].video.placement).to.exist.and.equal(3); - }); + bidRequest = deepClone(simpleBidRequest); + bidRequest.mediaTypes = { + video: { + context: 'instream', + playerSize: [640, 480] + } + }; + payload = JSON.parse(spec.buildRequests([bidRequest], bidderRequest)[0].data); + expect(payload.imp[0].video.placement).to.exist.and.equal(1); + bidRequest.mediaTypes.video.context = 'outstream'; + payload = JSON.parse(spec.buildRequests([bidRequest], bidderRequest)[0].data); + expect(payload.imp[0].video.placement).to.exist.and.equal(3); + }); - it('should set video params for instream', function() { - const bidRequest = deepClone(instreamBidRequest); - delete bidRequest.mediaTypes.video.playerSize; - const videoParams = { - mimes: ['video/mp4'], - skip: 1, - skipmin: 5, - skipafter: 30, - minduration: 15, - maxduration: 60, - startdelay: 5, - minbitrate: 500, - maxbitrate: 2000, - w: 1024, - h: 640, - placement: INSTREAM_TYPE, - }; - bidRequest.params.video = videoParams; - const request = spec.buildRequests([bidRequest], bidderRequest)[0]; - const payload = JSON.parse(request.data); - expect(payload.imp[0].video).to.deep.equal(videoParams); - }); + it('should set video params for instream', function() { + const bidRequest = deepClone(instreamBidRequest); + delete bidRequest.mediaTypes.video.playerSize; + const videoParams = { + mimes: ['video/mp4'], + skip: 1, + skipmin: 5, + skipafter: 30, + minduration: 15, + maxduration: 60, + startdelay: 5, + minbitrate: 500, + maxbitrate: 2000, + w: 1024, + h: 640, + placement: INSTREAM_TYPE, + }; + bidRequest.params.video = videoParams; + const request = spec.buildRequests([bidRequest], bidderRequest)[0]; + const payload = JSON.parse(request.data); + expect(payload.imp[0].video).to.deep.equal(videoParams); + }); - it('should set video playerSize over video params', () => { - const bidRequest = deepClone(instreamBidRequest); - bidRequest.params.video = { - w: 1024, h: 640 - } - const request = spec.buildRequests([bidRequest], bidderRequest)[0]; - const payload = JSON.parse(request.data); - expect(payload.imp[0].video.h).equal(480); - expect(payload.imp[0].video.w).equal(640); - }); + it('should set video playerSize over video params', () => { + const bidRequest = deepClone(instreamBidRequest); + bidRequest.params.video = { + w: 1024, h: 640 + } + const request = spec.buildRequests([bidRequest], bidderRequest)[0]; + const payload = JSON.parse(request.data); + expect(payload.imp[0].video.h).equal(480); + expect(payload.imp[0].video.w).equal(640); + }); - it('should ignore invalid/unexpected video params', function() { - const bidRequest = deepClone(instreamBidRequest); - // 1 - const videoTest = { - skip: 1, - skipmin: 5, - skipafter: 30 - } - const videoTestInvParam = Object.assign({}, videoTest); - videoTestInvParam.blah = 1; - bidRequest.params.video = videoTestInvParam; - let request = spec.buildRequests([bidRequest], {})[0]; - let payload = JSON.parse(request.data); - expect(payload.imp[0].video.blah).not.to.exist; - }); + it('should ignore invalid/unexpected video params', function() { + const bidRequest = deepClone(instreamBidRequest); + // 1 + const videoTest = { + skip: 1, + skipmin: 5, + skipafter: 30 + } + const videoTestInvParam = Object.assign({}, videoTest); + videoTestInvParam.blah = 1; + bidRequest.params.video = videoTestInvParam; + let request = spec.buildRequests([bidRequest], {})[0]; + let payload = JSON.parse(request.data); + expect(payload.imp[0].video.blah).not.to.exist; + }); - it('should set video params for outstream', function() { - const bidRequest = deepClone(outstreamBidRequest); - bidRequest.params.video = videoParams; - const request = spec.buildRequests([bidRequest], {})[0]; - const payload = JSON.parse(request.data); - expect(payload.imp[0].video).to.deep.equal({...{ - mimes: ['video/mp4'], - placement: OUTSTREAM_TYPE, - w: bidRequest.mediaTypes.video.playerSize[0], - h: bidRequest.mediaTypes.video.playerSize[1], - }, - ...videoParams}); - }); - // - it('should set video params for multi-format', function() { - const bidRequest = deepClone(multiFormatBidRequest); - bidRequest.params.video = videoParams; - const request = spec.buildRequests([bidRequest], {})[0]; - const payload = JSON.parse(request.data); - const testVideoParams = Object.assign({ - placement: OUTSTREAM_TYPE, - w: 640, - h: 480, - mimes: ['video/mp4'], - }, videoParams); - expect(payload.imp[0].video).to.deep.equal(testVideoParams); - }); + it('should set video params for outstream', function() { + const bidRequest = deepClone(outstreamBidRequest); + bidRequest.params.video = videoParams; + const request = spec.buildRequests([bidRequest], {})[0]; + const payload = JSON.parse(request.data); + expect(payload.imp[0].video).to.deep.equal({...{ + mimes: ['video/mp4'], + placement: OUTSTREAM_TYPE, + w: bidRequest.mediaTypes.video.playerSize[0], + h: bidRequest.mediaTypes.video.playerSize[1], + }, + ...videoParams}); + }); + // + it('should set video params for multi-format', function() { + const bidRequest = deepClone(multiFormatBidRequest); + bidRequest.params.video = videoParams; + const request = spec.buildRequests([bidRequest], {})[0]; + const payload = JSON.parse(request.data); + const testVideoParams = Object.assign({ + placement: OUTSTREAM_TYPE, + w: 640, + h: 480, + mimes: ['video/mp4'], + }, videoParams); + expect(payload.imp[0].video).to.deep.equal(testVideoParams); + }); + } it('should add schain', function () { const schain = '{"ver":"1.0","complete":1,"nodes":[{"asi":"headerlift.com","sid":"xyz","hp":1}]}'; @@ -569,7 +578,15 @@ describe('Improve Digital Adapter Tests', function () { }); it('should add eids', function () { - const userId = { id5id: { uid: '1111' } }; + const userIdAsEids = [ + { + source: 'id5-sync.com', + uids: [{ + atype: 1, + id: '1111' + }] + } + ]; const expectedUserObject = { ext: { eids: [{ source: 'id5-sync.com', uids: [{ @@ -578,7 +595,7 @@ describe('Improve Digital Adapter Tests', function () { }] }]}}; const bidRequest = Object.assign({}, simpleBidRequest); - bidRequest.userIdAsEids = createEidsArray(userId); + bidRequest.userIdAsEids = userIdAsEids; const request = spec.buildRequests([bidRequest], bidderRequestReferrer)[0]; const payload = JSON.parse(request.data); expect(payload.user).to.deep.equal(expectedUserObject); @@ -603,7 +620,7 @@ describe('Improve Digital Adapter Tests', function () { const request = JSON.parse(requests[0].data); expect(request.imp.length).to.equal(2); expect(request.imp[0].banner).to.exist; - expect(request.imp[1].video).to.exist; + if (FEATURES.VIDEO) { expect(request.imp[1].video).to.exist; } }); it('should create one request per endpoint in a single request mode', function () { @@ -617,7 +634,7 @@ describe('Improve Digital Adapter Tests', function () { const adServerRequest = JSON.parse(requests[1].data); expect(adServerRequest.imp.length).to.equal(2); expect(adServerRequest.imp[0].banner).to.exist; - expect(adServerRequest.imp[1].video).to.exist; + if (FEATURES.VIDEO) { expect(adServerRequest.imp[1].video).to.exist; } }); it('should set Prebid sizes in bid request', function () { @@ -664,7 +681,7 @@ describe('Improve Digital Adapter Tests', function () { it('should not set site when app is defined in CONFIG', function () { getConfigStub = sinon.stub(config, 'getConfig'); getConfigStub.withArgs('app').returns({ content: 'XYZ' }); - let request = spec.buildRequests([simpleBidRequest], bidderRequest)[0]; + let request = spec.buildRequests([simpleBidRequest], syncAddFPDToBidderRequest(bidderRequest))[0]; let payload = JSON.parse(request.data); expect(payload.site).does.not.exist; expect(payload.app).does.exist; @@ -678,21 +695,21 @@ describe('Improve Digital Adapter Tests', function () { page: 'https://improveditigal.com/', domain: 'improveditigal.com' }); - let request = spec.buildRequests([simpleBidRequest], bidderRequestReferrer)[0]; + let request = spec.buildRequests([simpleBidRequest], syncAddFPDToBidderRequest(bidderRequestReferrer))[0]; let payload = JSON.parse(request.data); expect(payload.site.content).does.exist.and.equal('XYZ'); expect(payload.site.page).does.exist.and.equal('https://improveditigal.com/'); expect(payload.site.domain).does.exist.and.equal('improveditigal.com'); getConfigStub.reset(); - request = spec.buildRequests([simpleBidRequest], bidderRequestReferrer)[0]; + request = spec.buildRequests([simpleBidRequest], syncAddFPDToBidderRequest(bidderRequestReferrer))[0]; payload = JSON.parse(request.data); expect(payload.site.content).does.not.exist; expect(payload.site.page).does.exist.and.equal('https://blah.com/test.html'); expect(payload.site.domain).does.exist.and.equal('blah.com'); const ortb2 = {site: {content: 'ZZZ'}}; - request = spec.buildRequests([simpleBidRequest], {...bidderRequestReferrer, ortb2})[0]; + request = spec.buildRequests([simpleBidRequest], syncAddFPDToBidderRequest({...bidderRequestReferrer, ortb2}))[0]; payload = JSON.parse(request.data); expect(payload.site.content).does.exist.and.equal('ZZZ'); expect(payload.site.page).does.exist.and.equal('https://blah.com/test.html'); @@ -703,7 +720,7 @@ describe('Improve Digital Adapter Tests', function () { getConfigStub = sinon.stub(config, 'getConfig'); getConfigStub.withArgs('app').returns(undefined); getConfigStub.withArgs('site').returns({}); - let request = spec.buildRequests([simpleBidRequest], bidderRequest)[0]; + let request = spec.buildRequests([simpleBidRequest], syncAddFPDToBidderRequest(bidderRequest))[0]; let payload = JSON.parse(request.data); expect(payload.site).does.exist; expect(payload.app).does.not.exist; @@ -1232,32 +1249,34 @@ describe('Improve Digital Adapter Tests', function () { } // Video - it('should return a well-formed instream video bid', function () { - const bids = spec.interpretResponse(serverResponseVideo, makeRequest(instreamBidderRequest)); - expectMatch(bids, expectedBidInstreamVideo); - }); + if (FEATURES.VIDEO) { + it('should return a well-formed instream video bid', function () { + const bids = spec.interpretResponse(serverResponseVideo, makeRequest(instreamBidderRequest)); + expectMatch(bids, expectedBidInstreamVideo); + }); - it('should return a well-formed outstream video bid', function () { - const bids = spec.interpretResponse(serverResponseVideo, makeRequest(outstreamBidderRequest)); - expect(bids[0].renderer).to.exist; - expectMatch(bids, expectedBidOutstreamVideo); - }); + it('should return a well-formed outstream video bid', function () { + const bids = spec.interpretResponse(serverResponseVideo, makeRequest(outstreamBidderRequest)); + expect(bids[0].renderer).to.exist; + expectMatch(bids, expectedBidOutstreamVideo); + }); - it('should return a well-formed outstream video bid for multi-format ad unit', function () { - const request = makeRequest(multiFormatBidderRequest); - const videoResponse = deepClone(serverResponseVideo); - let bids = spec.interpretResponse(videoResponse, request); - expect(bids[0].renderer).to.exist; - expectMatch(bids, expectedBidOutstreamVideo); + it('should return a well-formed outstream video bid for multi-format ad unit', function () { + const request = makeRequest(multiFormatBidderRequest); + const videoResponse = deepClone(serverResponseVideo); + let bids = spec.interpretResponse(videoResponse, request); + expect(bids[0].renderer).to.exist; + expectMatch(bids, expectedBidOutstreamVideo); - videoResponse.body.seatbid[0].bid[0].adm = '

Ad from IncrementX

', slotBidId: 'bid-id-123456', + adType: '1', + settings: '1,2', nurl: 'htt://nurl.com', statusText: 'Success' } @@ -80,6 +82,8 @@ describe('IncrementX', function () { requestId: 'bid-id-123456', cpm: '0.7', currency: 'USD', + adType: '1', + settings: '1,2', netRevenue: false, width: '300', height: '250', diff --git a/test/spec/modules/iqzoneBidAdapter_spec.js b/test/spec/modules/iqzoneBidAdapter_spec.js index de0459f4714..2e920d3b769 100644 --- a/test/spec/modules/iqzoneBidAdapter_spec.js +++ b/test/spec/modules/iqzoneBidAdapter_spec.js @@ -76,7 +76,8 @@ describe('IQZoneBidAdapter', function () { gdprConsent: 'COvFyGBOvFyGBAbAAAENAPCAAOAAAAAAAAAAAEEUACCKAAA.IFoEUQQgAIQwgIwQABAEAAAAOIAACAIAAAAQAIAgEAACEAAAAAgAQBAAAAAAAGBAAgAAAAAAAFAAECAAAgAAQARAEQAAAAAJAAIAAgAAAYQEAAAQmAgBC3ZAYzUw', refererInfo: { referer: 'https://test.com' - } + }, + timeout: 500 }; describe('isBidRequestValid', function () { diff --git a/test/spec/modules/ivsBidAdapter_spec.js b/test/spec/modules/ivsBidAdapter_spec.js new file mode 100644 index 00000000000..79b3d6811f4 --- /dev/null +++ b/test/spec/modules/ivsBidAdapter_spec.js @@ -0,0 +1,197 @@ +import { spec, converter } from 'modules/ivsBidAdapter.js'; +import { assert } from 'chai'; +import { deepClone } from '../../../src/utils'; + +describe('ivsBidAdapter', function () { + describe('isBidRequestValid()', function () { + let validBid = { + bidder: 'ivs', + mediaTypes: { + video: { + context: 'instream', + playerSize: [640, 480], + mimes: ['video/mp4'] + } + }, + params: { + bidderDomain: 'https://www.example.com', + publisherId: '3001234' + } + }; + + it('should return true for a valid bid', function () { + assert.isTrue(spec.isBidRequestValid(validBid)); + }); + + it('should return false if publisherId info is missing', function () { + let bid = deepClone(validBid); + delete bid.params.publisherId; + assert.isFalse(spec.isBidRequestValid(bid)); + }); + + it('should return false for empty video parameters', function () { + let bid = deepClone(validBid); + delete bid.mediaTypes.video; + assert.isFalse(spec.isBidRequestValid(bid)); + }); + + it('should return false for non instream context', function () { + let bid = deepClone(validBid); + bid.mediaTypes.video.context = 'outstream'; + assert.isFalse(spec.isBidRequestValid(bid)); + }); + }); + + describe('buildRequests()', function () { + let validBidRequests, validBidderRequest; + + beforeEach(function () { + validBidRequests = [{ + bidder: 'ivs', + mediaTypes: { + video: { + context: 'instream', + playerSize: [640, 480], + mimes: ['video/mp4'] + }, + adUnitCode: 'video1', + transactionId: '1f420478-a3cd-452d-8e33-ac851e7bfba6', + bidId: '2d986cea00fd01', + bidderRequestId: '1022d594d79bf5', + auctionId: '835eacc9-cfe7-4fa2-8738-ab4b5c4f26d2' + }, + params: { + bidderDomain: 'https://www.example.com', + publisherId: '3001234' + } + }]; + + validBidderRequest = { + bidderCode: 'ivs', + auctionId: '409bd13d-d0be-43c4-9c4f-e6f81ecff475', + bidderRequestId: '17bfe74bd98e68', + refererInfo: { + domain: 'example.com', + page: 'https://www.example.com/test.html', + }, + bids: [{ + bidder: 'ivs', + params: { + publisherId: '3001234' + }, + mediaTypes: { + video: { + context: 'instream', + playerSize: [[640, 480]], + mimes: ['video/mp4'] + } + }, + adUnitCode: 'video1', + transactionId: '91b1977f-d05c-45c3-af1f-69b4e7d11e86', + sizes: [ + [640, 480] + ], + }], + ortb2: { + site: { + publisher: { + domain: 'example.com', + } + } + } + }; + }); + + it('should return a validate bid request', function () { + const bidRequest = spec.buildRequests(validBidRequests, validBidderRequest); + assert.equal(bidRequest.method, 'POST'); + assert.deepEqual(bidRequest.options, { contentType: 'application/json' }); + assert.ok(bidRequest.data); + }); + + it('should contain the required parameters', function () { + const bidRequest = spec.buildRequests(validBidRequests, validBidderRequest); + const bidderRequest = bidRequest.data; + assert.equal(bidderRequest.id, validBidderRequest.auctionId); + assert.ok(bidderRequest.site); + assert.ok(bidderRequest.source); + assert.lengthOf(bidderRequest.imp, 1); + }); + }); + + describe('interpretResponse()', function () { + let serverResponse, bidderRequest, request; + + beforeEach(function () { + serverResponse = { + body: { + id: '635ba1ed-68ba-47b4-bcec-4a86565f25f9', + seatbid: [{ + bid: [{ + crid: 3715, + id: 'bca9823d-ca7a-4dac-b292-0e1fae5948f8', + impid: '200d1ca23b15a6', + price: 1.5, + nurl: 'https://a.ivstracker.net/dev/getvastxml?ad_creativeid=3715&domain=localhost%3A9999&ip=136.158.51.114&pageurl=http%3A%2F%2Flocalhost%3A9999%2Ftest%2Fpages%2Fivs.html&spid=3001234&adplacement=&brand=unknown&device=desktop&adsclientid=A45-fd46289e-dc60-4be2-a637-4bc8eb953ddf&clientid=3b5e435f-0351-4ba0-bd2d-8d6f3454c5ed&uastring=Mozilla%2F5.0%20(Macintosh%3B%20Intel%20Mac%20OS%20X%2010_15_7)%20AppleWebKit%2F537.36%20(KHTML%2C%20like%20Gecko)%20Chrome%2F109.0.0.0%20Safari%2F537.36' + }] + }], + cur: 'USD' + }, + headers: {} + }; + + bidderRequest = { + bidderCode: 'ivs', + auctionId: '635ba1ed-68ba-47b4-bcec-4a86565f25f9', + bidderRequestId: '1def3e1d03f5a', + bids: [{ + bidder: 'ivs', + params: { + publisherId: '3001234' + }, + mediaTypes: { + video: { + context: 'instream', + playerSize: [ + [640, 480] + ], + mimes: ['video/mp4'] + } + }, + adUnitCode: 'video1', + transactionId: '89e5a3e7-df30-4ed6-a130-edfa91941e67', + bidId: '200d1ca23b15a6', + bidderRequestId: '1def3e1d03f5a', + auctionId: '635ba1ed-68ba-47b4-bcec-4a86565f25f9' + }], + }; + + request = { data: converter.toORTB({ bidderRequest }) }; + }); + + if (FEATURES.VIDEO) { + it('should match parsed server response', function () { + const results = spec.interpretResponse(serverResponse, request); + const expected = { + mediaType: 'video', + playerWidth: 640, + playerHeight: 480, + vastUrl: 'https://a.ivstracker.net/dev/getvastxml?ad_creativeid=3715&domain=localhost%3A9999&ip=136.158.51.114&pageurl=http%3A%2F%2Flocalhost%3A9999%2Ftest%2Fpages%2Fivs.html&spid=3001234&adplacement=&brand=unknown&device=desktop&adsclientid=A45-fd46289e-dc60-4be2-a637-4bc8eb953ddf&clientid=3b5e435f-0351-4ba0-bd2d-8d6f3454c5ed&uastring=Mozilla%2F5.0%20(Macintosh%3B%20Intel%20Mac%20OS%20X%2010_15_7)%20AppleWebKit%2F537.36%20(KHTML%2C%20like%20Gecko)%20Chrome%2F109.0.0.0%20Safari%2F537.36', + requestId: '200d1ca23b15a6', + seatBidId: 'bca9823d-ca7a-4dac-b292-0e1fae5948f8', + cpm: 1.5, + currency: 'USD', + creativeId: 3715, + ttl: 360, + }; + + expect(results.length).to.equal(1); + sinon.assert.match(results[0], expected); + }); + } + + it('should return empty when no response', function () { + assert.ok(!spec.interpretResponse({}, request)); + }); + }); +}); diff --git a/test/spec/modules/ixBidAdapter_spec.js b/test/spec/modules/ixBidAdapter_spec.js index 0de300e4a32..8ea3b9dc522 100644 --- a/test/spec/modules/ixBidAdapter_spec.js +++ b/test/spec/modules/ixBidAdapter_spec.js @@ -2,14 +2,14 @@ import * as utils from 'src/utils.js'; import { config } from 'src/config.js'; import { expect } from 'chai'; import { newBidder } from 'src/adapters/bidderFactory.js'; -import { spec, storage, ERROR_CODES, FEATURE_TOGGLES, LOCAL_STORAGE_FEATURE_TOGGLES_KEY } from '../../../modules/ixBidAdapter.js'; +import { spec, storage, ERROR_CODES, FEATURE_TOGGLES, LOCAL_STORAGE_FEATURE_TOGGLES_KEY, REQUESTED_FEATURE_TOGGLES } from '../../../modules/ixBidAdapter.js'; import { createEidsArray } from 'modules/userId/eids.js'; import { deepAccess, deepClone } from '../../../src/utils.js'; describe('IndexexchangeAdapter', function () { const IX_SECURE_ENDPOINT = 'https://htlb.casalemedia.com/openrtb/pbjs'; - const VIDEO_ENDPOINT_VERSION = 8.1; - const BANNER_ENDPOINT_VERSION = 7.2; + + FEATURE_TOGGLES.REQUESTED_FEATURE_TOGGLES = ['test1', 'test2']; const SAMPLE_SCHAIN = { 'ver': '1.0', @@ -658,7 +658,7 @@ describe('IndexexchangeAdapter', function () { } }; - const DEFAULT_VIDEO_BID_RESPONSE_WITH_MTYPE_SET = { + const DEFAULT_VIDEO_BID_RESPONSE_WITH_XML_ADM = { cur: 'USD', id: '1aa2bb3cc4de', seatbid: [ @@ -675,7 +675,6 @@ describe('IndexexchangeAdapter', function () { mtype: 2, adm: ' Test In-Stream Video { + it('6 ad units should generate only 1 request if buildRequestV2 FT is enabled', function () { sandbox.stub(storage, 'localStorageIsEnabled').returns(true); - serverResponse.body.ext.features.pbjs_use_32kb_size_limit = { - activated: true - }; - FEATURE_TOGGLES.setFeatureToggles(serverResponse); - const bid = utils.deepClone(DEFAULT_MULTIFORMAT_VIDEO_VALID_BID[0]); - bid.bidderRequestId = Array(10000).join('#'); - - expect(spec.isBidRequestValid(bid)).to.be.true; - spec.buildRequests([bid], {}); - const lsData = JSON.parse(storage.getDataFromLocalStorage(LOCAL_STORAGE_FEATURE_TOGGLES_KEY)); - expect(lsData.features.pbjs_use_32kb_size_limit.activated).to.be.true; - }); - - it('6 ad units should generate only 2 requests if 32kb size limit FT is enabled', function () { - sandbox.stub(storage, 'localStorageIsEnabled').returns(true); - serverResponse.body.ext.features.pbjs_use_32kb_size_limit = { - activated: true - }; - serverResponse.body.ext.features.pbjs_enable_post = { + serverResponse.body.ext.features.pbjs_use_buildRequestV2 = { activated: true }; FEATURE_TOGGLES.setFeatureToggles(serverResponse); @@ -3549,47 +3626,106 @@ describe('IndexexchangeAdapter', function () { const requests = spec.buildRequests([bid1, bid2, bid3, bid4, bid5, bid6], DEFAULT_OPTION); expect(requests).to.be.an('array'); - // 32KB size limit causes only 2 requests to get generated. - expect(requests).to.have.lengthOf(2); + // buildRequestv2 enabled causes only 1 requests to get generated. + expect(requests).to.have.lengthOf(1); for (let request of requests) { expect(request.method).to.equal('POST'); } }); - it('4 ad units should generate only 1 requests if 32kb size limit FT is enabled', function () { + it('1 request with 2 ad units, buildRequestV2 enabled', function () { sandbox.stub(storage, 'localStorageIsEnabled').returns(true); - serverResponse.body.ext.features.pbjs_use_32kb_size_limit = { - activated: true - }; - serverResponse.body.ext.features.pbjs_enable_post = { + serverResponse.body.ext.features.pbjs_use_buildRequestV2 = { activated: true }; FEATURE_TOGGLES.setFeatureToggles(serverResponse); - const bid1 = utils.deepClone(DEFAULT_BANNER_VALID_BID[0]); - bid1.mediaTypes.banner.sizes = LARGE_SET_OF_SIZES; - bid1.params.siteId = '121'; - bid1.adUnitCode = 'div-gpt-1' - bid1.transactionId = 'tr1'; - bid1.bidId = '2f6g5s5e'; + const bid = utils.deepClone(DEFAULT_BANNER_VALID_BID[0]); + bid.mediaTypes.banner.sizes = LARGE_SET_OF_SIZES; + bid.params.siteId = '124'; + bid.adUnitCode = 'div-gpt-1' + bid.transactionId = '152e36d1-1241-4242-t35e-y1dv34d12315'; + bid.bidId = '2f6g5s5e'; - const bid2 = utils.deepClone(bid1); - bid2.transactionId = 'tr2'; + const requests = spec.buildRequests([bid, DEFAULT_BANNER_VALID_BID[0]], DEFAULT_OPTION); + expect(requests).to.be.an('array'); + expect(requests).to.have.lengthOf(1); + }); - const bid3 = utils.deepClone(bid1); - bid3.transactionId = 'tr3'; + it('request should have requested feature toggles when local storage is enabled', function () { + sandbox.stub(storage, 'localStorageIsEnabled').returns(true); + const bid = utils.deepClone(DEFAULT_BANNER_VALID_BID[0]); + const requests = spec.buildRequests([bid, DEFAULT_BANNER_VALID_BID[0]], DEFAULT_OPTION); + const r = extractPayload(requests[0]); + expect(r.ext.features).to.exist; + expect(Object.keys(r.ext.features).length).to.equal(FEATURE_TOGGLES.REQUESTED_FEATURE_TOGGLES.length); + }); - const bid4 = utils.deepClone(bid1); - bid4.transactionId = 'tr4'; + it('request should have requested feature toggles when local storage is not enabled', function () { + sandbox.stub(storage, 'localStorageIsEnabled').returns(false); + const bid = utils.deepClone(DEFAULT_BANNER_VALID_BID[0]); + const requests = spec.buildRequests([bid, DEFAULT_BANNER_VALID_BID[0]], DEFAULT_OPTION); + const r = extractPayload(requests[0]); + expect(r.ext.features).to.exist; + expect(Object.keys(r.ext.features).length).to.equal(FEATURE_TOGGLES.REQUESTED_FEATURE_TOGGLES.length); + }); - const requests = spec.buildRequests([bid1, bid2, bid3, bid4], DEFAULT_OPTION); + it('request should not have any feature toggles when there is no requested feature toggle', function () { + sandbox.stub(storage, 'localStorageIsEnabled').returns(true); + const bid = utils.deepClone(DEFAULT_BANNER_VALID_BID[0]); + FEATURE_TOGGLES.REQUESTED_FEATURE_TOGGLES = [] + const requests = spec.buildRequests([bid, DEFAULT_BANNER_VALID_BID[0]], DEFAULT_OPTION); + const r = extractPayload(requests[0]); + expect(r.ext.features).to.be.undefined; + }); - expect(requests).to.be.an('array'); - // 32KB size limit causes only 1 requests to get generated. - expect(requests).to.have.lengthOf(1); - for (let request of requests) { - expect(request.method).to.equal('POST'); + it('request should not have any feature toggles when there is no requested feature toggle and local storage not enabled', function () { + sandbox.stub(storage, 'localStorageIsEnabled').returns(false); + const bid = utils.deepClone(DEFAULT_BANNER_VALID_BID[0]); + const requests = spec.buildRequests([bid, DEFAULT_BANNER_VALID_BID[0]], DEFAULT_OPTION); + const r = extractPayload(requests[0]); + FEATURE_TOGGLES.REQUESTED_FEATURE_TOGGLES = [] + expect(r.ext.features).to.be.undefined; + }); + + it('correct activation status of requested feature toggles when local storage not enabled', function () { + sandbox.stub(storage, 'localStorageIsEnabled').returns(false); + const bid = utils.deepClone(DEFAULT_BANNER_VALID_BID[0]); + FEATURE_TOGGLES.REQUESTED_FEATURE_TOGGLES = ['test1'] + const requests = spec.buildRequests([bid, DEFAULT_BANNER_VALID_BID[0]], DEFAULT_OPTION); + const r = extractPayload(requests[0]); + expect(r.ext.features).to.deep.equal({ + [FEATURE_TOGGLES.REQUESTED_FEATURE_TOGGLES[0]]: { activated: false } + }); + }); + + it('correct activation status of requested feature toggles', function () { + sandbox.stub(storage, 'localStorageIsEnabled').returns(true); + serverResponse.body.ext.features = { + [FEATURE_TOGGLES.REQUESTED_FEATURE_TOGGLES[0]]: { + activated: true + } } + FEATURE_TOGGLES.setFeatureToggles(serverResponse); + let bid = utils.deepClone(DEFAULT_BANNER_VALID_BID[0]); + let requests = spec.buildRequests([bid, DEFAULT_BANNER_VALID_BID[0]], DEFAULT_OPTION); + let r = extractPayload(requests[0]); + expect(r.ext.features).to.deep.equal({ + [FEATURE_TOGGLES.REQUESTED_FEATURE_TOGGLES[0]]: { activated: true } + }); + + serverResponse.body.ext.features = { + [FEATURE_TOGGLES.REQUESTED_FEATURE_TOGGLES[0]]: { + activated: false + } + } + FEATURE_TOGGLES.setFeatureToggles(serverResponse); + bid = utils.deepClone(DEFAULT_BANNER_VALID_BID[0]); + requests = spec.buildRequests([bid, DEFAULT_BANNER_VALID_BID[0]], DEFAULT_OPTION); + r = extractPayload(requests[0]); + expect(r.ext.features).to.deep.equal({ + [FEATURE_TOGGLES.REQUESTED_FEATURE_TOGGLES[0]]: { activated: false } + }); }); }); @@ -3670,48 +3806,6 @@ describe('IndexexchangeAdapter', function () { expect(JSON.parse(localStorageValues[key])).to.deep.equal({ [TODAY]: { [ERROR_CODES.BID_FLOOR_INVALID_FORMAT]: 1 } }); }); - it('should log ERROR_CODES.IX_FPD_EXCEEDS_MAX_SIZE in LocalStorage when there is logError called.', () => { - const bid = utils.deepClone(DEFAULT_MULTIFORMAT_VIDEO_VALID_BID[0]); - - config.setConfig({ - ix: { - firstPartyData: { - cd: Array(1700).join('#') - } - } - }); - - expect(spec.isBidRequestValid(bid)).to.be.true; - spec.buildRequests([bid], {}); - expect(JSON.parse(localStorageValues[key])).to.deep.equal({ [TODAY]: { [ERROR_CODES.IX_FPD_EXCEEDS_MAX_SIZE]: 2 } }); - }); - - it('should log ERROR_CODES.EXCEEDS_MAX_SIZE in LocalStorage when there is logError called.', () => { - const bid = utils.deepClone(DEFAULT_MULTIFORMAT_VIDEO_VALID_BID[0]); - bid.bidderRequestId = Array(8000).join('#'); - - expect(spec.isBidRequestValid(bid)).to.be.true; - spec.buildRequests([bid], {}); - expect(JSON.parse(localStorageValues[key])).to.deep.equal({ [TODAY]: { [ERROR_CODES.EXCEEDS_MAX_SIZE]: 2 } }); - }); - - it('should log ERROR_CODES.PB_FPD_EXCEEDS_MAX_SIZE in LocalStorage when there is logError called.', () => { - const bid = utils.deepClone(DEFAULT_MULTIFORMAT_VIDEO_VALID_BID[0]); - const ortb2 = { - site: { - ext: { - data: { - pageType: Array(5700).join('#') - } - } - } - }; - - expect(spec.isBidRequestValid(bid)).to.be.true; - spec.buildRequests([bid], { ortb2 }); - expect(JSON.parse(localStorageValues[key])).to.deep.equal({ [TODAY]: { [ERROR_CODES.PB_FPD_EXCEEDS_MAX_SIZE]: 2 } }); - }); - it('should log ERROR_CODES.VIDEO_DURATION_INVALID in LocalStorage when there is logError called.', () => { const bid = utils.deepClone(DEFAULT_MULTIFORMAT_VIDEO_VALID_BID[0]); bid.params.video.minduration = 1; diff --git a/test/spec/modules/jixieBidAdapter_spec.js b/test/spec/modules/jixieBidAdapter_spec.js index 0c999f1cd51..5bf2a3b6fc9 100644 --- a/test/spec/modules/jixieBidAdapter_spec.js +++ b/test/spec/modules/jixieBidAdapter_spec.js @@ -71,6 +71,7 @@ describe('jixie Adapter', function () { const clientIdTest1_ = '1aba6a40-f711-11e9-868c-53a2ae972xxx'; const sessionIdTest1_ = '1594782644-1aba6a40-f711-11e9-868c-53a2ae972xxx'; + const jxtokoTest1_ = 'eyJJRCI6ImFiYyJ9'; // to serve as the object that prebid will call jixie buildRequest with: (param2) const bidderRequest_ = { @@ -198,6 +199,9 @@ describe('jixie Adapter', function () { // get the interceptors ready: let getCookieStub = sinon.stub(storage, 'getCookie'); let getLocalStorageStub = sinon.stub(storage, 'getDataFromLocalStorage'); + getCookieStub + .withArgs('_jxtoko') + .returns(jxtokoTest1_); getCookieStub .withArgs('_jxx') .returns(clientIdTest1_); @@ -227,6 +231,7 @@ describe('jixie Adapter', function () { expect(payload).to.have.property('client_id_ls', clientIdTest1_); expect(payload).to.have.property('session_id_c', sessionIdTest1_); expect(payload).to.have.property('session_id_ls', sessionIdTest1_); + expect(payload).to.have.property('jxtoko_id', jxtokoTest1_); expect(payload).to.have.property('device', device_); expect(payload).to.have.property('domain', domain_); expect(payload).to.have.property('pageurl', pageurl_); @@ -304,71 +309,53 @@ describe('jixie Adapter', function () { it('should populate eids when supported userIds are available', function () { const oneSpecialBidReq = Object.assign({}, bidRequests_[0], { - userId: { - tdid: '11111111-2222-3333-4444-555555555555', - uid2: { id: 'AbCdEfGhIjKlMnO9qdQBW7qtMw8f1WTUvtkHe6u+fqLfhbtsqrJ697Z6YoI3IB9klGUv1wvlFIbwH7ELDlqQBGtj8AC1v7fMJ/Q45E7W90dts7UQLTDMLNmtHBRDXVb0Fpas4Vh3yN1jGVQNhzXC/RpGIVtZE8dCxcjfa7VfcTNcvxxxxx==' }, - pubProvidedId: [{ - source: 'puburl1.com', - uids: [{ - id: 'pubid1', - atype: 1, - ext: { - stype: 'ppuid' - } - }] - }, { - source: 'puburl2.com', - uids: [{ - id: 'pubid2' - }] - }] - } - }); - const request = spec.buildRequests([oneSpecialBidReq], bidderRequest_); - const payload = JSON.parse(request.data); - expect(payload.eids).to.deep.include({ - 'source': 'adserver.org', - 'uids': [ + userIdAsEids: [ { - 'id': '11111111-2222-3333-4444-555555555555', - 'atype': 1, - 'ext': { - 'rtiPartner': 'TDID' - } - } - ] - }); - expect(payload.eids).to.deep.include({ - 'source': 'uidapi.com', - 'uids': [ + 'source': 'adserver.org', + 'uids': [ + { + 'id': '11111111-2222-3333-4444-555555555555', + 'atype': 1, + 'ext': { + 'rtiPartner': 'TDID' + } + } + ] + }, { - 'id': 'AbCdEfGhIjKlMnO9qdQBW7qtMw8f1WTUvtkHe6u+fqLfhbtsqrJ697Z6YoI3IB9klGUv1wvlFIbwH7ELDlqQBGtj8AC1v7fMJ/Q45E7W90dts7UQLTDMLNmtHBRDXVb0Fpas4Vh3yN1jGVQNhzXC/RpGIVtZE8dCxcjfa7VfcTNcvxxxxx==', - 'atype': 3 - } - ] - }); - - expect(payload.eids).to.deep.include({ - 'source': 'puburl1.com', - 'uids': [ + 'source': 'uidapi.com', + 'uids': [ + { + 'id': 'AbCdEfGhIjKlMnO9qdQBW7qtMw8f1WTUvtkHe6u+fqLfhbtsqrJ697Z6YoI3IB9klGUv1wvlFIbwH7ELDlqQBGtj8AC1v7fMJ/Q45E7W90dts7UQLTDMLNmtHBRDXVb0Fpas4Vh3yN1jGVQNhzXC/RpGIVtZE8dCxcjfa7VfcTNcvxxxxx==', + 'atype': 3 + } + ] + }, { - 'id': 'pubid1', - 'atype': 1, - 'ext': { - 'stype': 'ppuid' - } - } - ] - }); - - expect(payload.eids).to.deep.include({ - 'source': 'puburl2.com', - 'uids': [ + 'source': 'puburl1.com', + 'uids': [ + { + 'id': 'pubid1', + 'atype': 1, + 'ext': { + 'stype': 'ppuid' + } + } + ] + }, { - 'id': 'pubid2' - } - ] + 'source': 'puburl2.com', + 'uids': [ + { + 'id': 'pubid2' + } + ] + }, + ], }); + const request = spec.buildRequests([oneSpecialBidReq], bidderRequest_); + const payload = JSON.parse(request.data); + expect(payload.eids).to.eql(oneSpecialBidReq.userIdAsEids); }); });// describe diff --git a/test/spec/modules/kargoBidAdapter_spec.js b/test/spec/modules/kargoBidAdapter_spec.js index 565b83704fa..260ac6c2132 100644 --- a/test/spec/modules/kargoBidAdapter_spec.js +++ b/test/spec/modules/kargoBidAdapter_spec.js @@ -35,7 +35,7 @@ describe('kargo adapter tests', function () { }); describe('build request', function() { - var bids, undefinedCurrency, noAdServerCurrency, cookies = [], localStorageItems = [], sessionIds = [], requestCount = 0; + var bids, undefinedCurrency, noAdServerCurrency, nonUSDAdServerCurrency, cookies = [], localStorageItems = [], sessionIds = [], requestCount = 0; beforeEach(function () { $$PREBID_GLOBAL$$.bidderSettings = { @@ -45,6 +45,7 @@ describe('kargo adapter tests', function () { }; undefinedCurrency = false; noAdServerCurrency = false; + nonUSDAdServerCurrency = false; sandbox.stub(config, 'getConfig').callsFake(function(key) { if (key === 'currency') { if (undefinedCurrency) { @@ -53,6 +54,9 @@ describe('kargo adapter tests', function () { if (noAdServerCurrency) { return {}; } + if (nonUSDAdServerCurrency) { + return {adServerCurrency: 'EUR'}; + } return {adServerCurrency: 'USD'}; } if (key === 'debug') return true; @@ -63,13 +67,45 @@ describe('kargo adapter tests', function () { bids = [ { params: { - placementId: 'foo' + placementId: 'foo', + socialCanvas: { + segments: ['segment_1', 'segment_2', 'segment_3'], + url: 'https://socan.url' + } }, - bidId: 1, + auctionId: '1234098', + bidId: '1', + adUnitCode: '101', + transactionId: '10101', + sizes: [[320, 50], [300, 250], [300, 600]], + mediaTypes: { + banner: { + sizes: [[320, 50], [300, 50]] + } + }, + bidRequestsCount: 1, + bidderRequestsCount: 2, + bidderWinsCount: 3, userId: { - tdid: 'fake-tdid' + tdid: 'ed1562d5-e52b-406f-8e65-e5ab3ed5583c' + }, + userIdAsEids: [ + { + 'source': 'adserver.org', + 'uids': [ + { + 'id': 'ed1562d5-e52b-406f-8e65-e5ab3ed5583c', + 'atype': 1, + 'ext': { + 'rtiPartner': 'TDID' + } + } + ] + } + ], + floorData: { + floorMin: 1 }, - sizes: [[320, 50], [300, 250], [300, 600]], ortb2: { device: { sua: { @@ -91,25 +127,76 @@ describe('kargo adapter tests', function () { version: [ '99', '0', '0', '0' ] } ], - mobile: 0, - model: '' + mobile: 1, + model: 'model', + source: 1, } } + }, + ortb2Imp: { + ext: { + data: { + adServer: { + name: 'gam', + adSlot: '/22558409563,18834096/dfy_mobile_adhesion' + }, + pbAdSlot: '/22558409563,18834096/dfy_mobile_adhesion' + }, + gpid: '/22558409563,18834096/dfy_mobile_adhesion' + } } }, { params: { placementId: 'bar' }, - bidId: 2, - sizes: [[320, 50], [300, 250], [300, 600]] + bidId: '2', + adUnitCode: '202', + transactionId: '20202', + sizes: [[320, 50], [300, 250], [300, 600]], + mediaTypes: { + video: { + sizes: [[320, 50], [300, 50]] + } + }, + bidRequestsCount: 0, + bidderRequestsCount: 0, + bidderWinsCount: 0, + ortb2Imp: { + ext: { + data: { + adServer: { + name: 'gam', + adSlot: '/22558409563,18834096/dfy_mobile_adhesion' + }, + pbAdSlot: '/22558409563,18834096/dfy_mobile_adhesion' + } + } + } }, { params: { placementId: 'bar' }, - bidId: 3, - sizes: [[320, 50], [300, 250], [300, 600]] + bidId: '3', + adUnitCode: '303', + transactionId: '30303', + sizes: [[320, 50], [300, 250], [300, 600]], + mediaTypes: { + native: { + sizes: [[320, 50], [300, 50]] + } + }, + ortb2Imp: { + ext: { + data: { + adServer: { + name: 'gam', + adSlot: '/22558409563,18834096/dfy_mobile_adhesion' + } + } + } + } } ]; }); @@ -160,11 +247,19 @@ describe('kargo adapter tests', function () { function simulateNoCurrencyObject() { undefinedCurrency = true; noAdServerCurrency = false; + nonUSDAdServerCurrency = false; } function simulateNoAdServerCurrency() { undefinedCurrency = false; noAdServerCurrency = true; + nonUSDAdServerCurrency = false; + } + + function simulateNonUSDAdServerCurrency() { + undefinedCurrency = false; + noAdServerCurrency = false; + nonUSDAdServerCurrency = true; } function generateGDPR(applies, haveConsent) { @@ -182,6 +277,32 @@ describe('kargo adapter tests', function () { }; } + function generatePageView() { + return { + id: '112233', + timestamp: frozenNow.getTime(), + url: 'http://pageview.url' + } + } + + function generateRawCRB(rawCRB, rawCRBLocalStorage) { + if (rawCRB == null && rawCRBLocalStorage == null) { + return null + } + + let result = {} + + if (rawCRB != null) { + result.rawCRB = rawCRB + } + + if (rawCRBLocalStorage != null) { + result.rawCRBLocalStorage = rawCRBLocalStorage + } + + return result + } + function getKrgCrb() { return 'eyJzeW5jSWRzIjp7IjIiOiI4MmZhMjU1NS01OTY5LTQ2MTQtYjRjZS00ZGNmMTA4MGU5ZjkiLCIxNiI6IlZveElrOEFvSnowQUFFZENleUFBQUFDMiY1MDIiLCIyMyI6ImQyYTg1NWE1LTFiMWMtNDMwMC05NDBlLWE3MDhmYTFmMWJkZSIsIjI0IjoiVm94SWs4QW9KejBBQUVkQ2V5QUFBQUMyJjUwMiIsIjI1IjoiNWVlMjQxMzgtNWUwMy00YjlkLWE5NTMtMzhlODMzZjI4NDlmIiwiMl84MCI6ImQyYTg1NWE1LTFiMWMtNDMwMC05NDBlLWE3MDhmYTFmMWJkZSIsIjJfOTMiOiI1ZWUyNDEzOC01ZTAzLTRiOWQtYTk1My0zOGU4MzNmMjg0OWYifSwibGV4SWQiOiI1ZjEwODgzMS0zMDJkLTExZTctYmY2Yi00NTk1YWNkM2JmNmMiLCJjbGllbnRJZCI6IjI0MTBkOGYyLWMxMTEtNDgxMS04OGE1LTdiNWUxOTBlNDc1ZiIsIm9wdE91dCI6ZmFsc2UsImV4cGlyZVRpbWUiOjE0OTc0NDkzODI2NjgsImxhc3RTeW5jZWRBdCI6MTQ5NzM2Mjk3OTAxMn0='; } @@ -253,6 +374,12 @@ describe('kargo adapter tests', function () { setLocalStorageItem('krg_crb', getEmptyKrgCrb()); } + function initializePageView() { + setLocalStorageItem('pageViewId', 112233); + setLocalStorageItem('pageViewTimestamp', frozenNow.getTime()); + setLocalStorageItem('pageViewUrl', 'http://pageview.url'); + } + function initializeEmptyKrgCrbCookie() { setCookie('krg_crb', getEmptyKrgCrbOldStyle()); } @@ -261,30 +388,20 @@ describe('kargo adapter tests', function () { return spec._getSessionId(); } - function getExpectedKrakenParams(excludeUserIds, expectedRawCRB, expectedRawCRBCookie, expectedGDPR) { + function getExpectedKrakenParams(expectedCRB, expectedPage, excludeUserIds, expectedGDPR, currency) { var base = { + pbv: '$prebid.version$', + aid: '1234098', + requestCount: 0, + sid: getSessionId(), + url: 'https://www.prebid.org', timeout: 200, - requestCount: requestCount++, - currency: 'USD', - cpmGranularity: 1, - timestamp: frozenNow.getTime(), - cpmRange: { - floor: 0, - ceil: 20 - }, - bidIDs: { - 1: 'foo', - 2: 'bar', - 3: 'bar' - }, - bidSizes: { - 1: [[320, 50], [300, 250], [300, 600]], - 2: [[320, 50], [300, 250], [300, 600]], - 3: [[320, 50], [300, 250], [300, 600]] - }, + ts: frozenNow.getTime(), device: { - width: screen.width, - height: screen.height, + size: [ + screen.width, + screen.height + ], sua: { platform: { brand: 'macOS', @@ -304,14 +421,61 @@ describe('kargo adapter tests', function () { version: [ '99', '0', '0', '0' ] } ], - mobile: 0, - model: '', + mobile: 1, + model: 'model', + source: 1 }, }, - userIDs: { + imp: [ + { + code: '101', + id: '1', + pid: 'foo', + tid: '10101', + banner: { + sizes: [[320, 50], [300, 50]] + }, + bidRequestCount: 1, + bidderRequestCount: 2, + bidderWinCount: 3, + floor: 1, + fpd: { + gpid: '/22558409563,18834096/dfy_mobile_adhesion' + } + }, + { + code: '202', + id: '2', + pid: 'bar', + tid: '20202', + video: { + sizes: [[320, 50], [300, 50]] + }, + fpd: { + gpid: '/22558409563,18834096/dfy_mobile_adhesion' + } + }, + { + code: '303', + id: '3', + pid: 'bar', + tid: '30303', + native: { + sizes: [[320, 50], [300, 50]] + }, + fpd: { + gpid: '/22558409563,18834096/dfy_mobile_adhesion' + } + } + ], + socan: { + segments: ['segment_1', 'segment_2', 'segment_3'], + url: 'https://socan.url' + }, + user: { kargoID: '5f108831-302d-11e7-bf6b-4595acd3bf6c', clientID: '2410d8f2-c111-4811-88a5-7b5e190e475f', - tdID: 'fake-tdid', + tdID: 'ed1562d5-e52b-406f-8e65-e5ab3ed5583c', crbIDs: { 2: '82fa2555-5969-4614-b4ce-4dcf1080e9f9', 16: 'VoxIk8AoJz0AAEdCeyAAAAC2&502', @@ -322,85 +486,63 @@ describe('kargo adapter tests', function () { '2_93': '5ee24138-5e03-4b9d-a953-38e833f2849f' }, optOut: false, - usp: '1---' - }, - pageURL: 'https://www.prebid.org', - prebidRawBidRequests: [ - { - bidId: 1, - ortb2: { - device: { - sua: { - platform: { - brand: 'macOS', - version: [ '12', '6', '0' ] - }, - browsers: [ - { - brand: 'Chromium', - version: [ '106', '0', '5249', '119' ] - }, - { - brand: 'Google Chrome', - version: [ '106', '0', '5249', '119' ] - }, - { - brand: 'Not;A=Brand', - version: [ '99', '0', '0', '0' ] - } - ], - mobile: 0, - model: '' + usp: '1---', + sharedIDEids: [ + { + source: 'adserver.org', + uids: [ + { + id: 'ed1562d5-e52b-406f-8e65-e5ab3ed5583c', + atype: 1, + ext: { + rtiPartner: 'TDID' + } } - } - }, - params: { - placementId: 'foo' - }, - userId: { - tdid: 'fake-tdid' - }, - sizes: [[320, 50], [300, 250], [300, 600]], - }, - { - bidId: 2, - params: { - placementId: 'bar' - }, - sizes: [[320, 50], [300, 250], [300, 600]] - }, - { - bidId: 3, - params: { - placementId: 'bar' - }, - sizes: [[320, 50], [300, 250], [300, 600]] - } - ], - rawCRB: expectedRawCRBCookie, - rawCRBLocalStorage: expectedRawCRB + ] + } + ] + } }; + if (excludeUserIds) { + base.user.crbIDs = {}; + delete base.user.clientID; + delete base.user.kargoID; + delete base.user.optOut; + } + if (expectedGDPR) { - base.userIDs['gdpr'] = expectedGDPR; + base.user.gdpr = expectedGDPR; + } + + if (expectedPage) { + base.page = expectedPage; } - if (excludeUserIds === true) { - base.userIDs = { - crbIDs: {}, - usp: '1---' - }; - delete base.prebidRawBidRequests[0].userId.tdid; + if (currency) { + base.cur = currency; + } + + const reqCount = requestCount++; + if (reqCount > 0) { + base.requestCount = reqCount + } + + if (expectedCRB != null) { + if (expectedCRB.rawCRB != null) { + base.rawCRB = expectedCRB.rawCRB + } + if (expectedCRB.rawCRBLocalStorage != null) { + base.rawCRBLocalStorage = expectedCRB.rawCRBLocalStorage + } } return base; } - function testBuildRequests(excludeTdid, expected, gdpr) { + function testBuildRequests(expected, gdpr) { var clonedBids = JSON.parse(JSON.stringify(bids)); - if (excludeTdid) { - delete clonedBids[0].userId.tdid; - } + var payload = { timeout: 200, uspConsent: '1---', @@ -414,15 +556,13 @@ describe('kargo adapter tests', function () { } var request = spec.buildRequests(clonedBids, payload); - expected.sessionId = getSessionId(); - sessionIds.push(expected.sessionId); - var krakenParams = JSON.parse(decodeURIComponent(request.data.slice(5))); - expect(request.data.slice(0, 5)).to.equal('json='); - expect(request.url).to.equal('https://krk.kargo.com/api/v2/bid'); - expect(request.method).to.equal('GET'); - expect(request.currency).to.equal('USD'); + var krakenParams = request.data; + + expect(request.url).to.equal('https://krk2.kargo.com/api/v1/prebid'); + expect(request.method).to.equal('POST'); expect(request.timeout).to.equal(200); expect(krakenParams).to.deep.equal(expected); + // Make sure session ID stays the same across requests simulating multiple auctions on one page load for (let i in sessionIds) { if (i == 0) { @@ -435,76 +575,93 @@ describe('kargo adapter tests', function () { it('works when all params and localstorage and cookies are correctly set', function() { initializeKrgCrb(); - testBuildRequests(false, getExpectedKrakenParams(undefined, getKrgCrb(), getKrgCrbOldStyle())); + initializePageView(); + testBuildRequests(getExpectedKrakenParams(generateRawCRB(getKrgCrbOldStyle(), getKrgCrb()), generatePageView())); }); it('works when all params and cookies are correctly set but no localstorage', function() { initializeKrgCrb(true); - testBuildRequests(false, getExpectedKrakenParams(undefined, null, getKrgCrbOldStyle())); + testBuildRequests(getExpectedKrakenParams(generateRawCRB(getKrgCrbOldStyle()))); }); it('gracefully handles nothing being set', function() { - testBuildRequests(true, getExpectedKrakenParams(true, null, null)); + testBuildRequests(getExpectedKrakenParams(undefined, undefined, true)); }); it('gracefully handles browsers without localStorage', function() { simulateNoLocalStorage(); - testBuildRequests(true, getExpectedKrakenParams(true, null, null)); + testBuildRequests(getExpectedKrakenParams(undefined, undefined, true)); }); it('handles empty yet valid Kargo CRB', function() { initializeEmptyKrgCrb(); initializeEmptyKrgCrbCookie(); - testBuildRequests(true, getExpectedKrakenParams(true, getEmptyKrgCrb(), getEmptyKrgCrbOldStyle())); + initializePageView(); + testBuildRequests(getExpectedKrakenParams(generateRawCRB(getEmptyKrgCrbOldStyle(), getEmptyKrgCrb()), generatePageView(), true)); }); it('handles broken Kargo CRBs where base64 encoding is invalid', function() { initializeInvalidKrgCrbType1(); - testBuildRequests(true, getExpectedKrakenParams(true, getInvalidKrgCrbType1(), null)); + initializePageView(); + testBuildRequests(getExpectedKrakenParams(generateRawCRB(undefined, getInvalidKrgCrbType1()), generatePageView(), true)); }); it('handles broken Kargo CRBs where top level JSON is invalid on cookie', function() { initializeInvalidKrgCrbType1Cookie(); - testBuildRequests(true, getExpectedKrakenParams(true, null, getInvalidKrgCrbType1())); + initializePageView(); + testBuildRequests(getExpectedKrakenParams(generateRawCRB(getInvalidKrgCrbType1()), generatePageView(), true)); }); it('handles broken Kargo CRBs where decoded JSON is invalid', function() { initializeInvalidKrgCrbType2(); - testBuildRequests(true, getExpectedKrakenParams(true, getInvalidKrgCrbType2(), null)); + initializePageView(); + testBuildRequests(getExpectedKrakenParams(generateRawCRB(undefined, getInvalidKrgCrbType2()), generatePageView(), true)); }); it('handles broken Kargo CRBs where inner base 64 is invalid on cookie', function() { initializeInvalidKrgCrbType2Cookie(); - testBuildRequests(true, getExpectedKrakenParams(true, null, getInvalidKrgCrbType2OldStyle())); + initializePageView(); + testBuildRequests(getExpectedKrakenParams(generateRawCRB(getInvalidKrgCrbType2OldStyle()), generatePageView(), true)); }); it('handles broken Kargo CRBs where inner JSON is invalid on cookie', function() { initializeInvalidKrgCrbType3Cookie(); - testBuildRequests(true, getExpectedKrakenParams(true, null, getInvalidKrgCrbType3OldStyle())); + initializePageView(); + testBuildRequests(getExpectedKrakenParams(generateRawCRB(getInvalidKrgCrbType3OldStyle()), generatePageView(), true)); }); it('handles broken Kargo CRBs where inner JSON is falsey', function() { initializeInvalidKrgCrbType4Cookie(); - testBuildRequests(true, getExpectedKrakenParams(true, null, getInvalidKrgCrbType4OldStyle())); + initializePageView(); + testBuildRequests(getExpectedKrakenParams(generateRawCRB(getInvalidKrgCrbType4OldStyle()), generatePageView(), true)); }); it('handles a non-existant currency object on the config', function() { simulateNoCurrencyObject(); initializeKrgCrb(); - testBuildRequests(false, getExpectedKrakenParams(undefined, getKrgCrb(), getKrgCrbOldStyle())); + initializePageView(); + testBuildRequests(getExpectedKrakenParams(generateRawCRB(getKrgCrbOldStyle(), getKrgCrb()), generatePageView())); }); it('handles no ad server currency being set on the currency object in the config', function() { simulateNoAdServerCurrency(); initializeKrgCrb(); - testBuildRequests(false, getExpectedKrakenParams(undefined, getKrgCrb(), getKrgCrbOldStyle())); + initializePageView(); + testBuildRequests(getExpectedKrakenParams(generateRawCRB(getKrgCrbOldStyle(), getKrgCrb()), generatePageView())); + }); + + it('handles non-USD ad server currency being set on the currency object in the config', function() { + simulateNonUSDAdServerCurrency(); + initializeKrgCrb(); + initializePageView(); + testBuildRequests(getExpectedKrakenParams(generateRawCRB(getKrgCrbOldStyle(), getKrgCrb()), generatePageView(), undefined, undefined, 'EUR')); }); it('sends gdpr consent', function () { initializeKrgCrb(); - testBuildRequests(false, getExpectedKrakenParams(undefined, getKrgCrb(), getKrgCrbOldStyle(), generateGDPRExpect(true, true)), generateGDPR(true, true)); - testBuildRequests(false, getExpectedKrakenParams(undefined, getKrgCrb(), getKrgCrbOldStyle(), generateGDPRExpect(false, true)), generateGDPR(false, true)); - testBuildRequests(false, getExpectedKrakenParams(undefined, getKrgCrb(), getKrgCrbOldStyle(), generateGDPRExpect(false, false)), generateGDPR(false, false)); + testBuildRequests(getExpectedKrakenParams(generateRawCRB(getKrgCrbOldStyle(), getKrgCrb()), undefined, false, generateGDPRExpect(true, true)), generateGDPR(true, true)); + testBuildRequests(getExpectedKrakenParams(generateRawCRB(getKrgCrbOldStyle(), getKrgCrb()), undefined, false, generateGDPRExpect(false, true)), generateGDPR(false, true)); + testBuildRequests(getExpectedKrakenParams(generateRawCRB(getKrgCrbOldStyle(), getKrgCrb()), undefined, false, generateGDPRExpect(false, false)), generateGDPR(false, false)); }); }); @@ -556,6 +713,17 @@ describe('kargo adapter tests', function () { mediaType: 'video', metadata: {}, currency: 'EUR' + }, + 6: { + id: 'bar', + cpm: 2.5, + adm: '', + admUrl: 'https://foobar.com/vast_adm', + width: 300, + height: 250, + mediaType: 'video', + metadata: {}, + currency: 'EUR' } }}, { currency: 'USD', @@ -584,14 +752,19 @@ describe('kargo adapter tests', function () { params: { placementId: 'bar' } + }, { + bidId: 6, + params: { + placementId: 'bar' + } }] }); var expectation = [{ + ad: '
', requestId: '1', cpm: 3, width: 320, height: 50, - ad: '
', ttl: 300, creativeId: 'foo', dealId: undefined, @@ -603,10 +776,10 @@ describe('kargo adapter tests', function () { } }, { requestId: '2', + ad: '
', cpm: 2.5, width: 300, height: 250, - ad: '
', ttl: 300, creativeId: 'bar', dealId: 'dmpmptest1234', @@ -620,10 +793,10 @@ describe('kargo adapter tests', function () { } }, { requestId: '3', + ad: '
', cpm: 2.5, width: 300, height: 250, - ad: '
', ttl: 300, creativeId: 'bar', dealId: undefined, @@ -635,10 +808,10 @@ describe('kargo adapter tests', function () { } }, { requestId: '4', + ad: '
', cpm: 2.5, width: 300, height: 250, - ad: '
', ttl: 300, creativeId: 'bar', dealId: undefined, @@ -653,7 +826,6 @@ describe('kargo adapter tests', function () { cpm: 2.5, width: 300, height: 250, - ad: '', vastXml: '', ttl: 300, creativeId: 'bar', @@ -664,6 +836,21 @@ describe('kargo adapter tests', function () { meta: { mediaType: 'video' } + }, { + requestId: '6', + cpm: 2.5, + width: 300, + height: 250, + vastUrl: 'https://foobar.com/vast_adm', + ttl: 300, + creativeId: 'bar', + dealId: undefined, + netRevenue: true, + currency: 'EUR', + mediaType: 'video', + meta: { + mediaType: 'video' + } }]; expect(resp).to.deep.equal(expectation); }); @@ -798,7 +985,7 @@ describe('kargo adapter tests', function () { expect(triggerPixelStub.getCall(0)).to.be.null; spec.onTimeout([{ auctionId: '1234', timeout: 2000 }]); expect(triggerPixelStub.getCall(0)).to.not.be.null; - expect(triggerPixelStub.getCall(0).args[0]).to.exist.and.to.include('https://krk.kargo.com/api/v1/event/timeout'); + expect(triggerPixelStub.getCall(0).args[0]).to.exist.and.to.include('https://krk2.kargo.com/api/v1/event/timeout'); expect(triggerPixelStub.getCall(0).args[0]).to.include('aid=1234'); expect(triggerPixelStub.getCall(0).args[0]).to.include('ato=2000'); }); diff --git a/test/spec/modules/koblerBidAdapter_spec.js b/test/spec/modules/koblerBidAdapter_spec.js index 5bcf62ff95d..5fb0bef726e 100644 --- a/test/spec/modules/koblerBidAdapter_spec.js +++ b/test/spec/modules/koblerBidAdapter_spec.js @@ -366,26 +366,6 @@ describe('KoblerAdapter', function () { expect(openRtbRequest.imp[1].pmp.deals[1].id).to.be.equal(dealIds2[1]); }); - it('should read timeout from config', function () { - const timeout = 4000; - const validBidRequests = [createValidBidRequest()]; - // No timeout field - const bidderRequest = { - auctionId: 'c1243d83-0bed-4fdb-8c76-42b456be17d0', - refererInfo: { - page: 'example.com' - } - }; - config.setConfig({ - bidderTimeout: timeout - }); - - const result = spec.buildRequests(validBidRequests, bidderRequest); - const openRtbRequest = JSON.parse(result.data); - - expect(openRtbRequest.tmax).to.be.equal(timeout); - }); - it('should read floor price using floors module', function () { const floorPriceFor580x400 = 6.5148; const floorPriceForAnySize = 4.2343; diff --git a/test/spec/modules/kueezRtbBidAdapter_spec.js b/test/spec/modules/kueezRtbBidAdapter_spec.js index f9b2cd41a43..2f48fd6df8c 100644 --- a/test/spec/modules/kueezRtbBidAdapter_spec.js +++ b/test/spec/modules/kueezRtbBidAdapter_spec.js @@ -15,6 +15,8 @@ import { import * as utils from 'src/utils.js'; import {version} from 'package.json'; import {useFakeTimers} from 'sinon'; +import {BANNER, VIDEO} from '../../../src/mediaTypes'; +import {config} from '../../../src/config'; const SUB_DOMAIN = 'exchange'; @@ -35,19 +37,86 @@ const BID = { 'transactionId': 'c881914b-a3b5-4ecf-ad9c-1c2f37c6aabf', 'sizes': [[300, 250], [300, 600]], 'bidderRequestId': '1fdb5ff1b6eaa7', + 'auctionId': 'auction_id', + 'bidRequestsCount': 4, + 'bidderRequestsCount': 3, + 'bidderWinsCount': 1, 'requestId': 'b0777d85-d061-450e-9bc7-260dd54bbb7a', - 'schain': 'a0819c69-005b-41ed-af06-1be1e0aefefc' + 'schain': 'a0819c69-005b-41ed-af06-1be1e0aefefc', + 'mediaTypes': [BANNER] }; +const VIDEO_BID = { + 'bidId': '2d52001cabd527', + 'adUnitCode': '63550ad1ff6642d368cba59dh5884270560', + 'bidderRequestId': '12a8ae9ada9c13', + 'transactionId': '56e184c6-bde9-497b-b9b9-cf47a61381ee', + 'auctionId': 'auction_id', + 'bidRequestsCount': 4, + 'bidderRequestsCount': 3, + 'bidderWinsCount': 1, + 'schain': 'a0819c69-005b-41ed-af06-1be1e0aefefc', + 'params': { + 'subDomain': SUB_DOMAIN, + 'cId': '635509f7ff6642d368cb9837', + 'pId': '59ac17c192832d0011283fe3', + 'bidFloor': 0.1 + }, + 'sizes': [[545, 307]], + 'mediaTypes': { + 'video': { + 'playerSize': [[545, 307]], + 'context': 'instream', + 'mimes': [ + 'video/mp4', + 'application/javascript' + ], + 'protocols': [2, 3, 5, 6], + 'maxduration': 60, + 'minduration': 0, + 'startdelay': 0, + 'linearity': 1, + 'api': [2], + 'placement': 1 + } + } +} + const BIDDER_REQUEST = { 'gdprConsent': { 'consentString': 'consent_string', 'gdprApplies': true }, + 'gppString': 'gpp_string', + 'gppSid': [7], 'uspConsent': 'consent_string', 'refererInfo': { 'page': 'https://www.greatsite.com', 'ref': 'https://www.somereferrer.com' + }, + 'ortb2': { + 'regs': { + 'gpp': 'gpp_string', + 'gpp_sid': [7] + }, + 'device': { + 'sua': { + 'source': 2, + 'platform': { + 'brand': 'Android', + 'version': ['8', '0', '0'] + }, + 'browsers': [ + {'brand': 'Not_A Brand', 'version': ['99', '0', '0', '0']}, + {'brand': 'Google Chrome', 'version': ['109', '0', '5414', '119']}, + {'brand': 'Chromium', 'version': ['109', '0', '5414', '119']} + ], + 'mobile': 1, + 'model': 'SM-G955U', + 'bitness': '64', + 'architecture': '' + } + } } }; @@ -73,6 +142,23 @@ const SERVER_RESPONSE = { } }; +const VIDEO_SERVER_RESPONSE = { + body: { + 'cid': '635509f7ff6642d368cb9837', + 'results': [{ + 'ad': '', + 'advertiserDomains': ['kueezrtb.com'], + 'exp': 60, + 'width': 545, + 'height': 307, + 'mediaType': 'video', + 'creativeId': '12610997325162499419', + 'price': 2, + 'cookies': [] + }] + } +}; + const REQUEST = { data: { width: 300, @@ -111,6 +197,11 @@ describe('KueezRtbBidAdapter', function () { it('exists and is a string', function () { expect(adapter.code).to.exist.and.to.be.a('string'); }); + + it('exists and contains media types', function () { + expect(adapter.supportedMediaTypes).to.exist.and.to.be.an('array').with.length(2); + expect(adapter.supportedMediaTypes).to.contain.members([BANNER, VIDEO]); + }); }); describe('validate bid requests', function () { @@ -155,8 +246,85 @@ describe('KueezRtbBidAdapter', function () { sandbox.stub(Date, 'now').returns(1000); }); - it('should build request for each size', function () { + it('should build video request', function () { const hashUrl = hashCode(BIDDER_REQUEST.refererInfo.page); + config.setConfig({ + bidderTimeout: 3000 + }); + const requests = adapter.buildRequests([VIDEO_BID], BIDDER_REQUEST); + expect(requests).to.have.length(1); + expect(requests[0]).to.deep.equal({ + method: 'POST', + url: `${createDomain(SUB_DOMAIN)}/prebid/multi/635509f7ff6642d368cb9837`, + data: { + adUnitCode: '63550ad1ff6642d368cba59dh5884270560', + bidFloor: 0.1, + bidId: '2d52001cabd527', + bidderVersion: adapter.version, + bidderRequestId: '12a8ae9ada9c13', + cb: 1000, + gdpr: 1, + gdprConsent: 'consent_string', + usPrivacy: 'consent_string', + gppString: 'gpp_string', + gppSid: [7], + prebidVersion: version, + transactionId: '56e184c6-bde9-497b-b9b9-cf47a61381ee', + auctionId: 'auction_id', + bidRequestsCount: 4, + bidderRequestsCount: 3, + bidderWinsCount: 1, + bidderTimeout: 3000, + publisherId: '59ac17c192832d0011283fe3', + url: 'https%3A%2F%2Fwww.greatsite.com', + referrer: 'https://www.somereferrer.com', + res: `${window.top.screen.width}x${window.top.screen.height}`, + schain: VIDEO_BID.schain, + sizes: ['545x307'], + sua: { + 'source': 2, + 'platform': { + 'brand': 'Android', + 'version': ['8', '0', '0'] + }, + 'browsers': [ + {'brand': 'Not_A Brand', 'version': ['99', '0', '0', '0']}, + {'brand': 'Google Chrome', 'version': ['109', '0', '5414', '119']}, + {'brand': 'Chromium', 'version': ['109', '0', '5414', '119']} + ], + 'mobile': 1, + 'model': 'SM-G955U', + 'bitness': '64', + 'architecture': '' + }, + uniqueDealId: `${hashUrl}_${Date.now().toString()}`, + uqs: getTopWindowQueryParams(), + mediaTypes: { + video: { + api: [2], + context: 'instream', + linearity: 1, + maxduration: 60, + mimes: [ + 'video/mp4', + 'application/javascript' + ], + minduration: 0, + placement: 1, + playerSize: [[545, 307]], + protocols: [2, 3, 5, 6], + startdelay: 0 + } + } + } + }); + }); + + it('should build banner request for each size', function () { + const hashUrl = hashCode(BIDDER_REQUEST.refererInfo.page); + config.setConfig({ + bidderTimeout: 3000 + }); const requests = adapter.buildRequests([BID], BIDDER_REQUEST); expect(requests).to.have.length(1); expect(requests[0]).to.deep.equal({ @@ -165,8 +333,33 @@ describe('KueezRtbBidAdapter', function () { data: { gdprConsent: 'consent_string', gdpr: 1, + gppString: 'gpp_string', + gppSid: [7], usPrivacy: 'consent_string', + transactionId: 'c881914b-a3b5-4ecf-ad9c-1c2f37c6aabf', + auctionId: 'auction_id', + bidRequestsCount: 4, + bidderRequestsCount: 3, + bidderWinsCount: 1, + bidderTimeout: 3000, + bidderRequestId: '1fdb5ff1b6eaa7', sizes: ['300x250', '300x600'], + sua: { + 'source': 2, + 'platform': { + 'brand': 'Android', + 'version': ['8', '0', '0'] + }, + 'browsers': [ + {'brand': 'Not_A Brand', 'version': ['99', '0', '0', '0']}, + {'brand': 'Google Chrome', 'version': ['109', '0', '5414', '119']}, + {'brand': 'Chromium', 'version': ['109', '0', '5414', '119']} + ], + 'mobile': 1, + 'model': 'SM-G955U', + 'bitness': '64', + 'architecture': '' + }, url: 'https%3A%2F%2Fwww.greatsite.com', referrer: 'https://www.somereferrer.com', cb: 1000, @@ -179,6 +372,7 @@ describe('KueezRtbBidAdapter', function () { prebidVersion: version, schain: BID.schain, res: `${window.top.screen.width}x${window.top.screen.height}`, + mediaTypes: [BANNER], uqs: getTopWindowQueryParams(), 'ext.param1': 'loremipsum', 'ext.param2': 'dolorsitamet', @@ -235,7 +429,7 @@ describe('KueezRtbBidAdapter', function () { expect(responses).to.be.empty; }); - it('should return an array of interpreted responses', function () { + it('should return an array of interpreted banner responses', function () { const responses = adapter.interpretResponse(SERVER_RESPONSE, REQUEST); expect(responses).to.have.length(1); expect(responses[0]).to.deep.equal({ @@ -254,6 +448,26 @@ describe('KueezRtbBidAdapter', function () { }); }); + it('should return an array of interpreted video responses', function () { + const responses = adapter.interpretResponse(VIDEO_SERVER_RESPONSE, REQUEST); + expect(responses).to.have.length(1); + expect(responses[0]).to.deep.equal({ + requestId: '2d52001cabd527', + cpm: 2, + width: 545, + height: 307, + mediaType: 'video', + creativeId: '12610997325162499419', + currency: 'USD', + netRevenue: true, + ttl: 60, + vastXml: '', + meta: { + advertiserDomains: ['kueezrtb.com'] + } + }); + }); + it('should take default TTL', function () { const serverResponse = utils.deepClone(SERVER_RESPONSE); delete serverResponse.body.results[0].exp; diff --git a/test/spec/modules/kulturemediaBidAdapter_spec.js b/test/spec/modules/kulturemediaBidAdapter_spec.js new file mode 100644 index 00000000000..1872f6c171a --- /dev/null +++ b/test/spec/modules/kulturemediaBidAdapter_spec.js @@ -0,0 +1,613 @@ +import {expect} from 'chai'; +import {spec} from 'modules/kulturemediaBidAdapter.js'; + +const BANNER_REQUEST = { + 'bidderCode': 'kulturemedia', + 'auctionId': 'auctionId-56a2-4f71-9098-720a68f2f708', + 'bidderRequestId': 'requestId', + 'bidRequest': [{ + 'bidder': 'kulturemedia', + 'params': { + 'placementId': 123456, + }, + 'placementCode': 'div-gpt-dummy-placement-code', + 'mediaTypes': {'banner': {'sizes': [[300, 250]]}}, + 'bidId': 'bidId1', + 'bidderRequestId': 'bidderRequestId', + 'auctionId': 'auctionId-56a2-4f71-9098-720a68f2f708' + }, + { + 'bidder': 'kulturemedia', + 'params': { + 'placementId': 123456, + }, + 'placementCode': 'div-gpt-dummy-placement-code', + 'mediaTypes': {'banner': {'sizes': [[300, 250]]}}, + 'bidId': 'bidId2', + 'bidderRequestId': 'bidderRequestId', + 'auctionId': 'auctionId-56a2-4f71-9098-720a68f2f708' + }], + 'start': 1487883186070, + 'auctionStart': 1487883186069, + 'timeout': 3000 +}; + +const RESPONSE = { + 'headers': null, + 'body': { + 'id': 'responseId', + 'seatbid': [ + { + 'bid': [ + { + 'id': 'bidId1', + 'impid': 'bidId1', + 'price': 0.18, + 'adm': '', + 'adid': '144762342', + 'adomain': [ + 'https://dummydomain.com' + ], + 'iurl': 'iurl', + 'cid': '109', + 'crid': 'creativeId', + 'cat': [], + 'w': 300, + 'h': 250, + 'ext': { + 'prebid': { + 'type': 'banner' + }, + 'bidder': { + 'appnexus': { + 'brand_id': 334553, + 'auction_id': 514667951122925701, + 'bidder_id': 2, + 'bid_ad_type': 0 + } + } + } + }, + { + 'id': 'bidId2', + 'impid': 'bidId2', + 'price': 0.1, + 'adm': '', + 'adid': '144762342', + 'adomain': [ + 'https://dummydomain.com' + ], + 'iurl': 'iurl', + 'cid': '109', + 'crid': 'creativeId', + 'cat': [], + 'w': 300, + 'h': 250, + 'ext': { + 'prebid': { + 'type': 'banner' + }, + 'bidder': { + 'appnexus': { + 'brand_id': 386046, + 'auction_id': 517067951122925501, + 'bidder_id': 2, + 'bid_ad_type': 0 + } + } + } + } + ], + 'seat': 'kulturemedia' + } + ], + 'ext': { + 'usersync': { + 'sovrn': { + 'status': 'none', + 'syncs': [ + { + 'url': 'urlsovrn', + 'type': 'iframe' + } + ] + }, + 'appnexus': { + 'status': 'none', + 'syncs': [ + { + 'url': 'urlappnexus', + 'type': 'pixel' + } + ] + } + }, + 'responsetimemillis': { + 'appnexus': 127 + } + } + } +}; + +const DEFAULT_NETWORK_ID = 1; + +describe('kulturemediaBidAdapter:', function () { + let videoBidRequest; + + const VIDEO_REQUEST = { + 'bidderCode': 'kulturemedia', + 'auctionId': 'e158486f-8c7f-472f-94ce-b0cbfbb50ab4', + 'bidderRequestId': '34feaad34lkj2', + 'bids': videoBidRequest, + 'auctionStart': 1520001292880, + 'timeout': 3000, + 'start': 1520001292884, + 'doneCbCallCount': 0, + 'refererInfo': { + 'numIframes': 1, + 'reachedTop': true, + 'referer': 'test.com' + } + }; + + beforeEach(function () { + videoBidRequest = { + mediaTypes: { + video: { + context: 'instream', + playerSize: [[640, 480]], + } + }, + bidder: 'kulturemedia', + sizes: [640, 480], + bidId: '30b3efwfwe1e', + adUnitCode: 'video1', + params: { + video: { + playerWidth: 640, + playerHeight: 480, + mimes: ['video/mp4', 'application/javascript'], + protocols: [2, 5], + api: [2], + position: 1, + delivery: [2], + sid: 134, + rewarded: 1, + placement: 1, + hp: 1, + inventoryid: 123 + }, + site: { + id: 1, + page: 'https://test.com', + referrer: 'http://test.com' + }, + publisherId: 'km123' + } + }; + }); + + describe('isBidRequestValid', function () { + context('basic validation', function () { + beforeEach(function () { + // Basic Valid BidRequest + this.bid = { + bidder: 'kulturemedia', + mediaTypes: { + banner: { + sizes: [[250, 300]] + } + }, + params: { + placementId: 'placementId', + publisherId: 'publisherId', + } + }; + }); + + it('should accept request if placementId and publisherId are passed', function () { + expect(spec.isBidRequestValid(this.bid)).to.be.true; + }); + + it('reject requests without params', function () { + this.bid.params = {}; + expect(spec.isBidRequestValid(this.bid)).to.be.false; + }); + + it('returns false when banner mediaType does not exist', function () { + this.bid.mediaTypes = {} + expect(spec.isBidRequestValid(this.bid)).to.be.false; + }); + }); + + context('banner validation', function () { + it('returns true when banner sizes are defined', function () { + const bid = { + bidder: 'kulturemedia', + mediaTypes: { + banner: { + sizes: [[250, 300]] + } + }, + params: { + placementId: 'placementId', + publisherId: 'publisherId', + } + }; + + expect(spec.isBidRequestValid(bid)).to.be.true; + }); + + it('returns false when banner sizes are invalid', function () { + const invalidSizes = [ + undefined, + '2:1', + 123, + 'test' + ]; + + invalidSizes.forEach((sizes) => { + const bid = { + bidder: 'kulturemedia', + mediaTypes: { + banner: { + sizes + } + }, + params: { + placementId: 'placementId', + publisherId: 'publisherId', + } + }; + + expect(spec.isBidRequestValid(bid)).to.be.false; + }); + }); + }); + + context('video validation', function () { + beforeEach(function () { + // Basic Valid BidRequest + this.bid = { + bidder: 'kulturemedia', + mediaTypes: { + video: { + playerSize: [[300, 50]], + context: 'instream', + mimes: ['foo', 'bar'], + protocols: [1, 2] + } + }, + params: { + placementId: 'placementId', + publisherId: 'publisherId', + } + }; + }); + + it('should return true (skip validations) when e2etest = true', function () { + this.bid.params = { + e2etest: true + }; + expect(spec.isBidRequestValid(this.bid)).to.equal(true); + }); + + it('returns false when video context is not defined', function () { + delete this.bid.mediaTypes.video.context; + + expect(spec.isBidRequestValid(this.bid)).to.be.false; + }); + + it('returns false when video playserSize is invalid', function () { + const invalidSizes = [ + undefined, + '2:1', + 123, + 'test' + ]; + + invalidSizes.forEach((playerSize) => { + this.bid.mediaTypes.video.playerSize = playerSize; + expect(spec.isBidRequestValid(this.bid)).to.be.false; + }); + }); + + it('returns false when video mimes is invalid', function () { + const invalidMimes = [ + undefined, + 'test', + 1, + [] + ] + + invalidMimes.forEach((mimes) => { + this.bid.mediaTypes.video.mimes = mimes; + expect(spec.isBidRequestValid(this.bid)).to.be.false; + }) + }); + + it('returns false when video protocols is invalid', function () { + const invalidMimes = [ + undefined, + 'test', + 1, + [] + ] + + invalidMimes.forEach((protocols) => { + this.bid.mediaTypes.video.protocols = protocols; + expect(spec.isBidRequestValid(this.bid)).to.be.false; + }) + }); + }); + }); + + describe('buildRequests', function () { + context('when mediaType is banner', function () { + it('creates request data', function () { + let request = spec.buildRequests(BANNER_REQUEST.bidRequest, BANNER_REQUEST); + + expect(request).to.exist.and.to.be.a('object'); + const payload = JSON.parse(request.data); + expect(payload.imp[0]).to.have.property('id', BANNER_REQUEST.bidRequest[0].bidId); + expect(payload.imp[1]).to.have.property('id', BANNER_REQUEST.bidRequest[1].bidId); + }); + + it('has gdpr data if applicable', function () { + const req = Object.assign({}, BANNER_REQUEST, { + gdprConsent: { + consentString: 'consentString', + gdprApplies: true, + } + }); + let request = spec.buildRequests(BANNER_REQUEST.bidRequest, req); + + const payload = JSON.parse(request.data); + expect(payload.user.ext).to.have.property('consent', req.gdprConsent.consentString); + expect(payload.regs.ext).to.have.property('gdpr', 1); + }); + + it('should properly forward eids parameters', function () { + const req = Object.assign({}, BANNER_REQUEST); + req.bidRequest[0].userIdAsEids = [ + { + source: 'dummy.com', + uids: [ + { + id: 'd6d0a86c-20c6-4410-a47b-5cba383a698a', + atype: 1 + } + ] + }]; + let request = spec.buildRequests(req.bidRequest, req); + + const payload = JSON.parse(request.data); + expect(payload.user.ext.eids[0].source).to.equal('dummy.com'); + expect(payload.user.ext.eids[0].uids[0].id).to.equal('d6d0a86c-20c6-4410-a47b-5cba383a698a'); + expect(payload.user.ext.eids[0].uids[0].atype).to.equal(1); + }); + }); + + context('when mediaType is video', function () { + it('should create a POST request for every bid', function () { + const requests = spec.buildRequests([videoBidRequest], VIDEO_REQUEST); + expect(requests.method).to.equal('POST'); + expect(requests.url.trim()).to.equal(spec.ENDPOINT + '?pid=' + videoBidRequest.params.publisherId + '&nId=' + DEFAULT_NETWORK_ID); + }); + + it('should attach request data', function () { + const requests = spec.buildRequests([videoBidRequest], VIDEO_REQUEST); + const data = JSON.parse(requests.data); + const [width, height] = videoBidRequest.sizes; + const VERSION = '1.0.0'; + expect(data.imp[0].video.w).to.equal(width); + expect(data.imp[0].video.h).to.equal(height); + expect(data.imp[0].bidfloor).to.equal(videoBidRequest.params.bidfloor); + expect(data.ext.prebidver).to.equal('$prebid.version$'); + expect(data.ext.adapterver).to.equal(spec.VERSION); + }); + + it('should set pubId to e2etest when bid.params.e2etest = true', function () { + videoBidRequest.params.e2etest = true; + const requests = spec.buildRequests([videoBidRequest], VIDEO_REQUEST); + expect(requests.method).to.equal('POST'); + expect(requests.url).to.equal(spec.ENDPOINT + '?pid=e2etest&nId=' + DEFAULT_NETWORK_ID); + }); + + it('should attach End 2 End test data', function () { + videoBidRequest.params.e2etest = true; + const requests = spec.buildRequests([videoBidRequest], VIDEO_REQUEST); + const data = JSON.parse(requests.data); + expect(data.imp[0].bidfloor).to.not.exist; + expect(data.imp[0].video.w).to.equal(640); + expect(data.imp[0].video.h).to.equal(480); + }); + }); + }); + + describe('interpretResponse', function () { + context('when mediaType is banner', function () { + it('have bids', function () { + let bids = spec.interpretResponse(RESPONSE, BANNER_REQUEST); + expect(bids).to.be.an('array').that.is.not.empty; + validateBidOnIndex(0); + validateBidOnIndex(1); + + function validateBidOnIndex(index) { + expect(bids[index]).to.have.property('currency', 'USD'); + expect(bids[index]).to.have.property('requestId', RESPONSE.body.seatbid[0].bid[index].impid); + expect(bids[index]).to.have.property('cpm', RESPONSE.body.seatbid[0].bid[index].price); + expect(bids[index]).to.have.property('width', RESPONSE.body.seatbid[0].bid[index].w); + expect(bids[index]).to.have.property('height', RESPONSE.body.seatbid[0].bid[index].h); + expect(bids[index]).to.have.property('ad', RESPONSE.body.seatbid[0].bid[index].adm); + expect(bids[index]).to.have.property('creativeId', RESPONSE.body.seatbid[0].bid[index].crid); + expect(bids[index].meta).to.have.property('advertiserDomains', RESPONSE.body.seatbid[0].bid[index].adomain); + expect(bids[index]).to.have.property('ttl', 300); + expect(bids[index]).to.have.property('netRevenue', true); + } + }); + + it('handles empty response', function () { + const EMPTY_RESP = Object.assign({}, RESPONSE, {'body': {}}); + const bids = spec.interpretResponse(EMPTY_RESP, BANNER_REQUEST); + + expect(bids).to.be.empty; + }); + }); + + context('when mediaType is video', function () { + it('should return no bids if the response is not valid', function () { + const bidResponse = spec.interpretResponse({ + body: null + }, { + videoBidRequest + }); + expect(bidResponse.length).to.equal(0); + }); + + it('should return no bids if the response "nurl" and "adm" are missing', function () { + const serverResponse = { + seatbid: [{ + bid: [{ + price: 6.01 + }] + }] + }; + const bidResponse = spec.interpretResponse({ + body: serverResponse + }, { + videoBidRequest + }); + expect(bidResponse.length).to.equal(0); + }); + + it('should return no bids if the response "price" is missing', function () { + const serverResponse = { + seatbid: [{ + bid: [{ + adm: '' + }] + }] + }; + const bidResponse = spec.interpretResponse({ + body: serverResponse + }, { + videoBidRequest + }); + expect(bidResponse.length).to.equal(0); + }); + + it('should return a valid video bid response with just "adm"', function () { + const serverResponse = { + id: '123', + seatbid: [{ + bid: [{ + id: 1, + adid: 123, + impid: 456, + crid: 2, + price: 6.01, + adm: '', + adomain: [ + 'kulturemedia.com' + ], + w: 640, + h: 480, + ext: { + prebid: { + type: 'video' + }, + } + }] + }], + cur: 'USD' + }; + const bidResponse = spec.interpretResponse({ + body: serverResponse + }, { + videoBidRequest + }); + let o = { + requestId: serverResponse.seatbid[0].bid[0].impid, + ad: '', + bidderCode: spec.code, + cpm: serverResponse.seatbid[0].bid[0].price, + creativeId: serverResponse.seatbid[0].bid[0].crid, + vastXml: serverResponse.seatbid[0].bid[0].adm, + width: 640, + height: 480, + mediaType: 'video', + currency: 'USD', + ttl: 300, + netRevenue: true, + meta: { + advertiserDomains: ['kulturemedia.com'] + } + }; + expect(bidResponse[0]).to.deep.equal(o); + }); + + it('should default ttl to 300', function () { + const serverResponse = { + seatbid: [{bid: [{id: 1, adid: 123, crid: 2, price: 6.01, adm: ''}]}], + cur: 'USD' + }; + const bidResponse = spec.interpretResponse({body: serverResponse}, {videoBidRequest}); + expect(bidResponse[0].ttl).to.equal(300); + }); + it('should not allow ttl above 3601, default to 300', function () { + videoBidRequest.params.video.ttl = 3601; + const serverResponse = { + seatbid: [{bid: [{id: 1, adid: 123, crid: 2, price: 6.01, adm: ''}]}], + cur: 'USD' + }; + const bidResponse = spec.interpretResponse({body: serverResponse}, {videoBidRequest}); + expect(bidResponse[0].ttl).to.equal(300); + }); + it('should not allow ttl below 1, default to 300', function () { + videoBidRequest.params.video.ttl = 0; + const serverResponse = { + seatbid: [{bid: [{id: 1, adid: 123, crid: 2, price: 6.01, adm: ''}]}], + cur: 'USD' + }; + const bidResponse = spec.interpretResponse({body: serverResponse}, {videoBidRequest}); + expect(bidResponse[0].ttl).to.equal(300); + }); + }); + }); + + describe('getUserSyncs', function () { + it('handles no parameters', function () { + let opts = spec.getUserSyncs({}); + expect(opts).to.be.an('array').that.is.empty; + }); + it('returns non if sync is not allowed', function () { + let opts = spec.getUserSyncs({iframeEnabled: false, pixelEnabled: false}); + + expect(opts).to.be.an('array').that.is.empty; + }); + + it('iframe sync enabled should return results', function () { + let opts = spec.getUserSyncs({iframeEnabled: true, pixelEnabled: false}, [RESPONSE]); + + expect(opts.length).to.equal(1); + expect(opts[0].type).to.equal('iframe'); + expect(opts[0].url).to.equal(RESPONSE.body.ext.usersync['sovrn'].syncs[0].url); + }); + + it('pixel sync enabled should return results', function () { + let opts = spec.getUserSyncs({iframeEnabled: false, pixelEnabled: true}, [RESPONSE]); + + expect(opts.length).to.equal(1); + expect(opts[0].type).to.equal('image'); + expect(opts[0].url).to.equal(RESPONSE.body.ext.usersync['appnexus'].syncs[0].url); + }); + + it('all sync enabled should return all results', function () { + let opts = spec.getUserSyncs({iframeEnabled: true, pixelEnabled: true}, [RESPONSE]); + + expect(opts.length).to.equal(2); + }); + }); +}) +; diff --git a/test/spec/modules/lemmaDigitalBidAdapter_spec.js b/test/spec/modules/lemmaDigitalBidAdapter_spec.js new file mode 100644 index 00000000000..d50728dce3c --- /dev/null +++ b/test/spec/modules/lemmaDigitalBidAdapter_spec.js @@ -0,0 +1,623 @@ +import { expect } from 'chai'; +import { spec } from 'modules/lemmaDigitalBidAdapter.js'; +import * as utils from 'src/utils.js'; +import { config } from 'src/config.js'; +const constants = require('src/constants.json'); + +describe('lemmaDigitalBidAdapter', function () { + let bidRequests; + let videoBidRequests; + let bidResponses; + let videoBidResponse; + let schainConfig; + beforeEach(function () { + schainConfig = { + 'complete': 0, + 'nodes': [ + { + 'asi': 'mobupps.com', + 'sid': 'c74d97b01eae257e44aa9d5bade97baf5149', + 'rid': '79c25703ad5935b0b23b66d210dad1f3', + 'hp': 1 + }, + { + 'asi': 'lemmatechnologies.com', + 'sid': '975', + 'rid': 'a455157a-a1fb-11ed-a0e4-d08e79f7ace0', + 'hp': 1 + } + ] + }; + bidRequests = [{ + bidder: 'lemmadigital', + bidId: '22bddb28db77d', + mediaTypes: { + banner: { + sizes: [ + [300, 250], + [300, 600] + ], + } + }, + params: { + pubId: 1001, + adunitId: 1, + currency: 'AUD', + bidFloor: 1.3, + geo: { + lat: '12.3', + lon: '23.7', + }, + banner: { + w: 300, + h: 250, + }, + tmax: 300, + bcat: ['IAB-26'] + }, + sizes: [ + [300, 250], + [300, 600] + ], + schain: schainConfig + }]; + videoBidRequests = [{ + code: 'video1', + mediaTypes: { + video: { + playerSize: [640, 480], + context: 'instream' + } + }, + bidder: 'lemmadigital', + bidId: '22bddb28db77d', + params: { + pubId: 1001, + adunitId: 1, + bidFloor: 1.3, + tmax: 300, + bcat: ['IAB-26'], + video: { + mimes: ['video/mp4', 'video/x-flv'], + skippable: true, + minduration: 5, + maxduration: 30 + } + }, + schain: schainConfig + }]; + bidResponses = { + 'body': { + 'id': '93D3BAD6-E2E2-49FB-9D89-920B1761C865', + 'seatbid': [{ + 'bid': [{ + 'id': '74858439-49D7-4169-BA5D-44A046315B2F', + 'impid': '22bddb28db77d', + 'price': 1.3, + 'adm': '

lemma"Connecting Advertisers and Publishers directly"

', + 'adomain': ['amazon.com'], + 'iurl': 'https://thetradedesk-t-general.s3.amazonaws.com/AdvertiserLogos/vgl908z.png', + 'cid': '22918', + 'crid': 'v55jutrh', + 'dealid': 'ASEA-MS-KLY-TTD-DESKTOP-ID-VID-6S-030420', + 'h': 250, + 'w': 300, + 'ext': {} + }] + }] + } + }; + videoBidResponse = { + 'body': { + 'id': '93D3BAD6-E2E2-49FB-9D89-920B1761C865', + 'seatbid': [{ + 'bid': [{ + 'id': '74858439-49D7-4169-BA5D-44A046315B2F', + 'impid': '22bddb28db77d', + 'price': 1.3, + 'adm': 'Acudeo CompatibleVAST 2.0 Instream Test 1VAST 2.0 Instream Test 1', + 'adomain': ['amazon.com'], + 'iurl': 'https://thetradedesk-t-general.s3.amazonaws.com/AdvertiserLogos/vgl908z.png', + 'cid': '22918', + 'crid': 'v55jutrh', + 'dealid': 'ASEA-MS-KLY-TTD-DESKTOP-ID-VID-6S-030420', + 'h': 250, + 'w': 300, + 'ext': {} + }] + }] + } + }; + }); + describe('implementation', function () { + describe('Bid validations', function () { + it('valid bid case', function () { + let validBid = { + bidder: 'lemmadigital', + params: { + pubId: 1001, + adunitId: 1 + } + }, + isValid = spec.isBidRequestValid(validBid); + expect(isValid).to.equal(true); + }); + it('invalid bid case', function () { + let isValid = spec.isBidRequestValid(); + expect(isValid).to.equal(false); + }); + it('invalid bid case: pubId not passed', function () { + let validBid = { + bidder: 'lemmadigital', + params: { + adunitId: 1 + } + }, + isValid = spec.isBidRequestValid(validBid); + expect(isValid).to.equal(false); + }); + it('invalid bid case: pubId is not number', function () { + let validBid = { + bidder: 'lemmadigital', + params: { + pubId: '301', + adunitId: 1 + } + }, + isValid = spec.isBidRequestValid(validBid); + expect(isValid).to.equal(false); + }); + it('invalid bid case: adunitId is not passed', function () { + let validBid = { + bidder: 'lemmadigital', + params: { + pubId: 1001 + } + }, + isValid = spec.isBidRequestValid(validBid); + expect(isValid).to.equal(false); + }); + it('invalid bid case: video bid request mimes is not passed', function () { + let validBid = { + bidder: 'lemmadigital', + params: { + pubId: 1001, + adunitId: 1, + video: { + skippable: true, + minduration: 5, + maxduration: 30 + } + } + }, + isValid = spec.isBidRequestValid(validBid); + expect(isValid).to.equal(false); + validBid.params.video.mimes = []; + isValid = spec.isBidRequestValid(validBid); + expect(isValid).to.equal(false); + }); + }); + describe('Request formation', function () { + it('bidRequest check empty', function () { + let bidRequests = []; + let request = spec.buildRequests(bidRequests); + expect(request).to.equal(undefined); + }); + it('buildRequests function should not modify original bidRequests object', function () { + let originalBidRequests = utils.deepClone(bidRequests); + let request = spec.buildRequests(bidRequests); + expect(bidRequests).to.deep.equal(originalBidRequests); + }); + it('bidRequest imp array check empty', function () { + let request = spec.buildRequests(bidRequests); + let data = JSON.parse(request.data); + data.imp = []; + expect(data.imp.length).to.equal(0); + }); + it('Endpoint checking', function () { + let request = spec.buildRequests(bidRequests); + expect(request.url).to.equal('https://bid.lemmadigital.com/lemma/servad?pid=1001&aid=1'); + expect(request.method).to.equal('POST'); + }); + it('Request params check', function () { + let request = spec.buildRequests(bidRequests); + let data = JSON.parse(request.data); + expect(data.site.domain).to.be.a('string'); // domain should be set + expect(data.site.publisher.id).to.equal(bidRequests[0].params.pubId.toString()); // publisher Id + expect(data.imp[0].tagid).to.equal('1'); // tagid + expect(data.imp[0].bidfloorcur).to.equal(bidRequests[0].params.currency); + expect(data.imp[0].bidfloor).to.equal(bidRequests[0].params.bidFloor); + expect(data.source.ext.schain).to.deep.equal(bidRequests[0].schain); + }); + + it('Set sizes from mediaTypes object', function () { + let newBannerRequest = utils.deepClone(bidRequests); + delete newBannerRequest[0].sizes; + let request = spec.buildRequests(newBannerRequest); + let data = JSON.parse(request.data); + expect(data.sizes).to.equal(undefined); + }); + it('Check request banner object present', function () { + let newBannerRequest = utils.deepClone(bidRequests); + let request = spec.buildRequests(newBannerRequest); + let data = JSON.parse(request.data); + expect(data.banner).to.deep.equal(undefined); + }); + it('Check device, source object not present', function () { + let newBannerRequest = utils.deepClone(bidRequests); + delete newBannerRequest[0].schain; + let request = spec.buildRequests(newBannerRequest); + let data = JSON.parse(request.data); + delete data.device; + delete data.source; + expect(data.source).to.equal(undefined); + expect(data.device).to.equal(undefined); + }); + it('Set content from config, set site.content', function () { + let sandbox = sinon.sandbox.create(); + const content = { + 'id': 'alpha-numeric-id' + }; + sandbox.stub(config, 'getConfig').callsFake((key) => { + var config = { + content: content + }; + return config[key]; + }); + let request = spec.buildRequests(bidRequests); + let data = JSON.parse(request.data); + expect(data.site.content).to.deep.equal(content); + sandbox.restore(); + }); + it('Set content from config, set app.content', function () { + let bidRequest = [{ + bidder: 'lemmadigital', + params: { + pubId: 1001, + adunitId: 1, + video: { + skippable: true, + minduration: 5, + maxduration: 30 + }, + app: { + id: 'e0977d04e6bafece57b4b6e93314f10a', + name: 'AMC', + bundle: 'com.roku.amc', + storeurl: 'https://channelstore.roku.com/details/12716/amc', + cat: [ + 'IAB-26' + ], + publisher: { + 'id': '975' + } + }, + } + }]; + let sandbox = sinon.sandbox.create(); + const content = { + 'id': 'alpha-numeric-id' + }; + sandbox.stub(config, 'getConfig').callsFake((key) => { + var config = { + content: content + }; + return config[key]; + }); + let request = spec.buildRequests(bidRequest); + let data = JSON.parse(request.data); + expect(data.app.content).to.deep.equal(content); + sandbox.restore(); + }); + it('Set tmax from requestBids method', function () { + let request = spec.buildRequests(bidRequests); + let data = JSON.parse(request.data); + expect(data.tmax).to.deep.equal(300); + }); + it('Request params check without mediaTypes object', function () { + let bidRequests = [{ + bidder: 'lemmadigital', + params: { + pubId: 1001, + adunitId: 1, + currency: 'AUD' + }, + sizes: [ + [300, 250], + [300, 600] + ] + }]; + let request = spec.buildRequests(bidRequests); + let data = JSON.parse(request.data); + expect(data.imp[0].banner.w).to.equal(300); // width + expect(data.imp[0].banner.h).to.equal(250); // height + expect(data.imp[0].banner.format).exist.and.to.be.an('array'); + expect(data.imp[0].banner.format[0]).exist.and.to.be.an('object'); + expect(data.imp[0].banner.format[0].w).to.equal(300); // width + expect(data.imp[0].banner.format[0].h).to.equal(600); // height + }); + it('Request params check: without tagId', function () { + delete bidRequests[0].params.adunitId; + let request = spec.buildRequests(bidRequests); + let data = JSON.parse(request.data); + expect(data.site.domain).to.be.a('string'); // domain should be set + expect(data.site.publisher.id).to.equal(bidRequests[0].params.pubId.toString()); // publisher Id + expect(data.imp[0].tagid).to.equal(undefined); // tagid + expect(data.imp[0].bidfloorcur).to.equal(bidRequests[0].params.currency); + expect(data.imp[0].bidfloor).to.equal(bidRequests[0].params.bidFloor); + }); + it('Request params multi size format object check', function () { + let bidRequests = [{ + bidder: 'lemmadigital', + mediaTypes: { + banner: { + sizes: [ + [300, 250], + [300, 600] + ], + } + }, + params: { + pubId: 1001, + adunitId: 1, + currency: 'AUD' + }, + sizes: [ + [300, 250], + [300, 600] + ] + }]; + /* case 1 - size passed in adslot */ + let request = spec.buildRequests(bidRequests); + let data = JSON.parse(request.data); + expect(data.imp[0].banner.w).to.equal(300); // width + expect(data.imp[0].banner.h).to.equal(250); // height + /* case 2 - size passed in adslot as well as in sizes array */ + bidRequests[0].sizes = [ + [300, 600], + [300, 250] + ]; + bidRequests[0].mediaTypes = { + banner: { + sizes: [ + [300, 600], + [300, 250] + ] + } + }; + request = spec.buildRequests(bidRequests); + data = JSON.parse(request.data); + expect(data.imp[0].banner.w).to.equal(300); // width + expect(data.imp[0].banner.h).to.equal(600); // height + /* case 3 - size passed in sizes but not in adslot */ + bidRequests[0].params.adunitId = 1; + bidRequests[0].sizes = [ + [300, 250], + [300, 600] + ]; + bidRequests[0].mediaTypes = { + banner: { + sizes: [ + [300, 250], + [300, 600] + ] + } + }; + request = spec.buildRequests(bidRequests); + data = JSON.parse(request.data); + expect(data.imp[0].banner.w).to.equal(300); // width + expect(data.imp[0].banner.h).to.equal(250); // height + expect(data.imp[0].banner.format).exist.and.to.be.an('array'); + expect(data.imp[0].banner.format[0]).exist.and.to.be.an('object'); + expect(data.imp[0].banner.format[0].w).to.equal(300); // width + expect(data.imp[0].banner.format[0].h).to.equal(250); // height + }); + it('Request params currency check', function () { + let bidRequest = [{ + bidder: 'lemmadigital', + mediaTypes: { + banner: { + sizes: [ + [300, 250], + [300, 600] + ], + } + }, + params: { + pubId: 1001, + adunitId: 1, + currency: 'AUD' + }, + sizes: [ + [300, 250], + [300, 600] + ] + }]; + /* case 1 - + currency specified in adunits + output: imp[0] use currency specified in bidRequests[0].params.currency + */ + let request = spec.buildRequests(bidRequest); + let data = JSON.parse(request.data); + expect(data.imp[0].bidfloorcur).to.equal(bidRequests[0].params.currency); + /* case 2 - + currency specified in adunit + output: imp[0] use default currency - USD + */ + delete bidRequest[0].params.currency; + request = spec.buildRequests(bidRequest); + data = JSON.parse(request.data); + expect(data.imp[0].bidfloorcur).to.equal('USD'); + }); + it('Request params check for video ad', function () { + let request = spec.buildRequests(videoBidRequests); + let data = JSON.parse(request.data); + expect(data.imp[0].video).to.exist; + expect(data.imp[0].tagid).to.equal('1'); + expect(data.imp[0]['video']['mimes']).to.exist.and.to.be.an('array'); + expect(data.imp[0]['video']['mimes'][0]).to.equal(videoBidRequests[0].params.video['mimes'][0]); + expect(data.imp[0]['video']['mimes'][1]).to.equal(videoBidRequests[0].params.video['mimes'][1]); + expect(data.imp[0]['video']['minduration']).to.equal(videoBidRequests[0].params.video['minduration']); + expect(data.imp[0]['video']['maxduration']).to.equal(videoBidRequests[0].params.video['maxduration']); + expect(data.imp[0]['video']['w']).to.equal(videoBidRequests[0].mediaTypes.video.playerSize[0]); + expect(data.imp[0]['video']['h']).to.equal(videoBidRequests[0].mediaTypes.video.playerSize[1]); + expect(data.source.ext.schain).to.deep.equal(videoBidRequests[0].schain); + }); + describe('setting imp.floor using floorModule', function () { + /* + Use the minimum value among floor from floorModule per mediaType + If params.bidFloor is set then take max(floor, min(floors from floorModule)) + set imp.bidfloor only if it is more than 0 + */ + + let newRequest; + let floorModuleTestData; + let getFloor = function (req) { + return floorModuleTestData[req.mediaType]; + }; + + beforeEach(() => { + floorModuleTestData = { + 'banner': { + 'currency': 'AUD', + 'floor': 1.50 + }, + 'video': { + 'currency': 'AUD', + 'floor': 2.00 + } + }; + newRequest = utils.deepClone(bidRequests); + newRequest[0].getFloor = getFloor; + }); + + it('bidfloor should be undefined if calculation is <= 0', function () { + floorModuleTestData.banner.floor = 0; // lowest of them all + newRequest[0].params.bidFloor = undefined; + let request = spec.buildRequests(newRequest); + let data = JSON.parse(request.data); + data = data.imp[0]; + expect(data.bidfloor).to.equal(undefined); + }); + + it('ignore floormodule o/p if floor is not number', function () { + floorModuleTestData.banner.floor = 'INR'; + newRequest[0].params.bidFloor = undefined; + let request = spec.buildRequests(newRequest); + let data = JSON.parse(request.data); + data = data.imp[0]; + expect(data.bidfloor).to.equal(undefined); // video will be lowest now + }); + + it('ignore floormodule o/p if currency is not matched', function () { + floorModuleTestData.banner.currency = 'INR'; + newRequest[0].params.bidFloor = undefined; + let request = spec.buildRequests(newRequest); + let data = JSON.parse(request.data); + data = data.imp[0]; + expect(data.bidfloor).to.equal(undefined); // video will be lowest now + }); + + it('bidFloor is not passed, use minimum from floorModule', function () { + newRequest[0].params.bidFloor = undefined; + let request = spec.buildRequests(newRequest); + let data = JSON.parse(request.data); + data = data.imp[0]; + expect(data.bidfloor).to.equal(1.5); + }); + + it('bidFloor is passed as 1, use min of floorModule as it is highest', function () { + newRequest[0].params.bidFloor = '1.0';// yes, we want it as a string + let request = spec.buildRequests(newRequest); + let data = JSON.parse(request.data); + data = data.imp[0]; + expect(data.bidfloor).to.equal(1.5); + }); + }); + describe('Response checking', function () { + it('should check for valid response values', function () { + let request = spec.buildRequests(bidRequests); + let response = spec.interpretResponse(bidResponses, request); + expect(response).to.be.an('array').with.length.above(0); + expect(response[0].requestId).to.equal(bidResponses.body.seatbid[0].bid[0].impid); + expect(response[0].cpm).to.equal((bidResponses.body.seatbid[0].bid[0].price).toFixed(2)); + expect(response[0].width).to.equal(bidResponses.body.seatbid[0].bid[0].w); + expect(response[0].height).to.equal(bidResponses.body.seatbid[0].bid[0].h); + if (bidResponses.body.seatbid[0].bid[0].crid) { + expect(response[0].creativeId).to.equal(bidResponses.body.seatbid[0].bid[0].crid); + } else { + expect(response[0].creativeId).to.equal(bidResponses.body.seatbid[0].bid[0].id); + } + expect(response[0].dealId).to.equal(bidResponses.body.seatbid[0].bid[0].dealid); + expect(response[0].currency).to.equal('USD'); + expect(response[0].netRevenue).to.equal(false); + expect(response[0].ttl).to.equal(300); + }); + it('should check for valid banner mediaType in request', function () { + let request = spec.buildRequests(bidRequests); + let response = spec.interpretResponse(bidResponses, request); + + expect(response[0].mediaType).to.equal('banner'); + }); + it('should check for valid video mediaType in request', function () { + let request = spec.buildRequests(videoBidRequests); + let response = spec.interpretResponse(videoBidResponse, request); + + expect(response[0].mediaType).to.equal('video'); + }); + }); + }); + describe('Video request params', function () { + let sandbox, utilsMock, newVideoRequest; + beforeEach(() => { + utilsMock = sinon.mock(utils); + sandbox = sinon.sandbox.create(); + sandbox.spy(utils, 'logWarn'); + newVideoRequest = utils.deepClone(videoBidRequests); + }); + + afterEach(() => { + utilsMock.restore(); + sandbox.restore(); + }); + + it('Video params from mediaTypes and params obj of bid are not present', function () { + delete newVideoRequest[0].mediaTypes.video; + delete newVideoRequest[0].params.video; + let request = spec.buildRequests(newVideoRequest); + expect(request).to.equal(undefined); + }); + + it('Should consider video params from mediaType object of bid', function () { + delete newVideoRequest[0].params.video; + + let request = spec.buildRequests(newVideoRequest); + let data = JSON.parse(request.data); + expect(data.imp[0].video).to.exist; + expect(data.imp[0]['video']['w']).to.equal(videoBidRequests[0].mediaTypes.video.playerSize[0]); + expect(data.imp[0]['video']['h']).to.equal(videoBidRequests[0].mediaTypes.video.playerSize[1]); + expect(data.imp[0]['video']['battr']).to.equal(undefined); + }); + }); + describe('getUserSyncs', function () { + const syncurl_iframe = 'https://sync.lemmadigital.com/js/usersync.html?pid=1001'; + let sandbox; + beforeEach(function () { + sandbox = sinon.sandbox.create(); + }); + afterEach(function () { + sandbox.restore(); + }); + + it('execute as per config', function () { + expect(spec.getUserSyncs({ iframeEnabled: true }, {}, undefined, undefined)).to.deep.equal([{ + type: 'iframe', url: syncurl_iframe + }]); + }); + + it('not execute as per config', function () { + expect(spec.getUserSyncs({ iframeEnabled: false }, {}, undefined, undefined)).to.deep.equal(undefined); + }); + }); + }); +}); diff --git a/test/spec/modules/limelightDigitalBidAdapter_spec.js b/test/spec/modules/limelightDigitalBidAdapter_spec.js index 4efb4f4e84d..5a92110abb4 100644 --- a/test/spec/modules/limelightDigitalBidAdapter_spec.js +++ b/test/spec/modules/limelightDigitalBidAdapter_spec.js @@ -10,7 +10,12 @@ describe('limelightDigitalAdapter', function () { host: 'exchange.ortb.net', adUnitId: 123, adUnitType: 'banner', - publisherId: 'perfectPublisher' + publisherId: 'perfectPublisher', + custom1: 'custom1', + custom2: 'custom2', + custom3: 'custom3', + custom4: 'custom4', + custom5: 'custom5' }, placementCode: 'placement_0', auctionId: '74f78609-a92d-4cf1-869f-1b244bbfb5d2', @@ -49,7 +54,12 @@ describe('limelightDigitalAdapter', function () { params: { host: 'ads.project-limelight.com', adUnitId: 456, - adUnitType: 'banner' + adUnitType: 'banner', + custom1: 'custom1', + custom2: 'custom2', + custom3: 'custom3', + custom4: 'custom4', + custom5: 'custom5' }, placementCode: 'placement_1', auctionId: '482f88de-29ab-45c8-981a-d25e39454a34', @@ -90,7 +100,12 @@ describe('limelightDigitalAdapter', function () { host: 'exchange.ortb.net', adUnitId: 789, adUnitType: 'video', - publisherId: 'secondPerfectPublisher' + publisherId: 'secondPerfectPublisher', + custom1: 'custom1', + custom2: 'custom2', + custom3: 'custom3', + custom4: 'custom4', + custom5: 'custom5' }, placementCode: 'placement_2', auctionId: 'e4771143-6aa7-41ec-8824-ced4342c96c8', @@ -128,7 +143,12 @@ describe('limelightDigitalAdapter', function () { params: { host: 'exchange.ortb.net', adUnitId: 789, - adUnitType: 'video' + adUnitType: 'video', + custom1: 'custom1', + custom2: 'custom2', + custom3: 'custom3', + custom4: 'custom4', + custom5: 'custom5' }, placementCode: 'placement_2', auctionId: 'e4771143-6aa7-41ec-8824-ced4342c96c8', @@ -196,7 +216,12 @@ describe('limelightDigitalAdapter', function () { 'transactionId', 'publisherId', 'userIdAsEids', - 'supplyChain' + 'supplyChain', + 'custom1', + 'custom2', + 'custom3', + 'custom4', + 'custom5' ); expect(adUnit.id).to.be.a('number'); expect(adUnit.bidId).to.be.a('string'); @@ -205,6 +230,11 @@ describe('limelightDigitalAdapter', function () { expect(adUnit.sizes).to.be.an('array'); expect(adUnit.userIdAsEids).to.be.an('array'); expect(adUnit.supplyChain).to.be.an('object'); + expect(adUnit.custom1).to.be.a('string'); + expect(adUnit.custom2).to.be.a('string'); + expect(adUnit.custom3).to.be.a('string'); + expect(adUnit.custom4).to.be.a('string'); + expect(adUnit.custom5).to.be.a('string'); }) }) }) diff --git a/test/spec/modules/liveIntentIdSystem_spec.js b/test/spec/modules/liveIntentIdSystem_spec.js index 3c22cda1154..afbd1566438 100644 --- a/test/spec/modules/liveIntentIdSystem_spec.js +++ b/test/spec/modules/liveIntentIdSystem_spec.js @@ -5,7 +5,7 @@ import { server } from 'test/mocks/xhr.js'; resetLiveIntentIdSubmodule(); liveIntentIdSubmodule.setModuleMode('standard') const PUBLISHER_ID = '89899'; -const defaultConfigParams = { params: {publisherId: PUBLISHER_ID} }; +const defaultConfigParams = { params: {publisherId: PUBLISHER_ID, fireEventDelay: 1} }; const responseHeader = {'Content-Type': 'application/json'} describe('LiveIntentId', function() { @@ -45,7 +45,7 @@ describe('LiveIntentId', function() { let callBackSpy = sinon.spy(); let submoduleCallback = liveIntentIdSubmodule.getId(defaultConfigParams).callback; submoduleCallback(callBackSpy); - let request = server.requests[1]; + let request = server.requests[0]; expect(request.url).to.match(/.*us_privacy=1YNY.*&gdpr=1&n3pc=1&gdpr_consent=consentDataString.*/); const response = { unifiedId: 'a_unified_id', @@ -59,25 +59,31 @@ describe('LiveIntentId', function() { expect(callBackSpy.calledOnceWith(response)).to.be.true; }); - it('should fire an event when getId', function() { + it('should fire an event when getId', function(done) { uspConsentDataStub.returns('1YNY'); gdprConsentDataStub.returns({ gdprApplies: true, consentString: 'consentDataString' }) liveIntentIdSubmodule.getId(defaultConfigParams); - expect(server.requests[0].url).to.match(/https:\/\/rp.liadm.com\/j\?.*&us_privacy=1YNY.*&wpn=prebid.*&gdpr=1&n3pc=1&n3pct=1&nb=1&gdpr_consent=consentDataString.*/); + setTimeout(() => { + expect(server.requests[0].url).to.match(/https:\/\/rp.liadm.com\/j\?.*&us_privacy=1YNY.*&wpn=prebid.*&gdpr=1&n3pc=1&n3pct=1&nb=1&gdpr_consent=consentDataString.*/); + done(); + }, 200); }); - it('should fire an event when getId and a hash is provided', function() { + it('should fire an event when getId and a hash is provided', function(done) { liveIntentIdSubmodule.getId({ params: { ...defaultConfigParams, emailHash: '58131bc547fb87af94cebdaf3102321f' }}); - expect(server.requests[0].url).to.match(/https:\/\/rp.liadm.com\/j\?.*e=58131bc547fb87af94cebdaf3102321f.+/) + setTimeout(() => { + expect(server.requests[0].url).to.match(/https:\/\/rp.liadm.com\/j\?.*e=58131bc547fb87af94cebdaf3102321f.+/) + done(); + }, 200); }); - it('should initialize LiveConnect with the config params when decode and emit an event', function () { + it('should initialize LiveConnect with the config params when decode and emit an event', function (done) { liveIntentIdSubmodule.decode({}, { params: { ...defaultConfigParams.params, ...{ @@ -88,25 +94,34 @@ describe('LiveIntentId', function() { } } }}); - expect(server.requests[0].url).to.match(/https:\/\/collector.liveintent.com\/j\?.*aid=a-0001.*&wpn=prebid.*/); + setTimeout(() => { + expect(server.requests[0].url).to.match(/https:\/\/collector.liveintent.com\/j\?.*aid=a-0001.*&wpn=prebid.*/); + done(); + }, 200); }); - it('should initialize LiveConnect and emit an event with a privacy string when decode', function() { + it('should initialize LiveConnect and emit an event with a privacy string when decode', function(done) { uspConsentDataStub.returns('1YNY'); gdprConsentDataStub.returns({ gdprApplies: false, consentString: 'consentDataString' }) liveIntentIdSubmodule.decode({}, defaultConfigParams); - expect(server.requests[0].url).to.match(/.*us_privacy=1YNY.*&gdpr=0&gdpr_consent=consentDataString.*/); + setTimeout(() => { + expect(server.requests[0].url).to.match(/.*us_privacy=1YNY.*&gdpr=0&gdpr_consent=consentDataString.*/); + done(); + }, 200); }); - it('should fire an event when decode and a hash is provided', function() { + it('should fire an event when decode and a hash is provided', function(done) { liveIntentIdSubmodule.decode({}, { params: { ...defaultConfigParams.params, emailHash: '58131bc547fb87af94cebdaf3102321f' }}); - expect(server.requests[0].url).to.match(/https:\/\/rp.liadm.com\/j\?.*e=58131bc547fb87af94cebdaf3102321f.+/); + setTimeout(() => { + expect(server.requests[0].url).to.match(/https:\/\/rp.liadm.com\/j\?.*e=58131bc547fb87af94cebdaf3102321f.+/); + done(); + }, 200); }); it('should not return a decoded identifier when the unifiedId is not present in the value', function() { @@ -114,25 +129,31 @@ describe('LiveIntentId', function() { expect(result).to.be.eql({}); }); - it('should fire an event when decode', function() { + it('should fire an event when decode', function(done) { liveIntentIdSubmodule.decode({}, defaultConfigParams); - expect(server.requests[0].url).to.be.not.null + setTimeout(() => { + expect(server.requests[0].url).to.be.not.null + done(); + }, 200); }); - it('should initialize LiveConnect and send data only once', function() { + it('should initialize LiveConnect and send data only once', function(done) { liveIntentIdSubmodule.getId(defaultConfigParams); liveIntentIdSubmodule.decode({}, defaultConfigParams); liveIntentIdSubmodule.getId(defaultConfigParams); liveIntentIdSubmodule.decode({}, defaultConfigParams); - expect(server.requests.length).to.be.eq(1); + setTimeout(() => { + expect(server.requests.length).to.be.eq(1); + done(); + }, 200); }); - it('should call the Custom URL of the LiveIntent Identity Exchange endpoint', function() { + it('should call the custom URL of the LiveIntent Identity Exchange endpoint', function() { getCookieStub.returns(null); let callBackSpy = sinon.spy(); let submoduleCallback = liveIntentIdSubmodule.getId({ params: {...defaultConfigParams.params, ...{'url': 'https://dummy.liveintent.com/idex'}} }).callback; submoduleCallback(callBackSpy); - let request = server.requests[1]; + let request = server.requests[0]; expect(request.url).to.be.eq('https://dummy.liveintent.com/idex/prebid/89899?resolve=nonId'); request.respond( 204, @@ -152,7 +173,7 @@ describe('LiveIntentId', function() { } } }).callback; submoduleCallback(callBackSpy); - let request = server.requests[1]; + let request = server.requests[0]; expect(request.url).to.be.eq('https://dummy.liveintent.com/idex/rubicon/89899?resolve=nonId'); request.respond( 200, @@ -167,7 +188,7 @@ describe('LiveIntentId', function() { let callBackSpy = sinon.spy(); let submoduleCallback = liveIntentIdSubmodule.getId(defaultConfigParams).callback; submoduleCallback(callBackSpy); - let request = server.requests[1]; + let request = server.requests[0]; expect(request.url).to.be.eq('https://idx.liadm.com/idex/prebid/89899?resolve=nonId'); request.respond( 200, @@ -182,7 +203,7 @@ describe('LiveIntentId', function() { let callBackSpy = sinon.spy(); let submoduleCallback = liveIntentIdSubmodule.getId(defaultConfigParams).callback; submoduleCallback(callBackSpy); - let request = server.requests[1]; + let request = server.requests[0]; expect(request.url).to.be.eq('https://idx.liadm.com/idex/prebid/89899?resolve=nonId'); request.respond( 503, @@ -199,7 +220,7 @@ describe('LiveIntentId', function() { let callBackSpy = sinon.spy(); let submoduleCallback = liveIntentIdSubmodule.getId(defaultConfigParams).callback; submoduleCallback(callBackSpy); - let request = server.requests[1]; + let request = server.requests[0]; expect(request.url).to.be.eq(`https://idx.liadm.com/idex/prebid/89899?duid=${oldCookie}&resolve=nonId`); request.respond( 200, @@ -222,7 +243,7 @@ describe('LiveIntentId', function() { let callBackSpy = sinon.spy(); let submoduleCallback = liveIntentIdSubmodule.getId(configParams).callback; submoduleCallback(callBackSpy); - let request = server.requests[1]; + let request = server.requests[0]; expect(request.url).to.be.eq(`https://idx.liadm.com/idex/prebid/89899?duid=${oldCookie}&_thirdPC=third-pc&resolve=nonId`); request.respond( 200, @@ -244,7 +265,7 @@ describe('LiveIntentId', function() { let callBackSpy = sinon.spy(); let submoduleCallback = liveIntentIdSubmodule.getId(configParams).callback; submoduleCallback(callBackSpy); - let request = server.requests[1]; + let request = server.requests[0]; expect(request.url).to.be.eq('https://idx.liadm.com/idex/prebid/89899?_thirdPC=%7B%22key%22%3A%22value%22%7D&resolve=nonId'); request.respond( 200, @@ -277,7 +298,7 @@ describe('LiveIntentId', function() { ...{ requestedAttributesOverrides: { 'foo': true, 'bar': false } } } }).callback; submoduleCallback(callBackSpy); - let request = server.requests[1]; + let request = server.requests[0]; expect(request.url).to.be.eq(`https://idx.liadm.com/idex/prebid/89899?resolve=nonId&resolve=foo`); request.respond( 200, @@ -292,6 +313,16 @@ describe('LiveIntentId', function() { expect(result).to.eql({'lipb': {'lipbid': 'foo', 'nonId': 'foo', 'uid2': 'bar'}, 'uid2': {'id': 'bar'}}); }); + it('should decode a bidswitch id to a seperate object when present', function() { + const result = liveIntentIdSubmodule.decode({ nonId: 'foo', bidswitch: 'bar' }); + expect(result).to.eql({'lipb': {'lipbid': 'foo', 'nonId': 'foo', 'bidswitch': 'bar'}, 'bidswitch': {'id': 'bar'}}); + }); + + it('should decode a medianet id to a seperate object when present', function() { + const result = liveIntentIdSubmodule.decode({ nonId: 'foo', medianet: 'bar' }); + expect(result).to.eql({'lipb': {'lipbid': 'foo', 'nonId': 'foo', 'medianet': 'bar'}, 'medianet': {'id': 'bar'}}); + }); + it('should decode values with uid2 but no nonId', function() { const result = liveIntentIdSubmodule.decode({ uid2: 'bar' }); expect(result).to.eql({'uid2': {'id': 'bar'}}); @@ -304,7 +335,7 @@ describe('LiveIntentId', function() { ...{ requestedAttributesOverrides: { 'nonId': false, 'uid2': true } } } }).callback; submoduleCallback(callBackSpy); - let request = server.requests[1]; + let request = server.requests[0]; expect(request.url).to.be.eq(`https://idx.liadm.com/idex/prebid/89899?resolve=uid2`); request.respond( 200, diff --git a/test/spec/modules/livewrappedBidAdapter_spec.js b/test/spec/modules/livewrappedBidAdapter_spec.js index a4fee5e3b26..6bbdf6e1705 100644 --- a/test/spec/modules/livewrappedBidAdapter_spec.js +++ b/test/spec/modules/livewrappedBidAdapter_spec.js @@ -13,6 +13,10 @@ describe('Livewrapped adapter tests', function () { window.livewrapped = undefined; + config.setConfig({ + device: { w: 100, h: 100 } + }); + bidderRequest = { bidderCode: 'livewrapped', auctionId: 'c45dd708-a418-42ec-b8a7-b70a6c6fab0a', @@ -43,6 +47,7 @@ describe('Livewrapped adapter tests', function () { afterEach(function () { sandbox.restore(); + config.resetConfig(); }); describe('isBidRequestValid', function() { @@ -351,7 +356,7 @@ describe('Livewrapped adapter tests', function () { expect(data).to.deep.equal(expectedQuery); }); - it('should make a well-formed single request object with ad blocker revovered parameter', function() { + it('should make a well-formed single request object with ad blocker recovered parameter', function() { sandbox.stub(utils, 'getWindowTop').returns({ I12C: { Morph: 1 } }); sandbox.stub(utils, 'isSafariBrowser').callsFake(() => false); sandbox.stub(storage, 'cookiesAreEnabled').callsFake(() => true); @@ -489,7 +494,7 @@ describe('Livewrapped adapter tests', function () { return {bundle: 'bundle', domain: 'https://appdomain.com'}; } if (key === 'device') { - return {ifa: 'ifa', width: 300, height: 200}; + return {ifa: 'ifa', w: 300, h: 200}; } return origGetConfig.apply(config, arguments); }); @@ -839,6 +844,266 @@ describe('Livewrapped adapter tests', function () { expect(data).to.deep.equal(expectedQuery); }); + + it('width and height should default to values from window when not set in config', function() { + sandbox.stub(utils, 'isSafariBrowser').callsFake(() => false); + sandbox.stub(storage, 'cookiesAreEnabled').callsFake(() => true); + + config.resetConfig(); + + let testbidRequest = clone(bidderRequest); + let result = spec.buildRequests(testbidRequest.bids, testbidRequest); + let data = JSON.parse(result.data); + + expect(result.url).to.equal('https://lwadm.com/ad'); + + let expectedQuery = { + auctionId: 'F7557995-65F5-4682-8782-7D5D34D82A8C', + publisherId: '26947112-2289-405D-88C1-A7340C57E63E', + userId: 'user id', + url: 'https://www.domain.com', + seats: {'dsp': ['seat 1']}, + version: '1.4', + width: window.innerWidth, + height: window.innerHeight, + cookieSupport: true, + adRequests: [{ + adUnitId: '9E153CED-61BC-479E-98DF-24DC0D01BA37', + callerAdUnitId: 'panorama_d_1', + bidId: '2ffb201a808da7', + transactionId: '3D1C8CF7-D288-4D7F-8ADD-97C553056C3D', + formats: [{width: 980, height: 240}, {width: 980, height: 120}] + }] + }; + + expect(data).to.deep.equal(expectedQuery); + }); + }); + + describe('price floors module', function() { + it('price floors module disabled', function() { + sandbox.stub(utils, 'isSafariBrowser').callsFake(() => false); + sandbox.stub(storage, 'cookiesAreEnabled').callsFake(() => true); + + let testbidRequest = clone(bidderRequest); + let result = spec.buildRequests(testbidRequest.bids, testbidRequest); + let data = JSON.parse(result.data); + + expect(result.url).to.equal('https://lwadm.com/ad'); + + let expectedQuery = { + auctionId: 'F7557995-65F5-4682-8782-7D5D34D82A8C', + publisherId: '26947112-2289-405D-88C1-A7340C57E63E', + userId: 'user id', + url: 'https://www.domain.com', + seats: {'dsp': ['seat 1']}, + version: '1.4', + width: 100, + height: 100, + cookieSupport: true, + adRequests: [{ + adUnitId: '9E153CED-61BC-479E-98DF-24DC0D01BA37', + callerAdUnitId: 'panorama_d_1', + bidId: '2ffb201a808da7', + transactionId: '3D1C8CF7-D288-4D7F-8ADD-97C553056C3D', + formats: [{width: 980, height: 240}, {width: 980, height: 120}] + }] + }; + + expect(data).to.deep.equal(expectedQuery); + }); + + it('getFloor does not return an object', function() { + sandbox.stub(utils, 'isSafariBrowser').callsFake(() => false); + sandbox.stub(storage, 'cookiesAreEnabled').callsFake(() => true); + + let testbidRequest = clone(bidderRequest); + let bids = testbidRequest.bids.map(b => { + b.getFloor = function () { return undefined; } + return b; + }); + let result = spec.buildRequests(bids, testbidRequest); + let data = JSON.parse(result.data); + + expect(result.url).to.equal('https://lwadm.com/ad'); + + let expectedQuery = { + auctionId: 'F7557995-65F5-4682-8782-7D5D34D82A8C', + publisherId: '26947112-2289-405D-88C1-A7340C57E63E', + userId: 'user id', + url: 'https://www.domain.com', + seats: {'dsp': ['seat 1']}, + version: '1.4', + width: 100, + height: 100, + cookieSupport: true, + adRequests: [{ + adUnitId: '9E153CED-61BC-479E-98DF-24DC0D01BA37', + callerAdUnitId: 'panorama_d_1', + bidId: '2ffb201a808da7', + transactionId: '3D1C8CF7-D288-4D7F-8ADD-97C553056C3D', + formats: [{width: 980, height: 240}, {width: 980, height: 120}] + }] + }; + + expect(data).to.deep.equal(expectedQuery); + }); + + it('getFloor returns a NaN floor', function() { + sandbox.stub(utils, 'isSafariBrowser').callsFake(() => false); + sandbox.stub(storage, 'cookiesAreEnabled').callsFake(() => true); + + let testbidRequest = clone(bidderRequest); + let bids = testbidRequest.bids.map(b => { + b.getFloor = function () { return { floor: undefined }; } + return b; + }); + let result = spec.buildRequests(bids, testbidRequest); + let data = JSON.parse(result.data); + + expect(result.url).to.equal('https://lwadm.com/ad'); + + let expectedQuery = { + auctionId: 'F7557995-65F5-4682-8782-7D5D34D82A8C', + publisherId: '26947112-2289-405D-88C1-A7340C57E63E', + userId: 'user id', + url: 'https://www.domain.com', + seats: {'dsp': ['seat 1']}, + version: '1.4', + width: 100, + height: 100, + cookieSupport: true, + adRequests: [{ + adUnitId: '9E153CED-61BC-479E-98DF-24DC0D01BA37', + callerAdUnitId: 'panorama_d_1', + bidId: '2ffb201a808da7', + transactionId: '3D1C8CF7-D288-4D7F-8ADD-97C553056C3D', + formats: [{width: 980, height: 240}, {width: 980, height: 120}] + }] + }; + + expect(data).to.deep.equal(expectedQuery); + }); + + it('getFloor returns unexpected currency', function() { + sandbox.stub(utils, 'isSafariBrowser').callsFake(() => false); + sandbox.stub(storage, 'cookiesAreEnabled').callsFake(() => true); + + let testbidRequest = clone(bidderRequest); + let bids = testbidRequest.bids.map(b => { + b.getFloor = function () { return { floor: 10, currency: 'EUR' }; } + return b; + }); + let result = spec.buildRequests(bids, testbidRequest); + let data = JSON.parse(result.data); + + expect(result.url).to.equal('https://lwadm.com/ad'); + + let expectedQuery = { + auctionId: 'F7557995-65F5-4682-8782-7D5D34D82A8C', + publisherId: '26947112-2289-405D-88C1-A7340C57E63E', + userId: 'user id', + url: 'https://www.domain.com', + seats: {'dsp': ['seat 1']}, + version: '1.4', + width: 100, + height: 100, + cookieSupport: true, + adRequests: [{ + adUnitId: '9E153CED-61BC-479E-98DF-24DC0D01BA37', + callerAdUnitId: 'panorama_d_1', + bidId: '2ffb201a808da7', + transactionId: '3D1C8CF7-D288-4D7F-8ADD-97C553056C3D', + formats: [{width: 980, height: 240}, {width: 980, height: 120}] + }] + }; + + expect(data).to.deep.equal(expectedQuery); + }); + + it('getFloor returns valid floor - ad server currency', function() { + sandbox.stub(utils, 'isSafariBrowser').callsFake(() => false); + sandbox.stub(storage, 'cookiesAreEnabled').callsFake(() => true); + + let origGetConfig = config.getConfig; + sandbox.stub(config, 'getConfig').callsFake(function (key) { + if (key === 'currency.adServerCurrency') { + return 'EUR'; + } + return origGetConfig.apply(config, arguments); + }); + + let testbidRequest = clone(bidderRequest); + let bids = testbidRequest.bids.map(b => { + b.getFloor = function () { return { floor: 10, currency: 'EUR' }; } + return b; + }); + let result = spec.buildRequests(bids, testbidRequest); + let data = JSON.parse(result.data); + + expect(result.url).to.equal('https://lwadm.com/ad'); + + let expectedQuery = { + auctionId: 'F7557995-65F5-4682-8782-7D5D34D82A8C', + publisherId: '26947112-2289-405D-88C1-A7340C57E63E', + userId: 'user id', + url: 'https://www.domain.com', + seats: {'dsp': ['seat 1']}, + version: '1.4', + width: 100, + height: 100, + cookieSupport: true, + flrCur: 'EUR', + adRequests: [{ + adUnitId: '9E153CED-61BC-479E-98DF-24DC0D01BA37', + callerAdUnitId: 'panorama_d_1', + bidId: '2ffb201a808da7', + transactionId: '3D1C8CF7-D288-4D7F-8ADD-97C553056C3D', + formats: [{width: 980, height: 240}, {width: 980, height: 120}], + flr: 10 + }] + }; + + expect(data).to.deep.equal(expectedQuery); + }); + + it('getFloor returns valid floor - default currency', function() { + sandbox.stub(utils, 'isSafariBrowser').callsFake(() => false); + sandbox.stub(storage, 'cookiesAreEnabled').callsFake(() => true); + + let testbidRequest = clone(bidderRequest); + let bids = testbidRequest.bids.map(b => { + b.getFloor = function () { return { floor: 10, currency: 'USD' }; } + return b; + }); + let result = spec.buildRequests(bids, testbidRequest); + let data = JSON.parse(result.data); + + expect(result.url).to.equal('https://lwadm.com/ad'); + + let expectedQuery = { + auctionId: 'F7557995-65F5-4682-8782-7D5D34D82A8C', + publisherId: '26947112-2289-405D-88C1-A7340C57E63E', + userId: 'user id', + url: 'https://www.domain.com', + seats: {'dsp': ['seat 1']}, + version: '1.4', + width: 100, + height: 100, + cookieSupport: true, + flrCur: 'USD', + adRequests: [{ + adUnitId: '9E153CED-61BC-479E-98DF-24DC0D01BA37', + callerAdUnitId: 'panorama_d_1', + bidId: '2ffb201a808da7', + transactionId: '3D1C8CF7-D288-4D7F-8ADD-97C553056C3D', + formats: [{width: 980, height: 240}, {width: 980, height: 120}], + flr: 10 + }] + }; + + expect(data).to.deep.equal(expectedQuery); + }); }); it('should make use of user ids if available', function() { diff --git a/test/spec/modules/lotamePanoramaIdSystem_spec.js b/test/spec/modules/lotamePanoramaIdSystem_spec.js index 5dc055ac080..ea538db08e1 100644 --- a/test/spec/modules/lotamePanoramaIdSystem_spec.js +++ b/test/spec/modules/lotamePanoramaIdSystem_spec.js @@ -18,6 +18,7 @@ describe('LotameId', function() { let removeFromLocalStorageStub; let timeStampStub; let uspConsentDataStub; + let requestHost; const nowTimestamp = new Date().getTime(); @@ -33,6 +34,11 @@ describe('LotameId', function() { ); timeStampStub = sinon.stub(utils, 'timestamp').returns(nowTimestamp); uspConsentDataStub = sinon.stub(uspDataHandler, 'getConsentData'); + if (navigator.userAgent && navigator.userAgent.indexOf('Safari') != -1 && navigator.userAgent.indexOf('Chrome') == -1) { + requestHost = 'https://c.ltmsphrcl.net/id'; + } else { + requestHost = 'https://id.crwdcntrl.net/id'; + } }); afterEach(function () { @@ -69,7 +75,7 @@ describe('LotameId', function() { }); it('should call the remote server when getId is called', function () { - expect(request.url).to.be.eq('https://id.crwdcntrl.net/id'); + expect(request.url).to.be.eq(`${requestHost}`); expect(callBackSpy.calledOnce).to.be.true; }); @@ -439,7 +445,7 @@ describe('LotameId', function() { it('should pass the gdpr consent string back', function() { expect(request.url).to.be.eq( - 'https://id.crwdcntrl.net/id?gdpr_applies=true&gdpr_consent=consentGiven' + `${requestHost}?gdpr_applies=true&gdpr_consent=consentGiven` ); }); }); @@ -471,7 +477,7 @@ describe('LotameId', function() { it('should pass the gdpr consent string back', function() { expect(request.url).to.be.eq( - 'https://id.crwdcntrl.net/id?gdpr_applies=true&gdpr_consent=consentGiven' + `${requestHost}?gdpr_applies=true&gdpr_consent=consentGiven` ); }); }); @@ -503,7 +509,7 @@ describe('LotameId', function() { it('should pass the gdpr consent string back', function() { expect(request.url).to.be.eq( - 'https://id.crwdcntrl.net/id?gdpr_applies=true&gdpr_consent=consentGiven' + `${requestHost}?gdpr_applies=true&gdpr_consent=consentGiven` ); }); }); @@ -531,7 +537,7 @@ describe('LotameId', function() { it('should not include the gdpr consent string on the url', function() { expect(request.url).to.be.eq( - 'https://id.crwdcntrl.net/id?gdpr_applies=true' + `${requestHost}?gdpr_applies=true` ); }); }); @@ -560,7 +566,7 @@ describe('LotameId', function() { it('should pass the gdpr consent string back', function() { expect(request.url).to.be.eq( - 'https://id.crwdcntrl.net/id?gdpr_consent=consentGiven' + `${requestHost}?gdpr_consent=consentGiven` ); }); }); @@ -589,7 +595,7 @@ describe('LotameId', function() { it('should pass the gdpr consent string back', function() { expect(request.url).to.be.eq( - 'https://id.crwdcntrl.net/id?gdpr_consent=consentGiven' + `${requestHost}?gdpr_consent=consentGiven` ); }); }); @@ -613,7 +619,7 @@ describe('LotameId', function() { }); it('should pass the gdpr consent string back', function() { - expect(request.url).to.be.eq('https://id.crwdcntrl.net/id'); + expect(request.url).to.be.eq(`${requestHost}`); }); }); @@ -835,7 +841,7 @@ describe('LotameId', function() { it('should pass the usp consent string and client id back', function () { expect(request.url).to.be.eq( - 'https://id.crwdcntrl.net/id?gdpr_applies=false&us_privacy=1NNN&c=1234' + `${requestHost}?gdpr_applies=false&us_privacy=1NNN&c=1234` ); }); @@ -923,7 +929,7 @@ describe('LotameId', function() { it('should pass client id back', function () { expect(request.url).to.be.eq( - 'https://id.crwdcntrl.net/id?gdpr_applies=false&c=1234' + `${requestHost}?gdpr_applies=false&c=1234` ); }); diff --git a/test/spec/modules/luponmediaBidAdapter_spec.js b/test/spec/modules/luponmediaBidAdapter_spec.js index 9f109ff1892..c8d4c18c407 100755 --- a/test/spec/modules/luponmediaBidAdapter_spec.js +++ b/test/spec/modules/luponmediaBidAdapter_spec.js @@ -133,7 +133,7 @@ describe('luponmediaBidAdapter', function () { } ], 'auctionStart': 1587413920820, - 'timeout': 2000, + 'timeout': 1500, 'refererInfo': { 'page': 'https://novi.ba/clanak/176067/fast-car-beginner-s-guide-to-tuning-turbo-engines', 'reachedTop': true, diff --git a/test/spec/modules/magniteAnalyticsAdapter_spec.js b/test/spec/modules/magniteAnalyticsAdapter_spec.js index 69fd7794ced..bf9c3050bf6 100644 --- a/test/spec/modules/magniteAnalyticsAdapter_spec.js +++ b/test/spec/modules/magniteAnalyticsAdapter_spec.js @@ -3,6 +3,7 @@ import magniteAdapter, { getHostNameFromReferer, storage, rubiConf, + detectBrowserFromUa } from '../../../modules/magniteAnalyticsAdapter.js'; import CONSTANTS from 'src/constants.json'; import { config } from 'src/config.js'; @@ -27,7 +28,8 @@ const { BIDDER_DONE, BID_WON, BID_TIMEOUT, - BILLABLE_EVENT + BILLABLE_EVENT, + SEAT_NON_BID } } = CONSTANTS; @@ -101,6 +103,11 @@ const MOCK = { 'startTime': 1658868383748 } ], + 'ortb2': { + 'device': { + 'ua': 'Mozilla/ 5.0(Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/ 537.36(KHTML, like Gecko) Chrome/ 109.0.0.0 Safari / 537.36' + } + }, 'refererInfo': { 'page': 'http://a-test-domain.com:8000/test_pages/sanity/TEMP/prebidTest.html?pbjs_debug=true', }, @@ -154,6 +161,16 @@ const MOCK = { 'status': 'rendered', getStatusCode: () => 1, }, + SEAT_NON_BID: { + auctionId: '99785e47-a7c8-4c8a-ae05-ef1c717a4b4d', + seatnonbid: [{ + seat: 'rubicon', + nonbid: [{ + status: 1, + impid: 'box' + }] + }] + }, AUCTION_END: { 'auctionId': '99785e47-a7c8-4c8a-ae05-ef1c717a4b4d', 'auctionEnd': 1658868384019, @@ -209,6 +226,9 @@ const ANALYTICS_MESSAGE = { 'start': 1519767013781, 'expires': 1519788613781 }, + 'client': { + 'browser': 'Chrome' + }, 'auctions': [ { 'auctionId': '99785e47-a7c8-4c8a-ae05-ef1c717a4b4d', @@ -306,7 +326,7 @@ const ANALYTICS_MESSAGE = { describe('magnite analytics adapter', function () { let sandbox; let clock; - let getDataFromLocalStorageStub, setDataInLocalStorageStub, localStorageIsEnabledStub; + let getDataFromLocalStorageStub, setDataInLocalStorageStub, localStorageIsEnabledStub, removeDataFromLocalStorageStub; let gptSlot0; let gptSlotRenderEnded0; beforeEach(function () { @@ -325,6 +345,7 @@ describe('magnite analytics adapter', function () { getDataFromLocalStorageStub = sinon.stub(storage, 'getDataFromLocalStorage'); setDataInLocalStorageStub = sinon.stub(storage, 'setDataInLocalStorage'); localStorageIsEnabledStub = sinon.stub(storage, 'localStorageIsEnabled'); + removeDataFromLocalStorageStub = sinon.stub(storage, 'removeDataFromLocalStorage') sandbox = sinon.sandbox.create(); localStorageIsEnabledStub.returns(true); @@ -355,6 +376,7 @@ describe('magnite analytics adapter', function () { getDataFromLocalStorageStub.restore(); setDataInLocalStorageStub.restore(); localStorageIsEnabledStub.restore(); + removeDataFromLocalStorageStub.restore(); magniteAdapter.disableAnalytics(); }); @@ -548,6 +570,29 @@ describe('magnite analytics adapter', function () { ]); }); + [ + { + ua: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.0 Safari/605.1.15', + expected: 'Safari' + }, + { + ua: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:109.0) Gecko/20100101 Firefox/109.0', + expected: 'Firefox' + }, + { + ua: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36 Edg/109.0.1518.78', + expected: 'Edge' + }, + { + ua: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36 OPR/94.0.0.0', + expected: 'Opera' + } + ].forEach(testData => { + it(`should parse browser from ${testData.expected} user agent correctly`, function () { + expect(detectBrowserFromUa(testData.ua)).to.equal(testData.expected); + }); + }) + it('should pass along 1x1 size if no sizes in adUnit', function () { const auctionInit = utils.deepClone(MOCK.AUCTION_INIT); @@ -1932,19 +1977,21 @@ describe('magnite analytics adapter', function () { }); }); - it('getHostNameFromReferer correctly grabs hostname from an input URL', function () { - let inputUrl = 'https://www.prebid.org/some/path?pbjs_debug=true'; - expect(getHostNameFromReferer(inputUrl)).to.equal('www.prebid.org'); - inputUrl = 'https://www.prebid.com/some/path?pbjs_debug=true'; - expect(getHostNameFromReferer(inputUrl)).to.equal('www.prebid.com'); - inputUrl = 'https://prebid.org/some/path?pbjs_debug=true'; - expect(getHostNameFromReferer(inputUrl)).to.equal('prebid.org'); - inputUrl = 'http://xn--p8j9a0d9c9a.xn--q9jyb4c/'; - expect(typeof getHostNameFromReferer(inputUrl)).to.equal('string'); - - // not non-UTF char's in query / path which break if noDecodeWholeURL not set - inputUrl = 'https://prebid.org/search_results/%95x%8Em%92%CA/?category=000'; - expect(getHostNameFromReferer(inputUrl)).to.equal('prebid.org'); + describe('getHostNameFromReferer', () => { + it('correctly grabs hostname from an input URL', function () { + let inputUrl = 'https://www.prebid.org/some/path?pbjs_debug=true'; + expect(getHostNameFromReferer(inputUrl)).to.equal('www.prebid.org'); + inputUrl = 'https://www.prebid.com/some/path?pbjs_debug=true'; + expect(getHostNameFromReferer(inputUrl)).to.equal('www.prebid.com'); + inputUrl = 'https://prebid.org/some/path?pbjs_debug=true'; + expect(getHostNameFromReferer(inputUrl)).to.equal('prebid.org'); + inputUrl = 'http://xn--p8j9a0d9c9a.xn--q9jyb4c/'; + expect(typeof getHostNameFromReferer(inputUrl)).to.equal('string'); + + // not non-UTF char's in query / path which break if noDecodeWholeURL not set + inputUrl = 'https://prebid.org/search_results/%95x%8Em%92%CA/?category=000'; + expect(getHostNameFromReferer(inputUrl)).to.equal('prebid.org'); + }); }); describe(`handle currency conversions`, () => { @@ -1985,4 +2032,139 @@ describe('magnite analytics adapter', function () { expect(bidResponseObj.bidPriceUSD).to.equal(0); }); }); + + describe('onDataDeletionRequest', () => { + it('attempts to delete the magnite cookie when local storage is enabled', () => { + magniteAdapter.onDataDeletionRequest(); + + expect(removeDataFromLocalStorageStub.getCall(0).args[0]).to.equal('mgniSession'); + }); + + it('throws an error if it cannot access the cookie', (done) => { + localStorageIsEnabledStub.returns(false); + try { + magniteAdapter.onDataDeletionRequest(); + } catch (error) { + expect(error.message).to.equal('Unable to access local storage, no data deleted'); + done(); + } + }) + }); + + describe('BID_RESPONSE events', () => { + beforeEach(() => { + magniteAdapter.enableAnalytics({ + options: { + endpoint: '//localhost:9999/event', + accountId: 1001 + } + }); + config.setConfig({ rubicon: { updatePageView: true } }); + }); + + it('should add a no-bid bid to the add unit if it recieves one from the server', () => { + const bidResponse = utils.deepClone(MOCK.BID_RESPONSE); + const auctionInit = utils.deepClone(MOCK.AUCTION_INIT); + + bidResponse.requestId = 'fakeId'; + bidResponse.seatBidId = 'fakeId'; + + bidResponse.requestId = 'fakeId'; + events.emit(AUCTION_INIT, auctionInit); + events.emit(BID_REQUESTED, MOCK.BID_REQUESTED); + events.emit(BID_RESPONSE, bidResponse) + events.emit(BIDDER_DONE, MOCK.BIDDER_DONE); + events.emit(AUCTION_END, MOCK.AUCTION_END); + clock.tick(rubiConf.analyticsBatchTimeout + 1000); + + let message = JSON.parse(server.requests[0].requestBody); + expect(utils.generateUUID.called).to.equal(true); + + expect(message.auctions[0].adUnits[0].bids[1]).to.deep.equal( + { + bidder: 'rubicon', + source: 'server', + status: 'success', + bidResponse: { + 'bidPriceUSD': 3.4, + 'dimensions': { + 'height': 250, + 'width': 300 + }, + 'mediaType': 'banner' + }, + oldBidId: 'fakeId', + unknownBid: true, + bidId: 'fakeId', + clientLatencyMillis: 271 + } + ); + }); + }); + + describe('SEAT_NON_BID events', () => { + let seatnonbid; + + const runNonBidAuction = () => { + events.emit(AUCTION_INIT, MOCK.AUCTION_INIT); + events.emit(BID_REQUESTED, MOCK.BID_REQUESTED); + events.emit(SEAT_NON_BID, seatnonbid) + events.emit(BIDDER_DONE, MOCK.BIDDER_DONE); + events.emit(AUCTION_END, MOCK.AUCTION_END); + clock.tick(rubiConf.analyticsBatchTimeout + 1000); + }; + const checkStatusAgainstCode = (status, code, error, index) => { + seatnonbid.seatnonbid[0].nonbid[0].status = code; + runNonBidAuction(); + let message = JSON.parse(server.requests[index].requestBody); + let bid = message.auctions[0].adUnits[0].bids[1]; + + if (error) { + expect(bid.error).to.deep.equal(error); + } else { + expect(bid.error).to.equal(undefined); + } + expect(bid.source).to.equal('server'); + expect(bid.status).to.equal(status); + expect(bid.isSeatNonBid).to.equal(true); + }; + beforeEach(() => { + magniteAdapter.enableAnalytics({ + options: { + endpoint: '//localhost:9999/event', + accountId: 1001 + } + }); + seatnonbid = utils.deepClone(MOCK.SEAT_NON_BID); + }); + + it('adds seatnonbid info to bids array', () => { + runNonBidAuction(); + let message = JSON.parse(server.requests[0].requestBody); + + expect(message.auctions[0].adUnits[0].bids[1]).to.deep.equal( + { + bidder: 'rubicon', + source: 'server', + status: 'no-bid', + isSeatNonBid: true, + clientLatencyMillis: -139101369960 + } + ); + }); + + it('adjusts the status according to the status map', () => { + const statuses = [ + {code: 0, status: 'no-bid'}, + {code: 100, status: 'error', error: {code: 'request-error', description: 'general error'}}, + {code: 101, status: 'error', error: {code: 'timeout-error', description: 'prebid server timeout'}}, + {code: 200, status: 'rejected'}, + {code: 202, status: 'rejected'}, + {code: 301, status: 'rejected-ipf'} + ]; + statuses.forEach((info, index) => { + checkStatusAgainstCode(info.status, info.code, info.error, index); + }); + }); + }); }); diff --git a/test/spec/modules/mathildeadsBidAdapter_spec.js b/test/spec/modules/mathildeadsBidAdapter_spec.js index 0f0da6032eb..107906ec83d 100644 --- a/test/spec/modules/mathildeadsBidAdapter_spec.js +++ b/test/spec/modules/mathildeadsBidAdapter_spec.js @@ -74,7 +74,8 @@ describe('MathildeAdsBidAdapter', function () { gdprConsent: 'COvFyGBOvFyGBAbAAAENAPCAAOAAAAAAAAAAAEEUACCKAAA.IFoEUQQgAIQwgIwQABAEAAAAOIAACAIAAAAQAIAgEAACEAAAAAgAQBAAAAAAAGBAAgAAAAAAAFAAECAAAgAAQARAEQAAAAAJAAIAAgAAAYQEAAAQmAgBC3ZAYzUw', refererInfo: { referer: 'https://test.com' - } + }, + timeout: 500 }; describe('isBidRequestValid', function () { diff --git a/test/spec/modules/mediakeysBidAdapter_spec.js b/test/spec/modules/mediakeysBidAdapter_spec.js index 393b6ac6764..75c7f42cf58 100644 --- a/test/spec/modules/mediakeysBidAdapter_spec.js +++ b/test/spec/modules/mediakeysBidAdapter_spec.js @@ -601,33 +601,29 @@ describe('mediakeysBidAdapter', function () { }); describe('should support userId modules', function() { - const userId = { - pubcid: '01EAJWWNEPN3CYMM5N8M5VXY22', - unsuported: '666' - }; + const userIdAsEids = [{ + source: 'pubcid.org', + uids: [ + { + atype: 1, + id: '01EAJWWNEPN3CYMM5N8M5VXY22' + } + ] + }]; it('should send "user.eids" in the request for Prebid.js supported modules only', function() { const bidCopy = utils.deepClone(bid); - bidCopy.userId = userId; + bidCopy.userIdAsEids = userIdAsEids; const bidderRequestCopy = utils.deepClone(bidderRequest); - bidderRequestCopy.bids[0].userId = userId; + bidderRequestCopy.bids[0].userIdAsEids = userIdAsEids; const bidRequests = [utils.deepClone(bidCopy)]; const request = spec.buildRequests(bidRequests, bidderRequestCopy); const data = request.data; - const expected = [{ - source: 'pubcid.org', - uids: [ - { - atype: 1, - id: '01EAJWWNEPN3CYMM5N8M5VXY22' - } - ] - }]; + const expected = userIdAsEids; expect(data.user.ext).to.exist; - expect(data.user.ext.eids).to.have.lengthOf(1); expect(data.user.ext.eids).to.deep.equal(expected); }); }); diff --git a/test/spec/modules/medianetAnalyticsAdapter_spec.js b/test/spec/modules/medianetAnalyticsAdapter_spec.js index c408f23c4f4..e19c27cc2d3 100644 --- a/test/spec/modules/medianetAnalyticsAdapter_spec.js +++ b/test/spec/modules/medianetAnalyticsAdapter_spec.js @@ -30,9 +30,11 @@ const MOCK = { NO_BID: {'bidder': 'medianet', 'params': {'cid': 'test123', 'crid': '451466393', 'site': {}}, 'mediaTypes': {'banner': {'sizes': [[300, 250]], 'ext': ['asdads']}}, 'adUnitCode': 'div-gpt-ad-1460505748561-0', 'transactionId': '303fa0c6-682f-4aea-8e4a-dc68f0d5c7d5', 'sizes': [[300, 250], [300, 600]], 'bidId': '28248b0e6aece2', 'bidderRequestId': '13fccf3809fe43', 'auctionId': '8e0d5245-deb3-406c-96ca-9b609e077ff7', 'src': 'client'}, BID_TIMEOUT: [{'bidId': '28248b0e6aece2', 'bidder': 'medianet', 'adUnitCode': 'div-gpt-ad-1460505748561-0', 'auctionId': '8e0d5245-deb3-406c-96ca-9b609e077ff7', 'params': [{'cid': 'test123', 'crid': '451466393', 'site': {}}, {'cid': '8CUX0H51P', 'crid': '451466393', 'site': {}}], 'timeout': 6}], BIDS_SAME_REQ_DIFF_CPM: [{'bidderCode': 'medianet', 'width': 300, 'height': 250, 'adId': '3e6e4bce5c8fb3', 'requestId': '28248b0e6aece2', 'mediaType': 'banner', 'source': 'client', 'ext': {'pvid': 123, 'crid': '321'}, 'no_bid': false, 'cpm': 2.299, 'ad': 'AD_CODE', 'ttl': 180, 'creativeId': 'Test1', 'netRevenue': true, 'currency': 'USD', 'dfp_id': 'div-gpt-ad-1460505748561-0', 'originalCpm': 1.1495, 'originalCurrency': 'USD', 'floorData': {'floorValue': 1.10, 'floorRule': 'banner'}, 'auctionId': '8e0d5245-deb3-406c-96ca-9b609e077ff7', 'responseTimestamp': 1584563606009, 'requestTimestamp': 1584563605743, 'bidder': 'medianet', 'adUnitCode': 'div-gpt-ad-1460505748561-0', 'timeToRespond': 266, 'pbLg': '2.00', 'pbMg': '2.20', 'pbHg': '2.29', 'pbAg': '2.25', 'pbDg': '2.29', 'pbCg': '2.00', 'size': '300x250', 'adserverTargeting': {'hb_bidder': 'medianet', 'hb_adid': '3e6e4bce5c8fb3', 'hb_pb': '2.00', 'hb_size': '300x250', 'hb_source': 'client', 'hb_format': 'banner', 'prebid_test': 1}, 'status': 'rendered', 'params': [{'cid': 'test123', 'crid': '451466393'}]}, {'bidderCode': 'medianet', 'width': 300, 'height': 250, 'adId': '3e6e4bce5c8fb4', 'requestId': '28248b0e6aece2', 'mediaType': 'banner', 'source': 'client', 'ext': {'pvid': 123, 'crid': '321'}, 'no_bid': false, 'cpm': 1.299, 'ad': 'AD_CODE', 'ttl': 180, 'creativeId': 'Test1', 'netRevenue': true, 'currency': 'USD', 'dfp_id': 'div-gpt-ad-1460505748561-0', 'originalCpm': 1.1495, 'originalCurrency': 'USD', 'floorData': {'floorValue': 1.10, 'floorRule': 'banner'}, 'auctionId': '8e0d5245-deb3-406c-96ca-9b609e077ff7', 'responseTimestamp': 1584563606009, 'requestTimestamp': 1584563605743, 'bidder': 'medianet', 'adUnitCode': 'div-gpt-ad-1460505748561-0', 'timeToRespond': 278, 'pbLg': '1.00', 'pbMg': '1.20', 'pbHg': '1.29', 'pbAg': '1.25', 'pbDg': '1.29', 'pbCg': '1.00', 'size': '300x250', 'adserverTargeting': {'hb_bidder': 'medianet', 'hb_adid': '3e6e4bce5c8fb3', 'hb_pb': '1.00', 'hb_size': '300x250', 'hb_source': 'client', 'hb_format': 'banner', 'prebid_test': 1}, 'status': 'rendered', 'params': [{'cid': 'test123', 'crid': '451466393'}]}], + BIDS_SAME_REQ_DIFF_CPM_SAME_TIME: [{'bidderCode': 'medianet', 'width': 300, 'height': 250, 'adId': '3e6e4bce5c8fb4', 'requestId': '28248b0e6aece2', 'mediaType': 'banner', 'source': 'client', 'ext': {'pvid': 123, 'crid': '321'}, 'no_bid': false, 'cpm': 1.299, 'ad': 'AD_CODE', 'ttl': 180, 'creativeId': 'Test1', 'netRevenue': true, 'currency': 'USD', 'dfp_id': 'div-gpt-ad-1460505748561-0', 'originalCpm': 1.1495, 'originalCurrency': 'USD', 'floorData': {'floorValue': 1.10, 'floorRule': 'banner'}, 'auctionId': '8e0d5245-deb3-406c-96ca-9b609e077ff7', 'responseTimestamp': 1584563606009, 'requestTimestamp': 1584563605743, 'bidder': 'medianet', 'adUnitCode': 'div-gpt-ad-1460505748561-0', 'timeToRespond': 266, 'pbLg': '1.00', 'pbMg': '1.20', 'pbHg': '1.29', 'pbAg': '1.25', 'pbDg': '1.29', 'pbCg': '1.00', 'size': '300x250', 'adserverTargeting': {'hb_bidder': 'medianet', 'hb_adid': '3e6e4bce5c8fb3', 'hb_pb': '1.00', 'hb_size': '300x250', 'hb_source': 'client', 'hb_format': 'banner', 'prebid_test': 1}, 'status': 'rendered', 'params': [{'cid': 'test123', 'crid': '451466393'}]}, {'bidderCode': 'medianet', 'width': 300, 'height': 250, 'adId': '3e6e4bce5c8fb3', 'requestId': '28248b0e6aece2', 'mediaType': 'banner', 'source': 'client', 'ext': {'pvid': 123, 'crid': '321'}, 'no_bid': false, 'cpm': 2.299, 'ad': 'AD_CODE', 'ttl': 180, 'creativeId': 'Test1', 'netRevenue': true, 'currency': 'USD', 'dfp_id': 'div-gpt-ad-1460505748561-0', 'originalCpm': 1.1495, 'originalCurrency': 'USD', 'floorData': {'floorValue': 1.10, 'floorRule': 'banner'}, 'auctionId': '8e0d5245-deb3-406c-96ca-9b609e077ff7', 'responseTimestamp': 1584563606009, 'requestTimestamp': 1584563605743, 'bidder': 'medianet', 'adUnitCode': 'div-gpt-ad-1460505748561-0', 'timeToRespond': 266, 'pbLg': '2.00', 'pbMg': '2.20', 'pbHg': '2.29', 'pbAg': '2.25', 'pbDg': '2.29', 'pbCg': '2.00', 'size': '300x250', 'adserverTargeting': {'hb_bidder': 'medianet', 'hb_adid': '3e6e4bce5c8fb3', 'hb_pb': '2.00', 'hb_size': '300x250', 'hb_source': 'client', 'hb_format': 'banner', 'prebid_test': 1}, 'status': 'rendered', 'params': [{'cid': 'test123', 'crid': '451466393'}]}], BIDS_SAME_REQ_EQUAL_CPM: [{'bidderCode': 'medianet', 'width': 300, 'height': 250, 'adId': '3e6e4bce5c8fb3', 'requestId': '28248b0e6aece2', 'mediaType': 'banner', 'source': 'client', 'ext': {'pvid': 123, 'crid': '321'}, 'no_bid': false, 'cpm': 2.299, 'ad': 'AD_CODE', 'ttl': 180, 'creativeId': 'Test1', 'netRevenue': true, 'currency': 'USD', 'dfp_id': 'div-gpt-ad-1460505748561-0', 'originalCpm': 1.1495, 'originalCurrency': 'USD', 'floorData': {'floorValue': 1.1, 'floorRule': 'banner'}, 'auctionId': '8e0d5245-deb3-406c-96ca-9b609e077ff7', 'responseTimestamp': 1584563606009, 'requestTimestamp': 1584563605743, 'bidder': 'medianet', 'adUnitCode': 'div-gpt-ad-1460505748561-0', 'timeToRespond': 266, 'pbLg': '2.00', 'pbMg': '2.20', 'pbHg': '2.29', 'pbAg': '2.25', 'pbDg': '2.29', 'pbCg': '2.00', 'size': '300x250', 'adserverTargeting': {'hb_bidder': 'medianet', 'hb_adid': '3e6e4bce5c8fb3', 'hb_pb': '2.00', 'hb_size': '300x250', 'hb_source': 'client', 'hb_format': 'banner', 'prebid_test': 1}, 'status': 'rendered', 'params': [{'cid': 'test123', 'crid': '451466393'}]}, {'bidderCode': 'medianet', 'width': 300, 'height': 250, 'adId': '3e6e4bce5c8fb4', 'requestId': '28248b0e6aece2', 'mediaType': 'banner', 'source': 'client', 'ext': {'pvid': 123, 'crid': '321'}, 'no_bid': false, 'cpm': 2.299, 'ad': 'AD_CODE', 'ttl': 180, 'creativeId': 'Test1', 'netRevenue': true, 'currency': 'USD', 'dfp_id': 'div-gpt-ad-1460505748561-0', 'originalCpm': 1.1495, 'originalCurrency': 'USD', 'floorData': {'floorValue': 1.1, 'floorRule': 'banner'}, 'auctionId': '8e0d5245-deb3-406c-96ca-9b609e077ff7', 'responseTimestamp': 1584563606009, 'requestTimestamp': 1584563605743, 'bidder': 'medianet', 'adUnitCode': 'div-gpt-ad-1460505748561-0', 'timeToRespond': 286, 'pbLg': '2.00', 'pbMg': '2.20', 'pbHg': '2.29', 'pbAg': '2.25', 'pbDg': '2.29', 'pbCg': '2.00', 'size': '300x250', 'adserverTargeting': {'hb_bidder': 'medianet', 'hb_adid': '3e6e4bce5c8fb3', 'hb_pb': '2.00', 'hb_size': '300x250', 'hb_source': 'client', 'hb_format': 'banner', 'prebid_test': 1}, 'status': 'rendered', 'params': [{'cid': 'test123', 'crid': '451466393'}]}], BID_RESPONSES: [{'bidderCode': 'medianet', 'width': 300, 'height': 250, 'adId': '3e6e4bce5c8fb3', 'requestId': '28248b0e6aece2', 'mediaType': 'banner', 'source': 'client', 'ext': {'pvid': 123, 'crid': '321'}, 'no_bid': false, 'cpm': 2.299, 'ad': 'AD_CODE', 'ttl': 180, 'creativeId': 'Test1', 'netRevenue': true, 'currency': 'USD', 'dfp_id': 'div-gpt-ad-1460505748561-0', 'originalCpm': 1.1495, 'originalCurrency': 'USD', 'floorData': {'floorValue': 1.1, 'floorRule': 'banner'}, 'auctionId': '8e0d5245-deb3-406c-96ca-9b609e077ff7', 'responseTimestamp': 1584563606009, 'requestTimestamp': 1584563605743, 'bidder': 'medianet', 'adUnitCode': 'div-gpt-ad-1460505748561-0', 'timeToRespond': 266, 'pbLg': '2.00', 'pbMg': '2.20', 'pbHg': '2.29', 'pbAg': '2.25', 'pbDg': '2.29', 'pbCg': '2.00', 'size': '300x250', 'adserverTargeting': {'hb_bidder': 'medianet', 'hb_adid': '3e6e4bce5c8fb3', 'hb_pb': '2.00', 'hb_size': '300x250', 'hb_source': 'client', 'hb_format': 'banner', 'prebid_test': 1}, 'params': [{'cid': 'test123', 'crid': '451466393'}]}, {'bidderCode': 'appnexus', 'width': 300, 'height': 250, 'adId': '3e6e4bce5c8fb4', 'requestId': '28248b0e6aecd5', 'mediaType': 'banner', 'source': 'client', 'ext': {'pvid': 123, 'crid': '321'}, 'no_bid': false, 'cpm': 1.299, 'ad': 'AD_CODE', 'ttl': 180, 'creativeId': 'Test1', 'netRevenue': true, 'currency': 'USD', 'dfp_id': 'div-gpt-ad-1460505748561-0', 'originalCpm': 1.1495, 'originalCurrency': 'USD', 'floorData': {'floorValue': 1.1, 'floorRule': 'banner'}, 'auctionId': '8e0d5245-deb3-406c-96ca-9b609e077ff7', 'responseTimestamp': 1584563606009, 'requestTimestamp': 1584563605743, 'bidder': 'medianet', 'adUnitCode': 'div-gpt-ad-1460505748561-0', 'timeToRespond': 278, 'pbLg': '1.00', 'pbMg': '1.20', 'pbHg': '1.29', 'pbAg': '1.25', 'pbDg': '1.29', 'pbCg': '1.00', 'size': '300x250', 'adserverTargeting': {'hb_bidder': 'appnexus', 'hb_adid': '3e6e4bce5c8fb4', 'hb_pb': '1.00', 'hb_size': '300x250', 'hb_source': 'client', 'hb_format': 'banner', 'prebid_test': 1}, 'params': [{'publisherId': 'test123', 'placementId': '451466393'}]}], - BID_REQUESTS: [{'bidderCode': 'medianet', 'auctionId': '8e0d5245-deb3-406c-96ca-9b609e077ff7', 'bids': [{'bidder': 'medianet', 'params': {'cid': 'TEST_CID', 'crid': '451466393'}, 'mediaTypes': {'banner': {'sizes': [[300, 250]], 'ext': ['asdads']}}, 'adUnitCode': 'div-gpt-ad-1460505748561-0', 'sizes': [[300, 250]], 'bidId': '28248b0e6aece2', 'auctionId': '8e0d5245-deb3-406c-96ca-9b609e077ff7', 'src': 'client'}], 'auctionStart': 1584563605739, 'timeout': 6000, 'uspConsent': '1YY', 'start': 1584563605743}, {'bidderCode': 'appnexus', 'auctionId': '8e0d5245-deb3-406c-96ca-9b609e077ff7', 'bids': [{'bidder': 'appnexus', 'params': {'publisherId': 'TEST_CID', 'placementId': '451466393'}, 'mediaTypes': {'banner': {'sizes': [[300, 250]], 'ext': ['asdads']}}, 'adUnitCode': 'div-gpt-ad-1460505748561-0', 'sizes': [[300, 250]], 'bidId': '28248b0e6aecd5', 'auctionId': '8e0d5245-deb3-406c-96ca-9b609e077ff7', 'src': 'client'}], 'auctionStart': 1584563605739, 'timeout': 6000, 'uspConsent': '1YY', 'start': 1584563605743}] + BID_REQUESTS: [{'bidderCode': 'medianet', 'auctionId': '8e0d5245-deb3-406c-96ca-9b609e077ff7', 'bids': [{'bidder': 'medianet', 'params': {'cid': 'TEST_CID', 'crid': '451466393'}, 'mediaTypes': {'banner': {'sizes': [[300, 250]], 'ext': ['asdads']}}, 'adUnitCode': 'div-gpt-ad-1460505748561-0', 'sizes': [[300, 250]], 'bidId': '28248b0e6aece2', 'auctionId': '8e0d5245-deb3-406c-96ca-9b609e077ff7', 'src': 'client'}], 'auctionStart': 1584563605739, 'timeout': 6000, 'uspConsent': '1YY', 'start': 1584563605743}, {'bidderCode': 'appnexus', 'auctionId': '8e0d5245-deb3-406c-96ca-9b609e077ff7', 'bids': [{'bidder': 'appnexus', 'params': {'publisherId': 'TEST_CID', 'placementId': '451466393'}, 'mediaTypes': {'banner': {'sizes': [[300, 250]], 'ext': ['asdads']}}, 'adUnitCode': 'div-gpt-ad-1460505748561-0', 'sizes': [[300, 250]], 'bidId': '28248b0e6aecd5', 'auctionId': '8e0d5245-deb3-406c-96ca-9b609e077ff7', 'src': 'client'}], 'auctionStart': 1584563605739, 'timeout': 6000, 'uspConsent': '1YY', 'start': 1584563605743}], + MULTI_BID_RESPONSES: [{'bidderCode': 'medianet', 'width': 300, 'height': 250, 'adId': '3e6e4bce5c8fb3', 'requestId': '28248b0e6aece2', 'mediaType': 'banner', 'source': 'client', 'ext': {'pvid': 123, 'crid': '321'}, 'no_bid': false, 'cpm': 2.299, 'ad': 'AD_CODE', 'ttl': 180, 'creativeId': 'Test1', 'netRevenue': true, 'currency': 'USD', 'dfp_id': 'div-gpt-ad-1460505748561-0', 'originalCpm': 1.1495, 'originalCurrency': 'USD', 'floorData': {'floorValue': 1.10, 'floorRule': 'banner'}, 'auctionId': '8e0d5245-deb3-406c-96ca-9b609e077ff7', 'responseTimestamp': 1584563606009, 'requestTimestamp': 1584563605743, 'bidder': 'medianet', 'adUnitCode': 'div-gpt-ad-1460505748561-0', 'timeToRespond': 266, 'pbLg': '2.00', 'pbMg': '2.20', 'pbHg': '2.29', 'pbAg': '2.25', 'pbDg': '2.29', 'pbCg': '2.00', 'size': '300x250', 'adserverTargeting': {'hb_bidder': 'medianet', 'hb_adid': '3e6e4bce5c8fb3', 'hb_pb': '2.00', 'hb_size': '300x250', 'hb_source': 'client', 'hb_format': 'banner', 'prebid_test': 1}, 'status': 'rendered', 'params': [{'cid': 'test123', 'crid': '451466393'}]}, {'bidderCode': 'bidA2', 'originalBidder': 'medianet', 'width': 300, 'height': 250, 'adId': '3e6e4bce5c8fb4', 'requestId': '28248b0e6aebecc', 'originalRequestId': '28248b0e6aece2', 'mediaType': 'banner', 'source': 'client', 'ext': {'pvid': 123, 'crid': '321'}, 'no_bid': false, 'cpm': 3.299, 'ad': 'AD_CODE', 'ttl': 180, 'creativeId': 'Test1', 'netRevenue': true, 'currency': 'USD', 'dfp_id': 'div-gpt-ad-1460505748561-0', 'originalCpm': 3.1495, 'originalCurrency': 'USD', 'floorData': {'floorValue': 1.10, 'floorRule': 'banner'}, 'auctionId': '8e0d5245-deb3-406c-96ca-9b609e077ff7', 'responseTimestamp': 1584563606009, 'requestTimestamp': 1584563605743, 'bidder': 'medianet', 'adUnitCode': 'div-gpt-ad-1460505748561-0', 'timeToRespond': 266, 'pbLg': '3.00', 'pbMg': '3.20', 'pbHg': '3.29', 'pbAg': '3.25', 'pbDg': '3.29', 'pbCg': '3.00', 'size': '300x250', 'adserverTargeting': {'hb_bidder': 'medianet', 'hb_adid': '3e6e4bce5c8fb3', 'hb_pb': '3.00', 'hb_size': '300x250', 'hb_source': 'client', 'hb_format': 'banner', 'prebid_test': 1}, 'status': 'rendered', 'params': [{'cid': 'test123', 'crid': '451466393'}]}] }; function performAuctionWithFloorConfig() { @@ -102,6 +104,14 @@ function performStandardAuctionMultiBidResponseNoWin() { events.emit(SET_TARGETING, MOCK.SET_TARGETING); } +function performMultiBidAuction() { + events.emit(AUCTION_INIT, Object.assign({}, MOCK.AUCTION_INIT, {adUnits: MOCK.Ad_Units})); + events.emit(BID_REQUESTED, MOCK.BID_REQUESTED); + MOCK.MULTI_BID_RESPONSES.forEach(bidResp => events.emit(BID_RESPONSE, bidResp)); + events.emit(AUCTION_END, MOCK.AUCTION_END); + events.emit(SET_TARGETING, MOCK.SET_TARGETING); +} + function getQueryData(url, decode = false) { const queryArgs = url.split('?')[1].split('&'); return queryArgs.reduce((data, arg) => { @@ -309,6 +319,13 @@ describe('Media.net Analytics Adapter', function() { expect(winningBid.adid).equals('3e6e4bce5c8fb3'); }); + it('should pick winning bid if multibids with same request id and same time to respond', function() { + performStandardAuctionMultiBidWithSameRequestId(MOCK.BIDS_SAME_REQ_DIFF_CPM_SAME_TIME); + let winningBid = medianetAnalytics.getlogsQueue().map((log) => getQueryData(log)).filter(log => log.winner)[0]; + expect(winningBid.adid).equals('3e6e4bce5c8fb3'); + medianetAnalytics.clearlogsQueue(); + }); + it('should pick winning bid if multibids with same request id and equal cpm', function() { performStandardAuctionMultiBidWithSameRequestId(MOCK.BIDS_SAME_REQ_EQUAL_CPM); let winningBid = medianetAnalytics.getlogsQueue().map((log) => getQueryData(log)).filter(log => log.winner)[0]; @@ -348,5 +365,14 @@ describe('Media.net Analytics Adapter', function() { expect(errors.length).equals(1); expect(errors[0].event).equals(ERROR_WINNING_BID_ABSENT); }); + + it('can handle multi bid module', function () { + performMultiBidAuction(); + const queue = medianetAnalytics.getlogsQueue(); + expect(queue.length).equals(1); + const multiBidLog = queue.map((log) => getQueryData(log, true))[0]; + expect(multiBidLog.pvnm).to.have.ordered.members(['-2', 'medianet', 'medianet']); + expect(multiBidLog.status).to.have.ordered.members(['1', '1', '1']); + }) }); }); diff --git a/test/spec/modules/medianetRtdProvider_spec.js b/test/spec/modules/medianetRtdProvider_spec.js index 7d73ecd5d44..f9d4ef7c2cf 100644 --- a/test/spec/modules/medianetRtdProvider_spec.js +++ b/test/spec/modules/medianetRtdProvider_spec.js @@ -66,12 +66,12 @@ describe('medianet realtime module', function () { describe('getTargeting should work correctly', function () { it('should return empty if not loaded', function () { window.mnjs.loaded = false; - assert.deepEqual(medianetRTD.medianetRtdModule.getTargetingData([]), {}); + assert.deepEqual(medianetRTD.medianetRtdModule.getTargetingData([], {}, {}, {}), {}); }); it('should return ad unit codes when ad units are present', function () { const adUnitCodes = ['code1', 'code2']; - assert.deepEqual(medianetRTD.medianetRtdModule.getTargetingData(adUnitCodes), { + assert.deepEqual(medianetRTD.medianetRtdModule.getTargetingData(adUnitCodes, {}, {}, {}), { code1: {'mnadc': 'code1'}, code2: {'mnadc': 'code2'}, }); @@ -79,7 +79,7 @@ describe('medianet realtime module', function () { it('should call mnjs.getTargetingData if loaded', function () { window.mnjs.loaded = true; - medianetRTD.medianetRtdModule.getTargetingData([]); + medianetRTD.medianetRtdModule.getTargetingData([], {}, {}, {}); assert.equal(getTargetingDataSpy.called, true); }); }); diff --git a/test/spec/modules/mgidBidAdapter_spec.js b/test/spec/modules/mgidBidAdapter_spec.js index c79cad6245d..28530c4c4b4 100644 --- a/test/spec/modules/mgidBidAdapter_spec.js +++ b/test/spec/modules/mgidBidAdapter_spec.js @@ -1,7 +1,9 @@ -import {assert, expect} from 'chai'; +import {expect} from 'chai'; import { spec, storage } from 'modules/mgidBidAdapter.js'; import { version } from 'package.json'; import * as utils from '../../../src/utils.js'; +import {USERSYNC_DEFAULT_CONFIG} from '../../../src/userSync'; +import {config} from '../../../src/config'; describe('Mgid bid adapter', function () { let sandbox; @@ -21,10 +23,10 @@ describe('Mgid bid adapter', function () { const ua = navigator.userAgent; const screenHeight = screen.height; const screenWidth = screen.width; - const dnt = (navigator.doNotTrack == 'yes' || navigator.doNotTrack == '1' || navigator.msDoNotTrack == '1') ? 1 : 0; + const dnt = (navigator.doNotTrack === 'yes' || navigator.doNotTrack === '1' || navigator.msDoNotTrack === '1') ? 1 : 0; const language = navigator.language ? 'language' : 'userLanguage'; let lang = navigator[language].split('-')[0]; - if (lang.length != 2 && lang.length != 3) { + if (lang.length !== 2 && lang.length !== 3) { lang = ''; } const secure = window.location.protocol === 'https:' ? 1 : 0; @@ -36,7 +38,7 @@ describe('Mgid bid adapter', function () { }); describe('isBidRequestValid', function () { - let bid = { + let sbid = { 'adUnitCode': 'div', 'bidder': 'mgid', 'params': { @@ -46,26 +48,26 @@ describe('Mgid bid adapter', function () { }; it('should not accept bid without required params', function () { - let isValid = spec.isBidRequestValid(bid); + let isValid = spec.isBidRequestValid(sbid); expect(isValid).to.equal(false); }); it('should return false when params are not passed', function () { - let bid = Object.assign({}, bid); + let bid = Object.assign({}, sbid); delete bid.params; bid.params = {}; expect(spec.isBidRequestValid(bid)).to.equal(false); }); it('should return false when valid params are not passed', function () { - let bid = Object.assign({}, bid); + let bid = Object.assign({}, sbid); delete bid.params; bid.params = {accountId: '', placementId: ''}; expect(spec.isBidRequestValid(bid)).to.equal(false); }); it('should return false when valid params are not passed', function () { - let bid = Object.assign({}, bid); + let bid = Object.assign({}, sbid); delete bid.params; bid.adUnitCode = ''; bid.mediaTypes = { @@ -78,7 +80,7 @@ describe('Mgid bid adapter', function () { }); it('should return false when adUnitCode not passed', function () { - let bid = Object.assign({}, bid); + let bid = Object.assign({}, sbid); delete bid.params; bid.adUnitCode = ''; bid.mediaTypes = { @@ -91,7 +93,7 @@ describe('Mgid bid adapter', function () { }); it('should return true when valid params are passed as nums', function () { - let bid = Object.assign({}, bid); + let bid = Object.assign({}, sbid); delete bid.params; bid.adUnitCode = 'div'; bid.mediaTypes = { @@ -104,7 +106,7 @@ describe('Mgid bid adapter', function () { }); it('should return false when valid params are not passed', function () { - let bid = Object.assign({}, bid); + let bid = Object.assign({}, sbid); delete bid.params; bid.mediaTypes = { native: { @@ -116,14 +118,14 @@ describe('Mgid bid adapter', function () { }); it('should return false when valid mediaTypes are not passed', function () { - let bid = Object.assign({}, bid); + let bid = Object.assign({}, sbid); delete bid.params; bid.params = {accountId: '1', placementId: '1'}; expect(spec.isBidRequestValid(bid)).to.equal(false); }); it('should return false when valid mediaTypes.banner are not passed', function () { - let bid = Object.assign({}, bid); + let bid = Object.assign({}, sbid); delete bid.params; bid.params = {accountId: '1', placementId: '1'}; bid.mediaTypes = { @@ -132,7 +134,7 @@ describe('Mgid bid adapter', function () { }); it('should return false when valid mediaTypes.banner.sizes are not passed', function () { - let bid = Object.assign({}, bid); + let bid = Object.assign({}, sbid); delete bid.params; bid.params = {accountId: '1', placementId: '1'}; bid.mediaTypes = { @@ -142,7 +144,7 @@ describe('Mgid bid adapter', function () { }); it('should return false when valid mediaTypes.banner.sizes are not valid', function () { - let bid = Object.assign({}, bid); + let bid = Object.assign({}, sbid); delete bid.params; bid.params = {accountId: '1', placementId: '1'}; bid.mediaTypes = { @@ -152,7 +154,7 @@ describe('Mgid bid adapter', function () { }); it('should return true when valid params are passed as strings', function () { - let bid = Object.assign({}, bid); + let bid = Object.assign({}, sbid); delete bid.params; bid.adUnitCode = 'div'; bid.params = {accountId: '1', placementId: '1'}; @@ -165,7 +167,7 @@ describe('Mgid bid adapter', function () { }); it('should return false when valid mediaTypes.native is not object', function () { - let bid = Object.assign({}, bid); + let bid = Object.assign({}, sbid); bid.params = {accountId: '1', placementId: '1'}; bid.mediaTypes = { native: [] @@ -174,7 +176,7 @@ describe('Mgid bid adapter', function () { }); it('should return false when mediaTypes.native is empty object', function () { - let bid = Object.assign({}, bid); + let bid = Object.assign({}, sbid); delete bid.params; bid.params = {accountId: '1', placementId: '1'}; bid.mediaTypes = { @@ -184,7 +186,7 @@ describe('Mgid bid adapter', function () { }); it('should return false when mediaTypes.native is invalid object', function () { - let bid = Object.assign({}, bid); + let bid = Object.assign({}, sbid); delete bid.params; bid.params = {accountId: '1', placementId: '1'}; bid.mediaTypes = { @@ -198,7 +200,7 @@ describe('Mgid bid adapter', function () { }); it('should return false when mediaTypes.native has unsupported required asset', function () { - let bid = Object.assign({}, bid); + let bid = Object.assign({}, sbid); bid.params = {accountId: '2', placementId: '1'}; bid.mediaTypes = { native: { @@ -217,7 +219,7 @@ describe('Mgid bid adapter', function () { }); it('should return true when mediaTypes.native all assets needed', function () { - let bid = Object.assign({}, bid); + let bid = Object.assign({}, sbid); bid.adUnitCode = 'div'; bid.params = {accountId: '2', placementId: '1'}; bid.mediaTypes = { @@ -237,7 +239,7 @@ describe('Mgid bid adapter', function () { }); describe('override defaults', function () { - let bid = { + let sbid = { bidder: 'mgid', params: { accountId: '1', @@ -245,7 +247,7 @@ describe('Mgid bid adapter', function () { }, }; it('should return object', function () { - let bid = Object.assign({}, bid); + let bid = Object.assign({}, sbid); bid.mediaTypes = { banner: { sizes: [[300, 250]] @@ -257,7 +259,7 @@ describe('Mgid bid adapter', function () { }); it('should return overwrite default bidurl', function () { - let bid = Object.assign({}, bid); + let bid = Object.assign({}, sbid); bid.params = { bidUrl: 'https://newbidurl.com/', accountId: '1', @@ -273,7 +275,7 @@ describe('Mgid bid adapter', function () { expect(request.url).to.include('https://newbidurl.com/1'); }); it('should return overwrite default bidFloor', function () { - let bid = Object.assign({}, bid); + let bid = Object.assign({}, sbid); bid.params = { bidFloor: 1.1, accountId: '1', @@ -294,7 +296,7 @@ describe('Mgid bid adapter', function () { expect(data.imp[0].bidfloor).to.deep.equal(1.1); }); it('should return overwrite default currency', function () { - let bid = Object.assign({}, bid); + let bid = Object.assign({}, sbid); bid.params = { cur: 'GBP', accountId: '1', @@ -323,6 +325,9 @@ describe('Mgid bid adapter', function () { placementId: '2', }, }; + afterEach(function () { + config.setConfig({coppa: undefined}) + }) it('should return undefined if no validBidRequests passed', function () { expect(spec.buildRequests([])).to.be.undefined; @@ -344,6 +349,7 @@ describe('Mgid bid adapter', function () { getDataFromLocalStorageStub.restore(); }); it('should proper handle gdpr', function () { + config.setConfig({coppa: 1}) let bid = Object.assign({}, abid); bid.mediaTypes = { banner: { @@ -351,12 +357,72 @@ describe('Mgid bid adapter', function () { } }; let bidRequests = [bid]; - const request = spec.buildRequests(bidRequests, {gdprConsent: {consentString: 'gdpr', gdprApplies: true}}); + const request = spec.buildRequests(bidRequests, {gdprConsent: {consentString: 'gdpr', gdprApplies: true}, uspConsent: 'usp', gppConsent: {gppString: 'gpp'}}); expect(request.url).deep.equal('https://prebid.mgid.com/prebid/1'); expect(request.method).deep.equal('POST'); const data = JSON.parse(request.data); expect(data.user).deep.equal({ext: {consent: 'gdpr'}}); - expect(data.regs).deep.equal({ext: {gdpr: 1}}); + expect(data.regs).deep.equal({ext: {gdpr: 1, us_privacy: 'usp'}, gpp: 'gpp', coppa: 1}); + }); + it('should handle refererInfo', function () { + let bid = Object.assign({}, abid); + bid.mediaTypes = { + banner: { + sizes: [[300, 250]] + } + }; + let bidRequests = [bid]; + const domain = 'site.com' + const page = `http://${domain}/site.html` + const ref = 'http://ref.com/ref.html' + const request = spec.buildRequests(bidRequests, {refererInfo: {page, ref}}); + expect(request.url).deep.equal('https://prebid.mgid.com/prebid/1'); + expect(request.method).deep.equal('POST'); + const data = JSON.parse(request.data); + expect(data.site.domain).to.deep.equal(domain); + expect(data.site.page).to.deep.equal(page); + expect(data.site.ref).to.deep.equal(ref); + }); + it('should handle schain', function () { + let bid = Object.assign({}, abid); + bid.mediaTypes = { + banner: { + sizes: [[300, 250]] + } + }; + bid.schain = ['schain1', 'schain2']; + let bidRequests = [bid]; + const request = spec.buildRequests(bidRequests); + const data = JSON.parse(request.data); + expect(data.source).to.deep.equal({ext: {schain: bid.schain}}); + }); + it('should handle userId', function () { + let bid = Object.assign({}, abid); + bid.mediaTypes = { + banner: { + sizes: [[300, 250]] + } + }; + let bidRequests = [bid]; + const bidderRequest = {userId: 'userid'}; + const request = spec.buildRequests(bidRequests, bidderRequest); + expect(request.url).deep.equal('https://prebid.mgid.com/prebid/1'); + expect(request.method).deep.equal('POST'); + const data = JSON.parse(request.data); + expect(data.user.id).to.deep.equal(bidderRequest.userId); + }); + it('should handle eids', function () { + let bid = Object.assign({}, abid); + bid.mediaTypes = { + banner: { + sizes: [[300, 250]] + } + }; + bid.userIdAsEids = ['eid1', 'eid2'] + let bidRequests = [bid]; + const request = spec.buildRequests(bidRequests); + const data = JSON.parse(request.data); + expect(data.user.ext.eids).to.deep.equal(bid.userIdAsEids); }); it('should return proper banner imp', function () { let bid = Object.assign({}, abid); @@ -386,7 +452,7 @@ describe('Mgid bid adapter', function () { expect(request).to.deep.equal({ 'method': 'POST', 'url': 'https://prebid.mgid.com/prebid/1', - 'data': '{"site":{"domain":"' + domain + '","page":"' + page + '"},"cur":["USD"],"geo":{"utcoffset":' + utcOffset + '},"device":{"ua":"' + ua + '","js":1,"dnt":' + dnt + ',"h":' + screenHeight + ',"w":' + screenWidth + ',"language":"' + lang + '"},"ext":{"mgid_ver":"' + mgid_ver + '","prebid_ver":"' + version + '"},"imp":[{"tagid":"2/div","secure":' + secure + ',"banner":{"w":300,"h":250}}]}', + 'data': `{"site":{"domain":"${domain}","page":"${page}"},"cur":["USD"],"geo":{"utcoffset":${utcOffset}},"device":{"ua":"${ua}","js":1,"dnt":${dnt},"h":${screenHeight},"w":${screenWidth},"language":"${lang}"},"ext":{"mgid_ver":"${mgid_ver}","prebid_ver":"${version}"},"imp":[{"tagid":"2/div","secure":${secure},"banner":{"w":300,"h":250}}],"tmax":3000}`, }); }); it('should not return native imp if minimum asset list not requested', function () { @@ -435,7 +501,7 @@ describe('Mgid bid adapter', function () { expect(request).to.deep.equal({ 'method': 'POST', 'url': 'https://prebid.mgid.com/prebid/1', - 'data': '{"site":{"domain":"' + domain + '","page":"' + page + '"},"cur":["USD"],"geo":{"utcoffset":' + utcOffset + '},"device":{"ua":"' + ua + '","js":1,"dnt":' + dnt + ',"h":' + screenHeight + ',"w":' + screenWidth + ',"language":"' + lang + '"},"ext":{"mgid_ver":"' + mgid_ver + '","prebid_ver":"' + version + '"},"imp":[{"tagid":"2/div","secure":' + secure + ',"native":{"request":{"plcmtcnt":1,"assets":[{"id":1,"required":1,"title":{"len":80}},{"id":2,"required":0,"img":{"type":3,"w":80,"h":80}},{"id":11,"required":0,"data":{"type":1}}]}}}]}', + 'data': `{"site":{"domain":"${domain}","page":"${page}"},"cur":["USD"],"geo":{"utcoffset":${utcOffset}},"device":{"ua":"${ua}","js":1,"dnt":${dnt},"h":${screenHeight},"w":${screenWidth},"language":"${lang}"},"ext":{"mgid_ver":"${mgid_ver}","prebid_ver":"${version}"},"imp":[{"tagid":"2/div","secure":${secure},"native":{"request":{"plcmtcnt":1,"assets":[{"id":1,"required":1,"title":{"len":80}},{"id":2,"required":0,"img":{"type":3,"w":80,"h":80}},{"id":11,"required":0,"data":{"type":1}}]}}}],"tmax":3000}`, }); }); it('should return proper native imp with image altered', function () { @@ -472,7 +538,7 @@ describe('Mgid bid adapter', function () { expect(request).to.deep.equal({ 'method': 'POST', 'url': 'https://prebid.mgid.com/prebid/1', - 'data': '{"site":{"domain":"' + domain + '","page":"' + page + '"},"cur":["USD"],"geo":{"utcoffset":' + utcOffset + '},"device":{"ua":"' + ua + '","js":1,"dnt":' + dnt + ',"h":' + screenHeight + ',"w":' + screenWidth + ',"language":"' + lang + '"},"ext":{"mgid_ver":"' + mgid_ver + '","prebid_ver":"' + version + '"},"imp":[{"tagid":"2/div","secure":' + secure + ',"native":{"request":{"plcmtcnt":1,"assets":[{"id":1,"required":1,"title":{"len":80}},{"id":2,"required":1,"img":{"type":3,"w":492,"h":328,"wmin":50,"hmin":50}},{"id":3,"required":0,"img":{"type":1,"w":50,"h":50}},{"id":11,"required":0,"data":{"type":1}}]}}}]}', + 'data': `{"site":{"domain":"${domain}","page":"${page}"},"cur":["USD"],"geo":{"utcoffset":${utcOffset}},"device":{"ua":"${ua}","js":1,"dnt":${dnt},"h":${screenHeight},"w":${screenWidth},"language":"${lang}"},"ext":{"mgid_ver":"${mgid_ver}","prebid_ver":"${version}"},"imp":[{"tagid":"2/div","secure":${secure},"native":{"request":{"plcmtcnt":1,"assets":[{"id":1,"required":1,"title":{"len":80}},{"id":2,"required":1,"img":{"type":3,"w":492,"h":328,"wmin":50,"hmin":50}},{"id":3,"required":0,"img":{"type":1,"w":50,"h":50}},{"id":11,"required":0,"data":{"type":1}}]}}}],"tmax":3000}`, }); }); it('should return proper native imp with sponsoredBy', function () { @@ -508,7 +574,7 @@ describe('Mgid bid adapter', function () { expect(request).to.deep.equal({ 'method': 'POST', 'url': 'https://prebid.mgid.com/prebid/1', - 'data': '{"site":{"domain":"' + domain + '","page":"' + page + '"},"cur":["USD"],"geo":{"utcoffset":' + utcOffset + '},"device":{"ua":"' + ua + '","js":1,"dnt":' + dnt + ',"h":' + screenHeight + ',"w":' + screenWidth + ',"language":"' + lang + '"},"ext":{"mgid_ver":"' + mgid_ver + '","prebid_ver":"' + version + '"},"imp":[{"tagid":"2/div","secure":' + secure + ',"native":{"request":{"plcmtcnt":1,"assets":[{"id":1,"required":1,"title":{"len":80}},{"id":2,"required":0,"img":{"type":3,"w":80,"h":80}},{"id":4,"required":0,"data":{"type":1}}]}}}]}', + 'data': `{"site":{"domain":"${domain}","page":"${page}"},"cur":["USD"],"geo":{"utcoffset":${utcOffset}},"device":{"ua":"${ua}","js":1,"dnt":${dnt},"h":${screenHeight},"w":${screenWidth},"language":"${lang}"},"ext":{"mgid_ver":"${mgid_ver}","prebid_ver":"${version}"},"imp":[{"tagid":"2/div","secure":${secure},"native":{"request":{"plcmtcnt":1,"assets":[{"id":1,"required":1,"title":{"len":80}},{"id":2,"required":0,"img":{"type":3,"w":80,"h":80}},{"id":4,"required":0,"data":{"type":1}}]}}}],"tmax":3000}`, }); }); it('should return proper banner request', function () { @@ -542,7 +608,7 @@ describe('Mgid bid adapter', function () { expect(request).to.deep.equal({ 'method': 'POST', 'url': 'https://prebid.mgid.com/prebid/1', - 'data': '{"site":{"domain":"' + domain + '","page":"' + page + '"},"cur":["USD"],"geo":{"utcoffset":' + utcOffset + '},"device":{"ua":"' + ua + '","js":1,"dnt":' + dnt + ',"h":' + screenHeight + ',"w":' + screenWidth + ',"language":"' + lang + '"},"ext":{"mgid_ver":"' + mgid_ver + '","prebid_ver":"' + version + '"},"imp":[{"tagid":"2/div","secure":' + secure + ',"banner":{"w":300,"h":600,"format":[{"w":300,"h":600},{"w":300,"h":250}],"pos":1}}]}', + 'data': `{"site":{"domain":"${domain}","page":"${page}"},"cur":["USD"],"geo":{"utcoffset":${utcOffset}},"device":{"ua":"${ua}","js":1,"dnt":${dnt},"h":${screenHeight},"w":${screenWidth},"language":"${lang}"},"ext":{"mgid_ver":"${mgid_ver}","prebid_ver":"${version}"},"imp":[{"tagid":"2/div","secure":${secure},"banner":{"w":300,"h":600,"format":[{"w":300,"h":600},{"w":300,"h":250}],"pos":1}}],"tmax":3000}`, }); }); it('should proper handle ortb2 data', function () { @@ -555,7 +621,14 @@ describe('Mgid bid adapter', function () { let bidRequests = [bid]; let bidderRequest = { + gdprConsent: { + consentString: 'consent1', + gdprApplies: false, + }, ortb2: { + bcat: ['bcat1', 'bcat2'], + badv: ['badv1.com', 'badv2.com'], + wlang: ['l1', 'l2'], site: { content: { data: [{ @@ -571,6 +644,9 @@ describe('Mgid bid adapter', function () { } }, user: { + ext: { + consent: 'consent2 ', + }, data: [{ name: 'mgid.com', ext: { @@ -581,6 +657,11 @@ describe('Mgid bid adapter', function () { {'id': '987'}, ], }] + }, + regs: { + ext: { + gdpr: 1, + } } } }; @@ -589,7 +670,13 @@ describe('Mgid bid adapter', function () { expect(request.url).deep.equal('https://prebid.mgid.com/prebid/1'); expect(request.method).deep.equal('POST'); const data = JSON.parse(request.data); - expect(data.ext).deep.include(bidderRequest.ortb2); + expect(data.bcat).deep.equal(bidderRequest.ortb2.bcat); + expect(data.badv).deep.equal(bidderRequest.ortb2.badv); + expect(data.wlang).deep.equal(bidderRequest.ortb2.wlang); + expect(data.site.content).deep.equal(bidderRequest.ortb2.site.content); + expect(data.regs).deep.equal(bidderRequest.ortb2.regs); + expect(data.user.data).deep.equal(bidderRequest.ortb2.user.data); + expect(data.user.ext).deep.equal(bidderRequest.ortb2.user.ext); }); }); @@ -727,8 +814,69 @@ describe('Mgid bid adapter', function () { }); describe('getUserSyncs', function () { - it('should do nothing on getUserSyncs', function () { - spec.getUserSyncs() + afterEach(function() { + config.setConfig({userSync: {syncsPerBidder: USERSYNC_DEFAULT_CONFIG.syncsPerBidder}}); + }); + it('should do nothing on getUserSyncs without inputs', function () { + expect(spec.getUserSyncs()).to.equal(undefined) + }); + it('should return frame object with empty consents', function () { + const sync = spec.getUserSyncs({iframeEnabled: true}) + expect(sync).to.have.length(1) + expect(sync[0]).to.have.property('type', 'iframe') + expect(sync[0]).to.have.property('url').match(/https:\/\/cm\.mgid\.com\/i\.html\?cbuster=\d+&consentData=&gdprApplies=0/) + }); + it('should return frame object with gdpr consent', function () { + const sync = spec.getUserSyncs({iframeEnabled: true}, undefined, {consentString: 'consent', gdprApplies: true}) + expect(sync).to.have.length(1) + expect(sync[0]).to.have.property('type', 'iframe') + expect(sync[0]).to.have.property('url').match(/https:\/\/cm\.mgid\.com\/i\.html\?cbuster=\d+&consentData=consent&gdprApplies=1/) + }); + it('should return frame object with gdpr + usp', function () { + const sync = spec.getUserSyncs({iframeEnabled: true}, undefined, {consentString: 'consent1', gdprApplies: true}, {'consentString': 'consent2'}) + expect(sync).to.have.length(1) + expect(sync[0]).to.have.property('type', 'iframe') + expect(sync[0]).to.have.property('url').match(/https:\/\/cm\.mgid\.com\/i\.html\?cbuster=\d+&consentData=consent1&gdprApplies=1&uspString=consent2/) + }); + it('should return img object with gdpr + usp', function () { + config.setConfig({userSync: {syncsPerBidder: undefined}}); + const sync = spec.getUserSyncs({pixelEnabled: true}, undefined, {consentString: 'consent1', gdprApplies: true}, {'consentString': 'consent2'}) + expect(sync).to.have.length(USERSYNC_DEFAULT_CONFIG.syncsPerBidder) + for (let i = 0; i < USERSYNC_DEFAULT_CONFIG.syncsPerBidder; i++) { + expect(sync[i]).to.have.property('type', 'image') + expect(sync[i]).to.have.property('url').match(/https:\/\/cm\.mgid\.com\/i\.gif\?cbuster=\d+&consentData=consent1&gdprApplies=1&uspString=consent2/) + } + }); + it('should return frame object with gdpr + usp', function () { + const sync = spec.getUserSyncs({iframeEnabled: true, pixelEnabled: true}, undefined, {consentString: 'consent1', gdprApplies: true}, {'consentString': 'consent2'}) + expect(sync).to.have.length(1) + expect(sync[0]).to.have.property('type', 'iframe') + expect(sync[0]).to.have.property('url').match(/https:\/\/cm\.mgid\.com\/i\.html\?cbuster=\d+&consentData=consent1&gdprApplies=1&uspString=consent2/) + }); + it('should return img (pixels) objects with gdpr + usp', function () { + const response = [{body: {ext: {cm: ['http://cm.mgid.com/i.gif?cdsp=1111', 'http://cm.mgid.com/i.gif']}}}] + const sync = spec.getUserSyncs({iframeEnabled: false, pixelEnabled: true}, response, {consentString: 'consent1', gdprApplies: true}, {'consentString': 'consent2'}) + expect(sync).to.have.length(2) + expect(sync[0]).to.have.property('type', 'image') + expect(sync[0]).to.have.property('url').match(/http:\/\/cm\.mgid\.com\/i\.gif\?cdsp=1111&cbuster=\d+&consentData=consent1&gdprApplies=1&uspString=consent2/) + expect(sync[1]).to.have.property('type', 'image') + expect(sync[1]).to.have.property('url').match(/http:\/\/cm\.mgid\.com\/i\.gif\?cbuster=\d+&consentData=consent1&gdprApplies=1&uspString=consent2/) + }); + }); + + describe('getUserSyncs with img from ext.cm and gdpr + usp + coppa + gpp', function () { + afterEach(function() { + config.setConfig({coppa: undefined}) + }); + it('should return img (pixels) objects with gdpr + usp + coppa + gpp', function () { + config.setConfig({coppa: 1}); + const response = [{body: {ext: {cm: ['http://cm.mgid.com/i.gif?cdsp=1111', 'http://cm.mgid.com/i.gif']}}}] + const sync = spec.getUserSyncs({iframeEnabled: false, pixelEnabled: true}, response, {consentString: 'consent1', gdprApplies: true}, {'consentString': 'consent2'}, {gppString: 'gpp'}) + expect(sync).to.have.length(2) + expect(sync[0]).to.have.property('type', 'image') + expect(sync[0]).to.have.property('url').match(/http:\/\/cm\.mgid\.com\/i\.gif\?cdsp=1111&cbuster=\d+&consentData=consent1&gdprApplies=1&uspString=consent2&gppString=gpp&coppa=1/) + expect(sync[1]).to.have.property('type', 'image') + expect(sync[1]).to.have.property('url').match(/http:\/\/cm\.mgid\.com\/i\.gif\?cbuster=\d+&consentData=consent1&gdprApplies=1&uspString=consent2&gppString=gpp&coppa=1/) }); }); diff --git a/test/spec/modules/minutemediaplusBidAdapter_spec.js b/test/spec/modules/minutemediaplusBidAdapter_spec.js new file mode 100644 index 00000000000..000ddb2778d --- /dev/null +++ b/test/spec/modules/minutemediaplusBidAdapter_spec.js @@ -0,0 +1,610 @@ +import {expect} from 'chai'; +import { + spec as adapter, + SUPPORTED_ID_SYSTEMS, + createDomain, + hashCode, + extractPID, + extractCID, + extractSubDomain, + getStorageItem, + setStorageItem, + tryParseJSON, + getUniqueDealId, +} from 'modules/minutemediaplusBidAdapter.js'; +import * as utils from 'src/utils.js'; +import {version} from 'package.json'; +import {useFakeTimers} from 'sinon'; +import {BANNER, VIDEO} from '../../../src/mediaTypes'; +import {config} from '../../../src/config'; + +const SUB_DOMAIN = 'exchange'; + +const BID = { + 'bidId': '2d52001cabd527', + 'adUnitCode': 'div-gpt-ad-12345-0', + 'params': { + 'subDomain': SUB_DOMAIN, + 'cId': '59db6b3b4ffaa70004f45cdc', + 'pId': '59ac17c192832d0011283fe3', + 'bidFloor': 0.1, + 'ext': { + 'param1': 'loremipsum', + 'param2': 'dolorsitamet' + } + }, + 'placementCode': 'div-gpt-ad-1460505748561-0', + 'transactionId': 'c881914b-a3b5-4ecf-ad9c-1c2f37c6aabf', + 'sizes': [[300, 250], [300, 600]], + 'bidderRequestId': '1fdb5ff1b6eaa7', + 'auctionId': 'auction_id', + 'bidRequestsCount': 4, + 'bidderRequestsCount': 3, + 'bidderWinsCount': 1, + 'requestId': 'b0777d85-d061-450e-9bc7-260dd54bbb7a', + 'schain': 'a0819c69-005b-41ed-af06-1be1e0aefefc', + 'mediaTypes': [BANNER] +}; + +const VIDEO_BID = { + 'bidId': '2d52001cabd527', + 'adUnitCode': '63550ad1ff6642d368cba59dh5884270560', + 'bidderRequestId': '12a8ae9ada9c13', + 'auctionId': 'auction_id', + 'bidRequestsCount': 4, + 'bidderRequestsCount': 3, + 'bidderWinsCount': 1, + 'transactionId': '56e184c6-bde9-497b-b9b9-cf47a61381ee', + 'schain': 'a0819c69-005b-41ed-af06-1be1e0aefefc', + 'params': { + 'subDomain': SUB_DOMAIN, + 'cId': '635509f7ff6642d368cb9837', + 'pId': '59ac17c192832d0011283fe3', + 'bidFloor': 0.1 + }, + 'sizes': [[545, 307]], + 'mediaTypes': { + 'video': { + 'playerSize': [[545, 307]], + 'context': 'instream', + 'mimes': [ + 'video/mp4', + 'application/javascript' + ], + 'protocols': [2, 3, 5, 6], + 'maxduration': 60, + 'minduration': 0, + 'startdelay': 0, + 'linearity': 1, + 'api': [2], + 'placement': 1 + } + } +} + +const BIDDER_REQUEST = { + 'gdprConsent': { + 'consentString': 'consent_string', + 'gdprApplies': true + }, + 'gppString': 'gpp_string', + 'gppSid': [7], + 'uspConsent': 'consent_string', + 'refererInfo': { + 'page': 'https://www.greatsite.com', + 'ref': 'https://www.somereferrer.com' + }, + 'ortb2': { + 'regs': { + 'gpp': 'gpp_string', + 'gpp_sid': [7] + }, + 'device': { + 'sua': { + 'source': 2, + 'platform': { + 'brand': 'Android', + 'version': ['8', '0', '0'] + }, + 'browsers': [ + {'brand': 'Not_A Brand', 'version': ['99', '0', '0', '0']}, + {'brand': 'Google Chrome', 'version': ['109', '0', '5414', '119']}, + {'brand': 'Chromium', 'version': ['109', '0', '5414', '119']} + ], + 'mobile': 1, + 'model': 'SM-G955U', + 'bitness': '64', + 'architecture': '' + } + } + }, +}; + +const SERVER_RESPONSE = { + body: { + cid: 'testcid123', + results: [{ + 'ad': '', + 'price': 0.8, + 'creativeId': '12610997325162499419', + 'exp': 30, + 'width': 300, + 'height': 250, + 'advertiserDomains': ['securepubads.g.doubleclick.net'], + 'cookies': [{ + 'src': 'https://sync.com', + 'type': 'iframe' + }, { + 'src': 'https://sync.com', + 'type': 'img' + }] + }] + } +}; + +const VIDEO_SERVER_RESPONSE = { + body: { + 'cid': '635509f7ff6642d368cb9837', + 'results': [{ + 'ad': '', + 'advertiserDomains': ['minutemedia-prebid.com'], + 'exp': 60, + 'width': 545, + 'height': 307, + 'mediaType': 'video', + 'creativeId': '12610997325162499419', + 'price': 2, + 'cookies': [] + }] + } +}; + +const REQUEST = { + data: { + width: 300, + height: 250, + bidId: '2d52001cabd527' + } +}; + +function getTopWindowQueryParams() { + try { + const parsedUrl = utils.parseUrl(window.top.document.URL, {decodeSearchAsString: true}); + return parsedUrl.search; + } catch (e) { + return ''; + } +} + +describe('MinuteMediaPlus Bid Adapter', function () { + describe('validtae spec', function () { + it('exists and is a function', function () { + expect(adapter.isBidRequestValid).to.exist.and.to.be.a('function'); + }); + + it('exists and is a function', function () { + expect(adapter.buildRequests).to.exist.and.to.be.a('function'); + }); + + it('exists and is a function', function () { + expect(adapter.interpretResponse).to.exist.and.to.be.a('function'); + }); + + it('exists and is a function', function () { + expect(adapter.getUserSyncs).to.exist.and.to.be.a('function'); + }); + + it('exists and is a string', function () { + expect(adapter.code).to.exist.and.to.be.a('string'); + }); + + it('exists and contains media types', function () { + expect(adapter.supportedMediaTypes).to.exist.and.to.be.an('array').with.length(2); + expect(adapter.supportedMediaTypes).to.contain.members([BANNER, VIDEO]); + }); + }); + + describe('validate bid requests', function () { + it('should require cId', function () { + const isValid = adapter.isBidRequestValid({ + params: { + pId: 'pid' + } + }); + expect(isValid).to.be.false; + }); + + it('should require pId', function () { + const isValid = adapter.isBidRequestValid({ + params: { + cId: 'cid' + } + }); + expect(isValid).to.be.false; + }); + + it('should validate correctly', function () { + const isValid = adapter.isBidRequestValid({ + params: { + cId: 'cid', + pId: 'pid' + } + }); + expect(isValid).to.be.true; + }); + }); + + describe('build requests', function () { + let sandbox; + before(function () { + $$PREBID_GLOBAL$$.bidderSettings = { + mmplus: { + storageAllowed: true + } + }; + sandbox = sinon.sandbox.create(); + sandbox.stub(Date, 'now').returns(1000); + }); + + it('should build video request', function () { + const hashUrl = hashCode(BIDDER_REQUEST.refererInfo.page); + config.setConfig({ + bidderTimeout: 3000 + }); + const requests = adapter.buildRequests([VIDEO_BID], BIDDER_REQUEST); + expect(requests).to.have.length(1); + expect(requests[0]).to.deep.equal({ + method: 'POST', + url: `${createDomain(SUB_DOMAIN)}/prebid/multi/635509f7ff6642d368cb9837`, + data: { + adUnitCode: '63550ad1ff6642d368cba59dh5884270560', + bidFloor: 0.1, + bidId: '2d52001cabd527', + bidderVersion: adapter.version, + bidderRequestId: '12a8ae9ada9c13', + cb: 1000, + gdpr: 1, + gdprConsent: 'consent_string', + usPrivacy: 'consent_string', + gppString: 'gpp_string', + gppSid: [7], + prebidVersion: version, + transactionId: '56e184c6-bde9-497b-b9b9-cf47a61381ee', + auctionId: 'auction_id', + bidRequestsCount: 4, + bidderRequestsCount: 3, + bidderWinsCount: 1, + bidderTimeout: 3000, + publisherId: '59ac17c192832d0011283fe3', + url: 'https%3A%2F%2Fwww.greatsite.com', + referrer: 'https://www.somereferrer.com', + res: `${window.top.screen.width}x${window.top.screen.height}`, + schain: VIDEO_BID.schain, + sizes: ['545x307'], + sua: { + 'source': 2, + 'platform': { + 'brand': 'Android', + 'version': ['8', '0', '0'] + }, + 'browsers': [ + {'brand': 'Not_A Brand', 'version': ['99', '0', '0', '0']}, + {'brand': 'Google Chrome', 'version': ['109', '0', '5414', '119']}, + {'brand': 'Chromium', 'version': ['109', '0', '5414', '119']} + ], + 'mobile': 1, + 'model': 'SM-G955U', + 'bitness': '64', + 'architecture': '' + }, + uniqueDealId: `${hashUrl}_${Date.now().toString()}`, + uqs: getTopWindowQueryParams(), + mediaTypes: { + video: { + api: [2], + context: 'instream', + linearity: 1, + maxduration: 60, + mimes: [ + 'video/mp4', + 'application/javascript' + ], + minduration: 0, + placement: 1, + playerSize: [[545, 307]], + protocols: [2, 3, 5, 6], + startdelay: 0 + } + } + } + }); + }); + + it('should build banner request for each size', function () { + const hashUrl = hashCode(BIDDER_REQUEST.refererInfo.page); + config.setConfig({ + bidderTimeout: 3000 + }); + const requests = adapter.buildRequests([BID], BIDDER_REQUEST); + expect(requests).to.have.length(1); + expect(requests[0]).to.deep.equal({ + method: 'POST', + url: `${createDomain(SUB_DOMAIN)}/prebid/multi/59db6b3b4ffaa70004f45cdc`, + data: { + gdprConsent: 'consent_string', + gdpr: 1, + gppString: 'gpp_string', + gppSid: [7], + usPrivacy: 'consent_string', + transactionId: 'c881914b-a3b5-4ecf-ad9c-1c2f37c6aabf', + auctionId: 'auction_id', + bidRequestsCount: 4, + bidderRequestsCount: 3, + bidderWinsCount: 1, + bidderTimeout: 3000, + bidderRequestId: '1fdb5ff1b6eaa7', + sizes: ['300x250', '300x600'], + sua: { + 'source': 2, + 'platform': { + 'brand': 'Android', + 'version': ['8', '0', '0'] + }, + 'browsers': [ + {'brand': 'Not_A Brand', 'version': ['99', '0', '0', '0']}, + {'brand': 'Google Chrome', 'version': ['109', '0', '5414', '119']}, + {'brand': 'Chromium', 'version': ['109', '0', '5414', '119']} + ], + 'mobile': 1, + 'model': 'SM-G955U', + 'bitness': '64', + 'architecture': '' + }, + url: 'https%3A%2F%2Fwww.greatsite.com', + referrer: 'https://www.somereferrer.com', + cb: 1000, + bidFloor: 0.1, + bidId: '2d52001cabd527', + adUnitCode: 'div-gpt-ad-12345-0', + publisherId: '59ac17c192832d0011283fe3', + uniqueDealId: `${hashUrl}_${Date.now().toString()}`, + bidderVersion: adapter.version, + prebidVersion: version, + schain: BID.schain, + res: `${window.top.screen.width}x${window.top.screen.height}`, + mediaTypes: [BANNER], + uqs: getTopWindowQueryParams(), + 'ext.param1': 'loremipsum', + 'ext.param2': 'dolorsitamet', + } + }); + }); + + after(function () { + $$PREBID_GLOBAL$$.bidderSettings = {}; + sandbox.restore(); + }); + }); + describe('getUserSyncs', function () { + it('should have valid user sync with iframeEnabled', function () { + const result = adapter.getUserSyncs({iframeEnabled: true}, [SERVER_RESPONSE]); + + expect(result).to.deep.equal([{ + type: 'iframe', + url: 'https://sync.minutemedia-prebid.com/api/sync/iframe/?cid=testcid123&gdpr=0&gdpr_consent=&us_privacy=' + }]); + }); + + it('should have valid user sync with cid on response', function () { + const result = adapter.getUserSyncs({iframeEnabled: true}, [SERVER_RESPONSE]); + expect(result).to.deep.equal([{ + type: 'iframe', + url: 'https://sync.minutemedia-prebid.com/api/sync/iframe/?cid=testcid123&gdpr=0&gdpr_consent=&us_privacy=' + }]); + }); + + it('should have valid user sync with pixelEnabled', function () { + const result = adapter.getUserSyncs({pixelEnabled: true}, [SERVER_RESPONSE]); + + expect(result).to.deep.equal([{ + 'url': 'https://sync.minutemedia-prebid.com/api/sync/image/?cid=testcid123&gdpr=0&gdpr_consent=&us_privacy=', + 'type': 'image' + }]); + }) + }); + + describe('interpret response', function () { + it('should return empty array when there is no response', function () { + const responses = adapter.interpretResponse(null); + expect(responses).to.be.empty; + }); + + it('should return empty array when there is no ad', function () { + const responses = adapter.interpretResponse({price: 1, ad: ''}); + expect(responses).to.be.empty; + }); + + it('should return empty array when there is no price', function () { + const responses = adapter.interpretResponse({price: null, ad: 'great ad'}); + expect(responses).to.be.empty; + }); + + it('should return an array of interpreted banner responses', function () { + const responses = adapter.interpretResponse(SERVER_RESPONSE, REQUEST); + expect(responses).to.have.length(1); + expect(responses[0]).to.deep.equal({ + requestId: '2d52001cabd527', + cpm: 0.8, + width: 300, + height: 250, + creativeId: '12610997325162499419', + currency: 'USD', + netRevenue: true, + ttl: 30, + ad: '', + meta: { + advertiserDomains: ['securepubads.g.doubleclick.net'] + } + }); + }); + + it('should return an array of interpreted video responses', function () { + const responses = adapter.interpretResponse(VIDEO_SERVER_RESPONSE, REQUEST); + expect(responses).to.have.length(1); + expect(responses[0]).to.deep.equal({ + requestId: '2d52001cabd527', + cpm: 2, + width: 545, + height: 307, + mediaType: 'video', + creativeId: '12610997325162499419', + currency: 'USD', + netRevenue: true, + ttl: 60, + vastXml: '', + meta: { + advertiserDomains: ['minutemedia-prebid.com'] + } + }); + }); + + it('should take default TTL', function () { + const serverResponse = utils.deepClone(SERVER_RESPONSE); + delete serverResponse.body.results[0].exp; + const responses = adapter.interpretResponse(serverResponse, REQUEST); + expect(responses).to.have.length(1); + expect(responses[0].ttl).to.equal(300); + }); + }); + + describe('user id system', function () { + Object.keys(SUPPORTED_ID_SYSTEMS).forEach((idSystemProvider) => { + const id = Date.now().toString(); + const bid = utils.deepClone(BID); + + const userId = (function () { + switch (idSystemProvider) { + case 'lipb': + return {lipbid: id}; + case 'parrableId': + return {eid: id}; + case 'id5id': + return {uid: id}; + default: + return id; + } + })(); + + bid.userId = { + [idSystemProvider]: userId + }; + + it(`should include 'uid.${idSystemProvider}' in request params`, function () { + const requests = adapter.buildRequests([bid], BIDDER_REQUEST); + expect(requests[0].data[`uid.${idSystemProvider}`]).to.equal(id); + }); + }); + }); + + describe('alternate param names extractors', function () { + it('should return undefined when param not supported', function () { + const cid = extractCID({'c_id': '1'}); + const pid = extractPID({'p_id': '1'}); + const subDomain = extractSubDomain({'sub_domain': 'prebid'}); + expect(cid).to.be.undefined; + expect(pid).to.be.undefined; + expect(subDomain).to.be.undefined; + }); + + it('should return value when param supported', function () { + const cid = extractCID({'cID': '1'}); + const pid = extractPID({'Pid': '2'}); + const subDomain = extractSubDomain({'subDOMAIN': 'prebid'}); + expect(cid).to.be.equal('1'); + expect(pid).to.be.equal('2'); + expect(subDomain).to.be.equal('prebid'); + }); + }); + + describe('unique deal id', function () { + before(function () { + $$PREBID_GLOBAL$$.bidderSettings = { + mmplus: { + storageAllowed: true + } + }; + }); + after(function () { + $$PREBID_GLOBAL$$.bidderSettings = {}; + }); + const key = 'myKey'; + let uniqueDealId; + beforeEach(() => { + uniqueDealId = getUniqueDealId(key, 0); + }) + + it('should get current unique deal id', function (done) { + // waiting some time so `now` will become past + setTimeout(() => { + const current = getUniqueDealId(key); + expect(current).to.be.equal(uniqueDealId); + done(); + }, 200); + }); + + it('should get new unique deal id on expiration', function (done) { + setTimeout(() => { + const current = getUniqueDealId(key, 100); + expect(current).to.not.be.equal(uniqueDealId); + done(); + }, 200) + }); + }); + + describe('storage utils', function () { + before(function () { + $$PREBID_GLOBAL$$.bidderSettings = { + mmplus: { + storageAllowed: true + } + }; + }); + after(function () { + $$PREBID_GLOBAL$$.bidderSettings = {}; + }); + it('should get value from storage with create param', function () { + const now = Date.now(); + const clock = useFakeTimers({ + shouldAdvanceTime: true, + now + }); + setStorageItem('myKey', 2020); + const {value, created} = getStorageItem('myKey'); + expect(created).to.be.equal(now); + expect(value).to.be.equal(2020); + expect(typeof value).to.be.equal('number'); + expect(typeof created).to.be.equal('number'); + clock.restore(); + }); + + it('should get external stored value', function () { + const value = 'superman' + window.localStorage.setItem('myExternalKey', value); + const item = getStorageItem('myExternalKey'); + expect(item).to.be.equal(value); + }); + + it('should parse JSON value', function () { + const data = JSON.stringify({event: 'send'}); + const {event} = tryParseJSON(data); + expect(event).to.be.equal('send'); + }); + + it('should get original value on parse fail', function () { + const value = 21; + const parsed = tryParseJSON(value); + expect(typeof parsed).to.be.equal('number'); + expect(parsed).to.be.equal(value); + }); + }); +}); diff --git a/test/spec/modules/missenaBidAdapter_spec.js b/test/spec/modules/missenaBidAdapter_spec.js index 157137b4730..f61987298e8 100644 --- a/test/spec/modules/missenaBidAdapter_spec.js +++ b/test/spec/modules/missenaBidAdapter_spec.js @@ -13,6 +13,8 @@ describe('Missena Adapter', function () { sizes: [[1, 1]], params: { apiKey: 'PA-34745704', + placement: 'sticky', + formats: ['sticky-banner'], }, }; @@ -70,6 +72,14 @@ describe('Missena Adapter', function () { expect(payload.request_id).to.equal(bidId); }); + it('should send placement', function () { + expect(payload.placement).to.equal('sticky'); + }); + + it('should send formats', function () { + expect(payload.formats).to.eql(['sticky-banner']); + }); + it('should send referer information to the request', function () { expect(payload.referer).to.equal('https://referer'); expect(payload.referer_canonical).to.equal('https://canonical'); diff --git a/test/spec/modules/nativoBidAdapter_spec.js b/test/spec/modules/nativoBidAdapter_spec.js index 4cd4c92281b..4d70e6f7071 100644 --- a/test/spec/modules/nativoBidAdapter_spec.js +++ b/test/spec/modules/nativoBidAdapter_spec.js @@ -5,6 +5,9 @@ import { getMediaWildcardPrices, sizeToString, parseFloorPriceData, + getPageUrlFromBidRequest, + hasProtocol, + addProtocol, } from '../../../modules/nativoBidAdapter' describe('bidDataMap', function () { @@ -647,3 +650,84 @@ describe('parseFloorPriceData', () => { }) }) }) + +describe('hasProtocol', () => { + it('https://www.testpage.com', () => { + expect(hasProtocol('https://www.testpage.com')).to.be.true + }) + it('http://www.testpage.com', () => { + expect(hasProtocol('http://www.testpage.com')).to.be.true + }) + it('//www.testpage.com', () => { + expect(hasProtocol('//www.testpage.com')).to.be.false + }) + it('www.testpage.com', () => { + expect(hasProtocol('www.testpage.com')).to.be.false + }) + it('httpsgsjhgflih', () => { + expect(hasProtocol('httpsgsjhgflih')).to.be.false + }) +}) + +describe('addProtocol', () => { + it('www.testpage.com', () => { + expect(addProtocol('www.testpage.com')).to.be.equal('https://www.testpage.com') + }) + it('//www.testpage.com', () => { + expect(addProtocol('//www.testpage.com')).to.be.equal('https://www.testpage.com') + }) + it('http://www.testpage.com', () => { + expect(addProtocol('http://www.testpage.com')).to.be.equal('http://www.testpage.com') + }) + it('https://www.testpage.com', () => { + expect(addProtocol('https://www.testpage.com')).to.be.equal('https://www.testpage.com') + }) +}) + +describe('getPageUrlFromBidRequest', () => { + const bidRequest = {} + + beforeEach(() => { + bidRequest.params = {} + }) + + it('Returns undefined for no url param', () => { + const url = getPageUrlFromBidRequest(bidRequest) + expect(url).to.be.undefined + }) + + it('@testUrl', () => { + const url = getPageUrlFromBidRequest(bidRequest) + expect(url).to.be.undefined + }) + + it('https://www.testpage.com', () => { + bidRequest.params.url = 'https://www.testpage.com' + const url = getPageUrlFromBidRequest(bidRequest) + expect(url).not.to.be.undefined + }) + + it('https://www.testpage.com/test/path', () => { + bidRequest.params.url = 'https://www.testpage.com/test/path' + const url = getPageUrlFromBidRequest(bidRequest) + expect(url).not.to.be.undefined + }) + + it('www.testpage.com', () => { + bidRequest.params.url = 'www.testpage.com' + const url = getPageUrlFromBidRequest(bidRequest) + expect(url).not.to.be.undefined + }) + + it('http://www.testpage.com', () => { + bidRequest.params.url = 'http://www.testpage.com' + const url = getPageUrlFromBidRequest(bidRequest) + expect(url).not.to.be.undefined + }) + + it('//www.testpage.com', () => { + bidRequest.params.url = '//www.testpage.com' + const url = getPageUrlFromBidRequest(bidRequest) + expect(url).not.to.be.undefined + }) +}) diff --git a/test/spec/modules/neuwoRtdProvider_spec.js b/test/spec/modules/neuwoRtdProvider_spec.js new file mode 100644 index 00000000000..0ad3d7c1f74 --- /dev/null +++ b/test/spec/modules/neuwoRtdProvider_spec.js @@ -0,0 +1,123 @@ +import { server } from 'test/mocks/xhr.js'; +import * as neuwo from 'modules/neuwoRtdProvider'; + +const PUBLIC_TOKEN = 'public_key_0000'; +const config = () => ({ + params: { + publicToken: PUBLIC_TOKEN, + apiUrl: 'https://testing-requirement.neuwo.api' + } +}) + +const apiReturns = () => ({ + somethingExtra: { object: true }, + marketing_categories: { + iab_tier_1: [ + { ID: 'IAB21', label: 'Real Estate', relevance: '0.45699' } + ] + } +}) + +const TAX_ID = '441' + +/** + * Object generator, like above, written using alternative techniques + * @returns object with predefined (expected) bidsConfig fields + */ +function bidsConfiglike() { + return Object.assign({}, { + ortb2Fragments: { global: {} } + }) +} + +describe('neuwoRtdProvider', function () { + describe('neuwoRtdModule', function () { + it('initializes', function () { + expect(neuwo.neuwoRtdModule.init(config())).to.be.true; + }) + it('init needs that public token', function () { + expect(neuwo.neuwoRtdModule.init()).to.be.false; + }) + + describe('segment picking', function () { + it('handles bad inputs', function () { + expect(neuwo.pickSegments()).to.be.an('array').that.is.empty; + expect(neuwo.pickSegments('technically also an array')).to.be.an('array').that.is.empty; + expect(neuwo.pickSegments({ bad_object: 'bad' })).to.be.an('array').that.is.empty; + }) + it('handles malformations', function () { + let result = neuwo.pickSegments([{something_wrong: true}, null, { ID: 'IAB19-20' }, { id: 'IAB3-1', ID: 'IAB9-20' }]) + expect(result[0].id).to.equal('631') + expect(result[1].id).to.equal('58') + expect(result.length).to.equal(2) + }) + }) + + describe('topic injection', function () { + it('mutates bidsConfig', function () { + let topics = apiReturns() + let bidsConfig = bidsConfiglike() + neuwo.injectTopics(topics, bidsConfig, () => { }) + expect(bidsConfig.ortb2Fragments.global.site.content.data[0].name, 'name of first content data object').to.equal(neuwo.DATA_PROVIDER) + expect(bidsConfig.ortb2Fragments.global.site.content.data[0].segment[0].id, 'id of first segment in content.data').to.equal(TAX_ID) + expect(bidsConfig.ortb2Fragments.global.site.pagecat[0], 'category taxonomy code for pagecat').to.equal(TAX_ID) + }) + + it('handles malformed responses', function () { + let topics = { message: 'Forbidden' } + let bidsConfig = bidsConfiglike() + neuwo.injectTopics(topics, bidsConfig, () => { }) + expect(bidsConfig.ortb2Fragments.global.site.content.data[0].name, 'name of first content data object').to.equal(neuwo.DATA_PROVIDER) + expect(bidsConfig.ortb2Fragments.global.site.content.data[0].segment, 'length of segment(s) in content.data').to.be.an('array').that.is.empty; + + topics = '404 wouldn\'t really even show up for injection' + let bdsConfig = bidsConfiglike() + neuwo.injectTopics(topics, bdsConfig, () => { }) + expect(bdsConfig.ortb2Fragments.global.site.content.data[0].name, 'name of first content data object').to.equal(neuwo.DATA_PROVIDER) + expect(bdsConfig.ortb2Fragments.global.site.content.data[0].segment, 'length of segment(s) in content.data').to.be.an('array').that.is.empty; + + topics = undefined + let bdsConfigE = bidsConfiglike() + neuwo.injectTopics(topics, bdsConfigE, () => { }) + expect(bdsConfigE.ortb2Fragments.global.site.content.data[0].name, 'name of first content data object').to.equal(neuwo.DATA_PROVIDER) + expect(bdsConfigE.ortb2Fragments.global.site.content.data[0].segment, 'length of segment(s) in content.data').to.be.an('array').that.is.empty; + }) + }) + + describe('fragment addition', function () { + it('mutates input objects', function () { + let alphabet = { a: { b: { c: {} } } } + neuwo.addFragment(alphabet.a.b.c, 'd.e.f', { g: 'h' }) + expect(alphabet.a.b.c.d.e.f.g).to.equal('h') + }) + }) + + describe('getBidRequestData', function () { + it('forms requests properly and mutates input bidsConfig', function () { + let bids = bidsConfiglike() + let conf = config() + // control xhr api request target for testing + conf.params.argUrl = 'https://publisher.works/article.php?get=horrible_url_for_testing&id=5' + + neuwo.getBidRequestData(bids, () => { }, conf, 'any consent data works, clearly') + + let request = server.requests[0]; + expect(request.url).to.be.a('string').that.includes(conf.params.publicToken) + expect(request.url).to.include(encodeURIComponent(conf.params.argUrl)) + request.respond(200, { 'Content-Type': 'application/json; encoding=UTF-8' }, JSON.stringify(apiReturns())); + + expect(bids.ortb2Fragments.global.site.content.data[0].name, 'name of first content data object').to.equal(neuwo.DATA_PROVIDER) + expect(bids.ortb2Fragments.global.site.content.data[0].segment[0].id, 'id of first segment in content.data').to.equal(TAX_ID) + }) + + it('accepts detail not available result', function () { + let bidsConfig = bidsConfiglike() + let comparison = bidsConfiglike() + neuwo.getBidRequestData(bidsConfig, () => { }, config(), 'consensually') + let request = server.requests[0]; + request.respond(404, { 'Content-Type': 'application/json; encoding=UTF-8' }, JSON.stringify({ detail: 'Basically first time seeing this' })); + expect(bidsConfig).to.deep.equal(comparison) + }) + }) + }) +}) diff --git a/test/spec/modules/nextMillenniumBidAdapter_spec.js b/test/spec/modules/nextMillenniumBidAdapter_spec.js index 2b5ae801489..7f40e7d89f8 100644 --- a/test/spec/modules/nextMillenniumBidAdapter_spec.js +++ b/test/spec/modules/nextMillenniumBidAdapter_spec.js @@ -28,6 +28,34 @@ describe('nextMillenniumBidAdapterTests', function() { } ]; + const serverResponse = { + body: { + id: 'f7b3d2da-e762-410c-b069-424f92c4c4b2', + seatbid: [ + { + bid: [ + { + id: '7457329903666272789', + price: 0.5, + adm: 'Hello! It\'s a test ad!', + adid: '96846035', + adomain: ['test.addomain.com'], + w: 300, + h: 250 + } + ] + } + ], + cur: 'USD', + ext: { + sync: { + image: ['urlA?gdpr={{.GDPR}}'], + iframe: ['urlB'], + } + } + } + }; + const bidRequestDataGI = [ { adUnitCode: 'test-banner-gi', @@ -96,6 +124,47 @@ describe('nextMillenniumBidAdapterTests', function() { expect(JSON.parse(request[0].data).regs.ext.gdpr).to.equal(1); }); + it('Test getUserSyncs function', function () { + const syncOptions = { + 'iframeEnabled': false, + 'pixelEnabled': true + } + let userSync = spec.getUserSyncs(syncOptions, [serverResponse], bidRequestData[0].gdprConsent, bidRequestData[0].uspConsent); + expect(userSync).to.be.an('array').with.lengthOf(1); + expect(userSync[0].type).to.equal('image'); + expect(userSync[0].url).to.equal('urlA?gdpr=1'); + + syncOptions.iframeEnabled = true; + syncOptions.pixelEnabled = false; + userSync = spec.getUserSyncs(syncOptions, [serverResponse], bidRequestData[0].gdprConsent, bidRequestData[0].uspConsent); + expect(userSync).to.be.an('array').with.lengthOf(1); + expect(userSync[0].type).to.equal('iframe'); + expect(userSync[0].url).to.equal('urlB'); + }); + + it('Test getUserSyncs with no response', function () { + const syncOptions = { + 'iframeEnabled': true, + 'pixelEnabled': false + } + let userSync = spec.getUserSyncs(syncOptions, [], bidRequestData[0].gdprConsent, bidRequestData[0].uspConsent); + expect(userSync).to.be.an('array') + expect(userSync[0].type).to.equal('iframe') + expect(userSync[0].url).to.equal('https://cookies.nextmillmedia.com/sync?gdpr=1&gdpr_consent=kjfdniwjnifwenrif3&us_privacy=1---&type=iframe') + }) + + it('Test getUserSyncs function if GDPR is undefined', function () { + const syncOptions = { + 'iframeEnabled': false, + 'pixelEnabled': true + } + + let userSync = spec.getUserSyncs(syncOptions, [serverResponse], undefined, bidRequestData[0].uspConsent); + expect(userSync).to.be.an('array').with.lengthOf(1); + expect(userSync[0].type).to.equal('image'); + expect(userSync[0].url).to.equal('urlA?gdpr=0'); + }); + it('Request params check without GDPR Consent', function () { delete bidRequestData[0].gdprConsent const request = spec.buildRequests(bidRequestData, bidRequestData[0]); @@ -137,51 +206,16 @@ describe('nextMillenniumBidAdapterTests', function() { it('Check if imp object was added', function() { const request = spec.buildRequests(bidRequestData) expect(JSON.parse(request[0].data).imp).to.be.an('array') - }) + }); it('Check if imp prebid stored id is correct', function() { const request = spec.buildRequests(bidRequestData) const requestData = JSON.parse(request[0].data); const storedReqId = requestData.ext.prebid.storedrequest.id; expect(requestData.imp[0].ext.prebid.storedrequest.id).to.equal(storedReqId) - }) - - it('Test getUserSyncs function', function () { - const syncOptions = { - 'iframeEnabled': false, - 'pixelEnabled': true - } - const userSync = spec.getUserSyncs(syncOptions); - expect(userSync).to.be.an('array').with.lengthOf(1); - expect(userSync[0].type).to.exist; - expect(userSync[0].url).to.exist; - expect(userSync[0].type).to.be.equal('image'); - expect(userSync[0].url).to.be.equal('https://cookies.nextmillmedia.com/sync?type=image'); }); it('validate_response_params', function() { - const serverResponse = { - body: { - id: 'f7b3d2da-e762-410c-b069-424f92c4c4b2', - seatbid: [ - { - bid: [ - { - id: '7457329903666272789', - price: 0.5, - adm: 'Hello! It\'s a test ad!', - adid: '96846035', - adomain: ['test.addomain.com'], - w: 300, - h: 250 - } - ] - } - ], - cur: 'USD' - } - }; - let bids = spec.interpretResponse(serverResponse, bidRequestData[0]); expect(bids).to.have.lengthOf(1); @@ -276,4 +310,137 @@ describe('nextMillenniumBidAdapterTests', function() { expect(bid.height).to.equal(250); expect(bid.currency).to.equal('USD'); }); + + it('Check function of getting URL for sending statistics data', function() { + const dataForTests = [ + { + eventName: 'bidRequested', + bid: { + bidderCode: 'appnexus', + bids: [{bidder: 'appnexus', params: {}}], + }, + + expected: undefined, + }, + + { + eventName: 'bidRequested', + bid: { + bidderCode: 'appnexus', + bids: [{bidder: 'appnexus', params: {placement_id: '807'}}], + }, + + expected: undefined, + }, + + { + eventName: 'bidRequested', + bid: { + bidderCode: 'nextMillennium', + bids: [{bidder: 'nextMillennium', params: {placement_id: '807'}}], + }, + + expected: 'https://report2.hb.brainlyads.com/statistics/metric?event=bidRequested&bidder=nextMillennium&source=pbjs&placements=807', + }, + + { + eventName: 'bidRequested', + bid: { + bidderCode: 'nextMillennium', + bids: [ + {bidder: 'nextMillennium', params: {placement_id: '807'}}, + {bidder: 'nextMillennium', params: {placement_id: '111'}}, + ], + }, + + expected: 'https://report2.hb.brainlyads.com/statistics/metric?event=bidRequested&bidder=nextMillennium&source=pbjs&placements=807;111', + }, + + { + eventName: 'bidRequested', + bid: { + bidderCode: 'nextMillennium', + bids: [{bidder: 'nextMillennium', params: {placement_id: '807', group_id: '123'}}], + }, + + expected: 'https://report2.hb.brainlyads.com/statistics/metric?event=bidRequested&bidder=nextMillennium&source=pbjs&groups=123', + }, + + { + eventName: 'bidRequested', + bid: { + bidderCode: 'nextMillennium', + bids: [ + {bidder: 'nextMillennium', params: {placement_id: '807', group_id: '123'}}, + {bidder: 'nextMillennium', params: {group_id: '456'}}, + {bidder: 'nextMillennium', params: {placement_id: '222'}}, + ], + }, + + expected: 'https://report2.hb.brainlyads.com/statistics/metric?event=bidRequested&bidder=nextMillennium&source=pbjs&groups=123;456&placements=222', + }, + + { + eventName: 'bidResponse', + bid: { + bidderCode: 'appnexus', + }, + + expected: undefined, + }, + + { + eventName: 'bidResponse', + bid: { + bidderCode: 'nextMillennium', + params: {placement_id: '807'}, + }, + + expected: 'https://report2.hb.brainlyads.com/statistics/metric?event=bidResponse&bidder=nextMillennium&source=pbjs&placements=807', + }, + + { + eventName: 'noBid', + bid: { + bidder: 'appnexus', + }, + + expected: undefined, + }, + + { + eventName: 'noBid', + bid: { + bidder: 'nextMillennium', + params: {placement_id: '807'}, + }, + + expected: 'https://report2.hb.brainlyads.com/statistics/metric?event=noBid&bidder=nextMillennium&source=pbjs&placements=807', + }, + + { + eventName: 'bidTimeout', + bid: { + bidder: 'appnexus', + }, + + expected: undefined, + }, + + { + eventName: 'bidTimeout', + bid: { + bidder: 'nextMillennium', + params: {placement_id: '807'}, + }, + + expected: 'https://report2.hb.brainlyads.com/statistics/metric?event=bidTimeout&bidder=nextMillennium&source=pbjs&placements=807', + }, + ]; + + for (let {eventName, bid, expected} of dataForTests) { + const url = spec.getUrlPixelMetric(eventName, bid); + expect(url).to.equal(expected); + }; + }) }); diff --git a/test/spec/modules/nexx360BidAdapter_spec.js b/test/spec/modules/nexx360BidAdapter_spec.js index a5da8b29fbc..9ba3ac0e5a4 100644 --- a/test/spec/modules/nexx360BidAdapter_spec.js +++ b/test/spec/modules/nexx360BidAdapter_spec.js @@ -1,9 +1,41 @@ -import {expect} from 'chai'; -import {spec} from 'modules/nexx360BidAdapter.js'; -import {newBidder} from 'src/adapters/bidderFactory.js'; -import {config} from 'src/config.js'; -import * as utils from 'src/utils.js'; -import { requestBidsHook } from 'modules/consentManagement.js'; +import { expect } from 'chai'; +import { + spec, storage, getNexx360LocalStorage, +} from 'modules/nexx360BidAdapter.js'; +import { sandbox } from 'sinon'; + +const instreamResponse = { + 'id': '2be64380-ba0c-405a-ab53-51f51c7bde51', + 'cur': 'USD', + 'seatbid': [ + { + 'bid': [ + { + 'id': '8275140264321181514', + 'impid': '263cba3b8bfb72', + 'price': 5, + 'adomain': [ + 'appnexus.com' + ], + 'crid': '97517771', + 'h': 1, + 'w': 1, + 'ext': { + 'mediaType': 'instream', + 'ssp': 'appnexus', + 'divId': 'video1', + 'adUnitCode': 'video1', + 'vastXml': '\n \n \n Nexx360 Wrapper\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n ' + } + } + ], + 'seat': 'appnexus' + } + ], + 'ext': { + 'cookies': [] + } +}; describe('Nexx360 bid adapter tests', function () { const DISPLAY_BID_REQUEST = { @@ -110,6 +142,32 @@ describe('Nexx360 bid adapter tests', function () { 'auctionId': '98932591-c822-42e3-850e-4b3cf748d063', } }); + + it('We verify isBidRequestValid with unvalid adUnitName', function() { + bannerBid.params = { adUnitName: 1 }; + expect(spec.isBidRequestValid(bannerBid)).to.be.equal(false); + }); + + it('We verify isBidRequestValid with empty adUnitName', function() { + bannerBid.params = { adUnitName: '' }; + expect(spec.isBidRequestValid(bannerBid)).to.be.equal(false); + }); + + it('We verify isBidRequestValid with unvalid adUnitPath', function() { + bannerBid.params = { adUnitPath: 1 }; + expect(spec.isBidRequestValid(bannerBid)).to.be.equal(false); + }); + + it('We verify isBidRequestValid with unvalid divId', function() { + bannerBid.params = { divId: 1 }; + expect(spec.isBidRequestValid(bannerBid)).to.be.equal(false); + }); + + it('We verify isBidRequestValid unvalid allBids', function() { + bannerBid.params = { allBids: 1 }; + expect(spec.isBidRequestValid(bannerBid)).to.be.equal(false); + }); + it('We verify isBidRequestValid with uncorrect tagid', function() { bannerBid.params = { 'tagid': 'luvxjvgn' }; expect(spec.isBidRequestValid(bannerBid)).to.be.equal(false); @@ -121,63 +179,87 @@ describe('Nexx360 bid adapter tests', function () { }); }); - describe('when request is for a multiformat ad', function () { - describe('and request config uses mediaTypes video and banner', () => { - const multiformatBid = { - 'bidder': 'nexx360', - 'params': {'tagId': 'luvxjvgn'}, - 'mediaTypes': { - 'banner': { - 'sizes': [[300, 250]] - }, - 'video': { - 'playerSize': [300, 250] - } - }, - 'adUnitCode': 'div-1', - 'transactionId': '70bdc37e-9475-4b27-8c74-4634bdc2ee66', - 'sizes': [[300, 250], [300, 600]], - 'bidId': '4906582fc87d0c', - 'bidderRequestId': '332fda16002dbe', - 'auctionId': '98932591-c822-42e3-850e-4b3cf748d063', - } - it('should return true multisize when required params found', function () { - expect(spec.isBidRequestValid(multiformatBid)).to.equal(true); - }); + describe('getNexx360LocalStorage disabled', function () { + before(function () { + sandbox.stub(storage, 'localStorageIsEnabled').callsFake(() => false); }); - }); + it('We test if we get the nexx360Id', function() { + const output = getNexx360LocalStorage(); + expect(output).to.be.eql(false); + }); + after(function () { + sandbox.restore() + }); + }) - describe('when request is for a video ad', function () { - describe('and request config uses mediaTypes video and banner', () => { - const videoBid = { - 'bidder': 'nexx360', - 'params': {'tagId': 'luvxjvgn'}, - 'mediaTypes': { - 'video': { - 'playerSize': [300, 250] - } - }, - 'adUnitCode': 'div-1', - 'transactionId': '70bdc37e-9475-4b27-8c74-4634bdc2ee66', - 'bidId': '4906582fc87d0c', - 'bidderRequestId': '332fda16002dbe', - 'auctionId': '98932591-c822-42e3-850e-4b3cf748d063', - } - it('should return true multisize when required params found', function () { - expect(spec.isBidRequestValid(videoBid)).to.equal(true); - }); + describe('getNexx360LocalStorage enabled but nothing', function () { + before(function () { + sandbox.stub(storage, 'localStorageIsEnabled').callsFake(() => true); + sandbox.stub(storage, 'setDataInLocalStorage'); + sandbox.stub(storage, 'getDataFromLocalStorage').callsFake((key) => null); }); - }); + it('We test if we get the nexx360Id', function() { + const output = getNexx360LocalStorage(); + expect(typeof output.nexx360Id).to.be.eql('string'); + }); + after(function () { + sandbox.restore() + }); + }) + + describe('getNexx360LocalStorage enabled but wrong payload', function () { + before(function () { + sandbox.stub(storage, 'localStorageIsEnabled').callsFake(() => true); + sandbox.stub(storage, 'setDataInLocalStorage'); + sandbox.stub(storage, 'getDataFromLocalStorage').callsFake((key) => '{"nexx360Id":"5ad89a6e-7801-48e7-97bb-fe6f251f6cb4",}'); + }); + it('We test if we get the nexx360Id', function() { + const output = getNexx360LocalStorage(); + expect(output).to.be.eql(false); + }); + after(function () { + sandbox.restore() + }); + }) + + describe('getNexx360LocalStorage enabled', function () { + before(function () { + sandbox.stub(storage, 'localStorageIsEnabled').callsFake(() => true); + sandbox.stub(storage, 'setDataInLocalStorage'); + sandbox.stub(storage, 'getDataFromLocalStorage').callsFake((key) => '{"nexx360Id":"5ad89a6e-7801-48e7-97bb-fe6f251f6cb4"}'); + }); + it('We test if we get the nexx360Id', function() { + const output = getNexx360LocalStorage(); + expect(output.nexx360Id).to.be.eql('5ad89a6e-7801-48e7-97bb-fe6f251f6cb4'); + }); + after(function () { + sandbox.restore() + }); + }) describe('buildRequests()', function() { + before(function () { + const documentStub = sandbox.stub(document, 'getElementById'); + documentStub.withArgs('div-1').returns({ + offsetWidth: 200, + offsetHeight: 250, + style: { + maxWidth: '400px', + maxHeight: '350px', + } + }); + }); describe('We test with a multiple display bids', function() { const sampleBids = [ { bidder: 'nexx360', params: { - tagId: 'luvxjvgn' + tagId: 'luvxjvgn', + divId: 'div-1', + adUnitName: 'header-ad', + adUnitPath: '/12345/nexx360/Homepage/HP/Header-Ad', }, - adUnitCode: 'div-1', + adUnitCode: 'header-ad-1234', transactionId: '469a570d-f187-488d-b1cb-48c1a2009be9', sizes: [[300, 250], [300, 600]], bidId: '44a2706ac3574', @@ -221,7 +303,7 @@ describe('Nexx360 bid adapter tests', function () { sizes: [[728, 90], [970, 250]] } }, - adUnitCode: 'div-2', + adUnitCode: 'div-2-abcd', transactionId: '6196885d-4e76-40dc-a09c-906ed232626b', sizes: [[728, 90], [970, 250]], bidId: '5ba94555219a03', @@ -276,63 +358,83 @@ describe('Nexx360 bid adapter tests', function () { expect(requestContent.cur[0]).to.be.eql('USD'); expect(requestContent.imp.length).to.be.eql(2); expect(requestContent.imp[0].id).to.be.eql('44a2706ac3574'); - expect(requestContent.imp[0].tagid).to.be.eql('div-1'); + expect(requestContent.imp[0].tagid).to.be.eql('header-ad'); expect(requestContent.imp[0].ext.divId).to.be.eql('div-1'); + expect(requestContent.imp[0].ext.adUnitCode).to.be.eql('header-ad-1234'); + expect(requestContent.imp[0].ext.adUnitName).to.be.eql('header-ad'); + expect(requestContent.imp[0].ext.adUnitPath).to.be.eql('/12345/nexx360/Homepage/HP/Header-Ad'); + expect(requestContent.imp[0].ext.dimensions.slotW).to.be.eql(200); + expect(requestContent.imp[0].ext.dimensions.slotH).to.be.eql(250); + expect(requestContent.imp[0].ext.dimensions.cssMaxW).to.be.eql('400px'); + expect(requestContent.imp[0].ext.dimensions.cssMaxH).to.be.eql('350px'); expect(requestContent.imp[0].ext.nexx360.tagId).to.be.eql('luvxjvgn'); - expect(requestContent.imp[0].ext.nexx360.allBids).to.be.eql(false); expect(requestContent.imp[0].banner.format.length).to.be.eql(2); expect(requestContent.imp[0].banner.format[0].w).to.be.eql(300); expect(requestContent.imp[0].banner.format[0].h).to.be.eql(250); expect(requestContent.imp[1].ext.nexx360.allBids).to.be.eql(true); - expect(requestContent.regs.ext.gdpr).to.be.eql(1); - expect(requestContent.user.ext.consent).to.be.eql(bidderRequest.gdprConsent.consentString); - expect(requestContent.user.ext.eids.length).to.be.eql(2); + expect(requestContent.imp[1].tagid).to.be.eql('div-2-abcd'); + expect(requestContent.imp[1].ext.adUnitCode).to.be.eql('div-2-abcd'); + expect(requestContent.imp[1].ext.divId).to.be.eql('div-2-abcd'); + expect(requestContent.ext.bidderVersion).to.be.eql('2.0'); + expect(requestContent.ext.source).to.be.eql('prebid.js'); }); - it('We perform a test with a multiformat adunit', function() { - const multiformatBids = [...sampleBids]; - multiformatBids[0].mediaTypes = { - banner: { - sizes: [[300, 250], [300, 600]] - }, - video: { - context: 'outstream', - playerSize: [640, 480], - mimes: ['video/mp4'], - protocols: [1, 2, 3, 4, 5, 6, 7, 8], - playbackmethod: [2], - skip: 1, - playback_method: ['auto_play_sound_off'] - } - }; - const request = spec.buildRequests(multiformatBids, bidderRequest); - const requestContent = request.data; - expect(requestContent.imp[0].video.context).to.be.eql('outstream'); - expect(requestContent.imp[0].video.playbackmethod[0]).to.be.eql(2); - }); + if (FEATURES.VIDEO) { + it('We perform a test with a multiformat adunit', function() { + const multiformatBids = [...sampleBids]; + multiformatBids[0].mediaTypes = { + banner: { + sizes: [[300, 250], [300, 600]] + }, + video: { + context: 'outstream', + playerSize: [640, 480], + mimes: ['video/mp4'], + protocols: [1, 2, 3, 4, 5, 6, 7, 8], + playbackmethod: [2], + skip: 1, + playback_method: ['auto_play_sound_off'] + } + }; + const request = spec.buildRequests(multiformatBids, bidderRequest); + const requestContent = request.data; + expect(requestContent.imp[0].video.ext.context).to.be.eql('outstream'); + expect(requestContent.imp[0].video.playbackmethod[0]).to.be.eql(2); + }); - it('We perform a test with a video adunit', function() { - const videoBids = [sampleBids[0]]; - videoBids[0].mediaTypes = { - video: { - context: 'instream', - playerSize: [640, 480], - mimes: ['video/mp4'], - protocols: [1, 2, 3, 4, 5, 6], - playbackmethod: [2], - skip: 1 - } - }; - const request = spec.buildRequests(videoBids, bidderRequest); - const requestContent = request.data; - expect(request).to.have.property('method').and.to.equal('POST'); - expect(requestContent.imp[0].video.context).to.be.eql('instream'); - expect(requestContent.imp[0].video.playbackmethod[0]).to.be.eql(2); - }) + it('We perform a test with a instream adunit', function() { + const videoBids = [sampleBids[0]]; + videoBids[0].mediaTypes = { + video: { + context: 'instream', + playerSize: [640, 480], + mimes: ['video/mp4'], + protocols: [1, 2, 3, 4, 5, 6], + playbackmethod: [2], + skip: 1 + } + }; + const request = spec.buildRequests(videoBids, bidderRequest); + const requestContent = request.data; + expect(request).to.have.property('method').and.to.equal('POST'); + expect(requestContent.imp[0].video.ext.context).to.be.eql('instream'); + expect(requestContent.imp[0].video.playbackmethod[0]).to.be.eql(2); + }) + } + }); + after(function () { + sandbox.restore() }); }); describe('interpretResponse()', function() { + it('empty response', function() { + const response = { + body: '' + }; + const output = spec.interpretResponse(response); + expect(output.length).to.be.eql(0); + }); it('banner responses', function() { const response = { body: { @@ -345,7 +447,6 @@ describe('Nexx360 bid adapter tests', function () { 'id': '4427551302944024629', 'impid': '226175918ebeda', 'price': 1.5, - 'type': 'banner', 'adomain': [ 'http://prebid.org' ], @@ -356,145 +457,153 @@ describe('Nexx360 bid adapter tests', function () { 'cat': [ 'IAB3-1' ], - 'creativeuuid': 'fdddcebc-1edf-489d-880d-1418d8bdc493', - 'adUrl': 'https://fast.nexx360.io/cache?uuid=fdddcebc-1edf-489d-880d-1418d8bdc493', 'ext': { - 'dsp_id': 'ssp1', - 'buyer_id': 'foo', - 'brand_id': 'bar' + 'adUnitCode': 'div-1', + 'mediaType': 'banner', + 'adUrl': 'https://fast.nexx360.io/cache?uuid=fdddcebc-1edf-489d-880d-1418d8bdc493', + 'ssp': 'appnexus', } } ], 'seat': 'appnexus' } ], - 'cookies': [] + 'ext': { + 'id': 'de3de7c7-e1cf-4712-80a9-94eb26bfc718', + 'cookies': [] + }, } }; const output = spec.interpretResponse(response); - expect(output[0].bidderCode).to.be.eql('nexx360'); - expect(output[0].adUrl).to.be.eql(response.body.seatbid[0].bid[0].adUrl); - expect(output[0].mediaType).to.be.eql(response.body.seatbid[0].bid[0].type); + expect(output[0].adUrl).to.be.eql(response.body.seatbid[0].bid[0].ext.adUrl); + expect(output[0].mediaType).to.be.eql(response.body.seatbid[0].bid[0].ext.mediaType); expect(output[0].currency).to.be.eql(response.body.cur); expect(output[0].cpm).to.be.eql(response.body.seatbid[0].bid[0].price); - expect(output[0].meta.networkId).to.be.eql(response.body.seatbid[0].bid[0].ext.dsp_id); - expect(output[0].meta.advertiserId).to.be.eql(response.body.seatbid[0].bid[0].ext.buyer_id); - expect(output[0].meta.brandId).to.be.eql(response.body.seatbid[0].bid[0].ext.brand_id); }); - it('video responses', function() { + it('instream responses', function() { const response = { body: { - 'id': '33894759-0ea2-41f1-84b3-75132eefedb6', + 'id': '2be64380-ba0c-405a-ab53-51f51c7bde51', 'cur': 'USD', 'seatbid': [ { 'bid': [ { - 'id': '294478680080716675', - 'impid': '2c835a6039e65f', + 'id': '8275140264321181514', + 'impid': '263cba3b8bfb72', 'price': 5, - 'type': 'instream', 'adomain': [ - '' + 'appnexus.com' ], 'crid': '97517771', - 'ssp': 'appnexus', 'h': 1, 'w': 1, - 'vastXml': '\n \n \n prebid.org wrapper\n \n \n \n \n \n ' + 'ext': { + 'mediaType': 'instream', + 'ssp': 'appnexus', + 'adUnitCode': 'video1', + 'vastXml': '\n \n \n Nexx360 Wrapper\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n ' + } } ], 'seat': 'appnexus' } ], - 'cookies': [] + 'ext': { + 'cookies': [] + } } }; const output = spec.interpretResponse(response); - expect(output[0].bidderCode).to.be.eql('nexx360'); - expect(output[0].vastXml).to.be.eql(response.body.seatbid[0].bid[0].vastXml); + expect(output[0].vastXml).to.be.eql(response.body.seatbid[0].bid[0].ext.vastXml); expect(output[0].mediaType).to.be.eql('video'); expect(output[0].currency).to.be.eql(response.body.cur); expect(output[0].cpm).to.be.eql(response.body.seatbid[0].bid[0].price); }); - }); - describe('interpretResponse()', function() { - it('banner responses', function() { + it('outstream responses', function() { const response = { body: { - 'id': 'a8d3a675-a4ba-4d26-807f-c8f2fad821e0', + 'id': '40c23932-135e-4602-9701-ca36f8d80c07', 'cur': 'USD', 'seatbid': [ { 'bid': [ { - 'id': '4427551302944024629', - 'impid': '226175918ebeda', - 'price': 1.5, - 'type': 'banner', + 'id': '1186971142548769361', + 'impid': '4ce809b61a3928', + 'price': 5, 'adomain': [ - 'http://prebid.org' - ], - 'crid': '98493581', - 'ssp': 'appnexus', - 'h': 600, - 'w': 300, - 'cat': [ - 'IAB3-1' + 'appnexus.com' ], - 'creativeuuid': 'fdddcebc-1edf-489d-880d-1418d8bdc493', - 'adUrl': 'https://fast.nexx360.io/cache?uuid=fdddcebc-1edf-489d-880d-1418d8bdc493' + 'crid': '97517771', + 'h': 1, + 'w': 1, + 'ext': { + 'mediaType': 'outstream', + 'ssp': 'appnexus', + 'adUnitCode': 'div-1', + 'vastXml': '\n \n \n Nexx360 Wrapper\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n ' + } } ], 'seat': 'appnexus' } ], - 'cookies': [] + 'ext': { + 'cookies': [] + } } }; const output = spec.interpretResponse(response); - expect(output[0].bidderCode).to.be.eql('nexx360'); - expect(output[0].adUrl).to.be.eql(response.body.seatbid[0].bid[0].adUrl); - expect(output[0].mediaType).to.be.eql(response.body.seatbid[0].bid[0].type); + expect(output[0].vastXml).to.be.eql(response.body.seatbid[0].bid[0].ext.vastXml); + expect(output[0].mediaType).to.be.eql('video'); expect(output[0].currency).to.be.eql(response.body.cur); + expect(typeof output[0].renderer).to.be.eql('object'); expect(output[0].cpm).to.be.eql(response.body.seatbid[0].bid[0].price); }); - it('video responses', function() { + + it('native responses', function() { const response = { body: { - 'id': '33894759-0ea2-41f1-84b3-75132eefedb6', + 'id': '3c0290c1-6e75-4ef7-9e37-17f5ebf3bfa3', 'cur': 'USD', 'seatbid': [ { 'bid': [ { - 'id': '294478680080716675', - 'impid': '2c835a6039e65f', - 'price': 5, - 'type': 'instream', + 'id': '6624930625245272225', + 'impid': '23e11d845514bb', + 'price': 10, 'adomain': [ - '' + 'prebid.org' ], - 'crid': '97517771', - 'ssp': 'appnexus', + 'crid': '97494204', 'h': 1, 'w': 1, - 'vastXml': '\n \n \n prebid.org wrapper\n \n \n \n \n \n ' + 'cat': [ + 'IAB3-1' + ], + 'ext': { + 'mediaType': 'native', + 'ssp': 'appnexus', + 'adUnitCode': '/19968336/prebid_native_example_1' + }, + 'adm': '{"ver":"1.2","assets":[{"id":1,"img":{"url":"https:\\/\\/vcdn.adnxs.com\\/p\\/creative-image\\/f8\\/7f\\/0f\\/13\\/f87f0f13-230c-4f05-8087-db9216e393de.jpg","w":989,"h":742,"ext":{"appnexus":{"prevent_crop":0}}}},{"id":0,"title":{"text":"This is a Prebid Native Creative"}},{"id":2,"data":{"value":"Prebid.org"}}],"link":{"url":"https:\\/\\/ams3-ib.adnxs.com\\/click?AAAAAAAAJEAAAAAAAAAkQAAAAAAAACRAAAAAAAAAJEAAAAAAAAAkQKZS4ZZl5vVbR6p-A-MwnyTZ7QVkAAAAAOLoyQBtJAAAbSQAAAIAAAC8pM8FnPgWAAAAAABVU0QAVVNEAAEAAQBNXQAAAAABAgMCAAAAALoAURe69gAAAAA.\\/bcr=AAAAAAAA8D8=\\/pp=${AUCTION_PRICE}\\/cnd=%21JBC72Aj8-LwKELzJvi4YnPFbIAQoADEAAAAAAAAkQDoJQU1TMzo2MTM1QNAwSQAAAAAAAPA_UQAAAAAAAAAAWQAAAAAAAAAAYQAAAAAAAAAAaQAAAAAAAAAAcQAAAAAAAAAAeACJAQAAAAAAAAAA\\/cca=OTMyNSNBTVMzOjYxMzU=\\/bn=97062\\/clickenc=http%3A%2F%2Fprebid.org%2Fdev-docs%2Fshow-native-ads.html"},"eventtrackers":[{"event":1,"method":1,"url":"https:\\/\\/ams3-ib.adnxs.com\\/it?an_audit=0&referrer=https%3A%2F%2Ftest.nexx360.io%2Fadapter%2Fnative%2Ftest.html&e=wqT_3QKJCqAJBQAAAwDWAAUBCNnbl6AGEKalhbfZzPn6WxjH1PqbsJzMzyQqNgkAAAECCCRAEQEHEAAAJEAZEQkAIREJACkRCQAxEQmoMOLRpwY47UhA7UhIAlC8yb4uWJzxW2AAaM26dXim9gWAAQGKAQNVU0SSAQEG9F4BmAEBoAEBqAEBsAEAuAECwAEDyAEC0AEJ2AEA4AEA8AEAigIpdWYoJ2EnLCAyNTI5ODg1LCAwKTt1ZigncicsIDk3NDk0MjA0LCAwKTuSAvEDIS0xRDNJQWo4LUx3S0VMekp2aTRZQUNDYzhWc3dBRGdBUUFSSTdVaFE0dEduQmxnQVlQX19fXzhQYUFCd0FYZ0JnQUVCaUFFQmtBRUJtQUVCb0FFQnFBRURzQUVBdVFIenJXcWtBQUFrUU1FQjg2MXFwQUFBSkVESkFYSUtWbWViSmZJXzJRRUFBQUFBQUFEd1AtQUJBUFVCQUFBQUFKZ0NBS0FDQUxVQ0FBQUFBTDBDQUFBQUFNQUNBY2dDQWRBQ0FkZ0NBZUFDQU9nQ0FQZ0NBSUFEQVpnREFib0RDVUZOVXpNNk5qRXpOZUFEMERDSUJBQ1FCQUNZQkFIQkJBQUFBQUFBQUFBQXlRUUFBCQscQUFOZ0VBUEURlSxBQUFDSUJmY3ZxUVUBDQRBQQGoCDdFRgEKCQEMREJCUQkKAQEAeRUoAUwyKAAAWi4oALg0QVhBaEQzd0JhTEQzd0w0QmQyMG1nR0NCZ05WVTBTSUJnQ1FCZ0dZQmdDaEJnQQFONEFBQ1JBcUFZQnNnWWtDHXQARR0MAEcdDABJHQw8dUFZS5oClQEhSkJDNzJBajL1ASRuUEZiSUFRb0FEFfhUa1FEb0pRVTFUTXpvMk1UTTFRTkF3UxFRDFBBX1URDAxBQUFXHQwAWR0MAGEdDABjHQwQZUFDSkEdEMjYAvfpA-ACrZhI6gIwaHR0cHM6Ly90ZXN0Lm5leHgzNjAuaW8vYWRhcHRlci9uYXRpdmUJH_CaaHRtbIADAIgDAZADAJgDFKADAaoDAMAD4KgByAMA2AMA4AMA6AMA-AMDgAQAkgQJL29wZW5ydGIymAQAqAQAsgQMCAAQABgAIAAwADgAuAQAwASA2rgiyAQA0gQOOTMyNSNBTVMzOjYxMzXaBAIIAeAEAPAEvMm-LvoEEgkAAABAPG1IQBEAAACgV8oCQIgFAZgFAKAF______8BBbABqgUkM2MwMjkwYzEtNmU3NS00ZWY3LTllMzctMTdmNWViZjNiZmEzwAUAyQWJFxTwP9IFCQkJDHgAANgFAeAFAfAFmfQh-gUECAAQAJAGAZgGALgGAMEGCSUo8D_QBvUv2gYWChAJERkBAdpg4AYM8gYCCACABwGIBwCgB0HIB6b2BdIHDRVkASYI2gcGAV1oGADgBwDqBwIIAPAHAIoIAhAAlQgAAIA_mAgB&s=ccf63f2e483a37091d2475d895e7cf7c911d1a78&pp=${AUCTION_PRICE}"}]}' } ], 'seat': 'appnexus' } ], - 'cookies': [] + 'ext': { + 'cookies': [], + } } }; const output = spec.interpretResponse(response); - expect(output[0].bidderCode).to.be.eql('nexx360'); - expect(output[0].vastXml).to.be.eql(response.body.seatbid[0].bid[0].vastXml); - expect(output[0].mediaType).to.be.eql('video'); - expect(output[0].currency).to.be.eql(response.body.cur); - expect(output[0].cpm).to.be.eql(response.body.seatbid[0].bid[0].price); + expect(output[0].native.ortb.ver).to.be.eql('1.2'); + expect(output[0].native.ortb.assets[0].id).to.be.eql(1); + expect(output[0].mediaType).to.be.eql('native'); }); }); @@ -505,7 +614,9 @@ describe('Nexx360 bid adapter tests', function () { expect(syncs).to.have.lengthOf(0); }); it('Verifies user sync with cookies in bid response', function () { - response.body.cookies = [{'type': 'image', 'url': 'http://www.cookie.sync.org/'}]; + response.body.ext = { + cookies: [{'type': 'image', 'url': 'http://www.cookie.sync.org/'}] + }; var syncs = spec.getUserSyncs({}, [response], DEFAULT_OPTIONS.gdprConsent); expect(syncs).to.have.lengthOf(1); expect(syncs[0]).to.have.property('type').and.to.equal('image'); diff --git a/test/spec/modules/nobidBidAdapter_spec.js b/test/spec/modules/nobidBidAdapter_spec.js index 7ea89f7dd3f..8328aae33d8 100644 --- a/test/spec/modules/nobidBidAdapter_spec.js +++ b/test/spec/modules/nobidBidAdapter_spec.js @@ -14,6 +14,38 @@ describe('Nobid Adapter', function () { }); }); + describe('buildRequestsWithFloor', function () { + const SITE_ID = 2; + const REFERER = 'https://www.examplereferer.com'; + let bidRequests = [ + { + 'bidder': 'nobid', + 'params': { + 'siteId': SITE_ID + }, + 'getFloor': () => { return { currency: 'USD', floor: 1.00 } }, + 'adUnitCode': 'adunit-code', + 'sizes': [[300, 250]], + 'bidId': '30b31c1838de1e', + 'bidderRequestId': '22edbae2733bf6', + 'auctionId': '1d1a030790a475' + } + ]; + + let bidderRequest = { + refererInfo: {page: REFERER} + } + + it('should FLoor = 1', function () { + spec.buildRequests(bidRequests, bidderRequest); + const request = spec.buildRequests(bidRequests, bidderRequest); + /* eslint-disable no-console */ + console.log('request.data:', request.data); + const payload = JSON.parse(request.data); + expect(payload.a[0].floor).to.equal(1); + }); + }); + describe('isBidRequestValid', function () { let bid = { 'bidder': 'nobid', diff --git a/test/spec/modules/novatiqIdSystem_spec.js b/test/spec/modules/novatiqIdSystem_spec.js index b92fb0d219a..6d25601d958 100644 --- a/test/spec/modules/novatiqIdSystem_spec.js +++ b/test/spec/modules/novatiqIdSystem_spec.js @@ -138,16 +138,31 @@ describe('novatiqIdSystem', function () { }); describe('decode', function() { - it('should log message if novatiqId has wrong format', function() { + it('should return the same novatiqId as passed in if not async', function() { const novatiqId = '81b001ec-8914-488c-a96e-8c220d4ee08895ef'; const response = novatiqIdSubmodule.decode(novatiqId); expect(response.novatiq.snowflake).to.have.length(40); }); - it('should log message if novatiqId has wrong format', function() { - const novatiqId = '81b001ec-8914-488c-a96e-8c220d4ee08895ef'; + it('should change the result format if async', function() { + let novatiqId = {}; + novatiqId.id = '81b001ec-8914-488c-a96e-8c220d4ee08895ef'; + novatiqId.syncResponse = 2; const response = novatiqIdSubmodule.decode(novatiqId); - expect(response.novatiq.snowflake).should.be.not.empty; + expect(response.novatiq.ext.syncResponse).should.be.not.empty; + expect(response.novatiq.snowflake.id).should.be.not.empty; + expect(response.novatiq.snowflake.syncResponse).should.be.not.empty; + }); + + it('should remove syncResponse if removeAdditionalInfo true', function() { + let novatiqId = {}; + novatiqId.id = '81b001ec-8914-488c-a96e-8c220d4ee08895ef'; + novatiqId.syncResponse = 2; + var config = {params: {removeAdditionalInfo: true}}; + const response = novatiqIdSubmodule.decode(novatiqId, config); + expect(response.novatiq.ext.syncResponse).should.be.not.empty; + expect(response.novatiq.snowflake.id).should.be.not.empty; + should.equal(response.novatiq.snowflake.syncResponse, undefined); }); }); }) diff --git a/test/spec/modules/oguryBidAdapter_spec.js b/test/spec/modules/oguryBidAdapter_spec.js index f7cdcc0d11a..2d48dca5ebb 100644 --- a/test/spec/modules/oguryBidAdapter_spec.js +++ b/test/spec/modules/oguryBidAdapter_spec.js @@ -245,6 +245,7 @@ describe('OguryBidAdapter', function () { const stubbedWidth = 200 const stubbedHeight = 600 const stubbedCurrentTime = 1234567890 + const stubbedDevicePixelRatio = 1 const stubbedWidthMethod = sinon.stub(window.top.document.documentElement, 'clientWidth').get(function() { return stubbedWidth; }); @@ -255,6 +256,10 @@ describe('OguryBidAdapter', function () { return stubbedCurrentTime; }); + const stubbedDevicePixelMethod = sinon.stub(window, 'devicePixelRatio').get(function() { + return stubbedDevicePixelRatio; + }); + const defaultTimeout = 1000; const expectedRequestObject = { id: bidRequests[0].auctionId, @@ -305,11 +310,12 @@ describe('OguryBidAdapter', function () { }, ext: { prebidversion: '$prebid.version$', - adapterversion: '1.4.0' + adapterversion: '1.4.1' }, device: { w: stubbedWidth, - h: stubbedHeight + h: stubbedHeight, + pxratio: stubbedDevicePixelRatio, } }; @@ -317,6 +323,7 @@ describe('OguryBidAdapter', function () { stubbedWidthMethod.restore(); stubbedHeightMethod.restore(); stubbedCurrentTimeMethod.restore(); + stubbedDevicePixelMethod.restore(); }); it('sends bid request to ENDPOINT via POST', function () { @@ -338,6 +345,14 @@ describe('OguryBidAdapter', function () { stubbedTimelineMethod.restore(); }); + it('send device pixel ratio in bid request', function() { + const validBidRequests = utils.deepClone(bidRequests) + + const request = spec.buildRequests(validBidRequests, bidderRequest); + expect(request.data).to.deep.equal(expectedRequestObject); + expect(request.data.device.pxratio).to.be.a('number'); + }) + it('bid request object should be conform', function () { const validBidRequests = utils.deepClone(bidRequests) @@ -697,7 +712,7 @@ describe('OguryBidAdapter', function () { advertiserDomains: openRtbBidResponse.body.seatbid[0].bid[0].adomain }, nurl: openRtbBidResponse.body.seatbid[0].bid[0].nurl, - adapterVersion: '1.4.0', + adapterVersion: '1.4.1', prebidVersion: '$prebid.version$' }, { requestId: openRtbBidResponse.body.seatbid[0].bid[1].impid, @@ -714,7 +729,7 @@ describe('OguryBidAdapter', function () { advertiserDomains: openRtbBidResponse.body.seatbid[0].bid[1].adomain }, nurl: openRtbBidResponse.body.seatbid[0].bid[1].nurl, - adapterVersion: '1.4.0', + adapterVersion: '1.4.1', prebidVersion: '$prebid.version$' }] diff --git a/test/spec/modules/onetagBidAdapter_spec.js b/test/spec/modules/onetagBidAdapter_spec.js index 2dc0a43bbb0..5bd65cf0fd5 100644 --- a/test/spec/modules/onetagBidAdapter_spec.js +++ b/test/spec/modules/onetagBidAdapter_spec.js @@ -1,8 +1,8 @@ import { spec, isValid, hasTypeVideo, isSchainValid } from 'modules/onetagBidAdapter.js'; import { expect } from 'chai'; -import {find} from 'src/polyfill.js'; +import { find } from 'src/polyfill.js'; import { BANNER, VIDEO } from 'src/mediaTypes.js'; -import {INSTREAM, OUTSTREAM} from 'src/video.js'; +import { INSTREAM, OUTSTREAM } from 'src/video.js'; describe('onetag', function () { function createBid() { @@ -122,14 +122,14 @@ describe('onetag', function () { it('Should return true when correct multi format bid is passed', function () { expect(spec.isBidRequestValid(createMultiFormatBid())).to.be.true; }); - it('Should split multi format bid into two single format bid with same bidId', function() { - const bids = JSON.parse(spec.buildRequests([ createMultiFormatBid() ]).data).bids; + it('Should split multi format bid into two single format bid with same bidId', function () { + const bids = JSON.parse(spec.buildRequests([createMultiFormatBid()]).data).bids; expect(bids.length).to.equal(2); expect(bids[0].bidId).to.equal(bids[1].bidId); }); - it('Should retrieve correct request bid when extracting video request data', function() { + it('Should retrieve correct request bid when extracting video request data', function () { const requestBid = createMultiFormatBid(); - const multiFormatRequest = spec.buildRequests([ requestBid ]); + const multiFormatRequest = spec.buildRequests([requestBid]); const serverResponse = { body: { bids: [ @@ -173,24 +173,30 @@ describe('onetag', function () { const data = JSON.parse(d); it('Should contain all keys', function () { expect(data).to.be.an('object'); - expect(data).to.include.all.keys('location', 'referrer', 'masked', 'sHeight', 'sWidth', 'docHeight', 'wHeight', 'wWidth', 'oHeight', 'oWidth', 'aWidth', 'aHeight', 'sLeft', 'sTop', 'hLength', 'bids', 'docHidden', 'xOffset', 'yOffset', 'timing', 'version'); - expect(data.location).to.be.a('string'); - expect(data.masked).to.be.oneOf([0, 1, 2]); + expect(data).to.include.all.keys('location', 'referrer', 'stack', 'numIframes', 'sHeight', 'sWidth', 'docHeight', 'wHeight', 'wWidth', 'oHeight', 'oWidth', 'aWidth', 'aHeight', 'sLeft', 'sTop', 'hLength', 'bids', 'docHidden', 'xOffset', 'yOffset', 'networkConnectionType', 'networkEffectiveConnectionType', 'timing', 'version'); + expect(data.location).to.satisfy(function (value) { + return value === null || typeof value === 'string'; + }); expect(data.referrer).to.satisfy(referrer => referrer === null || typeof referrer === 'string'); + expect(data.stack).to.be.an('array'); + expect(data.numIframes).to.be.a('number'); expect(data.sHeight).to.be.a('number'); expect(data.sWidth).to.be.a('number'); expect(data.wWidth).to.be.a('number'); expect(data.wHeight).to.be.a('number'); expect(data.oHeight).to.be.a('number'); expect(data.oWidth).to.be.a('number'); - expect(data.ancestorOrigin).to.satisfy(function (value) { - return value === null || typeof value === 'string'; - }); expect(data.aWidth).to.be.a('number'); expect(data.aHeight).to.be.a('number'); expect(data.sLeft).to.be.a('number'); expect(data.sTop).to.be.a('number'); expect(data.hLength).to.be.a('number'); + expect(data.networkConnectionType).to.satisfy(function (value) { + return value === null || typeof value === 'string' + }); + expect(data.networkEffectiveConnectionType).to.satisfy(function (value) { + return value === null || typeof value === 'string' + }); expect(data.bids).to.be.an('array'); expect(data.version).to.have.all.keys('prebid', 'adapter'); const bids = data['bids']; @@ -231,14 +237,14 @@ describe('onetag', function () { expect(bid.pubId).to.be.a('string'); } }); - } catch (e) {} + } catch (e) { } it('Returns empty data if no valid requests are passed', function () { serverRequest = spec.buildRequests([]); let dataString = serverRequest.data; try { let dataObj = JSON.parse(dataString); expect(dataObj.bids).to.be.an('array').that.is.empty; - } catch (e) {} + } catch (e) { } }); it('should send GDPR consent data', function () { let consentString = 'consentString'; @@ -260,6 +266,27 @@ describe('onetag', function () { expect(payload.gdprConsent.consentString).to.exist.and.to.equal(consentString); expect(payload.gdprConsent.consentRequired).to.exist.and.to.be.true; }); + it('Should send GPP consent data', function () { + let consentString = 'consentString'; + let applicableSections = [1, 2, 3]; + let bidderRequest = { + 'bidderCode': 'onetag', + 'auctionId': '1d1a030790a475', + 'bidderRequestId': '22edbae2733bf6', + 'timeout': 3000, + 'gppConsent': { + gppString: consentString, + applicableSections: applicableSections + } + }; + let serverRequest = spec.buildRequests([bannerBid], bidderRequest); + const payload = JSON.parse(serverRequest.data); + + expect(payload).to.exist; + expect(payload.gppConsent).to.exist; + expect(payload.gppConsent.consentString).to.exist.and.to.equal(consentString); + expect(payload.gppConsent.applicableSections).to.have.same.members(applicableSections); + }); it('Should send us privacy string', function () { let consentString = 'us_foo'; let bidderRequest = { @@ -287,7 +314,7 @@ describe('onetag', function () { let dataItem = interpretedResponse[i]; expect(dataItem).to.include.all.keys('requestId', 'cpm', 'width', 'height', 'ttl', 'creativeId', 'netRevenue', 'currency', 'meta', 'dealId'); if (dataItem.meta.mediaType === VIDEO) { - const {context} = find(requestData.bids, (item) => item.bidId === dataItem.requestId); + const { context } = find(requestData.bids, (item) => item.bidId === dataItem.requestId); if (context === INSTREAM) { expect(dataItem).to.include.all.keys('videoCacheKey', 'vastUrl'); expect(dataItem.vastUrl).to.be.a('string'); @@ -321,7 +348,7 @@ describe('onetag', function () { describe('getUserSyncs', function () { const sync_endpoint = 'https://onetag-sys.com/usync/'; it('Returns an iframe if iframeEnabled is true', function () { - const syncs = spec.getUserSyncs({iframeEnabled: true}); + const syncs = spec.getUserSyncs({ iframeEnabled: true }); expect(syncs).to.be.an('array'); expect(syncs.length).to.equal(1); expect(syncs[0].type).to.equal('iframe'); @@ -369,6 +396,28 @@ describe('onetag', function () { expect(syncs[0].url).to.include(sync_endpoint); expect(syncs[0].url).to.not.match(/(?:[?&](?:gdpr_consent=([^&]*)|gdpr=([^&]*)))+$/); }); + it('Must pass gpp consent string when gppConsent object is available', function () { + const syncs = spec.getUserSyncs({ iframeEnabled: true }, {}, {}, {}, { + gppString: 'foo' + }); + expect(syncs[0].type).to.equal('iframe'); + expect(syncs[0].url).to.include(sync_endpoint); + expect(syncs[0].url).to.match(/(?:[?&](?:gpp_consent=foo([^&]*)))+$/); + }); + it('Must pass no gpp params when consentString is null', function () { + const syncs = spec.getUserSyncs({ iframeEnabled: true }, {}, {}, {}, { + gppString: null + }); + expect(syncs[0].type).to.equal('iframe'); + expect(syncs[0].url).to.include(sync_endpoint); + expect(syncs[0].url).to.not.match(/(?:[?&](?:gpp_consent=([^&]*)))+$/); + }); + it('Must pass no gpp params when consentString is empty', function () { + const syncs = spec.getUserSyncs({ iframeEnabled: true }, {}, {}, {}, {}); + expect(syncs[0].type).to.equal('iframe'); + expect(syncs[0].url).to.include(sync_endpoint); + expect(syncs[0].url).to.not.match(/(?:[?&](?:gpp_consent=([^&]*)))+$/); + }); it('Should send us privacy string', function () { let usConsentString = 'us_foo'; const syncs = spec.getUserSyncs({ iframeEnabled: true }, {}, {}, usConsentString); @@ -383,16 +432,16 @@ describe('onetag', function () { expect(isSchainValid(undefined)).to.be.false; }); it('Should return false when schain is missing nodes key', function () { - const schain = {'otherKey': 'otherValue'}; + const schain = { 'otherKey': 'otherValue' }; expect(isSchainValid(schain)).to.be.false; }); it('Should return false when schain is missing one of the required SupplyChainNode attribute', function () { - const missingAsiNode = {'sid': '00001', 'hp': 1}; - const missingSidNode = {'asi': 'indirectseller.com', 'hp': 1}; - const missingHpNode = {'asi': 'indirectseller.com', 'sid': '00001'}; - expect(isSchainValid({'config': {'nodes': [missingAsiNode]}})).to.be.false; - expect(isSchainValid({'config': {'nodes': [missingSidNode]}})).to.be.false; - expect(isSchainValid({'config': {'nodes': [missingHpNode]}})).to.be.false; + const missingAsiNode = { 'sid': '00001', 'hp': 1 }; + const missingSidNode = { 'asi': 'indirectseller.com', 'hp': 1 }; + const missingHpNode = { 'asi': 'indirectseller.com', 'sid': '00001' }; + expect(isSchainValid({ 'config': { 'nodes': [missingAsiNode] } })).to.be.false; + expect(isSchainValid({ 'config': { 'nodes': [missingSidNode] } })).to.be.false; + expect(isSchainValid({ 'config': { 'nodes': [missingHpNode] } })).to.be.false; }); it('Should return true when schain contains all required attributes', function () { const validSchain = { diff --git a/test/spec/modules/openxBidAdapter_spec.js b/test/spec/modules/openxBidAdapter_spec.js index cc1c2d1e607..d8ea79ac698 100644 --- a/test/spec/modules/openxBidAdapter_spec.js +++ b/test/spec/modules/openxBidAdapter_spec.js @@ -1,204 +1,81 @@ import {expect} from 'chai'; -import {spec, USER_ID_CODE_TO_QUERY_ARG} from 'modules/openxBidAdapter.js'; +import {spec, REQUEST_URL, SYNC_URL, DEFAULT_PH} from 'modules/openxOrtbBidAdapter.js'; import {newBidder} from 'src/adapters/bidderFactory.js'; import {BANNER, VIDEO} from 'src/mediaTypes.js'; -import {userSync} from 'src/userSync.js'; import {config} from 'src/config.js'; import * as utils from 'src/utils.js'; +// load modules that register ORTB processors +import 'src/prebid.js' +import 'modules/currency.js'; +import 'modules/userId/index.js'; +import 'modules/multibid/index.js'; +import 'modules/priceFloors.js'; +import 'modules/consentManagement.js'; +import 'modules/consentManagementUsp.js'; +import 'modules/schain.js'; +import {deepClone} from 'src/utils.js'; +import {syncAddFPDToBidderRequest} from '../../helpers/fpd.js'; +import {hook} from '../../../src/hook.js'; + +const DEFAULT_SYNC = SYNC_URL + '?ph=' + DEFAULT_PH; + +const BidRequestBuilder = function BidRequestBuilder(options) { + const defaults = { + request: { + auctionId: '4fd1ca2d-846c-4211-b9e5-321dfe1709c9', + adUnitCode: 'adunit-code', + bidder: 'openx' + }, + params: { + unit: '12345678', + delDomain: 'test-del-domain' + }, + sizes: [[300, 250], [300, 600]], + }; -const URLBASE = '/w/1.0/arj'; -const URLBASEVIDEO = '/v/1.0/avjp'; - -describe('OpenxAdapter', function () { - const adapter = newBidder(spec); - - /** - * Type Definitions - */ - - /** - * @typedef {{ - * impression: string, - * inview: string, - * click: string - * }} - */ - let OxArjTracking; - /** - * @typedef {{ - * ads: { - * version: number, - * count: number, - * pixels: string, - * ad: Array - * } - * }} - */ - let OxArjResponse; - /** - * @typedef {{ - * adunitid: number, - * adid:number, - * type: string, - * htmlz: string, - * framed: number, - * is_fallback: number, - * ts: string, - * cpipc: number, - * pub_rev: string, - * tbd: ?string, - * adv_id: string, - * deal_id: string, - * auct_win_is_deal: number, - * brand_id: string, - * currency: string, - * idx: string, - * creative: Array - * }} - */ - let OxArjAdUnit; - /** - * @typedef {{ - * id: string, - * width: string, - * height: string, - * target: string, - * mime: string, - * media: string, - * tracking: OxArjTracking - * }} - */ - let OxArjCreative; - - // HELPER METHODS - /** - * @type {OxArjCreative} - */ - const DEFAULT_TEST_ARJ_CREATIVE = { - id: '0', - width: 'test-width', - height: 'test-height', - target: 'test-target', - mime: 'test-mime', - media: 'test-media', - tracking: { - impression: 'test-impression', - inview: 'test-inview', - click: 'test-click' - } + const request = { + ...defaults.request, + ...options }; - /** - * @type {OxArjAdUnit} - */ - const DEFAULT_TEST_ARJ_AD_UNIT = { - adunitid: 0, - type: 'test-type', - html: 'test-html', - framed: 0, - is_fallback: 0, - ts: 'test-ts', - tbd: 'NaN', - deal_id: undefined, - auct_win_is_deal: undefined, - cpipc: 0, - pub_rev: 'test-pub_rev', - adv_id: 'test-adv_id', - brand_id: 'test-brand_id', - currency: 'test-currency', - idx: '0', - creative: [DEFAULT_TEST_ARJ_CREATIVE] + this.withParams = (options) => { + request.params = { + ...defaults.params, + ...options + }; + return this; }; - /** - * @type {OxArjResponse} - */ - const DEFAULT_ARJ_RESPONSE = { - ads: { - version: 0, - count: 1, - pixels: 'https://testpixels.net', - ad: [DEFAULT_TEST_ARJ_AD_UNIT] + this.build = () => request; +}; + +const BidderRequestBuilder = function BidderRequestBuilder(options) { + const defaults = { + bidderCode: 'openx', + auctionId: '4fd1ca2d-846c-4211-b9e5-321dfe1709c9', + bidderRequestId: '7g36s867Tr4xF90X', + timeout: 3000, + refererInfo: { + numIframes: 0, + reachedTop: true, + referer: 'http://test.io/index.html?pbjs_debug=true' } }; - // Sample bid requests + const request = { + ...defaults, + ...options + }; - const BANNER_BID_REQUESTS_WITH_MEDIA_TYPES = [{ - bidder: 'openx', - params: { - unit: '11', - delDomain: 'test-del-domain' - }, - adUnitCode: '/adunit-code/test-path', - mediaTypes: { - banner: { - sizes: [[300, 250], [300, 600]] - } - }, - bidId: 'test-bid-id-1', - bidderRequestId: 'test-bid-request-1', - auctionId: 'test-auction-1', - ortb2Imp: { ext: { data: { pbadslot: '/12345/my-gpt-tag-0' } } }, - }, { - bidder: 'openx', - params: { - unit: '22', - delDomain: 'test-del-domain' - }, - adUnitCode: 'adunit-code', - mediaTypes: { - banner: { - sizes: [[728, 90]] - } - }, - bidId: 'test-bid-id-2', - bidderRequestId: 'test-bid-request-2', - auctionId: 'test-auction-2', - ortb2Imp: { ext: { data: { pbadslot: '/12345/my-gpt-tag-1' } } }, - }]; - - const VIDEO_BID_REQUESTS_WITH_MEDIA_TYPES = [{ - bidder: 'openx', - mediaTypes: { - video: { - playerSize: [640, 480] - } - }, - params: { - unit: '12345678', - delDomain: 'test-del-domain' - }, - adUnitCode: 'adunit-code', + this.build = () => request; +}; - bidId: '30b31c1838de1e', - bidderRequestId: '22edbae2733bf6', - auctionId: '1d1a030790a475', - transactionId: '4008d88a-8137-410b-aa35-fbfdabcb478e', - ortb2Imp: { ext: { data: { pbadslot: '/12345/my-gpt-tag-0' } } }, - }]; +describe('OpenxRtbAdapter', function () { + before(() => { + hook.ready(); + }); - const MULTI_FORMAT_BID_REQUESTS = [{ - bidder: 'openx', - params: { - unit: '12345678', - delDomain: 'test-del-domain' - }, - adUnitCode: 'adunit-code', - mediaTypes: { - banner: { - sizes: [[300, 250]] - }, - video: { - playerSize: [300, 250] - } - }, - bidId: '30b31c1838de1e', - bidderRequestId: '22edbae2733bf6', - auctionId: '1d1a030790a475', - transactionId: '4008d88a-8137-410b-aa35-fbfdabcb478e', - ortb2Imp: { ext: { data: { pbadslot: '/12345/my-gpt-tag-0' } } }, - }]; + const adapter = newBidder(spec); describe('inherited functions', function () { it('exists and is a function', function () { @@ -206,7 +83,7 @@ describe('OpenxAdapter', function () { }); }); - describe('isBidRequestValid', function () { + describe('isBidRequestValid()', function () { describe('when request is for a banner ad', function () { let bannerBid; beforeEach(function () { @@ -259,8 +136,28 @@ describe('OpenxAdapter', function () { describe('when request is for a multiformat ad', function () { describe('and request config uses mediaTypes video and banner', () => { + const multiformatBid = { + bidder: 'openx', + params: { + unit: '12345678', + delDomain: 'test-del-domain' + }, + adUnitCode: 'adunit-code', + mediaTypes: { + banner: { + sizes: [[300, 250]] + }, + video: { + playerSize: [300, 250] + } + }, + bidId: '30b31c1838de1e', + bidderRequestId: '22edbae2733bf6', + auctionId: '1d1a030790a475', + transactionId: '4008d88a-8137-410b-aa35-fbfdabcb478e' + }; it('should return true multisize when required params found', function () { - expect(spec.isBidRequestValid(MULTI_FORMAT_BID_REQUESTS[0])).to.equal(true); + expect(spec.isBidRequestValid(multiformatBid)).to.equal(true); }); }); }); @@ -349,1468 +246,1035 @@ describe('OpenxAdapter', function () { expect(spec.isBidRequestValid(videoBidWithMediaType)).to.equal(false); }); }); - - describe('and request config uses test', () => { - const videoBidWithTest = { - bidder: 'openx', - params: { - unit: '12345678', - delDomain: 'test-del-domain', - test: true - }, - adUnitCode: 'adunit-code', - mediaTypes: { - video: { - playerSize: [640, 480] - } - }, - bidId: '30b31c1838de1e', - bidderRequestId: '22edbae2733bf6', - auctionId: '1d1a030790a475', - transactionId: '4008d88a-8137-410b-aa35-fbfdabcb478e' - }; - - let mockBidderRequest = {refererInfo: {}}; - - it('should return true when required params found', function () { - expect(spec.isBidRequestValid(videoBidWithTest)).to.equal(true); - }); - - it('should send video bid request to openx url via GET, with vtest=1 video parameter', function () { - const request = spec.buildRequests([videoBidWithTest], mockBidderRequest); - expect(request[0].data.vtest).to.equal(1); - }); - }); }); }); - describe('buildRequests for banner ads', function () { - const bidRequestsWithMediaTypes = BANNER_BID_REQUESTS_WITH_MEDIA_TYPES; - - const bidRequestsWithPlatform = [{ - 'bidder': 'openx', - 'params': { - 'unit': '11', - 'platform': '1cabba9e-cafe-3665-beef-f00f00f00f00' - }, - 'adUnitCode': '/adunit-code/test-path', - mediaTypes: { - banner: { - sizes: [[300, 250], [300, 600]] - } - }, - 'bidId': 'test-bid-id-1', - 'bidderRequestId': 'test-bid-request-1', - 'auctionId': 'test-auction-1' - }, { - 'bidder': 'openx', - 'params': { - 'unit': '11', - 'platform': '1cabba9e-cafe-3665-beef-f00f00f00f00' - }, - 'adUnitCode': '/adunit-code/test-path', - mediaTypes: { - banner: { - sizes: [[300, 250], [300, 600]] - } - }, - 'bidId': 'test-bid-id-1', - 'bidderRequestId': 'test-bid-request-1', - 'auctionId': 'test-auction-1' - }]; - - const mockBidderRequest = {refererInfo: {}}; - - it('should send bid request to openx url via GET, with mediaTypes specified with banner type', function () { - const request = spec.buildRequests(bidRequestsWithMediaTypes, mockBidderRequest); - expect(request[0].url).to.equal('https://' + bidRequestsWithMediaTypes[0].params.delDomain + URLBASE); - expect(request[0].data.ph).to.be.undefined; - expect(request[0].method).to.equal('GET'); - }); - - it('should send bid request to openx platform url via GET, if platform is present', function () { - const request = spec.buildRequests(bidRequestsWithPlatform, mockBidderRequest); - expect(request[0].url).to.equal(`https://u.openx.net${URLBASE}`); - expect(request[0].data.ph).to.equal(bidRequestsWithPlatform[0].params.platform); - expect(request[0].method).to.equal('GET'); - }); - - it('should send bid request to openx platform url via GET, if both params present', function () { - const bidRequestsWithPlatformAndDelDomain = [{ - 'bidder': 'openx', - 'params': { - 'unit': '11', - 'delDomain': 'test-del-domain', - 'platform': '1cabba9e-cafe-3665-beef-f00f00f00f00' - }, - 'adUnitCode': '/adunit-code/test-path', - mediaTypes: { - banner: { - sizes: [[300, 250], [300, 600]] - } - }, - 'bidId': 'test-bid-id-1', - 'bidderRequestId': 'test-bid-request-1', - 'auctionId': 'test-auction-1' - }, { - 'bidder': 'openx', - 'params': { - 'unit': '11', - 'delDomain': 'test-del-domain', - 'platform': '1cabba9e-cafe-3665-beef-f00f00f00f00' - }, - 'adUnitCode': '/adunit-code/test-path', - mediaTypes: { - banner: { - sizes: [[300, 250], [300, 600]] - } - }, - 'bidId': 'test-bid-id-1', - 'bidderRequestId': 'test-bid-request-1', - 'auctionId': 'test-auction-1' - }]; - - const request = spec.buildRequests(bidRequestsWithPlatformAndDelDomain, mockBidderRequest); - expect(request[0].url).to.equal(`https://u.openx.net${URLBASE}`); - expect(request[0].data.ph).to.equal(bidRequestsWithPlatform[0].params.platform); - expect(request[0].method).to.equal('GET'); - }); + describe('buildRequests()', function () { + let bidRequestsWithMediaTypes; + let bidRequestsWithPlatform; + let mockBidderRequest; - it('should send the adunit codes', function () { - const request = spec.buildRequests(bidRequestsWithMediaTypes, mockBidderRequest); - expect(request[0].data.divids).to.equal(`${encodeURIComponent(bidRequestsWithMediaTypes[0].adUnitCode)},${encodeURIComponent(bidRequestsWithMediaTypes[1].adUnitCode)}`); - }); - - it('should send the gpids', function () { - const request = spec.buildRequests(bidRequestsWithMediaTypes, mockBidderRequest); - expect(request[0].data.aucs).to.equal(`${encodeURIComponent('/12345/my-gpt-tag-0')},${encodeURIComponent('/12345/my-gpt-tag-1')}`); - }); + beforeEach(function () { + mockBidderRequest = {refererInfo: {}}; - it('should send ad unit ids when any are defined', function () { - const bidRequestsWithUnitIds = [{ - 'bidder': 'openx', - 'params': { - 'delDomain': 'test-del-domain' + bidRequestsWithMediaTypes = [{ + bidder: 'openx', + params: { + unit: '11', + delDomain: 'test-del-domain', + platform: '1cabba9e-cafe-3665-beef-f00f00f00f00', }, - 'adUnitCode': 'adunit-code', + adUnitCode: '/adunit-code/test-path', mediaTypes: { banner: { sizes: [[300, 250], [300, 600]] } }, - 'bidId': 'test-bid-id-1', - 'bidderRequestId': 'test-bid-request-1', - 'auctionId': 'test-auction-1' - }, { - 'bidder': 'openx', - 'params': { - 'unit': '22', - 'delDomain': 'test-del-domain' - }, - 'adUnitCode': 'adunit-code', - mediaTypes: { - banner: { - sizes: [[728, 90]] - } - }, - 'bidId': 'test-bid-id-2', - 'bidderRequestId': 'test-bid-request-2', - 'auctionId': 'test-auction-2' - }]; - const request = spec.buildRequests(bidRequestsWithUnitIds, mockBidderRequest); - expect(request[0].data.auid).to.equal(`,${bidRequestsWithUnitIds[1].params.unit}`); - }); - - it('should not send any ad unit ids when none are defined', function () { - const bidRequestsWithoutUnitIds = [{ - 'bidder': 'openx', - 'params': { - 'delDomain': 'test-del-domain' - }, - 'adUnitCode': 'adunit-code', - mediaTypes: { - banner: { - sizes: [[300, 250], [300, 600]] + bidId: 'test-bid-id-1', + bidderRequestId: 'test-bid-request-1', + auctionId: 'test-auction-1', + transactionId: 'test-transactionId-1', + ortb2Imp: { + ext: { + ae: 2 } - }, - 'bidId': 'test-bid-id-1', - 'bidderRequestId': 'test-bid-request-1', - 'auctionId': 'test-auction-1' + } }, { - 'bidder': 'openx', - 'params': { - 'delDomain': 'test-del-domain' + bidder: 'openx', + params: { + unit: '22', + delDomain: 'test-del-domain', + platform: '1cabba9e-cafe-3665-beef-f00f00f00f00', }, - 'adUnitCode': 'adunit-code', + adUnitCode: 'adunit-code', mediaTypes: { - banner: { - sizes: [[728, 90]] + video: { + playerSize: [640, 480] } }, - 'bidId': 'test-bid-id-2', - 'bidderRequestId': 'test-bid-request-2', - 'auctionId': 'test-auction-2' + bidId: 'test-bid-id-2', + bidderRequestId: 'test-bid-request-2', + auctionId: 'test-auction-2', + transactionId: 'test-transactionId-2' }]; - const request = spec.buildRequests(bidRequestsWithoutUnitIds, mockBidderRequest); - expect(request[0].data).to.not.have.any.keys('auid'); }); - it('should send out custom params on bids that have customParams specified', function () { - const bidRequest = Object.assign({}, - bidRequestsWithMediaTypes[0], - { - params: { - 'unit': '12345678', - 'delDomain': 'test-del-domain', - 'customParams': {'Test1': 'testval1+', 'test2': ['testval2/', 'testval3']} - } + context('common requests checks', function() { + it('should be able to handle multiformat requests', () => { + const multiformat = utils.deepClone(bidRequestsWithMediaTypes[0]); + multiformat.mediaTypes.video = { + context: 'outstream', + playerSize: [640, 480] } - ); + const requests = spec.buildRequests([multiformat], mockBidderRequest); + const outgoingFormats = requests.flatMap(rq => rq.data.imp.flatMap(imp => ['banner', 'video'].filter(k => imp[k] != null))); + const expected = FEATURES.VIDEO ? ['banner', 'video'] : ['banner'] + expect(outgoingFormats).to.have.members(expected); + }) - const request = spec.buildRequests([bidRequest], mockBidderRequest); - const dataParams = request[0].data; + it('should send bid request to openx url via POST', function () { + const request = spec.buildRequests(bidRequestsWithMediaTypes, mockBidderRequest); + expect(request[0].url).to.equal(REQUEST_URL); + expect(request[0].method).to.equal('POST'); + }); - expect(dataParams.tps).to.exist; - expect(dataParams.tps).to.equal(btoa('test1=testval1.&test2=testval2_,testval3')); - }); + it('should send delivery domain, if available', function () { + const request = spec.buildRequests(bidRequestsWithMediaTypes, mockBidderRequest); + expect(request[0].data.ext.delDomain).to.equal(bidRequestsWithMediaTypes[0].params.delDomain); + expect(request[0].data.ext.platformId).to.be.undefined; + }); - it('should send out custom bc parameter, if override is present', function () { - const bidRequest = Object.assign({}, - bidRequestsWithMediaTypes[0], - { - params: { - 'unit': '12345678', - 'delDomain': 'test-del-domain', - 'bc': 'hb_override' - } - } - ); + it('should send platform id, if available', function () { + bidRequestsWithMediaTypes[0].params.platform = '1cabba9e-cafe-3665-beef-f00f00f00f00'; + bidRequestsWithMediaTypes[1].params.platform = '1cabba9e-cafe-3665-beef-f00f00f00f00'; - const request = spec.buildRequests([bidRequest], mockBidderRequest); - const dataParams = request[0].data; + const request = spec.buildRequests(bidRequestsWithMediaTypes, mockBidderRequest); + expect(request[0].data.ext.platform).to.equal(bidRequestsWithMediaTypes[0].params.platform); + expect(request[1].data.ext.platform).to.equal(bidRequestsWithMediaTypes[0].params.platform); + }); - expect(dataParams.bc).to.exist; - expect(dataParams.bc).to.equal('hb_override'); - }); + it('should send openx adunit codes', function () { + const request = spec.buildRequests(bidRequestsWithMediaTypes, mockBidderRequest); + expect(request[0].data.imp[0].tagid).to.equal(bidRequestsWithMediaTypes[0].params.unit); + expect(request[1].data.imp[0].tagid).to.equal(bidRequestsWithMediaTypes[1].params.unit); + }); - it('should not send any consent management properties', function () { - const request = spec.buildRequests(bidRequestsWithMediaTypes, mockBidderRequest); - expect(request[0].data.gdpr).to.equal(undefined); - expect(request[0].data.gdpr_consent).to.equal(undefined); - expect(request[0].data.x_gdpr_f).to.equal(undefined); - }); + it('should send out custom params on bids that have customParams specified', function () { + const bidRequest = Object.assign({}, + bidRequestsWithMediaTypes[0], + { + params: { + unit: '12345678', + delDomain: 'test-del-domain', + customParams: {'Test1': 'testval1+', 'test2': ['testval2/', 'testval3']} + } + } + ); - describe('when there is a consent management framework', function () { - let bidRequests; - let mockConfig; - let bidderRequest; - const IAB_CONSENT_FRAMEWORK_CODE = 1; + mockBidderRequest.bids = [bidRequest]; + const request = spec.buildRequests([bidRequest], mockBidderRequest); + expect(request[0].data.imp[0].ext.customParams).to.equal(bidRequest.params.customParams); + }) - beforeEach(function () { - bidRequests = [{ - bidder: 'openx', - params: { - unit: '12345678-banner', - delDomain: 'test-del-domain' - }, - adUnitCode: 'adunit-code', - mediaTypes: { - banner: { - sizes: [[300, 250], [300, 600]] - } - }, - bidId: 'test-bid-id', - bidderRequestId: 'test-bidder-request-id', - auctionId: 'test-auction-id' - }, { - 'bidder': 'openx', - 'mediaTypes': { - video: { - playerSize: [640, 480] + describe('floors', function () { + it('should send out custom floors on bids that have customFloors, no currency as account currency is used', function () { + const bidRequest = Object.assign({}, + bidRequestsWithMediaTypes[0], + { + params: { + unit: '12345678', + delDomain: 'test-del-domain', + customFloor: 1.500 + } } - }, - 'params': { - 'unit': '12345678-video', - 'delDomain': 'test-del-domain' - }, - 'adUnitCode': 'adunit-code', + ); - bidId: 'test-bid-id', - bidderRequestId: 'test-bidder-request-id', - auctionId: 'test-auction-id', - transactionId: '4008d88a-8137-410b-aa35-fbfdabcb478e' - }]; - }); + const request = spec.buildRequests([bidRequest], mockBidderRequest); + expect(request[0].data.imp[0].bidfloor).to.equal(bidRequest.params.customFloor); + expect(request[0].data.imp[0].bidfloorcur).to.equal(undefined); + }); - afterEach(function () { - config.getConfig.restore(); - }); + context('with floors module', function () { + let adServerCurrencyStub; - describe('when us_privacy applies', function () { - beforeEach(function () { - bidderRequest = { - uspConsent: '1YYN', - refererInfo: {} - }; + beforeEach(function () { + adServerCurrencyStub = sinon + .stub(config, 'getConfig') + .withArgs('currency.adServerCurrency') + }); - sinon.stub(config, 'getConfig').callsFake((key) => { - return utils.deepAccess(mockConfig, key); + afterEach(function () { + config.getConfig.restore(); }); - }); - it('should send a signal to specify that GDPR applies to this request', function () { - const request = spec.buildRequests(bidRequests, bidderRequest); - expect(request[0].data.us_privacy).to.equal('1YYN'); - expect(request[1].data.us_privacy).to.equal('1YYN'); - }); - }); + it('should send out floors on bids in USD', function () { + const bidRequest = Object.assign({}, + bidRequestsWithMediaTypes[0], + { + getFloor: () => { + return { + currency: 'USD', + floor: 9.99 + } + } + } + ); - describe('when us_privacy does not applies', function () { - beforeEach(function () { - bidderRequest = { - refererInfo: {} - }; + const request = spec.buildRequests([bidRequest], mockBidderRequest); + expect(request[0].data.imp[0].bidfloor).to.equal(9.99); + expect(request[0].data.imp[0].bidfloorcur).to.equal('USD'); + }); - sinon.stub(config, 'getConfig').callsFake((key) => { - return utils.deepAccess(mockConfig, key); + it('should send not send floors', function () { + adServerCurrencyStub.returns('EUR'); + const bidRequest = Object.assign({}, + bidRequestsWithMediaTypes[0], + { + getFloor: () => { + return { + currency: 'BTC', + floor: 9.99 + } + } + } + ); + + const request = spec.buildRequests([bidRequest], mockBidderRequest); + expect(request[0].data.imp[0].bidfloor).to.equal(undefined) + expect(request[0].data.imp[0].bidfloorcur).to.equal(undefined) }); - }); + }) + }) - it('should not send the consent string, when consent string is undefined', function () { - delete bidderRequest.uspConsent; - const request = spec.buildRequests(bidRequests, bidderRequest); - expect(request[0].data).to.not.have.property('us_privacy'); - expect(request[1].data).to.not.have.property('us_privacy'); - }); - }); + describe('FPD', function() { + let bidRequests; + const mockBidderRequest = {refererInfo: {}}; - describe('when GDPR applies', function () { beforeEach(function () { - bidderRequest = { - gdprConsent: { - consentString: 'test-gdpr-consent-string', - gdprApplies: true + bidRequests = [{ + bidder: 'openx', + params: { + unit: '12345678-banner', + delDomain: 'test-del-domain' }, - refererInfo: {} - }; + adUnitCode: 'adunit-code', + mediaTypes: { + banner: { + sizes: [[300, 250], [300, 600]] + } + }, + bidId: 'test-bid-id', + bidderRequestId: 'test-bidder-request-id', + auctionId: 'test-auction-id', + transactionId: 'test-transaction-id-1' + }, { + bidder: 'openx', + mediaTypes: { + video: { + playerSize: [640, 480] + } + }, + params: { + unit: '12345678-video', + delDomain: 'test-del-domain' + }, + 'adUnitCode': 'adunit-code', - mockConfig = { - consentManagement: { - cmpApi: 'iab', - timeout: 1111, - allowAuctionWithoutConsent: 'cancel' - } - }; + bidId: 'test-bid-id', + bidderRequestId: 'test-bidder-request-id', + auctionId: 'test-auction-id', + transactionId: 'test-transaction-id-2' + }]; + }); - sinon.stub(config, 'getConfig').callsFake((key) => { - return utils.deepAccess(mockConfig, key); + it('ortb2.site should be merged in the request', function() { + const request = spec.buildRequests(bidRequests, { + ...mockBidderRequest, + 'ortb2': { + site: { + domain: 'page.example.com', + cat: ['IAB2'], + sectioncat: ['IAB2-2'] + } + } }); + let data = request[0].data; + expect(data.site.domain).to.equal('page.example.com'); + expect(data.site.cat).to.deep.equal(['IAB2']); + expect(data.site.sectioncat).to.deep.equal(['IAB2-2']); }); - it('should send a signal to specify that GDPR applies to this request', function () { - const request = spec.buildRequests(bidRequests, bidderRequest); - expect(request[0].data.gdpr).to.equal(1); - expect(request[1].data.gdpr).to.equal(1); + it('ortb2.user should be merged in the request', function() { + const request = spec.buildRequests(bidRequests, { + ...mockBidderRequest, + 'ortb2': { + user: { + yob: 1985 + } + } + }); + let data = request[0].data; + expect(data.user.yob).to.equal(1985); }); - it('should send the consent string', function () { - const request = spec.buildRequests(bidRequests, bidderRequest); - expect(request[0].data.gdpr_consent).to.equal(bidderRequest.gdprConsent.consentString); - expect(request[1].data.gdpr_consent).to.equal(bidderRequest.gdprConsent.consentString); - }); + describe('ortb2Imp', function() { + describe('ortb2Imp.ext.data.pbadslot', function() { + beforeEach(function () { + if (bidRequests[0].hasOwnProperty('ortb2Imp')) { + delete bidRequests[0].ortb2Imp; + } + }); - it('should send the consent management framework code', function () { - const request = spec.buildRequests(bidRequests, bidderRequest); - expect(request[0].data.x_gdpr_f).to.equal(IAB_CONSENT_FRAMEWORK_CODE); - expect(request[1].data.x_gdpr_f).to.equal(IAB_CONSENT_FRAMEWORK_CODE); - }); - }); + it('should not send if imp[].ext.data object is invalid', function() { + bidRequests[0].ortb2Imp = { + ext: {} + }; + const request = spec.buildRequests(bidRequests, mockBidderRequest); + let data = request[0].data; + expect(data.imp[0].ext).to.not.have.property('data'); + }); - describe('when GDPR does not apply', function () { - beforeEach(function () { - bidderRequest = { - gdprConsent: { - consentString: 'test-gdpr-consent-string', - gdprApplies: false - }, - refererInfo: {} - }; - - mockConfig = { - consentManagement: { - cmpApi: 'iab', - timeout: 1111, - allowAuctionWithoutConsent: 'cancel' - } - }; + it('should not send if imp[].ext.data.pbadslot is undefined', function() { + bidRequests[0].ortb2Imp = { + ext: { + data: { + } + } + }; + const request = spec.buildRequests(bidRequests, mockBidderRequest); + let data = request[0].data; + if (data.imp[0].ext.data) { + expect(data.imp[0].ext.data).to.not.have.property('pbadslot'); + } else { + expect(data.imp[0].ext).to.not.have.property('data'); + } + }); - sinon.stub(config, 'getConfig').callsFake((key) => { - return utils.deepAccess(mockConfig, key); + it('should send if imp[].ext.data.pbadslot is string', function() { + bidRequests[0].ortb2Imp = { + ext: { + data: { + pbadslot: 'abcd' + } + } + }; + const request = spec.buildRequests(bidRequests, mockBidderRequest); + let data = request[0].data; + expect(data.imp[0].ext.data).to.have.property('pbadslot'); + expect(data.imp[0].ext.data.pbadslot).to.equal('abcd'); + }); }); - }); - it('should not send a signal to specify that GDPR does not apply to this request', function () { - const request = spec.buildRequests(bidRequests, bidderRequest); - expect(request[0].data.gdpr).to.equal(0); - expect(request[1].data.gdpr).to.equal(0); - }); + describe('ortb2Imp.ext.data.adserver', function() { + beforeEach(function () { + if (bidRequests[0].hasOwnProperty('ortb2Imp')) { + delete bidRequests[0].ortb2Imp; + } + }); - it('should send the consent string', function () { - const request = spec.buildRequests(bidRequests, bidderRequest); - expect(request[0].data.gdpr_consent).to.equal(bidderRequest.gdprConsent.consentString); - expect(request[1].data.gdpr_consent).to.equal(bidderRequest.gdprConsent.consentString); - }); + it('should not send if imp[].ext.data object is invalid', function() { + bidRequests[0].ortb2Imp = { + ext: {} + }; + const request = spec.buildRequests(bidRequests, mockBidderRequest); + let data = request[0].data; + expect(data.imp[0].ext).to.not.have.property('data'); + }); - it('should send the consent management framework code', function () { - const request = spec.buildRequests(bidRequests, bidderRequest); - expect(request[0].data.x_gdpr_f).to.equal(IAB_CONSENT_FRAMEWORK_CODE); - expect(request[1].data.x_gdpr_f).to.equal(IAB_CONSENT_FRAMEWORK_CODE); - }); - }); + it('should not send if imp[].ext.data.adserver is undefined', function() { + bidRequests[0].ortb2Imp = { + ext: { + data: { + } + } + }; + const request = spec.buildRequests(bidRequests, mockBidderRequest); + let data = request[0].data; + if (data.imp[0].ext.data) { + expect(data.imp[0].ext.data).to.not.have.property('adserver'); + } else { + expect(data.imp[0].ext).to.not.have.property('data'); + } + }); - describe('when GDPR consent has undefined data', function () { - beforeEach(function () { - bidderRequest = { - gdprConsent: { - consentString: 'test-gdpr-consent-string', - gdprApplies: true - }, - refererInfo: {} - }; + it('should send', function() { + let adSlotValue = 'abc'; + bidRequests[0].ortb2Imp = { + ext: { + data: { + adserver: { + name: 'GAM', + adslot: adSlotValue + } + } + } + }; + const request = spec.buildRequests(bidRequests, mockBidderRequest); + let data = request[0].data; + expect(data.imp[0].ext.data.adserver.name).to.equal('GAM'); + expect(data.imp[0].ext.data.adserver.adslot).to.equal(adSlotValue); + }); + }); - mockConfig = { - consentManagement: { - cmpApi: 'iab', - timeout: 1111, - allowAuctionWithoutConsent: 'cancel' - } - }; + describe('ortb2Imp.ext.data.other', function() { + beforeEach(function () { + if (bidRequests[0].hasOwnProperty('ortb2Imp')) { + delete bidRequests[0].ortb2Imp; + } + }); - sinon.stub(config, 'getConfig').callsFake((key) => { - return utils.deepAccess(mockConfig, key); - }); - }); + it('should not send if imp[].ext.data object is invalid', function() { + bidRequests[0].ortb2Imp = { + ext: {} + }; + const request = spec.buildRequests(bidRequests, mockBidderRequest); + let data = request[0].data; + expect(data.imp[0].ext).to.not.have.property('data'); + }); - it('should not send a signal to specify whether GDPR applies to this request, when GDPR application is undefined', function () { - delete bidderRequest.gdprConsent.gdprApplies; - const request = spec.buildRequests(bidRequests, bidderRequest); - expect(request[0].data).to.not.have.property('gdpr'); - expect(request[1].data).to.not.have.property('gdpr'); - }); + it('should not send if imp[].ext.data.other is undefined', function() { + bidRequests[0].ortb2Imp = { + ext: { + data: { + } + } + }; + const request = spec.buildRequests(bidRequests, mockBidderRequest); + let data = request[0].data; + if (data.imp[0].ext.data) { + expect(data.imp[0].ext.data).to.not.have.property('other'); + } else { + expect(data.imp[0].ext).to.not.have.property('data'); + } + }); - it('should not send the consent string, when consent string is undefined', function () { - delete bidderRequest.gdprConsent.consentString; - const request = spec.buildRequests(bidRequests, bidderRequest); - expect(request[0].data).to.not.have.property('gdpr_consent'); - expect(request[1].data).to.not.have.property('gdpr_consent'); + it('ortb2Imp.ext.data.other', function() { + bidRequests[0].ortb2Imp = { + ext: { + data: { + other: 1234 + } + } + }; + const request = spec.buildRequests(bidRequests, mockBidderRequest); + let data = request[0].data; + expect(data.imp[0].ext.data.other).to.equal(1234); + }); + }); }); - it('should not send the consent management framework code, when format is undefined', function () { - delete mockConfig.consentManagement.cmpApi; - const request = spec.buildRequests(bidRequests, bidderRequest); - expect(request[0].data).to.not.have.property('x_gdpr_f'); - expect(request[1].data).to.not.have.property('x_gdpr_f'); + describe('with user agent client hints', function () { + it('should add device.sua if available', function () { + const bidderRequestWithUserAgentClientHints = { refererInfo: {}, + ortb2: { + device: { + sua: { + source: 2, + platform: { + brand: 'macOS', + version: [ '12', '4', '0' ] + }, + browsers: [ + { + brand: 'Chromium', + version: [ '106', '0', '5249', '119' ] + }, + { + brand: 'Google Chrome', + version: [ '106', '0', '5249', '119' ] + }, + { + brand: 'Not;A=Brand', + version: [ '99', '0', '0', '0' ] + }], + mobile: 0, + model: 'Pro', + bitness: '64', + architecture: 'x86' + } + } + }}; + + let request = spec.buildRequests(bidRequests, bidderRequestWithUserAgentClientHints); + expect(request[0].data.device.sua).to.exist; + expect(request[0].data.device.sua).to.deep.equal(bidderRequestWithUserAgentClientHints.ortb2.device.sua); + const bidderRequestWithoutUserAgentClientHints = {refererInfo: {}, ortb2: {}}; + request = spec.buildRequests(bidRequests, bidderRequestWithoutUserAgentClientHints); + expect(request[0].data.device?.sua).to.not.exist; + }); }); }); - }); - it('should not send a coppa query param when there are no coppa param settings in the bid requests', function () { - const bidRequestsWithoutCoppa = [{ - bidder: 'openx', - params: { - unit: '11', - delDomain: 'test-del-domain', - coppa: false - }, - adUnitCode: 'adunit-code', - mediaTypes: { - banner: { - sizes: [[300, 250], [300, 600]] - } - }, - bidId: 'test-bid-id-1', - bidderRequestId: 'test-bid-request-1', - auctionId: 'test-auction-1' - }, { - bidder: 'openx', - params: { - unit: '22', - delDomain: 'test-del-domain' - }, - adUnitCode: 'adunit-code', - mediaTypes: { - banner: { - sizes: [[728, 90]] - } - }, - bidId: 'test-bid-id-2', - bidderRequestId: 'test-bid-request-2', - auctionId: 'test-auction-2' - }]; - const request = spec.buildRequests(bidRequestsWithoutCoppa, mockBidderRequest); - expect(request[0].data).to.not.have.any.keys('tfcd'); - }); + context('when there is a consent management framework', function () { + let bidRequests; + let mockConfig; + let bidderRequest; - it('should send a coppa flag there is when there is coppa param settings in the bid requests', function () { - const bidRequestsWithCoppa = [{ - bidder: 'openx', - params: { - unit: '11', - delDomain: 'test-del-domain', - coppa: false - }, - adUnitCode: 'adunit-code', - mediaTypes: { - banner: { - sizes: [[300, 250], [300, 600]] - } - }, - bidId: 'test-bid-id-1', - bidderRequestId: 'test-bid-request-1', - auctionId: 'test-auction-1' - }, { - bidder: 'openx', - params: { - unit: '22', - delDomain: 'test-del-domain', - coppa: true - }, - adUnitCode: 'adunit-code', - mediaTypes: { - banner: { - sizes: [[728, 90]] - } - }, - bidId: 'test-bid-id-2', - bidderRequestId: 'test-bid-request-2', - auctionId: 'test-auction-2' - }]; - const request = spec.buildRequests(bidRequestsWithCoppa, mockBidderRequest); - expect(request[0].data.tfcd).to.equal(1); - }); - - it('should not send a "no segmentation" flag there no DoNotTrack setting that is set to true', function () { - const bidRequestsWithoutDnt = [{ - bidder: 'openx', - params: { - unit: '11', - delDomain: 'test-del-domain', - doNotTrack: false - }, - adUnitCode: 'adunit-code', - mediaTypes: { - banner: { - sizes: [[300, 250], [300, 600]] - } - }, - bidId: 'test-bid-id-1', - bidderRequestId: 'test-bid-request-1', - auctionId: 'test-auction-1' - }, { - bidder: 'openx', - params: { - unit: '22', - delDomain: 'test-del-domain' - }, - adUnitCode: 'adunit-code', - mediaTypes: { - banner: { - sizes: [[728, 90]] - } - }, - bidId: 'test-bid-id-2', - bidderRequestId: 'test-bid-request-2', - auctionId: 'test-auction-2' - }]; - const request = spec.buildRequests(bidRequestsWithoutDnt, mockBidderRequest); - expect(request[0].data).to.not.have.any.keys('ns'); - }); - - it('should send a "no segmentation" flag there is any DoNotTrack setting that is set to true', function () { - const bidRequestsWithDnt = [{ - bidder: 'openx', - params: { - unit: '11', - delDomain: 'test-del-domain', - doNotTrack: false - }, - adUnitCode: 'adunit-code', - mediaTypes: { - banner: { - sizes: [[300, 250], [300, 600]] - } - }, - bidId: 'test-bid-id-1', - bidderRequestId: 'test-bid-request-1', - auctionId: 'test-auction-1' - }, { - bidder: 'openx', - params: { - unit: '22', - delDomain: 'test-del-domain', - doNotTrack: true - }, - adUnitCode: 'adunit-code', - mediaTypes: { - banner: { - sizes: [[728, 90]] - } - }, - bidId: 'test-bid-id-2', - bidderRequestId: 'test-bid-request-2', - auctionId: 'test-auction-2' - }]; - const request = spec.buildRequests(bidRequestsWithDnt, mockBidderRequest); - expect(request[0].data.ns).to.equal(1); - }); - - describe('when schain is provided', function () { - let bidRequests; - let schainConfig; - const supplyChainNodePropertyOrder = ['asi', 'sid', 'hp', 'rid', 'name', 'domain']; - - beforeEach(function () { - schainConfig = { - 'ver': '1.0', - 'complete': 1, - 'nodes': [ - { - 'asi': 'exchange1.com', - 'sid': '1234', - 'hp': 1, - 'rid': 'bid-request-1', - 'name': 'publisher', - 'domain': 'publisher.com' - // omitted ext + beforeEach(function () { + bidRequests = [{ + bidder: 'openx', + params: { + unit: '12345678-banner', + delDomain: 'test-del-domain' }, - { - 'asi': 'exchange2.com', - 'sid': 'abcd', - 'hp': 1, - 'rid': 'bid-request-2', - // name field missing - 'domain': 'intermediary.com' + adUnitCode: 'adunit-code', + mediaTypes: { + banner: { + sizes: [[300, 250], [300, 600]] + } }, - { - 'asi': 'exchange3.com', - 'sid': '4321', - 'hp': 1, - // request id - // name field missing - 'domain': 'intermediary-2.com' - } - ] - }; + bidId: 'test-bid-id', + bidderRequestId: 'test-bidder-request-id', + auctionId: 'test-auction-id', + transactionId: 'test-transaction-id-1' + }, { + bidder: 'openx', + mediaTypes: { + video: { + playerSize: [640, 480] + } + }, + params: { + unit: '12345678-video', + delDomain: 'test-del-domain' + }, + 'adUnitCode': 'adunit-code', - bidRequests = [{ - 'bidder': 'openx', - 'params': { - 'unit': '11', - 'delDomain': 'test-del-domain' - }, - 'adUnitCode': '/adunit-code/test-path', - mediaTypes: { - banner: { - sizes: [[300, 250], [300, 600]] - } - }, - 'bidId': 'test-bid-id-1', - 'bidderRequestId': 'test-bid-request-1', - 'auctionId': 'test-auction-1', - 'schain': schainConfig - }]; - }); + bidId: 'test-bid-id', + bidderRequestId: 'test-bidder-request-id', + auctionId: 'test-auction-id', + transactionId: 'test-transaction-id-2' + }]; + }); - it('should send a schain parameter with the proper delimiter symbols', function () { - const request = spec.buildRequests(bidRequests, mockBidderRequest); - const dataParams = request[0].data; - const numNodes = schainConfig.nodes.length; + describe('us_privacy', function () { + beforeEach(function () { + bidderRequest = { + uspConsent: '1YYN', + refererInfo: {} + }; - // each node will have a ! to denote beginning of a new node - expect(dataParams.schain.match(/!/g).length).to.equal(numNodes); + sinon.stub(config, 'getConfig').callsFake((key) => { + return utils.deepAccess(mockConfig, key); + }); + }); - // 1 comma in the front for version - // 5 commas per node - expect(dataParams.schain.match(/,/g).length).to.equal(numNodes * 5 + 1); - }); + afterEach(function () { + config.getConfig.restore(); + }); - it('should send a schain with the right version', function () { - const request = spec.buildRequests(bidRequests, mockBidderRequest); - const dataParams = request[0].data; - let serializedSupplyChain = dataParams.schain.split('!'); - let version = serializedSupplyChain.shift().split(',')[0]; + it('should send a signal to specify that US Privacy applies to this request', function () { + const request = spec.buildRequests(bidRequests, syncAddFPDToBidderRequest(bidderRequest)); + expect(request[0].data.regs.ext.us_privacy).to.equal('1YYN'); + expect(request[1].data.regs.ext.us_privacy).to.equal('1YYN'); + }); - expect(version).to.equal(bidRequests[0].schain.ver); - }); + it('should not send the regs object, when consent string is undefined', function () { + delete bidderRequest.uspConsent; + const request = spec.buildRequests(bidRequests, syncAddFPDToBidderRequest(bidderRequest)); + expect(request[0].data.regs?.us_privacy).to.not.exist; + }); + }); - it('should send a schain with the right complete value', function () { - const request = spec.buildRequests(bidRequests, mockBidderRequest); - const dataParams = request[0].data; - let serializedSupplyChain = dataParams.schain.split('!'); - let isComplete = serializedSupplyChain.shift().split(',')[1]; + describe('GDPR', function () { + beforeEach(function () { + bidderRequest = { + gdprConsent: { + consentString: 'test-gdpr-consent-string', + addtlConsent: 'test-addtl-consent-string', + gdprApplies: true + }, + refererInfo: {} + }; + + mockConfig = { + consentManagement: { + cmpApi: 'iab', + timeout: 1111, + allowAuctionWithoutConsent: 'cancel' + } + }; - expect(isComplete).to.equal(String(bidRequests[0].schain.complete)); - }); + sinon.stub(config, 'getConfig').callsFake((key) => { + return utils.deepAccess(mockConfig, key); + }); + }); - it('should send all available params in the right order', function () { - const request = spec.buildRequests(bidRequests, mockBidderRequest); - const dataParams = request[0].data; - let serializedSupplyChain = dataParams.schain.split('!'); - serializedSupplyChain.shift(); + afterEach(function () { + config.getConfig.restore(); + }); - serializedSupplyChain.forEach((serializedNode, nodeIndex) => { - let nodeProperties = serializedNode.split(','); + it('should send a signal to specify that GDPR applies to this request', function () { + bidderRequest.bids = bidRequests; + const request = spec.buildRequests(bidRequests, syncAddFPDToBidderRequest(bidderRequest)); + expect(request[0].data.regs.ext.gdpr).to.equal(1); + expect(request[1].data.regs.ext.gdpr).to.equal(1); + }); - nodeProperties.forEach((nodeProperty, propertyIndex) => { - let node = schainConfig.nodes[nodeIndex]; - let key = supplyChainNodePropertyOrder[propertyIndex]; + it('should send the consent string', function () { + bidderRequest.bids = bidRequests; + const request = spec.buildRequests(bidRequests, syncAddFPDToBidderRequest(bidderRequest)); + expect(request[0].data.user.ext.consent).to.equal(bidderRequest.gdprConsent.consentString); + expect(request[1].data.user.ext.consent).to.equal(bidderRequest.gdprConsent.consentString); + }); - expect(nodeProperty).to.equal(node[key] ? String(node[key]) : '', - `expected node '${nodeIndex}' property '${nodeProperty}' to key '${key}' to be the same value`) + it('should send the addtlConsent string', function () { + bidderRequest.bids = bidRequests; + const request = spec.buildRequests(bidRequests, syncAddFPDToBidderRequest(bidderRequest)); + expect(request[0].data.user.ext.ConsentedProvidersSettings.consented_providers).to.equal(bidderRequest.gdprConsent.addtlConsent); + expect(request[1].data.user.ext.ConsentedProvidersSettings.consented_providers).to.equal(bidderRequest.gdprConsent.addtlConsent); }); - }); - }); - }); - describe('when there are userid providers', function () { - const EXAMPLE_DATA_BY_ATTR = { - britepoolid: '1111-britepoolid', - criteoId: '1111-criteoId', - fabrickId: '1111-fabrickid', - haloId: '1111-haloid', - id5id: {uid: '1111-id5id'}, - idl_env: '1111-idl_env', - IDP: '1111-zeotap-idplusid', - idxId: '1111-idxid', - intentIqId: '1111-intentiqid', - lipb: {lipbid: '1111-lipb'}, - lotamePanoramaId: '1111-lotameid', - merkleId: {id: '1111-merkleid'}, - netId: 'fH5A3n2O8_CZZyPoJVD-eabc6ECb7jhxCicsds7qSg', - parrableId: { eid: 'eidVersion.encryptionKeyReference.encryptedValue' }, - pubcid: '1111-pubcid', - quantcastId: '1111-quantcastid', - tapadId: '111-tapadid', - tdid: '1111-tdid', - uid2: {id: '1111-uid2'}, - novatiq: {snowflake: '1111-novatiqid'}, - admixerId: '1111-admixerid', - deepintentId: '1111-deepintentid', - dmdId: '111-dmdid', - nextrollId: '1111-nextrollid', - mwOpenLinkId: '1111-mwopenlinkid', - dapId: '1111-dapId', - amxId: '1111-amxid', - kpuid: '1111-kpuid', - publinkId: '1111-publinkid', - naveggId: '1111-naveggid', - imuid: '1111-imuid', - adtelligentId: '1111-adtelligentid' - }; - - // generates the same set of tests for each id provider - utils._each(USER_ID_CODE_TO_QUERY_ARG, (userIdQueryArg, userIdProviderKey) => { - describe(`with userId attribute: ${userIdProviderKey}`, function () { - it(`should not send a ${userIdQueryArg} query param when there is no userId.${userIdProviderKey} defined in the bid requests`, function () { - const request = spec.buildRequests(bidRequestsWithMediaTypes, mockBidderRequest); - expect(request[0].data).to.not.have.any.keys(userIdQueryArg); + it('should send a signal to specify that GDPR does not apply to this request', function () { + bidderRequest.gdprConsent.gdprApplies = false; + bidderRequest.bids = bidRequests; + const request = spec.buildRequests(bidRequests, syncAddFPDToBidderRequest(bidderRequest)); + expect(request[0].data.regs.ext.gdpr).to.equal(0); + expect(request[1].data.regs.ext.gdpr).to.equal(0); }); - it(`should send a ${userIdQueryArg} query param when userId.${userIdProviderKey} is defined in the bid requests`, function () { - const bidRequestsWithUserId = [{ - bidder: 'openx', - params: { - unit: '11', - delDomain: 'test-del-domain' - }, - userId: { - }, - adUnitCode: 'adunit-code', - mediaTypes: { - banner: { - sizes: [[300, 250], [300, 600]] - } - }, - bidId: 'test-bid-id-1', - bidderRequestId: 'test-bid-request-1', - auctionId: 'test-auction-1' - }]; - // enrich bid request with userId key/value - bidRequestsWithUserId[0].userId[userIdProviderKey] = EXAMPLE_DATA_BY_ATTR[userIdProviderKey]; - - const request = spec.buildRequests(bidRequestsWithUserId, mockBidderRequest); - - let userIdValue; - // handle cases where userId key refers to an object - switch (userIdProviderKey) { - case 'merkleId': - userIdValue = EXAMPLE_DATA_BY_ATTR.merkleId.id; - break; - case 'uid2': - userIdValue = EXAMPLE_DATA_BY_ATTR.uid2.id; - break; - case 'lipb': - userIdValue = EXAMPLE_DATA_BY_ATTR.lipb.lipbid; - break; - case 'parrableId': - userIdValue = EXAMPLE_DATA_BY_ATTR.parrableId.eid; - break; - case 'id5id': - userIdValue = EXAMPLE_DATA_BY_ATTR.id5id.uid; - break; - case 'novatiq': - userIdValue = EXAMPLE_DATA_BY_ATTR.novatiq.snowflake; - break; - default: - userIdValue = EXAMPLE_DATA_BY_ATTR[userIdProviderKey]; - } + it('when GDPR application is undefined, should not send a signal to specify whether GDPR applies to this request, ' + + 'but can send consent data, ', function () { + delete bidderRequest.gdprConsent.gdprApplies; + bidderRequest.bids = bidRequests; + const request = spec.buildRequests(bidRequests, syncAddFPDToBidderRequest(bidderRequest)); + expect(request[0].data.regs?.ext?.gdpr).to.not.be.ok; + expect(request[0].data.user.ext.consent).to.equal(bidderRequest.gdprConsent.consentString); + expect(request[1].data.user.ext.consent).to.equal(bidderRequest.gdprConsent.consentString); + }); - expect(request[0].data[USER_ID_CODE_TO_QUERY_ARG[userIdProviderKey]]).to.equal(userIdValue); + it('when consent string is undefined, should not send the consent string, ', function () { + delete bidderRequest.gdprConsent.consentString; + bidderRequest.bids = bidRequests; + const request = spec.buildRequests(bidRequests, syncAddFPDToBidderRequest(bidderRequest)); + expect(request[0].data.imp[0].ext.consent).to.equal(undefined); + expect(request[1].data.imp[0].ext.consent).to.equal(undefined); }); }); }); - }); - describe('floors', function () { - it('should send out custom floors on bids that have customFloors specified', function () { - const bidRequest = Object.assign({}, - bidRequestsWithMediaTypes[0], - { - params: { - 'unit': '12345678', - 'delDomain': 'test-del-domain', - 'customFloor': 1.500001 - } - } - ); - - const request = spec.buildRequests([bidRequest], mockBidderRequest); - const dataParams = request[0].data; - - expect(dataParams.aumfs).to.exist; - expect(dataParams.aumfs).to.equal('1500'); - }); - - context('with floors module', function () { - let adServerCurrencyStub; - - beforeEach(function () { - adServerCurrencyStub = sinon - .stub(config, 'getConfig') - .withArgs('currency.adServerCurrency') + context('coppa', function() { + it('when there are no coppa param settings, should not send a coppa flag', function () { + const request = spec.buildRequests(bidRequestsWithMediaTypes, syncAddFPDToBidderRequest(mockBidderRequest)); + expect(request[0].data.regs?.coppa).to.be.not.ok; }); - afterEach(function () { - config.getConfig.restore(); - }); - - it('should send out floors on bids', function () { - const bidRequest1 = Object.assign({}, - bidRequestsWithMediaTypes[0], - { - getFloor: () => { - return { - currency: 'AUS', - floor: 9.99 - } - } - } - ); - - const bidRequest2 = Object.assign({}, - bidRequestsWithMediaTypes[1], - { - getFloor: () => { - return { - currency: 'AUS', - floor: 18.881 - } - } - } - ); + it('should send a coppa flag there is when there is coppa param settings in the bid requests', function () { + let mockConfig = { + coppa: true + }; - const request = spec.buildRequests([bidRequest1, bidRequest2], mockBidderRequest); - const dataParams = request[0].data; + sinon.stub(config, 'getConfig').callsFake((key) => { + return utils.deepAccess(mockConfig, key); + }); - expect(dataParams.aumfs).to.exist; - expect(dataParams.aumfs).to.equal('9990,18881'); + const request = spec.buildRequests(bidRequestsWithMediaTypes, syncAddFPDToBidderRequest(mockBidderRequest)); + expect(request[0].data.regs.coppa).to.equal(1); }); - it('should send out floors on bids in the default currency', function () { - const bidRequest1 = Object.assign({}, - bidRequestsWithMediaTypes[0], - { - getFloor: () => { - return {}; - } - } - ); - - let getFloorSpy = sinon.spy(bidRequest1, 'getFloor'); - - spec.buildRequests([bidRequest1], mockBidderRequest); - expect(getFloorSpy.args[0][0].mediaType).to.equal(BANNER); - expect(getFloorSpy.args[0][0].currency).to.equal('USD'); + it('should send a coppa flag there is when there is coppa param settings in the bid params', function () { + const request = spec.buildRequests(bidRequestsWithMediaTypes, syncAddFPDToBidderRequest(mockBidderRequest)); + request.params = {coppa: true}; + expect(request[0].data.regs.coppa).to.equal(1); }); - it('should send out floors on bids in the ad server currency if defined', function () { - adServerCurrencyStub.returns('bitcoin'); - - const bidRequest1 = Object.assign({}, - bidRequestsWithMediaTypes[0], - { - getFloor: () => { - return {}; - } - } - ); - - let getFloorSpy = sinon.spy(bidRequest1, 'getFloor'); - - spec.buildRequests([bidRequest1], mockBidderRequest); - expect(getFloorSpy.args[0][0].mediaType).to.equal(BANNER); - expect(getFloorSpy.args[0][0].currency).to.equal('bitcoin'); + after(function () { + config.getConfig.restore() }); - }) - }) - }); - - describe('buildRequests for video', function () { - const bidRequestsWithMediaTypes = VIDEO_BID_REQUESTS_WITH_MEDIA_TYPES; - const mockBidderRequest = {refererInfo: {}}; - - it('should send bid request to openx url via GET, with mediaTypes having video parameter', function () { - const request = spec.buildRequests(bidRequestsWithMediaTypes, mockBidderRequest); - expect(request[0].url).to.equal('https://' + bidRequestsWithMediaTypes[0].params.delDomain + URLBASEVIDEO); - expect(request[0].method).to.equal('GET'); - }); - it('should have the correct parameters', function () { - const request = spec.buildRequests(bidRequestsWithMediaTypes, mockBidderRequest); - const dataParams = request[0].data; - - expect(dataParams.auid).to.equal('12345678'); - expect(dataParams.vht).to.equal(480); - expect(dataParams.vwd).to.equal(640); - expect(dataParams.aucs).to.equal(encodeURIComponent('/12345/my-gpt-tag-0')); - }); - - it('shouldn\'t have the test parameter', function () { - const request = spec.buildRequests(bidRequestsWithMediaTypes, mockBidderRequest); - expect(request[0].data.vtest).to.be.undefined; - }); - - it('should send a bc parameter', function () { - const request = spec.buildRequests(bidRequestsWithMediaTypes, mockBidderRequest); - const dataParams = request[0].data; - - expect(dataParams.bc).to.have.string('hb_pb'); - }); - - describe('when using the video param', function () { - let videoBidRequest; - let mockBidderRequest = {refererInfo: {}}; - - beforeEach(function () { - videoBidRequest = { - 'bidder': 'openx', - 'mediaTypes': { - video: { - context: 'instream', - playerSize: [640, 480] - } - }, - 'params': { - 'unit': '12345678', - 'delDomain': 'test-del-domain' - }, - 'adUnitCode': 'adunit-code', - 'bidId': '30b31c1838de1e', - 'bidderRequestId': '22edbae2733bf6', - 'auctionId': '1d1a030790a475', - 'transactionId': '4008d88a-8137-410b-aa35-fbfdabcb478e' - }; - mockBidderRequest = {refererInfo: {}}; - }); - - it('should not allow you to set a url', function () { - videoBidRequest.params.video = { - url: 'test-url' - }; - const request = spec.buildRequests([videoBidRequest], mockBidderRequest); - - expect(request[0].data.url).to.be.undefined; - }); - - it('should not allow you to override the javascript url', function () { - let myUrl = 'my-url'; - videoBidRequest.params.video = { - ju: myUrl - }; - const request = spec.buildRequests([videoBidRequest], mockBidderRequest); - - expect(request[0].data.ju).to.not.equal(myUrl); }); - describe('when using the openrtb video params', function () { - it('should parse legacy params.video.openrtb', function () { - let myOpenRTBObject = {mimes: ['application/javascript']}; - videoBidRequest.params.video = { - openrtb: myOpenRTBObject - }; - const expected = {imp: [{video: {w: 640, h: 480, mimes: ['application/javascript']}}]} - const request = spec.buildRequests([videoBidRequest], mockBidderRequest); + context('do not track (DNT)', function() { + let doNotTrackStub; - expect(request[0].data.openrtb).to.equal(JSON.stringify(expected)); + beforeEach(function () { + doNotTrackStub = sinon.stub(utils, 'getDNT'); }); - - it('should parse legacy params.openrtb', function () { - let myOpenRTBObject = {mimes: ['application/javascript']}; - videoBidRequest.params.openrtb = myOpenRTBObject; - const expected = {imp: [{video: {w: 640, h: 480, mimes: ['application/javascript']}}]} - const request = spec.buildRequests([videoBidRequest], mockBidderRequest); - - expect(request[0].data.openrtb).to.equal(JSON.stringify(expected)); + afterEach(function() { + doNotTrackStub.restore(); }); - it('should parse legacy params.video', function () { - let myOpenRTBObject = {mimes: ['application/javascript']}; - videoBidRequest.params.video = myOpenRTBObject; - const expected = {imp: [{video: {w: 640, h: 480, mimes: ['application/javascript']}}]} - const request = spec.buildRequests([videoBidRequest], mockBidderRequest); + it('when there is a do not track, should send a dnt', function () { + doNotTrackStub.returns(1); - expect(request[0].data.openrtb).to.equal(JSON.stringify(expected)); + const request = spec.buildRequests(bidRequestsWithMediaTypes, syncAddFPDToBidderRequest(mockBidderRequest)); + expect(request[0].data.device.dnt).to.equal(1); }); - it('should parse legacy params.video as full openrtb', function () { - let myOpenRTBObject = {imp: [{video: {mimes: ['application/javascript']}}]}; - videoBidRequest.params.video = myOpenRTBObject; - const expected = {imp: [{video: {w: 640, h: 480, mimes: ['application/javascript']}}]} - const request = spec.buildRequests([videoBidRequest], mockBidderRequest); + it('when there is not do not track, don\'t send dnt', function () { + doNotTrackStub.returns(0); - expect(request[0].data.openrtb).to.equal(JSON.stringify(expected)); + const request = spec.buildRequests(bidRequestsWithMediaTypes, syncAddFPDToBidderRequest(mockBidderRequest)); + expect(request[0].data.device.dnt).to.equal(0); }); - it('should parse legacy video.openrtb', function () { - let myOpenRTBObject = {mimes: ['application/javascript']}; - videoBidRequest.params.video = { - openrtb: myOpenRTBObject - }; - const expected = {imp: [{video: {w: 640, h: 480, mimes: ['application/javascript']}}]} - const request = spec.buildRequests([videoBidRequest], mockBidderRequest); + it('when there is no defined do not track, don\'t send dnt', function () { + doNotTrackStub.returns(null); - expect(request[0].data.openrtb).to.equal(JSON.stringify(expected)); + const request = spec.buildRequests(bidRequestsWithMediaTypes, syncAddFPDToBidderRequest(mockBidderRequest)); + expect(request[0].data.device.dnt).to.equal(0); }); + }); + + context('supply chain (schain)', function () { + let bidRequests; + let schainConfig; + const supplyChainNodePropertyOrder = ['asi', 'sid', 'hp', 'rid', 'name', 'domain']; - it('should omit filtered values for legacy', function () { - let myOpenRTBObject = {mimes: ['application/javascript'], dont: 'use'}; - videoBidRequest.params.video = { - openrtb: myOpenRTBObject + beforeEach(function () { + schainConfig = { + ver: '1.0', + complete: 1, + nodes: [ + { + asi: 'exchange1.com', + sid: '1234', + hp: 1, + rid: 'bid-request-1', + name: 'publisher', + domain: 'publisher.com' + // omitted ext + }, + { + asi: 'exchange2.com', + sid: 'abcd', + hp: 1, + rid: 'bid-request-2', + // name field missing + domain: 'intermediary.com' + }, + { + asi: 'exchange3.com', + sid: '4321', + hp: 1, + // request id + // name field missing + domain: 'intermediary-2.com' + } + ] }; - const expected = {imp: [{video: {w: 640, h: 480, mimes: ['application/javascript']}}]} - const request = spec.buildRequests([videoBidRequest], mockBidderRequest); - expect(request[0].data.openrtb).to.equal(JSON.stringify(expected)); + bidRequests = [{ + bidder: 'openx', + params: { + unit: '11', + delDomain: 'test-del-domain' + }, + adUnitCode: '/adunit-code/test-path', + mediaTypes: { + banner: { + sizes: [[300, 250], [300, 600]] + } + }, + bidId: 'test-bid-id-1', + bidderRequestId: 'test-bid-request-1', + auctionId: 'test-auction-1', + schain: schainConfig + }]; }); - it('should parse mediatypes.video', function () { - videoBidRequest.mediaTypes.video.mimes = ['application/javascript'] - videoBidRequest.mediaTypes.video.minduration = 15 - const request = spec.buildRequests([videoBidRequest], mockBidderRequest); - const openRtbRequestParams = JSON.parse(request[0].data.openrtb); - expect(openRtbRequestParams.imp[0].video.mimes).to.eql(['application/javascript']); - expect(openRtbRequestParams.imp[0].video.minduration).to.equal(15); + it('should send a supply chain object', function () { + const request = spec.buildRequests(bidRequests, mockBidderRequest); + expect(request[0].data.source.ext.schain).to.equal(schainConfig); }); - it('should filter mediatypes.video', function () { - videoBidRequest.mediaTypes.video.mimes = ['application/javascript'] - videoBidRequest.mediaTypes.video.minnothing = 15 - const request = spec.buildRequests([videoBidRequest], mockBidderRequest); - const openRtbRequestParams = JSON.parse(request[0].data.openrtb); - expect(openRtbRequestParams.imp[0].video.mimes).to.eql(['application/javascript']); - expect(openRtbRequestParams.imp[0].video.minnothing).to.equal(undefined); + it('should send the supply chain object with the right version', function () { + const request = spec.buildRequests(bidRequests, mockBidderRequest); + expect(request[0].data.source.ext.schain.ver).to.equal(schainConfig.ver); }); - it("should use the bidRequest's playerSize", function () { - const width = 200; - const height = 100; - const myOpenRTBObject = {v: height, w: width}; - videoBidRequest.params.video = { - openrtb: myOpenRTBObject - }; - const request = spec.buildRequests([videoBidRequest], mockBidderRequest); - const openRtbRequestParams = JSON.parse(request[0].data.openrtb); - - expect(openRtbRequestParams.imp[0].video.w).to.equal(640); - expect(openRtbRequestParams.imp[0].video.h).to.equal(480); + it('should send the supply chain object with the right complete value', function () { + const request = spec.buildRequests(bidRequests, mockBidderRequest); + expect(request[0].data.source.ext.schain.complete).to.equal(schainConfig.complete); }); }); - }); - describe('floors', function () { - it('should send out custom floors on bids that have customFloors specified', function () { - const bidRequest = Object.assign({}, - bidRequestsWithMediaTypes[0], + context('when there are userid providers', function () { + const userIdAsEids = [ { - params: { - 'unit': '12345678', - 'delDomain': 'test-del-domain', - 'customFloor': 1.500001 - } + source: 'adserver.org', + uids: [{ + id: 'some-random-id-value', + atype: 1, + ext: { + rtiPartner: 'TDID' + } + }] + }, + { + source: 'id5-sync.com', + uids: [{ + id: 'some-random-id-value', + atype: 1 + }] + }, + { + source: 'sharedid.org', + uids: [{ + id: 'some-random-id-value', + atype: 1, + ext: { + third: 'some-random-id-value' + } + }] } - ); - - const request = spec.buildRequests([bidRequest], mockBidderRequest); - const dataParams = request[0].data; - - expect(dataParams.aumfs).to.exist; - expect(dataParams.aumfs).to.equal('1500'); - }); + ]; - context('with floors module', function () { - let adServerCurrencyStub; - function makeBidWithFloorInfo(floorInfo) { - return Object.assign(utils.deepClone(bidRequestsWithMediaTypes[0]), - { - getFloor: () => { - return floorInfo; + it(`should send the user id under the extended ids`, function () { + const bidRequestsWithUserId = [{ + bidder: 'openx', + params: { + unit: '11', + delDomain: 'test-del-domain' + }, + userId: { + }, + adUnitCode: 'adunit-code', + mediaTypes: { + banner: { + sizes: [[300, 250], [300, 600]] } - }); - } - - beforeEach(function () { - adServerCurrencyStub = sinon - .stub(config, 'getConfig') - .withArgs('currency.adServerCurrency') + }, + bidId: 'test-bid-id-1', + bidderRequestId: 'test-bid-request-1', + auctionId: 'test-auction-1', + userIdAsEids: userIdAsEids + }]; + // enrich bid request with userId key/value + + const request = spec.buildRequests(bidRequestsWithUserId, mockBidderRequest); + expect(request[0].data.user.ext.eids).to.equal(userIdAsEids); }); - afterEach(function () { - config.getConfig.restore(); + it(`when no user ids are available, it should not send any extended ids`, function () { + const request = spec.buildRequests(bidRequestsWithMediaTypes, mockBidderRequest); + expect(request[0].data).to.not.have.any.keys('user'); }); + }); - it('should send out floors on bids', function () { - const floors = [9.99, 18.881]; - const bidRequests = floors.map(floor => { - return makeBidWithFloorInfo({ - currency: 'AUS', - floor: floor - }); + context('FLEDGE', function() { + it('when FLEDGE is enabled, should send whatever is set in ortb2imp.ext.ae in all bid requests', function () { + const request = spec.buildRequests(bidRequestsWithMediaTypes, { + ...mockBidderRequest, + fledgeEnabled: true }); - const request = spec.buildRequests(bidRequests, mockBidderRequest); - - expect(request[0].data.aumfs).to.exist; - expect(request[0].data.aumfs).to.equal('9990'); - expect(request[1].data.aumfs).to.exist; - expect(request[1].data.aumfs).to.equal('18881'); + expect(request[0].data.imp[0].ext.ae).to.equal(2); }); + }); + }); - it('should send out floors on bids in the default currency', function () { - const bidRequest1 = makeBidWithFloorInfo({}); - - let getFloorSpy = sinon.spy(bidRequest1, 'getFloor'); + context('banner', function () { + it('should send bid request with a mediaTypes specified with banner type', function () { + const request = spec.buildRequests(bidRequestsWithMediaTypes, mockBidderRequest); + expect(request[0].data.imp[0]).to.have.any.keys(BANNER); + }); + }); - spec.buildRequests([bidRequest1], mockBidderRequest); - expect(getFloorSpy.args[0][0].mediaType).to.equal(VIDEO); - expect(getFloorSpy.args[0][0].currency).to.equal('USD'); + if (FEATURES.VIDEO) { + context('video', function () { + it('should send bid request with a mediaTypes specified with video type', function () { + const request = spec.buildRequests(bidRequestsWithMediaTypes, mockBidderRequest); + expect(request[1].data.imp[0]).to.have.any.keys(VIDEO); }); - it('should send out floors on bids in the ad server currency if defined', function () { - adServerCurrencyStub.returns('bitcoin'); - - const bidRequest1 = makeBidWithFloorInfo({}); - - let getFloorSpy = sinon.spy(bidRequest1, 'getFloor'); - - spec.buildRequests([bidRequest1], mockBidderRequest); - expect(getFloorSpy.args[0][0].mediaType).to.equal(VIDEO); - expect(getFloorSpy.args[0][0].currency).to.equal('bitcoin'); + it('Update imp.video with OpenRTB options from mimeTypes and params', function() { + const bid01 = new BidRequestBuilder({ + adUnitCode: 'adunit-code-01', + mediaTypes: { + banner: { sizes: [[300, 250]] }, + video: { + context: 'outstream', + playerSize: [[300, 250]], + mimes: ['video/mp4'], + protocols: [8] + } + }, + }).withParams({ + // options in video, will merge + video: { + skip: 1, + skipafter: 4, + minduration: 10, + maxduration: 30 + } + }).build(); + + const bidderRequest = new BidderRequestBuilder().build(); + const expected = { + mimes: ['video/mp4'], + skip: 1, + skipafter: 4, + minduration: 10, + maxduration: 30, + placement: 4, + protocols: [8], + w: 300, + h: 250 + }; + const requests = spec.buildRequests([bid01], bidderRequest); + expect(requests).to.have.lengthOf(2); + expect(requests[1].data.imp[0].video).to.deep.equal(expected); }); - }) - }) + }); + } }); - describe('buildRequest for multi-format ad', function () { - const multiformatBid = MULTI_FORMAT_BID_REQUESTS[0]; - let mockBidderRequest = {refererInfo: {}}; - - it('should default to a banner request', function () { - const request = spec.buildRequests([multiformatBid], mockBidderRequest); - const dataParams = request[0].data; - - expect(dataParams.divids).to.have.string(multiformatBid.adUnitCode); - }); - }); + describe('interpretResponse()', function () { + let bidRequestConfigs; + let bidRequest; + let bidResponse; + let bid; - describe('buildRequests for all kinds of ads', function () { - utils._each({ - banner: BANNER_BID_REQUESTS_WITH_MEDIA_TYPES[0], - video: VIDEO_BID_REQUESTS_WITH_MEDIA_TYPES[0], - multi: MULTI_FORMAT_BID_REQUESTS[0] - }, (bidRequest, name) => { - describe('with segments', function () { - const TESTS = [ - { - name: 'should send proprietary segment data from ortb2.user.data', - config: { - ortb2: { - user: { - data: [ - {name: 'dmp1', ext: {segtax: 4}, segment: [{id: 'foo'}, {id: 'bar'}]}, - {name: 'dmp2', segment: [{id: 'baz'}]}, - ] - } - } - }, - expect: {sm: 'dmp1/4:foo|bar,dmp2:baz'}, - }, - { - name: 'should send proprietary segment data from ortb2.site.content.data', - config: { - ortb2: { - site: { - content: { - data: [ - {name: 'dmp1', ext: {segtax: 4}, segment: [{id: 'foo'}, {id: 'bar'}]}, - {name: 'dmp2', segment: [{id: 'baz'}]}, - ] - } - } - } - }, - expect: {scsm: 'dmp1/4:foo|bar,dmp2:baz'}, - }, - { - name: 'should send proprietary segment data from both ortb2.site.content.data and ortb2.user.data', - config: { - ortb2: { - user: { - data: [ - {name: 'dmp1', ext: {segtax: 4}, segment: [{id: 'foo'}, {id: 'bar'}]}, - {name: 'dmp2', segment: [{id: 'baz'}]}, - ] - }, - site: { - content: { - data: [ - {name: 'dmp3', ext: {segtax: 5}, segment: [{id: 'foo2'}, {id: 'bar2'}]}, - {name: 'dmp4', segment: [{id: 'baz2'}]}, - ] - } - } - } - }, - expect: { - sm: 'dmp1/4:foo|bar,dmp2:baz', - scsm: 'dmp3/5:foo2|bar2,dmp4:baz2' - }, - }, - { - name: 'should combine same provider segment data from ortb2.user.data', - config: { - ortb2: { - user: { - data: [ - {name: 'dmp1', ext: {segtax: 4}, segment: [{id: 'foo'}, {id: 'bar'}]}, - {name: 'dmp1', ext: {}, segment: [{id: 'baz'}]}, - ] - } - } - }, - expect: {sm: 'dmp1/4:foo|bar,dmp1:baz'}, + context('when there is an nbr response', function () { + let bids; + beforeEach(function () { + bidRequestConfigs = [{ + bidder: 'openx', + params: { + unit: '12345678', + delDomain: 'test-del-domain' }, - { - name: 'should combine same provider segment data from ortb2.site.content.data', - config: { - ortb2: { - site: { - content: { - data: [ - {name: 'dmp1', ext: {segtax: 4}, segment: [{id: 'foo'}, {id: 'bar'}]}, - {name: 'dmp1', ext: {}, segment: [{id: 'baz'}]}, - ] - } - } - } + adUnitCode: 'adunit-code', + mediaTypes: { + banner: { + sizes: [[300, 250], [300, 600]], }, - expect: {scsm: 'dmp1/4:foo|bar,dmp1:baz'}, - }, - { - name: 'should not send any segment data if first party config is incomplete', - config: { - ortb2: { - user: { - data: [ - {name: 'provider-with-no-segments'}, - {segment: [{id: 'segments-with-no-provider'}]}, - {}, - ] - } - } - } }, - { - name: 'should send first party data segments and liveintent segments from request', - config: { - ortb2: { - user: { - data: [ - {name: 'dmp1', segment: [{id: 'foo'}, {id: 'bar'}]}, - {name: 'dmp2', segment: [{id: 'baz'}]}, - ] - }, - site: { - content: { - data: [ - {name: 'dmp3', ext: {segtax: 5}, segment: [{id: 'foo2'}, {id: 'bar2'}]}, - {name: 'dmp4', segment: [{id: 'baz2'}]}, - ] - } - } - } - }, - request: { - userId: { - lipb: { - lipbid: 'aaa', - segments: ['l1', 'l2'] - }, - }, - }, - expect: { - sm: 'dmp1:foo|bar,dmp2:baz,liveintent:l1|l2', - scsm: 'dmp3/5:foo2|bar2,dmp4:baz2' - }, + bidId: 'test-bid-id', + bidderRequestId: 'test-bidder-request-id', + auctionId: 'test-auction-id' + }]; + + bidRequest = spec.buildRequests(bidRequestConfigs, {refererInfo: {}})[0]; + + bidResponse = {nbr: 0}; // Unknown error + bids = spec.interpretResponse({body: bidResponse}, bidRequest); + }); + + it('should not return any bids', function () { + expect(bids.length).to.equal(0); + }); + }); + + context('when no seatbid in response', function () { + let bids; + beforeEach(function () { + bidRequestConfigs = [{ + bidder: 'openx', + params: { + unit: '12345678', + delDomain: 'test-del-domain' }, - { - name: 'should send just liveintent segment from request if no first party config', - config: {}, - request: { - userId: { - lipb: { - lipbid: 'aaa', - segments: ['l1', 'l2'] - }, - }, + adUnitCode: 'adunit-code', + mediaTypes: { + banner: { + sizes: [[300, 250], [300, 600]], }, - expect: {sm: 'liveintent:l1|l2'}, }, - { - name: 'should send nothing if lipb section does not contain segments', - config: {}, - request: { - userId: { - lipb: { - lipbid: 'aaa', - }, - }, + bidId: 'test-bid-id', + bidderRequestId: 'test-bidder-request-id', + auctionId: 'test-auction-id' + }]; + + bidRequest = spec.buildRequests(bidRequestConfigs, {refererInfo: {}})[0]; + + bidResponse = {ext: {}, id: 'test-bid-id'}; + bids = spec.interpretResponse({body: bidResponse}, bidRequest); + }); + + it('should not return any bids', function () { + expect(bids.length).to.equal(0); + }); + }); + + context('when there is no response', function () { + let bids; + beforeEach(function () { + bidRequestConfigs = [{ + bidder: 'openx', + params: { + unit: '12345678', + delDomain: 'test-del-domain' + }, + adUnitCode: 'adunit-code', + mediaTypes: { + banner: { + sizes: [[300, 250], [300, 600]], }, }, - ]; - utils._each(TESTS, (t) => { - context('in ortb2.user.data', function () { - let bidRequests; - beforeEach(function () { - bidRequests = [{...bidRequest, ...t.request}]; - }); + bidId: 'test-bid-id', + bidderRequestId: 'test-bidder-request-id', + auctionId: 'test-auction-id' + }]; - const mockBidderRequest = {refererInfo: {}, ortb2: t.config.ortb2}; - it(`${t.name} for type ${name}`, function () { - const request = spec.buildRequests(bidRequests, mockBidderRequest) - expect(request.length).to.equal(1); - if (t.expect) { - for (const key in t.expect) { - expect(request[0].data[key]).to.exist; - expect(request[0].data[key]).to.equal(t.expect[key]); - } - } else { - expect(request[0].data.sm).to.not.exist; - expect(request[0].data.scsm).to.not.exist; - } - }); - }); - }); + bidRequest = spec.buildRequests(bidRequestConfigs, {refererInfo: {}})[0]; + + bidResponse = ''; // Unknown error + bids = spec.interpretResponse({body: bidResponse}, bidRequest); }); - }); - }) - describe('interpretResponse for banner ads', function () { - beforeEach(function () { - sinon.spy(userSync, 'registerSync'); + it('should not return any bids', function () { + expect(bids.length).to.equal(0); + }); }); - afterEach(function () { - userSync.registerSync.restore(); - }); + const SAMPLE_BID_REQUESTS = [{ + bidder: 'openx', + params: { + unit: '12345678', + delDomain: 'test-del-domain' + }, + adUnitCode: 'adunit-code', + mediaTypes: { + banner: { + sizes: [[300, 250], [300, 600]], + }, + }, + bidId: 'test-bid-id', + bidderRequestId: 'test-bidder-request-id', + auctionId: 'test-auction-id' + }]; - describe('when there is a standard response', function () { - const creativeOverride = { - id: 234, - width: '300', - height: '250', - tracking: { - impression: 'https://openx-d.openx.net/v/1.0/ri?ts=ts' + const SAMPLE_BID_RESPONSE = { + seatbid: [{ + bid: [{ + impid: 'test-bid-id', + price: 2, + w: 300, + h: 250, + crid: 'test-creative-id', + dealid: 'test-deal-id', + adm: 'test-ad-markup', + adomain: ['brand.com'], + ext: { + dsp_id: '123', + buyer_id: '456', + brand_id: '789', + paf: { + content_id: 'paf_content_id' + } + } + }] + }], + cur: 'AUS', + ext: { + paf: { + transmission: {version: '12'} } - }; - - const adUnitOverride = { - ts: 'test-1234567890-ts', - idx: '0', - currency: 'USD', - pub_rev: '10000', - html: '
OpenX Ad
' - }; - let adUnit; - let bidResponse; - - let bid; - let bidRequest; - let bidRequestConfigs; + } + }; + context('when there is a response, the common response properties', function () { beforeEach(function () { - bidRequestConfigs = [{ - 'bidder': 'openx', - 'params': { - 'unit': '12345678', - 'delDomain': 'test-del-domain' - }, - 'adUnitCode': 'adunit-code', - 'mediaType': 'banner', - 'sizes': [[300, 250], [300, 600]], - 'bidId': '30b31c1838de1e', - 'bidderRequestId': '22edbae2733bf6', - 'auctionId': '1d1a030790a475' - }]; - - bidRequest = { - method: 'GET', - url: 'https://openx-d.openx.net/v/1.0/arj', - data: {}, - payload: {'bids': bidRequestConfigs, 'startTime': new Date()} - }; + bidRequestConfigs = deepClone(SAMPLE_BID_REQUESTS); + bidRequest = spec.buildRequests(bidRequestConfigs, {refererInfo: {}})[0]; + bidResponse = deepClone(SAMPLE_BID_RESPONSE); - adUnit = mockAdUnit(adUnitOverride, creativeOverride); - bidResponse = mockArjResponse(undefined, [adUnit]); bid = spec.interpretResponse({body: bidResponse}, bidRequest)[0]; }); it('should return a price', function () { - expect(bid.cpm).to.equal(parseInt(adUnitOverride.pub_rev, 10) / 1000); + expect(bid.cpm).to.equal(bidResponse.seatbid[0].bid[0].price); }); it('should return a request id', function () { - expect(bid.requestId).to.equal(bidRequest.payload.bids[0].bidId); + expect(bid.requestId).to.equal(bidResponse.seatbid[0].bid[0].impid); }); it('should return width and height for the creative', function () { - expect(bid.width).to.equal(creativeOverride.width); - expect(bid.height).to.equal(creativeOverride.height); + expect(bid.width).to.equal(bidResponse.seatbid[0].bid[0].w); + expect(bid.height).to.equal(bidResponse.seatbid[0].bid[0].h); }); it('should return a creativeId', function () { - expect(bid.creativeId).to.equal(creativeOverride.id); + expect(bid.creativeId).to.equal(bidResponse.seatbid[0].bid[0].crid); }); it('should return an ad', function () { - expect(bid.ad).to.equal(adUnitOverride.html); + expect(bid.ad).to.equal(bidResponse.seatbid[0].bid[0].adm); + }); + + it('should return a deal id if it exists', function () { + expect(bid.dealId).to.equal(bidResponse.seatbid[0].bid[0].dealid); }); it('should have a time-to-live of 5 minutes', function () { @@ -1822,415 +1286,293 @@ describe('OpenxAdapter', function () { }); it('should return a currency', function () { - expect(bid.currency).to.equal(adUnitOverride.currency); - }); - - it('should return a transaction state', function () { - expect(bid.ts).to.equal(adUnitOverride.ts); + expect(bid.currency).to.equal(bidResponse.cur); }); it('should return a brand ID', function () { - expect(bid.meta.brandId).to.equal(DEFAULT_TEST_ARJ_AD_UNIT.brand_id); + expect(bid.meta.brandId).to.equal(bidResponse.seatbid[0].bid[0].ext.brand_id); }); - it('should return an adomain', function () { - expect(bid.meta.advertiserDomains).to.deep.equal([]); + it('should return a dsp ID', function () { + expect(bid.meta.networkId).to.equal(bidResponse.seatbid[0].bid[0].ext.dsp_id); }); - it('should return a dsp ID', function () { - expect(bid.meta.dspid).to.equal(DEFAULT_TEST_ARJ_AD_UNIT.adv_id); + it('should return a buyer ID', function () { + expect(bid.meta.advertiserId).to.equal(bidResponse.seatbid[0].bid[0].ext.buyer_id); }); - }); - describe('when there is a deal', function () { - const adUnitOverride = { - deal_id: 'ox-1000' - }; - let adUnit; - let bidResponse; + it('should return adomain', function () { + expect(bid.meta.advertiserDomains).to.equal(bidResponse.seatbid[0].bid[0].adomain); + }); - let bid; - let bidRequestConfigs; - let bidRequest; + it('should return paf fields', function () { + const paf = { + transmission: {version: '12'}, + content_id: 'paf_content_id' + } + expect(bid.meta.paf).to.deep.equal(paf); + }); + }); + context('when there is more than one response', () => { + let bids; beforeEach(function () { - bidRequestConfigs = [{ - 'bidder': 'openx', - 'params': { - 'unit': '12345678', - 'delDomain': 'test-del-domain' - }, - 'adUnitCode': 'adunit-code', - 'mediaType': 'banner', - 'sizes': [[300, 250], [300, 600]], - 'bidId': '30b31c1838de1e', - 'bidderRequestId': '22edbae2733bf6', - 'auctionId': '1d1a030790a475' - }]; + bidRequestConfigs = deepClone(SAMPLE_BID_REQUESTS); + bidRequest = spec.buildRequests(bidRequestConfigs, {refererInfo: {}})[0]; + bidResponse = deepClone(SAMPLE_BID_RESPONSE); + bidResponse.seatbid[0].bid.push(deepClone(bidResponse.seatbid[0].bid[0])); + bidResponse.seatbid[0].bid[1].ext.paf.content_id = 'second_paf' - bidRequest = { - method: 'GET', - url: 'https://openx-d.openx.net/v/1.0/arj', - data: {}, - payload: {'bids': bidRequestConfigs, 'startTime': new Date()} - }; - adUnit = mockAdUnit(adUnitOverride); - bidResponse = mockArjResponse(null, [adUnit]); - bid = spec.interpretResponse({body: bidResponse}, bidRequest)[0]; - mockArjResponse(); + bids = spec.interpretResponse({body: bidResponse}, bidRequest); }); - it('should return a deal id', function () { - expect(bid.dealId).to.equal(adUnitOverride.deal_id); + it('should not confuse paf content_id', () => { + expect(bids.map(b => b.meta.paf.content_id)).to.eql(['paf_content_id', 'second_paf']); }); - }); - - describe('when there is no bids in the response', function () { - let bidRequest; - let bidRequestConfigs; + }) + context('when the response is a banner', function() { beforeEach(function () { bidRequestConfigs = [{ - 'bidder': 'openx', - 'params': { - 'unit': '12345678', - 'delDomain': 'test-del-domain' + bidder: 'openx', + params: { + unit: '12345678', + delDomain: 'test-del-domain' }, - 'adUnitCode': 'adunit-code', - 'mediaType': 'banner', - 'sizes': [[300, 250], [300, 600]], - 'bidId': '30b31c1838de1e', - 'bidderRequestId': '22edbae2733bf6', - 'auctionId': '1d1a030790a475' + adUnitCode: 'adunit-code', + mediaTypes: { + banner: { + sizes: [[300, 250], [300, 600]], + }, + }, + bidId: 'test-bid-id', + bidderRequestId: 'test-bidder-request-id', + auctionId: 'test-auction-id' }]; - bidRequest = { - method: 'GET', - url: 'https://openx-d.openx.net/v/1.0/arj', - data: {}, - payload: {'bids': bidRequestConfigs, 'startTime': new Date()} - }; - }); - - it('handles nobid responses', function () { - const bidResponse = { - 'ads': - { - 'version': 1, - 'count': 1, - 'pixels': 'https://testpixels.net', - 'ad': [] - } + bidRequest = spec.buildRequests(bidRequestConfigs, {refererInfo: {}})[0]; + + bidResponse = { + seatbid: [{ + bid: [{ + impid: 'test-bid-id', + price: 2, + w: 300, + h: 250, + crid: 'test-creative-id', + dealid: 'test-deal-id', + adm: 'test-ad-markup' + }] + }], + cur: 'AUS' }; - const result = spec.interpretResponse({body: bidResponse}, bidRequest); - expect(result.length).to.equal(0); + bid = spec.interpretResponse({body: bidResponse}, bidRequest)[0]; }); - }); - describe('when adunits return out of order', function () { - const bidRequests = [{ - bidder: 'openx', - params: { - unit: '12345678', - delDomain: 'test-del-domain' - }, - adUnitCode: 'adunit-code', - mediaTypes: { - banner: { - sizes: [[100, 111]] - } - }, - bidId: 'test-bid-request-id-1', - bidderRequestId: 'test-request-1', - auctionId: 'test-auction-id-1' - }, { - bidder: 'openx', - params: { - unit: '12345678', - delDomain: 'test-del-domain' - }, - adUnitCode: 'adunit-code', - mediaTypes: { - banner: { - sizes: [[200, 222]] - } - }, - bidId: 'test-bid-request-id-2', - bidderRequestId: 'test-request-1', - auctionId: 'test-auction-id-1' - }, { - bidder: 'openx', - params: { - unit: '12345678', - delDomain: 'test-del-domain' - }, - adUnitCode: 'adunit-code', - mediaTypes: { - banner: { - sizes: [[300, 333]] - } - }, - 'bidId': 'test-bid-request-id-3', - 'bidderRequestId': 'test-request-1', - 'auctionId': 'test-auction-id-1' - }]; - const bidRequest = { - method: 'GET', - url: 'https://openx-d.openx.net/v/1.0/arj', - data: {}, - payload: {'bids': bidRequests, 'startTime': new Date()} - }; - - let outOfOrderAdunits = [ - mockAdUnit({ - idx: '1' - }, { - width: bidRequests[1].mediaTypes.banner.sizes[0][0], - height: bidRequests[1].mediaTypes.banner.sizes[0][1] - }), - mockAdUnit({ - idx: '2' - }, { - width: bidRequests[2].mediaTypes.banner.sizes[0][0], - height: bidRequests[2].mediaTypes.banner.sizes[0][1] - }), - mockAdUnit({ - idx: '0' - }, { - width: bidRequests[0].mediaTypes.banner.sizes[0][0], - height: bidRequests[0].mediaTypes.banner.sizes[0][1] - }) - ]; - - let bidResponse = mockArjResponse(undefined, outOfOrderAdunits); - - it('should return map adunits back to the proper request', function () { - const bids = spec.interpretResponse({body: bidResponse}, bidRequest); - expect(bids[0].requestId).to.equal(bidRequests[1].bidId); - expect(bids[0].width).to.equal(bidRequests[1].mediaTypes.banner.sizes[0][0]); - expect(bids[0].height).to.equal(bidRequests[1].mediaTypes.banner.sizes[0][1]); - expect(bids[1].requestId).to.equal(bidRequests[2].bidId); - expect(bids[1].width).to.equal(bidRequests[2].mediaTypes.banner.sizes[0][0]); - expect(bids[1].height).to.equal(bidRequests[2].mediaTypes.banner.sizes[0][1]); - expect(bids[2].requestId).to.equal(bidRequests[0].bidId); - expect(bids[2].width).to.equal(bidRequests[0].mediaTypes.banner.sizes[0][0]); - expect(bids[2].height).to.equal(bidRequests[0].mediaTypes.banner.sizes[0][1]); + it('should return the proper mediaType', function () { + it('should return a creativeId', function () { + expect(bid.mediaType).to.equal(Object.keys(bidRequestConfigs[0].mediaTypes)[0]); + }); }); }); - }); - - describe('interpretResponse for video ads', function () { - beforeEach(function () { - sinon.spy(userSync, 'registerSync'); - }); - afterEach(function () { - userSync.registerSync.restore(); - }); - - const bidsWithMediaTypes = [{ - 'bidder': 'openx', - 'mediaTypes': {video: {}}, - 'params': { - 'unit': '12345678', - 'delDomain': 'test-del-domain' - }, - 'adUnitCode': 'adunit-code', - 'sizes': [640, 480], - 'bidId': '30b31c1838de1e', - 'bidderRequestId': '22edbae2733bf6', - 'auctionId': '1d1a030790a475', - 'transactionId': '4008d88a-8137-410b-aa35-fbfdabcb478e' - }]; - const bidsWithMediaType = [{ - 'bidder': 'openx', - 'mediaType': 'video', - 'params': { - 'unit': '12345678', - 'delDomain': 'test-del-domain' - }, - 'adUnitCode': 'adunit-code', - 'sizes': [640, 480], - 'bidId': '30b31c1838de1e', - 'bidderRequestId': '22edbae2733bf6', - 'auctionId': '1d1a030790a475', - 'transactionId': '4008d88a-8137-410b-aa35-fbfdabcb478e' - }]; - const bidRequestsWithMediaTypes = { - method: 'GET', - url: 'https://openx-d.openx.net/v/1.0/avjp', - data: {}, - payload: {'bid': bidsWithMediaTypes[0], 'startTime': new Date()} - }; - const bidRequestsWithMediaType = { - method: 'GET', - url: 'https://openx-d.openx.net/v/1.0/avjp', - data: {}, - payload: {'bid': bidsWithMediaType[0], 'startTime': new Date()} - }; - const bidResponse = { - 'pub_rev': '1000', - 'width': '640', - 'height': '480', - 'adid': '5678', - 'currency': 'AUD', - 'vastUrl': 'https://testvast.com', - 'pixels': 'https://testpixels.net' - }; + if (FEATURES.VIDEO) { + context('when the response is a video', function() { + beforeEach(function () { + bidRequestConfigs = [{ + bidder: 'openx', + params: { + unit: '12345678', + delDomain: 'test-del-domain' + }, + adUnitCode: 'adunit-code', + mediaTypes: { + video: { + playerSize: [[640, 360], [854, 480]], + }, + }, + bidId: 'test-bid-id', + bidderRequestId: 'test-bidder-request-id', + auctionId: 'test-auction-id' + }]; + + bidRequest = spec.buildRequests(bidRequestConfigs, {refererInfo: {}})[0]; + + bidResponse = { + seatbid: [{ + bid: [{ + impid: 'test-bid-id', + price: 2, + w: 854, + h: 480, + crid: 'test-creative-id', + dealid: 'test-deal-id', + adm: 'test-ad-markup', + }] + }], + cur: 'AUS' + }; + }); - it('should return correct bid response with MediaTypes', function () { - const expectedResponse = { - 'requestId': '30b31c1838de1e', - 'cpm': 1, - 'width': 640, - 'height': 480, - 'mediaType': 'video', - 'creativeId': '5678', - 'vastUrl': 'https://testvast.com', - 'ttl': 300, - 'netRevenue': true, - 'currency': 'AUD' - }; - - const result = spec.interpretResponse({body: bidResponse}, bidRequestsWithMediaTypes); - expect(result[0]).to.eql(expectedResponse); - }); + it('should return the proper mediaType', function () { + bid = spec.interpretResponse({body: bidResponse}, bidRequest)[0]; + expect(bid.mediaType).to.equal(Object.keys(bidRequestConfigs[0].mediaTypes)[0]); + }); - it('should return correct bid response with MediaType', function () { - const expectedResponse = [ - { - 'requestId': '30b31c1838de1e', - 'cpm': 1, - 'width': '640', - 'height': '480', - 'mediaType': 'video', - 'creativeId': '5678', - 'vastUrl': 'https://testvast.com', - 'ttl': 300, - 'netRevenue': true, - 'currency': 'USD' - } - ]; + it('should return the proper mediaType', function () { + const winUrl = 'https//my.win.url'; + bidResponse.seatbid[0].bid[0].nurl = winUrl + bid = spec.interpretResponse({body: bidResponse}, bidRequest)[0]; - const result = spec.interpretResponse({body: bidResponse}, bidRequestsWithMediaType); - expect(JSON.stringify(Object.keys(result[0]).sort())).to.eql(JSON.stringify(Object.keys(expectedResponse[0]).sort())); - }); + expect(bid.vastUrl).to.equal(winUrl); + }); + }); + } - it('should return correct bid response with MediaType and deal_id', function () { - const bidResponseOverride = { 'deal_id': 'OX-mydeal' }; - const bidResponseWithDealId = Object.assign({}, bidResponse, bidResponseOverride); - const result = spec.interpretResponse({body: bidResponseWithDealId}, bidRequestsWithMediaType); - expect(result[0].dealId).to.equal(bidResponseOverride.deal_id); - }); + context('when the response contains FLEDGE interest groups config', function() { + let response; - it('should handle nobid responses for bidRequests with MediaTypes', function () { - const bidResponse = {'vastUrl': '', 'pub_rev': '', 'width': '', 'height': '', 'adid': '', 'pixels': ''}; - const result = spec.interpretResponse({body: bidResponse}, bidRequestsWithMediaTypes); - expect(result.length).to.equal(0); - }); + beforeEach(function () { + sinon.stub(config, 'getConfig') + .withArgs('fledgeEnabled') + .returns(true); - it('should handle nobid responses for bidRequests with MediaType', function () { - const bidResponse = {'vastUrl': '', 'pub_rev': '', 'width': '', 'height': '', 'adid': '', 'pixels': ''}; - const result = spec.interpretResponse({body: bidResponse}, bidRequestsWithMediaType); - expect(result.length).to.equal(0); - }); - }); + bidRequestConfigs = [{ + bidder: 'openx', + params: { + unit: '12345678', + delDomain: 'test-del-domain' + }, + adUnitCode: 'adunit-code', + mediaTypes: { + banner: { + sizes: [[300, 250], [300, 600]], + }, + }, + bidId: 'test-bid-id', + bidderRequestId: 'test-bidder-request-id', + auctionId: 'test-auction-id' + }]; - describe('user sync', function () { - const syncUrl = 'https://testpixels.net'; + bidRequest = spec.buildRequests(bidRequestConfigs, {refererInfo: {}})[0]; + + bidResponse = { + seatbid: [{ + bid: [{ + impid: 'test-bid-id', + price: 2, + w: 300, + h: 250, + crid: 'test-creative-id', + dealid: 'test-deal-id', + adm: 'test-ad-markup' + }] + }], + cur: 'AUS', + ext: { + fledge_auction_configs: { + 'test-bid-id': { + seller: 'codinginadtech.com', + interestGroupBuyers: ['somedomain.com'], + sellerTimeout: 0, + perBuyerSignals: { + 'somedomain.com': { + base_bid_micros: 0.1, + disallowed_advertiser_ids: [ + '1234', + '2345' + ], + multiplier: 1.3, + use_bid_multiplier: true, + win_reporting_id: '1234567asdf' + } + } + } + } + } + }; - describe('iframe sync', function () { - it('should register the pixel iframe from banner ad response', function () { - let syncs = spec.getUserSyncs( - {iframeEnabled: true}, - [{body: {ads: {pixels: syncUrl}}}] - ); - expect(syncs).to.deep.equal([{type: 'iframe', url: syncUrl}]); + response = spec.interpretResponse({body: bidResponse}, bidRequest); }); - it('should register the pixel iframe from video ad response', function () { - let syncs = spec.getUserSyncs( - {iframeEnabled: true}, - [{body: {pixels: syncUrl}}] - ); - expect(syncs).to.deep.equal([{type: 'iframe', url: syncUrl}]); + afterEach(function () { + config.getConfig.restore(); }); - it('should register the default iframe if no pixels available', function () { - let syncs = spec.getUserSyncs( - {iframeEnabled: true}, - [] - ); - expect(syncs).to.deep.equal([{type: 'iframe', url: 'https://u.openx.net/w/1.0/pd'}]); + it('should return FLEDGE auction_configs alongside bids', function () { + expect(response).to.have.property('bids'); + expect(response).to.have.property('fledgeAuctionConfigs'); + expect(response.fledgeAuctionConfigs.length).to.equal(1); + expect(response.fledgeAuctionConfigs[0].bidId).to.equal('test-bid-id'); }); }); + }); - describe('pixel sync', function () { - it('should register the image pixel from banner ad response', function () { - let syncs = spec.getUserSyncs( - {pixelEnabled: true}, - [{body: {ads: {pixels: syncUrl}}}] - ); - expect(syncs).to.deep.equal([{type: 'image', url: syncUrl}]); - }); + describe('user sync', function () { + it('should register the default image pixel if no pixels available', function () { + let syncs = spec.getUserSyncs( + {pixelEnabled: true}, + [] + ); + expect(syncs).to.deep.equal([{type: 'image', url: DEFAULT_SYNC}]); + }); - it('should register the image pixel from video ad response', function () { - let syncs = spec.getUserSyncs( - {pixelEnabled: true}, - [{body: {pixels: syncUrl}}] - ); - expect(syncs).to.deep.equal([{type: 'image', url: syncUrl}]); - }); + it('should register custom syncUrl when exists', function () { + let syncs = spec.getUserSyncs( + {pixelEnabled: true}, + [{body: {ext: {delDomain: 'www.url.com'}}}] + ); + expect(syncs).to.deep.equal([{type: 'image', url: 'https://www.url.com/w/1.0/pd'}]); + }); - it('should register the default image pixel if no pixels available', function () { - let syncs = spec.getUserSyncs( - {pixelEnabled: true}, - [] - ); - expect(syncs).to.deep.equal([{type: 'image', url: 'https://u.openx.net/w/1.0/pd'}]); - }); + it('should register custom syncUrl when exists', function () { + let syncs = spec.getUserSyncs( + {pixelEnabled: true}, + [{body: {ext: {platform: 'abc'}}}] + ); + expect(syncs).to.deep.equal([{type: 'image', url: SYNC_URL + '?ph=abc'}]); + }); + + it('when iframe sync is allowed, it should register an iframe sync', function () { + let syncs = spec.getUserSyncs( + {iframeEnabled: true}, + [] + ); + expect(syncs).to.deep.equal([{type: 'iframe', url: DEFAULT_SYNC}]); }); it('should prioritize iframe over image for user sync', function () { let syncs = spec.getUserSyncs( {iframeEnabled: true, pixelEnabled: true}, - [{body: {ads: {pixels: syncUrl}}}] + [] ); - expect(syncs).to.deep.equal([{type: 'iframe', url: syncUrl}]); + expect(syncs).to.deep.equal([{type: 'iframe', url: DEFAULT_SYNC}]); }); describe('when gdpr applies', function () { let gdprConsent; let gdprPixelUrl; + const consentString = 'gdpr-pixel-consent'; + const gdprApplies = '1'; beforeEach(() => { gdprConsent = { - consentString: 'test-gdpr-consent-string', + consentString, gdprApplies: true }; - gdprPixelUrl = 'https://testpixels.net?gdpr=1&gdpr_consent=gdpr-pixel-consent' + gdprPixelUrl = `${SYNC_URL}&gdpr=${gdprApplies}&gdpr_consent=${consentString}`; }); it('when there is a response, it should have the gdpr query params', () => { - let [{url}] = spec.getUserSyncs( - {iframeEnabled: true, pixelEnabled: true}, - [{body: {ads: {pixels: gdprPixelUrl}}}], - gdprConsent - ); - - expect(url).to.have.string('gdpr_consent=gdpr-pixel-consent'); - expect(url).to.have.string('gdpr=1'); - }); - - it('when there is no response, it should append gdpr query params', () => { let [{url}] = spec.getUserSyncs( {iframeEnabled: true, pixelEnabled: true}, [], gdprConsent ); - expect(url).to.have.string('gdpr_consent=test-gdpr-consent-string'); - expect(url).to.have.string('gdpr=1'); + + expect(url).to.have.string(`gdpr_consent=${consentString}`); + expect(url).to.have.string(`gdpr=${gdprApplies}`); }); it('should not send signals if no consent object is available', function () { @@ -2246,28 +1588,19 @@ describe('OpenxAdapter', function () { describe('when ccpa applies', function () { let usPrivacyConsent; let uspPixelUrl; + const privacyString = 'TEST'; beforeEach(() => { usPrivacyConsent = 'TEST'; - uspPixelUrl = 'https://testpixels.net?us_privacy=AAAA' + uspPixelUrl = `${DEFAULT_SYNC}&us_privacy=${privacyString}` }); - it('when there is a response, it should send the us privacy string from the response, ', () => { - let [{url}] = spec.getUserSyncs( - {iframeEnabled: true, pixelEnabled: true}, - [{body: {ads: {pixels: uspPixelUrl}}}], - undefined, - usPrivacyConsent - ); - - expect(url).to.have.string('us_privacy=AAAA'); - }); - it('when there is no response, it send have the us privacy string', () => { + it('should send the us privacy string, ', () => { let [{url}] = spec.getUserSyncs( {iframeEnabled: true, pixelEnabled: true}, [], undefined, usPrivacyConsent ); - expect(url).to.have.string(`us_privacy=${usPrivacyConsent}`); + expect(url).to.have.string(`us_privacy=${privacyString}`); }); it('should not send signals if no consent string is available', function () { @@ -2279,75 +1612,4 @@ describe('OpenxAdapter', function () { }); }); }); - - /** - * Makes sure the override object does not introduce - * new fields against the contract - * - * This does a shallow check in order to make key checking simple - * with respect to what a helper handles. For helpers that have - * nested fields, either check your design on maybe breaking it up - * to smaller, manageable pieces - * - * OR just call this on your nth level field if necessary. - * - * @param {Object} override Object with keys that overrides the default - * @param {Object} contract Original object contains the default fields - * @param {string} typeName Name of the type we're checking for error messages - * @throws {AssertionError} - */ - function overrideKeyCheck(override, contract, typeName) { - expect(contract).to.include.all.keys(Object.keys(override)); - } - - /** - * Creates a mock ArjResponse - * @param {OxArjResponse=} response - * @param {Array=} adUnits - * @throws {AssertionError} - * @return {OxArjResponse} - */ - function mockArjResponse(response, adUnits = []) { - let mockedArjResponse = utils.deepClone(DEFAULT_ARJ_RESPONSE); - - if (response) { - overrideKeyCheck(response, DEFAULT_ARJ_RESPONSE, 'OxArjResponse'); - overrideKeyCheck(response.ads, DEFAULT_ARJ_RESPONSE.ads, 'OxArjResponse'); - Object.assign(mockedArjResponse, response); - } - - if (adUnits.length) { - mockedArjResponse.ads.count = adUnits.length; - mockedArjResponse.ads.ad = adUnits.map((adUnit) => { - overrideKeyCheck(adUnit, DEFAULT_TEST_ARJ_AD_UNIT, 'OxArjAdUnit'); - return Object.assign(utils.deepClone(DEFAULT_TEST_ARJ_AD_UNIT), adUnit); - }); - } - - return mockedArjResponse; - } - - /** - * Creates a mock ArjAdUnit - * @param {OxArjAdUnit=} adUnit - * @param {OxArjCreative=} creative - * @throws {AssertionError} - * @return {OxArjAdUnit} - */ - function mockAdUnit(adUnit, creative) { - overrideKeyCheck(adUnit, DEFAULT_TEST_ARJ_AD_UNIT, 'OxArjAdUnit'); - - let mockedAdUnit = Object.assign(utils.deepClone(DEFAULT_TEST_ARJ_AD_UNIT), adUnit); - - if (creative) { - overrideKeyCheck(creative, DEFAULT_TEST_ARJ_CREATIVE); - if (creative.tracking) { - overrideKeyCheck(creative.tracking, DEFAULT_TEST_ARJ_CREATIVE.tracking, 'OxArjCreative'); - } - Object.assign(mockedAdUnit.creative[0], creative); - } - - return mockedAdUnit; - } -}) -; +}); diff --git a/test/spec/modules/openxOrtbBidAdapter_spec.js b/test/spec/modules/openxOrtbBidAdapter_spec.js index 09e76d6ca7a..d8ea79ac698 100644 --- a/test/spec/modules/openxOrtbBidAdapter_spec.js +++ b/test/spec/modules/openxOrtbBidAdapter_spec.js @@ -14,10 +14,67 @@ import 'modules/consentManagement.js'; import 'modules/consentManagementUsp.js'; import 'modules/schain.js'; import {deepClone} from 'src/utils.js'; +import {syncAddFPDToBidderRequest} from '../../helpers/fpd.js'; +import {hook} from '../../../src/hook.js'; const DEFAULT_SYNC = SYNC_URL + '?ph=' + DEFAULT_PH; +const BidRequestBuilder = function BidRequestBuilder(options) { + const defaults = { + request: { + auctionId: '4fd1ca2d-846c-4211-b9e5-321dfe1709c9', + adUnitCode: 'adunit-code', + bidder: 'openx' + }, + params: { + unit: '12345678', + delDomain: 'test-del-domain' + }, + sizes: [[300, 250], [300, 600]], + }; + + const request = { + ...defaults.request, + ...options + }; + + this.withParams = (options) => { + request.params = { + ...defaults.params, + ...options + }; + return this; + }; + + this.build = () => request; +}; + +const BidderRequestBuilder = function BidderRequestBuilder(options) { + const defaults = { + bidderCode: 'openx', + auctionId: '4fd1ca2d-846c-4211-b9e5-321dfe1709c9', + bidderRequestId: '7g36s867Tr4xF90X', + timeout: 3000, + refererInfo: { + numIframes: 0, + reachedTop: true, + referer: 'http://test.io/index.html?pbjs_debug=true' + } + }; + + const request = { + ...defaults, + ...options + }; + + this.build = () => request; +}; + describe('OpenxRtbAdapter', function () { + before(() => { + hook.ready(); + }); + const adapter = newBidder(spec); describe('inherited functions', function () { @@ -243,6 +300,18 @@ describe('OpenxRtbAdapter', function () { }); context('common requests checks', function() { + it('should be able to handle multiformat requests', () => { + const multiformat = utils.deepClone(bidRequestsWithMediaTypes[0]); + multiformat.mediaTypes.video = { + context: 'outstream', + playerSize: [640, 480] + } + const requests = spec.buildRequests([multiformat], mockBidderRequest); + const outgoingFormats = requests.flatMap(rq => rq.data.imp.flatMap(imp => ['banner', 'video'].filter(k => imp[k] != null))); + const expected = FEATURES.VIDEO ? ['banner', 'video'] : ['banner'] + expect(outgoingFormats).to.have.members(expected); + }) + it('should send bid request to openx url via POST', function () { const request = spec.buildRequests(bidRequestsWithMediaTypes, mockBidderRequest); expect(request[0].url).to.equal(REQUEST_URL); @@ -573,6 +642,47 @@ describe('OpenxRtbAdapter', function () { }); }); }); + + describe('with user agent client hints', function () { + it('should add device.sua if available', function () { + const bidderRequestWithUserAgentClientHints = { refererInfo: {}, + ortb2: { + device: { + sua: { + source: 2, + platform: { + brand: 'macOS', + version: [ '12', '4', '0' ] + }, + browsers: [ + { + brand: 'Chromium', + version: [ '106', '0', '5249', '119' ] + }, + { + brand: 'Google Chrome', + version: [ '106', '0', '5249', '119' ] + }, + { + brand: 'Not;A=Brand', + version: [ '99', '0', '0', '0' ] + }], + mobile: 0, + model: 'Pro', + bitness: '64', + architecture: 'x86' + } + } + }}; + + let request = spec.buildRequests(bidRequests, bidderRequestWithUserAgentClientHints); + expect(request[0].data.device.sua).to.exist; + expect(request[0].data.device.sua).to.deep.equal(bidderRequestWithUserAgentClientHints.ortb2.device.sua); + const bidderRequestWithoutUserAgentClientHints = {refererInfo: {}, ortb2: {}}; + request = spec.buildRequests(bidRequests, bidderRequestWithoutUserAgentClientHints); + expect(request[0].data.device?.sua).to.not.exist; + }); + }); }); context('when there is a consent management framework', function () { @@ -634,14 +744,14 @@ describe('OpenxRtbAdapter', function () { }); it('should send a signal to specify that US Privacy applies to this request', function () { - const request = spec.buildRequests(bidRequests, bidderRequest); + const request = spec.buildRequests(bidRequests, syncAddFPDToBidderRequest(bidderRequest)); expect(request[0].data.regs.ext.us_privacy).to.equal('1YYN'); expect(request[1].data.regs.ext.us_privacy).to.equal('1YYN'); }); it('should not send the regs object, when consent string is undefined', function () { delete bidderRequest.uspConsent; - const request = spec.buildRequests(bidRequests, bidderRequest); + const request = spec.buildRequests(bidRequests, syncAddFPDToBidderRequest(bidderRequest)); expect(request[0].data.regs?.us_privacy).to.not.exist; }); }); @@ -676,21 +786,21 @@ describe('OpenxRtbAdapter', function () { it('should send a signal to specify that GDPR applies to this request', function () { bidderRequest.bids = bidRequests; - const request = spec.buildRequests(bidRequests, bidderRequest); + const request = spec.buildRequests(bidRequests, syncAddFPDToBidderRequest(bidderRequest)); expect(request[0].data.regs.ext.gdpr).to.equal(1); expect(request[1].data.regs.ext.gdpr).to.equal(1); }); it('should send the consent string', function () { bidderRequest.bids = bidRequests; - const request = spec.buildRequests(bidRequests, bidderRequest); + const request = spec.buildRequests(bidRequests, syncAddFPDToBidderRequest(bidderRequest)); expect(request[0].data.user.ext.consent).to.equal(bidderRequest.gdprConsent.consentString); expect(request[1].data.user.ext.consent).to.equal(bidderRequest.gdprConsent.consentString); }); it('should send the addtlConsent string', function () { bidderRequest.bids = bidRequests; - const request = spec.buildRequests(bidRequests, bidderRequest); + const request = spec.buildRequests(bidRequests, syncAddFPDToBidderRequest(bidderRequest)); expect(request[0].data.user.ext.ConsentedProvidersSettings.consented_providers).to.equal(bidderRequest.gdprConsent.addtlConsent); expect(request[1].data.user.ext.ConsentedProvidersSettings.consented_providers).to.equal(bidderRequest.gdprConsent.addtlConsent); }); @@ -698,7 +808,7 @@ describe('OpenxRtbAdapter', function () { it('should send a signal to specify that GDPR does not apply to this request', function () { bidderRequest.gdprConsent.gdprApplies = false; bidderRequest.bids = bidRequests; - const request = spec.buildRequests(bidRequests, bidderRequest); + const request = spec.buildRequests(bidRequests, syncAddFPDToBidderRequest(bidderRequest)); expect(request[0].data.regs.ext.gdpr).to.equal(0); expect(request[1].data.regs.ext.gdpr).to.equal(0); }); @@ -707,7 +817,7 @@ describe('OpenxRtbAdapter', function () { 'but can send consent data, ', function () { delete bidderRequest.gdprConsent.gdprApplies; bidderRequest.bids = bidRequests; - const request = spec.buildRequests(bidRequests, bidderRequest); + const request = spec.buildRequests(bidRequests, syncAddFPDToBidderRequest(bidderRequest)); expect(request[0].data.regs?.ext?.gdpr).to.not.be.ok; expect(request[0].data.user.ext.consent).to.equal(bidderRequest.gdprConsent.consentString); expect(request[1].data.user.ext.consent).to.equal(bidderRequest.gdprConsent.consentString); @@ -716,7 +826,7 @@ describe('OpenxRtbAdapter', function () { it('when consent string is undefined, should not send the consent string, ', function () { delete bidderRequest.gdprConsent.consentString; bidderRequest.bids = bidRequests; - const request = spec.buildRequests(bidRequests, bidderRequest); + const request = spec.buildRequests(bidRequests, syncAddFPDToBidderRequest(bidderRequest)); expect(request[0].data.imp[0].ext.consent).to.equal(undefined); expect(request[1].data.imp[0].ext.consent).to.equal(undefined); }); @@ -725,7 +835,7 @@ describe('OpenxRtbAdapter', function () { context('coppa', function() { it('when there are no coppa param settings, should not send a coppa flag', function () { - const request = spec.buildRequests(bidRequestsWithMediaTypes, mockBidderRequest); + const request = spec.buildRequests(bidRequestsWithMediaTypes, syncAddFPDToBidderRequest(mockBidderRequest)); expect(request[0].data.regs?.coppa).to.be.not.ok; }); @@ -738,7 +848,13 @@ describe('OpenxRtbAdapter', function () { return utils.deepAccess(mockConfig, key); }); - const request = spec.buildRequests(bidRequestsWithMediaTypes, mockBidderRequest); + const request = spec.buildRequests(bidRequestsWithMediaTypes, syncAddFPDToBidderRequest(mockBidderRequest)); + expect(request[0].data.regs.coppa).to.equal(1); + }); + + it('should send a coppa flag there is when there is coppa param settings in the bid params', function () { + const request = spec.buildRequests(bidRequestsWithMediaTypes, syncAddFPDToBidderRequest(mockBidderRequest)); + request.params = {coppa: true}; expect(request[0].data.regs.coppa).to.equal(1); }); @@ -760,21 +876,21 @@ describe('OpenxRtbAdapter', function () { it('when there is a do not track, should send a dnt', function () { doNotTrackStub.returns(1); - const request = spec.buildRequests(bidRequestsWithMediaTypes, mockBidderRequest); + const request = spec.buildRequests(bidRequestsWithMediaTypes, syncAddFPDToBidderRequest(mockBidderRequest)); expect(request[0].data.device.dnt).to.equal(1); }); it('when there is not do not track, don\'t send dnt', function () { doNotTrackStub.returns(0); - const request = spec.buildRequests(bidRequestsWithMediaTypes, mockBidderRequest); + const request = spec.buildRequests(bidRequestsWithMediaTypes, syncAddFPDToBidderRequest(mockBidderRequest)); expect(request[0].data.device.dnt).to.equal(0); }); it('when there is no defined do not track, don\'t send dnt', function () { doNotTrackStub.returns(null); - const request = spec.buildRequests(bidRequestsWithMediaTypes, mockBidderRequest); + const request = spec.buildRequests(bidRequestsWithMediaTypes, syncAddFPDToBidderRequest(mockBidderRequest)); expect(request[0].data.device.dnt).to.equal(0); }); }); @@ -916,17 +1032,6 @@ describe('OpenxRtbAdapter', function () { }); context('FLEDGE', function() { - it('when FLEDGE is disabled, should not send imp.ext.ae', function () { - const request = spec.buildRequests( - bidRequestsWithMediaTypes, - { - ...mockBidderRequest, - fledgeEnabled: false - } - ); - expect(request[0].data.imp[0].ext).to.not.have.property('ae'); - }); - it('when FLEDGE is enabled, should send whatever is set in ortb2imp.ext.ae in all bid requests', function () { const request = spec.buildRequests(bidRequestsWithMediaTypes, { ...mockBidderRequest, @@ -944,51 +1049,53 @@ describe('OpenxRtbAdapter', function () { }); }); - context('video', function () { - it('should send bid request with a mediaTypes specified with video type', function () { - const request = spec.buildRequests(bidRequestsWithMediaTypes, mockBidderRequest); - expect(request[1].data.imp[0]).to.have.any.keys(VIDEO); - }); - }); + if (FEATURES.VIDEO) { + context('video', function () { + it('should send bid request with a mediaTypes specified with video type', function () { + const request = spec.buildRequests(bidRequestsWithMediaTypes, mockBidderRequest); + expect(request[1].data.imp[0]).to.have.any.keys(VIDEO); + }); - it.skip('should send ad unit ids when any are defined', function () { - const bidRequestsWithUnitIds = [{ - bidder: 'openx', - params: { - delDomain: 'test-del-domain' - }, - adUnitCode: 'adunit-code', - mediaTypes: { - banner: { - sizes: [[300, 250], [300, 600]] - } - }, - bidId: 'test-bid-id-1', - bidderRequestId: 'test-bid-request-1', - auctionId: 'test-auction-1', - transactionId: 'test-transaction-id-1' - }, { - bidder: 'openx', - params: { - unit: '22', - delDomain: 'test-del-domain' - }, - adUnitCode: 'adunit-code', - mediaTypes: { - banner: { - sizes: [[728, 90]] - } - }, - bidId: 'test-bid-id-2', - bidderRequestId: 'test-bid-request-2', - auctionId: 'test-auction-2', - transactionId: 'test-transaction-id-2' - }]; - mockBidderRequest.bids = bidRequestsWithUnitIds; - const request = spec.buildRequests(bidRequestsWithUnitIds, mockBidderRequest); - expect(request[0].data.imp[1].tagid).to.equal(bidRequestsWithUnitIds[1].params.unit); - expect(request[0].data.imp[1].ext.divid).to.equal(bidRequestsWithUnitIds[1].params.adUnitCode); - }); + it('Update imp.video with OpenRTB options from mimeTypes and params', function() { + const bid01 = new BidRequestBuilder({ + adUnitCode: 'adunit-code-01', + mediaTypes: { + banner: { sizes: [[300, 250]] }, + video: { + context: 'outstream', + playerSize: [[300, 250]], + mimes: ['video/mp4'], + protocols: [8] + } + }, + }).withParams({ + // options in video, will merge + video: { + skip: 1, + skipafter: 4, + minduration: 10, + maxduration: 30 + } + }).build(); + + const bidderRequest = new BidderRequestBuilder().build(); + const expected = { + mimes: ['video/mp4'], + skip: 1, + skipafter: 4, + minduration: 10, + maxduration: 30, + placement: 4, + protocols: [8], + w: 300, + h: 250 + }; + const requests = spec.buildRequests([bid01], bidderRequest); + expect(requests).to.have.lengthOf(2); + expect(requests[1].data.imp[0].video).to.deep.equal(expected); + }); + }); + } }); describe('interpretResponse()', function () { @@ -1270,56 +1377,58 @@ describe('OpenxRtbAdapter', function () { }); }); - context('when the response is a video', function() { - beforeEach(function () { - bidRequestConfigs = [{ - bidder: 'openx', - params: { - unit: '12345678', - delDomain: 'test-del-domain' - }, - adUnitCode: 'adunit-code', - mediaTypes: { - video: { - playerSize: [[640, 360], [854, 480]], + if (FEATURES.VIDEO) { + context('when the response is a video', function() { + beforeEach(function () { + bidRequestConfigs = [{ + bidder: 'openx', + params: { + unit: '12345678', + delDomain: 'test-del-domain' }, - }, - bidId: 'test-bid-id', - bidderRequestId: 'test-bidder-request-id', - auctionId: 'test-auction-id' - }]; - - bidRequest = spec.buildRequests(bidRequestConfigs, {refererInfo: {}})[0]; + adUnitCode: 'adunit-code', + mediaTypes: { + video: { + playerSize: [[640, 360], [854, 480]], + }, + }, + bidId: 'test-bid-id', + bidderRequestId: 'test-bidder-request-id', + auctionId: 'test-auction-id' + }]; - bidResponse = { - seatbid: [{ - bid: [{ - impid: 'test-bid-id', - price: 2, - w: 854, - h: 480, - crid: 'test-creative-id', - dealid: 'test-deal-id', - adm: 'test-ad-markup', - }] - }], - cur: 'AUS' - }; - }); + bidRequest = spec.buildRequests(bidRequestConfigs, {refererInfo: {}})[0]; + + bidResponse = { + seatbid: [{ + bid: [{ + impid: 'test-bid-id', + price: 2, + w: 854, + h: 480, + crid: 'test-creative-id', + dealid: 'test-deal-id', + adm: 'test-ad-markup', + }] + }], + cur: 'AUS' + }; + }); - it('should return the proper mediaType', function () { - bid = spec.interpretResponse({body: bidResponse}, bidRequest)[0]; - expect(bid.mediaType).to.equal(Object.keys(bidRequestConfigs[0].mediaTypes)[0]); - }); + it('should return the proper mediaType', function () { + bid = spec.interpretResponse({body: bidResponse}, bidRequest)[0]; + expect(bid.mediaType).to.equal(Object.keys(bidRequestConfigs[0].mediaTypes)[0]); + }); - it('should return the proper mediaType', function () { - const winUrl = 'https//my.win.url'; - bidResponse.seatbid[0].bid[0].nurl = winUrl - bid = spec.interpretResponse({body: bidResponse}, bidRequest)[0]; + it('should return the proper mediaType', function () { + const winUrl = 'https//my.win.url'; + bidResponse.seatbid[0].bid[0].nurl = winUrl + bid = spec.interpretResponse({body: bidResponse}, bidRequest)[0]; - expect(bid.vastUrl).to.equal(winUrl); + expect(bid.vastUrl).to.equal(winUrl); + }); }); - }); + } context('when the response contains FLEDGE interest groups config', function() { let response; @@ -1503,5 +1612,4 @@ describe('OpenxRtbAdapter', function () { }); }); }); -}) -; +}); diff --git a/test/spec/modules/optidigitalBidAdapter_spec.js b/test/spec/modules/optidigitalBidAdapter_spec.js new file mode 100755 index 00000000000..8c222650f7e --- /dev/null +++ b/test/spec/modules/optidigitalBidAdapter_spec.js @@ -0,0 +1,604 @@ +import { expect } from 'chai'; +import { spec } from 'modules/optidigitalBidAdapter.js'; +import * as utils from 'src/utils.js'; + +const ENDPOINT = 'https://pbs.optidigital.com/bidder'; + +describe('optidigitalAdapterTests', function () { + describe('isBidRequestValid', function () { + it('bidRequest with publisherId and placementId', function () { + expect(spec.isBidRequestValid({ + bidder: 'optidigital', + params: { + publisherId: 's123', + placementId: 'Billboard_Top' + } + })).to.equal(true); + }); + it('bidRequest without publisherId', function () { + expect(spec.isBidRequestValid({ + bidder: 'optidigital', + params: { + placementId: 'Billboard_Top' + } + })).to.equal(false); + }); + it('bidRequest without placementId', function () { + expect(spec.isBidRequestValid({ + bidder: 'optidigital', + params: { + publisherId: 's123' + } + })).to.equal(false); + }); + it('bidRequest without required parameters', function () { + expect(spec.isBidRequestValid({ + bidder: 'optidigital', + params: {} + })).to.equal(false); + }); + }); + + describe('buildRequests', function () { + const bidderRequest = { + bids: [ + { + 'bidder': 'optidigital', + 'params': { + 'publisherId': 's123', + 'placementId': 'Billboard_Top', + 'divId': 'Billboard_Top_3c5425', + 'badv': ['example.com'], + 'bcat': ['IAB1-1'], + 'bapp': ['com.blocked'], + 'battr': [1, 2] + }, + 'crumbs': { + 'pubcid': '7769fd03-574c-48fe-b512-8147f7c4023a' + }, + 'ortb2Imp': { + 'ext': { + 'tid': '0cb56262-9637-474d-a572-86fa860fd8b7', + 'data': { + 'adserver': { + 'name': 'gam', + 'adslot': '/19968336/header-bid-tag-0' + }, + 'pbadslot': '/19968336/header-bid-tag-0' + }, + 'gpid': '/19968336/header-bid-tag-0' + } + }, + 'mediaTypes': { + 'banner': { + 'sizes': [ [ 300, 250 ], [ 300, 600 ] ] + } + }, + 'adUnitCode': 'div-gpt-ad-1460505748561-0', + 'transactionId': '0cb56262-9637-474d-a572-86fa860fd8b7', + 'sizes': [ [ 300, 250 ], [ 300, 600 ] ], + 'bidId': '245d89f17f289f', + 'bidderRequestId': '199d7ffafa1e91', + 'auctionId': 'b66f01cd-3441-4403-99fa-d8062e795933', + 'src': 'client', + 'metrics': { + 'requestBids.usp': 0.5, + 'requestBids.pubCommonId': 0.29999999701976776, + 'requestBids.fpd': 3.1000000089406967, + 'requestBids.validate': 0.5, + 'requestBids.makeRequests': 2.2000000029802322, + 'requestBids.total': 570, + 'requestBids.callBids': 320.5, + 'adapter.client.net': [ + 317.30000001192093 + ], + 'adapters.client.optidigital.net': [ + 317.30000001192093 + ], + 'adapter.client.interpretResponse': [ + 0 + ], + 'adapters.client.optidigital.interpretResponse': [ + 0 + ], + 'adapter.client.validate': 0, + 'adapters.client.optidigital.validate': 0, + 'adapter.client.buildRequests': 1, + 'adapters.client.optidigital.buildRequests': 1, + 'adapter.client.total': 318.59999999403954, + 'adapters.client.optidigital.total': 318.59999999403954 + }, + 'bidRequestsCount': 1, + 'bidderRequestsCount': 1, + 'bidderWinsCount': 0, + 'ortb2': { + 'site': { + 'page': 'https://example.com', + 'ref': 'https://example.com', + 'domain': 'example.com', + 'publisher': { + 'domain': 'example.com' + } + }, + 'device': { + 'w': 1605, + 'h': 1329, + 'dnt': 0, + 'ua': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Safari/537.36', + 'language': 'pl', + 'sua': { + 'source': 2, + 'platform': { + 'brand': 'Windows', + 'version': [ + '10', + '0', + '0' + ] + }, + 'browsers': [ + { + 'brand': 'Not_A Brand', + 'version': [ + '99', + '0', + '0', + '0' + ] + }, + { + 'brand': 'Google Chrome', + 'version': [ + '109', + '0', + '5414', + '75' + ] + }, + { + 'brand': 'Chromium', + 'version': [ + '109', + '0', + '5414', + '75' + ] + } + ], + 'mobile': 0, + 'model': '', + 'bitness': '64', + 'architecture': 'x86' + } + } + } + } + ], + 'refererInfo': { + 'canonicalUrl': 'https://www.prebid.org/the/link/to/the/page' + } + }; + + let validBidRequests = [ + { + 'bidder': 'optidigital', + 'bidId': '51ef8751f9aead', + 'params': { + 'publisherId': 's123', + 'placementId': 'Billboard_Top', + 'badv': ['example.com'], + 'bcat': ['IAB1-1'], + 'bapp': ['com.blocked'], + 'battr': [1, 2] + }, + 'adUnitCode': 'div-gpt-ad-1460505748561-0', + 'transactionId': 'd7b773de-ceaa-484d-89ca-d9f51b8d61ec', + 'sizes': [[320, 50], [300, 250], [300, 600]], + 'bidderRequestId': '418b37f85e772c', + 'auctionId': '18fd8b8b0bd757' + } + ] + + it('should return an empty array if there are no bid requests', () => { + const emptyBidRequests = []; + const request = spec.buildRequests(emptyBidRequests, emptyBidRequests); + expect(request).to.be.an('array').that.is.empty; + }); + + it('should send bid request via POST', function() { + const request = spec.buildRequests(validBidRequests, bidderRequest); + expect(request.method).to.equal('POST'); + }); + + it('should send bid request to given endpoint', function() { + const request = spec.buildRequests(validBidRequests, bidderRequest); + expect(request.url).to.equal(ENDPOINT); + }); + + it('should be bidRequest data', function () { + const request = spec.buildRequests(validBidRequests, bidderRequest); + expect(request.data).to.exist; + }); + + it('should add schain object to payload if exists', function () { + const bidRequest = Object.assign({}, validBidRequests[0], { + schain: { + ver: '1.0', + complete: 1, + nodes: [{ + asi: 'examplewebsite.com', + sid: '00001', + hp: 1 + }] + } + }); + const request = spec.buildRequests([bidRequest], bidderRequest); + const payload = JSON.parse(request.data) + expect(payload.schain).to.exist; + expect(payload.schain).to.deep.equal({ + ver: '1.0', + complete: 1, + nodes: [{ + asi: 'examplewebsite.com', + sid: '00001', + hp: 1 + }] + }); + }); + + it('should add adContainerWidth and adContainerHeight to payload if divId exsists in parameter', function () { + let validBidRequestsWithDivId = [ + { + 'bidder': 'optidigital', + 'bidId': '51ef8751f9aead', + 'params': { + 'publisherId': 's123', + 'placementId': 'Billboard_Top', + 'divId': 'div-gpt-ad-1460505748561-0' + }, + 'adUnitCode': 'div-gpt-ad-1460505748561-0', + 'transactionId': 'd7b773de-ceaa-484d-89ca-d9f51b8d61ec', + 'mediaTypes': { + 'banner': { + 'sizes': [[300, 50], [300, 250], [300, 600]] + } + }, + 'sizes': [[320, 50], [300, 250], [300, 600]], + 'bidderRequestId': '418b37f85e772c', + 'auctionId': '18fd8b8b0bd757' + } + ] + const request = spec.buildRequests(validBidRequestsWithDivId, bidderRequest); + const payload = JSON.parse(request.data) + payload.imp[0].adContainerWidth = 1920 + payload.imp[0].adContainerHeight = 1080 + expect(payload.imp[0].adContainerWidth).to.exist; + expect(payload.imp[0].adContainerHeight).to.exist; + }); + + it('should add pageTemplate to payload if pageTemplate exsists in parameter', function () { + let validBidRequestsWithDivId = [ + { + 'bidder': 'optidigital', + 'bidId': '51ef8751f9aead', + 'params': { + 'publisherId': 's123', + 'placementId': 'Billboard_Top', + 'pageTemplate': 'home' + }, + 'adUnitCode': 'div-gpt-ad-1460505748561-0', + 'transactionId': 'd7b773de-ceaa-484d-89ca-d9f51b8d61ec', + 'mediaTypes': { + 'banner': { + 'sizes': [[300, 50], [300, 250], [300, 600]] + } + }, + 'sizes': [[320, 50], [300, 250], [300, 600]], + 'bidderRequestId': '418b37f85e772c', + 'auctionId': '18fd8b8b0bd757' + } + ] + const request = spec.buildRequests(validBidRequestsWithDivId, bidderRequest); + const payload = JSON.parse(request.data) + payload.imp[0].pageTemplate = 'home' + expect(payload.imp[0].pageTemplate).to.exist; + }); + + it('should add referrer to payload if it exsists in bidderRequest', function () { + bidderRequest.refererInfo.page = 'https://www.prebid.org'; + const request = spec.buildRequests(validBidRequests, bidderRequest); + const payload = JSON.parse(request.data) + expect(payload.referrer).to.equal('https://www.prebid.org'); + }); + + it('should use value for badv, bcat, bapp from params', function () { + bidderRequest.ortb2 = { + 'site': { + 'page': 'https://example.com', + 'ref': 'https://example.com', + 'domain': 'example.com', + 'publisher': { + 'domain': 'example.com' + } + }, + 'device': { + 'w': 1507, + 'h': 1329, + 'dnt': 0, + 'ua': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Safari/537.36', + 'language': 'pl', + 'sua': { + 'source': 2, + 'platform': { + 'brand': 'Windows', + 'version': [ + '10', + '0', + '0' + ] + }, + 'browsers': [ + { + 'brand': 'Not_A Brand', + 'version': [ + '99', + '0', + '0', + '0' + ] + }, + { + 'brand': 'Google Chrome', + 'version': [ + '109', + '0', + '5414', + '120' + ] + }, + { + 'brand': 'Chromium', + 'version': [ + '109', + '0', + '5414', + '120' + ] + } + ], + 'mobile': 0, + 'model': '', + 'bitness': '64', + 'architecture': 'x86' + } + } + }; + const request = spec.buildRequests(validBidRequests, bidderRequest); + const payload = JSON.parse(request.data); + expect(payload.badv).to.deep.equal(validBidRequests[0].params.badv); + expect(payload.bcat).to.deep.equal(validBidRequests[0].params.bcat); + expect(payload.bapp).to.deep.equal(validBidRequests[0].params.bapp); + }); + + it('should send empty GDPR consent and required set to false', function() { + const request = spec.buildRequests(validBidRequests, bidderRequest); + const payload = JSON.parse(request.data); + expect(payload.gdpr.consent).to.equal(''); + expect(payload.gdpr.required).to.equal(false); + }); + + it('should send GDPR to given endpoint', function() { + let consentString = 'DFR8KRePoQNsRREZCADBG+A=='; + bidderRequest.gdprConsent = { + 'consentString': consentString, + 'gdprApplies': true, + 'vendorData': { + 'hasGlobalConsent': false + }, + 'apiVersion': 1 + } + const request = spec.buildRequests(validBidRequests, bidderRequest); + const payload = JSON.parse(request.data); + expect(payload.gdpr).to.exist; + expect(payload.gdpr.consent).to.equal(consentString); + expect(payload.gdpr.required).to.exist.and.to.be.true; + }); + + it('should send empty GDPR consent to endpoint', function() { + let consentString = false; + bidderRequest.gdprConsent = { + 'consentString': consentString, + 'gdprApplies': true, + 'vendorData': { + 'hasGlobalConsent': false + }, + 'apiVersion': 1 + } + const request = spec.buildRequests(validBidRequests, bidderRequest); + const payload = JSON.parse(request.data); + expect(payload.gdpr.consent).to.equal(''); + }); + + it('should send uspConsent to given endpoint', function() { + bidderRequest.uspConsent = '1YYY'; + const request = spec.buildRequests(validBidRequests, bidderRequest); + const payload = JSON.parse(request.data); + expect(payload.uspConsent).to.exist; + }); + + it('should use appropriate mediaTypes banner sizes', function() { + const mediaTypesBannerSize = { + 'mediaTypes': { + 'banner': { + 'sizes': [300, 600] + } + } + }; + returnBannerSizes(mediaTypesBannerSize, '300x600'); + }); + + it('should use appropriate mediaTypes banner sizes as array', function() { + const mediaTypesBannerSize = { + 'mediaTypes': { + 'banner': { + 'sizes': [300, 600] + } + } + }; + returnBannerSizes(mediaTypesBannerSize, ['300x600']); + }); + + it('should fetch floor from floor module if it is available', function() { + let validBidRequestsWithCurrency = [ + { + 'bidder': 'optidigital', + 'bidId': '51ef8751f9aead', + 'params': { + 'publisherId': 's123', + 'placementId': 'Billboard_Top', + 'pageTemplate': 'home', + 'currency': 'USD' + }, + 'adUnitCode': 'div-gpt-ad-1460505748561-0', + 'transactionId': 'd7b773de-ceaa-484d-89ca-d9f51b8d61ec', + 'mediaTypes': { + 'banner': { + 'sizes': [[300, 50], [300, 250], [300, 600]] + } + }, + 'sizes': [[320, 50], [300, 250], [300, 600]], + 'bidderRequestId': '418b37f85e772c', + 'auctionId': '18fd8b8b0bd757' + } + ] + let floorInfo; + validBidRequestsWithCurrency[0].getFloor = () => floorInfo; + floorInfo = { currency: 'USD', floor: 1.99 }; + let request = spec.buildRequests(validBidRequestsWithCurrency, bidderRequest); + const payload = JSON.parse(request.data); + expect(payload.imp[0].bidFloor).to.exist; + }); + + function returnBannerSizes(mediaTypes, expectedSizes) { + const bidRequest = Object.assign(validBidRequests[0], mediaTypes); + const request = spec.buildRequests([bidRequest], bidderRequest); + const payload = JSON.parse(request.data); + return payload.imp.forEach(bid => { + if (Array.isArray(expectedSizes)) { + expect(JSON.stringify(bid.sizes)).to.equal(JSON.stringify(expectedSizes)); + } else { + expect(bid.sizes[0]).to.equal(expectedSizes); + } + }); + } + }); + describe('getUserSyncs', function() { + const syncurlIframe = 'https://scripts.opti-digital.com/js/presync.html?endpoint=optidigital'; + let test; + beforeEach(function () { + test = sinon.sandbox.create(); + }); + afterEach(function() { + test.restore(); + }); + + it('should be executed as in config', function() { + expect(spec.getUserSyncs({ iframeEnabled: true }, {}, undefined, undefined)).to.deep.equal([{ + type: 'iframe', url: syncurlIframe + }]); + }); + + it('should return appropriate URL', function() { + expect(spec.getUserSyncs({ iframeEnabled: true }, {}, {gdprApplies: true, consentString: 'foo'}, undefined)).to.deep.equal([{ + type: 'iframe', url: `${syncurlIframe}&gdpr=1&gdpr_consent=foo` + }]); + expect(spec.getUserSyncs({ iframeEnabled: true }, {}, {gdprApplies: false, consentString: 'foo'}, undefined)).to.deep.equal([{ + type: 'iframe', url: `${syncurlIframe}&gdpr=0&gdpr_consent=foo` + }]); + expect(spec.getUserSyncs({ iframeEnabled: true }, {}, {gdprApplies: true, consentString: undefined}, undefined)).to.deep.equal([{ + type: 'iframe', url: `${syncurlIframe}&gdpr=1&gdpr_consent=` + }]); + expect(spec.getUserSyncs({ iframeEnabled: true }, {}, {gdprApplies: true, consentString: 'foo'}, {consentString: 'fooUsp'})).to.deep.equal([{ + type: 'iframe', url: `${syncurlIframe}&gdpr=1&gdpr_consent=foo&ccpa_consent=fooUsp` + }]); + }); + }); + describe('interpretResponse', function () { + it('should get bids', function() { + let bids = { + 'body': { + 'bids': [{ + 'transactionId': 'cf5faec3-fcee-4f26-80ae-fc8b6cf23b7d', + 'placementId': 'Billboard_Top', + 'bidId': '83fb53a5e67f49', + 'ttl': 150, + 'creativeId': 'mobile_pos_2', + 'cur': 'USD', + 'cpm': 0.445455, + 'w': '300', + 'h': '600', + 'adm': '', + 'adomain': [] + }] + } + }; + let expectedResponse = [ + { + 'placementId': 'Billboard_Top', + 'transactionId': 'cf5faec3-fcee-4f26-80ae-fc8b6cf23b7d', + 'requestId': '83fb53a5e67f49', + 'ttl': 150, + 'creativeId': 'mobile_pos_2', + 'currency': 'USD', + 'cpm': 0.445455, + 'width': '300', + 'height': '600', + 'ad': '', + 'netRevenue': true, + 'meta': { + 'advertiserDomains': [] + } + } + ]; + let result = spec.interpretResponse(bids); + expect(result).to.eql(expectedResponse); + }); + + it('should handle empty array bid response', function() { + let bids = { + 'body': { + 'bids': [] + } + }; + let result = spec.interpretResponse(bids); + expect(result.length).to.equal(0); + }); + }); +}); diff --git a/test/spec/modules/orbitsoftBidAdapter_spec.js b/test/spec/modules/orbitsoftBidAdapter_spec.js new file mode 100644 index 00000000000..8c3187e9324 --- /dev/null +++ b/test/spec/modules/orbitsoftBidAdapter_spec.js @@ -0,0 +1,255 @@ +import {expect} from 'chai'; +import {spec} from 'modules/orbitsoftBidAdapter.js'; + +const ENDPOINT_URL = 'https://orbitsoft.com/php/ads/hb.phps'; +const REFERRER_URL = 'http://referrer.url/?_='; + +describe('Orbitsoft adapter', function () { + describe('implementation', function () { + describe('for requests', function () { + it('should accept valid bid', function () { + let validBid = { + bidder: 'orbitsoft', + params: { + placementId: '123', + requestUrl: ENDPOINT_URL + } + }, + isValid = spec.isBidRequestValid(validBid); + + expect(isValid).to.equal(true); + }); + + it('should reject invalid bid', function () { + let invalidBid = { + bidder: 'orbitsoft' + }, + isValid = spec.isBidRequestValid(invalidBid); + + expect(isValid).to.equal(false); + }); + }); + describe('for requests', function () { + it('should accept valid bid with styles', function () { + let validBid = { + bidder: 'orbitsoft', + params: { + placementId: '123', + requestUrl: ENDPOINT_URL, + style: { + title: { + family: 'Tahoma', + size: 'medium', + weight: 'normal', + style: 'normal', + color: '0053F9' + }, + description: { + family: 'Tahoma', + size: 'medium', + weight: 'normal', + style: 'normal', + color: '0053F9' + }, + url: { + family: 'Tahoma', + size: 'medium', + weight: 'normal', + style: 'normal', + color: '0053F9' + }, + colors: { + background: 'ffffff', + border: 'E0E0E0', + link: '5B99FE' + } + } + }, + refererInfo: {referer: REFERRER_URL}, + }, + isValid = spec.isBidRequestValid(validBid); + expect(isValid).to.equal(true); + + let buildRequest = spec.buildRequests([validBid])[0]; + let requestUrl = buildRequest.url; + let requestUrlParams = buildRequest.data; + expect(requestUrl).to.equal(ENDPOINT_URL); + expect(requestUrlParams).have.property('f1', 'Tahoma'); + expect(requestUrlParams).have.property('fs1', 'medium'); + expect(requestUrlParams).have.property('w1', 'normal'); + expect(requestUrlParams).have.property('s1', 'normal'); + expect(requestUrlParams).have.property('c3', '0053F9'); + expect(requestUrlParams).have.property('f2', 'Tahoma'); + expect(requestUrlParams).have.property('fs2', 'medium'); + expect(requestUrlParams).have.property('w2', 'normal'); + expect(requestUrlParams).have.property('s2', 'normal'); + expect(requestUrlParams).have.property('c4', '0053F9'); + expect(requestUrlParams).have.property('f3', 'Tahoma'); + expect(requestUrlParams).have.property('fs3', 'medium'); + expect(requestUrlParams).have.property('w3', 'normal'); + expect(requestUrlParams).have.property('s3', 'normal'); + expect(requestUrlParams).have.property('c5', '0053F9'); + expect(requestUrlParams).have.property('c2', 'ffffff'); + expect(requestUrlParams).have.property('c1', 'E0E0E0'); + expect(requestUrlParams).have.property('c6', '5B99FE'); + }); + + it('should accept valid bid with custom params', function () { + let validBid = { + bidder: 'orbitsoft', + params: { + placementId: '123', + requestUrl: ENDPOINT_URL, + customParams: { + cacheBuster: 'bf4d7c1', + clickUrl: 'http://testclickurl.com' + } + }, + refererInfo: {referer: REFERRER_URL}, + }, + isValid = spec.isBidRequestValid(validBid); + expect(isValid).to.equal(true); + + let buildRequest = spec.buildRequests([validBid])[0]; + let requestUrlCustomParams = buildRequest.data; + expect(requestUrlCustomParams).have.property('c.cacheBuster', 'bf4d7c1'); + expect(requestUrlCustomParams).have.property('c.clickUrl', 'http://testclickurl.com'); + }); + + it('should reject invalid bid without requestUrl', function () { + let invalidBid = { + bidder: 'orbitsoft', + params: { + placementId: '123' + } + }, + isValid = spec.isBidRequestValid(invalidBid); + + expect(isValid).to.equal(false); + }); + + it('should reject invalid bid without placementId', function () { + let invalidBid = { + bidder: 'orbitsoft', + params: { + requestUrl: ENDPOINT_URL + } + }, + isValid = spec.isBidRequestValid(invalidBid); + + expect(isValid).to.equal(false); + }); + }); + describe('bid responses', function () { + it('should return complete bid response', function () { + let serverResponse = { + body: { + callback_uid: '265b29b70cc106', + cpm: 0.5, + width: 240, + height: 240, + content_url: 'https://orbitsoft.com/php/ads/hb.html', + adomain: ['test.adomain.tld'] + } + }; + + let bidRequests = [ + { + bidder: 'orbitsoft', + params: { + placementId: '123', + requestUrl: ENDPOINT_URL + } + } + ]; + let bids = spec.interpretResponse(serverResponse, {'bidRequest': bidRequests[0]}); + expect(bids).to.be.lengthOf(1); + expect(bids[0].cpm).to.equal(serverResponse.body.cpm); + expect(bids[0].width).to.equal(serverResponse.body.width); + expect(bids[0].height).to.equal(serverResponse.body.height); + expect(bids[0].currency).to.equal('USD'); + expect(bids[0].netRevenue).to.equal(true); + expect(bids[0].adUrl).to.have.length.above(1); + expect(bids[0].adUrl).to.have.string('https://orbitsoft.com/php/ads/hb.html'); + expect(Object.keys(bids[0].meta)).to.include.members(['advertiserDomains']); + expect(bids[0].meta.advertiserDomains).to.deep.equal(serverResponse.body.adomain); + }); + + it('should return empty bid response', function () { + let bidRequests = [ + { + bidder: 'orbitsoft', + params: { + placementId: '123', + requestUrl: ENDPOINT_URL + } + } + ]; + let serverResponse = { + body: { + callback_uid: '265b29b70cc106', + cpm: 0 + } + }, + bids = spec.interpretResponse(serverResponse, {'bidRequest': bidRequests[0]}); + + expect(bids).to.be.lengthOf(0); + }); + + it('should return empty bid response on incorrect size', function () { + let bidRequests = [ + { + bidder: 'orbitsoft', + params: { + placementId: '123', + requestUrl: ENDPOINT_URL + } + } + ]; + let serverResponse = { + body: { + callback_uid: '265b29b70cc106', + cpm: 1.5, + width: 0, + height: 0 + } + }, + bids = spec.interpretResponse(serverResponse, {'bidRequest': bidRequests[0]}); + + expect(bids).to.be.lengthOf(0); + }); + + it('should return empty bid response with error', function () { + let bidRequests = [ + { + bidder: 'orbitsoft', + params: { + placementId: '123', + requestUrl: ENDPOINT_URL + } + } + ]; + let serverResponse = {error: 'error'}, + bids = spec.interpretResponse(serverResponse, {'bidRequest': bidRequests[0]}); + + expect(bids).to.be.lengthOf(0); + }); + + it('should return empty bid response on empty body', function () { + let bidRequests = [ + { + bidder: 'orbitsoft', + params: { + placementId: '123', + requestUrl: ENDPOINT_URL + } + } + ]; + let serverResponse = {}, + bids = spec.interpretResponse(serverResponse, {'bidRequest': bidRequests[0]}); + + expect(bids).to.be.lengthOf(0); + }); + }); + }); +}); diff --git a/test/spec/modules/outbrainBidAdapter_spec.js b/test/spec/modules/outbrainBidAdapter_spec.js index 5d7bebc1de1..f5ce00ed8df 100644 --- a/test/spec/modules/outbrainBidAdapter_spec.js +++ b/test/spec/modules/outbrainBidAdapter_spec.js @@ -1,7 +1,7 @@ -import {expect} from 'chai'; -import {spec} from 'modules/outbrainBidAdapter.js'; -import {config} from 'src/config.js'; -import {server} from 'test/mocks/xhr'; +import { expect } from 'chai'; +import { spec } from 'modules/outbrainBidAdapter.js'; +import { config } from 'src/config.js'; +import { server } from 'test/mocks/xhr'; import { createEidsArray } from 'modules/userId/eids.js'; describe('Outbrain Adapter', function () { @@ -45,6 +45,26 @@ describe('Outbrain Adapter', function () { ] } + const videoBidRequestParams = { + mediaTypes: { + video: { + playerSize: [[640, 480]], + mimes: ['video/mp4'], + protocols: [1, 2, 3, 4, 5, 6, 7, 8], + playbackmethod: [1], + skip: 1, + api: [2], + minbitrate: 1000, + maxbitrate: 3000, + minduration: 3, + maxduration: 10, + startdelay: 2, + placement: 4, + linearity: 1 + } + } + } + describe('isBidRequestValid', function () { before(() => { config.setConfig({ @@ -93,6 +113,34 @@ describe('Outbrain Adapter', function () { } expect(spec.isBidRequestValid(bid)).to.equal(true) }) + it('should succeed when bid contains video', function () { + const bid = { + bidder: 'outbrain', + params: { + publisher: { + id: 'publisher-id', + } + }, + ...videoBidRequestParams, + } + expect(spec.isBidRequestValid(bid)).to.equal(true) + }) + it('should fail when bid contains insufficient video information', function () { + const bid = { + bidder: 'outbrain', + params: { + publisher: { + id: 'publisher-id', + } + }, + mediaTypes: { + video: { + context: 'outstream' + } + }, + } + expect(spec.isBidRequestValid(bid)).to.equal(false) + }) it('should fail if publisher id is not set', function () { const bid = { bidder: 'outbrain', @@ -298,6 +346,61 @@ describe('Outbrain Adapter', function () { expect(res.data).to.deep.equal(JSON.stringify(expectedData)) }) + it('should build video request', function () { + const bidRequest = { + ...commonBidRequest, + ...videoBidRequestParams, + } + const expectedData = { + site: { + page: 'https://example.com/', + publisher: { + id: 'publisher-id' + } + }, + device: { + ua: navigator.userAgent + }, + source: { + fd: 1 + }, + cur: [ + 'USD' + ], + imp: [ + { + id: '1', + video: { + w: 640, + h: 480, + protocols: [1, 2, 3, 4, 5, 6, 7, 8], + playbackmethod: [1], + mimes: ['video/mp4'], + skip: 1, + api: [2], + minbitrate: 1000, + maxbitrate: 3000, + minduration: 3, + maxduration: 10, + startdelay: 2, + placement: 4, + linearity: 1 + } + } + ], + ext: { + prebid: { + channel: { + name: 'pbjs', version: '$prebid.version$' + } + } + } + } + const res = spec.buildRequests([bidRequest], commonBidderRequest) + expect(res.url).to.equal('https://bidder-url.com') + expect(res.data).to.deep.equal(JSON.stringify(expectedData)) + }) + it('should pass optional parameters in request', function () { const bidRequest = { ...commonBidRequest, @@ -390,7 +493,7 @@ describe('Outbrain Adapter', function () { ...commonBidRequest, ...nativeBidRequestParams, } - config.setConfig({coppa: true}) + config.setConfig({ coppa: true }) const res = spec.buildRequests([bidRequest], commonBidderRequest) const resData = JSON.parse(res.data) @@ -412,7 +515,7 @@ describe('Outbrain Adapter', function () { let res = spec.buildRequests([bidRequest], commonBidderRequest); const resData = JSON.parse(res.data) expect(resData.user.ext.eids).to.deep.equal([ - {source: 'liveramp.com', uids: [{id: 'id-value', atype: 3}]} + { source: 'liveramp.com', uids: [{ id: 'id-value', atype: 3 }] } ]); }); @@ -421,7 +524,7 @@ describe('Outbrain Adapter', function () { ...commonBidRequest, ...nativeBidRequestParams, } - bidRequest.getFloor = function() { + bidRequest.getFloor = function () { return { currency: 'USD', floor: 1.23, @@ -619,6 +722,67 @@ describe('Outbrain Adapter', function () { const res = spec.interpretResponse(serverResponse, request) expect(res).to.deep.equal(expectedRes) }); + + it('should interpret video response', function () { + const serverResponse = { + body: { + id: '123', + seatbid: [ + { + bid: [ + { + id: '111', + impid: '1', + price: 1.1, + adm: '\u003cVAST version="3.0"\u003e\u003cAd\u003e\u003cInLine\u003e\u003cAdSystem\u003ezemanta\u003c/AdSystem\u003e\u003cAdTitle\u003e1\u003c/AdTitle\u003e\u003cImpression\u003ehttp://win.com\u003c/Impression\u003e\u003cImpression\u003ehttp://example.com/imptracker\u003c/Impression\u003e\u003cCreatives\u003e\u003cCreative\u003e\u003cLinear\u003e\u003cDuration\u003e00:00:25\u003c/Duration\u003e\u003cTrackingEvents\u003e\u003cTracking event="start"\u003ehttp://example.com/start\u003c/Tracking\u003e\u003cTracking event="progress" offset="00:00:03"\u003ehttp://example.com/p3s\u003c/Tracking\u003e\u003c/TrackingEvents\u003e\u003cVideoClicks\u003e\u003cClickThrough\u003ehttp://link.com\u003c/ClickThrough\u003e\u003c/VideoClicks\u003e\u003cMediaFiles\u003e\u003cMediaFile delivery="progressive" type="video/mp4" bitrate="700" width="640" height="360"\u003ehttps://example.com/123_360p.mp4\u003c/MediaFile\u003e\u003c/MediaFiles\u003e\u003c/Linear\u003e\u003c/Creative\u003e\u003c/Creatives\u003e\u003c/InLine\u003e\u003c/Ad\u003e\u003c/VAST\u003e', + adid: '100', + cid: '5', + crid: '29998660', + cat: ['cat-1'], + adomain: [ + 'example.com' + ], + nurl: 'http://example.com/win/${AUCTION_PRICE}' + } + ], + seat: '100', + group: 1 + } + ], + bidid: '456', + cur: 'USD' + } + } + const request = { + bids: [ + { + ...commonBidRequest, + ...videoBidRequestParams + } + ] + } + const expectedRes = [ + { + requestId: request.bids[0].bidId, + cpm: 1.1, + creativeId: '29998660', + ttl: 360, + netRevenue: false, + currency: 'USD', + mediaType: 'video', + nurl: 'http://example.com/win/${AUCTION_PRICE}', + vastXml: 'zemanta1http://win.comhttp://example.com/imptracker00:00:25http://example.com/starthttp://example.com/p3shttp://link.comhttps://example.com/123_360p.mp4', + meta: { + 'advertiserDomains': [ + 'example.com' + ] + }, + } + ] + + const res = spec.interpretResponse(serverResponse, request) + expect(res).to.deep.equal(expectedRes) + }); }) }) @@ -637,41 +801,41 @@ describe('Outbrain Adapter', function () { }) it('should return user sync if pixel enabled with outbrain config', function () { - const ret = spec.getUserSyncs({pixelEnabled: true}) - expect(ret).to.deep.equal([{type: 'image', url: usersyncUrl}]) + const ret = spec.getUserSyncs({ pixelEnabled: true }) + expect(ret).to.deep.equal([{ type: 'image', url: usersyncUrl }]) }) it('should not return user sync if pixel disabled', function () { - const ret = spec.getUserSyncs({pixelEnabled: false}) + const ret = spec.getUserSyncs({ pixelEnabled: false }) expect(ret).to.be.an('array').that.is.empty }) it('should not return user sync if url is not set', function () { config.resetConfig() - const ret = spec.getUserSyncs({pixelEnabled: true}) + const ret = spec.getUserSyncs({ pixelEnabled: true }) expect(ret).to.be.an('array').that.is.empty }) - it('should pass GDPR consent', function() { - expect(spec.getUserSyncs({ pixelEnabled: true }, {}, {gdprApplies: true, consentString: 'foo'}, undefined)).to.deep.equal([{ + it('should pass GDPR consent', function () { + expect(spec.getUserSyncs({ pixelEnabled: true }, {}, { gdprApplies: true, consentString: 'foo' }, undefined)).to.deep.equal([{ type: 'image', url: `${usersyncUrl}?gdpr=1&gdpr_consent=foo` }]); - expect(spec.getUserSyncs({ pixelEnabled: true }, {}, {gdprApplies: false, consentString: 'foo'}, undefined)).to.deep.equal([{ + expect(spec.getUserSyncs({ pixelEnabled: true }, {}, { gdprApplies: false, consentString: 'foo' }, undefined)).to.deep.equal([{ type: 'image', url: `${usersyncUrl}?gdpr=0&gdpr_consent=foo` }]); - expect(spec.getUserSyncs({ pixelEnabled: true }, {}, {gdprApplies: true, consentString: undefined}, undefined)).to.deep.equal([{ + expect(spec.getUserSyncs({ pixelEnabled: true }, {}, { gdprApplies: true, consentString: undefined }, undefined)).to.deep.equal([{ type: 'image', url: `${usersyncUrl}?gdpr=1&gdpr_consent=` }]); }); - it('should pass US consent', function() { + it('should pass US consent', function () { expect(spec.getUserSyncs({ pixelEnabled: true }, {}, undefined, '1NYN')).to.deep.equal([{ type: 'image', url: `${usersyncUrl}?us_privacy=1NYN` }]); }); - it('should pass GDPR and US consent', function() { - expect(spec.getUserSyncs({ pixelEnabled: true }, {}, {gdprApplies: true, consentString: 'foo'}, '1NYN')).to.deep.equal([{ + it('should pass GDPR and US consent', function () { + expect(spec.getUserSyncs({ pixelEnabled: true }, {}, { gdprApplies: true, consentString: 'foo' }, '1NYN')).to.deep.equal([{ type: 'image', url: `${usersyncUrl}?gdpr=1&gdpr_consent=foo&us_privacy=1NYN` }]); }); diff --git a/test/spec/modules/permutiveRtdProvider_spec.js b/test/spec/modules/permutiveRtdProvider_spec.js index 3f104ee1e2e..1f39d1a2cda 100644 --- a/test/spec/modules/permutiveRtdProvider_spec.js +++ b/test/spec/modules/permutiveRtdProvider_spec.js @@ -2,12 +2,15 @@ import { permutiveSubmodule, storage, getSegments, - initSegments, isAcEnabled, isPermutiveOnPage, setBidderRtb, getModuleConfig, PERMUTIVE_SUBMODULE_CONFIG_KEY, + readAndSetCohorts, + PERMUTIVE_STANDARD_KEYWORD, + PERMUTIVE_STANDARD_AUD_KEYWORD, + PERMUTIVE_CUSTOM_COHORTS_KEYWORD, } from 'modules/permutiveRtdProvider.js' import { deepAccess, deepSetValue, mergeDeep } from '../../../src/utils.js' import { config } from 'src/config.js' @@ -188,24 +191,81 @@ describe('permutiveRtdProvider', function () { const moduleConfig = getConfig() const bidderConfig = {}; const acBidders = moduleConfig.params.acBidders - const expectedTargetingData = transformedTargeting().ac.map(seg => { + const segmentsData = transformedTargeting() + const expectedTargetingData = segmentsData.ac.map(seg => { return { id: seg } }) - setBidderRtb(bidderConfig, moduleConfig) + setBidderRtb(bidderConfig, moduleConfig, segmentsData) acBidders.forEach(bidder => { - expect(bidderConfig[bidder].user.data).to.deep.include.members([{ - name: 'permutive.com', - segment: expectedTargetingData - }]) + const customCohorts = segmentsData[bidder] || [] + expect(bidderConfig[bidder].user.data).to.deep.include.members([ + { + name: 'permutive.com', + segment: expectedTargetingData, + }, + // Should have custom cohorts specific for that bidder + { + name: 'permutive', + segment: customCohorts.map(seg => { + return { id: seg } + }), + }, + ]) }) }) + + it('should override existing ortb2.user.data reserved by permutive RTD', function () { + const reservedPermutiveStandardName = 'permutive.com' + const reservedPermutiveCustomCohortName = 'permutive' + + const moduleConfig = getConfig() + const acBidders = moduleConfig.params.acBidders + const segmentsData = transformedTargeting() + + const sampleOrtbConfig = { + user: { + data: [ + { + name: reservedPermutiveCustomCohortName, + segment: [{ id: 'remove-me' }, { id: 'remove-me-also' }] + }, + { + name: reservedPermutiveStandardName, + segment: [{ id: 'remove-me-also-also' }, { id: 'remove-me-also-also-also' }] + } + ] + } + } + + const bidderConfig = Object.fromEntries(acBidders.map(bidder => [bidder, sampleOrtbConfig])) + + setBidderRtb(bidderConfig, moduleConfig, segmentsData) + + acBidders.forEach(bidder => { + const customCohorts = segmentsData[bidder] || [] + + expect(bidderConfig[bidder].user.data).to.not.deep.include.members([...sampleOrtbConfig.user.data]) + expect(bidderConfig[bidder].user.data).to.deep.include.members([ + { + name: reservedPermutiveCustomCohortName, + segment: customCohorts.map(id => ({ id })), + }, + { + name: reservedPermutiveStandardName, + segment: segmentsData.ac.map(id => ({ id })), + }, + ]) + }) + }) + it('should include ortb2 user data transformation for IAB audience taxonomy', function() { const moduleConfig = getConfig() const bidderConfig = {} const acBidders = moduleConfig.params.acBidders - const expectedTargetingData = transformedTargeting().ac.map(seg => { + const segmentsData = transformedTargeting() + const expectedTargetingData = segmentsData.ac.map(seg => { return { id: seg } }) @@ -225,7 +285,7 @@ describe('permutiveRtdProvider', function () { } ) - setBidderRtb(bidderConfig, moduleConfig) + setBidderRtb(bidderConfig, moduleConfig, segmentsData) acBidders.forEach(bidder => { expect(bidderConfig[bidder].user.data).to.deep.include.members([ @@ -244,12 +304,13 @@ describe('permutiveRtdProvider', function () { it('should not overwrite ortb2 config', function () { const moduleConfig = getConfig() const acBidders = moduleConfig.params.acBidders + const segmentsData = transformedTargeting() + const sampleOrtbConfig = { site: { name: 'example' }, user: { - keywords: 'a,b', data: [ { name: 'www.dataprovider1.com', @@ -268,160 +329,254 @@ describe('permutiveRtdProvider', function () { segment: [1, 2, 3] } - setBidderRtb(bidderConfig, moduleConfig, { - // TODO: this argument is unused, is the test still valid / needed? - testTransformation: userData => transformedUserData - }) + setBidderRtb(bidderConfig, moduleConfig, segmentsData) acBidders.forEach(bidder => { expect(bidderConfig[bidder].site.name).to.equal(sampleOrtbConfig.site.name) - expect(bidderConfig[bidder].user.keywords).to.equal(sampleOrtbConfig.user.keywords) expect(bidderConfig[bidder].user.data).to.deep.include.members([sampleOrtbConfig.user.data[0]]) }) }) - }) + it('should update user.keywords and not override existing values', function () { + const moduleConfig = getConfig() + const acBidders = moduleConfig.params.acBidders + const segmentsData = transformedTargeting() - describe('Getting segments', function () { - it('should retrieve segments in the expected structure', function () { - const data = transformedTargeting() - expect(getSegments(250)).to.deep.equal(data) - }) - it('should enforce max segments', function () { - const max = 1 - const segments = getSegments(max) + const sampleOrtbConfig = { + site: { + name: 'example' + }, + user: { + keywords: 'a,b', + data: [ + { + name: 'www.dataprovider1.com', + ext: { taxonomyname: 'iab_audience_taxonomy' }, + segment: [{ id: '687' }, { id: '123' }] + } + ] + } + } - for (const key in segments) { - expect(segments[key]).to.have.length(max) + const bidderConfig = Object.fromEntries(acBidders.map(bidder => [bidder, sampleOrtbConfig])) + + const transformedUserData = { + name: 'transformation', + ext: { test: true }, + segment: [1, 2, 3] } - }) - }) - describe('Default segment targeting', function () { - it('sets segment targeting for Xandr', function () { - const data = transformedTargeting() - const adUnits = getAdUnits() - const config = getConfig() + setBidderRtb(bidderConfig, moduleConfig, segmentsData) - initSegments({ adUnits }, callback, config) + acBidders.forEach(bidder => { + const customCohortsData = segmentsData[bidder] || [] + const keywordGroups = { + [PERMUTIVE_STANDARD_KEYWORD]: segmentsData.ac, + [PERMUTIVE_STANDARD_AUD_KEYWORD]: segmentsData.ssp.cohorts, + [PERMUTIVE_CUSTOM_COHORTS_KEYWORD]: customCohortsData + } - function callback () { - adUnits.forEach(adUnit => { - adUnit.bids.forEach(bid => { - const { bidder, params } = bid + // Transform groups of key-values into a single array of strings + // i.e { permutive: ['1', '2'], p_standard: ['3', '4'] } => ['permutive=1', 'permutive=2', 'p_standard=3',' p_standard=4'] + const transformedKeywordGroups = Object.entries(keywordGroups) + .flatMap(([keyword, ids]) => ids.map(id => `${keyword}=${id}`)) - if (bidder === 'appnexus') { - expect(deepAccess(params, 'keywords.permutive')).to.eql(data.appnexus) - expect(deepAccess(params, 'keywords.p_standard')).to.eql(data.ac) - } - }) - }) - } + const keywords = `${sampleOrtbConfig.user.keywords},${transformedKeywordGroups.join(',')}` + + expect(bidderConfig[bidder].site.name).to.equal(sampleOrtbConfig.site.name) + expect(bidderConfig[bidder].user.data).to.deep.include.members([sampleOrtbConfig.user.data[0]]) + expect(bidderConfig[bidder].user.keywords).to.deep.equal(keywords) + }) }) + it('should merge ortb2 correctly for ac and ssps', function () { + const customTargetingData = { + ...getTargetingData(), + '_ppam': [], + '_psegs': [], + '_pcrprs': ['abc', 'def', 'xyz'], + '_pssps': { + ssps: ['foo', 'bar'], + cohorts: ['xyz', 'uvw'], + } + } + const segmentsData = transformedTargeting(customTargetingData) + setLocalStorage(customTargetingData) - it('sets segment targeting for Magnite', function () { - const data = transformedTargeting() - const adUnits = getAdUnits() - const config = getConfig() + const moduleConfig = { + name: 'permutive', + waitForIt: true, + params: { + acBidders: ['foo', 'other'], + maxSegs: 30 + } + } + const bidderConfig = {}; - initSegments({ adUnits }, callback, config) + setBidderRtb(bidderConfig, moduleConfig, segmentsData) - function callback () { - adUnits.forEach(adUnit => { - adUnit.bids.forEach(bid => { - const { bidder, params } = bid + // include both ac and ssp cohorts, as foo is both in ac bidders and ssps + const expectedFooTargetingData = [ + { id: 'abc' }, + { id: 'def' }, + { id: 'xyz' }, + { id: 'uvw' }, + ] + expect(bidderConfig['foo'].user.data).to.deep.include.members([{ + name: 'permutive.com', + segment: expectedFooTargetingData + }]) + + // don't include ac targeting as it's not in ac bidders + const expectedBarTargetingData = [ + { id: 'xyz' }, + { id: 'uvw' }, + ] + expect(bidderConfig['bar'].user.data).to.deep.include.members([{ + name: 'permutive.com', + segment: expectedBarTargetingData + }]) + + // only include ac targeting as this ssp is not in ssps list + const expectedOtherTargetingData = [ + { id: 'abc' }, + { id: 'def' }, + { id: 'xyz' }, + ] + expect(bidderConfig['other'].user.data).to.deep.include.members([{ + name: 'permutive.com', + segment: expectedOtherTargetingData + }]) + }) - if (bidder === 'rubicon') { - expect(deepAccess(params, 'visitor.permutive')).to.eql(data.rubicon) - expect(deepAccess(params, 'visitor.p_standard')).to.eql(data.ac) - } - }) + describe('ortb2.user.ext tests', function () { + it('should add nothing if there are no cohorts data', function () { + // Empty module config means we default + const moduleConfig = getConfig() + + const bidderConfig = {} + + // Passing empty values means there is no segment data + const segmentsData = transformedTargeting({ + _pdfps: [], + _prubicons: [], + _papns: [], + _psegs: [], + _ppam: [], + _pcrprs: [], + _pssps: { ssps: [], cohorts: [] } }) - } - }) - it('sets segment targeting for Magnite video', function () { - const targetingData = getTargetingData() - targetingData._prubicons.push(321) + setBidderRtb(bidderConfig, moduleConfig, segmentsData) - setLocalStorage(targetingData) + moduleConfig.params.acBidders.forEach(bidder => { + expect(bidderConfig[bidder].user).to.not.have.property('ext') + }) + }) - const data = transformedTargeting(targetingData) - const config = getConfig() + it('should add standard and custom cohorts', function () { + const moduleConfig = getConfig() - const adUnits = getAdUnits().filter(adUnit => adUnit.mediaTypes.video) - expect(adUnits).to.have.lengthOf(1) + const bidderConfig = {} - initSegments({ adUnits }, callback, config) + const segmentsData = transformedTargeting() - function callback() { - adUnits.forEach(adUnit => { - adUnit.bids.forEach(bid => { - const { bidder, params } = bid + setBidderRtb(bidderConfig, moduleConfig, segmentsData) - if (bidder === 'rubicon') { - expect( - deepAccess(params, 'visitor.permutive'), - 'Should map all targeting values to a string', - ).to.eql(data.rubicon.map(String)) - expect(deepAccess(params, 'visitor.p_standard')).to.eql(data.ac) - } - }) + moduleConfig.params.acBidders.forEach(bidder => { + const userExtData = { + // Default targeting + p_standard: segmentsData.ac, + } + + const customCohorts = segmentsData[bidder] || [] + if (customCohorts.length > 0) { + deepSetValue(userExtData, 'permutive', customCohorts) + } + + expect(bidderConfig[bidder].user.ext.data).to.deep + .eq(userExtData) }) - } - }) + }) - it('sets segment targeting for Ozone', function () { - const data = transformedTargeting() - const adUnits = getAdUnits() - const config = getConfig() + it('should add ac cohorts ONLY', function () { + const moduleConfig = getConfig() - initSegments({ adUnits }, callback, config) + const bidderConfig = {} - function callback () { - adUnits.forEach(adUnit => { - adUnit.bids.forEach(bid => { - const { bidder, params } = bid + const segmentsData = transformedTargeting() + moduleConfig.params.acBidders.forEach((bidder) => { + // Remove custom cohorts + delete segmentsData[bidder] + }) - if (bidder === 'ozone') { - expect(deepAccess(params, 'customData.0.targeting.p_standard')).to.eql(data.ac) - } + setBidderRtb(bidderConfig, moduleConfig, segmentsData) + + moduleConfig.params.acBidders.forEach((bidder) => { + expect(bidderConfig[bidder].user.ext.data).to.deep.equal({ + p_standard: segmentsData.ac }) }) - } + }) + + it('should add custom cohorts ONLY', function () { + const moduleConfig = getConfig() + + const bidderConfig = {} + + const segmentsData = transformedTargeting() + // Empty the AC cohorts + segmentsData['ac'] = [] + + setBidderRtb(bidderConfig, moduleConfig, segmentsData) + + moduleConfig.params.acBidders.forEach(bidder => { + const customCohorts = segmentsData[bidder] || [] + if (customCohorts.length > 0) { + expect(bidderConfig[bidder].user.ext.data).to.deep + .eq({ permutive: customCohorts }) + } else { + expect(bidderConfig[bidder].user).to.not.have.property('ext') + } + }) + }) }) }) - describe('Custom segment targeting', function () { - it('sets custom segment targeting for Magnite', function () { + describe('Getting segments', function () { + it('should retrieve segments in the expected structure', function () { const data = transformedTargeting() - const adUnits = getAdUnits() - const config = getConfig() + expect(getSegments(250)).to.deep.equal(data) + }) + it('should enforce max segments', function () { + const max = 1 + const segments = getSegments(max) - config.params.overwrites = { - rubicon: function (bid, data, acEnabled, utils, defaultFn) { - if (defaultFn) { - bid = defaultFn(bid, data, acEnabled) - } - if (data.gam && data.gam.length) { - utils.deepSetValue(bid, 'params.visitor.permutive', data.gam) - } + for (const key in segments) { + if (key === 'ssp') { + expect(segments[key].cohorts).to.have.length(max) + } else { + expect(segments[key]).to.have.length(max) } } + }) + }) - initSegments({ adUnits }, callback, config) + describe('Default segment targeting', function () { + it('sets segment targeting for Ozone', function () { + const data = transformedTargeting() + const adUnits = getAdUnits() + const config = getConfig() - function callback () { - adUnits.forEach(adUnit => { - adUnit.bids.forEach(bid => { - const { bidder, params } = bid + readAndSetCohorts({ adUnits }, config) - if (bidder === 'rubicon') { - expect(deepAccess(params, 'visitor.permutive')).to.eql(data.gam) - expect(deepAccess(params, 'visitor.p_standard')).to.eql(data.ac) - } - }) + adUnits.forEach(adUnit => { + adUnit.bids.forEach(bid => { + const { bidder, params } = bid + + if (bidder === 'ozone') { + expect(deepAccess(params, 'customData.0.targeting.p_standard')).to.eql(data.ac.concat(data.ssp.cohorts)) + } }) - } + }) }) }) @@ -430,73 +585,65 @@ describe('permutiveRtdProvider', function () { const adUnits = getAdUnits() const config = getConfig() - initSegments({ adUnits }, callback, config) + readAndSetCohorts({ adUnits }, config) - function callback () { - adUnits.forEach(adUnit => { - adUnit.bids.forEach(bid => { - const { bidder, params } = bid + adUnits.forEach(adUnit => { + adUnit.bids.forEach(bid => { + const { bidder, params } = bid - if (bidder === 'appnexus') { - expect(deepAccess(params, 'keywords.test_kv')).to.eql(['true']) - } - }) + if (bidder === 'appnexus') { + expect(deepAccess(params, 'keywords.test_kv')).to.eql(['true']) + } }) - } + }) }) it('doesn\'t overwrite existing key-values for Magnite', function () { const adUnits = getAdUnits() const config = getConfig() - initSegments({ adUnits }, callback, config) + readAndSetCohorts({ adUnits }, config) - function callback () { - adUnits.forEach(adUnit => { - adUnit.bids.forEach(bid => { - const { bidder, params } = bid + adUnits.forEach(adUnit => { + adUnit.bids.forEach(bid => { + const { bidder, params } = bid - if (bidder === 'rubicon') { - expect(deepAccess(params, 'visitor.test_kv')).to.eql(['true']) - } - }) + if (bidder === 'rubicon') { + expect(deepAccess(params, 'visitor.test_kv')).to.eql(['true']) + } }) - } + }) }) it('doesn\'t overwrite existing key-values for Ozone', function () { const adUnits = getAdUnits() const config = getConfig() - initSegments({ adUnits }, callback, config) + readAndSetCohorts({ adUnits }, config) - function callback () { - adUnits.forEach(adUnit => { - adUnit.bids.forEach(bid => { - const { bidder, params } = bid + adUnits.forEach(adUnit => { + adUnit.bids.forEach(bid => { + const { bidder, params } = bid - if (bidder === 'ozone') { - expect(deepAccess(params, 'customData.0.targeting.test_kv')).to.eql(['true']) - } - }) + if (bidder === 'ozone') { + expect(deepAccess(params, 'customData.0.targeting.test_kv')).to.eql(['true']) + } }) - } + }) }) it('doesn\'t overwrite existing key-values for TrustX', function () { const adUnits = getAdUnits() const config = getConfig() - initSegments({ adUnits }, callback, config) + readAndSetCohorts({ adUnits }, config) - function callback () { - adUnits.forEach(adUnit => { - adUnit.bids.forEach(bid => { - const { bidder, params } = bid + adUnits.forEach(adUnit => { + adUnit.bids.forEach(bid => { + const { bidder, params } = bid - if (bidder === 'trustx') { - expect(deepAccess(params, 'keywords.test_kv')).to.eql(['true']) - } - }) + if (bidder === 'trustx') { + expect(deepAccess(params, 'keywords.test_kv')).to.eql(['true']) + } }) - } + }) }) }) @@ -553,8 +700,10 @@ function transformedTargeting (data = getTargetingData()) { return { ac: [...data._pcrprs, ...data._ppam, ...data._psegs.filter(seg => seg >= 1000000)], appnexus: data._papns, + ix: data._pindexs, rubicon: data._prubicons, gam: data._pdfps, + ssp: data._pssps, } } @@ -565,7 +714,9 @@ function getTargetingData () { _papns: ['appnexus1', 'appnexus2'], _psegs: ['1234', '1000001', '1000002'], _ppam: ['ppam1', 'ppam2'], - _pcrprs: ['pcrprs1', 'pcrprs2'] + _pindexs: ['pindex1', 'pindex2'], + _pcrprs: ['pcrprs1', 'pcrprs2', 'dup'], + _pssps: { ssps: ['xyz', 'abc', 'dup'], cohorts: ['123', 'abc'] } } } diff --git a/test/spec/modules/prebidServerBidAdapter_spec.js b/test/spec/modules/prebidServerBidAdapter_spec.js index f5f77e61056..9516f0402c1 100644 --- a/test/spec/modules/prebidServerBidAdapter_spec.js +++ b/test/spec/modules/prebidServerBidAdapter_spec.js @@ -1,4 +1,5 @@ -import { expect } from 'chai'; +/* eslint-disable no-trailing-spaces */ +import {expect} from 'chai'; import { PrebidServer as Adapter, resetSyncedStatus, @@ -7,31 +8,32 @@ import { } from 'modules/prebidServerBidAdapter/index.js'; import adapterManager from 'src/adapterManager.js'; import * as utils from 'src/utils.js'; -import { ajax } from 'src/ajax.js'; -import { config } from 'src/config.js'; +import {deepAccess, deepClone, mergeDeep} from 'src/utils.js'; +import {ajax} from 'src/ajax.js'; +import {config} from 'src/config.js'; import * as events from 'src/events.js'; import CONSTANTS from 'src/constants.json'; -import { server } from 'test/mocks/xhr.js'; -import { createEidsArray } from 'modules/userId/eids.js'; -import { deepAccess, deepClone } from 'src/utils.js'; -import 'modules/appnexusBidAdapter.js' // appnexus alias test -import 'modules/rubiconBidAdapter.js' // rubicon alias test -import 'src/prebid.js' // $$PREBID_GLOBAL$$.aliasBidder test -import 'modules/currency.js' // adServerCurrency test -// also load modules that register ORTB processors -import 'modules/currency.js'; +import {server} from 'test/mocks/xhr.js'; +import {createEidsArray} from 'modules/userId/eids.js'; +import 'modules/appnexusBidAdapter.js'; // appnexus alias test +import 'modules/rubiconBidAdapter.js'; // rubicon alias test +import 'src/prebid.js'; // $$PREBID_GLOBAL$$.aliasBidder test +import 'modules/currency.js'; // adServerCurrency test import 'modules/userId/index.js'; import 'modules/multibid/index.js'; import 'modules/priceFloors.js'; import 'modules/consentManagement.js'; import 'modules/consentManagementUsp.js'; import 'modules/schain.js'; -import { hook } from '../../../src/hook.js'; -import { decorateAdUnitsWithNativeParams } from '../../../src/native.js'; -import { auctionManager } from '../../../src/auctionManager.js'; -import { stubAuctionIndex } from '../../helpers/indexStub.js'; -import { registerBidder } from 'src/adapters/bidderFactory.js'; +import 'modules/fledgeForGpt.js'; +import {hook} from '../../../src/hook.js'; +import {decorateAdUnitsWithNativeParams} from '../../../src/native.js'; +import {auctionManager} from '../../../src/auctionManager.js'; +import {stubAuctionIndex} from '../../helpers/indexStub.js'; +import {addComponentAuction, registerBidder} from 'src/adapters/bidderFactory.js'; import {getGlobal} from '../../../src/prebidGlobal.js'; +import {syncAddFPDEnrichments, syncAddFPDToBidderRequest} from '../../helpers/fpd.js'; +import {deepSetValue} from '../../../src/utils.js'; let CONFIG = { accountId: '1', @@ -544,6 +546,16 @@ const RESPONSE_OPENRTB_NATIVE = { ] }; +function addFpdEnrichmentsToS2SRequest(s2sReq, bidderRequests) { + return { + ...s2sReq, + ortb2Fragments: { + ...(s2sReq.ortb2Fragments || {}), + global: syncAddFPDToBidderRequest({...(bidderRequests?.[0] || {}), ortb2: s2sReq.ortb2Fragments?.global || {}}).ortb2 + } + } +} + describe('S2S Adapter', function () { let adapter, addBidResponse = sinon.spy(), @@ -696,51 +708,53 @@ describe('S2S Adapter', function () { expect(server.requests.length).to.equal(0); }); - it('should add outstream bc renderer exists on mediatype', function () { - config.setConfig({ s2sConfig: CONFIG }); + if (FEATURES.VIDEO) { + it('should add outstream bc renderer exists on mediatype', function () { + config.setConfig({ s2sConfig: CONFIG }); - adapter.callBids(OUTSTREAM_VIDEO_REQUEST, BID_REQUESTS, addBidResponse, done, ajax); + adapter.callBids(OUTSTREAM_VIDEO_REQUEST, BID_REQUESTS, addBidResponse, done, ajax); - const requestBid = JSON.parse(server.requests[0].requestBody); - expect(requestBid.imp[0].banner).to.exist; - expect(requestBid.imp[0].video).to.exist; - }); + const requestBid = JSON.parse(server.requests[0].requestBody); + expect(requestBid.imp[0].banner).to.exist; + expect(requestBid.imp[0].video).to.exist; + }); - it('should default video placement if not defined and instream', function () { - let ortb2Config = utils.deepClone(CONFIG); - ortb2Config.endpoint.p1Consent = 'https://prebid.adnxs.com/pbs/v1/openrtb2/auction'; + it('should default video placement if not defined and instream', function () { + let ortb2Config = utils.deepClone(CONFIG); + ortb2Config.endpoint.p1Consent = 'https://prebid.adnxs.com/pbs/v1/openrtb2/auction'; - config.setConfig({ s2sConfig: ortb2Config }); + config.setConfig({ s2sConfig: ortb2Config }); - let videoBid = utils.deepClone(VIDEO_REQUEST); - videoBid.ad_units[0].mediaTypes.video.context = 'instream'; - adapter.callBids(videoBid, BID_REQUESTS, addBidResponse, done, ajax); + let videoBid = utils.deepClone(VIDEO_REQUEST); + videoBid.ad_units[0].mediaTypes.video.context = 'instream'; + adapter.callBids(videoBid, BID_REQUESTS, addBidResponse, done, ajax); - const requestBid = JSON.parse(server.requests[0].requestBody); - expect(requestBid.imp[0].banner).to.not.exist; - expect(requestBid.imp[0].video).to.exist; - expect(requestBid.imp[0].video.placement).to.equal(1); - }); + const requestBid = JSON.parse(server.requests[0].requestBody); + expect(requestBid.imp[0].banner).to.not.exist; + expect(requestBid.imp[0].video).to.exist; + expect(requestBid.imp[0].video.placement).to.equal(1); + }); - it('converts video mediaType properties into openRTB format', function () { - let ortb2Config = utils.deepClone(CONFIG); - ortb2Config.endpoint.p1Consent = 'https://prebid.adnxs.com/pbs/v1/openrtb2/auction'; + it('converts video mediaType properties into openRTB format', function () { + let ortb2Config = utils.deepClone(CONFIG); + ortb2Config.endpoint.p1Consent = 'https://prebid.adnxs.com/pbs/v1/openrtb2/auction'; - config.setConfig({ s2sConfig: ortb2Config }); + config.setConfig({ s2sConfig: ortb2Config }); - let videoBid = utils.deepClone(VIDEO_REQUEST); - videoBid.ad_units[0].mediaTypes.video.context = 'instream'; - adapter.callBids(videoBid, BID_REQUESTS, addBidResponse, done, ajax); + let videoBid = utils.deepClone(VIDEO_REQUEST); + videoBid.ad_units[0].mediaTypes.video.context = 'instream'; + adapter.callBids(videoBid, BID_REQUESTS, addBidResponse, done, ajax); - const requestBid = JSON.parse(server.requests[0].requestBody); - expect(requestBid.imp[0].banner).to.not.exist; - expect(requestBid.imp[0].video).to.exist; - expect(requestBid.imp[0].video.placement).to.equal(1); - expect(requestBid.imp[0].video.w).to.equal(640); - expect(requestBid.imp[0].video.h).to.equal(480); - expect(requestBid.imp[0].video.playerSize).to.be.undefined; - expect(requestBid.imp[0].video.context).to.be.undefined; - }); + const requestBid = JSON.parse(server.requests[0].requestBody); + expect(requestBid.imp[0].banner).to.not.exist; + expect(requestBid.imp[0].video).to.exist; + expect(requestBid.imp[0].video.placement).to.equal(1); + expect(requestBid.imp[0].video.w).to.equal(640); + expect(requestBid.imp[0].video.h).to.equal(480); + expect(requestBid.imp[0].video.playerSize).to.be.undefined; + expect(requestBid.imp[0].video.context).to.be.undefined; + }); + } it('exists and is a function', function () { expect(adapter.callBids).to.exist.and.to.be.a('function'); @@ -766,7 +780,7 @@ describe('S2S Adapter', function () { let gdprBidRequest = utils.deepClone(BID_REQUESTS); gdprBidRequest[0].gdprConsent = mockConsent(); - adapter.callBids(REQUEST, gdprBidRequest, addBidResponse, done, ajax); + adapter.callBids(addFpdEnrichmentsToS2SRequest(REQUEST, gdprBidRequest), gdprBidRequest, addBidResponse, done, ajax); let requestBid = JSON.parse(server.requests[0].requestBody); expect(requestBid.regs.ext.gdpr).is.equal(1); @@ -775,7 +789,7 @@ describe('S2S Adapter', function () { config.resetConfig(); config.setConfig({ s2sConfig: CONFIG }); - adapter.callBids(REQUEST, BID_REQUESTS, addBidResponse, done, ajax); + adapter.callBids(addFpdEnrichmentsToS2SRequest(REQUEST, BID_REQUESTS), BID_REQUESTS, addBidResponse, done, ajax); requestBid = JSON.parse(server.requests[1].requestBody); expect(requestBid.regs).to.not.exist; @@ -791,7 +805,7 @@ describe('S2S Adapter', function () { addtlConsent: 'superduperconsent', }); - adapter.callBids(REQUEST, gdprBidRequest, addBidResponse, done, ajax); + adapter.callBids(addFpdEnrichmentsToS2SRequest(REQUEST, gdprBidRequest), gdprBidRequest, addBidResponse, done, ajax); let requestBid = JSON.parse(server.requests[0].requestBody); expect(requestBid.regs.ext.gdpr).is.equal(1); @@ -877,13 +891,13 @@ describe('S2S Adapter', function () { $$PREBID_GLOBAL$$.requestBids.removeAll(); }); - it('is added to ortb2 request when in bidRequest', function () { + it('is added to ortb2 request when in FPD', function () { config.setConfig({ s2sConfig: CONFIG }); let uspBidRequest = utils.deepClone(BID_REQUESTS); uspBidRequest[0].uspConsent = '1NYN'; - adapter.callBids(REQUEST, uspBidRequest, addBidResponse, done, ajax); + adapter.callBids(addFpdEnrichmentsToS2SRequest(REQUEST, uspBidRequest), uspBidRequest, addBidResponse, done, ajax); let requestBid = JSON.parse(server.requests[0].requestBody); expect(requestBid.regs.ext.us_privacy).is.equal('1NYN'); @@ -891,7 +905,7 @@ describe('S2S Adapter', function () { config.resetConfig(); config.setConfig({ s2sConfig: CONFIG }); - adapter.callBids(REQUEST, BID_REQUESTS, addBidResponse, done, ajax); + adapter.callBids(addFpdEnrichmentsToS2SRequest(REQUEST, BID_REQUESTS), BID_REQUESTS, addBidResponse, done, ajax); requestBid = JSON.parse(server.requests[1].requestBody); expect(requestBid.regs).to.not.exist; @@ -929,7 +943,7 @@ describe('S2S Adapter', function () { consentBidRequest[0].uspConsent = '1NYN'; consentBidRequest[0].gdprConsent = mockConsent(); - adapter.callBids(REQUEST, consentBidRequest, addBidResponse, done, ajax); + adapter.callBids(addFpdEnrichmentsToS2SRequest(REQUEST, consentBidRequest), consentBidRequest, addBidResponse, done, ajax); let requestBid = JSON.parse(server.requests[0].requestBody); expect(requestBid.regs.ext.us_privacy).is.equal('1NYN'); @@ -977,14 +991,14 @@ describe('S2S Adapter', function () { }; config.setConfig(_config); - adapter.callBids(REQUEST, BID_REQUESTS, addBidResponse, done, ajax); + adapter.callBids(addFpdEnrichmentsToS2SRequest(REQUEST, BID_REQUESTS), BID_REQUESTS, addBidResponse, done, ajax); const requestBid = JSON.parse(server.requests[0].requestBody); sinon.assert.match(requestBid.device, { ifa: '6D92078A-8246-4BA4-AE5B-76104861E7DC', w: window.innerWidth, h: window.innerHeight }) - expect(requestBid.app).to.deep.equal({ + sinon.assert.match(requestBid.app, { bundle: 'com.test.app', publisher: { 'id': '1' } }); @@ -1004,14 +1018,14 @@ describe('S2S Adapter', function () { }; config.setConfig(_config); - adapter.callBids(REQUEST, BID_REQUESTS, addBidResponse, done, ajax); + adapter.callBids(addFpdEnrichmentsToS2SRequest(REQUEST, BID_REQUESTS), BID_REQUESTS, addBidResponse, done, ajax); const requestBid = JSON.parse(server.requests[0].requestBody); sinon.assert.match(requestBid.device, { ifa: '6D92078A-8246-4BA4-AE5B-76104861E7DC', w: window.innerWidth, h: window.innerHeight }) - expect(requestBid.app).to.deep.equal({ + sinon.assert.match(requestBid.app, { bundle: 'com.test.app', publisher: { 'id': '1' } }); @@ -1367,13 +1381,13 @@ describe('S2S Adapter', function () { }; config.setConfig(_config); - adapter.callBids(REQUEST, BID_REQUESTS, addBidResponse, done, ajax); + adapter.callBids(addFpdEnrichmentsToS2SRequest(REQUEST, BID_REQUESTS), BID_REQUESTS, addBidResponse, done, ajax); const requestBid = JSON.parse(server.requests[0].requestBody); sinon.assert.match(requestBid.device, { w: window.innerWidth, h: window.innerHeight }) - expect(requestBid.app).to.deep.equal({ + sinon.assert.match(requestBid.app, { bundle: 'com.test.app', publisher: { 'id': '1' } }); @@ -1474,7 +1488,7 @@ describe('S2S Adapter', function () { }; config.setConfig(_config); - adapter.callBids(REQUEST, BID_REQUESTS, addBidResponse, done, ajax); + adapter.callBids(addFpdEnrichmentsToS2SRequest(REQUEST, BID_REQUESTS), BID_REQUESTS, addBidResponse, done, ajax); const requestBid = JSON.parse(server.requests[0].requestBody); expect(requestBid.site).to.exist.and.to.be.a('object'); expect(requestBid.site.publisher).to.exist.and.to.be.a('object'); @@ -1491,10 +1505,33 @@ describe('S2S Adapter', function () { content: { language: 'en' }, + domain: 'mytestpage.com', page: 'http://mytestpage.com' }); }); + it('site should not be present when app is present', function () { + const _config = { + s2sConfig: CONFIG, + app: { bundle: 'com.test.app' }, + site: { + publisher: { + id: '1234', + domain: 'test.com' + }, + content: { + language: 'en' + } + } + }; + + config.setConfig(_config); + adapter.callBids(addFpdEnrichmentsToS2SRequest(REQUEST, BID_REQUESTS), BID_REQUESTS, addBidResponse, done, ajax); + const requestBid = JSON.parse(server.requests[0].requestBody); + expect(requestBid.site).to.not.exist; + expect(requestBid.app).to.exist.and.to.be.a('object'); + }); + it('adds appnexus aliases to request', function () { config.setConfig({ s2sConfig: CONFIG }); @@ -1693,6 +1730,121 @@ describe('S2S Adapter', function () { }]); }); + describe('filterSettings', function () { + const getRequestBid = userSync => { + let cookieSyncConfig = utils.deepClone(CONFIG); + const s2sBidRequest = utils.deepClone(REQUEST); + cookieSyncConfig.syncEndpoint = { p1Consent: 'https://prebid.adnxs.com/pbs/v1/cookie_sync' }; + s2sBidRequest.s2sConfig = cookieSyncConfig; + + config.setConfig({ userSync, s2sConfig: cookieSyncConfig }); + + let bidRequest = utils.deepClone(BID_REQUESTS); + adapter.callBids(s2sBidRequest, bidRequest, addBidResponse, done, ajax); + return JSON.parse(server.requests[0].requestBody); + } + + it('correctly adds filterSettings to the cookie_sync request if userSync.filterSettings is present in the config and only the all key is present in userSync.filterSettings', function () { + const userSync = { + filterSettings: { + all: { + bidders: ['appnexus', 'rubicon', 'pubmatic'], + filter: 'exclude' + } + } + }; + const requestBid = getRequestBid(userSync); + + expect(requestBid.filterSettings).to.deep.equal({ + 'image': { + 'bidders': ['appnexus', 'rubicon', 'pubmatic'], + 'filter': 'exclude' + }, + 'iframe': { + 'bidders': ['appnexus', 'rubicon', 'pubmatic'], + 'filter': 'exclude' + } + }); + }); + + it('correctly adds filterSettings to the cookie_sync request if userSync.filterSettings is present in the config and only the iframe key is present in userSync.filterSettings', function () { + const userSync = { + filterSettings: { + iframe: { + bidders: ['rubicon', 'pubmatic'], + filter: 'include' + } + } + }; + const requestBid = getRequestBid(userSync); + + expect(requestBid.filterSettings).to.deep.equal({ + 'image': { + 'bidders': '*', + 'filter': 'include' + }, + 'iframe': { + 'bidders': ['rubicon', 'pubmatic'], + 'filter': 'include' + } + }); + }); + + it('correctly adds filterSettings to the cookie_sync request if userSync.filterSettings is present in the config and the image and iframe keys are both present in userSync.filterSettings', function () { + const userSync = { + filterSettings: { + image: { + bidders: ['triplelift', 'appnexus'], + filter: 'include' + }, + iframe: { + bidders: ['pulsepoint', 'triplelift', 'appnexus', 'rubicon'], + filter: 'exclude' + } + } + }; + const requestBid = getRequestBid(userSync); + + expect(requestBid.filterSettings).to.deep.equal({ + 'image': { + 'bidders': ['triplelift', 'appnexus'], + 'filter': 'include' + }, + 'iframe': { + 'bidders': ['pulsepoint', 'triplelift', 'appnexus', 'rubicon'], + 'filter': 'exclude' + } + }); + }); + + it('correctly adds filterSettings to the cookie_sync request if userSync.filterSettings is present in the config and the all and iframe keys are both present in userSync.filterSettings', function () { + const userSync = { + filterSettings: { + all: { + bidders: ['triplelift', 'appnexus'], + filter: 'include' + }, + iframe: { + bidders: ['pulsepoint', 'triplelift', 'appnexus', 'rubicon'], + filter: 'exclude' + } + } + }; + const requestBid = getRequestBid(userSync); + + expect(requestBid.filterSettings).to.deep.equal({ + 'image': { + 'bidders': ['triplelift', 'appnexus'], + 'filter': 'include' + }, + 'iframe': { + 'bidders': ['pulsepoint', 'triplelift', 'appnexus', 'rubicon'], + 'filter': 'exclude' + } + }); + }); + }); + it('adds limit to the cookie_sync request if userSyncLimit is greater than 0', function () { let cookieSyncConfig = utils.deepClone(CONFIG); cookieSyncConfig.syncEndpoint = { p1Consent: 'https://prebid.adnxs.com/pbs/v1/cookie_sync' }; @@ -1796,7 +1948,7 @@ describe('S2S Adapter', function () { device: device }); - adapter.callBids(s2sBidRequest, BID_REQUESTS, addBidResponse, done, ajax); + adapter.callBids(addFpdEnrichmentsToS2SRequest(s2sBidRequest, BID_REQUESTS), BID_REQUESTS, addBidResponse, done, ajax); const requestBid = JSON.parse(server.requests[0].requestBody); expect(requestBid.site).to.exist.and.to.be.a('object'); @@ -1815,7 +1967,7 @@ describe('S2S Adapter', function () { device: device }); - adapter.callBids(s2sBidRequest, BID_REQUESTS, addBidResponse, done, ajax); + adapter.callBids(addFpdEnrichmentsToS2SRequest(s2sBidRequest, BID_REQUESTS), BID_REQUESTS, addBidResponse, done, ajax); const requestBid = JSON.parse(server.requests[0].requestBody); expect(requestBid.site).to.exist.and.to.be.a('object'); @@ -2343,7 +2495,11 @@ describe('S2S Adapter', function () { })); const commonContextExpected = utils.mergeDeep({ 'page': 'http://mytestpage.com', - 'publisher': { 'id': '1' } + 'domain': 'mytestpage.com', + 'publisher': { + 'id': '1', + 'domain': 'mytestpage.com' + } }, commonSite); const ortb2Fragments = { @@ -2351,7 +2507,7 @@ describe('S2S Adapter', function () { bidder: Object.fromEntries(allowedBidders.map(bidder => [bidder, {site, user, bcat, badv}])) }; - adapter.callBids({...s2sBidRequest, ortb2Fragments}, bidRequests, addBidResponse, done, ajax); + adapter.callBids(addFpdEnrichmentsToS2SRequest({...s2sBidRequest, ortb2Fragments}, bidRequests), bidRequests, addBidResponse, done, ajax); const parsedRequestBody = JSON.parse(server.requests[0].requestBody); expect(parsedRequestBody.ext.prebid.bidderconfig).to.deep.equal(expected); expect(parsedRequestBody.site).to.deep.equal(commonContextExpected); @@ -2508,6 +2664,62 @@ describe('S2S Adapter', function () { }); }); + describe('Bidder-level ortb2Imp', () => { + beforeEach(() => { + config.setConfig({ + s2sConfig: { + ...CONFIG, + bidders: ['A', 'B'] + } + }) + }) + it('should be set on imp.ext.prebid.imp', () => { + const s2sReq = utils.deepClone(REQUEST); + s2sReq.ad_units[0].ortb2Imp = {l0: 'adUnit'}; + s2sReq.ad_units[0].bids = [ + { + bidder: 'A', + bid_id: 1, + ortb2Imp: { + l2: 'A' + } + }, + { + bidder: 'B', + bid_id: 2, + ortb2Imp: { + l2: 'B' + } + } + ]; + const bidderReqs = [ + { + ...BID_REQUESTS[0], + bidderCode: 'A', + bids: [{ + bidId: 1, + bidder: 'A' + }] + }, + { + ...BID_REQUESTS[0], + bidderCode: 'B', + bids: [{ + bidId: 2, + bidder: 'B' + }] + } + ] + adapter.callBids(s2sReq, bidderReqs, addBidResponse, done, ajax); + const req = JSON.parse(server.requests[0].requestBody); + expect(req.imp[0].l0).to.eql('adUnit'); + expect(req.imp[0].ext.prebid.imp).to.eql({ + A: {l2: 'A'}, + B: {l2: 'B'} + }); + }); + }); + describe('ext.prebid config', function () { it('should send \"imp.ext.prebid.storedrequest.id\" if \"ortb2Imp.ext.prebid.storedrequest.id\" is set', function () { const consentConfig = { s2sConfig: CONFIG }; @@ -2601,28 +2813,30 @@ describe('S2S Adapter', function () { expect(response).to.have.property('dealId', 'test-dealid'); }); - it('should pass through default adserverTargeting if present in bidObject for video request', function () { - config.setConfig({ s2sConfig: CONFIG }); - const cacheResponse = utils.deepClone(RESPONSE_OPENRTB); - const targetingTestData = { - hb_cache_path: '/cache', - hb_cache_host: 'prebid-cache.testurl.com' - }; + if (FEATURES.VIDEO) { + it('should pass through default adserverTargeting if present in bidObject for video request', function () { + config.setConfig({ s2sConfig: CONFIG }); + const cacheResponse = utils.deepClone(RESPONSE_OPENRTB); + const targetingTestData = { + hb_cache_path: '/cache', + hb_cache_host: 'prebid-cache.testurl.com' + }; - cacheResponse.seatbid.forEach(item => { - item.bid[0].ext.prebid.targeting = targetingTestData - }); - adapter.callBids(VIDEO_REQUEST, BID_REQUESTS, addBidResponse, done, ajax); - server.requests[0].respond(200, {}, JSON.stringify(cacheResponse)); + cacheResponse.seatbid.forEach(item => { + item.bid[0].ext.prebid.targeting = targetingTestData + }); + adapter.callBids(VIDEO_REQUEST, BID_REQUESTS, addBidResponse, done, ajax); + server.requests[0].respond(200, {}, JSON.stringify(cacheResponse)); - sinon.assert.calledOnce(addBidResponse); - const response = addBidResponse.firstCall.args[1]; - expect(response).to.have.property('adserverTargeting'); - expect(response.adserverTargeting).to.deep.equal({ - 'hb_cache_path': '/cache', - 'hb_cache_host': 'prebid-cache.testurl.com' + sinon.assert.calledOnce(addBidResponse); + const response = addBidResponse.firstCall.args[1]; + expect(response).to.have.property('adserverTargeting'); + expect(response.adserverTargeting).to.deep.equal({ + 'hb_cache_path': '/cache', + 'hb_cache_host': 'prebid-cache.testurl.com' + }); }); - }); + } it('should set the bidResponse currency to whats in the PBS response', function () { adapter.callBids(REQUEST, BID_REQUESTS, addBidResponse, done, ajax); @@ -2720,6 +2934,21 @@ describe('S2S Adapter', function () { expect(response).to.have.property('ttl', 60); }); + it('handles seatnonbid responses and calls SEAT_NON_BID', function () { + const original = CONFIG; + CONFIG.extPrebid = { returnallbidstatus: true }; + const nonbidResponse = {...RESPONSE_OPENRTB, ext: {seatnonbid: [{}]}}; + config.setConfig({ CONFIG }); + CONFIG = original; + adapter.callBids(REQUEST, BID_REQUESTS, addBidResponse, done, ajax); + const responding = deepClone(nonbidResponse); + Object.assign(responding.ext.seatnonbid, [{auctionId: 2}]) + server.requests[0].respond(200, {}, JSON.stringify(responding)); + const event = events.emit.secondCall.args; + expect(event[0]).to.equal(CONSTANTS.EVENTS.SEAT_NON_BID); + expect(event[1].seatnonbid[0]).to.have.property('auctionId', 2); + }); + it('respects defaultTtl', function () { const s2sConfig = Object.assign({}, CONFIG, { defaultTtl: 30 @@ -2738,60 +2967,62 @@ describe('S2S Adapter', function () { expect(response).to.have.property('ttl', 30); }); - it('handles OpenRTB video responses', function () { - const s2sConfig = Object.assign({}, CONFIG, { - endpoint: { - p1Consent: 'https://prebidserverurl/openrtb2/auction?querystring=param' - } - }); - config.setConfig({ s2sConfig }); - - const s2sVidRequest = utils.deepClone(VIDEO_REQUEST); - s2sVidRequest.s2sConfig = s2sConfig; + if (FEATURES.VIDEO) { + it('handles OpenRTB video responses', function () { + const s2sConfig = Object.assign({}, CONFIG, { + endpoint: { + p1Consent: 'https://prebidserverurl/openrtb2/auction?querystring=param' + } + }); + config.setConfig({ s2sConfig }); - adapter.callBids(s2sVidRequest, BID_REQUESTS, addBidResponse, done, ajax); - server.requests[0].respond(200, {}, JSON.stringify(RESPONSE_OPENRTB_VIDEO)); + const s2sVidRequest = utils.deepClone(VIDEO_REQUEST); + s2sVidRequest.s2sConfig = s2sConfig; - sinon.assert.calledOnce(addBidResponse); - const response = addBidResponse.firstCall.args[1]; - expect(response).to.have.property('statusMessage', 'Bid available'); - expect(response).to.have.property('vastXml', RESPONSE_OPENRTB_VIDEO.seatbid[0].bid[0].adm); - expect(response).to.have.property('mediaType', 'video'); - expect(response).to.have.property('bidderCode', 'appnexus'); - expect(response).to.have.property('requestId', '123'); - expect(response).to.have.property('cpm', 10); - }); + adapter.callBids(s2sVidRequest, BID_REQUESTS, addBidResponse, done, ajax); + server.requests[0].respond(200, {}, JSON.stringify(RESPONSE_OPENRTB_VIDEO)); - it('handles response cache from ext.prebid.cache.vastXml', function () { - const s2sConfig = Object.assign({}, CONFIG, { - endpoint: { - p1Consent: 'https://prebidserverurl/openrtb2/auction?querystring=param' - } + sinon.assert.calledOnce(addBidResponse); + const response = addBidResponse.firstCall.args[1]; + expect(response).to.have.property('statusMessage', 'Bid available'); + expect(response).to.have.property('vastXml', RESPONSE_OPENRTB_VIDEO.seatbid[0].bid[0].adm); + expect(response).to.have.property('mediaType', 'video'); + expect(response).to.have.property('bidderCode', 'appnexus'); + expect(response).to.have.property('requestId', '123'); + expect(response).to.have.property('cpm', 10); }); - config.setConfig({ s2sConfig }); - const cacheResponse = utils.deepClone(RESPONSE_OPENRTB_VIDEO); - cacheResponse.seatbid.forEach(item => { - item.bid[0].ext.prebid.cache = { - vastXml: { - cacheId: 'abcd1234', - url: 'https://prebid-cache.net/cache?uuid=abcd1234' + + it('handles response cache from ext.prebid.cache.vastXml', function () { + const s2sConfig = Object.assign({}, CONFIG, { + endpoint: { + p1Consent: 'https://prebidserverurl/openrtb2/auction?querystring=param' } - } - }); + }); + config.setConfig({ s2sConfig }); + const cacheResponse = utils.deepClone(RESPONSE_OPENRTB_VIDEO); + cacheResponse.seatbid.forEach(item => { + item.bid[0].ext.prebid.cache = { + vastXml: { + cacheId: 'abcd1234', + url: 'https://prebid-cache.net/cache?uuid=abcd1234' + } + } + }); - const s2sVidRequest = utils.deepClone(VIDEO_REQUEST); - s2sVidRequest.s2sConfig = s2sConfig; + const s2sVidRequest = utils.deepClone(VIDEO_REQUEST); + s2sVidRequest.s2sConfig = s2sConfig; - adapter.callBids(s2sVidRequest, BID_REQUESTS, addBidResponse, done, ajax); - server.requests[0].respond(200, {}, JSON.stringify(cacheResponse)); + adapter.callBids(s2sVidRequest, BID_REQUESTS, addBidResponse, done, ajax); + server.requests[0].respond(200, {}, JSON.stringify(cacheResponse)); - sinon.assert.calledOnce(addBidResponse); - const response = addBidResponse.firstCall.args[1]; + sinon.assert.calledOnce(addBidResponse); + const response = addBidResponse.firstCall.args[1]; - expect(response).to.have.property('statusMessage', 'Bid available'); - expect(response).to.have.property('videoCacheKey', 'abcd1234'); - expect(response).to.have.property('vastUrl', 'https://prebid-cache.net/cache?uuid=abcd1234'); - }); + expect(response).to.have.property('statusMessage', 'Bid available'); + expect(response).to.have.property('videoCacheKey', 'abcd1234'); + expect(response).to.have.property('vastUrl', 'https://prebid-cache.net/cache?uuid=abcd1234'); + }); + } it('add adserverTargeting object to bids when ext.prebid.targeting is defined', function () { const s2sConfig = Object.assign({}, CONFIG, { @@ -2810,20 +3041,22 @@ describe('S2S Adapter', function () { item.bid[0].ext.prebid.targeting = targetingTestData }); - const s2sVidRequest = utils.deepClone(VIDEO_REQUEST); - s2sVidRequest.s2sConfig = s2sConfig; + if (FEATURES.VIDEO) { + const s2sVidRequest = utils.deepClone(VIDEO_REQUEST); + s2sVidRequest.s2sConfig = s2sConfig; - adapter.callBids(s2sVidRequest, BID_REQUESTS, addBidResponse, done, ajax); - server.requests[0].respond(200, {}, JSON.stringify(cacheResponse)); + adapter.callBids(s2sVidRequest, BID_REQUESTS, addBidResponse, done, ajax); + server.requests[0].respond(200, {}, JSON.stringify(cacheResponse)); - sinon.assert.calledOnce(addBidResponse); - const response = addBidResponse.firstCall.args[1]; + sinon.assert.calledOnce(addBidResponse); + const response = addBidResponse.firstCall.args[1]; - expect(response).to.have.property('adserverTargeting'); - expect(response.adserverTargeting).to.deep.equal({ - 'hb_cache_path': '/cache', - 'hb_cache_host': 'prebid-cache.testurl.com' - }); + expect(response).to.have.property('adserverTargeting'); + expect(response.adserverTargeting).to.deep.equal({ + 'hb_cache_path': '/cache', + 'hb_cache_host': 'prebid-cache.testurl.com' + }); + } }); it('handles response cache from ext.prebid.targeting', function () { @@ -2842,18 +3075,20 @@ describe('S2S Adapter', function () { } }); - const s2sVidRequest = utils.deepClone(VIDEO_REQUEST); - s2sVidRequest.s2sConfig = s2sConfig; + if (FEATURES.VIDEO) { + const s2sVidRequest = utils.deepClone(VIDEO_REQUEST); + s2sVidRequest.s2sConfig = s2sConfig; - adapter.callBids(s2sVidRequest, BID_REQUESTS, addBidResponse, done, ajax); - server.requests[0].respond(200, {}, JSON.stringify(cacheResponse)); + adapter.callBids(s2sVidRequest, BID_REQUESTS, addBidResponse, done, ajax); + server.requests[0].respond(200, {}, JSON.stringify(cacheResponse)); - sinon.assert.calledOnce(addBidResponse); - const response = addBidResponse.firstCall.args[1]; + sinon.assert.calledOnce(addBidResponse); + const response = addBidResponse.firstCall.args[1]; - expect(response).to.have.property('statusMessage', 'Bid available'); - expect(response).to.have.property('videoCacheKey', 'a5ad3993'); - expect(response).to.have.property('vastUrl', 'https://prebid-cache.net/cache?uuid=a5ad3993'); + expect(response).to.have.property('statusMessage', 'Bid available'); + expect(response).to.have.property('videoCacheKey', 'a5ad3993'); + expect(response).to.have.property('vastUrl', 'https://prebid-cache.net/cache?uuid=a5ad3993'); + } }); it('handles response cache from ext.prebid.targeting with wurl', function () { @@ -2874,15 +3109,18 @@ describe('S2S Adapter', function () { hb_cache_path: '/cache' } }); - const s2sVidRequest = utils.deepClone(VIDEO_REQUEST); - s2sVidRequest.s2sConfig = s2sConfig; - adapter.callBids(s2sVidRequest, BID_REQUESTS, addBidResponse, done, ajax); - server.requests[0].respond(200, {}, JSON.stringify(cacheResponse)); + if (FEATURES.VIDEO) { + const s2sVidRequest = utils.deepClone(VIDEO_REQUEST); + s2sVidRequest.s2sConfig = s2sConfig; - sinon.assert.calledOnce(addBidResponse); - const response = addBidResponse.firstCall.args[1]; - expect(response).to.have.property('pbsBidId', '654321'); + adapter.callBids(s2sVidRequest, BID_REQUESTS, addBidResponse, done, ajax); + server.requests[0].respond(200, {}, JSON.stringify(cacheResponse)); + + sinon.assert.calledOnce(addBidResponse); + const response = addBidResponse.firstCall.args[1]; + expect(response).to.have.property('pbsBidId', '654321'); + } }); it('add request property pbsBidId with ext.prebid.bidid value', function () { @@ -2894,16 +3132,18 @@ describe('S2S Adapter', function () { config.setConfig({ s2sConfig }); const cacheResponse = utils.deepClone(RESPONSE_OPENRTB_VIDEO); - const s2sVidRequest = utils.deepClone(VIDEO_REQUEST); - s2sVidRequest.s2sConfig = s2sConfig; + if (FEATURES.VIDEO) { + const s2sVidRequest = utils.deepClone(VIDEO_REQUEST); + s2sVidRequest.s2sConfig = s2sConfig; - adapter.callBids(s2sVidRequest, BID_REQUESTS, addBidResponse, done, ajax); - server.requests[0].respond(200, {}, JSON.stringify(cacheResponse)); + adapter.callBids(s2sVidRequest, BID_REQUESTS, addBidResponse, done, ajax); + server.requests[0].respond(200, {}, JSON.stringify(cacheResponse)); - sinon.assert.calledOnce(addBidResponse); - const response = addBidResponse.firstCall.args[1]; + sinon.assert.calledOnce(addBidResponse); + const response = addBidResponse.firstCall.args[1]; - expect(response).to.have.property('pbsBidId', '654321'); + expect(response).to.have.property('pbsBidId', '654321'); + } }); if (FEATURES.NATIVE) { @@ -3058,6 +3298,70 @@ describe('S2S Adapter', function () { }); }); }); + describe('when the response contains ext.prebid.fledge', () => { + let fledgeStub, request, bidderRequests; + + function fledgeHook(next, ...args) { + fledgeStub(...args); + } + + before(() => { + addComponentAuction.before(fledgeHook); + }); + + after(() => { + addComponentAuction.getHooks({hook: fledgeHook}).remove(); + }) + + beforeEach(function () { + fledgeStub = sinon.stub(); + config.setConfig({CONFIG}); + request = deepClone(REQUEST); + request.ad_units.forEach(au => deepSetValue(au, 'ortb2Imp.ext.ae', 1)); + bidderRequests = deepClone(BID_REQUESTS); + bidderRequests.forEach(req => req.fledgeEnabled = true); + }); + + const AU = 'div-gpt-ad-1460505748561-0'; + const FLEDGE_RESP = { + ext: { + prebid: { + fledge: { + auctionconfigs: [ + { + impid: AU, + config: { + id: 1 + } + }, + { + impid: AU, + config: { + id: 2 + } + } + ] + } + } + } + } + + it('calls addComponentAuction alongside addBidResponse', function () { + adapter.callBids(request, bidderRequests, addBidResponse, done, ajax); + server.requests[0].respond(200, {}, JSON.stringify(mergeDeep({}, RESPONSE_OPENRTB, FLEDGE_RESP))); + expect(addBidResponse.called).to.be.true; + sinon.assert.calledWith(fledgeStub, AU, {id: 1}); + sinon.assert.calledWith(fledgeStub, AU, {id: 2}); + }); + + it('calls addComponentAuction when there is no bid in the response', () => { + adapter.callBids(request, bidderRequests, addBidResponse, done, ajax); + server.requests[0].respond(200, {}, JSON.stringify(FLEDGE_RESP)); + expect(addBidResponse.called).to.be.false; + sinon.assert.calledWith(fledgeStub, AU, {id: 1}); + sinon.assert.calledWith(fledgeStub, AU, {id: 2}); + }) + }); }); describe('bid won events', function () { diff --git a/test/spec/modules/prebidmanagerAnalyticsAdapter_spec.js b/test/spec/modules/prebidmanagerAnalyticsAdapter_spec.js index 522a78627d7..25834e8574d 100644 --- a/test/spec/modules/prebidmanagerAnalyticsAdapter_spec.js +++ b/test/spec/modules/prebidmanagerAnalyticsAdapter_spec.js @@ -70,7 +70,7 @@ describe('Prebid Manager Analytics Adapter', function () { prebidmanagerAnalytics.flush(); expect(server.requests.length).to.equal(1); - expect(server.requests[0].url).to.equal('https://endpoint.prebidmanager.com/endpoint'); + expect(server.requests[0].url).to.equal('https://endpt.prebidmanager.com/endpoint'); expect(server.requests[0].requestBody.substring(0, 2)).to.equal('1:'); const pmEvents = JSON.parse(server.requests[0].requestBody.substring(2)); diff --git a/test/spec/modules/priceFloors_spec.js b/test/spec/modules/priceFloors_spec.js index b7d771814d0..f232631d73d 100644 --- a/test/spec/modules/priceFloors_spec.js +++ b/test/spec/modules/priceFloors_spec.js @@ -1495,6 +1495,161 @@ describe('the price floors module', function () { }); }); + it('should use inverseFloorAdjustment function before bidder cpm adjustment', function () { + let functionUsed; + getGlobal().bidderSettings = { + rubicon: { + bidCpmAdjustment: function (bidCpm, bidResponse) { + functionUsed = 'Rubicon Adjustment'; + bidCpm *= 0.5; + if (bidResponse.mediaType === 'video') bidCpm -= 0.18; + return bidCpm; + }, + inverseBidAdjustment: function (bidCpm, bidRequest) { + functionUsed = 'Rubicon Inverse'; + // if video is the only mediaType on Bid Request => add 0.18 + if (bidRequest.mediaTypes.video && Object.keys(bidRequest.mediaTypes).length === 1) bidCpm += 0.18; + return bidCpm / 0.5; + }, + }, + appnexus: { + bidCpmAdjustment: function (bidCpm, bidResponse) { + functionUsed = 'Appnexus Adjustment'; + bidCpm *= 0.75; + if (bidResponse.mediaType === 'video') bidCpm -= 0.18; + return bidCpm; + }, + inverseBidAdjustment: function (bidCpm, bidRequest) { + functionUsed = 'Appnexus Inverse'; + // if video is the only mediaType on Bid Request => add 0.18 + if (bidRequest.mediaTypes.video && Object.keys(bidRequest.mediaTypes).length === 1) bidCpm += 0.18; + return bidCpm / 0.75; + }, + } + }; + + _floorDataForAuction[bidRequest.auctionId] = utils.deepClone(basicFloorConfig); + + _floorDataForAuction[bidRequest.auctionId].data.values = { '*': 1.0 }; + + // start with banner as only mediaType + bidRequest.mediaTypes = { banner: { sizes: [[300, 250]] } }; + let appnexusBid = { + ...bidRequest, + bidder: 'appnexus', + }; + + // should be same as the adjusted calculated inverses above test (banner) + expect(bidRequest.getFloor()).to.deep.equal({ + currency: 'USD', + floor: 2.0 + }); + + // should use rubicon inverse + expect(functionUsed).to.equal('Rubicon Inverse'); + + // appnexus just using banner should be same + expect(appnexusBid.getFloor()).to.deep.equal({ + currency: 'USD', + floor: 1.3334 + }); + + expect(functionUsed).to.equal('Appnexus Inverse'); + + // now since asking for 'video' only mediaType inverse function should include the .18 + bidRequest.mediaTypes = { video: { context: 'instream' } }; + expect(bidRequest.getFloor({ mediaType: 'video' })).to.deep.equal({ + currency: 'USD', + floor: 2.36 + }); + + expect(functionUsed).to.equal('Rubicon Inverse'); + + // now since asking for 'video' inverse function should include the .18 + appnexusBid.mediaTypes = { video: { context: 'instream' } }; + expect(appnexusBid.getFloor({ mediaType: 'video' })).to.deep.equal({ + currency: 'USD', + floor: 1.5734 + }); + + expect(functionUsed).to.equal('Appnexus Inverse'); + }); + + it('should pass inverseFloorAdjustment the bidRequest object so it can be used', function () { + // Adjustment factors based on Bid Media Type + const mediaTypeFactors = { + banner: 0.5, + native: 0.7, + video: 0.9 + } + getGlobal().bidderSettings = { + rubicon: { + bidCpmAdjustment: function (bidCpm, bidResponse) { + return bidCpm * mediaTypeFactors[bidResponse.mediaType]; + }, + inverseBidAdjustment: function (bidCpm, bidRequest) { + // For the inverse we add up each mediaType in the request and divide by number of Mt's to get the inverse number + let factor = Object.keys(bidRequest.mediaTypes).reduce((sum, mediaType) => sum += mediaTypeFactors[mediaType], 0); + factor = factor / Object.keys(bidRequest.mediaTypes).length; + return bidCpm / factor; + }, + } + }; + + _floorDataForAuction[bidRequest.auctionId] = utils.deepClone(basicFloorConfig); + + _floorDataForAuction[bidRequest.auctionId].data.values = { '*': 1.0 }; + + // banner only should be 2 + bidRequest.mediaTypes = { banner: {} }; + expect(bidRequest.getFloor()).to.deep.equal({ + currency: 'USD', + floor: 2.0 + }); + + // native only should be 1.4286 + bidRequest.mediaTypes = { native: {} }; + expect(bidRequest.getFloor()).to.deep.equal({ + currency: 'USD', + floor: 1.4286 + }); + + // video only should be 1.1112 + bidRequest.mediaTypes = { video: {} }; + expect(bidRequest.getFloor()).to.deep.equal({ + currency: 'USD', + floor: 1.1112 + }); + + // video and banner should even out to 0.7 factor so 1.4286 + bidRequest.mediaTypes = { video: {}, banner: {} }; + expect(bidRequest.getFloor()).to.deep.equal({ + currency: 'USD', + floor: 1.4286 + }); + + // video and native should even out to 0.8 factor so -- 1.25 + bidRequest.mediaTypes = { video: {}, native: {} }; + expect(bidRequest.getFloor()).to.deep.equal({ + currency: 'USD', + floor: 1.25 + }); + + // banner and native should even out to 0.6 factor so -- 1.6667 + bidRequest.mediaTypes = { banner: {}, native: {} }; + expect(bidRequest.getFloor()).to.deep.equal({ + currency: 'USD', + floor: 1.6667 + }); + + // all 3 banner video and native should even out to 0.7 factor so -- 1.4286 + bidRequest.mediaTypes = { banner: {}, native: {}, video: {} }; + expect(bidRequest.getFloor()).to.deep.equal({ + currency: 'USD', + floor: 1.4286 + }); + }); + it('should use standard cpmAdjustment if no bidder cpmAdjustment', function () { getGlobal().bidderSettings = { rubicon: { diff --git a/test/spec/modules/proxistoreBidAdapter_spec.js b/test/spec/modules/proxistoreBidAdapter_spec.js index 5b9f35c5bcf..bcddb9e8b04 100644 --- a/test/spec/modules/proxistoreBidAdapter_spec.js +++ b/test/spec/modules/proxistoreBidAdapter_spec.js @@ -55,9 +55,9 @@ describe('ProxistoreBidAdapter', function () { }); describe('buildRequests', function () { const url = { - cookieBase: 'https://abs.proxistore.com/v3/rtb/prebid/multi', + cookieBase: 'https://api.proxistore.com/v3/rtb/prebid/multi', cookieLess: - 'https://abs.cookieless-proxistore.com/v3/rtb/prebid/multi', + 'https://api.cookieless-proxistore.com/v3/rtb/prebid/multi', }; let request = spec.buildRequests([bid], bidderRequest); diff --git a/test/spec/modules/publinkIdSystem_spec.js b/test/spec/modules/publinkIdSystem_spec.js index 4656afe1585..7d98b724bd8 100644 --- a/test/spec/modules/publinkIdSystem_spec.js +++ b/test/spec/modules/publinkIdSystem_spec.js @@ -1,11 +1,12 @@ import {publinkIdSubmodule} from 'modules/publinkIdSystem.js'; -import {getStorageManager} from '../../../src/storageManager'; +import {getCoreStorageManager, getStorageManager} from '../../../src/storageManager'; import {server} from 'test/mocks/xhr.js'; import sinon from 'sinon'; import {uspDataHandler} from '../../../src/adapterManager'; import {parseUrl} from '../../../src/utils'; -export const storage = getStorageManager({gvlid: 24}); +const storage = getCoreStorageManager(); + const TEST_COOKIE_VALUE = 'cookievalue'; describe('PublinkIdSystem', () => { describe('decode', () => { diff --git a/test/spec/modules/pubmaticAnalyticsAdapter_spec.js b/test/spec/modules/pubmaticAnalyticsAdapter_spec.js index fa4519c84e1..bd35297b027 100755 --- a/test/spec/modules/pubmaticAnalyticsAdapter_spec.js +++ b/test/spec/modules/pubmaticAnalyticsAdapter_spec.js @@ -1,4 +1,4 @@ -import pubmaticAnalyticsAdapter from 'modules/pubmaticAnalyticsAdapter.js'; +import pubmaticAnalyticsAdapter, { getMetadata } from 'modules/pubmaticAnalyticsAdapter.js'; import adapterManager from 'src/adapterManager.js'; import CONSTANTS from 'src/constants.json'; import { config } from 'src/config.js'; @@ -68,6 +68,14 @@ const BID = { 'hb_size': '640x480', 'hb_source': 'server' }, + 'floorData': { + 'cpmAfterAdjustments': 6.3, + 'enforcements': {'enforceJS': true, 'enforcePBS': false, 'floorDeals': false, 'bidAdjustment': true}, + 'floorCurrency': 'USD', + 'floorRule': 'banner', + 'floorRuleValue': 1.1, + 'floorValue': 1.1 + }, getStatusCode() { return 1; } @@ -200,7 +208,18 @@ const MOCK = { 'bidId': '3bd4ebb1c900e2', 'seatBidId': 'aaaa-bbbb-cccc-dddd', 'bidderRequestId': '1be65d7958826a', - 'auctionId': '25c6d7f5-699a-4bfc-87c9-996f915341fa' + 'auctionId': '25c6d7f5-699a-4bfc-87c9-996f915341fa', + 'floorData': { + 'fetchStatus': 'success', + 'floorMin': undefined, + 'floorProvider': 'pubmatic', + 'location': 'fetch', + 'modelTimestamp': undefined, + 'modelVersion': 'floorModelTest', + 'modelWeight': undefined, + 'skipRate': 0, + 'skipped': false + } } ], 'auctionStart': 1519149536560, @@ -343,10 +362,13 @@ describe('pubmatic analytics adapter', function () { expect(data.orig).to.equal('www.test.com'); expect(data.tst).to.equal(1519767016); expect(data.tgid).to.equal(15); + expect(data.fmv).to.equal('floorModelTest'); + expect(data.ft).to.equal(1); expect(data.s).to.be.an('array'); expect(data.s.length).to.equal(2); // slot 1 expect(data.s[0].sn).to.equal('/19968336/header-bid-tag-0'); + expect(data.s[0].fskp).to.equal(0); expect(data.s[0].sz).to.deep.equal(['640x480']); expect(data.s[0].ps).to.be.an('array'); expect(data.s[0].ps.length).to.equal(1); @@ -370,8 +392,10 @@ describe('pubmatic analytics adapter', function () { expect(data.s[0].ps[0].af).to.equal('video'); expect(data.s[0].ps[0].ocpm).to.equal(1.23); expect(data.s[0].ps[0].ocry).to.equal('USD'); + expect(data.s[0].ps[0].frv).to.equal(undefined); // slot 2 expect(data.s[1].sn).to.equal('/19968336/header-bid-tag-1'); + expect(data.s[1].fskp).to.equal(0); expect(data.s[1].sz).to.deep.equal(['1000x300', '970x250', '728x90']); expect(data.s[1].ps).to.be.an('array'); expect(data.s[1].ps.length).to.equal(1); @@ -397,6 +421,7 @@ describe('pubmatic analytics adapter', function () { expect(data.s[1].ps[0].af).to.equal('banner'); expect(data.s[1].ps[0].ocpm).to.equal(1.52); expect(data.s[1].ps[0].ocry).to.equal('USD'); + expect(data.s[1].ps[0].frv).to.equal(undefined); // tracker slot1 let firstTracker = requests[0].url; @@ -446,11 +471,14 @@ describe('pubmatic analytics adapter', function () { let data = getLoggerJsonFromRequest(request.requestBody); expect(data.pubid).to.equal('9999'); expect(data.pid).to.equal('1111'); + expect(data.fmv).to.equal('floorModelTest'); + expect(data.ft).to.equal(1); expect(data.s).to.be.an('array'); expect(data.s.length).to.equal(2); expect(data.tgid).to.equal(0); // slot 1 expect(data.s[0].sn).to.equal('/19968336/header-bid-tag-0'); + expect(data.s[0].fskp).to.equal(0); expect(data.s[0].sz).to.deep.equal(['640x480']); expect(data.s[0].ps).to.be.an('array'); expect(data.s[0].ps.length).to.equal(1); @@ -464,6 +492,7 @@ describe('pubmatic analytics adapter', function () { expect(data.s[0].ps[0].af).to.equal('video'); expect(data.s[0].ps[0].ocpm).to.equal(1.23); expect(data.s[0].ps[0].ocry).to.equal('USD'); + expect(data.s[1].ps[0].frv).to.equal(undefined); // tracker slot1 let firstTracker = requests[0].url; expect(firstTracker.split('?')[0]).to.equal('https://t.pubmatic.com/wt'); @@ -515,6 +544,8 @@ describe('pubmatic analytics adapter', function () { expect(data.pubid).to.equal('9999'); expect(data.pid).to.equal('1111'); expect(data.tgid).to.equal(0);// test group id should be between 0-15 else set to 0 + expect(data.fmv).to.equal('floorModelTest'); + expect(data.ft).to.equal(1); expect(data.s).to.be.an('array'); expect(data.s.length).to.equal(2); // slot 1 @@ -563,6 +594,7 @@ describe('pubmatic analytics adapter', function () { let data = getLoggerJsonFromRequest(request.requestBody); expect(data.tgid).to.equal(0);// test group id should be an INT between 0-15 else set to 0 expect(data.s[1].sn).to.equal('/19968336/header-bid-tag-1'); + expect(data.s[1].fskp).to.equal(0); expect(data.s[1].sz).to.deep.equal(['1000x300', '970x250', '728x90']); expect(data.s[1].ps).to.be.an('array'); expect(data.s[1].ps.length).to.equal(1); @@ -586,6 +618,7 @@ describe('pubmatic analytics adapter', function () { expect(data.s[1].ps[0].af).to.equal(undefined); expect(data.s[1].ps[0].ocpm).to.equal(0); expect(data.s[1].ps[0].ocry).to.equal('USD'); + expect(data.s[1].ps[0].frv).to.equal(undefined); }); it('Logger: post-timeout check without bid response', function() { @@ -643,6 +676,7 @@ describe('pubmatic analytics adapter', function () { let request = requests[0]; let data = getLoggerJsonFromRequest(request.requestBody); expect(data.s[1].sn).to.equal('/19968336/header-bid-tag-1'); + expect(data.s[1].fskp).to.equal(0); expect(data.s[1].sz).to.deep.equal(['1000x300', '970x250', '728x90']); expect(data.s[1].ps).to.be.an('array'); expect(data.s[1].ps.length).to.equal(1); @@ -667,6 +701,7 @@ describe('pubmatic analytics adapter', function () { expect(data.s[1].ps[0].af).to.equal('banner'); expect(data.s[1].ps[0].ocpm).to.equal(1.52); expect(data.s[1].ps[0].ocry).to.equal('USD'); + expect(data.s[1].ps[0].frv).to.equal(undefined); }); it('Logger: currency conversion check', function() { @@ -748,6 +783,7 @@ describe('pubmatic analytics adapter', function () { 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].fskp).to.equal(0); expect(data.s[1].sz).to.deep.equal(['1000x300', '970x250', '728x90']); expect(data.s[1].ps).to.be.an('array'); expect(data.s[1].ps.length).to.equal(1); @@ -772,6 +808,7 @@ describe('pubmatic analytics adapter', function () { expect(data.s[1].ps[0].af).to.equal('banner'); expect(data.s[1].ps[0].ocpm).to.equal(1.52); expect(data.s[1].ps[0].ocry).to.equal('USD'); + expect(data.s[1].ps[0].frv).to.equal(undefined); expect(data.dvc).to.deep.equal({'plt': 2}); // respective tracker slot let firstTracker = requests[1].url; @@ -829,6 +866,7 @@ describe('pubmatic analytics adapter', function () { expect(data.s[1].ps[0].ocpm).to.equal(1.52); expect(data.s[1].ps[0].ocry).to.equal('USD'); expect(data.dvc).to.deep.equal({'plt': 1}); + expect(data.s[1].ps[0].frv).to.equal(undefined); // respective tracker slot let firstTracker = requests[1].url; expect(firstTracker.split('?')[0]).to.equal('https://t.pubmatic.com/wt'); @@ -856,6 +894,7 @@ describe('pubmatic analytics adapter', function () { 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].fskp).to.equal(0); expect(data.s[1].sz).to.deep.equal(['1000x300', '970x250', '728x90']); expect(data.s[1].ps).to.be.an('array'); expect(data.s[1].ps.length).to.equal(1); @@ -880,6 +919,7 @@ describe('pubmatic analytics adapter', function () { expect(data.s[1].ps[0].af).to.equal('banner'); expect(data.s[1].ps[0].ocpm).to.equal(1.52); expect(data.s[1].ps[0].ocry).to.equal('USD'); + expect(data.s[1].ps[0].frv).to.equal(undefined); // respective tracker slot let firstTracker = requests[1].url; expect(firstTracker.split('?')[0]).to.equal('https://t.pubmatic.com/wt'); @@ -979,11 +1019,14 @@ describe('pubmatic analytics adapter', function () { expect(data.orig).to.equal('www.test.com'); expect(data.tst).to.equal(1519767016); expect(data.tgid).to.equal(15); + expect(data.fmv).to.equal('floorModelTest'); + expect(data.ft).to.equal(1); expect(data.s).to.be.an('array'); expect(data.s.length).to.equal(2); // slot 1 expect(data.s[0].sn).to.equal('/19968336/header-bid-tag-0'); + expect(data.s[0].fskp).to.equal(0); expect(data.s[0].sz).to.deep.equal(['640x480']); expect(data.s[0].ps).to.be.an('array'); expect(data.s[0].ps.length).to.equal(1); @@ -1007,9 +1050,11 @@ describe('pubmatic analytics adapter', function () { expect(data.s[0].ps[0].af).to.equal('video'); expect(data.s[0].ps[0].ocpm).to.equal(1.23); expect(data.s[0].ps[0].ocry).to.equal('USD'); + expect(data.s[0].ps[0].frv).to.equal(1.1); // slot 2 expect(data.s[1].sn).to.equal('/19968336/header-bid-tag-1'); + expect(data.s[1].fskp).to.equal(0); expect(data.s[1].sz).to.deep.equal(['1000x300', '970x250', '728x90']); expect(data.s[1].ps).to.be.an('array'); expect(data.s[1].ps.length).to.equal(1); @@ -1035,6 +1080,7 @@ describe('pubmatic analytics adapter', function () { expect(data.s[1].ps[0].af).to.equal('banner'); expect(data.s[1].ps[0].ocpm).to.equal(1.52); expect(data.s[1].ps[0].ocry).to.equal('USD'); + expect(data.s[1].ps[0].frv).to.equal(undefined); // tracker slot1 let firstTracker = requests[0].url; @@ -1091,11 +1137,14 @@ describe('pubmatic analytics adapter', function () { expect(data.orig).to.equal('www.test.com'); expect(data.tst).to.equal(1519767016); expect(data.tgid).to.equal(15); + expect(data.fmv).to.equal('floorModelTest'); + expect(data.ft).to.equal(1); expect(data.s).to.be.an('array'); expect(data.s.length).to.equal(2); // slot 1 expect(data.s[0].sn).to.equal('/19968336/header-bid-tag-0'); + expect(data.s[0].fskp).to.equal(0); expect(data.s[0].sz).to.deep.equal(['640x480']); expect(data.s[0].ps).to.be.an('array'); expect(data.s[0].ps.length).to.equal(1); @@ -1119,6 +1168,7 @@ describe('pubmatic analytics adapter', function () { expect(data.s[0].ps[0].af).to.equal('video'); expect(data.s[0].ps[0].ocpm).to.equal(1.23); expect(data.s[0].ps[0].ocry).to.equal('USD'); + expect(data.s[0].ps[0].frv).to.equal(1.1); // slot 2 expect(data.s[1].sn).to.equal('/19968336/header-bid-tag-1'); @@ -1169,4 +1219,59 @@ describe('pubmatic analytics adapter', function () { expect(data.piid).to.equal('partnerImpressionID-1'); }); }); + + describe('Get Metadata function', function () { + it('should get the metadata object', function () { + const meta = { + networkId: 'nwid', + advertiserId: 'adid', + networkName: 'nwnm', + primaryCatId: 'pcid', + advertiserName: 'adnm', + agencyId: 'agid', + agencyName: 'agnm', + brandId: 'brid', + brandName: 'brnm', + dchain: 'dc', + demandSource: 'ds', + secondaryCatIds: ['secondaryCatIds'] + }; + const metadataObj = getMetadata(meta); + + expect(metadataObj.nwid).to.equal('nwid'); + expect(metadataObj.adid).to.equal('adid'); + expect(metadataObj.nwnm).to.equal('nwnm'); + expect(metadataObj.pcid).to.equal('pcid'); + expect(metadataObj.adnm).to.equal('adnm'); + expect(metadataObj.agid).to.equal('agid'); + expect(metadataObj.agnm).to.equal('agnm'); + expect(metadataObj.brid).to.equal('brid'); + expect(metadataObj.brnm).to.equal('brnm'); + expect(metadataObj.dc).to.equal('dc'); + expect(metadataObj.ds).to.equal('ds'); + expect(metadataObj.scids).to.be.an('array').with.length.above(0); + expect(metadataObj.scids[0]).to.equal('secondaryCatIds'); + }); + + it('should return undefined if meta is null', function () { + const meta = null; + const metadataObj = getMetadata(meta); + expect(metadataObj).to.equal(undefined); + }); + + it('should return undefined if meta is a empty object', function () { + const meta = {}; + const metadataObj = getMetadata(meta); + expect(metadataObj).to.equal(undefined); + }); + + it('should return undefined if meta object has different properties', function () { + const meta = { + a: 123, + b: 456 + }; + const metadataObj = getMetadata(meta); + expect(metadataObj).to.equal(undefined); + }); + }); }); diff --git a/test/spec/modules/pubmaticBidAdapter_spec.js b/test/spec/modules/pubmaticBidAdapter_spec.js index 6b240cb2d06..66bcf42bb0f 100644 --- a/test/spec/modules/pubmaticBidAdapter_spec.js +++ b/test/spec/modules/pubmaticBidAdapter_spec.js @@ -1,5 +1,5 @@ import { expect } from 'chai'; -import { spec, checkVideoPlacement, _getDomainFromURL, assignDealTier } from 'modules/pubmaticBidAdapter.js'; +import { spec, checkVideoPlacement, _getDomainFromURL, assignDealTier, prepareMetaObject } from 'modules/pubmaticBidAdapter.js'; import * as utils from 'src/utils.js'; import { config } from 'src/config.js'; import { createEidsArray } from 'modules/userId/eids.js'; @@ -194,6 +194,14 @@ describe('PubMatic adapter', function () { image: { required: true, sizes: [300, 250] }, sponsoredBy: { required: true } }, + nativeOrtbRequest: { + ver: '1.2', + assets: [ + { id: 0, required: 1, title: {len: 140} }, + { id: 1, required: 1, img: { type: 3, w: 300, h: 250 } }, + { id: 2, required: 1, data: { type: 1 } } + ] + }, bidder: 'pubmatic', params: { publisherId: '5670', @@ -243,6 +251,22 @@ describe('PubMatic adapter', function () { desc2: {required: true, len: 10, ext: {'desc21': 'desc22'}}, displayurl: {required: true, len: 10, ext: {'displayurl1': 'displayurl2'}} }, + nativeOrtbRequest: { + 'ver': '1.2', + 'assets': [ + {'id': 0, 'required': 1, 'title': {'len': 80}}, + {'id': 1, 'required': 1, 'img': {'type': 1, 'w': 50, 'h': 50}}, + {'id': 2, 'required': 1, 'img': {'type': 3, 'w': 728, 'h': 90}}, + {'id': 3, 'required': 1, 'data': {'type': 1, 'len': 10}}, + {'id': 4, 'required': 1, 'data': {'type': 2, 'len': 10}}, + {'id': 5, 'required': 1, 'data': {'type': 3, 'len': 10}}, + {'id': 6, 'required': 1, 'data': {'type': 4, 'len': 10}}, + {'id': 7, 'required': 1, 'data': {'type': 5, 'len': 10}}, + {'id': 8, 'required': 1, 'data': {'type': 6, 'len': 10}}, + {'id': 9, 'required': 1, 'data': {'type': 8, 'len': 10}}, + {'id': 10, 'required': 1, 'data': {'type': 9, 'len': 10}} + ] + }, bidder: 'pubmatic', params: { publisherId: '5670', @@ -301,6 +325,14 @@ describe('PubMatic adapter', function () { image: { required: false, sizes: [300, 250] }, sponsoredBy: { required: true } }, + nativeOrtbRequest: { + ver: '1.2', + assets: [ + { id: 0, required: 0, title: {len: 140} }, + { id: 1, required: 0, img: {type: 3, w: 300, h: 250} }, + { id: 2, required: 1, data: {type: 1} } + ] + }, bidder: 'pubmatic', params: { publisherId: '5670', @@ -389,6 +421,14 @@ describe('PubMatic adapter', function () { image: { required: true, sizes: [300, 250] }, sponsoredBy: { required: true } }, + nativeOrtbRequest: { + ver: '1.2', + assets: [ + {id: 0, required: 1, title: {len: 140}}, + {id: 1, required: 1, img: {type: 3, w: 300, h: 250}}, + {id: 2, required: 1, data: {type: 1}} + ] + }, bidder: 'pubmatic', params: { publisherId: '301', @@ -442,6 +482,14 @@ describe('PubMatic adapter', function () { image: { required: true, sizes: [300, 250] }, sponsoredBy: { required: true } }, + nativeOrtbRequest: { + ver: '1.2', + assets: [ + { id: 0, required: 1, title: {len: 140} }, + { id: 1, required: 1, img: { type: 3, w: 300, h: 250 } }, + { id: 2, required: 1, data: { type: 1 } } + ] + }, bidder: 'pubmatic', params: { publisherId: '301', @@ -503,6 +551,14 @@ describe('PubMatic adapter', function () { image: { required: true, sizes: [300, 250] }, sponsoredBy: { required: true } }, + nativeOrtbRequest: { + ver: '1.2', + assets: [ + { id: 0, required: 1, title: {len: 80} }, + { id: 1, required: 1, img: { type: 3, w: 300, h: 250 } }, + { id: 2, required: 1, data: { type: 1 } } + ] + }, bidder: 'pubmatic', params: { publisherId: '301', @@ -553,7 +609,8 @@ describe('PubMatic adapter', function () { 'ext': { 'deal_channel': 6, 'advid': 976, - 'dspid': 123 + 'dspid': 123, + 'dchain': 'dchain' } }] }, { @@ -602,7 +659,7 @@ describe('PubMatic adapter', function () { validnativeBidImpression = { 'native': { - 'request': '{"assets":[{"id":1,"required":1,"title":{"len":80}},{"id":2,"required":1,"img":{"type":3,"w":300,"h":250}},{"id":4,"required":1,"data":{"type":1}}]}' + 'request': '{"ver":"1.2","assets":[{"id":0,"required":1,"title":{"len":80}},{"id":1,"required":1,"img":{"type":3,"w":300,"h":250}},{"id":2,"required":1,"data":{"type":1}}]}' } } @@ -614,13 +671,13 @@ describe('PubMatic adapter', function () { validnativeBidImpressionWithRequiredParam = { 'native': { - 'request': '{"assets":[{"id":1,"required":0,"title":{"len":80}},{"id":2,"required":0,"img":{"type":3,"w":300,"h":250}},{"id":4,"required":1,"data":{"type":1}}]}' + 'request': '{"ver":"1.2","assets":[{"id":0,"required":0,"title":{"len":80}},{"id":1,"required":0,"img":{"type":3,"w":300,"h":250}},{"id":2,"required":1,"data":{"type":1}}]}' } } validnativeBidImpressionWithAllParams = { native: { - 'request': '{"assets":[{"id":1,"required":1,"title":{"len":80,"ext":{"title1":"title2"}}},{"id":3,"required":1,"img":{"type":1,"w":50,"h":50}},{"id":2,"required":1,"img":{"type":3,"w":728,"h":90,"mimes":["image/png","image/gif"],"ext":{"image1":"image2"}}},{"id":4,"required":1,"data":{"type":1,"len":10,"ext":{"sponsor1":"sponsor2"}}},{"id":5,"required":1,"data":{"type":2,"len":10,"ext":{"body1":"body2"}}},{"id":13,"required":1,"data":{"type":3,"len":10,"ext":{"rating1":"rating2"}}},{"id":14,"required":1,"data":{"type":4,"len":10,"ext":{"likes1":"likes2"}}},{"id":15,"required":1,"data":{"type":5,"len":10,"ext":{"downloads1":"downloads2"}}},{"id":16,"required":1,"data":{"type":6,"len":10,"ext":{"price1":"price2"}}},{"id":17,"required":1,"data":{"type":7,"len":10,"ext":{"saleprice1":"saleprice2"}}},{"id":18,"required":1,"data":{"type":8,"len":10,"ext":{"phone1":"phone2"}}},{"id":19,"required":1,"data":{"type":9,"len":10,"ext":{"address1":"address2"}}},{"id":20,"required":1,"data":{"type":10,"len":10,"ext":{"desc21":"desc22"}}},{"id":21,"required":1,"data":{"type":11,"len":10,"ext":{"displayurl1":"displayurl2"}}}]}' + 'request': '{"ver":"1.2","assets":[{"id":0,"required":1,"title":{"len":80,"ext":{"title1":"title2"}}},{"id":1,"required":1,"img":{"type":1,"w":50,"h":50,"ext":{"icon1":"icon2"}}},{"id":2,"required":1,"img":{"type":3,"w":728,"h":90,"ext":{"image1":"image2"},"mimes":["image/png","image/gif"]}},{"id":3,"required":1,"data":{"type":1,"len":10,"ext":{"sponsor1":"sponsor2"}}},{"id":4,"required":1,"data":{"type":2,"len":10,"ext":{"body1":"body2"}}},{"id":5,"required":1,"data":{"type":3,"len":10,"ext":{"rating1":"rating2"}}},{"id":6,"required":1,"data":{"type":4,"len":10,"ext":{"likes1":"likes2"}}},{"id":7,"required":1,"data":{"type":5,"len":10,"ext":{"downloads1":"downloads2"}}},{"id":8,"required":1,"data":{"type":6,"len":10,"ext":{"price1":"price2"}}},{"id":9,"required":1,"data":{"type":7,"len":10,"ext":{"saleprice1":"saleprice2"}}},{"id":10,"required":1,"data":{"type":8,"len":10,"ext":{"phone1":"phone2"}}},{"id":11,"required":1,"data":{"type":9,"len":10,"ext":{"address1":"address2"}}},{"id":12,"required":1,"data":{"type":10,"len":10,"ext":{"desc21":"desc22"}}},{"id":13,"required":1,"data":{"type":11,"len":10,"ext":{"displayurl1":"displayurl2"}}}]}' } } @@ -2607,6 +2664,21 @@ describe('PubMatic adapter', function () { expect(data.imp[1]['video']['h']).to.equal(multipleMediaRequests[1].mediaTypes.video.playerSize[1]); }); + it('should pass device.sua if present in bidderRequest fpd ortb2 object', function () { + const suaObject = {'source': 2, 'platform': {'brand': 'macOS', 'version': ['12', '4', '0']}, 'browsers': [{'brand': 'Not_A Brand', 'version': ['99', '0', '0', '0']}, {'brand': 'Google Chrome', 'version': ['109', '0', '5414', '119']}, {'brand': 'Chromium', 'version': ['109', '0', '5414', '119']}], 'mobile': 0, 'model': '', 'bitness': '64', 'architecture': 'x86'}; + let request = spec.buildRequests(multipleMediaRequests, { + auctionId: 'new-auction-id', + ortb2: { + device: { + sua: suaObject + } + } + }); + let data = JSON.parse(request.data); + expect(data.device.sua).to.exist.and.to.be.an('object'); + expect(data.device.sua).to.deep.equal(suaObject); + }); + it('Request params should have valid native bid request for all valid params', function () { let request = spec.buildRequests(nativeBidRequests, { auctionId: 'new-auction-id' @@ -2639,13 +2711,6 @@ describe('PubMatic adapter', function () { expect(data.imp[0]['native']['request']).to.exist.and.to.equal(validnativeBidImpressionWithRequiredParam.native.request); }); - it('should not have valid native request if assets are not defined with minimum required params and only native is the slot', function () { - let request = spec.buildRequests(nativeBidRequestsWithoutAsset, { - auctionId: 'new-auction-id' - }); - expect(request).to.deep.equal(undefined); - }); - it('Request params should have valid native bid request for all native params', function () { let request = spec.buildRequests(nativeBidRequestsWithAllParams, { auctionId: 'new-auction-id' @@ -2848,7 +2913,7 @@ describe('PubMatic adapter', function () { expect(data.native.request).to.exist; }); - it('Request params - banner and native multiformat request - should not have native object incase of invalid config present', function() { + it('Request params - banner and native multiformat request - should have native object incase of invalid config present', function() { bannerAndNativeBidRequests[0].mediaTypes.native = { title: { required: true }, image: { required: true }, @@ -2868,10 +2933,10 @@ describe('PubMatic adapter', function () { data = data.imp[0]; expect(data.banner).to.exist; - expect(data.native).to.not.exist; + expect(data.native).to.exist; }); - it('Request params - video and native multiformat request - should not have native object incase of invalid config present', function() { + it('Request params - video and native multiformat request - should have native object incase of invalid config present', function() { videoAndNativeBidRequests[0].mediaTypes.native = { title: { required: true }, image: { required: true }, @@ -2891,7 +2956,7 @@ describe('PubMatic adapter', function () { data = data.imp[0]; expect(data.video).to.exist; - expect(data.native).to.not.exist; + expect(data.native).to.exist; }); it('should build video impression if video params are present in adunit.mediaTypes instead of bid.params', function() { @@ -3420,7 +3485,8 @@ describe('PubMatic adapter', function () { expect(response[0].ttl).to.equal(300); expect(response[0].meta.networkId).to.equal(123); expect(response[0].adserverTargeting.hb_buyid_pubmatic).to.equal('BUYER-ID-987'); - expect(response[0].meta.buyerId).to.equal(976); + expect(response[0].meta.buyerId).to.equal('seat-id'); + expect(response[0].meta.dchain).to.equal('dchain'); expect(response[0].meta.clickUrl).to.equal('blackrock.com'); expect(response[0].meta.advertiserDomains[0]).to.equal('blackrock.com'); expect(response[0].referrer).to.include(data.site.ref); @@ -3485,16 +3551,17 @@ describe('PubMatic adapter', function () { data.imp[0].id = '2a5571261281d4'; request.data = JSON.stringify(data); let response = spec.interpretResponse(nativeBidResponse, request); + let assets = response[0].native.ortb.assets; expect(response).to.be.an('array').with.length.above(0); expect(response[0].native).to.exist.and.to.be.an('object'); expect(response[0].mediaType).to.exist.and.to.equal('native'); - expect(response[0].native.title).to.exist.and.to.be.an('string'); - expect(response[0].native.image).to.exist.and.to.be.an('object'); - expect(response[0].native.image.url).to.exist.and.to.be.an('string'); - expect(response[0].native.image.height).to.exist; - expect(response[0].native.image.width).to.exist; - expect(response[0].native.sponsoredBy).to.exist.and.to.be.an('string'); - expect(response[0].native.clickUrl).to.exist.and.to.be.an('string'); + expect(assets).to.be.an('array').with.length.above(0); + expect(assets[0].title).to.exist.and.to.be.an('object'); + expect(assets[1].img).to.exist.and.to.be.an('object'); + expect(assets[1].img.url).to.exist.and.to.be.an('string'); + expect(assets[1].img.h).to.exist; + expect(assets[1].img.w).to.exist; + expect(assets[2].data).to.exist.and.to.be.an('object'); }); it('should check for valid banner mediaType in case of multiformat request', function() { @@ -3708,7 +3775,87 @@ describe('PubMatic adapter', function () { }); let newresponse = spec.interpretResponse(newvideoBidResponses, newrequest); expect(newresponse[0].mediaType).to.equal('video') - }) + }); + }); + + describe('Preapare metadata', function () { + it('Should copy all fields from ext to meta', function () { + const bid = { + 'adomain': [ + 'mystartab.com' + ], + cat: ['IAB_CATEGORY'], + ext: { + advid: '12', + 'dspid': 6, + 'deal_channel': 1, + 'bidtype': 0, + advertiserId: 'adid', + // networkName: 'nwnm', + // primaryCatId: 'pcid', + // advertiserName: 'adnm', + // agencyId: 'agid', + // agencyName: 'agnm', + // brandId: 'brid', + // brandName: 'brnm', + // dchain: 'dc', + // demandSource: 'ds', + // secondaryCatIds: ['secondaryCatIds'] + } + }; + + const br = {}; + prepareMetaObject(br, bid, null); + expect(br.meta.networkId).to.equal(6); // dspid + expect(br.meta.buyerId).to.equal('12'); // adid + expect(br.meta.advertiserId).to.equal('12'); + // expect(br.meta.networkName).to.equal('nwnm'); + expect(br.meta.primaryCatId).to.equal('IAB_CATEGORY'); + // expect(br.meta.advertiserName).to.equal('adnm'); + expect(br.meta.agencyId).to.equal('12'); + // expect(br.meta.agencyName).to.equal('agnm'); + expect(br.meta.brandId).to.equal('mystartab.com'); + // expect(br.meta.brandName).to.equal('brnm'); + // expect(br.meta.dchain).to.equal('dc'); + expect(br.meta.demandSource).to.equal(6); + expect(br.meta.secondaryCatIds).to.be.an('array').with.length.above(0); + expect(br.meta.secondaryCatIds[0]).to.equal('IAB_CATEGORY'); + expect(br.meta.advertiserDomains).to.be.an('array').with.length.above(0); // adomain + expect(br.meta.clickUrl).to.equal('mystartab.com'); // adomain + }); + + it('Should be empty, when ext and adomain is absent in bid object', function () { + const bid = {}; + const br = {}; + prepareMetaObject(br, bid, null); + expect(Object.keys(br.meta).length).to.equal(0); + }); + + it('Should be empty, when ext and adomain will not have properties', function () { + const bid = { + 'adomain': [], + ext: {} + }; + const br = {}; + prepareMetaObject(br, bid, null); + expect(Object.keys(br.meta).length).to.equal(0); + expect(br.meta.advertiserDomains).to.equal(undefined); // adomain + expect(br.meta.clickUrl).to.equal(undefined); // adomain + }); + + it('Should have buyerId,advertiserId, agencyId value of site ', function () { + const bid = { + 'adomain': [], + ext: { + advid: '12', + } + }; + const br = {}; + prepareMetaObject(br, bid, '5100'); + expect(br.meta.buyerId).to.equal('5100'); // adid + expect(br.meta.advertiserId).to.equal('5100'); + expect(br.meta.agencyId).to.equal('5100'); + }); }); describe('getUserSyncs', function() { @@ -3808,184 +3955,6 @@ describe('PubMatic adapter', function () { }); }); - describe('JW player segment data for S2S', function() { - let sandbox = sinon.sandbox.create(); - beforeEach(function () { - sandbox = sinon.sandbox.create(); - }); - afterEach(function() { - sandbox.restore(); - }); - it('Should append JW player segment data to dctr values in auction endpoint', function() { - var videoAdUnit = { - 'bidderCode': 'pubmatic', - 'bids': [ - { - 'bidder': 'pubmatic', - 'params': { - 'publisherId': '156276', - 'adSlot': 'pubmatic_video2', - 'dctr': 'key1=123|key2=345', - 'pmzoneid': '1243', - 'video': { - 'mimes': ['video/mp4', 'video/x-flv'], - 'skippable': true, - 'minduration': 5, - 'maxduration': 30, - 'startdelay': 5, - 'playbackmethod': [1, 3], - 'api': [1, 2], - 'protocols': [2, 3], - 'battr': [13, 14], - 'linearity': 1, - 'placement': 2, - 'minbitrate': 10, - 'maxbitrate': 10 - } - }, - 'rtd': { - 'jwplayer': { - 'targeting': { - 'segments': ['80011026', '80011035'], - 'content': { - 'id': 'jw_d9J2zcaA' - } - } - } - }, - 'bid_id': '17a6771be26cc4', - 'ortb2Imp': { - 'ext': { - 'data': { - 'pbadslot': 'abcd', - 'jwTargeting': { - 'playerID': 'myElement1', - 'mediaID': 'd9J2zcaA' - } - } - } - } - } - ], - 'auctionStart': 1630923178417, - 'timeout': 1000, - 'src': 's2s' - } - - spec.transformBidParams(bidRequests[0].params, true, videoAdUnit); - expect(bidRequests[0].params.dctr).to.equal('key1:val1,val2|key2:val1|jw-id=jw_d9J2zcaA|jw-80011026=1|jw-80011035=1'); - }); - it('Should send only JW player segment data in auction endpoint, if dctr is missing', function() { - var videoAdUnit = { - 'bidderCode': 'pubmatic', - 'bids': [ - { - 'bidder': 'pubmatic', - 'params': { - 'publisherId': '156276', - 'adSlot': 'pubmatic_video2', - 'dctr': 'key1=123|key2=345', - 'pmzoneid': '1243', - 'video': { - 'mimes': ['video/mp4', 'video/x-flv'], - 'skippable': true, - 'minduration': 5, - 'maxduration': 30, - 'startdelay': 5, - 'playbackmethod': [1, 3], - 'api': [1, 2], - 'protocols': [2, 3], - 'battr': [13, 14], - 'linearity': 1, - 'placement': 2, - 'minbitrate': 10, - 'maxbitrate': 10 - } - }, - 'rtd': { - 'jwplayer': { - 'targeting': { - 'segments': ['80011026', '80011035'], - 'content': { - 'id': 'jw_d9J2zcaA' - } - } - } - }, - 'bid_id': '17a6771be26cc4', - 'ortb2Imp': { - 'ext': { - 'data': { - 'pbadslot': 'abcd', - 'jwTargeting': { - 'playerID': 'myElement1', - 'mediaID': 'd9J2zcaA' - } - } - } - } - } - ], - 'auctionStart': 1630923178417, - 'timeout': 1000, - 'src': 's2s' - } - - delete bidRequests[0].params.dctr; - spec.transformBidParams(bidRequests[0].params, true, videoAdUnit); - expect(bidRequests[0].params.dctr).to.equal('jw-id=jw_d9J2zcaA|jw-80011026=1|jw-80011035=1'); - }); - - it('Should not send any JW player segment data in auction endpoint, if it is not available', function() { - var videoAdUnit = { - 'bidderCode': 'pubmatic', - 'bids': [ - { - 'bidder': 'pubmatic', - 'params': { - 'publisherId': '156276', - 'adSlot': 'pubmatic_video2', - 'dctr': 'key1=123|key2=345', - 'pmzoneid': '1243', - 'video': { - 'mimes': ['video/mp4', 'video/x-flv'], - 'skippable': true, - 'minduration': 5, - 'maxduration': 30, - 'startdelay': 5, - 'playbackmethod': [1, 3], - 'api': [1, 2], - 'protocols': [2, 3], - 'battr': [13, 14], - 'linearity': 1, - 'placement': 2, - 'minbitrate': 10, - 'maxbitrate': 10 - } - }, - 'bid_id': '17a6771be26cc4', - 'ortb2Imp': { - 'ext': { - 'data': { - 'pbadslot': 'abcd', - 'jwTargeting': { - 'playerID': 'myElement1', - 'mediaID': 'd9J2zcaA' - } - } - } - } - } - ], - 'auctionStart': 1630923178417, - 'timeout': 1000, - 'src': 's2s' - } - spec.transformBidParams(bidRequests[0].params, true, videoAdUnit); - expect(bidRequests[0].params.dctr).to.equal('key1:val1,val2|key2:val1'); - }); - }) - describe('Checking for Video.Placement property', function() { let sandbox, utilsMock; const adUnit = 'Div1'; diff --git a/test/spec/modules/pubwiseBidAdapter_spec.js b/test/spec/modules/pubwiseBidAdapter_spec.js index d7b7a527485..780cc8b8fdb 100644 --- a/test/spec/modules/pubwiseBidAdapter_spec.js +++ b/test/spec/modules/pubwiseBidAdapter_spec.js @@ -2,7 +2,7 @@ import {expect} from 'chai'; import {spec} from 'modules/pubwiseBidAdapter.js'; -import {_checkMediaType} from 'modules/pubwiseBidAdapter.js'; // this is exported only for testing so maintaining the JS convention of _ to indicate the intent +import {_checkVideoPlacement, _checkMediaType} from 'modules/pubwiseBidAdapter.js'; // this is exported only for testing so maintaining the JS convention of _ to indicate the intent import {_parseAdSlot} from 'modules/pubwiseBidAdapter.js'; // this is exported only for testing so maintaining the JS convention of _ to indicate the intent import * as utils from 'src/utils.js'; @@ -486,6 +486,28 @@ const samplePBBidObjects = [ ]; describe('PubWiseAdapter', function () { + describe('Handles Params Properly', function () { + it('properly sets the default endpoint', function () { + const referenceEndpoint = 'https://bid.pubwise.io/prebid'; + let endpointBidRequest = utils.deepClone(sampleValidBidRequests); + // endpointBidRequest.forEach((bidRequest) => { + // bidRequest.params.endpoint_url = newEndpoint; + // }); + let result = spec.buildRequests(endpointBidRequest, {auctionId: 'placeholder'}); + expect(result.url).to.equal(referenceEndpoint); + }); + + it('allows endpoint to be reset', function () { + const newEndpoint = 'http://www.pubwise.io/endpointtest'; + let endpointBidRequest = utils.deepClone(sampleValidBidRequests); + endpointBidRequest.forEach((bidRequest) => { + bidRequest.params.endpoint_url = newEndpoint; + }); + let result = spec.buildRequests(endpointBidRequest, {auctionId: 'placeholder'}); + expect(result.url).to.equal(newEndpoint); + }); + }); + describe('Properly Validates Bids', function () { it('valid bid', function () { let validBid = { @@ -555,14 +577,14 @@ describe('PubWiseAdapter', function () { it('identifies native adm type', function() { let adm = '{"ver":"1.2","assets":[{"title":{"text":"PubWise Test"}},{"img":{"type":3,"url":"http://www.pubwise.io"}},{"img":{"type":1,"url":"http://www.pubwise.io"}},{"data":{"type":2,"value":"PubWise Test Desc"}},{"data":{"type":1,"value":"PubWise.io"}}],"link":{"url":""}}'; let newBid = {mediaType: 'unknown'}; - _checkMediaType(adm, newBid); + _checkMediaType({adm}, newBid); expect(newBid.mediaType).to.equal('native', adm + ' Is a Native adm'); }); it('identifies banner adm type', function() { let adm = '

PubWise Test Bid

'; let newBid = {mediaType: 'unknown'}; - _checkMediaType(adm, newBid); + _checkMediaType({adm}, newBid); expect(newBid.mediaType).to.equal('banner', adm + ' Is a Banner adm'); }); }); @@ -582,4 +604,292 @@ describe('PubWiseAdapter', function () { expect(pbResponse).to.deep.equal(samplePBBidObjects); }); }); + + describe('Video Testing', function () { + /** + * Video Testing + */ + + const videoBidRequests = + [ + { + code: 'video1', + mediaTypes: { + video: { + playerSize: [640, 480], + context: 'instream' + } + }, + bidder: 'pwbid', + bidId: '22bddb28db77d', + adUnitCode: 'Div1', + params: { + siteId: 'xxxxxx', + video: { + mimes: ['video/mp4', 'video/x-flv'], + skippable: true, + minduration: 5, + maxduration: 30, + startdelay: 5, + playbackmethod: [1, 3], + api: [1, 2], + protocols: [2, 3], + battr: [13, 14], + linearity: 1, + placement: 2, + minbitrate: 10, + maxbitrate: 10 + } + } + } + ]; + + let newvideoRequests = [{ + 'bidder': 'pwbid', + 'params': { + 'siteId': 'xxxxx', + 'video': { + 'mimes': ['video/mp4'], + 'skippable': true, + 'protocols': [1, 2, 5], + 'linearity': 1 + } + }, + 'mediaTypes': { + 'video': { + 'playerSize': [ + [640, 480] + ], + 'protocols': [1, 2, 5], + 'context': 'instream', + 'mimes': ['video/flv'], + 'skippable': false, + 'skip': 1, + 'linearity': 2 + } + }, + 'adUnitCode': 'video1', + 'transactionId': '803e3750-0bbe-4ffe-a548-b6eca15087bf', + 'sizes': [ + [640, 480] + ], + 'bidId': '2c95df014cfe97', + 'bidderRequestId': '1fe59391566442', + 'auctionId': '3a4118ef-fb96-4416-b0b0-3cfc1cebc142', + 'src': 'client', + 'bidRequestsCount': 1, + 'bidderRequestsCount': 1, + 'bidderWinsCount': 0 + }]; + + let newvideoBidResponses = { + 'body': { + 'id': '1621441141473', + 'cur': 'USD', + 'customdata': 'openrtb1', + 'ext': { + 'buyid': 'myBuyId' + }, + 'seatbid': [{ + 'bid': [{ + 'id': '2c95df014cfe97', + 'impid': '2c95df014cfe97', + 'price': 4.2, + 'cid': 'test1', + 'crid': 'test2', + 'adm': "Acudeo CompatibleVAST 2.0 Instream Test 1VAST 2.0 Instream Test 1", + 'w': 0, + 'h': 0 + }], + 'ext': { + 'buyid': 'myBuyId' + } + }] + }, + 'headers': {} + }; + + let videoBidResponse = { + 'body': { + 'id': '93D3BAD6-E2E2-49FB-9D89-920B1761C865', + 'seatbid': [{ + 'bid': [{ + 'id': '74858439-49D7-4169-BA5D-44A046315B2F', + 'impid': '22bddb28db77d', + 'price': 1.3, + 'adm': '', + 'h': 250, + 'w': 300, + 'ext': { + 'deal_channel': 6 + } + }] + }] + } + }; + + it('Request params check for video ad', function () { + let request = spec.buildRequests(videoBidRequests, { + auctionId: 'new-auction-id' + }); + let data = request.data; + expect(data.imp[0].video).to.exist; + expect(data.imp[0].tagid).to.equal('Div1'); + expect(data.imp[0]['video']['mimes']).to.exist.and.to.be.an('array'); + expect(data.imp[0]['video']['mimes'][0]).to.equal(videoBidRequests[0].params.video['mimes'][0]); + expect(data.imp[0]['video']['mimes'][1]).to.equal(videoBidRequests[0].params.video['mimes'][1]); + expect(data.imp[0]['video']['minduration']).to.equal(videoBidRequests[0].params.video['minduration']); + expect(data.imp[0]['video']['maxduration']).to.equal(videoBidRequests[0].params.video['maxduration']); + expect(data.imp[0]['video']['startdelay']).to.equal(videoBidRequests[0].params.video['startdelay']); + + expect(data.imp[0]['video']['playbackmethod']).to.exist.and.to.be.an('array'); + expect(data.imp[0]['video']['playbackmethod'][0]).to.equal(videoBidRequests[0].params.video['playbackmethod'][0]); + expect(data.imp[0]['video']['playbackmethod'][1]).to.equal(videoBidRequests[0].params.video['playbackmethod'][1]); + + expect(data.imp[0]['video']['api']).to.exist.and.to.be.an('array'); + expect(data.imp[0]['video']['api'][0]).to.equal(videoBidRequests[0].params.video['api'][0]); + expect(data.imp[0]['video']['api'][1]).to.equal(videoBidRequests[0].params.video['api'][1]); + + expect(data.imp[0]['video']['protocols']).to.exist.and.to.be.an('array'); + expect(data.imp[0]['video']['protocols'][0]).to.equal(videoBidRequests[0].params.video['protocols'][0]); + expect(data.imp[0]['video']['protocols'][1]).to.equal(videoBidRequests[0].params.video['protocols'][1]); + + expect(data.imp[0]['video']['battr']).to.exist.and.to.be.an('array'); + expect(data.imp[0]['video']['battr'][0]).to.equal(videoBidRequests[0].params.video['battr'][0]); + expect(data.imp[0]['video']['battr'][1]).to.equal(videoBidRequests[0].params.video['battr'][1]); + + expect(data.imp[0]['video']['linearity']).to.equal(videoBidRequests[0].params.video['linearity']); + expect(data.imp[0]['video']['placement']).to.equal(videoBidRequests[0].params.video['placement']); + expect(data.imp[0]['video']['minbitrate']).to.equal(videoBidRequests[0].params.video['minbitrate']); + expect(data.imp[0]['video']['maxbitrate']).to.equal(videoBidRequests[0].params.video['maxbitrate']); + + expect(data.imp[0]['video']['w']).to.equal(videoBidRequests[0].mediaTypes.video.playerSize[0]); + expect(data.imp[0]['video']['h']).to.equal(videoBidRequests[0].mediaTypes.video.playerSize[1]); + }); + + it('should assign mediaType even if bid.ext.mediaType does not exists', function() { + let newrequest = spec.buildRequests(newvideoRequests, { + auctionId: 'new-auction-id' + }); + let newresponse = spec.interpretResponse(newvideoBidResponses, newrequest); + expect(newresponse[0].mediaType).to.equal('video'); + }); + + it('should not assign renderer if bid is video and request is for instream', function() { + let request = spec.buildRequests(videoBidRequests, { + auctionId: 'new-auction-id' + }); + let response = spec.interpretResponse(videoBidResponse, request); + expect(response[0].renderer).to.not.exist; + }); + + it('should process instream and outstream', function() { + let validOutstreamRequest = + { + code: 'video1', + mediaTypes: { + video: { + playerSize: [640, 480], + context: 'outstream' + } + }, + bidder: 'pwbid', + bidId: '47acc48ad47af5', + requestId: '0fb4905b-1234-4152-86be-c6f6d259ba99', + bidderRequestId: '1c56ad30b9b8ca8', + transactionId: '92489f71-1bf2-49a0-adf9-000cea934729', + params: { + siteId: 'xxxxx', + adSlot: 'Div1', // ad_id or tagid + video: { + mimes: ['video/mp4', 'video/x-flv'], + skippable: true, + minduration: 5, + maxduration: 30 + } + } + }; + + let outstreamBidRequest = + [ + validOutstreamRequest + ]; + + let validInstreamRequest = { + code: 'video1', + mediaTypes: { + video: { + playerSize: [640, 480], + context: 'instream' + } + }, + bidder: 'pwbid', + bidId: '47acc48ad47af5', + requestId: '0fb4905b-1234-4152-86be-c6f6d259ba99', + bidderRequestId: '1c56ad30b9b8ca8', + transactionId: '92489f71-1bf2-49a0-adf9-000cea934729', + params: { + siteId: 'xxxxx', + adSlot: 'Div1', // ad_id or tagid + video: { + mimes: ['video/mp4', 'video/x-flv'], + skippable: true, + minduration: 5, + maxduration: 30 + } + } + }; + + let instreamBidRequest = + [ + validInstreamRequest + ]; + + let outstreamRequest = spec.isBidRequestValid(validOutstreamRequest); + expect(outstreamRequest).to.equal(false); + + let instreamRequest = spec.isBidRequestValid(validInstreamRequest); + expect(instreamRequest).to.equal(true); + }); + + describe('Checking for Video.Placement property', function() { + let sandbox, utilsMock; + const adUnit = 'DivCheckPlacement'; + const msg_placement_missing = 'PubWise: Video.Placement param missing for DivCheckPlacement'; + let videoData = { + battr: [6, 7], + skipafter: 15, + maxduration: 50, + context: 'instream', + playerSize: [640, 480], + skip: 0, + connectiontype: [1, 2, 6], + skipmin: 10, + minduration: 10, + mimes: ['video/mp4', 'video/x-flv'], + } + beforeEach(() => { + utilsMock = sinon.mock(utils); + sandbox = sinon.sandbox.create(); + sandbox.spy(utils, 'logWarn'); + }); + + afterEach(() => { + utilsMock.restore(); + sandbox.restore(); + }) + + it('should log Video.Placement param missing', function() { + _checkVideoPlacement(videoData, adUnit); + // when failing this gives an odd message about "AssertError: expected logWarn to be called with arguments" it means the specific message expected + sinon.assert.calledWith(utils.logWarn, msg_placement_missing); + }) + it('shoud not log Video.Placement param missing', function() { + videoData['placement'] = 1; + _checkVideoPlacement(videoData, adUnit); + sinon.assert.neverCalledWith(utils.logWarn, msg_placement_missing); + }) + }); + // end video testing + }); }); diff --git a/test/spec/modules/pubxBidAdapter_spec.js b/test/spec/modules/pubxBidAdapter_spec.js index 06bb5b5f638..b387264bf91 100644 --- a/test/spec/modules/pubxBidAdapter_spec.js +++ b/test/spec/modules/pubxBidAdapter_spec.js @@ -39,14 +39,22 @@ describe('pubxAdapter', function () { id: '26c1ee0038ac11', params: { sid: '12345abc' + }, + ortb2: { + site: { + page: `${location.href}?test=1` + } } } ]; const data = { banner: { - sid: '12345abc' - } + sid: '12345abc', + pu: encodeURIComponent( + utils.deepAccess(bidRequests[0], 'ortb2.site.page').replace(/\?.*$/, '') + ), + }, }; it('sends bid request to ENDPOINT via GET', function () { diff --git a/test/spec/modules/pubxaiAnalyticsAdapter_spec.js b/test/spec/modules/pubxaiAnalyticsAdapter_spec.js index 1ad23b9a41e..1dce87e4b8e 100644 --- a/test/spec/modules/pubxaiAnalyticsAdapter_spec.js +++ b/test/spec/modules/pubxaiAnalyticsAdapter_spec.js @@ -567,7 +567,9 @@ describe('pubxai analytics adapter', function() { 'host': location.host, 'path': location.pathname, 'search': location.search, - 'adUnitCount': 1 + 'adUnits': [ + '/19968336/header-bid-tag-1' + ] }, 'floorDetail': { 'fetchStatus': 'success', diff --git a/test/spec/modules/pulsepointBidAdapter_spec.js b/test/spec/modules/pulsepointBidAdapter_spec.js index 825b3abf432..60dca9e6da0 100644 --- a/test/spec/modules/pulsepointBidAdapter_spec.js +++ b/test/spec/modules/pulsepointBidAdapter_spec.js @@ -2,7 +2,6 @@ import {expect} from 'chai'; import {spec} from 'modules/pulsepointBidAdapter.js'; import {deepClone} from 'src/utils.js'; -import { config } from 'src/config.js'; describe('PulsePoint Adapter Tests', function () { const slotConfigs = [{ @@ -225,6 +224,8 @@ describe('PulsePoint Adapter Tests', function () { expect(ortbRequest.imp[1].banner).to.not.equal(null); expect(ortbRequest.imp[1].banner.w).to.equal(728); expect(ortbRequest.imp[1].banner.h).to.equal(90); + // tmax + expect(ortbRequest.tmax).to.equal(500); }); it('Verify parse response', function () { @@ -918,4 +919,13 @@ describe('PulsePoint Adapter Tests', function () { } }); }); + + it('Verify bid request timeouts', function () { + const mkRequest = (bidderRequest) => spec.buildRequests(slotConfigs, bidderRequest).data; + // assert default is used when no bidderRequest.timeout value is available + expect(mkRequest(bidderRequest).tmax).to.equal(500) + + // assert bidderRequest value is used when available + expect(mkRequest(Object.assign({}, { timeout: 6000 }, bidderRequest)).tmax).to.equal(6000) + }); }); diff --git a/test/spec/modules/realTimeDataModule_spec.js b/test/spec/modules/realTimeDataModule_spec.js index ced2f697649..f9c41b2fda0 100644 --- a/test/spec/modules/realTimeDataModule_spec.js +++ b/test/spec/modules/realTimeDataModule_spec.js @@ -5,6 +5,8 @@ import {default as CONSTANTS} from '../../../src/constants.json'; import * as events from '../../../src/events.js'; import 'src/prebid.js'; import {attachRealTimeDataProvider, onDataDeletionRequest} from 'modules/rtdModule/index.js'; +import {GDPR_GVLIDS} from '../../../src/consentHandler.js'; +import {MODULE_TYPE_RTD} from '../../../src/activities/modules.js'; const getBidRequestDataSpy = sinon.spy(); @@ -84,6 +86,26 @@ describe('Real time module', function () { sandbox.restore(); }); + describe('GVL IDs', () => { + beforeEach(() => { + sinon.stub(GDPR_GVLIDS, 'register'); + }); + + afterEach(() => { + GDPR_GVLIDS.register.restore(); + }); + + it('are registered when RTD module is registered', () => { + let mod; + try { + mod = attachRealTimeDataProvider({name: 'mockRtd', gvlid: 123}); + sinon.assert.calledWith(GDPR_GVLIDS.register, MODULE_TYPE_RTD, 'mockRtd', 123); + } finally { + mod && mod(); + } + }) + }) + describe('', () => { const PROVIDERS = [validSM, invalidSM, failureSM, nonConfSM, validSMWait]; let _detachers; diff --git a/test/spec/modules/relaidoBidAdapter_spec.js b/test/spec/modules/relaidoBidAdapter_spec.js index 6a6e79c633d..0f2f9abd583 100644 --- a/test/spec/modules/relaidoBidAdapter_spec.js +++ b/test/spec/modules/relaidoBidAdapter_spec.js @@ -1,13 +1,13 @@ -import { expect } from 'chai'; -import { spec } from 'modules/relaidoBidAdapter.js'; +import {expect} from 'chai'; +import {spec} from 'modules/relaidoBidAdapter.js'; import * as utils from 'src/utils.js'; -import { BANNER, VIDEO } from 'src/mediaTypes.js'; -import { getStorageManager } from '../../../src/storageManager.js'; +import {VIDEO} from 'src/mediaTypes.js'; +import {getCoreStorageManager} from '../../../src/storageManager.js'; const UUID_KEY = 'relaido_uuid'; const relaido_uuid = 'hogehoge'; -const storage = getStorageManager(); +const storage = getCoreStorageManager(); storage.setCookie(UUID_KEY, relaido_uuid); describe('RelaidoAdapter', function () { diff --git a/test/spec/modules/relevadRtdProvider_spec.js b/test/spec/modules/relevadRtdProvider_spec.js new file mode 100644 index 00000000000..678ea26eed6 --- /dev/null +++ b/test/spec/modules/relevadRtdProvider_spec.js @@ -0,0 +1,412 @@ +import { addRtdData, getBidRequestData, relevadSubmodule, serverData } from 'modules/relevadRtdProvider.js'; +import { server } from 'test/mocks/xhr.js'; +import {config} from 'src/config.js'; +import { deepClone, deepAccess, deepSetValue } from '../../../src/utils.js'; + +const responseHeader = {'Content-Type': 'application/json'}; + +const moduleConfigCommon = { + 'dryrun': true, + params: { + setgpt: true, + minscore: 50, + partnerid: 12345, + bidders: [{ bidder: 'appnexus' }, + { bidder: 'rubicon', }, + { bidder: 'smart', }, + { bidder: 'ix', }, + { bidder: 'proxistore', }, + { bidder: 'other' }] + } +}; + +const reqBidsCommon = { + 'timeout': 10000, + 'adUnitCodes': ['/19968336/header-bid-tag-0'], + 'ortb2Fragments': { + 'global': { + 'site': { + 'page': 'http://www.localhost.localdomain:8888/integrationExamples/gpt/relevadRtdProvider_example.html?pbjs_debug=true', + 'domain': 'localhost.localdomain:8888', + 'publisher': { + 'domain': 'localhost.localdomain:8888' + } + }, + 'device': { + 'w': 355, + 'h': 682, + 'dnt': 0, + 'ua': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36', + 'language': 'en' + } + }, + 'bidder': {} + } +}; + +const adUnitsCommon = [ + { + 'code': '/19968336/header-bid-tag-0', + 'mediaTypes': { + 'banner': { 'sizes': [[728, 90]] } + }, + 'bids': [ + { + 'bidder': 'appnexus', + 'params': { 'placementId': '13144370' } + }, + { bidder: 'other' }, + { bidder: 'rubicon', 'params': { id: 1 } }, + { bidder: 'smart', }, + { bidder: 'ix', }, + { bidder: 'proxistore', } + ] + } +]; + +describe('relevadRtdProvider', function() { + describe('relevadSubmodule', function() { + it('successfully instantiates', function () { + expect(relevadSubmodule.init()).to.equal(true); + }); + }); + + describe('Add segments and categories test 1', function() { + it('adds contextual categories and segments', function() { + let moduleConfig = { ...deepClone(moduleConfigCommon) }; + let reqBids = { + ...deepClone(reqBidsCommon), + 'adUnits': deepClone(adUnitsCommon), + }; + + let data = { + segments: ['segment1', 'segment2'], + cats: { 'category3': 100 }, + }; + + (config.getConfig('ix') || {}).firstPartyData = null; + addRtdData(reqBids, data, moduleConfig, () => {}); + expect(reqBids.adUnits[0].bids[0].params.keywords).to.have.deep.property('relevad_rtd', ['segment1', 'segment2', 'category3']); + expect(reqBids.adUnits[0].bids[1].ortb2.site.ext.data).to.have.deep.property('relevad_rtd', ['category3']); + expect(reqBids.adUnits[0].bids[1].ortb2.user.ext.data).to.have.deep.property('relevad_rtd', ['segment1', 'segment2']); + expect(reqBids.adUnits[0].bids[3].params).to.have.deep.property('target', 'relevad_rtd=segment1;relevad_rtd=segment2;relevad_rtd=category3'); + expect(reqBids.adUnits[0].bids[5].ortb2.user.ext.data).to.have.deep.property('segments', ['segment1', 'segment2']); + expect(reqBids.adUnits[0].bids[5].ortb2.user.ext.data).to.have.deep.property('contextual_categories', ['category3']); + expect(reqBids.ortb2Fragments.bidder.rubicon.user.ext.data).to.have.deep.property('relevad_rtd', ['segment1', 'segment2']); + expect(config.getConfig('ix.firstPartyData')).to.have.deep.property('relevad_rtd', ['segment1', 'segment2', 'category3']); + }); + }); + + describe('Add segments and categories test 2 to one bidder out of many', function() { + it('adds contextual categories and segments', function() { + let moduleConfig = { ...deepClone(moduleConfigCommon) }; + let reqBids = { + ...deepClone(reqBidsCommon), + 'adUnits': deepClone(adUnitsCommon), + }; + + let data = { + segments: ['segment1', 'segment2'], + cats: { 'category3': 100 }, + wl: { 'appnexus': { 'placementId': '13144370' } }, + }; + + (config.getConfig('ix') || {}).firstPartyData = null; + addRtdData(reqBids, data, moduleConfig, () => { }); + expect(reqBids.adUnits[0].bids[0].params.keywords).to.have.deep.property('relevad_rtd', ['segment1', 'segment2', 'category3']); + expect(reqBids.adUnits[0].bids[1].ortb2?.site?.ext?.data || {}).to.not.have.property('relevad_rtd'); + expect(reqBids.adUnits[0].bids[1].ortb2?.user?.ext?.data || {}).to.not.have.property('relevad_rtd'); + expect(reqBids.adUnits[0].bids[3].params || {}).to.not.have.deep.property('target', 'relevad_rtd=segment1;relevad_rtd=segment2;relevad_rtd=category3'); + expect(reqBids.adUnits[0].bids[5].ortb2?.user?.ext?.data || {}).to.not.have.deep.property('segments', ['segment1', 'segment2']); + expect(reqBids.adUnits[0].bids[5].ortb2?.user?.ext?.data || {}).to.not.have.deep.property('contextual_categories', ['category3']); + expect(reqBids.adUnits[0].bids[5].ortb2?.user?.ext?.data || {}).to.not.have.deep.property('contextual_categories', {'0': 'category3'}); + expect(reqBids.ortb2Fragments?.bidder?.rubicon?.user?.ext?.data || {}).to.not.have.deep.property('relevad_rtd', ['segment1', 'segment2']); + expect(config.getConfig('ix.firstPartyData') || {}).to.not.have.deep.property('relevad_rtd', ['segment1', 'segment2', 'category3']); + }); + }); + + describe('Add segments and categories test 4', function() { + it('adds contextual categories and segments', function() { + let moduleConfig = { + 'dryrun': true, + params: { + setgpt: true, + minscore: 50, + partnerid: 12345, + } + }; + + let reqBids = { + 'timeout': 10000, + 'adUnits': deepClone(adUnitsCommon), + 'adUnitCodes': [ '/19968336/header-bid-tag-0' ], + 'ortb2Fragments': { + 'global': { + 'site': { + 'page': 'http://www.localhost.localdomain:8888/integrationExamples/gpt/relevadRtdProvider_example.html?pbjs_debug=true', + 'domain': 'localhost.localdomain:8888', + 'publisher': { + 'domain': 'localhost.localdomain:8888' + } + }, + 'device': { + 'w': 355, + 'h': 682, + 'dnt': 0, + 'ua': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36', + 'language': 'en' + } + }, + 'bidder': {} + }, + 'metrics': {}, + 'defer': { 'promise': {} } + } + + let data = { + segments: ['segment1', 'segment2'], + cats: {'category3': 100} + }; + (config.getConfig('ix') || {}).firstPartyData = null; + addRtdData(reqBids, data, moduleConfig, () => {}); + expect(reqBids.adUnits[0].bids[0].params.keywords).to.have.deep.property('relevad_rtd', ['segment1', 'segment2', 'category3']); + }); + }); + + describe('Get Segments And Categories', function() { + it('gets data from async request and adds contextual categories and segments', function() { + const moduleConfig = { + params: { + 'dryrun': true, + sdtgpt: false, + minscore: 50, + bidders: [{ bidder: 'appnexus' }, + { bidder: 'other' }] + } + }; + + let reqBidsConfigObj = { + adUnits: [{ + bids: [{ + bidder: 'appnexus', + params: { + placementId: 13144370 + } + }, { + bidder: 'other' + }] + }] + }; + + let data = { + segments: ['segment1', 'segment2'], + cats: {'category3': 100} + }; + + getBidRequestData(reqBidsConfigObj, () => {}, moduleConfig, {}); + + let request = server.requests[0]; + request.respond(200, responseHeader, JSON.stringify(data)); + + expect(reqBidsConfigObj.adUnits[0].bids[0].params.keywords).to.have.deep.property('relevad_rtd', ['segment1', 'segment2', 'category3']); + expect(reqBidsConfigObj.adUnits[0].bids[1].ortb2.site.ext.data).to.have.deep.property('relevad_rtd', ['category3']); + expect(reqBidsConfigObj.adUnits[0].bids[1].ortb2.user.ext.data).to.have.deep.property('relevad_rtd', ['segment1', 'segment2']); + }); + }); +}); + +describe('Process auction end data', function() { + it('Collects bid data on auction end event', function() { + const auctionEndData = { + 'auctionDetails': { + 'auctionId': 'f7ec9895-5809-475e-8fef-49cbc221921a', + 'auctionStatus': 'completed', + 'adUnits': [ + { + 'code': '/19968336/header-bid-tag-0', + 'mediaTypes': { + 'banner': { 'sizes': [ [ 728, 90 ] ] } + }, + 'bids': [ + { + 'bidder': 'appnexus', + 'params': { + 'placementId': '13144370', + 'keywords': { + 'relevad_rtd': [ 'IAB410-391', 'IAB63-53' ] + } + } + } + ], + 'ortb2Imp': { 'ext': { 'data': { 'relevad_rtd': [ 'IAB410-391', 'IAB63-53' ] }, } }, + 'sizes': [ [ 728, 90 ] ], + } + ], + 'adUnitCodes': [ '/19968336/header-bid-tag-0' ], + 'bidderRequests': [ + { + 'bidderCode': 'appnexus', + 'auctionId': 'f7ec9895-5809-475e-8fef-49cbc221921a', + 'bidderRequestId': '1d917281b2bf6c', + 'bids': [ + { + 'bidder': 'appnexus', + 'params': { + 'placementId': '13144370', + 'keywords': { + 'relevad_rtd': [ + 'IAB410-391', + 'IAB63-53' + ] + } + }, + 'ortb2Imp': { + 'ext': { 'data': { 'relevad_rtd': [ 'IAB410-391', 'IAB63-53' ] }, } + }, + 'mediaTypes': { 'banner': { 'sizes': [ [ 728, 90 ] ] } }, + 'adUnitCode': '/19968336/header-bid-tag-0', + 'sizes': [ [ 728, 90 ] ], + 'bidId': '20f0b347b07f94', + 'bidderRequestId': '1d917281b2bf6c', + 'auctionId': 'f7ec9895-5809-475e-8fef-49cbc221921a', + 'src': 'client', + 'bidRequestsCount': 1, + 'bidderRequestsCount': 1, + 'bidderWinsCount': 0, + 'ortb2': { + 'site': { + 'page': 'http://www.localhost.localdomain:8888/integrationExamples/gpt/relevadRtdProvider_example.html', + 'domain': 'localhost.localdomain:8888', + 'publisher': { 'domain': 'localhost.localdomain:8888' }, + 'cat': [ 'IAB410-391', 'IAB63-53' ], + 'pagecat': [ 'IAB410-391', 'IAB63-53' ], + 'sectioncat': [ 'IAB410-391', 'IAB63-53' ] + }, + 'device': { + 'w': 326, + 'h': 649, + 'dnt': 0, + 'ua': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36', + 'language': 'en' + } + } + } + ], + 'timeout': 10000, + 'refererInfo': { + 'reachedTop': true, + 'isAmp': false, + 'numIframes': 0, + 'stack': [ + 'http://www.localhost.localdomain:8888/integrationExamples/gpt/relevadRtdProvider_example.html' + ], + 'topmostLocation': 'http://www.localhost.localdomain:8888/integrationExamples/gpt/relevadRtdProvider_example.html', + 'location': 'http://www.localhost.localdomain:8888/integrationExamples/gpt/relevadRtdProvider_example.html', + 'canonicalUrl': null, + 'page': 'http://www.localhost.localdomain:8888/integrationExamples/gpt/relevadRtdProvider_example.html', + 'domain': 'www.localhost.localdomain:8888', + 'ref': null, + 'legacy': { + 'reachedTop': true, + 'isAmp': false, + 'numIframes': 0, + 'stack': [ 'http://www.localhost.localdomain:8888/integrationExamples/gpt/relevadRtdProvider_example.html' ], + 'referer': 'http://www.localhost.localdomain:8888/integrationExamples/gpt/relevadRtdProvider_example.html', + 'canonicalUrl': null + } + }, + 'ortb2': { + 'site': { + 'page': 'http://www.localhost.localdomain:8888/integrationExamples/gpt/relevadRtdProvider_example.html', + 'domain': 'localhost.localdomain:8888', + 'publisher': { 'domain': 'localhost.localdomain:8888' }, + 'cat': [ 'IAB410-391', 'IAB63-53' ], + 'pagecat': [ 'IAB410-391', 'IAB63-53' ], + 'sectioncat': [ 'IAB410-391', 'IAB63-53' ] + }, + 'device': { + 'w': 326, + 'h': 649, + 'dnt': 0, + 'ua': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36', + 'language': 'en' + } + }, + 'start': 1674132848498 + } + ], + 'noBids': [], + 'bidsReceived': [ + { + 'bidderCode': 'appnexus', + 'width': 728, + 'height': 90, + 'statusMessage': 'Bid available', + 'adId': '3222e6ead116f3', + 'requestId': '20f0b347b07f94', + 'transactionId': 'df8586ac-6476-4fbf-a727-eda99996dc39', + 'auctionId': 'f7ec9895-5809-475e-8fef-49cbc221921a', + 'mediaType': 'banner', + 'source': 'client', + 'cpm': 1.5, + 'creativeId': 98493734, + 'currency': 'USD', + 'netRevenue': true, + 'ttl': 300, + 'adUnitCode': '/19968336/header-bid-tag-0', + 'adapterCode': 'appnexus', + 'originalCpm': 1.5, + 'originalCurrency': 'USD', + 'responseTimestamp': 1674132848649, + 'requestTimestamp': 1674132848498, + 'bidder': 'appnexus', + 'size': '728x90', + } + ], + }, + 'config': { + 'name': 'RelevadRTDModule', + 'waitForIt': true, + 'dryrun': true, + 'params': { + 'partnerid': 12345, + 'setgpt': true + } + }, + 'userConsent': { 'gdpr': null, 'usp': null, 'gpp': null, 'coppa': false } + }; + + let auctionDetails = auctionEndData['auctionDetails']; + let userConsent = auctionEndData['userConsent']; + let moduleConfig = auctionEndData['config']; + + relevadSubmodule.onAuctionEndEvent(auctionDetails, moduleConfig, userConsent); + expect(serverData.clientdata).to.deep.equal( + { + 'event': 'bids', + 'adunits': [ + { + 'code': '/19968336/header-bid-tag-0', + 'bids': [ + { + 'bidder': 'appnexus', + 'cpm': 1.5, + 'currency': 'USD', + 'type': 'banner', + 'ttr': undefined, + 'dealId': undefined, + 'size': '728x90' + } + ] + } + ], + 'reledata': { segments: ['segment1', 'segment2'], cats: { 'category3': 100 }, }, + 'gdpra': '', + 'gdprc': '', + 'aid': '', + 'cid': '12345', + 'pid': '', + } + ); + }); +}); diff --git a/test/spec/modules/riseBidAdapter_spec.js b/test/spec/modules/riseBidAdapter_spec.js index 4fa4ff354ec..de88c85931d 100644 --- a/test/spec/modules/riseBidAdapter_spec.js +++ b/test/spec/modules/riseBidAdapter_spec.js @@ -527,4 +527,20 @@ describe('riseAdapter', function () { expect(utils.triggerPixel.callCount).to.equal(1) }) }) + + describe('COPPA param', function () { + it('should add COPPA param to payload when prebid config has parameter COPPA equal to true', function () { + config.setConfig({ coppa: true }); + const request = spec.buildRequests(bidRequests, bidderRequest); + const data = JSON.parse(request.data); + expect(data.coppa).to.equal(true); + }); + + it('should not add COPPA param to payload when prebid config has parameter COPPA equal to false', function () { + config.setConfig({ coppa: false }); + const request = spec.buildRequests(bidRequests, bidderRequest); + const data = JSON.parse(request.data); + expect(data.coppa).to.be.undefined; + }); + }) }); diff --git a/test/spec/modules/rtbhouseBidAdapter_spec.js b/test/spec/modules/rtbhouseBidAdapter_spec.js index 6d41df7605b..792d3ab0a9e 100644 --- a/test/spec/modules/rtbhouseBidAdapter_spec.js +++ b/test/spec/modules/rtbhouseBidAdapter_spec.js @@ -53,16 +53,18 @@ describe('RTBHouseAdapter', () => { describe('buildRequests', function () { let bidRequests; - const bidderRequest = { - 'refererInfo': { - 'numIframes': 0, - 'reachedTop': true, - 'referer': 'https://example.com', - 'stack': ['https://example.com'] - } - }; + let bidderRequest; beforeEach(() => { + bidderRequest = { + 'auctionId': 'bidderrequest-auction-id', + 'refererInfo': { + 'numIframes': 0, + 'reachedTop': true, + 'referer': 'https://example.com', + 'stack': ['https://example.com'] + } + }; bidRequests = [ { 'bidder': 'rtbhouse', @@ -82,6 +84,11 @@ describe('RTBHouseAdapter', () => { 'bidderRequestId': '22edbae2733bf6', 'auctionId': '1d1a030790a475', 'transactionId': 'example-transaction-id', + 'ortb2Imp': { + 'ext': { + 'tid': 'ortb2Imp-transaction-id-1' + } + }, 'schain': { 'ver': '1.0', 'complete': 1, @@ -203,7 +210,7 @@ describe('RTBHouseAdapter', () => { const bidRequest = Object.assign([], bidRequests); const request = spec.buildRequests(bidRequest, bidderRequest); const data = JSON.parse(request.data); - expect(data.source.tid).to.equal('example-transaction-id'); + expect(data.source.tid).to.equal('bidderrequest-auction-id'); }); it('should include bidfloor from floor module if avaiable', () => { @@ -256,6 +263,13 @@ describe('RTBHouseAdapter', () => { expect(data.source).to.have.deep.property('tid'); }); + it('should include impression level transaction id when provided', () => { + const bidRequest = Object.assign([], bidRequests); + const request = spec.buildRequests(bidRequest, bidderRequest); + const data = JSON.parse(request.data); + expect(data.imp[0].ext.tid).to.equal('ortb2Imp-transaction-id-1'); + }); + it('should not include invalid schain', () => { const bidRequest = Object.assign([], bidRequests); bidRequest[0].schain = { diff --git a/test/spec/modules/rubiconBidAdapter_spec.js b/test/spec/modules/rubiconBidAdapter_spec.js index e81ef1c805f..e3ebf15619d 100644 --- a/test/spec/modules/rubiconBidAdapter_spec.js +++ b/test/spec/modules/rubiconBidAdapter_spec.js @@ -5,13 +5,22 @@ import { masSizeOrdering, resetUserSync, classifiedAsVideo, - resetRubiConf + resetRubiConf, + converter } from 'modules/rubiconBidAdapter.js'; import {parse as parseQuery} from 'querystring'; import {config} from 'src/config.js'; import * as utils from 'src/utils.js'; import {find} from 'src/polyfill.js'; import {createEidsArray} from 'modules/userId/eids.js'; +import 'modules/schain.js'; +import 'modules/consentManagement.js'; +import 'modules/consentManagementUsp.js'; +import 'modules/userId/index.js'; +import 'modules/priceFloors.js'; +import 'modules/multibid/index.js'; +import adapterManager from 'src/adapterManager.js'; +import {syncAddFPDToBidderRequest} from '../../helpers/fpd.js'; const INTEGRATION = `pbjs_lite_v$prebid.version$`; // $prebid.version$ will be substituted in by gulp in built prebid const PBS_INTEGRATION = 'pbjs'; @@ -102,6 +111,9 @@ describe('the rubicon adapter', function () { referrer: 'localhost', latLong: [40.7607823, '111.8910325'] }, + mediaTypes: { + banner: [[300, 250]] + }, adUnitCode: '/19968336/header-bid-tag-0', code: 'div-1', sizes: [[300, 250], [320, 50]], @@ -307,7 +319,6 @@ describe('the rubicon adapter', function () { accountId: '14062', siteId: '70608', zoneId: '335918', - pchain: 'GAM:11111-reseller1:22222', userId: '12346', keywords: ['a', 'b', 'c'], inventory: { @@ -323,6 +334,9 @@ describe('the rubicon adapter', function () { referrer: 'localhost', latLong: [40.7607823, '111.8910325'] }, + mediaTypes: { + banner: [[300, 250]] + }, adUnitCode: '/19968336/header-bid-tag-0', code: 'div-1', sizes: [[300, 250], [320, 50]], @@ -415,7 +429,6 @@ describe('the rubicon adapter', function () { 'rand': '0.1', 'tk_flint': INTEGRATION, 'x_source.tid': 'd45dd707-a418-42ec-b8a7-b70a6c6fab0b', - 'x_source.pchain': 'GAM:11111-reseller1:22222', 'p_screen_res': /\d+x\d+/, 'tk_user_key': '12346', 'kw': 'a,b,c', @@ -583,20 +596,11 @@ describe('the rubicon adapter', function () { expect(data['p_pos']).to.equal('atf;;btf;;'); }); - it('should not send x_source.pchain to AE if params.pchain is not specified', function () { - var noPchainRequest = utils.deepClone(bidderRequest); - delete noPchainRequest.bids[0].params.pchain; - - let [request] = spec.buildRequests(noPchainRequest.bids, noPchainRequest); - expect(request.data).to.contain('&site_id=70608&'); - expect(request.data).to.not.contain('x_source.pchain'); - }); - it('ad engine query params should be ordered correctly', function () { sandbox.stub(Math, 'random').callsFake(() => 0.1); let [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); - const referenceOrdering = ['account_id', 'site_id', 'zone_id', 'size_id', 'alt_size_ids', 'p_pos', 'rf', 'p_geo.latitude', 'p_geo.longitude', 'kw', 'tg_v.ucat', 'tg_v.lastsearch', 'tg_v.likes', 'tg_i.rating', 'tg_i.prodtype', 'tk_flint', 'x_source.tid', 'l_pb_bid_id', 'x_source.pchain', 'p_screen_res', 'rp_secure', 'tk_user_key', 'tg_fl.eid', 'rp_maxbids', 'slots', 'rand']; + const referenceOrdering = ['account_id', 'site_id', 'zone_id', 'size_id', 'alt_size_ids', 'p_pos', 'rf', 'p_geo.latitude', 'p_geo.longitude', 'kw', 'tg_v.ucat', 'tg_v.lastsearch', 'tg_v.likes', 'tg_i.rating', 'tg_i.prodtype', 'tk_flint', 'x_source.tid', 'l_pb_bid_id', 'p_screen_res', 'rp_secure', 'tk_user_key', 'x_imp.ext.tid', 'tg_fl.eid', 'rp_maxbids', 'slots', 'rand']; request.data.split('&').forEach((item, i) => { expect(item.split('=')[0]).to.equal(referenceOrdering[i]); @@ -615,6 +619,7 @@ describe('the rubicon adapter', function () { 'rand': '0.1', 'tk_flint': INTEGRATION, 'x_source.tid': 'd45dd707-a418-42ec-b8a7-b70a6c6fab0b', + 'x_imp.ext.tid': 'd45dd707-a418-42ec-b8a7-b70a6c6fab0b', 'p_screen_res': /\d+x\d+/, 'tk_user_key': '12346', 'kw': 'a,b,c', @@ -1534,717 +1539,711 @@ describe('the rubicon adapter', function () { }); }); - describe('for video requests', function () { - it('should make a well-formed video request', function () { - createVideoBidderRequest(); - - sandbox.stub(Date, 'now').callsFake(() => - bidderRequest.auctionStart + 100 - ); - - let [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); - let post = request.data; - - expect(post).to.have.property('imp'); - // .with.length.of(1); - let imp = post.imp[0]; - expect(imp.id).to.equal(bidderRequest.bids[0].adUnitCode); - expect(imp.exp).to.equal(undefined); // now undefined - expect(imp.video.w).to.equal(640); - expect(imp.video.h).to.equal(480); - expect(imp.video.pos).to.equal(1); - expect(imp.video.context).to.equal('instream'); - expect(imp.video.minduration).to.equal(15); - expect(imp.video.maxduration).to.equal(30); - expect(imp.video.startdelay).to.equal(0); - expect(imp.video.skip).to.equal(1); - expect(imp.video.skipafter).to.equal(15); - expect(imp.ext.rubicon.video.playerWidth).to.equal(640); - expect(imp.ext.rubicon.video.playerHeight).to.equal(480); - expect(imp.ext.rubicon.video.size_id).to.equal(201); - expect(imp.ext.rubicon.video.language).to.equal('en'); - // Also want it to be in post.site.content.language - expect(post.site.content.language).to.equal('en'); - expect(imp.ext.rubicon.video.skip).to.equal(1); - expect(imp.ext.rubicon.video.skipafter).to.equal(15); - expect(imp.ext.prebid.auctiontimestamp).to.equal(1472239426000); - // should contain version - expect(post.ext.prebid.channel).to.deep.equal({name: 'pbjs', version: 'v$prebid.version$'}); - expect(post.user.ext.consent).to.equal('BOJ/P2HOJ/P2HABABMAAAAAZ+A=='); - // EIDs should exist - expect(post.user.ext).to.have.property('eids').that.is.an('array'); - // LiveIntent should exist - expect(post.user.ext.eids[0].source).to.equal('liveintent.com'); - expect(post.user.ext.eids[0].uids[0].id).to.equal('0000-1111-2222-3333'); - expect(post.user.ext.eids[0].uids[0].atype).to.equal(3); - expect(post.user.ext.eids[0]).to.have.property('ext').that.is.an('object'); - expect(post.user.ext.eids[0].ext).to.have.property('segments').that.is.an('array'); - expect(post.user.ext.eids[0].ext.segments[0]).to.equal('segA'); - expect(post.user.ext.eids[0].ext.segments[1]).to.equal('segB'); - // LiveRamp should exist - expect(post.user.ext.eids[1].source).to.equal('liveramp.com'); - expect(post.user.ext.eids[1].uids[0].id).to.equal('1111-2222-3333-4444'); - expect(post.user.ext.eids[1].uids[0].atype).to.equal(3); - // UnifiedId should exist - expect(post.user.ext.eids[2].source).to.equal('adserver.org'); - expect(post.user.ext.eids[2].uids[0].atype).to.equal(1); - expect(post.user.ext.eids[2].uids[0].id).to.equal('3000'); - // PubCommonId should exist - expect(post.user.ext.eids[3].source).to.equal('pubcid.org'); - expect(post.user.ext.eids[3].uids[0].atype).to.equal(1); - expect(post.user.ext.eids[3].uids[0].id).to.equal('4000'); - // example should exist - expect(post.user.ext.eids[4].source).to.equal('example.com'); - expect(post.user.ext.eids[4].uids[0].id).to.equal('333333'); - // id-partner.com - expect(post.user.ext.eids[5].source).to.equal('id-partner.com'); - expect(post.user.ext.eids[5].uids[0].id).to.equal('4444444'); - // CriteoId should exist - expect(post.user.ext.eids[6].source).to.equal('criteo.com'); - expect(post.user.ext.eids[6].uids[0].id).to.equal('1111'); - expect(post.user.ext.eids[6].uids[0].atype).to.equal(1); - - expect(post.regs.ext.gdpr).to.equal(1); - expect(post.regs.ext.us_privacy).to.equal('1NYN'); - expect(post).to.have.property('ext').that.is.an('object'); - expect(post.ext.prebid.targeting.includewinners).to.equal(true); - expect(post.ext.prebid).to.have.property('cache').that.is.an('object'); - expect(post.ext.prebid.cache).to.have.property('vastxml').that.is.an('object'); - expect(post.ext.prebid.cache.vastxml).to.have.property('returnCreative').that.is.an('boolean'); - expect(post.ext.prebid.cache.vastxml.returnCreative).to.equal(false); - expect(post.ext.prebid.bidders.rubicon.integration).to.equal(PBS_INTEGRATION); - }); - - describe('ortb2imp sent to video bids', function () { - beforeEach(function () { - // initialize - if (bidderRequest.bids[0].hasOwnProperty('ortb2Imp')) { - delete bidderRequest.bids[0].ortb2Imp; - } - }); - - it('should add ortb values to video requests', function () { + if (FEATURES.VIDEO) { + describe('for video requests', function () { + it('should make a well-formed video request', function () { createVideoBidderRequest(); sandbox.stub(Date, 'now').callsFake(() => bidderRequest.auctionStart + 100 ); - bidderRequest.bids[0].ortb2Imp = { - ext: { - gpid: '/test/gpid', - data: { - pbadslot: '/test/pbadslot' - }, - prebid: { - storedauctionresponse: { - id: 'sample_video_response' - } - } - } - } - - let [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); + let [request] = spec.buildRequests(bidderRequest.bids, syncAddFPDToBidderRequest(bidderRequest)); let post = request.data; expect(post).to.have.property('imp'); // .with.length.of(1); let imp = post.imp[0]; - expect(imp.ext.gpid).to.equal('/test/gpid'); - expect(imp.ext.data.pbadslot).to.equal('/test/pbadslot'); - expect(imp.ext.prebid.storedauctionresponse.id).to.equal('sample_video_response'); - }); - }); + expect(imp.id).to.equal(bidderRequest.bids[0].adUnitCode); + expect(imp.exp).to.equal(undefined); // now undefined + expect(imp.video.w).to.equal(640); + expect(imp.video.h).to.equal(480); + expect(imp.video.pos).to.equal(1); + expect(imp.video.minduration).to.equal(15); + expect(imp.video.maxduration).to.equal(30); + expect(imp.video.startdelay).to.equal(0); + expect(imp.video.skip).to.equal(1); + expect(imp.video.skipafter).to.equal(15); + expect(imp.ext.prebid.bidder.rubicon.video.playerWidth).to.equal(640); + expect(imp.ext.prebid.bidder.rubicon.video.playerHeight).to.equal(480); + expect(imp.ext.prebid.bidder.rubicon.video.size_id).to.equal(201); + expect(imp.ext.prebid.bidder.rubicon.video.language).to.equal('en'); + // Also want it to be in post.site.content.language + expect(imp.ext.prebid.bidder.rubicon.video.skip).to.equal(1); + expect(imp.ext.prebid.bidder.rubicon.video.skipafter).to.equal(15); + expect(post.ext.prebid.auctiontimestamp).to.equal(1472239426000); + // should contain version + expect(post.ext.prebid.channel).to.deep.equal({name: 'pbjs', version: $$PREBID_GLOBAL$$.version}); + expect(post.user.ext.consent).to.equal('BOJ/P2HOJ/P2HABABMAAAAAZ+A=='); + // EIDs should exist + expect(post.user.ext).to.have.property('eids').that.is.an('array'); + // LiveIntent should exist + expect(post.user.ext.eids[0].source).to.equal('liveintent.com'); + expect(post.user.ext.eids[0].uids[0].id).to.equal('0000-1111-2222-3333'); + expect(post.user.ext.eids[0].uids[0].atype).to.equal(3); + expect(post.user.ext.eids[0]).to.have.property('ext').that.is.an('object'); + expect(post.user.ext.eids[0].ext).to.have.property('segments').that.is.an('array'); + expect(post.user.ext.eids[0].ext.segments[0]).to.equal('segA'); + expect(post.user.ext.eids[0].ext.segments[1]).to.equal('segB'); + // LiveRamp should exist + expect(post.user.ext.eids[1].source).to.equal('liveramp.com'); + expect(post.user.ext.eids[1].uids[0].id).to.equal('1111-2222-3333-4444'); + expect(post.user.ext.eids[1].uids[0].atype).to.equal(3); + // UnifiedId should exist + expect(post.user.ext.eids[2].source).to.equal('adserver.org'); + expect(post.user.ext.eids[2].uids[0].atype).to.equal(1); + expect(post.user.ext.eids[2].uids[0].id).to.equal('3000'); + // PubCommonId should exist + expect(post.user.ext.eids[3].source).to.equal('pubcid.org'); + expect(post.user.ext.eids[3].uids[0].atype).to.equal(1); + expect(post.user.ext.eids[3].uids[0].id).to.equal('4000'); + // example should exist + expect(post.user.ext.eids[4].source).to.equal('example.com'); + expect(post.user.ext.eids[4].uids[0].id).to.equal('333333'); + // id-partner.com + expect(post.user.ext.eids[5].source).to.equal('id-partner.com'); + expect(post.user.ext.eids[5].uids[0].id).to.equal('4444444'); + // CriteoId should exist + expect(post.user.ext.eids[6].source).to.equal('criteo.com'); + expect(post.user.ext.eids[6].uids[0].id).to.equal('1111'); + expect(post.user.ext.eids[6].uids[0].atype).to.equal(1); + + expect(post.regs.ext.gdpr).to.equal(1); + expect(post.regs.ext.us_privacy).to.equal('1NYN'); + expect(post).to.have.property('ext').that.is.an('object'); + expect(post.ext.prebid.targeting.includewinners).to.equal(true); + expect(post.ext.prebid).to.have.property('cache').that.is.an('object'); + expect(post.ext.prebid.cache).to.have.property('vastxml').that.is.an('object'); + expect(post.ext.prebid.cache.vastxml).to.have.property('returnCreative').that.is.an('boolean'); + expect(post.ext.prebid.cache.vastxml.returnCreative).to.equal(false); + expect(post.ext.prebid.bidders.rubicon.integration).to.equal(PBS_INTEGRATION); + }); + + describe('ortb2imp sent to video bids', function () { + beforeEach(function () { + // initialize + if (bidderRequest.bids[0].hasOwnProperty('ortb2Imp')) { + delete bidderRequest.bids[0].ortb2Imp; + } + }); - it('should correctly set bidfloor on imp when getfloor in scope', function () { - createVideoBidderRequest(); - // default getFloor response is empty object so should not break and not send hard_floor - bidderRequest.bids[0].getFloor = () => getFloorResponse; - sinon.spy(bidderRequest.bids[0], 'getFloor'); + it('should add ortb values to video requests', function () { + createVideoBidderRequest(); - sandbox.stub(Date, 'now').callsFake(() => - bidderRequest.auctionStart + 100 - ); + sandbox.stub(Date, 'now').callsFake(() => + bidderRequest.auctionStart + 100 + ); - let [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); + bidderRequest.bids[0].ortb2Imp = { + ext: { + gpid: '/test/gpid', + data: { + pbadslot: '/test/pbadslot' + }, + prebid: { + storedauctionresponse: { + id: 'sample_video_response' + } + } + } + } - // make sure banner bid called with right stuff - expect( - bidderRequest.bids[0].getFloor.calledWith({ - currency: 'USD', - mediaType: 'video', - size: [640, 480] - }) - ).to.be.true; + let [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); + let post = request.data; - // not an object should work and not send - expect(request.data.imp[0].bidfloor).to.be.undefined; + expect(post).to.have.property('imp'); + // .with.length.of(1); + let imp = post.imp[0]; + expect(imp.ext.gpid).to.equal('/test/gpid'); + expect(imp.ext.data.pbadslot).to.equal('/test/pbadslot'); + expect(imp.ext.prebid.storedauctionresponse.id).to.equal('sample_video_response'); + }); + }); - // make it respond with a non USD floor should not send it - getFloorResponse = {currency: 'EUR', floor: 1.0}; - [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); - expect(request.data.imp[0].bidfloor).to.be.undefined; + it('should correctly set bidfloor on imp when getfloor in scope', function () { + createVideoBidderRequest(); + // default getFloor response is empty object so should not break and not send hard_floor + bidderRequest.bids[0].getFloor = () => getFloorResponse; + sinon.spy(bidderRequest.bids[0], 'getFloor'); - // make it respond with a non USD floor should not send it - getFloorResponse = {currency: 'EUR'}; - [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); - expect(request.data.imp[0].bidfloor).to.be.undefined; + sandbox.stub(Date, 'now').callsFake(() => + bidderRequest.auctionStart + 100 + ); - // make it respond with USD floor and string floor - getFloorResponse = {currency: 'USD', floor: '1.23'}; - [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); - expect(request.data.imp[0].bidfloor).to.equal(1.23); + let [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); - // make it respond with USD floor and num floor - getFloorResponse = {currency: 'USD', floor: 1.23}; - [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); - expect(request.data.imp[0].bidfloor).to.equal(1.23); - }); + // make sure banner bid called with right stuff + expect( + bidderRequest.bids[0].getFloor.calledWith({ + currency: 'USD', + mediaType: '*', + size: '*' + }) + ).to.be.true; - it('should continue with auction and log error if getFloor throws one', function () { - createVideoBidderRequest(); - // default getFloor response is empty object so should not break and not send hard_floor - bidderRequest.bids[0].getFloor = () => { - throw new Error('An exception!'); - }; - sandbox.stub(Date, 'now').callsFake(() => - bidderRequest.auctionStart + 100 - ); + // not an object should work and not send + expect(request.data.imp[0].bidfloor).to.be.undefined; - let [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); + // make it respond with a non USD floor should not send it + getFloorResponse = {currency: 'EUR', floor: 1.0}; + [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); + expect(request.data.imp[0].bidfloor).to.be.undefined; - // log error called - expect(logErrorSpy.calledOnce).to.equal(true); + // make it respond with a non USD floor should not send it + getFloorResponse = {currency: 'EUR'}; + [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); + expect(request.data.imp[0].bidfloor).to.be.undefined; - // should have an imp - expect(request.data.imp).to.exist.and.to.be.a('array'); - expect(request.data.imp).to.have.lengthOf(1); + // make it respond with USD floor and string floor + getFloorResponse = {currency: 'USD', floor: '1.23'}; + [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); + expect(request.data.imp[0].bidfloor).to.equal(1.23); - // should be NO bidFloor - expect(request.data.imp[0].bidfloor).to.be.undefined; - }); + // make it respond with USD floor and num floor + getFloorResponse = {currency: 'USD', floor: 1.23}; + [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); + expect(request.data.imp[0].bidfloor).to.equal(1.23); + }); - it('should add alias name to PBS Request', function () { - createVideoBidderRequest(); + it('should continue with auction if getFloor throws error', function () { + createVideoBidderRequest(); + // default getFloor response is empty object so should not break and not send hard_floor + bidderRequest.bids[0].getFloor = () => { + throw new Error('An exception!'); + }; + sandbox.stub(Date, 'now').callsFake(() => + bidderRequest.auctionStart + 100 + ); - bidderRequest.bidderCode = 'superRubicon'; - bidderRequest.bids[0].bidder = 'superRubicon'; - let [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); + let [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); - // should have the aliases object sent to PBS - expect(request.data.ext.prebid).to.haveOwnProperty('aliases'); - expect(request.data.ext.prebid.aliases).to.deep.equal({superRubicon: 'rubicon'}); + // should have an imp + expect(request.data.imp).to.exist.and.to.be.a('array'); + expect(request.data.imp).to.have.lengthOf(1); - // should have the imp ext bidder params be under the alias name not rubicon superRubicon - expect(request.data.imp[0].ext).to.have.property('superRubicon').that.is.an('object'); - expect(request.data.imp[0].ext).to.not.haveOwnProperty('rubicon'); - }); + // should be NO bidFloor + expect(request.data.imp[0].bidfloor).to.be.undefined; + expect(request.data.imp[0].bidfloorcur).to.be.undefined; + }); - it('should add floors flag correctly to PBS Request', function () { - createVideoBidderRequest(); - let [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); + it('should add alias name to PBS Request', function () { + createVideoBidderRequest(); + adapterManager.aliasRegistry['superRubicon'] = 'rubicon'; + bidderRequest.bidderCode = 'superRubicon'; + bidderRequest.bids[0].bidder = 'superRubicon'; + let [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); - // should not pass if undefined - expect(request.data.ext.prebid.floors).to.be.undefined; + // should have the aliases object sent to PBS + expect(request.data.ext.prebid).to.haveOwnProperty('aliases'); + expect(request.data.ext.prebid.aliases).to.deep.equal({superRubicon: 'rubicon'}); - // should pass it as false - bidderRequest.bids[0].floorData = { - skipped: false, - location: 'fetch', - } - let [newRequest] = spec.buildRequests(bidderRequest.bids, bidderRequest); - expect(newRequest.data.ext.prebid.floors).to.deep.equal({ enabled: false }); - }); + // should have the imp ext bidder params be under the alias name not rubicon superRubicon + expect(request.data.imp[0].ext.prebid.bidder).to.have.property('superRubicon').that.is.an('object'); + expect(request.data.imp[0].ext.prebid.bidder).to.not.haveOwnProperty('rubicon'); + }); - it('should add multibid configuration to PBS Request', function () { - createVideoBidderRequest(); + it('should add floors flag correctly to PBS Request', function () { + createVideoBidderRequest(); + let [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); - const multibid = [{ - bidder: 'bidderA', - maxBids: 2 - }, { - bidder: 'bidderB', - maxBids: 2 - }]; - const expected = [{ - bidder: 'bidderA', - maxbids: 2 - }, { - bidder: 'bidderB', - maxbids: 2 - }]; + // should not pass if undefined + expect(request.data.ext.prebid.floors).to.be.undefined; - config.setConfig({multibid: multibid}); + // should pass it as false + bidderRequest.bids[0].floorData = { + skipped: false, + location: 'fetch', + } + let [newRequest] = spec.buildRequests(bidderRequest.bids, bidderRequest); + expect(newRequest.data.ext.prebid.floors).to.deep.equal({ enabled: false }); + }); - let [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); + it('should add multibid configuration to PBS Request', function () { + createVideoBidderRequest(); - // should have the aliases object sent to PBS - expect(request.data.ext.prebid).to.haveOwnProperty('multibid'); - expect(request.data.ext.prebid.multibid).to.deep.equal(expected); - }); + const multibid = [{ + bidder: 'bidderA', + maxBids: 2 + }, { + bidder: 'bidderB', + maxBids: 2 + }]; + const expected = [{ + bidder: 'bidderA', + maxbids: 2 + }, { + bidder: 'bidderB', + maxbids: 2 + }]; + + config.setConfig({multibid: multibid}); - it('should pass client analytics to PBS endpoint if all modules included', function () { - createVideoBidderRequest(); - $$PREBID_GLOBAL$$.installedModules = []; - let [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); - let payload = request.data; + let [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); - expect(payload.ext.prebid.analytics).to.not.be.undefined; - expect(payload.ext.prebid.analytics).to.deep.equal({'rubicon': {'client-analytics': true}}); - }); + // should have the aliases object sent to PBS + expect(request.data.ext.prebid).to.haveOwnProperty('multibid'); + expect(request.data.ext.prebid.multibid).to.deep.equal(expected); + }); - it('should pass client analytics to PBS endpoint if rubicon analytics adapter is included', function () { - createVideoBidderRequest(); - $$PREBID_GLOBAL$$.installedModules = ['rubiconBidAdapter', 'rubiconAnalyticsAdapter']; - let [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); - let payload = request.data; + it('should pass client analytics to PBS endpoint if all modules included', function () { + createVideoBidderRequest(); + $$PREBID_GLOBAL$$.installedModules = []; + let [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); + let payload = request.data; - expect(payload.ext.prebid.analytics).to.not.be.undefined; - expect(payload.ext.prebid.analytics).to.deep.equal({'rubicon': {'client-analytics': true}}); - }); + expect(payload.ext.prebid.analytics).to.not.be.undefined; + expect(payload.ext.prebid.analytics).to.deep.equal({'rubicon': {'client-analytics': true}}); + }); - it('should not pass client analytics to PBS endpoint if rubicon analytics adapter is not included', function () { - createVideoBidderRequest(); - $$PREBID_GLOBAL$$.installedModules = ['rubiconBidAdapter']; - let [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); - let payload = request.data; + it('should pass client analytics to PBS endpoint if rubicon analytics adapter is included', function () { + createVideoBidderRequest(); + $$PREBID_GLOBAL$$.installedModules = ['rubiconBidAdapter', 'rubiconAnalyticsAdapter']; + let [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); + let payload = request.data; - expect(payload.ext.prebid.analytics).to.be.undefined; - }); + expect(payload.ext.prebid.analytics).to.not.be.undefined; + expect(payload.ext.prebid.analytics).to.deep.equal({'rubicon': {'client-analytics': true}}); + }); - it('should send video exp param correctly when set', function () { - createVideoBidderRequest(); - config.setConfig({s2sConfig: {defaultTtl: 600}}); - let [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); - let post = request.data; + it('should not pass client analytics to PBS endpoint if rubicon analytics adapter is not included', function () { + createVideoBidderRequest(); + $$PREBID_GLOBAL$$.installedModules = ['rubiconBidAdapter']; + let [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); + let payload = request.data; - // should exp set to the right value according to config - let imp = post.imp[0]; - expect(imp.exp).to.equal(600); - }); + expect(payload.ext.prebid.analytics).to.be.undefined; + }); - it('should not send video exp at all if not set in s2sConfig config', function () { - createVideoBidderRequest(); - let [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); - let post = request.data; + it('should send video exp param correctly when set', function () { + createVideoBidderRequest(); + config.setConfig({s2sConfig: {defaultTtl: 600}}); + let [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); + let post = request.data; - // should exp set to the right value according to config - let imp = post.imp[0]; - // bidderFactory stringifies request body before sending so removes undefined attributes: - expect(imp.exp).to.equal(undefined); - }); + // should exp set to the right value according to config + let imp = post.imp[0]; + expect(imp.exp).to.equal(600); + }); - it('should send tmax as the bidderRequest timeout value', function () { - createVideoBidderRequest(); - bidderRequest.timeout = 3333; - let [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); - let post = request.data; - expect(post.tmax).to.equal(3333); - }); + it('should not send video exp at all if not set in s2sConfig config', function () { + createVideoBidderRequest(); + let [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); + let post = request.data; - it('should send correct bidfloor to PBS', function () { - createVideoBidderRequest(); + // should exp set to the right value according to config + let imp = post.imp[0]; + // bidderFactory stringifies request body before sending so removes undefined attributes: + expect(imp.exp).to.equal(undefined); + }); - bidderRequest.bids[0].params.floor = 0.1; - let [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); - expect(request.data.imp[0].bidfloor).to.equal(0.1); + it('should send tmax as the bidderRequest timeout value', function () { + createVideoBidderRequest(); + bidderRequest.timeout = 3333; + let [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); + let post = request.data; + expect(post.tmax).to.equal(3333); + }); - bidderRequest.bids[0].params.floor = 5.5; - [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); - expect(request.data.imp[0].bidfloor).to.equal(5.5); + it('should send correct bidfloor to PBS', function () { + createVideoBidderRequest(); - bidderRequest.bids[0].params.floor = '1.7'; - [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); - expect(request.data.imp[0].bidfloor).to.equal(1.7); + bidderRequest.bids[0].params.floor = 0.1; + let [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); + expect(request.data.imp[0].bidfloor).to.equal(0.1); - bidderRequest.bids[0].params.floor = 0; - [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); - expect(request.data.imp[0].bidfloor).to.equal(0); + bidderRequest.bids[0].params.floor = 5.5; + [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); + expect(request.data.imp[0].bidfloor).to.equal(5.5); - bidderRequest.bids[0].params.floor = undefined; - [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); - expect(request.data.imp[0]).to.not.haveOwnProperty('bidfloor'); + bidderRequest.bids[0].params.floor = '1.7'; + [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); + expect(request.data.imp[0].bidfloor).to.equal(1.7); - bidderRequest.bids[0].params.floor = null; - [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); - expect(request.data.imp[0]).to.not.haveOwnProperty('bidfloor'); - }); + bidderRequest.bids[0].params.floor = 0; + [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); + expect(request.data.imp[0].bidfloor).to.equal(0); - it('should send request with proper ad position', function () { - createVideoBidderRequest(); - let positionBidderRequest = utils.deepClone(bidderRequest); - positionBidderRequest.bids[0].mediaTypes.video.pos = 1; - let [request] = spec.buildRequests(positionBidderRequest.bids, positionBidderRequest); - expect(request.data.imp[0].video.pos).to.equal(1); - - positionBidderRequest = utils.deepClone(bidderRequest); - positionBidderRequest.bids[0].params.position = undefined; - positionBidderRequest.bids[0].mediaTypes.video.pos = undefined; - [request] = spec.buildRequests(positionBidderRequest.bids, positionBidderRequest); - expect(request.data.imp[0].video.pos).to.equal(undefined); - - positionBidderRequest = utils.deepClone(bidderRequest); - positionBidderRequest.bids[0].params.position = 'atf' - positionBidderRequest.bids[0].mediaTypes.video.pos = undefined; - [request] = spec.buildRequests(positionBidderRequest.bids, positionBidderRequest); - expect(request.data.imp[0].video.pos).to.equal(1); - - positionBidderRequest = utils.deepClone(bidderRequest); - positionBidderRequest.bids[0].params.position = 'btf'; - positionBidderRequest.bids[0].mediaTypes.video.pos = undefined; - [request] = spec.buildRequests(positionBidderRequest.bids, positionBidderRequest); - expect(request.data.imp[0].video.pos).to.equal(3); - - positionBidderRequest = utils.deepClone(bidderRequest); - positionBidderRequest.bids[0].params.position = 'foobar'; - positionBidderRequest.bids[0].mediaTypes.video.pos = undefined; - [request] = spec.buildRequests(positionBidderRequest.bids, positionBidderRequest); - expect(request.data.imp[0].video.pos).to.equal(undefined); - }); + bidderRequest.bids[0].params.floor = undefined; + [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); + expect(request.data.imp[0]).to.not.haveOwnProperty('bidfloor'); - it('should properly enforce video.context to be either instream or outstream', function () { - let bid = bidderRequest.bids[0]; - bid.mediaTypes = { - video: { - context: 'instream', - mimes: ['video/mp4', 'video/x-ms-wmv'], - protocols: [2, 5], - maxduration: 30, - linearity: 1, - api: [2] + bidderRequest.bids[0].params.floor = null; + [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); + expect(request.data.imp[0]).to.not.haveOwnProperty('bidfloor'); + }); + + it('should send request with proper ad position', function () { + createVideoBidderRequest(); + let positionBidderRequest = utils.deepClone(bidderRequest); + positionBidderRequest.bids[0].mediaTypes.video.pos = 1; + let [request] = spec.buildRequests(positionBidderRequest.bids, positionBidderRequest); + expect(request.data.imp[0].video.pos).to.equal(1); + + positionBidderRequest = utils.deepClone(bidderRequest); + positionBidderRequest.bids[0].params.position = undefined; + positionBidderRequest.bids[0].mediaTypes.video.pos = undefined; + [request] = spec.buildRequests(positionBidderRequest.bids, positionBidderRequest); + expect(request.data.imp[0].video.pos).to.equal(undefined); + + positionBidderRequest = utils.deepClone(bidderRequest); + positionBidderRequest.bids[0].params.position = 'atf' + positionBidderRequest.bids[0].mediaTypes.video.pos = undefined; + [request] = spec.buildRequests(positionBidderRequest.bids, positionBidderRequest); + expect(request.data.imp[0].video.pos).to.equal(1); + + positionBidderRequest = utils.deepClone(bidderRequest); + positionBidderRequest.bids[0].params.position = 'btf'; + positionBidderRequest.bids[0].mediaTypes.video.pos = undefined; + [request] = spec.buildRequests(positionBidderRequest.bids, positionBidderRequest); + expect(request.data.imp[0].video.pos).to.equal(3); + + positionBidderRequest = utils.deepClone(bidderRequest); + positionBidderRequest.bids[0].params.position = 'foobar'; + positionBidderRequest.bids[0].mediaTypes.video.pos = undefined; + [request] = spec.buildRequests(positionBidderRequest.bids, positionBidderRequest); + expect(request.data.imp[0].video.pos).to.equal(undefined); + }); + + it('should properly enforce video.context to be either instream or outstream', function () { + let bid = bidderRequest.bids[0]; + bid.mediaTypes = { + video: { + context: 'instream', + mimes: ['video/mp4', 'video/x-ms-wmv'], + protocols: [2, 5], + maxduration: 30, + linearity: 1, + api: [2] + } } - } - bid.params.video = {}; + bid.params.video = {}; - sandbox.stub(Date, 'now').callsFake(() => - bidderRequest.auctionStart + 100 - ); + sandbox.stub(Date, 'now').callsFake(() => + bidderRequest.auctionStart + 100 + ); - const bidRequestCopy = utils.deepClone(bidderRequest.bids[0]); - expect(spec.isBidRequestValid(bidRequestCopy)).to.equal(true); + const bidRequestCopy = utils.deepClone(bidderRequest.bids[0]); + expect(spec.isBidRequestValid(bidRequestCopy)).to.equal(true); - // change context to outstream, still true - bidRequestCopy.mediaTypes.video.context = 'outstream'; - expect(spec.isBidRequestValid(bidRequestCopy)).to.equal(true); + // change context to outstream, still true + bidRequestCopy.mediaTypes.video.context = 'outstream'; + expect(spec.isBidRequestValid(bidRequestCopy)).to.equal(true); - // change context to random, false now - bidRequestCopy.mediaTypes.video.context = 'random'; - expect(spec.isBidRequestValid(bidRequestCopy)).to.equal(false); + // change context to random, false now + bidRequestCopy.mediaTypes.video.context = 'random'; + expect(spec.isBidRequestValid(bidRequestCopy)).to.equal(false); - // change context to undefined, still false - bidRequestCopy.mediaTypes.video.context = undefined; - expect(spec.isBidRequestValid(bidRequestCopy)).to.equal(false); + // change context to undefined, still false + bidRequestCopy.mediaTypes.video.context = undefined; + expect(spec.isBidRequestValid(bidRequestCopy)).to.equal(false); - // remove context, still false - delete bidRequestCopy.mediaTypes.video.context; - expect(spec.isBidRequestValid(bidRequestCopy)).to.equal(false); - }); + // remove context, still false + delete bidRequestCopy.mediaTypes.video.context; + expect(spec.isBidRequestValid(bidRequestCopy)).to.equal(false); + }); - it('should enforce the new required mediaTypes.video params', function () { - createVideoBidderRequest(); + it('should enforce the new required mediaTypes.video params', function () { + createVideoBidderRequest(); - sandbox.stub(Date, 'now').callsFake(() => - bidderRequest.auctionStart + 100 - ); + sandbox.stub(Date, 'now').callsFake(() => + bidderRequest.auctionStart + 100 + ); - expect(spec.isBidRequestValid(bidderRequest.bids[0])).to.equal(true); + expect(spec.isBidRequestValid(bidderRequest.bids[0])).to.equal(true); - // change mimes to a non array, no good - createVideoBidderRequest(); - bidderRequest.bids[0].mediaTypes.video.mimes = 'video/mp4'; - expect(spec.isBidRequestValid(bidderRequest.bids[0])).to.equal(false); + // change mimes to a non array, no good + createVideoBidderRequest(); + bidderRequest.bids[0].mediaTypes.video.mimes = 'video/mp4'; + expect(spec.isBidRequestValid(bidderRequest.bids[0])).to.equal(false); - // delete mimes, no good - createVideoBidderRequest(); - delete bidderRequest.bids[0].mediaTypes.video.mimes; - expect(spec.isBidRequestValid(bidderRequest.bids[0])).to.equal(false); + // delete mimes, no good + createVideoBidderRequest(); + delete bidderRequest.bids[0].mediaTypes.video.mimes; + expect(spec.isBidRequestValid(bidderRequest.bids[0])).to.equal(false); - // change protocols to an int not array of ints, no good - createVideoBidderRequest(); - bidderRequest.bids[0].mediaTypes.video.protocols = 1; - expect(spec.isBidRequestValid(bidderRequest.bids[0])).to.equal(false); + // change protocols to an int not array of ints, no good + createVideoBidderRequest(); + bidderRequest.bids[0].mediaTypes.video.protocols = 1; + expect(spec.isBidRequestValid(bidderRequest.bids[0])).to.equal(false); - // delete protocols, no good - createVideoBidderRequest(); - delete bidderRequest.bids[0].mediaTypes.video.protocols; - expect(spec.isBidRequestValid(bidderRequest.bids[0])).to.equal(false); + // delete protocols, no good + createVideoBidderRequest(); + delete bidderRequest.bids[0].mediaTypes.video.protocols; + expect(spec.isBidRequestValid(bidderRequest.bids[0])).to.equal(false); - // change linearity to an string, no good - createVideoBidderRequest(); - bidderRequest.bids[0].mediaTypes.video.linearity = 'string'; - expect(spec.isBidRequestValid(bidderRequest.bids[0])).to.equal(false); + // change linearity to an string, no good + createVideoBidderRequest(); + bidderRequest.bids[0].mediaTypes.video.linearity = 'string'; + expect(spec.isBidRequestValid(bidderRequest.bids[0])).to.equal(false); - // delete linearity, no good - createVideoBidderRequest(); - delete bidderRequest.bids[0].mediaTypes.video.linearity; - expect(spec.isBidRequestValid(bidderRequest.bids[0])).to.equal(false); + // delete linearity, no good + createVideoBidderRequest(); + delete bidderRequest.bids[0].mediaTypes.video.linearity; + expect(spec.isBidRequestValid(bidderRequest.bids[0])).to.equal(false); - // change api to an string, no good - createVideoBidderRequest(); - bidderRequest.bids[0].mediaTypes.video.api = 'string'; - expect(spec.isBidRequestValid(bidderRequest.bids[0])).to.equal(false); + // change api to an string, no good + createVideoBidderRequest(); + bidderRequest.bids[0].mediaTypes.video.api = 'string'; + expect(spec.isBidRequestValid(bidderRequest.bids[0])).to.equal(false); - // delete api, no good - createVideoBidderRequest(); - delete bidderRequest.bids[0].mediaTypes.video.api; - expect(spec.isBidRequestValid(bidderRequest.bids[0])).to.equal(false); - }); + // delete api, no good + createVideoBidderRequest(); + delete bidderRequest.bids[0].mediaTypes.video.api; + expect(spec.isBidRequestValid(bidderRequest.bids[0])).to.equal(false); + }); - it('bid request is valid when video context is outstream', function () { - createVideoBidderRequestOutstream(); - sandbox.stub(Date, 'now').callsFake(() => - bidderRequest.auctionStart + 100 - ); + it('bid request is valid when video context is outstream', function () { + createVideoBidderRequestOutstream(); + sandbox.stub(Date, 'now').callsFake(() => + bidderRequest.auctionStart + 100 + ); - const bidRequestCopy = utils.deepClone(bidderRequest); + const bidRequestCopy = utils.deepClone(bidderRequest); - let [request] = spec.buildRequests(bidRequestCopy.bids, bidRequestCopy); - expect(spec.isBidRequestValid(bidderRequest.bids[0])).to.equal(true); - expect(request.data.imp[0].ext.rubicon.video.size_id).to.equal(203); - }); + let [request] = spec.buildRequests(bidRequestCopy.bids, bidRequestCopy); + expect(spec.isBidRequestValid(bidderRequest.bids[0])).to.equal(true); + expect(request.data.imp[0].ext.prebid.bidder.rubicon.video.size_id).to.equal(203); + }); - it('should send banner request when outstream or instream video included but no rubicon video obect is present', function () { + it('should send banner request when outstream or instream video included but no rubicon video obect is present', function () { // add banner and video mediaTypes - bidderRequest.mediaTypes = { - banner: { - sizes: [[300, 250]] - }, - video: { - context: 'outstream' - } - }; - // no video object in rubicon params, so we should see one call made for banner + bidderRequest.mediaTypes = { + banner: { + sizes: [[300, 250]] + }, + video: { + context: 'outstream' + } + }; + // no video object in rubicon params, so we should see one call made for banner - sandbox.stub(Date, 'now').callsFake(() => - bidderRequest.auctionStart + 100 - ); + sandbox.stub(Date, 'now').callsFake(() => + bidderRequest.auctionStart + 100 + ); - let requests = spec.buildRequests(bidderRequest.bids, bidderRequest); + let requests = spec.buildRequests(bidderRequest.bids, bidderRequest); - expect(requests.length).to.equal(1); - expect(requests[0].url).to.equal('https://fastlane.rubiconproject.com/a/api/fastlane.json'); + expect(requests.length).to.equal(1); + expect(requests[0].url).to.equal('https://fastlane.rubiconproject.com/a/api/fastlane.json'); - bidderRequest.mediaTypes.video.context = 'instream'; + bidderRequest.mediaTypes.video.context = 'instream'; - requests = spec.buildRequests(bidderRequest.bids, bidderRequest); + requests = spec.buildRequests(bidderRequest.bids, bidderRequest); - expect(requests.length).to.equal(1); - expect(requests[0].url).to.equal('https://fastlane.rubiconproject.com/a/api/fastlane.json'); - }); + expect(requests.length).to.equal(1); + expect(requests[0].url).to.equal('https://fastlane.rubiconproject.com/a/api/fastlane.json'); + }); - it('should send request as banner when invalid video bid in multiple mediaType bidRequest', function () { - createVideoBidderRequestNoVideo(); + it('should send request as banner when invalid video bid in multiple mediaType bidRequest', function () { + createVideoBidderRequestNoVideo(); - let bid = bidderRequest.bids[0]; - bid.mediaTypes.banner = { - sizes: [[300, 250]] - }; + let bid = bidderRequest.bids[0]; + bid.mediaTypes.banner = { + sizes: [[300, 250]] + }; - sandbox.stub(Date, 'now').callsFake(() => - bidderRequest.auctionStart + 100 - ); + sandbox.stub(Date, 'now').callsFake(() => + bidderRequest.auctionStart + 100 + ); - const bidRequestCopy = utils.deepClone(bidderRequest); + const bidRequestCopy = utils.deepClone(bidderRequest); - let requests = spec.buildRequests(bidRequestCopy.bids, bidRequestCopy); - expect(requests.length).to.equal(1); - expect(requests[0].url).to.equal('https://fastlane.rubiconproject.com/a/api/fastlane.json'); - }); + let requests = spec.buildRequests(bidRequestCopy.bids, bidRequestCopy); + expect(requests.length).to.equal(1); + expect(requests[0].url).to.equal('https://fastlane.rubiconproject.com/a/api/fastlane.json'); + }); - it('should include coppa flag in video bid request', () => { - createVideoBidderRequest(); + it('should include coppa flag in video bid request', () => { + createVideoBidderRequest(); - sandbox.stub(Date, 'now').callsFake(() => - bidderRequest.auctionStart + 100 - ); + sandbox.stub(Date, 'now').callsFake(() => + bidderRequest.auctionStart + 100 + ); - sandbox.stub(config, 'getConfig').callsFake(key => { - const config = { - 'coppa': true - }; - return config[key]; + sandbox.stub(config, 'getConfig').callsFake(key => { + const config = { + 'coppa': true + }; + return config[key]; + }); + const [request] = spec.buildRequests(bidderRequest.bids, syncAddFPDToBidderRequest(bidderRequest)); + expect(request.data.regs.coppa).to.equal(1); }); - const [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); - expect(request.data.regs.coppa).to.equal(1); - }); - - it('should include first party data', () => { - createVideoBidderRequest(); + it('should include first party data', () => { + createVideoBidderRequest(); - const site = { - ext: { - data: { - page: 'home' - } - }, - content: { + const site = { + ext: { + data: { + page: 'home' + } + }, + content: { + data: [{foo: 'bar'}] + }, + keywords: 'e,f', + rating: '4-star', data: [{foo: 'bar'}] - }, - keywords: 'e,f', - rating: '4-star', - data: [{foo: 'bar'}] - }; - const user = { - ext: { - data: { - age: 31 - } - }, - keywords: 'd', - gender: 'M', - yob: '1984', - geo: {country: 'ca'}, - data: [{foo: 'bar'}] - }; + }; + const user = { + ext: { + data: { + age: 31 + } + }, + keywords: 'd', + gender: 'M', + yob: '1984', + geo: {country: 'ca'}, + data: [{foo: 'bar'}] + }; - const ortb2 = { - site, - user - }; + const ortb2 = { + site, + user + }; - const [request] = spec.buildRequests(bidderRequest.bids.map((b) => ({...b, ortb2})), bidderRequest); + const [request] = spec.buildRequests(bidderRequest.bids.map((b) => ({...b, ortb2})), bidderRequest); - const expected = { - site: Object.assign({}, site, {keywords: bidderRequest.bids[0].params.keywords.join(',')}), - user: Object.assign({}, user), - siteData: Object.assign({}, site.ext.data, bidderRequest.bids[0].params.inventory), - userData: Object.assign({}, user.ext.data, bidderRequest.bids[0].params.visitor), - }; + const expected = { + site: Object.assign({}, site, {keywords: bidderRequest.bids[0].params.keywords.join(',')}), + user: Object.assign({}, user), + siteData: Object.assign({}, site.ext.data, bidderRequest.bids[0].params.inventory), + userData: Object.assign({}, user.ext.data, bidderRequest.bids[0].params.visitor), + }; - delete request.data.site.page; - delete request.data.site.content.language; + delete request.data.site.page; + delete request.data.site.content.language; - expect(request.data.site.keywords).to.deep.equal('a,b,c'); - expect(request.data.user.keywords).to.deep.equal('d'); - expect(request.data.site.ext.data).to.deep.equal(expected.siteData); - expect(request.data.user.ext.data).to.deep.equal(expected.userData); - }); + expect(request.data.site.keywords).to.deep.equal('a,b,c'); + expect(request.data.user.keywords).to.deep.equal('d'); + expect(request.data.site.ext.data).to.deep.equal(expected.siteData); + expect(request.data.user.ext.data).to.deep.equal(expected.userData); + }); - it('should include pbadslot in bid request', function () { - createVideoBidderRequest(); - bidderRequest.bids[0].ortb2Imp = { - ext: { - data: { - pbadslot: '1234567890' + it('should include pbadslot in bid request', function () { + createVideoBidderRequest(); + bidderRequest.bids[0].ortb2Imp = { + ext: { + data: { + pbadslot: '1234567890' + } } } - } - sandbox.stub(Date, 'now').callsFake(() => - bidderRequest.auctionStart + 100 - ); + sandbox.stub(Date, 'now').callsFake(() => + bidderRequest.auctionStart + 100 + ); - const [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); - expect(request.data.imp[0].ext.data.pbadslot).to.equal('1234567890'); - }); + const [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); + expect(request.data.imp[0].ext.data.pbadslot).to.equal('1234567890'); + }); - it('should NOT include storedrequests in pbs payload', function () { - createVideoBidderRequest(); - bidderRequest.bids[0].ortb2 = { - ext: { - prebid: { - storedrequest: 'no-send-top-level-sr' + it('should NOT include storedrequests in pbs payload', function () { + createVideoBidderRequest(); + bidderRequest.bids[0].ortb2 = { + ext: { + prebid: { + storedrequest: 'no-send-top-level-sr' + } } } - } - bidderRequest.bids[0].ortb2Imp = { - ext: { - prebid: { - storedrequest: 'no-send-imp-sr' + bidderRequest.bids[0].ortb2Imp = { + ext: { + prebid: { + storedrequest: 'no-send-imp-sr' + } } } - } - sandbox.stub(Date, 'now').callsFake(() => - bidderRequest.auctionStart + 100 - ); + sandbox.stub(Date, 'now').callsFake(() => + bidderRequest.auctionStart + 100 + ); - const [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); - expect(request.data.ext.prebid.storedrequest).to.be.undefined; - expect(request.data.imp[0].ext.prebid.storedrequest).to.be.undefined; - }); + const [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); + expect(request.data.ext.prebid.storedrequest).to.be.undefined; + expect(request.data.imp[0].ext.prebid.storedrequest).to.be.undefined; + }); - it('should include GAM ad unit in bid request', function () { - createVideoBidderRequest(); - bidderRequest.bids[0].ortb2Imp = { - ext: { - data: { - adserver: { - adslot: '1234567890', - name: 'adServerName1' + it('should include GAM ad unit in bid request', function () { + createVideoBidderRequest(); + bidderRequest.bids[0].ortb2Imp = { + ext: { + data: { + adserver: { + adslot: '1234567890', + name: 'adServerName1' + } } } - } - }; + }; - sandbox.stub(Date, 'now').callsFake(() => - bidderRequest.auctionStart + 100 - ); + sandbox.stub(Date, 'now').callsFake(() => + bidderRequest.auctionStart + 100 + ); - const [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); - expect(request.data.imp[0].ext.data.adserver.adslot).to.equal('1234567890'); - expect(request.data.imp[0].ext.data.adserver.name).to.equal('adServerName1'); - }); + const [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); + expect(request.data.imp[0].ext.data.adserver.adslot).to.equal('1234567890'); + expect(request.data.imp[0].ext.data.adserver.name).to.equal('adServerName1'); + }); - it('should use the integration type provided in the config instead of the default', () => { - createVideoBidderRequest(); - config.setConfig({rubicon: {int_type: 'testType'}}); - const [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); - expect(request.data.ext.prebid.bidders.rubicon.integration).to.equal('testType'); - }); + it('should use the integration type provided in the config instead of the default', () => { + createVideoBidderRequest(); + config.setConfig({rubicon: {int_type: 'testType'}}); + const [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); + expect(request.data.ext.prebid.bidders.rubicon.integration).to.equal('testType'); + }); - it('should pass the user.id provided in the config', function () { - config.setConfig({user: {id: '123'}}); - createVideoBidderRequest(); + it('should pass the user.id provided in the config', function () { + config.setConfig({user: {id: '123'}}); + createVideoBidderRequest(); + + sandbox.stub(Date, 'now').callsFake(() => + bidderRequest.auctionStart + 100 + ); - sandbox.stub(Date, 'now').callsFake(() => - bidderRequest.auctionStart + 100 - ); + let [request] = spec.buildRequests(bidderRequest.bids, syncAddFPDToBidderRequest(bidderRequest)); + let post = request.data; - let [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); - let post = request.data; - - expect(post).to.have.property('imp') - // .with.length.of(1); - let imp = post.imp[0]; - expect(imp.id).to.equal(bidderRequest.bids[0].adUnitCode); - expect(imp.exp).to.equal(undefined); - expect(imp.video.w).to.equal(640); - expect(imp.video.h).to.equal(480); - expect(imp.video.pos).to.equal(1); - expect(imp.video.context).to.equal('instream'); - expect(imp.video.minduration).to.equal(15); - expect(imp.video.maxduration).to.equal(30); - expect(imp.video.startdelay).to.equal(0); - expect(imp.video.skip).to.equal(1); - expect(imp.video.skipafter).to.equal(15); - expect(imp.ext.rubicon.video.playerWidth).to.equal(640); - expect(imp.ext.rubicon.video.playerHeight).to.equal(480); - expect(imp.ext.rubicon.video.size_id).to.equal(201); - expect(imp.ext.rubicon.video.language).to.equal('en'); - // Also want it to be in post.site.content.language - expect(post.site.content.language).to.equal('en'); - expect(imp.ext.rubicon.video.skip).to.equal(1); - expect(imp.ext.rubicon.video.skipafter).to.equal(15); - expect(imp.ext.prebid.auctiontimestamp).to.equal(1472239426000); - expect(post.user.ext.consent).to.equal('BOJ/P2HOJ/P2HABABMAAAAAZ+A=='); - - // Config user.id - expect(post.user.id).to.equal('123'); - - expect(post.regs.ext.gdpr).to.equal(1); - expect(post.regs.ext.us_privacy).to.equal('1NYN'); - expect(post).to.have.property('ext').that.is.an('object'); - expect(post.ext.prebid.targeting.includewinners).to.equal(true); - expect(post.ext.prebid).to.have.property('cache').that.is.an('object'); - expect(post.ext.prebid.cache).to.have.property('vastxml').that.is.an('object'); - expect(post.ext.prebid.cache.vastxml).to.have.property('returnCreative').that.is.an('boolean'); - expect(post.ext.prebid.cache.vastxml.returnCreative).to.equal(false); - expect(post.ext.prebid.bidders.rubicon.integration).to.equal(PBS_INTEGRATION); - }) - }); + expect(post).to.have.property('imp') + // .with.length.of(1); + let imp = post.imp[0]; + expect(imp.id).to.equal(bidderRequest.bids[0].adUnitCode); + expect(imp.exp).to.equal(undefined); + expect(imp.video.w).to.equal(640); + expect(imp.video.h).to.equal(480); + expect(imp.video.pos).to.equal(1); + expect(imp.video.minduration).to.equal(15); + expect(imp.video.maxduration).to.equal(30); + expect(imp.video.startdelay).to.equal(0); + expect(imp.video.skip).to.equal(1); + expect(imp.video.skipafter).to.equal(15); + expect(imp.ext.prebid.bidder.rubicon.video.playerWidth).to.equal(640); + expect(imp.ext.prebid.bidder.rubicon.video.playerHeight).to.equal(480); + expect(imp.ext.prebid.bidder.rubicon.video.language).to.equal('en'); + + // Also want it to be in post.site.content.language + expect(post.site.content.language).to.equal('en'); + expect(post.ext.prebid.auctiontimestamp).to.equal(1472239426000); + expect(post.user.ext.consent).to.equal('BOJ/P2HOJ/P2HABABMAAAAAZ+A=='); + + // Config user.id + expect(post.user.id).to.equal('123'); + + expect(post.regs.ext.gdpr).to.equal(1); + expect(post.regs.ext.us_privacy).to.equal('1NYN'); + expect(post).to.have.property('ext').that.is.an('object'); + expect(post.ext.prebid.targeting.includewinners).to.equal(true); + expect(post.ext.prebid).to.have.property('cache').that.is.an('object'); + expect(post.ext.prebid.cache).to.have.property('vastxml').that.is.an('object'); + expect(post.ext.prebid.cache.vastxml).to.have.property('returnCreative').that.is.an('boolean'); + expect(post.ext.prebid.cache.vastxml.returnCreative).to.equal(false); + expect(post.ext.prebid.bidders.rubicon.integration).to.equal(PBS_INTEGRATION); + }) + }); + } describe('combineSlotUrlParams', function () { it('should combine an array of slot url params', function () { @@ -2366,6 +2365,110 @@ describe('the rubicon adapter', function () { expect(bid.params.video).to.not.be.undefined; }); }); + + if (FEATURES.NATIVE) { + describe('when there is a native request', function () { + describe('and bidonmultiformat = undefined (false)', () => { + it('should send only one native bid to PBS endpoint', function () { + const bidReq = addNativeToBidRequest(bidderRequest); + bidReq.bids[0].params = { + video: {} + } + let [request] = spec.buildRequests(bidReq.bids, bidReq); + expect(request.method).to.equal('POST'); + expect(request.url).to.equal('https://prebid-server.rubiconproject.com/openrtb2/auction'); + expect(request.data.imp).to.have.nested.property('[0].native'); + }); + + describe('that contains also a banner mediaType', function () { + it('should send the banner to fastlane BUT NOT the native bid because missing params.video', function() { + const bidReq = addNativeToBidRequest(bidderRequest); + bidReq.bids[0].mediaTypes.banner = { + sizes: [[300, 250]] + } + let [request] = spec.buildRequests(bidReq.bids, bidReq); + expect(request.method).to.equal('GET'); + expect(request.url).to.include('https://fastlane.rubiconproject.com/a/api/fastlane.json'); + }); + }); + describe('with another banner request', () => { + it('should send the native bid to PBS and the banner to fastlane', function() { + const bidReq = addNativeToBidRequest(bidderRequest); + bidReq.bids[0].params = { video: {} }; + // add second bidRqeuest + bidReq.bids.push({ + mediaTypes: { + banner: { + sizes: [[300, 250]] + } + }, + params: bidReq.bids[0].params + }) + let [request1, request2] = spec.buildRequests(bidReq.bids, bidReq); + expect(request1.method).to.equal('POST'); + expect(request1.url).to.equal('https://prebid-server.rubiconproject.com/openrtb2/auction'); + expect(request1.data.imp).to.have.nested.property('[0].native'); + expect(request2.method).to.equal('GET'); + expect(request2.url).to.include('https://fastlane.rubiconproject.com/a/api/fastlane.json'); + }); + }); + }); + + describe('with bidonmultiformat === true', () => { + it('should send two requests, to PBS with 2 imps', () => { + const bidReq = addNativeToBidRequest(bidderRequest); + // add second mediaType + bidReq.bids[0].mediaTypes = { + ...bidReq.bids[0].mediaTypes, + banner: { + sizes: [[300, 250]] + } + }; + bidReq.bids[0].params.bidonmultiformat = true; + let [pbsRequest, fastlanteRequest] = spec.buildRequests(bidReq.bids, bidReq); + expect(pbsRequest.method).to.equal('POST'); + expect(pbsRequest.url).to.equal('https://prebid-server.rubiconproject.com/openrtb2/auction'); + expect(pbsRequest.data.imp).to.have.nested.property('[0].native'); + expect(fastlanteRequest.url).to.equal('https://fastlane.rubiconproject.com/a/api/fastlane.json'); + }); + }); + describe('with bidonmultiformat === false', () => { + it('should send only banner request because there\'s no params.video', () => { + const bidReq = addNativeToBidRequest(bidderRequest); + // add second mediaType + bidReq.bids[0].mediaTypes = { + ...bidReq.bids[0].mediaTypes, + banner: { + sizes: [[300, 250]] + } + }; + + let [fastlanteRequest, ...others] = spec.buildRequests(bidReq.bids, bidReq); + expect(fastlanteRequest.url).to.equal('https://fastlane.rubiconproject.com/a/api/fastlane.json'); + expect(others).to.be.empty; + }); + + it('should not send native to PBS even if there\'s param.video', () => { + const bidReq = addNativeToBidRequest(bidderRequest); + // add second mediaType + bidReq.bids[0].mediaTypes = { + ...bidReq.bids[0].mediaTypes, + banner: { + sizes: [[300, 250]] + } + }; + // by adding this, when bidonmultiformat is false, the native request will be sent to pbs + bidReq.bids[0].params = { + video: {} + } + let [fastlaneRequest, ...other] = spec.buildRequests(bidReq.bids, bidReq); + expect(fastlaneRequest.method).to.equal('GET'); + expect(fastlaneRequest.url).to.equal('https://fastlane.rubiconproject.com/a/api/fastlane.json'); + expect(other).to.be.empty; + }); + }); + }); + } }); describe('interpretResponse', function () { @@ -3108,211 +3211,227 @@ describe('the rubicon adapter', function () { }); }); - describe('for video', function () { - beforeEach(function () { - createVideoBidderRequest(); - }); + if (FEATURES.VIDEO) { + describe('for video', function () { + beforeEach(function () { + createVideoBidderRequest(); + }); - it('should register a successful bid', function () { - let response = { - cur: 'USD', - seatbid: [{ - bid: [{ - id: '0', - impid: 'instream_video1', - adomain: ['test.com'], - price: 2, - crid: '4259970', - ext: { - bidder: { - rp: { - mime: 'application/javascript', - size_id: 201, - advid: 12345 - } - }, - prebid: { - targeting: { - hb_uuid: '0c498f63-5111-4bed-98e2-9be7cb932a64' + it('should register a successful bid', function () { + let response = { + cur: 'USD', + seatbid: [{ + bid: [{ + id: '0', + impid: '/19968336/header-bid-tag-0', + adomain: ['test.com'], + price: 2, + crid: '4259970', + ext: { + bidder: { + rp: { + mime: 'application/javascript', + size_id: 201, + advid: 12345 + } }, - type: 'video' + prebid: { + targeting: { + hb_uuid: '0c498f63-5111-4bed-98e2-9be7cb932a64' + }, + type: 'video' + } } - } + }], + group: 0, + seat: 'rubicon' }], - group: 0, - seat: 'rubicon' - }], - }; + }; - let bids = spec.interpretResponse({body: response}, { - bidRequest: bidderRequest.bids[0] - }); + const request = converter.toORTB({bidderRequest, bidRequests: bidderRequest.bids}); - expect(bids).to.be.lengthOf(1); + let bids = spec.interpretResponse({body: response}, {data: request}); - expect(bids[0].seatBidId).to.equal('0'); - expect(bids[0].creativeId).to.equal('4259970'); - expect(bids[0].cpm).to.equal(2); - expect(bids[0].ttl).to.equal(300); - expect(bids[0].netRevenue).to.equal(true); - expect(bids[0].adserverTargeting).to.deep.equal({hb_uuid: '0c498f63-5111-4bed-98e2-9be7cb932a64'}); - expect(bids[0].mediaType).to.equal('video'); - expect(bids[0].meta.mediaType).to.equal('video'); - expect(String(bids[0].meta.advertiserDomains)).to.equal('test.com'); - expect(bids[0].meta.advertiserId).to.equal(12345); - expect(bids[0].bidderCode).to.equal('rubicon'); - expect(bids[0].currency).to.equal('USD'); - expect(bids[0].width).to.equal(640); - expect(bids[0].height).to.equal(480); - }); - }); + expect(bids).to.be.lengthOf(1); - describe('for outstream video', function () { - const sandbox = sinon.createSandbox(); - beforeEach(function () { - createVideoBidderRequestOutstream(); - config.setConfig({rubicon: { - rendererConfig: { - align: 'left', - closeButton: true - }, - rendererUrl: 'https://example.test/renderer.js' - }}); - window.MagniteApex = { - renderAd: function() { - return null; - } - } + expect(bids[0].seatBidId).to.equal('0'); + expect(bids[0].creativeId).to.equal('4259970'); + expect(bids[0].cpm).to.equal(2); + expect(bids[0].ttl).to.equal(300); + expect(bids[0].netRevenue).to.equal(true); + expect(bids[0].adserverTargeting).to.deep.equal({hb_uuid: '0c498f63-5111-4bed-98e2-9be7cb932a64'}); + expect(bids[0].mediaType).to.equal('video'); + expect(bids[0].meta.mediaType).to.equal('video'); + expect(String(bids[0].meta.advertiserDomains)).to.equal('test.com'); + expect(bids[0].meta.advertiserId).to.equal(12345); + expect(bids[0].bidderCode).to.equal('rubicon'); + expect(bids[0].currency).to.equal('USD'); + expect(bids[0].width).to.equal(640); + expect(bids[0].height).to.equal(480); + }); }); + } - afterEach(function () { - sandbox.restore(); - delete window.MagniteApex; + if (FEATURES.NATIVE) { + describe('for native', () => { + it('should get a native bid', () => { + const nativeBidderRequest = addNativeToBidRequest(bidderRequest); + const request = converter.toORTB({bidderRequest: nativeBidderRequest, bidRequests: nativeBidderRequest.bids}); + let response = getNativeResponse({impid: request.imp[0].id}); + let bids = spec.interpretResponse({body: response}, {data: request}); + expect(bids).to.have.nested.property('[0].native'); + }); }); + } - it('should register a successful bid', function () { - let response = { - cur: 'USD', - seatbid: [{ - bid: [{ - id: '0', - impid: 'outstream_video1', - adomain: ['test.com'], - price: 2, - crid: '4259970', - ext: { - bidder: { - rp: { - mime: 'application/javascript', - size_id: 201, - advid: 12345 - } - }, - prebid: { - targeting: { - hb_uuid: '0c498f63-5111-4bed-98e2-9be7cb932a64' - }, - type: 'video' - } - } - }], - group: 0, - seat: 'rubicon' - }], - }; - - let bids = spec.interpretResponse({body: response}, { - bidRequest: bidderRequest.bids[0] + if (FEATURES.VIDEO) { + describe('for outstream video', function () { + const sandbox = sinon.createSandbox(); + beforeEach(function () { + createVideoBidderRequestOutstream(); + config.setConfig({rubicon: { + rendererConfig: { + align: 'left', + closeButton: true + }, + rendererUrl: 'https://example.test/renderer.js' + }}); + window.MagniteApex = { + renderAd: function() { + return null; + } + } }); - expect(bids).to.be.lengthOf(1); - - expect(bids[0].seatBidId).to.equal('0'); - expect(bids[0].creativeId).to.equal('4259970'); - expect(bids[0].cpm).to.equal(2); - expect(bids[0].ttl).to.equal(300); - expect(bids[0].netRevenue).to.equal(true); - expect(bids[0].adserverTargeting).to.deep.equal({hb_uuid: '0c498f63-5111-4bed-98e2-9be7cb932a64'}); - expect(bids[0].mediaType).to.equal('video'); - expect(bids[0].meta.mediaType).to.equal('video'); - expect(String(bids[0].meta.advertiserDomains)).to.equal('test.com'); - expect(bids[0].meta.advertiserId).to.equal(12345); - expect(bids[0].bidderCode).to.equal('rubicon'); - expect(bids[0].currency).to.equal('USD'); - expect(bids[0].width).to.equal(640); - expect(bids[0].height).to.equal(320); - // check custom renderer - expect(typeof bids[0].renderer).to.equal('object'); - expect(bids[0].renderer.getConfig()).to.deep.equal({ - align: 'left', - closeButton: true - }); - expect(bids[0].renderer.url).to.equal('https://example.test/renderer.js'); - }); + afterEach(function () { + sandbox.restore(); + delete window.MagniteApex; + }); - it('should render ad with Magnite renderer', function () { - let response = { - cur: 'USD', - seatbid: [{ - bid: [{ - id: '0', - impid: 'outstream_video1', - adomain: ['test.com'], - price: 2, - crid: '4259970', - ext: { - bidder: { - rp: { - mime: 'application/javascript', - size_id: 201, - advid: 12345 - } - }, - prebid: { - targeting: { - hb_uuid: '0c498f63-5111-4bed-98e2-9be7cb932a64' + it('should register a successful bid', function () { + let response = { + cur: 'USD', + seatbid: [{ + bid: [{ + id: '0', + impid: '/19968336/header-bid-tag-0', + adomain: ['test.com'], + price: 2, + crid: '4259970', + ext: { + bidder: { + rp: { + mime: 'application/javascript', + size_id: 201, + advid: 12345 + } }, - type: 'video' + prebid: { + targeting: { + hb_uuid: '0c498f63-5111-4bed-98e2-9be7cb932a64' + }, + type: 'video' + } } - }, - nurl: 'https://test.com/vast.xml' + }], + group: 0, + seat: 'rubicon' }], - group: 0, - seat: 'rubicon' - }], - }; - - sinon.spy(window.MagniteApex, 'renderAd'); + }; - let bids = spec.interpretResponse({body: response}, { - bidRequest: bidderRequest.bids[0] - }); - const bid = bids[0]; - bid.adUnitCode = 'outstream_video1_placement'; - const adUnit = document.createElement('div'); - adUnit.id = bid.adUnitCode; - document.body.appendChild(adUnit); - - bid.renderer.render(bid); - - const renderCall = window.MagniteApex.renderAd.getCall(0); - expect(renderCall.args[0]).to.deep.equal({ - closeButton: true, - collapse: true, - height: 320, - label: undefined, - placement: { + const request = converter.toORTB({bidderRequest, bidRequests: bidderRequest.bids}); + + let bids = spec.interpretResponse({body: response}, { data: request }); + + expect(bids).to.be.lengthOf(1); + + expect(bids[0].seatBidId).to.equal('0'); + expect(bids[0].creativeId).to.equal('4259970'); + expect(bids[0].cpm).to.equal(2); + expect(bids[0].ttl).to.equal(300); + expect(bids[0].netRevenue).to.equal(true); + expect(bids[0].adserverTargeting).to.deep.equal({hb_uuid: '0c498f63-5111-4bed-98e2-9be7cb932a64'}); + expect(bids[0].mediaType).to.equal('video'); + expect(bids[0].meta.mediaType).to.equal('video'); + expect(String(bids[0].meta.advertiserDomains)).to.equal('test.com'); + expect(bids[0].meta.advertiserId).to.equal(12345); + expect(bids[0].bidderCode).to.equal('rubicon'); + expect(bids[0].currency).to.equal('USD'); + expect(bids[0].width).to.equal(640); + expect(bids[0].height).to.equal(320); + // check custom renderer + expect(typeof bids[0].renderer).to.equal('object'); + expect(bids[0].renderer.getConfig()).to.deep.equal({ align: 'left', - attachTo: adUnit, - position: 'append', - }, - vastUrl: 'https://test.com/vast.xml', - width: 640 + closeButton: true + }); + expect(bids[0].renderer.url).to.equal('https://example.test/renderer.js'); + }); + + it('should render ad with Magnite renderer', function () { + let response = { + cur: 'USD', + seatbid: [{ + bid: [{ + id: '0', + impid: '/19968336/header-bid-tag-0', + adomain: ['test.com'], + price: 2, + crid: '4259970', + ext: { + bidder: { + rp: { + mime: 'application/javascript', + size_id: 201, + advid: 12345 + } + }, + prebid: { + targeting: { + hb_uuid: '0c498f63-5111-4bed-98e2-9be7cb932a64' + }, + type: 'video' + } + }, + nurl: 'https://test.com/vast.xml' + }], + group: 0, + seat: 'rubicon' + }], + }; + + const request = converter.toORTB({bidderRequest, bidRequests: bidderRequest.bids}); + + sinon.spy(window.MagniteApex, 'renderAd'); + + let bids = spec.interpretResponse({body: response}, {data: request}); + const bid = bids[0]; + bid.adUnitCode = 'outstream_video1_placement'; + const adUnit = document.createElement('div'); + adUnit.id = bid.adUnitCode; + document.body.appendChild(adUnit); + + bid.renderer.render(bid); + + const renderCall = window.MagniteApex.renderAd.getCall(0); + expect(renderCall.args[0]).to.deep.equal({ + closeButton: true, + collapse: true, + height: 320, + label: undefined, + placement: { + align: 'left', + attachTo: adUnit, + position: 'append', + }, + vastUrl: 'https://test.com/vast.xml', + width: 640 + }); + // cleanup + adUnit.parentNode.removeChild(adUnit); }); - // cleanup - adUnit.parentNode.removeChild(adUnit); }); - }); + } describe('config with integration type', () => { it('should use the integration type provided in the config instead of the default', () => { @@ -3617,3 +3736,164 @@ describe('the rubicon adapter', function () { }); }); }); + +function addNativeToBidRequest(bidderRequest) { + const nativeOrtbRequest = { + assets: [{ + id: 0, + required: 1, + title: { + len: 140 + } + }, + { + id: 1, + required: 1, + img: { + type: 3, + w: 300, + h: 600 + } + }, + { + id: 2, + required: 1, + data: { + type: 1 + } + }] + }; + bidderRequest.refererInfo = { + page: 'localhost' + } + bidderRequest.bids[0] = { + bidder: 'rubicon', + params: { + accountId: '14062', + siteId: '70608', + zoneId: '335918', + }, + adUnitCode: '/19968336/header-bid-tag-0', + code: 'div-1', + bidId: '2ffb201a808da7', + bidderRequestId: '178e34bad3658f', + auctionId: 'c45dd708-a418-42ec-b8a7-b70a6c6fab0a', + transactionId: 'd45dd707-a418-42ec-b8a7-b70a6c6fab0b', + mediaTypes: { + native: { + ortb: { + ...nativeOrtbRequest + } + } + }, + nativeOrtbRequest + } + return bidderRequest; +} + +function getNativeResponse(options = {impid: 1234}) { + return { + 'id': 'd7786a80-bfb4-4541-859f-225a934e81d4', + 'seatbid': [ + { + 'bid': [ + { + 'id': '971650', + 'impid': options.impid, + 'price': 20, + 'adm': { + 'ver': '1.2', + 'assets': [ + { + 'id': 0, + 'title': { + 'text': 'This is a title' + }, + 'link': { + 'clicktrackers': [ + 'http://localhost:5500/event?type=click1&component=card&asset=0' + ] + } + }, + { + 'id': 1, + 'img': { + 'url': 'https:\\\\/\\\\/vcdn.adnxs.com\\\\/p\\\\/creative-image\\\\/94\\\\/22\\\\/cd\\\\/0f\\\\/9422cd0f-f400-45d3-80f5-2b92629d9257.jpg', + 'h': 2250, + 'w': 3000 + }, + 'link': { + 'clicktrackers': [ + 'http://localhost:5500/event?type=click1&component=card&asset=1' + ] + } + }, + { + 'id': 2, + 'data': { + 'value': 'this is asset data 1 that corresponds to sponsoredBy' + } + } + ], + 'link': { + 'url': 'https://magnite.com', + 'clicktrackers': [ + 'http://localhost:5500/event?type=click1&component=card', + 'http://localhost:5500/event?type=click2&component=card' + ] + }, + 'jstracker': '', + 'eventtrackers': [ + { + 'event': 1, + 'method': 2, + 'url': 'http://localhost:5500/event?type=1&method=2' + }, + { + 'event': 2, + 'method': 1, + 'url': 'http://localhost:5500/event?type=v50&component=card' + } + ] + }, + 'adid': '392180', + 'adomain': [ + 'http://prebid.org' + ], + 'iurl': 'https://lax1-ib.adnxs.com/cr?id=97494403', + 'cid': '9325', + 'crid': '97494403', + 'cat': [ + 'IAB3-1' + ], + 'w': 300, + 'h': 600, + 'ext': { + 'prebid': { + 'targeting': { + 'hb_bidder': 'rubicon', + 'hb_cache_host': 'prebid.lax1.adnxs-simple.com', + 'hb_cache_path': '/pbc/v1/cache', + 'hb_pb': '20.00' + }, + 'type': 'native', + 'video': { + 'duration': 0, + 'primary_category': '' + } + }, + 'rubicon': { + 'auction_id': 642778043863823100, + 'bid_ad_type': 3, + 'bidder_id': 2, + 'brand_id': 555545 + } + } + } + ], + 'seat': 'rubicon' + } + ], + 'cur': 'USD' + }; +} diff --git a/test/spec/modules/scatteredBidAdapter_spec.js b/test/spec/modules/scatteredBidAdapter_spec.js new file mode 100644 index 00000000000..7f0b13bc07b --- /dev/null +++ b/test/spec/modules/scatteredBidAdapter_spec.js @@ -0,0 +1,210 @@ +import { spec, converter } from 'modules/scatteredBidAdapter.js'; +import { assert } from 'chai'; +import { config } from 'src/config.js'; +import { deepClone, mergeDeep } from '../../../src/utils'; +describe('Scattered adapter', function () { + describe('isBidRequestValid', function () { + // A valid bid + let validBid = { + bidder: 'scattered', + mediaTypes: { + banner: { + sizes: [[300, 250], [760, 400]] + } + }, + params: { + bidderDomain: 'https://prebid.scattered.eu', + test: 0 + } + }; + + // Because this valid bid is modified to create invalid bids in following tests we first check it. + // We must be sure it is a valid one, not to get false negatives. + it('should accept a valid bid', function () { + assert.isTrue(spec.isBidRequestValid(validBid)); + }); + + it('should skip if bidderDomain info is missing', function () { + let bid = deepClone(validBid); + + delete bid.params.bidderDomain; + assert.isFalse(spec.isBidRequestValid(bid)); + }); + + it('should expect at least one banner size', function () { + let bid = deepClone(validBid); + + delete bid.mediaTypes.banner; + assert.isFalse(spec.isBidRequestValid(bid)); + + // empty sizes array + bid.mediaTypes = { + banner: { + sizes: [] + } + }; + assert.isFalse(spec.isBidRequestValid(bid)); + }); + }); + + describe('buildRequests', function () { + let arrayOfValidBidRequests, validBidderRequest; + + beforeEach(function () { + arrayOfValidBidRequests = [{ + bidder: 'scattered', + params: { + bidderDomain: 'https://prebid.scattered.eu', + test: 0 + }, + mediaTypes: { + banner: { + sizes: [[300, 250], [760, 400]] + }, + adUnitCode: 'test-div', + transactionId: '32d09c47-c6b8-40b0-9605-2e251a472ea4', + bidId: '21adc5d8765aa1', + bidderRequestId: '130728f7662afc', + auctionId: 'b4a45a23-8371-4d87-9308-39146b29ca32', + }, + }]; + + validBidderRequest = { + bidderCode: 'scattered', + auctionId: 'b4a45a23-8371-4d87-9308-39146b29ca32', + gdprConsent: { consentString: 'BOtmiBKOtmiBKABABAENAFAAAAACeAAA', gdprApplies: true }, + refererInfo: { + domain: 'localhost', + page: 'http://localhost:9999/integrationExamples/gpt/hello_world.html?pbjs_debug=true', + }, + ortb2: { + site: { + publisher: { + name: 'publisher1 INC.' + } + } + } + }; + }); + + it('should validate request format', function () { + let request = spec.buildRequests(arrayOfValidBidRequests, validBidderRequest); + assert.equal(request.method, 'POST'); + assert.deepEqual(request.options, { contentType: 'application/json' }); + assert.ok(request.data); + }); + + it('has the right fields filled', function () { + let request = spec.buildRequests(arrayOfValidBidRequests, validBidderRequest); + const bidderRequest = request.data; + assert.equal(bidderRequest.id, validBidderRequest.auctionId); + assert.ok(bidderRequest.site); + assert.ok(bidderRequest.source); + assert.lengthOf(bidderRequest.imp, 1); + }); + + it('should configure the site object', function () { + let request = spec.buildRequests(arrayOfValidBidRequests, validBidderRequest); + const site = request.data.site; + assert.equal(site.publisher.name, validBidderRequest.ortb2.site.publisher.name) + }); + + it('should configure site with ortb2', function () { + const req = mergeDeep({}, validBidderRequest, { + ortb2: { + site: { + id: '876', + publisher: { + domain: 'publisher1.eu' + } + } + } + }); + + let request = spec.buildRequests(arrayOfValidBidRequests, req); + const site = request.data.site; + assert.deepEqual(site, { + id: '876', + publisher: { + domain: 'publisher1.eu', + name: 'publisher1 INC.' + } + }); + }); + + it('should send device info', function () { + it('should send info about device', function () { + config.setConfig({ + device: { w: 375, h: 273 } + }); + + let request = spec.buildRequests(arrayOfValidBidRequests, validBidderRequest); + + assert.equal(request.device.ua, navigator.userAgent); + assert.equal(request.device.w, 375); + assert.equal(request.device.h, 273); + }); + }) + }) +}) + +describe('interpretResponse', function () { + const serverResponse = { + body: { + id: 'b4a45a23-8371-4d87-9308-39146b29ca32', + bidid: '11111111-2222-2222-2222-333333333333', + cur: 'PLN', + seatbid: [{ + bid: [ + { + id: '234234-234234-234234', // bidder generated + impid: '123', + price: '34.2', + nurl: 'https://scattered.eu/nurl', + adm: '
', + cpm: '34.2', + creativeId: '2345-2345-23', + currency: 'PLN', + height: 456, + width: 345, + mediaType: 'banner', + requestId: '123', + ttl: 360, + }; + expect(results.length).to.equal(1); + sinon.assert.match(results[0], expected); + }); +}); diff --git a/test/spec/modules/seedingAllianceAdapter_spec.js b/test/spec/modules/seedingAllianceAdapter_spec.js index 81af9546ff0..6086db01de4 100755 --- a/test/spec/modules/seedingAllianceAdapter_spec.js +++ b/test/spec/modules/seedingAllianceAdapter_spec.js @@ -38,7 +38,7 @@ describe('SeedingAlliance adapter', function () { }); it('should have default request structure', function () { - let keys = 'site,device,cur,imp,user,regs'.split(','); + let keys = 'site,cur,imp,regs'.split(','); let validBidRequests = [{ bidId: 'bidId', params: {} @@ -60,14 +60,17 @@ describe('SeedingAlliance adapter', function () { assert.equal(request.id, validBidRequests[0].auctionId); }); - it('Verify the device', function () { + it('Verify the site url', function () { + let siteUrl = 'https://www.yourdomain.tld/your-directory/'; let validBidRequests = [{ bidId: 'bidId', - params: {} + params: { + url: siteUrl + } }]; let request = JSON.parse(spec.buildRequests(validBidRequests, { refererInfo: { referer: 'page' } }).data); - assert.equal(request.device.ua, navigator.userAgent); + assert.equal(request.site.page, siteUrl); }); it('Verify native asset ids', function () { @@ -109,7 +112,7 @@ describe('SeedingAlliance adapter', function () { }); describe('interpretResponse', function () { - const goodResponse = { + const goodNativeResponse = { body: { cur: 'EUR', id: '4b516b80-886e-4ec0-82ae-9209e6d625fb', @@ -136,51 +139,91 @@ describe('SeedingAlliance adapter', function () { ] } }; + + const goodBannerResponse = { + body: { + cur: 'EUR', + id: 'b4516b80-886e-4ec0-82ae-9209e6d625fb', + seatbid: [ + { + seat: 'seedingAlliance', + bid: [{ + adm: '', + impid: 1, + price: 0.90, + h: 250, + w: 300 + }] + } + ] + } + }; + const badResponse = { body: { cur: 'EUR', id: '4b516b80-886e-4ec0-82ae-9209e6d625fb', seatbid: [] }}; - const bidRequest = { + const bidNativeRequest = { data: {}, - bids: [{ bidId: 'bidId1' }] + bidRequests: [{bidId: 'bidId1', nativeParams: {title: {required: true, len: 800}}}] + }; + + const bidBannerRequest = { + data: {}, + bidRequests: [{bidId: 'bidId1', sizes: [300, 250]}] }; it('should return null if body is missing or empty', function () { - const result = spec.interpretResponse(badResponse, bidRequest); + const result = spec.interpretResponse(badResponse, bidNativeRequest); assert.equal(result.length, 0); delete badResponse.body - const result1 = spec.interpretResponse(badResponse, bidRequest); + const result1 = spec.interpretResponse(badResponse, bidNativeRequest); assert.equal(result.length, 0); }); it('should return the correct params', function () { - const result = spec.interpretResponse(goodResponse, bidRequest); - const bid = goodResponse.body.seatbid[0].bid[0]; - - assert.deepEqual(result[0].currency, goodResponse.body.cur); - assert.deepEqual(result[0].requestId, bidRequest.bids[0].bidId); - assert.deepEqual(result[0].cpm, bid.price); - assert.deepEqual(result[0].creativeId, bid.crid); - assert.deepEqual(result[0].mediaType, 'native'); - assert.deepEqual(result[0].bidderCode, 'seedingAlliance'); + const resultNative = spec.interpretResponse(goodNativeResponse, bidNativeRequest); + const bidNative = goodNativeResponse.body.seatbid[0].bid[0]; + + assert.deepEqual(resultNative[0].bidderCode, 'seedingAlliance'); + assert.deepEqual(resultNative[0].currency, goodNativeResponse.body.cur); + assert.deepEqual(resultNative[0].requestId, bidNativeRequest.bidRequests[0].bidId); + assert.deepEqual(resultNative[0].cpm, bidNative.price); + assert.deepEqual(resultNative[0].creativeId, bidNative.crid); + assert.deepEqual(resultNative[0].mediaType, 'native'); + + const resultBanner = spec.interpretResponse(goodBannerResponse, bidBannerRequest); + + assert.deepEqual(resultBanner[0].bidderCode, 'seedingAlliance'); + assert.deepEqual(resultBanner[0].mediaType, 'banner'); + assert.deepEqual(resultBanner[0].width, bidBannerRequest.bidRequests[0].sizes[0]); + assert.deepEqual(resultBanner[0].height, bidBannerRequest.bidRequests[0].sizes[1]); }); - it('should return the correct tracking links', function () { - const result = spec.interpretResponse(goodResponse, bidRequest); - const bid = goodResponse.body.seatbid[0].bid[0]; + it('should return the correct native tracking links', function () { + const result = spec.interpretResponse(goodNativeResponse, bidNativeRequest); + const bid = goodNativeResponse.body.seatbid[0].bid[0]; const regExpPrice = new RegExp('price=' + bid.price); result[0].native.clickTrackers.forEach(function (clickTracker) { - assert.ok(clickTracker.search(regExpPrice) > -1); + assert.ok(clickTracker.search(regExpPrice) > -1); }); result[0].native.impressionTrackers.forEach(function (impTracker) { - assert.ok(impTracker.search(regExpPrice) > -1); + assert.ok(impTracker.search(regExpPrice) > -1); }); }); + + it('should return the correct banner content', function () { + const result = spec.interpretResponse(goodBannerResponse, bidBannerRequest); + const bid = goodBannerResponse.body.seatbid[0].bid[0]; + const regExpContent = new RegExp(''); + + assert.ok(result[0].ad.search(regExpContent) > -1); + }); }); }); diff --git a/test/spec/modules/seedtagBidAdapter_spec.js b/test/spec/modules/seedtagBidAdapter_spec.js index d0efd5f1f75..3627296975b 100644 --- a/test/spec/modules/seedtagBidAdapter_spec.js +++ b/test/spec/modules/seedtagBidAdapter_spec.js @@ -33,30 +33,60 @@ function createInStreamSlotConfig(mediaType) { }); } +const createBannerSlotConfig = (placement, mediatypes) => { + return getSlotConfigs(mediatypes || { banner: {} }, { + publisherId: PUBLISHER_ID, + adUnitId: ADUNIT_ID, + placement, + }); +}; + describe('Seedtag Adapter', function () { 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 () { - const createBannerSlotConfig = (placement) => { - return getSlotConfigs( - { banner: {} }, - { - publisherId: PUBLISHER_ID, - adUnitId: ADUNIT_ID, - placement, - } + const placements = ['inBanner', 'inImage', 'inScreen', 'inArticle']; + placements.forEach((placement) => { + it(placement + 'should be valid', function () { + const isBidRequestValid = spec.isBidRequestValid( + createBannerSlotConfig(placement) ); - }; - const placements = ['inBanner', 'inImage', 'inScreen', 'inArticle']; - placements.forEach((placement) => { - it('should be ' + placement, function () { + expect(isBidRequestValid).to.equal(true); + }); + + it( + placement + + ' should be valid when has display and video mediatypes, and video context is outstream', + function () { const isBidRequestValid = spec.isBidRequestValid( - createBannerSlotConfig(placement) + createBannerSlotConfig(placement, { + banner: {}, + video: { + context: 'outstream', + playerSize: [[600, 200]], + }, + }) ); expect(isBidRequestValid).to.equal(true); - }); - }); + } + ); + + it( + placement + + " shouldn't be valid when has display and video mediatypes, and video context is instream", + function () { + const isBidRequestValid = spec.isBidRequestValid( + createBannerSlotConfig(placement, { + banner: {}, + video: { + context: 'instream', + playerSize: [[600, 200]], + }, + }) + ); + expect(isBidRequestValid).to.equal(false); + } + ); }); }); describe('when video slot has all mandatory params', function () { @@ -70,7 +100,18 @@ describe('Seedtag Adapter', function () { const isBidRequestValid = spec.isBidRequestValid(slotConfig); expect(isBidRequestValid).to.equal(true); }); - it('should return true, when video context is instream, but placement is not inStream', function () { + it('should return true, when video context is instream and mediatype is video and banner', function () { + const slotConfig = createInStreamSlotConfig({ + video: { + context: 'instream', + playerSize: [[600, 200]], + }, + banner: {}, + }); + const isBidRequestValid = spec.isBidRequestValid(slotConfig); + expect(isBidRequestValid).to.equal(true); + }); + it('should return false, when video context is instream, but placement is not inStream', function () { const slotConfig = getSlotConfigs( { video: { @@ -395,6 +436,52 @@ describe('Seedtag Adapter', function () { expect(payload.schain).to.not.exist; }); }); + + describe('GPP param', function () { + it('should be added to payload when bidderRequest has gppConsent param', function () { + const gppConsent = { + gppString: 'someGppString', + applicableSections: [7] + } + bidderRequest['gppConsent'] = gppConsent + const request = spec.buildRequests(validBidRequests, bidderRequest); + const data = JSON.parse(request.data); + expect(data.gppConsent).to.exist; + expect(data.gppConsent.gppString).to.equal(gppConsent.gppString); + expect(data.gppConsent.applicableSections[0]).to.equal(gppConsent.applicableSections[0]); + }); + + it('should be undefined on payload when bidderRequest has not gppConsent param', function () { + bidderRequest.gppConsent = undefined + const request = spec.buildRequests(validBidRequests, bidderRequest); + const data = JSON.parse(request.data); + expect(data.gppConsent).to.be.undefined; + }); + + it('should be added to payload when bidderRequest has ortb2 param', function () { + const ortb2 = { + regs: { + gpp: 'someGppString', + gpp_sid: [7] + } + } + bidderRequest['gppConsent'] = undefined + bidderRequest['ortb2'] = ortb2; + const request = spec.buildRequests(validBidRequests, bidderRequest); + const data = JSON.parse(request.data); + expect(data.gppConsent).to.exist; + expect(data.gppConsent.gppString).to.equal(ortb2.regs.gpp); + expect(data.gppConsent.applicableSections[0]).to.equal(ortb2.regs.gpp_sid[0]); + }); + + it('should be added to payload when bidderRequest has neither gppConsent nor ortb2', function () { + bidderRequest['ortb2'] = undefined; + bidderRequest['gppConsent'] = undefined; + const request = spec.buildRequests(validBidRequests, bidderRequest); + const data = JSON.parse(request.data); + expect(data.gppConsent).to.be.undefined; + }); + }); }); describe('interpret response method', function () { @@ -533,11 +620,11 @@ describe('Seedtag Adapter', function () { const timeoutUrl = getTimeoutUrl(timeoutData); expect(timeoutUrl).to.equal( 'https://s.seedtag.com/se/hb/timeout?publisherToken=' + - params.publisherId + - '&adUnitId=' + - params.adUnitId + - '&timeout=' + - timeout + params.publisherId + + '&adUnitId=' + + params.adUnitId + + '&timeout=' + + timeout ); }); @@ -549,11 +636,11 @@ describe('Seedtag Adapter', function () { expect( utils.triggerPixel.calledWith( 'https://s.seedtag.com/se/hb/timeout?publisherToken=' + - params.publisherId + - '&adUnitId=' + - params.adUnitId + - '&timeout=' + - timeout + params.publisherId + + '&adUnitId=' + + params.adUnitId + + '&timeout=' + + timeout ) ).to.equal(true); }); diff --git a/test/spec/modules/sharedIdSystem_spec.js b/test/spec/modules/sharedIdSystem_spec.js index 8ef34a1599e..fcfbe5f7c3f 100644 --- a/test/spec/modules/sharedIdSystem_spec.js +++ b/test/spec/modules/sharedIdSystem_spec.js @@ -91,50 +91,4 @@ describe('SharedId System', function () { expect(result).to.be.undefined; }); }); - - describe('SharedID System domainOverride', () => { - let sandbox, domain, cookies, rejectCookiesFor; - - beforeEach(() => { - sandbox = sinon.createSandbox(); - sandbox.stub(document, 'domain').get(() => domain); - cookies = {}; - sandbox.stub(storage, 'getCookie').callsFake((key) => cookies[key]); - rejectCookiesFor = null; - sandbox.stub(storage, 'setCookie').callsFake((key, value, expires, sameSite, domain) => { - if (domain !== rejectCookiesFor) { - if (expires != null) { - expires = new Date(expires); - } - if (expires == null || expires > Date.now()) { - cookies[key] = value; - } else { - delete cookies[key]; - } - } - }); - }); - - afterEach(() => { - sandbox.restore(); - }); - - it('should return TLD if cookies can be set there', () => { - domain = 'sub.domain.com'; - rejectCookiesFor = 'com'; - expect(sharedIdSystemSubmodule.domainOverride()).to.equal('domain.com'); - }); - - it('should return undefined when cookies cannot be set', () => { - domain = 'sub.domain.com'; - rejectCookiesFor = 'sub.domain.com'; - expect(sharedIdSystemSubmodule.domainOverride()).to.be.undefined; - }); - - it('should return half-way domain if parent domain rejects cookies', () => { - domain = 'inner.outer.domain.com'; - rejectCookiesFor = 'domain.com'; - expect(sharedIdSystemSubmodule.domainOverride()).to.equal('outer.domain.com'); - }); - }); }); diff --git a/test/spec/modules/sharethroughBidAdapter_spec.js b/test/spec/modules/sharethroughBidAdapter_spec.js index 56e10a74240..fc4fbc86018 100644 --- a/test/spec/modules/sharethroughBidAdapter_spec.js +++ b/test/spec/modules/sharethroughBidAdapter_spec.js @@ -93,31 +93,107 @@ describe('sharethrough adapter spec', function () { }, }, }, - userId: { - tdid: 'fake-tdid', - pubcid: 'fake-pubcid', - idl_env: 'fake-identity-link', - id5id: { - uid: 'fake-id5id', - ext: { - linkType: 2, - }, + userIdAsEids: [ + { + 'source': 'pubcid.org', + 'uids': [ + { + 'atype': 1, + 'id': 'fake-pubcid' + }, + ] + }, + { + 'source': 'liveramp.com', + 'uids': [ + { + 'atype': 1, + 'id': 'fake-identity-link' + } + ] + }, + { + 'source': 'id5-sync.com', + 'uids': [ + { + 'atype': 1, + 'id': 'fake-id5id' + } + ] }, - lipb: { - lipbid: 'fake-lipbid', + { + 'source': 'adserver.org', + 'uids': [ + { + 'atype': 1, + 'id': 'fake-tdid' + } + ] }, - criteoId: 'fake-criteo', - britepoolid: 'fake-britepool', - intentIqId: 'fake-intentiq', - lotamePanoramaId: 'fake-lotame', - parrableId: { - eid: 'fake-parrable', + { + 'source': 'criteo.com', + 'uids': [ + { + 'atype': 1, + 'id': 'fake-criteo' + } + ] }, - netId: 'fake-netid', - sharedid: { - id: 'fake-sharedid', + { + 'source': 'britepool.com', + 'uids': [ + { + 'atype': 1, + 'id': 'fake-britepool' + } + ] }, - }, + { + 'source': 'liveintent.com', + 'uids': [ + { + 'atype': 1, + 'id': 'fake-lipbid' + } + ] + }, + { + 'source': 'intentiq.com', + 'uids': [ + { + 'atype': 1, + 'id': 'fake-intentiq' + } + ] + }, + { + 'source': 'crwdcntrl.net', + 'uids': [ + { + 'atype': 1, + 'id': 'fake-lotame' + } + ] + }, + { + 'source': 'parrable.com', + 'uids': [ + { + 'atype': 1, + 'id': 'fake-parrable' + } + ] + }, + { + 'source': 'netid.de', + 'uids': [ + { + 'atype': 1, + 'id': 'fake-netid' + } + ] + } + ], crumbs: { pubcid: 'fake-pubcid-in-crumbs-obj', }, @@ -172,7 +248,8 @@ describe('sharethrough adapter spec', function () { refererInfo: { ref: 'https://referer.com', }, - auctionId: 'auction-id' + auctionId: 'auction-id', + timeout: 242 }; }); diff --git a/test/spec/modules/showheroes-bsBidAdapter_spec.js b/test/spec/modules/showheroes-bsBidAdapter_spec.js index 1aa7b132221..30e95b04ccf 100644 --- a/test/spec/modules/showheroes-bsBidAdapter_spec.js +++ b/test/spec/modules/showheroes-bsBidAdapter_spec.js @@ -5,7 +5,7 @@ import {VIDEO, BANNER} from 'src/mediaTypes.js' const bidderRequest = { refererInfo: { - referer: 'https://example.com' + canonicalUrl: 'https://example.com' } } @@ -19,6 +19,8 @@ const gdpr = { } } +const uspConsent = '1---'; + const schain = { 'schain': { 'validation': 'strict', @@ -338,18 +340,28 @@ describe('shBidAdapter', function () { }); }) - it('passes gdpr if present', function () { - const request = spec.buildRequests([bidRequestVideo], {...bidderRequest, ...gdpr}) + it('passes gdpr & uspConsent if present', function () { + const request = spec.buildRequests([bidRequestVideo], { + ...bidderRequest, + ...gdpr, + uspConsent, + }) const payload = request.data.requests[0]; expect(payload).to.be.an('object'); expect(payload.gdprConsent).to.eql(gdpr.gdprConsent) + expect(payload.uspConsent).to.eql(uspConsent) }) - it('passes gdpr if present (V2)', function () { - const request = spec.buildRequests([bidRequestVideoV2], {...bidderRequest, ...gdpr}) + it('passes gdpr & usp if present (V2)', function () { + const request = spec.buildRequests([bidRequestVideoV2], { + ...bidderRequest, + ...gdpr, + uspConsent, + }) const context = request.data.context; expect(context).to.be.an('object'); expect(context.gdprConsent).to.eql(gdpr.gdprConsent) + expect(context.uspConsent).to.eql(uspConsent) }) it('passes schain object if present', function() { @@ -379,7 +391,7 @@ describe('shBidAdapter', function () { expect(spec.interpretResponse({body: []}, {data: {meta: {}}}).length).to.equal(0) }) - const vastTag = 'https://video-library.stage.showheroes.com/commercial/wrapper?player_id=47427aa0-f11a-4d24-abca-1295a46a46cd&ad_bidder=showheroes-bs&master_shadt=1&description_url=https%3A%2F%2Fbid-service.stage.showheroes.com%2Fvast%2Fad%2Fcache%2F4840b920-40e1-4e09-9231-60bbf088c8d6' + const vastTag = 'https://test.com/commercial/wrapper?player_id=47427aa0-f11a-4d24-abca-1295a46a46cd&ad_bidder=showheroes-bs&master_shadt=1&description_url=https%3A%2F%2Fbid-service.stage.showheroes.com%2Fvast%2Fad%2Fcache%2F4840b920-40e1-4e09-9231-60bbf088c8d6' const vastXml = '' const basicResponse = { @@ -389,7 +401,7 @@ describe('shBidAdapter', function () { 'context': 'instream', 'bidId': '38b373e1e31c18', 'size': {'width': 640, 'height': 480}, - 'vastTag': 'https:\/\/video-library.stage.showheroes.com\/commercial\/wrapper?player_id=47427aa0-f11a-4d24-abca-1295a46a46cd&ad_bidder=showheroes-bs&master_shadt=1&description_url=https%3A%2F%2Fbid-service.stage.showheroes.com%2Fvast%2Fad%2Fcache%2F4840b920-40e1-4e09-9231-60bbf088c8d6', + 'vastTag': 'https:\/\/test.com\/commercial\/wrapper?player_id=47427aa0-f11a-4d24-abca-1295a46a46cd&ad_bidder=showheroes-bs&master_shadt=1&description_url=https%3A%2F%2Fbid-service.stage.showheroes.com%2Fvast%2Fad%2Fcache%2F4840b920-40e1-4e09-9231-60bbf088c8d6', 'vastXml': vastXml, 'adomain': adomain, }; @@ -423,13 +435,13 @@ describe('shBidAdapter', function () { 'height': 480, 'advertiserDomain': [], 'callbacks': { - 'won': ['https://api-n729.qa.viralize.com/track/?ver=15&session_id=01ecd03ce381505ccdeb88e555b05001&category=request_session&type=event&request_session_id=01ecd03ce381505ccdeb88e555b05001&label=prebid_won&reason=ok'] + 'won': ['https://test.com/track/?ver=15&session_id=01ecd03ce381505ccdeb88e555b05001&category=request_session&type=event&request_session_id=01ecd03ce381505ccdeb88e555b05001&label=prebid_won&reason=ok'] }, 'mediaType': 'video', 'adomain': adomain, }; - const vastUrl = 'https://api-n729.qa.viralize.com/vast/?zid=AACBWAcof-611K4U&u=https://example.org/?foo=bar&gdpr=0&cs=XXXXXXXXXXXXXXXXXXXX&sid=01ecd03ce381505ccdeb88e555b05001&width=300&height=200&prebidmode=1' + const vastUrl = 'https://test.com/vast/?zid=AACBWAcof-611K4U&u=https://example.org/?foo=bar&gdpr=0&cs=XXXXXXXXXXXXXXXXXXXX&sid=01ecd03ce381505ccdeb88e555b05001&width=300&height=200&prebidmode=1' const responseVideoV2 = { 'bidResponses': [{ @@ -443,7 +455,7 @@ describe('shBidAdapter', function () { 'bidResponses': [{ ...basicResponseV2, 'context': 'outstream', - 'ad': '', + 'ad': '', }], }; @@ -533,10 +545,6 @@ describe('shBidAdapter', function () { expect(renderer.config.vastUrl).to.equal(vastTag) renderer.render(bid) - // TODO: fix these. our tests should not be reliant on third-party scripts. wtf - // const scripts = document.querySelectorAll('script[src="https://static.showheroes.com/publishertag.js"]') - // expect(scripts.length).to.equal(1) - const spots = document.querySelectorAll('.showheroes-spot') expect(spots.length).to.equal(1) }) @@ -590,8 +598,6 @@ describe('shBidAdapter', function () { renderer.render(bid) const iframeDocument = iframe.contentDocument || (iframe.contentWindow && iframe.contentWindow.document) - // const scripts = iframeDocument.querySelectorAll('script[src="https://static.showheroes.com/publishertag.js"]') - // expect(scripts.length).to.equal(1) const spots = iframeDocument.querySelectorAll('.showheroes-spot') expect(spots.length).to.equal(1) }) @@ -602,8 +608,6 @@ describe('shBidAdapter', function () { customRender: function (bid, embedCode) { const container = document.createElement('div') container.appendChild(embedCode) - // const scripts = container.querySelectorAll('script[src="https://static.showheroes.com/publishertag.js"]') - // expect(scripts.length).to.equal(1) const spots = container.querySelectorAll('.showheroes-spot') expect(spots.length).to.equal(1) diff --git a/test/spec/modules/sirdataRtdProvider_spec.js b/test/spec/modules/sirdataRtdProvider_spec.js index 9cf392ebd62..eccc4777906 100644 --- a/test/spec/modules/sirdataRtdProvider_spec.js +++ b/test/spec/modules/sirdataRtdProvider_spec.js @@ -1,17 +1,17 @@ -import { addSegmentData, getSegmentsAndCategories, sirdataSubmodule } from 'modules/sirdataRtdProvider.js'; -import { server } from 'test/mocks/xhr.js'; +import {addSegmentData, getSegmentsAndCategories, sirdataSubmodule} from 'modules/sirdataRtdProvider.js'; +import {server} from 'test/mocks/xhr.js'; const responseHeader = {'Content-Type': 'application/json'}; -describe('sirdataRtdProvider', function() { - describe('sirdataSubmodule', function() { +describe('sirdataRtdProvider', function () { + describe('sirdataSubmodule', function () { it('successfully instantiates', function () { - expect(sirdataSubmodule.init()).to.equal(true); + expect(sirdataSubmodule.init()).to.equal(true); }); }); - describe('Add Segment Data', function() { - it('adds segment data', function() { + describe('Add Segment Data', function () { + it('adds segment data', function () { const config = { params: { setGptKeyValues: false, @@ -42,23 +42,34 @@ describe('sirdataRtdProvider', function() { contextual_categories: {'333333': 100} }; - addSegmentData({adUnits}, data, config, () => {}); + addSegmentData({adUnits}, data, config, () => { + }); expect(adUnits[0].bids[0].params.keywords).to.have.deep.property('sd_rtd', ['111111', '222222', '333333']); - expect(adUnits[0].bids[1].ortb2.site.ext.data).to.have.deep.property('sd_rtd', ['333333']); - expect(adUnits[0].bids[1].ortb2.user.ext.data).to.have.deep.property('sd_rtd', ['111111', '222222']); }); }); - describe('Get Segments And Categories', function() { - it('gets data from async request and adds segment data', function() { + describe('Get Segments And Categories', function () { + it('gets data from async request and adds segment data', function () { + const overrideAppnexus = function (adUnit, list, data, bid) { + deepSetValue(bid, 'params.keywords.custom', list); + } + const config = { params: { setGptKeyValues: false, contextualMinRelevancyScore: 50, bidders: [{ - bidder: 'appnexus' + bidder: 'appnexus', + customFunction: overrideAppnexus }, { - bidder: 'other' + bidder: 'smartadserver' + }, { + bidder: 'ix', + sizeLimit: 1200, + }, { + bidder: 'rubicon', + }, { + bidder: 'proxistore', }] } }; @@ -71,24 +82,107 @@ describe('sirdataRtdProvider', function() { placementId: 13144370 } }, { - bidder: 'other' + bidder: 'smartadserver', + params: { + siteId: 207435, + pageId: 896536, + formatId: 62913 + } + }, { + bidder: 'proxistore', + params: {website: 'demo.sirdata.com', language: 'fr'}, + adUnitCode: 'HALFPAGE_CENTER_LOADER', + transactionId: '92ac333a-a569-4827-abf1-01fc9d19278a', + sizes: [[300, 600]], + mediaTypes: { + banner: { + filteredSizeConfig: [ + {minViewPort: [1600, 0], sizes: [[300, 600]]}, + ], + sizeConfig: [ + {minViewPort: [0, 0], sizes: [[300, 600]]}, + {minViewPort: [768, 0], sizes: [[300, 600]]}, + {minViewPort: [1200, 0], sizes: [[300, 600]]}, + {minViewPort: [1600, 0], sizes: [[300, 600]]}, + ], + sizes: [[300, 600]], + }, + }, + bidId: '190bab495bc5f6e', + bidderRequestId: '18c0b0f0c91cd88', + auctionId: '9bdd917b-908d-4d9f-8f2f-d443277a62fc', + src: 'client', + bidRequestsCount: 1, + bidderRequestsCount: 1, + bidderWinsCount: 0, + }, { + bidder: 'ix', + params: { + siteId: '12345', + size: [300, 600] + } + }, { + bidder: 'rubicon', + params: { + accountId: 14062, + siteId: 70608, + zoneId: 498816 + } }] - }] + }], + ortb2Fragments: { + global: {} + } }; let data = { - segments: [111111, 222222], - contextual_categories: {'333333': 100} + 'segments': [111111, 222222], + 'segtaxid': null, + 'cattaxid': null, + 'contextual_categories': {'333333': 100}, + 'shared_taxonomy': { + '27440': { + 'segments': [444444, 555555], + 'segtaxid': 552, + 'cattaxid': 553, + 'contextual_categories': {'666666': 100} + } + }, + 'global_taxonomy': { + '9998': { + 'segments': [123, 234], + 'segtaxid': 4, + 'cattaxid': 7, + 'contextual_categories': {'345': 100, '456': 100} + } + } }; - getSegmentsAndCategories(reqBidsConfigObj, () => {}, config, {}); + getSegmentsAndCategories(reqBidsConfigObj, () => { + }, config, {}); let request = server.requests[0]; request.respond(200, responseHeader, JSON.stringify(data)); - expect(reqBidsConfigObj.adUnits[0].bids[0].params.keywords).to.have.deep.property('sd_rtd', ['111111', '222222', '333333']); - expect(reqBidsConfigObj.adUnits[0].bids[1].ortb2.site.ext.data).to.have.deep.property('sd_rtd', ['333333']); - expect(reqBidsConfigObj.adUnits[0].bids[1].ortb2.user.ext.data).to.have.deep.property('sd_rtd', ['111111', '222222']); + expect(reqBidsConfigObj.adUnits[0].bids[1].params).to.have.deep.property('target', 'sd_rtd=111111;sd_rtd=222222;sd_rtd=333333;sd_rtd=444444;sd_rtd=555555;sd_rtd=666666'); + + expect(reqBidsConfigObj.ortb2Fragments.global.site.content.data[0].name).to.equal( + 'sirdata.com' + ); + expect(reqBidsConfigObj.ortb2Fragments.global.site.content.data[0].segment).to.eql([ + {id: '345'}, + {id: '456'} + ]); + expect(reqBidsConfigObj.ortb2Fragments.global.site.content.data[0].ext.segtax).to.equal(7); + + expect(reqBidsConfigObj.ortb2Fragments.global.user.data[0].name).to.equal( + 'sirdata.com' + ); + expect(reqBidsConfigObj.ortb2Fragments.global.user.data[0].segment).to.eql([ + {id: '123'}, + {id: '234'} + ]); + expect(reqBidsConfigObj.ortb2Fragments.global.user.data[0].ext.segtax).to.equal(4); }); }); }); diff --git a/test/spec/modules/sizeMappingV2_spec.js b/test/spec/modules/sizeMappingV2_spec.js index 27fd7a33c14..16c1527a3ad 100644 --- a/test/spec/modules/sizeMappingV2_spec.js +++ b/test/spec/modules/sizeMappingV2_spec.js @@ -445,6 +445,10 @@ describe('sizeMappingV2', function () { }); describe('video mediaTypes checks', function () { + if (!FEATURES.VIDEO) { + return; + } + beforeEach(function () { sinon.spy(adUnitSetupChecks, 'validateVideoMediaType'); }); diff --git a/test/spec/modules/smaatoBidAdapter_spec.js b/test/spec/modules/smaatoBidAdapter_spec.js index c7dbf1363e7..7f727d5d9e3 100644 --- a/test/spec/modules/smaatoBidAdapter_spec.js +++ b/test/spec/modules/smaatoBidAdapter_spec.js @@ -297,6 +297,21 @@ describe('smaatoBidAdapterTest', () => { expect(req.regs.ext.us_privacy).to.equal('uspConsentString'); }); + it('sends gpp', () => { + const ortb2 = { + regs: { + gpp: 'gppString', + gpp_sid: [7] + } + }; + + const reqs = spec.buildRequests([singleBannerBidRequest], {...defaultBidderRequest, ortb2}); + + const req = extractPayloadOfFirstAndOnlyRequest(reqs); + expect(req.regs.ext.gpp).to.eql('gppString'); + expect(req.regs.ext.gpp_sid).to.eql([7]); + }); + it('sends no schain if no schain exists', () => { const reqs = spec.buildRequests([singleBannerBidRequest], defaultBidderRequest); @@ -341,6 +356,13 @@ describe('smaatoBidAdapterTest', () => { keywords: 'a,b', gender: 'M', yob: 1984 + }, + device: { + ifa: 'ifa', + geo: { + lat: 53.5488, + lon: 9.9872 + } } }; @@ -353,6 +375,9 @@ describe('smaatoBidAdapterTest', () => { expect(req.user.ext.consent).to.equal(CONSENT_STRING); expect(req.site.keywords).to.eql('power tools,drills'); expect(req.site.publisher.id).to.equal('publisherId'); + expect(req.device.ifa).to.equal('ifa'); + expect(req.device.geo.lat).to.equal(53.5488); + expect(req.device.geo.lon).to.equal(9.9872); }); it('has no user ids', () => { @@ -992,6 +1017,28 @@ describe('smaatoBidAdapterTest', () => { expect(req.device.ifa).to.equal(DEVICE_ID); }); + it('when geo and ifa info present and fpd present, then prefer fpd', () => { + const ortb2 = { + device: { + ifa: 'ifa', + geo: { + lat: 53.5488, + lon: 9.9872 + } + } + }; + + const inAppBidRequest = utils.deepClone(inAppBidRequestWithoutAppParams); + inAppBidRequest.params.app = {ifa: DEVICE_ID, geo: LOCATION}; + + const reqs = spec.buildRequests([inAppBidRequest], {...defaultBidderRequest, ortb2}); + + const req = extractPayloadOfFirstAndOnlyRequest(reqs); + expect(req.device.geo.lat).to.equal(53.5488); + expect(req.device.geo.lon).to.equal(9.9872); + expect(req.device.ifa).to.equal('ifa'); + }); + it('when ifa is present but geo is missing, then add only ifa to device object', () => { const inAppBidRequest = utils.deepClone(inAppBidRequestWithoutAppParams); inAppBidRequest.params.app = {ifa: DEVICE_ID}; diff --git a/test/spec/modules/smartadserverBidAdapter_spec.js b/test/spec/modules/smartadserverBidAdapter_spec.js index db61983c9c9..504ff978e9e 100644 --- a/test/spec/modules/smartadserverBidAdapter_spec.js +++ b/test/spec/modules/smartadserverBidAdapter_spec.js @@ -1,4 +1,5 @@ import { expect } from 'chai'; +import { BANNER, VIDEO } from 'src/mediaTypes.js'; import { config } from 'src/config.js'; import { spec } from 'modules/smartadserverBidAdapter.js'; @@ -56,18 +57,80 @@ describe('Smart bid adapter tests', function () { }, requestId: 'efgh5678', transactionId: 'zsfgzzg', - userId: { - britepoolid: '1111', - criteoId: '1111', - digitrustid: { data: { id: 'DTID', keyv: 4, privacy: { optout: false }, producer: 'ABC', version: 2 } }, - id5id: { uid: '1111' }, - idl_env: '1111', - lipbid: '1111', - parrableid: 'eidVersion.encryptionKeyReference.encryptedValue', - pubcid: '1111', - tdid: '1111', - netId: 'fH5A3n2O8_CZZyPoJVD-eabc6ECb7jhxCicsds7qSg', - } + userIdAsEids: [ + { + 'source': 'pubcid.org', + 'uids': [ + { + 'atype': 1, + 'id': '1111' + } + ] + }, + { + 'source': 'britepoolid', + 'uids': [ + { + 'atype': 1, + 'id': '1111' + } + ] + }, + { + 'source': 'id5id', + 'uids': [ + { + 'atype': 1, + 'id': '1111' + } + ] + }, + { + 'source': 'idl_env', + 'uids': [ + { + 'atype': 1, + 'id': '1111' + } + ] + }, + { + 'source': 'lipbid', + 'uids': [ + { + 'atype': 1, + 'id': '1111' + } + ] + }, + { + 'source': 'parrableid', + 'uids': [ + { + 'atype': 1, + 'id': 'eidVersion.encryptionKeyReference.encryptedValue' + } + ] + }, + { + 'source': 'tdid', + 'uids': [ + { + 'atype': 1, + 'id': '1111' + } + ] + }, + { + 'source': 'netId', + 'uids': [ + { + 'atype': 1, + 'id': 'fH5A3n2O8_CZZyPoJVD-eabc6ECb7jhxCicsds7qSg' + } + ] + } + ] }]; // Default params without optional ones @@ -394,7 +457,6 @@ describe('Smart bid adapter tests', function () { afterEach(function () { config.setConfig({ ortb2: undefined }); config.resetConfig(); - $$PREBID_GLOBAL$$.requestBids.removeAll(); }); it('Verify build request with GDPR', function () { @@ -443,10 +505,33 @@ describe('Smart bid adapter tests', function () { }); }); + describe('GPP', function () { + it('should be added to payload when gppConsent available in bidder request', function () { + const options = { + gppConsent: { + gppString: 'some-gpp-string', + applicableSections: [3, 5] + } + }; + const request = spec.buildRequests(DEFAULT_PARAMS_WO_OPTIONAL, options); + const payload = JSON.parse(request[0].data); + + expect(payload).to.have.property('gpp').and.to.equal(options.gppConsent.gppString); + expect(payload).to.have.property('gpp_sid').and.to.be.an('array'); + expect(payload.gpp_sid).to.have.lengthOf(2).and.to.deep.equal(options.gppConsent.applicableSections); + }); + + it('should be undefined on payload when gppConsent unavailable in bidder request', function () { + const request = spec.buildRequests(DEFAULT_PARAMS_WO_OPTIONAL, {}); + const payload = JSON.parse(request[0].data); + + expect(payload.gpp).to.be.undefined; + }); + }); + describe('ccpa/us privacy tests', function () { afterEach(function () { config.resetConfig(); - $$PREBID_GLOBAL$$.requestBids.removeAll(); }); it('Verify build request with us privacy', function () { @@ -475,7 +560,6 @@ describe('Smart bid adapter tests', function () { describe('Instream video tests', function () { afterEach(function () { config.resetConfig(); - $$PREBID_GLOBAL$$.requestBids.removeAll(); }); const INSTREAM_DEFAULT_PARAMS = [{ @@ -746,7 +830,6 @@ describe('Smart bid adapter tests', function () { describe('Outstream video tests', function () { afterEach(function () { config.resetConfig(); - $$PREBID_GLOBAL$$.requestBids.removeAll(); }); const OUTSTREAM_DEFAULT_PARAMS = [{ @@ -1055,6 +1138,17 @@ describe('Smart bid adapter tests', function () { }); describe('Floors module', function () { + const getFloor = (bid) => { + switch (bid.mediaType) { + case BANNER: + return { currency: 'USD', floor: 1.93 }; + case VIDEO: + return { currency: 'USD', floor: 2.72 }; + default: + return {}; + } + }; + it('should include floor from bid params', function() { const bidRequest = JSON.parse((spec.buildRequests(DEFAULT_PARAMS))[0].data); expect(bidRequest.bidfloor).to.deep.equal(DEFAULT_PARAMS[0].params.bidfloor); @@ -1094,12 +1188,91 @@ describe('Smart bid adapter tests', function () { const floor = spec.getBidFloor(bidRequest, null); expect(floor).to.deep.equal(0); }); + + it('should take floor from bidder params over ad unit', function() { + const bidRequest = [{ + mediaTypes: { + banner: { + sizes: [[300, 250]] + } + }, + getFloor, + params: { siteId: 1234, pageId: 678, formatId: 73, bidfloor: 1.25 } + }]; + + const request = spec.buildRequests(bidRequest); + const requestContent = JSON.parse(request[0].data); + + expect(requestContent).to.have.property('bidfloor').and.to.equal(1.25); + }); + + it('should take floor from banner ad unit', function() { + const bidRequest = [{ + mediaTypes: { + banner: { + sizes: [[300, 250], [300, 600]] + } + }, + getFloor, + params: { siteId: 1234, pageId: 678, formatId: 73 } + }]; + + const request = spec.buildRequests(bidRequest); + const requestContent = JSON.parse(request[0].data); + + expect(requestContent).to.have.property('bidfloor').and.to.equal(1.93); + }); + + it('should take floor from video ad unit', function() { + const bidRequest = [{ + mediaTypes: { + video: { + context: 'outstream', + playerSize: [[640, 480]] + } + }, + getFloor, + params: { siteId: 1234, pageId: 678, formatId: 73 } + }]; + + const request = spec.buildRequests(bidRequest); + const requestContent = JSON.parse(request[0].data); + + expect(requestContent).to.have.property('bidfloor').and.to.equal(2.72); + }); + + it('should take floor from multiple media type ad unit', function() { + const bidRequest = [{ + mediaTypes: { + banner: { + sizes: [[300, 600]] + }, + video: { + context: 'outstream', + playerSize: [[640, 480]] + } + }, + getFloor, + params: { siteId: 1234, pageId: 678, formatId: 73 } + }]; + + const requests = spec.buildRequests(bidRequest); + expect(requests).to.have.lengthOf(2); + + const requestContents = requests.map(r => JSON.parse(r.data)); + const videoRequest = requestContents.filter(r => r.videoData)[0]; + expect(videoRequest).to.not.equal(null).and.to.not.be.undefined; + expect(videoRequest).to.have.property('bidfloor').and.to.equal(2.72); + + const bannerRequest = requestContents.filter(r => !r.videoData)[0]; + expect(bannerRequest).to.not.equal(null).and.to.not.be.undefined; + expect(bannerRequest).to.have.property('bidfloor').and.to.equal(1.93); + }); }); describe('Verify bid requests with multiple mediaTypes', function () { afterEach(function () { config.resetConfig(); - $$PREBID_GLOBAL$$.requestBids.removeAll(); }); var DEFAULT_PARAMS_MULTIPLE_MEDIA_TYPES = [{ diff --git a/test/spec/modules/smarthubBidAdapter_spec.js b/test/spec/modules/smarthubBidAdapter_spec.js index e1787dfe880..e01d0c72f6b 100644 --- a/test/spec/modules/smarthubBidAdapter_spec.js +++ b/test/spec/modules/smarthubBidAdapter_spec.js @@ -91,7 +91,8 @@ describe('SmartHubBidAdapter', function () { gdprConsent: 'COvFyGBOvFyGBAbAAAENAPCAAOAAAAAAAAAAAEEUACCKAAA.IFoEUQQgAIQwgIwQABAEAAAAOIAACAIAAAAQAIAgEAACEAAAAAgAQBAAAAAAAGBAAgAAAAAAAFAAECAAAgAAQARAEQAAAAAJAAIAAgAAAYQEAAAQmAgBC3ZAYzUw', refererInfo: { page: 'https://test.com' - } + }, + timeout: 500 }; describe('isBidRequestValid', function () { diff --git a/test/spec/modules/smartxBidAdapter_spec.js b/test/spec/modules/smartxBidAdapter_spec.js index 5bd08064c79..c3d0711632e 100644 --- a/test/spec/modules/smartxBidAdapter_spec.js +++ b/test/spec/modules/smartxBidAdapter_spec.js @@ -374,6 +374,16 @@ describe('The smartx adapter', function () { } }) }); + + it('should pass sitekey param', function () { + var request; + + bid.params.sitekey = 'foo' + + request = spec.buildRequests([bid], bidRequestObj)[0]; + + expect(request.data.site.content.ext.sitekey).to.equal('foo'); + }); }); describe('interpretResponse', function () { diff --git a/test/spec/modules/smartytechBidAdapter_spec.js b/test/spec/modules/smartytechBidAdapter_spec.js index 78503d7a6f0..6b3147859bf 100644 --- a/test/spec/modules/smartytechBidAdapter_spec.js +++ b/test/spec/modules/smartytechBidAdapter_spec.js @@ -19,20 +19,116 @@ describe('SmartyTechDSPAdapter: inherited functions', function () { describe('SmartyTechDSPAdapter: isBidRequestValid', function () { it('Invalid bid request. Should return false', function () { - const invalidBidFixture = { + const bidFixture = { params: { use_id: 13144375 } } - expect(spec.isBidRequestValid(invalidBidFixture)).to.be.false + + expect(spec.isBidRequestValid(bidFixture)).to.be.false }); it('Valid bid request. Should return true', function () { - const validBidFixture = { + const bidFixture = { params: { endpointId: 13144375 } } - expect(spec.isBidRequestValid(validBidFixture)).to.be.true + expect(spec.isBidRequestValid(bidFixture)).to.be.true + }); + + it('Invalid bid request. Check video block', function () { + const bidFixture = { + params: { + endpointId: 1 + }, + mediaTypes: { + video: {} + } + } + expect(spec.isBidRequestValid(bidFixture)).to.be.false + }); + + it('Invalid bid request. Check playerSize', function () { + const bidFixture = { + params: { + endpointId: 1 + }, + mediaTypes: { + video: { + playerSize: '300x250' + } + } + } + expect(spec.isBidRequestValid(bidFixture)).to.be.false + }); + + it('Invalid bid request. Check context', function () { + const bidFixture = { + params: { + endpointId: 1 + }, + mediaTypes: { + video: { + playerSize: [300, 250] + } + } + } + expect(spec.isBidRequestValid(bidFixture)).to.be.false + }); + + it('Valid bid request. valid video bid', function () { + const bidFixture = { + params: { + endpointId: 1 + }, + mediaTypes: { + video: { + playerSize: [300, 250], + context: 'instream' + } + } + } + expect(spec.isBidRequestValid(bidFixture)).to.be.true + }); + + it('Invalid bid request. Check banner block', function () { + const bidFixture = { + params: { + endpointId: 1 + }, + mediaTypes: { + banner: {} + } + } + expect(spec.isBidRequestValid(bidFixture)).to.be.false + }); + + it('Invalid bid request. Check banner sizes', function () { + const bidFixture = { + params: { + endpointId: 1 + }, + mediaTypes: { + banner: { + sizes: '300x250' + } + } + } + expect(spec.isBidRequestValid(bidFixture)).to.be.false + }); + + it('Valid bid request. valid banner bid', function () { + const bidFixture = { + params: { + endpointId: 1 + }, + mediaTypes: { + banner: { + sizes: [[300, 250]] + } + } + } + expect(spec.isBidRequestValid(bidFixture)).to.be.true }); }); @@ -42,16 +138,38 @@ function mockRandomSizeArray(len) { }); } -function mockBidRequestListData(size) { +function mockBidRequestListData(mediaType, size, customSizes) { return Array.apply(null, {length: size}).map((i, index) => { const id = Math.floor(Math.random() * 800) * (index + 1); + let mediaTypes; + let params = { + endpointId: id + } + + if (mediaType == 'video') { + mediaTypes = { + video: { + playerSize: mockRandomSizeArray(1), + context: 'instream' + }, + } + } else { + mediaTypes = { + banner: { + sizes: mockRandomSizeArray(index + 1) + } + } + } + + if (customSizes === undefined || customSizes.length > 0) { + params.sizes = customSizes + } + return { adUnitCode: `adUnitCode-${id}`, - sizes: mockRandomSizeArray(index + 1), + mediaTypes: mediaTypes, bidId: `bidId-${id}`, - params: { - endpointId: id - } + params: params } }); } @@ -66,18 +184,27 @@ function mockRefererData() { function mockResponseData(requestData) { let data = {} - requestData.data.forEach((request, index) => { - const sizeArrayIndex = Math.floor(Math.random() * (request.sizes.length - 1)); const rndIndex = Math.floor(Math.random() * 800); + let width, height, mediaType; + if (request.video !== undefined) { + width = request.video.playerSize[0][0]; + height = request.video.playerSize[0][1]; + mediaType = 'video'; + } else { + width = request.banner.sizes[0][0]; + height = request.banner.sizes[0][1]; + mediaType = 'banner'; + } data[request.adUnitCode] = { ad: `ad-${rndIndex}`, - width: request.sizes[sizeArrayIndex][0], - height: request.sizes[sizeArrayIndex][1], + width: width, + height: height, creativeId: `creative-id-${index}`, cpm: Math.floor(Math.random() * 100), - currency: `UAH-${rndIndex}` + currency: `UAH-${rndIndex}`, + mediaType: mediaType }; }); return { @@ -89,7 +216,7 @@ describe('SmartyTechDSPAdapter: buildRequests', () => { let mockBidRequest; let mockReferer; beforeEach(() => { - mockBidRequest = mockBidRequestListData(8); + mockBidRequest = mockBidRequestListData('banner', 8, []); mockReferer = mockRefererData(); }); it('has return data', () => { @@ -108,7 +235,7 @@ describe('SmartyTechDSPAdapter: buildRequests', () => { const data = spec.buildRequests(mockBidRequest, mockReferer).data; data.forEach((request, index) => { expect(request.adUnitCode).to.be.equal(mockBidRequest[index].adUnitCode); - expect(request.sizes).to.be.equal(mockBidRequest[index].sizes); + expect(request.banner).to.be.equal(mockBidRequest[index].mediaTypes.banner); expect(request.bidId).to.be.equal(mockBidRequest[index].bidId); expect(request.endpointId).to.be.equal(mockBidRequest[index].params.endpointId); expect(request.referer).to.be.equal(mockReferer.refererInfo.page); @@ -116,13 +243,45 @@ describe('SmartyTechDSPAdapter: buildRequests', () => { }); }); +describe('SmartyTechDSPAdapter: buildRequests banner custom size', () => { + let mockBidRequest; + let mockReferer; + beforeEach(() => { + mockBidRequest = mockBidRequestListData('banner', 8, [[300, 600]]); + mockReferer = mockRefererData(); + }); + + it('correct request data', () => { + const data = spec.buildRequests(mockBidRequest, mockReferer).data; + data.forEach((request, index) => { + expect(request.banner.sizes).to.be.equal(mockBidRequest[index].params.sizes); + }) + }); +}); + +describe('SmartyTechDSPAdapter: buildRequests video custom size', () => { + let mockBidRequest; + let mockReferer; + beforeEach(() => { + mockBidRequest = mockBidRequestListData('video', 8, [[300, 300], [250, 250]]); + mockReferer = mockRefererData(); + }); + + it('correct request data', () => { + const data = spec.buildRequests(mockBidRequest, mockReferer).data; + data.forEach((request, index) => { + expect(request.video.sizes).to.be.equal(mockBidRequest[index].params.sizes); + }) + }); +}); + describe('SmartyTechDSPAdapter: interpretResponse', () => { let mockBidRequest; let mockReferer; let request; let mockResponse; beforeEach(() => { - const brData = mockBidRequestListData(2); + const brData = mockBidRequestListData('banner', 2, []); mockReferer = mockRefererData(); request = spec.buildRequests(brData, mockReferer); mockBidRequest = { @@ -157,6 +316,42 @@ describe('SmartyTechDSPAdapter: interpretResponse', () => { expect(responseItem.requestId).to.be.equal(mockBidRequest.data[index].bidId); expect(responseItem.width).to.be.equal(mockResponse.body[keys[index]].width); expect(responseItem.height).to.be.equal(mockResponse.body[keys[index]].height); + expect(responseItem.mediaType).to.be.equal(mockResponse.body[keys[index]].mediaType); + }); + }); +}); + +describe('SmartyTechDSPAdapter: interpretResponse video', () => { + let mockBidRequest; + let mockReferer; + let request; + let mockResponse; + beforeEach(() => { + const brData = mockBidRequestListData('video', 2, []); + mockReferer = mockRefererData(); + request = spec.buildRequests(brData, mockReferer); + mockBidRequest = { + data: brData + } + mockResponse = mockResponseData(request); + }); + + it('interpretResponse: convert to correct data', () => { + const keys = Object.keys(mockResponse.body); + const data = spec.interpretResponse(mockResponse, mockBidRequest); + + data.forEach((responseItem, index) => { + expect(responseItem.ad).to.be.equal(mockResponse.body[keys[index]].ad); + expect(responseItem.cpm).to.be.equal(mockResponse.body[keys[index]].cpm); + expect(responseItem.creativeId).to.be.equal(mockResponse.body[keys[index]].creativeId); + expect(responseItem.currency).to.be.equal(mockResponse.body[keys[index]].currency); + expect(responseItem.netRevenue).to.be.true; + expect(responseItem.ttl).to.be.equal(60); + expect(responseItem.requestId).to.be.equal(mockBidRequest.data[index].bidId); + expect(responseItem.width).to.be.equal(mockResponse.body[keys[index]].width); + expect(responseItem.height).to.be.equal(mockResponse.body[keys[index]].height); + expect(responseItem.mediaType).to.be.equal(mockResponse.body[keys[index]].mediaType); + expect(responseItem.vastXml).to.be.equal(mockResponse.body[keys[index]].ad); }); }); }); diff --git a/test/spec/modules/smilewantedBidAdapter_spec.js b/test/spec/modules/smilewantedBidAdapter_spec.js index b9a816cf3d5..44d2c7c6507 100644 --- a/test/spec/modules/smilewantedBidAdapter_spec.js +++ b/test/spec/modules/smilewantedBidAdapter_spec.js @@ -1,9 +1,6 @@ import { expect } from 'chai'; import { spec } from 'modules/smilewantedBidAdapter.js'; -import { newBidder } from 'src/adapters/bidderFactory.js'; import { config } from 'src/config.js'; -import * as utils from 'src/utils.js'; -import { requestBidsHook } from 'modules/consentManagement.js'; const DISPLAY_REQUEST = [{ adUnitCode: 'sw_300x250', @@ -20,6 +17,37 @@ const DISPLAY_REQUEST = [{ transactionId: 'trans_abcd1234' }]; +const DISPLAY_REQUEST_WITH_EIDS = [{ + adUnitCode: 'sw_300x250', + bidId: '12345', + sizes: [ + [300, 250], + [300, 200] + ], + bidder: 'smilewanted', + params: { + zoneId: 1 + }, + requestId: 'request_abcd1234', + transactionId: 'trans_abcd1234', + userIdAsEids: [{ + source: 'pubcid.org', + uids: [{ + id: 'some-random-id-value', + atype: 1 + }] + }, { + source: 'adserver.org', + uids: [{ + id: 'some-random-id-value', + atype: 1, + ext: { + rtiPartner: 'TDID' + } + }] + }] +}]; + const DISPLAY_REQUEST_WITH_POSITION_TYPE = [{ adUnitCode: 'sw_300x250', bidId: '12345', @@ -170,6 +198,29 @@ describe('smilewantedBidAdapterTests', function () { expect(requestContent).to.have.property('pageDomain').and.to.equal('https://localhost/Prebid.js/integrationExamples/gpt/hello_world.html'); }); + it('Verify external ids in request and ids found', function () { + config.setConfig({ + 'currency': { + 'adServerCurrency': 'EUR' + } + }); + const request = spec.buildRequests(DISPLAY_REQUEST_WITH_EIDS, {}); + const requestContent = JSON.parse(request[0].data); + + expect(requestContent).to.have.property('eids'); + expect(requestContent.eids).to.not.equal(null).and.to.not.be.undefined; + expect(requestContent.eids.length).to.greaterThan(0); + for (let index in requestContent.eids) { + let eid = requestContent.eids[index]; + expect(eid.source).to.not.equal(null).and.to.not.be.undefined; + expect(eid.uids).to.not.equal(null).and.to.not.be.undefined; + for (let uidsIndex in eid.uids) { + let uid = eid.uids[uidsIndex]; + expect(uid.id).to.not.equal(null).and.to.not.be.undefined; + } + } + }); + describe('gdpr tests', function () { afterEach(function () { config.resetConfig(); diff --git a/test/spec/modules/sonobiBidAdapter_spec.js b/test/spec/modules/sonobiBidAdapter_spec.js index 7803cfff394..b9bd0dc4d9f 100644 --- a/test/spec/modules/sonobiBidAdapter_spec.js +++ b/test/spec/modules/sonobiBidAdapter_spec.js @@ -238,6 +238,13 @@ describe('SonobiBidAdapter', function () { }); describe('.buildRequests', function () { + before(function () { + $$PREBID_GLOBAL$$.bidderSettings = { + sonobi: { + storageAllowed: true + } + }; + }); let sandbox; beforeEach(function () { sinon.stub(userSync, 'canBidderRegisterSync'); @@ -287,6 +294,7 @@ describe('SonobiBidAdapter', function () { }, mediaTypes: { video: { + sizes: [[300, 250], [300, 600]], context: 'outstream' } } @@ -331,7 +339,7 @@ describe('SonobiBidAdapter', function () { }]; let keyMakerData = { - '30b31c1838de1f': '1a2b3c4d5e6f1a2b3c4d||f=1.25,gpid=/123123/gpt_publisher/adunit-code-1,c=v,', + '30b31c1838de1f': '1a2b3c4d5e6f1a2b3c4d|300x250,300x600|f=1.25,gpid=/123123/gpt_publisher/adunit-code-1,c=v,', '30b31c1838de1d': '1a2b3c4d5e6f1a2b3c4e|300x250,300x600|f=0.42,gpid=/123123/gpt_publisher/adunit-code-3,c=d,', '/7780971/sparks_prebid_LB|30b31c1838de1e': '300x250,300x600|gpid=/7780971/sparks_prebid_LB,c=d,', }; @@ -370,7 +378,7 @@ describe('SonobiBidAdapter', function () { } } }; - const bidRequests = spec.buildRequests(bidRequest, {...bidderRequests, ortb2}); + const bidRequests = spec.buildRequests(bidRequest, { ...bidderRequests, ortb2 }); expect(bidRequests.data.fpd).to.equal(JSON.stringify(ortb2)); }); @@ -388,6 +396,10 @@ describe('SonobiBidAdapter', function () { expect(bidRequests.data.coppa).to.equal(0); }); + it('should have storageAllowed set to true', function () { + expect($$PREBID_GLOBAL$$.bidderSettings.sonobi.storageAllowed).to.be.true; + }); + it('should return a properly formatted request', function () { const bidRequests = spec.buildRequests(bidRequest, bidderRequests) const bidRequestsPageViewID = spec.buildRequests(bidRequest, bidderRequests) @@ -397,6 +409,8 @@ describe('SonobiBidAdapter', function () { expect(bidRequests.data.ref).not.to.be.empty expect(bidRequests.data.s).not.to.be.empty expect(bidRequests.data.pv).to.equal(bidRequestsPageViewID.data.pv) + expect(JSON.parse(bidRequests.data.iqid).pcid).to.match(/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/) + expect(JSON.parse(bidRequests.data.iqid).pcidDate).to.match(/^[0-9]{13}$/) expect(bidRequests.data.hfa).to.not.exist expect(bidRequests.bidderRequests).to.eql(bidRequest); expect(bidRequests.data.ref).to.equal('overrides_top_window_location'); @@ -539,7 +553,7 @@ describe('SonobiBidAdapter', function () { ]); }); - it('should return a properly formatted request with userid as a JSON-encoded set of User ID results', function () { + it('should return a properly formatted request with the userid value omitted when the userId object is present on the bidRequest. ', function () { bidRequest[0].userId = { 'pubcid': 'abcd-efg-0101', 'tdid': 'td-abcd-efg-0101', 'id5id': { 'uid': 'ID5-ZHMOrVeUVTUKgrZ-a2YGxeh5eS_pLzHCQGYOEAiTBQ', 'ext': { 'linkType': 2 } } }; bidRequest[1].userId = { 'pubcid': 'abcd-efg-0101', 'tdid': 'td-abcd-efg-0101', 'id5id': { 'uid': 'ID5-ZHMOrVeUVTUKgrZ-a2YGxeh5eS_pLzHCQGYOEAiTBQ', 'ext': { 'linkType': 2 } } }; const bidRequests = spec.buildRequests(bidRequest, bidderRequests); @@ -547,29 +561,7 @@ describe('SonobiBidAdapter', function () { expect(bidRequests.method).to.equal('GET'); expect(bidRequests.data.ref).not.to.be.empty; expect(bidRequests.data.s).not.to.be.empty; - expect(JSON.parse(bidRequests.data.userid)).to.eql({ 'pubcid': 'abcd-efg-0101', 'tdid': 'td-abcd-efg-0101', 'id5id': 'ID5-ZHMOrVeUVTUKgrZ-a2YGxeh5eS_pLzHCQGYOEAiTBQ' }); - }); - - it('should return a properly formatted request with userid omitted if there are no userIds', function () { - bidRequest[0].userId = {}; - bidRequest[1].userId = {}; - const bidRequests = spec.buildRequests(bidRequest, bidderRequests); - expect(bidRequests.url).to.equal('https://apex.go.sonobi.com/trinity.json'); - expect(bidRequests.method).to.equal('GET'); - expect(bidRequests.data.ref).not.to.be.empty; - expect(bidRequests.data.s).not.to.be.empty; - expect(bidRequests.data.userid).to.equal(undefined); - }); - - it('should return a properly formatted request with userid omitted', function () { - bidRequest[0].userId = undefined; - bidRequest[1].userId = undefined; - const bidRequests = spec.buildRequests(bidRequest, bidderRequests); - expect(bidRequests.url).to.equal('https://apex.go.sonobi.com/trinity.json'); - expect(bidRequests.method).to.equal('GET'); - expect(bidRequests.data.ref).not.to.be.empty; - expect(bidRequests.data.s).not.to.be.empty; - expect(bidRequests.data.userid).to.equal(undefined); + expect(bidRequests.data.userid).to.be.undefined; }); it('should return a properly formatted request with keywrods included as a csv of strings', function () { diff --git a/test/spec/modules/sovrnBidAdapter_spec.js b/test/spec/modules/sovrnBidAdapter_spec.js index 02a3d86b29a..0fedd4b9f49 100644 --- a/test/spec/modules/sovrnBidAdapter_spec.js +++ b/test/spec/modules/sovrnBidAdapter_spec.js @@ -270,14 +270,33 @@ describe('sovrnBidAdapter', function() { expect(data.source.ext.schain.nodes.length).to.equal(1) }) - it('should add eds to the bid request', function() { + it('should add eids to the bid request', function() { const criteoIdRequest = { ...baseBidRequest, - userId: { - criteoId: 'A_CRITEO_ID', - tdid: 'SOMESORTOFID', - } - } + userIdAsEids: [ + { + source: 'criteo.com', + uids: [ + { + atype: 1, + id: 'A_CRITEO_ID' + } + ] + }, + { + source: 'adserver.org', + uids: [ + { + atype: 1, + ext: { + rtiPartner: 'TDID' + }, + id: 'SOMESORTOFID' + } + ] + } + ] + }; const criteoIdRequests = [criteoIdRequest, baseBidRequest] const ext = JSON.parse(spec.buildRequests(criteoIdRequests, baseBidderRequest).data).user.ext const firstEID = ext.eids[0] diff --git a/test/spec/modules/sspBCBidAdapter_spec.js b/test/spec/modules/sspBCBidAdapter_spec.js index d182b9db24c..8c9bbe3b336 100644 --- a/test/spec/modules/sspBCBidAdapter_spec.js +++ b/test/spec/modules/sspBCBidAdapter_spec.js @@ -557,6 +557,7 @@ describe('SSPBC adapter', function () { const nativeAssets = payloadNative.imp && payloadNative.imp[0].native.request; expect(payloadNative.imp.length).to.equal(1); + expect(nativeAssets).to.contain('{"id":0,"required":true,"title":{"len":80}}'); expect(nativeAssets).to.contain('{"id":2,"required":true,"img":{"type":1,"w":50,"h":50}}'); expect(nativeAssets).to.contain('{"id":3,"required":true,"img":{"type":3,"w":150,"h":50}}'); @@ -610,14 +611,14 @@ describe('SSPBC adapter', function () { expect(result.length).to.equal(bids.length); expect(resultSingle.length).to.equal(1); - expect(resultSingle[0]).to.have.keys('ad', 'cpm', 'width', 'height', 'bidderCode', 'mediaType', 'meta', 'requestId', 'creativeId', 'currency', 'netRevenue', 'ttl'); + expect(resultSingle[0]).to.have.keys('ad', 'cpm', 'width', 'height', 'bidderCode', 'mediaType', 'meta', 'requestId', 'creativeId', 'currency', 'netRevenue', 'ttl', 'vurls'); }); it('should create bid from OneCode (parameter-less) request, if response contains siteId', function () { let resultOneCode = spec.interpretResponse(serverResponseOneCode, requestOneCode); expect(resultOneCode.length).to.equal(1); - expect(resultOneCode[0]).to.have.keys('ad', 'cpm', 'width', 'height', 'bidderCode', 'mediaType', 'meta', 'requestId', 'creativeId', 'currency', 'netRevenue', 'ttl'); + expect(resultOneCode[0]).to.have.keys('ad', 'cpm', 'width', 'height', 'bidderCode', 'mediaType', 'meta', 'requestId', 'creativeId', 'currency', 'netRevenue', 'ttl', 'vurls'); }); it('should not create bid from OneCode (parameter-less) request, if response does not contain siteId', function () { @@ -648,7 +649,7 @@ describe('SSPBC adapter', function () { expect(resultVideo.length).to.equal(1); let videoBid = resultVideo[0]; - expect(videoBid).to.have.keys('adType', 'bidderCode', 'cpm', 'creativeId', 'currency', 'width', 'height', 'meta', 'mediaType', 'netRevenue', 'requestId', 'ttl', 'vastContent', 'vastXml', 'vastUrl'); + expect(videoBid).to.have.keys('adType', 'bidderCode', 'cpm', 'creativeId', 'currency', 'width', 'height', 'meta', 'mediaType', 'netRevenue', 'requestId', 'ttl', 'vastContent', 'vastXml', 'vastUrl', 'vurls'); expect(videoBid.adType).to.equal('instream'); expect(videoBid.mediaType).to.equal('video'); expect(videoBid.vastXml).to.match(/^<\?xml.*<\/VAST>$/); @@ -662,8 +663,8 @@ describe('SSPBC adapter', function () { expect(resultNative.length).to.equal(1); let nativeBid = resultNative[0]; - expect(nativeBid).to.have.keys('bidderCode', 'cpm', 'creativeId', 'currency', 'width', 'height', 'meta', 'mediaType', 'netRevenue', 'requestId', 'ttl', 'native'); - expect(nativeBid.native).to.have.keys('image', 'icon', 'title', 'sponsoredBy', 'body', 'clickUrl', 'impressionTrackers', 'javascriptTrackers'); + expect(nativeBid).to.have.keys('bidderCode', 'cpm', 'creativeId', 'currency', 'width', 'height', 'meta', 'mediaType', 'netRevenue', 'requestId', 'ttl', 'native', 'vurls'); + expect(nativeBid.native).to.have.keys('image', 'icon', 'title', 'sponsoredBy', 'body', 'clickUrl', 'impressionTrackers', 'javascriptTrackers', 'clickTrackers'); }); }); diff --git a/test/spec/modules/stroeerCoreBidAdapter_spec.js b/test/spec/modules/stroeerCoreBidAdapter_spec.js index 6489d8ece7e..c2806317ee6 100644 --- a/test/spec/modules/stroeerCoreBidAdapter_spec.js +++ b/test/spec/modules/stroeerCoreBidAdapter_spec.js @@ -636,10 +636,13 @@ describe('stroeerCore bid adapter', function () { } }); - const gdprSamples = [{consentString: 'RG9ua2V5IEtvbmc=', gdprApplies: true}, { - consentString: 'UGluZyBQb25n', - gdprApplies: false - }]; + const gdprSamples = [ + {consentString: 'RG9ua2V5IEtvbmc=', gdprApplies: true}, + {consentString: 'UGluZyBQb25n', gdprApplies: false}, + {consentString: undefined, gdprApplies: true}, + {consentString: undefined, gdprApplies: false}, + {consentString: undefined, gdprApplies: undefined}, + ]; gdprSamples.forEach((sample) => { it(`should add GDPR info ${JSON.stringify(sample)} when provided`, () => { const bidReq = buildBidderRequest(); @@ -653,22 +656,14 @@ describe('stroeerCore bid adapter', function () { }); }); - 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; + it(`should not add GDPR info when not provided`, () => { + const bidReq = buildBidderRequest(); - const serverRequestInfo = spec.buildRequests(bidReq.bids, bidReq); + delete bidReq.gdprConsent; - const actualGdpr = serverRequestInfo.data.gdpr; - assert.isUndefined(actualGdpr); - }); + const serverRequestInfo = spec.buildRequests(bidReq.bids, bidReq); + + assert.notProperty(serverRequestInfo.data, 'gdpr'); }); it('should be able to build without third party user id data', () => { diff --git a/test/spec/modules/stvBidAdapter_spec.js b/test/spec/modules/stvBidAdapter_spec.js new file mode 100644 index 00000000000..d095fd3cf55 --- /dev/null +++ b/test/spec/modules/stvBidAdapter_spec.js @@ -0,0 +1,402 @@ +import { expect } from 'chai'; +import { spec } from 'modules/stvBidAdapter.js'; +import { newBidder } from 'src/adapters/bidderFactory.js'; + +const ENDPOINT_URL = 'https://ads.smartstream.tv/r/'; +const ENDPOINT_URL_DEV = 'https://ads.smartstream.tv/r/'; + +describe('stvAdapter', function() { + const adapter = newBidder(spec); + + describe('isBidRequestValid', function() { + let bid = { + 'bidder': 'stv', + 'params': { + 'placement': '6682', + 'floorprice': 1000000, + 'bcat': 'IAB2,IAB4', + 'dvt': 'desktop' + }, + 'sizes': [ + [300, 250] + ], + 'bidId': '30b31c1838de1e', + 'bidderRequestId': '22edbae2733bf6', + 'auctionId': '1d1a030790a475' + }; + + it('should return true when required params found', function() { + expect(spec.isBidRequestValid(bid)).to.equal(true); + }); + + it('should return false when required params are not passed', function() { + let bid = Object.assign({}, bid); + delete bid.params; + bid.params = { + 'someIncorrectParam': 0 + }; + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + }); + + describe('buildRequests', function() { + let bidRequests = [ + // banner + { + 'bidder': 'stv', + 'params': { + 'placement': '6682', + 'floorprice': 1000000, + 'geo': { + 'country': 'DE' + }, + 'bcat': 'IAB2,IAB4', + 'dvt': 'desktop' + }, + 'sizes': [ + [300, 250] + ], + 'bidId': '30b31c1838de1e1', + 'bidderRequestId': '22edbae2733bf61', + 'auctionId': '1d1a030790a475', + 'adUnitCode': 'testDiv1', + }, + { + 'bidder': 'stv', + 'params': { + 'placement': '101', + 'devMode': true + }, + 'sizes': [ + [300, 250] + ], + 'bidId': '30b31c1838de1e2', + 'bidderRequestId': '22edbae2733bf62', + 'auctionId': '1d1a030790a476' + }, { + 'bidder': 'stv', + 'params': { + 'placement': '6682', + 'floorprice': 1000000, + 'geo': { + 'country': 'DE' + }, + 'bcat': 'IAB2,IAB4', + 'dvt': 'desktop' + }, + 'sizes': [ + [300, 250] + ], + 'bidId': '30b31c1838de1e3', + 'bidderRequestId': '22edbae2733bf69', + 'auctionId': '1d1a030790a477', + 'adUnitCode': 'testDiv2' + }, + // video + { + 'bidder': 'stv', + 'params': { + 'placement': '101', + 'devMode': true, + 'max_duration': 20, + }, + 'mediaTypes': { + 'video': { + 'playerSize': [640, 480], + 'context': 'instream' + }, + }, + + 'bidId': '30b31c1838de1e4', + 'bidderRequestId': '22edbae2733bf67', + 'auctionId': '1d1a030790a478', + 'adUnitCode': 'testDiv3' + }, + { + 'bidder': 'stv', + 'params': { + 'placement': '101', + 'devMode': true, + }, + 'mediaTypes': { + 'video': { + 'playerSize': [640, 480], + 'context': 'instream', + 'maxduration': 40, + } + }, + 'bidId': '30b31c1838de1e41', + 'bidderRequestId': '22edbae2733bf67', + 'auctionId': '1d1a030790a478', + 'adUnitCode': 'testDiv4' + }, + { + 'bidder': 'stv', + 'params': { + 'placement': '101', + 'devMode': true, + 'max_duration': 20, + }, + 'mediaTypes': { + 'video': { + 'playerSize': [640, 480], + 'context': 'instream', + 'maxduration': 40, + } + }, + 'bidId': '30b31c1838de1e41', + 'bidderRequestId': '22edbae2733bf67', + 'auctionId': '1d1a030790a478', + 'adUnitCode': 'testDiv4' + } + + ]; + + // With gdprConsent + var bidderRequest = { + refererInfo: { + referer: 'some_referrer.net' + }, + gdprConsent: { + consentString: 'BOJ/P2HOJ/P2HABABMAAAAAZ+A==', + vendorData: { someData: 'value' }, + gdprApplies: true + } + }; + + var request1 = spec.buildRequests([bidRequests[0]], bidderRequest)[0]; + it('sends bid request 1 to our endpoint via GET', function() { + expect(request1.method).to.equal('GET'); + expect(request1.url).to.equal(ENDPOINT_URL); + let data = request1.data.replace(/rnd=\d+\&/g, '').replace(/ref=.*\&bid/g, 'bid').replace(/pbver=.*?&/g, 'pbver=test&'); + expect(data).to.equal('_f=html&alternative=prebid_js&_ps=6682&srw=300&srh=250&idt=100&bid_id=30b31c1838de1e1&pbver=test&pfilter%5Bfloorprice%5D=1000000&pfilter%5Bgeo%5D%5Bcountry%5D=DE&gdpr_consent=BOJ%2FP2HOJ%2FP2HABABMAAAAAZ%2BA%3D%3D&gdpr=true&bcat=IAB2%2CIAB4&dvt=desktop&pbcode=testDiv1&media_types%5Bbanner%5D=300x250'); + }); + + var request2 = spec.buildRequests([bidRequests[1]], bidderRequest)[0]; + it('sends bid request 2 endpoint via GET', function() { + expect(request2.method).to.equal('GET'); + expect(request2.url).to.equal(ENDPOINT_URL); + let data = request2.data.replace(/rnd=\d+\&/g, '').replace(/ref=.*\&bid/g, 'bid').replace(/pbver=.*?&/g, 'pbver=test&'); + expect(data).to.equal('_f=html&alternative=prebid_js&_ps=101&srw=300&srh=250&idt=100&bid_id=30b31c1838de1e2&pbver=test&gdpr_consent=BOJ%2FP2HOJ%2FP2HABABMAAAAAZ%2BA%3D%3D&gdpr=true&prebidDevMode=1&media_types%5Bbanner%5D=300x250'); + }); + + // Without gdprConsent + var bidderRequestWithoutGdpr = { + refererInfo: { + referer: 'some_referrer.net' + } + }; + var request3 = spec.buildRequests([bidRequests[2]], bidderRequestWithoutGdpr)[0]; + it('sends bid request 3 without gdprConsent to our endpoint via GET', function() { + expect(request3.method).to.equal('GET'); + expect(request3.url).to.equal(ENDPOINT_URL); + let data = request3.data.replace(/rnd=\d+\&/g, '').replace(/ref=.*\&bid/g, 'bid').replace(/pbver=.*?&/g, 'pbver=test&'); + expect(data).to.equal('_f=html&alternative=prebid_js&_ps=6682&srw=300&srh=250&idt=100&bid_id=30b31c1838de1e3&pbver=test&pfilter%5Bfloorprice%5D=1000000&pfilter%5Bgeo%5D%5Bcountry%5D=DE&bcat=IAB2%2CIAB4&dvt=desktop&pbcode=testDiv2&media_types%5Bbanner%5D=300x250'); + }); + + var request4 = spec.buildRequests([bidRequests[3]], bidderRequestWithoutGdpr)[0]; + it('sends bid request 4 (video) without gdprConsent endpoint via GET', function() { + expect(request4.method).to.equal('GET'); + expect(request4.url).to.equal(ENDPOINT_URL); + let data = request4.data.replace(/rnd=\d+\&/g, '').replace(/ref=.*\&bid/g, 'bid').replace(/pbver=.*?&/g, 'pbver=test&'); + expect(data).to.equal('_f=vast2&alternative=prebid_js&_ps=101&srw=640&srh=480&idt=100&bid_id=30b31c1838de1e4&pbver=test&pfilter%5Bmax_duration%5D=20&prebidDevMode=1&pbcode=testDiv3&media_types%5Bvideo%5D=640x480'); + }); + + var request5 = spec.buildRequests([bidRequests[4]], bidderRequestWithoutGdpr)[0]; + it('sends bid request 5 (video) to our endpoint via GET', function() { + expect(request5.method).to.equal('GET'); + expect(request5.url).to.equal(ENDPOINT_URL); + let data = request5.data.replace(/rnd=\d+\&/g, '').replace(/ref=.*\&bid/g, 'bid').replace(/pbver=.*?&/g, 'pbver=test&'); + expect(data).to.equal('_f=vast2&alternative=prebid_js&_ps=101&srw=640&srh=480&idt=100&bid_id=30b31c1838de1e41&pbver=test&pfilter%5Bmax_duration%5D=40&prebidDevMode=1&pbcode=testDiv4&media_types%5Bvideo%5D=640x480'); + }); + + var request6 = spec.buildRequests([bidRequests[5]], bidderRequestWithoutGdpr)[0]; + it('sends bid request 6 (video) to our endpoint via GET', function() { + expect(request6.method).to.equal('GET'); + expect(request6.url).to.equal(ENDPOINT_URL); + let data = request6.data.replace(/rnd=\d+\&/g, '').replace(/ref=.*\&bid/g, 'bid').replace(/pbver=.*?&/g, 'pbver=test&'); + expect(data).to.equal('_f=vast2&alternative=prebid_js&_ps=101&srw=640&srh=480&idt=100&bid_id=30b31c1838de1e41&pbver=test&pfilter%5Bmax_duration%5D=20&prebidDevMode=1&pbcode=testDiv4&media_types%5Bvideo%5D=640x480'); + }); + }); + + describe('interpretResponse', function() { + let serverResponse = { + 'body': { + 'cpm': 5000000, + 'crid': 100500, + 'width': '300', + 'height': '250', + 'type': 'sspHTML', + 'tag': '', + 'requestId': '220ed41385952a', + 'currency': 'EUR', + 'ttl': 60, + 'netRevenue': true, + 'zone': '6682', + 'adomain': ['bdomain'] + } + }; + let serverVideoResponse = { + 'body': { + 'cpm': 5000000, + 'crid': 100500, + 'width': '300', + 'height': '250', + 'vastXml': '{"reason":7001,"status":"accepted"}', + 'requestId': '220ed41385952a', + 'type': 'vast2', + 'currency': 'EUR', + 'ttl': 60, + 'netRevenue': true, + 'zone': '6682' + } + }; + + let expectedResponse = [{ + requestId: '23beaa6af6cdde', + cpm: 0.5, + width: 0, + height: 0, + creativeId: 100500, + dealId: '', + currency: 'EUR', + netRevenue: true, + ttl: 300, + meta: { advertiserDomains: ['bdomain'] }, + ad: '', + }, { + requestId: '23beaa6af6cdde', + cpm: 0.5, + width: 0, + height: 0, + creativeId: 100500, + dealId: '', + currency: 'EUR', + netRevenue: true, + ttl: 300, + meta: { advertiserDomains: [] }, + vastXml: '{"reason":7001,"status":"accepted"}', + mediaType: 'video', + }]; + + it('should get the correct bid response by display ad', function() { + let bidRequest = [{ + 'method': 'GET', + 'url': ENDPOINT_URL, + 'data': { + 'bid_id': '30b31c1838de1e' + } + }]; + let result = spec.interpretResponse(serverResponse, bidRequest[0]); + expect(Object.keys(result[0])).to.have.members(Object.keys(expectedResponse[0])); + expect(result[0].meta.advertiserDomains.length).to.equal(1); + expect(result[0].meta.advertiserDomains[0]).to.equal(expectedResponse[0].meta.advertiserDomains[0]); + }); + + it('should get the correct smartstream video bid response by display ad', function() { + let bidRequest = [{ + 'method': 'GET', + 'url': ENDPOINT_URL, + 'mediaTypes': { + 'video': { + 'playerSize': [640, 480], + 'context': 'instream' + } + }, + 'data': { + 'bid_id': '30b31c1838de1e' + } + }]; + let result = spec.interpretResponse(serverVideoResponse, bidRequest[0]); + expect(Object.keys(result[0])).to.have.members(Object.keys(expectedResponse[1])); + expect(result[0].meta.advertiserDomains.length).to.equal(0); + }); + + it('handles empty bid response', function() { + let response = { + body: {} + }; + let result = spec.interpretResponse(response); + expect(result.length).to.equal(0); + }); + }); + + describe(`getUserSyncs test usage`, function() { + let serverResponses; + + beforeEach(function() { + serverResponses = [{ + body: { + requestId: '23beaa6af6cdde', + cpm: 0.5, + width: 0, + height: 0, + creativeId: 100500, + dealId: '', + currency: 'EUR', + netRevenue: true, + ttl: 300, + type: 'sspHTML', + ad: '', + userSync: { + iframeUrl: ['anyIframeUrl?a=1'], + imageUrl: ['anyImageUrl', 'anyImageUrl2'] + } + } + }]; + }); + + it(`return value should be an array`, function() { + expect(spec.getUserSyncs({ iframeEnabled: true })).to.be.an('array'); + }); + it(`array should have only one object and it should have a property type = 'iframe'`, function() { + expect(spec.getUserSyncs({ iframeEnabled: true }, serverResponses).length).to.be.equal(1); + let [userSync] = spec.getUserSyncs({ iframeEnabled: true }, serverResponses); + expect(userSync).to.have.property('type'); + expect(userSync.type).to.be.equal('iframe'); + }); + it(`we have valid sync url for iframe`, function() { + let [userSync] = spec.getUserSyncs({ iframeEnabled: true }, serverResponses, { consentString: 'anyString' }); + expect(userSync.url).to.be.equal('anyIframeUrl?a=1&gdpr_consent=anyString') + expect(userSync.type).to.be.equal('iframe'); + }); + it(`we have valid sync url for image`, function() { + let [userSync] = spec.getUserSyncs({ pixelEnabled: true }, serverResponses, { gdprApplies: true, consentString: 'anyString' }); + expect(userSync.url).to.be.equal('anyImageUrl?gdpr=1&gdpr_consent=anyString') + expect(userSync.type).to.be.equal('image'); + }); + it(`we have valid sync url for image and iframe`, function() { + let userSync = spec.getUserSyncs({ iframeEnabled: true, pixelEnabled: true }, serverResponses, { gdprApplies: true, consentString: 'anyString' }); + expect(userSync.length).to.be.equal(3); + expect(userSync[0].url).to.be.equal('anyIframeUrl?a=1&gdpr=1&gdpr_consent=anyString') + expect(userSync[0].type).to.be.equal('iframe'); + expect(userSync[1].url).to.be.equal('anyImageUrl?gdpr=1&gdpr_consent=anyString') + expect(userSync[1].type).to.be.equal('image'); + expect(userSync[2].url).to.be.equal('anyImageUrl2?gdpr=1&gdpr_consent=anyString') + expect(userSync[2].type).to.be.equal('image'); + }); + }); + + describe(`getUserSyncs test usage in passback response`, function() { + let serverResponses; + + beforeEach(function() { + serverResponses = [{ + body: { + reason: 8002, + status: 'error', + msg: 'passback', + } + }]; + }); + + it(`check for zero array when iframeEnabled`, function() { + expect(spec.getUserSyncs({ iframeEnabled: true })).to.be.an('array'); + expect(spec.getUserSyncs({ iframeEnabled: true }, serverResponses).length).to.be.equal(0); + }); + it(`check for zero array when iframeEnabled`, function() { + expect(spec.getUserSyncs({ pixelEnabled: true })).to.be.an('array'); + expect(spec.getUserSyncs({ pixelEnabled: true }, serverResponses).length).to.be.equal(0); + }); + }); +}); diff --git a/test/spec/modules/synacormediaBidAdapter_spec.js b/test/spec/modules/synacormediaBidAdapter_spec.js index 0773d29789d..747dc4edc63 100644 --- a/test/spec/modules/synacormediaBidAdapter_spec.js +++ b/test/spec/modules/synacormediaBidAdapter_spec.js @@ -313,32 +313,6 @@ describe('synacormediaBidAdapter ', function () { expect(req.data.tmax).to.eql(bidderRequestWithTimeout.timeout); }); - it('should return tmax equal to smaller global timeout', function () { - let sandbox = sinon.sandbox.create(); - sandbox.stub(config, 'getConfig').callsFake(key => { - const config = { - 'bidderTimeout': bidderRequestWithTimeout.timeout - 100 - }; - return config[key]; - }); - let req = spec.buildRequests([validBidRequest], bidderRequestWithTimeout); - sandbox.restore(); - expect(req.data.tmax).to.eql(bidderRequestWithTimeout.timeout - 100); - }); - - it('should return tmax equal to smaller callback timeout', function () { - let sandbox = sinon.sandbox.create(); - sandbox.stub(config, 'getConfig').callsFake(key => { - const config = { - 'bidderTimeout': bidderRequestWithTimeout.timeout + 100 - }; - return config[key]; - }); - let req = spec.buildRequests([validBidRequest], bidderRequestWithTimeout); - sandbox.restore(); - expect(req.data.tmax).to.eql(bidderRequestWithTimeout.timeout); - }); - it('should return multiple bids when multiple valid requests with the same seatId are used', function () { let secondBidRequest = { bidId: 'foobar', diff --git a/test/spec/modules/taboolaBidAdapter_spec.js b/test/spec/modules/taboolaBidAdapter_spec.js index 5bde75cd0b6..206a0142043 100644 --- a/test/spec/modules/taboolaBidAdapter_spec.js +++ b/test/spec/modules/taboolaBidAdapter_spec.js @@ -2,6 +2,7 @@ import {expect} from 'chai'; import {spec, internal, END_POINT_URL, userData} from 'modules/taboolaBidAdapter.js'; import {config} from '../../../src/config' import * as utils from '../../../src/utils' +import {server} from '../../mocks/xhr' describe('Taboola Adapter', function () { let hasLocalStorage, cookiesAreEnabled, getDataFromLocalStorage, localStorageIsEnabled, getCookie, commonBidRequest; @@ -91,6 +92,31 @@ describe('Taboola Adapter', function () { }) }) + describe('onBidWon', function () { + it('onBidWon exist as a function', () => { + expect(spec.onBidWon).to.exist.and.to.be.a('function'); + }); + + it('should resolve price macro in nurl', function () { + const nurl = 'http://win.example.com/${AUCTION_PRICE}'; + const bid = { + requestId: 1, + cpm: 2, + originalCpm: 3.4, + creativeId: 1, + ttl: 60, + netRevenue: true, + mediaType: 'banner', + ad: '...', + width: 300, + height: 250, + nurl: nurl + } + spec.onBidWon(bid); + expect(server.requests[0].url).to.equals('http://win.example.com/3.4') + }); + }); + describe('buildRequests', function () { const defaultBidRequest = { ...createBidRequest(), @@ -122,7 +148,8 @@ describe('Taboola Adapter', function () { }, 'tagid': commonBidRequest.params.tagId, 'bidfloor': null, - 'bidfloorcur': 'USD' + 'bidfloorcur': 'USD', + 'ext': {} }], 'site': { 'id': commonBidRequest.params.publisherId, @@ -137,11 +164,13 @@ describe('Taboola Adapter', function () { 'source': {'fd': 1}, 'bcat': [], 'badv': [], + 'wlang': [], 'user': { 'buyeruid': 0, 'ext': {}, }, - 'regs': {'coppa': 0, 'ext': {}} + 'regs': {'coppa': 0, 'ext': {}}, + 'ext': {} }; const res = spec.buildRequests([defaultBidRequest], commonBidderRequest); @@ -184,7 +213,7 @@ describe('Taboola Adapter', function () { expect(resData.imp[0].bidfloorcur).to.deep.equal('USD'); }); - it('should pass bid floor even if they is a bid floor param', function () { + it('should pass bid floor even if it is a bid floor param', function () { const optionalParams = { bidfloor: 0.25, bidfloorcur: 'EUR' @@ -206,6 +235,38 @@ describe('Taboola Adapter', function () { expect(resData.imp[0].bidfloorcur).to.deep.equal('USD'); }); + it('should pass impression position', function () { + const optionalParams = { + position: 2 + }; + + const bidRequest = { + ...defaultBidRequest, + params: {...commonBidRequest.params, ...optionalParams} + }; + + const res = spec.buildRequests([bidRequest], commonBidderRequest); + const resData = JSON.parse(res.data); + expect(resData.imp[0].banner.pos).to.deep.equal(2); + }); + + it('should pass gpid if configured', function () { + const ortb2Imp = { + ext: { + gpid: '/homepage/#1' + } + } + const bidRequest = { + ...defaultBidRequest, + ortb2Imp: ortb2Imp, + params: {...commonBidRequest.params} + }; + + const res = spec.buildRequests([bidRequest], commonBidderRequest); + const resData = JSON.parse(res.data); + expect(resData.imp[0].ext.gpid).to.deep.equal('/homepage/#1'); + }); + it('should pass bidder timeout', function () { const bidderRequest = { ...commonBidderRequest, @@ -216,6 +277,40 @@ describe('Taboola Adapter', function () { expect(resData.tmax).to.equal(500); }); + describe('first party data', function () { + it('should parse first party data', function () { + const bidderRequest = { + ...commonBidderRequest, + ortb2: { + bcat: ['EX1', 'EX2', 'EX3'], + badv: ['site.com'], + wlang: ['de'], + } + } + const res = spec.buildRequests([defaultBidRequest], bidderRequest); + const resData = JSON.parse(res.data); + expect(resData.bcat).to.deep.equal(bidderRequest.ortb2.bcat) + expect(resData.badv).to.deep.equal(bidderRequest.ortb2.badv) + expect(resData.wlang).to.deep.equal(bidderRequest.ortb2.wlang) + }); + + it('should pass pageType if exists in ortb2', function () { + const bidderRequest = { + ...commonBidderRequest, + ortb2: { + ext: { + data: { + pageType: 'news' + } + } + } + } + const res = spec.buildRequests([defaultBidRequest], bidderRequest); + const resData = JSON.parse(res.data); + expect(resData.ext.pageType).to.deep.equal(bidderRequest.ortb2.ext.data.pageType); + }); + }); + describe('handle privacy segments when building request', function () { it('should pass GDPR consent', function () { const bidderRequest = { @@ -234,6 +329,20 @@ describe('Taboola Adapter', function () { expect(resData.regs.ext.gdpr).to.equal(1) }); + it('should pass GPP consent if exist in ortb2', function () { + const ortb2 = { + regs: { + gpp: 'testGpp', + gpp_sid: [1, 2, 3] + } + } + + const res = spec.buildRequests([defaultBidRequest], {...commonBidderRequest, ortb2}) + const resData = JSON.parse(res.data) + expect(resData.regs.ext.gpp).to.equal('testGpp') + expect(resData.regs.ext.gpp_sid).to.deep.equal([1, 2, 3]) + }); + it('should pass us privacy consent', function () { const bidderRequest = { refererInfo: { @@ -351,7 +460,9 @@ describe('Taboola Adapter', function () { 'w': 300, 'h': 250, 'exp': 60, - 'lurl': 'http://us-trc.taboola.com/sample' + 'lurl': 'http://us-trc.taboola.com/sample', + 'nurl': 'http://win.example.com/', + } ], 'seat': '14204545260' @@ -437,7 +548,8 @@ describe('Taboola Adapter', function () { 'w': 300, 'h': 250, 'exp': 60, - 'lurl': 'http://us-trc.taboola.com/sample' + 'lurl': 'http://us-trc.taboola.com/sample', + 'nurl': 'http://win.example.com/' }, { 'id': '0b3dd94348-134b-435f-8db5-6bf5afgfc39e86c', @@ -453,7 +565,9 @@ describe('Taboola Adapter', function () { 'w': 300, 'h': 250, 'exp': 60, - 'lurl': 'http://us-trc.taboola.com/sample' + 'lurl': 'http://us-trc.taboola.com/sample', + 'nurl': 'http://win.example.com/' + } ], 'seat': '14204545260' @@ -477,6 +591,7 @@ describe('Taboola Adapter', function () { ad: multiServerResponse.body.seatbid[0].bid[0].adm, width: bid.w, height: bid.h, + nurl: 'http://win.example.com/', meta: { 'advertiserDomains': bid.adomain }, @@ -492,6 +607,7 @@ describe('Taboola Adapter', function () { ad: multiServerResponse.body.seatbid[0].bid[1].adm, width: bid.w, height: bid.h, + nurl: 'http://win.example.com/', meta: { 'advertiserDomains': bid.adomain }, @@ -516,6 +632,7 @@ describe('Taboola Adapter', function () { ad: bid.adm, width: bid.w, height: bid.h, + nurl: 'http://win.example.com/', meta: { 'advertiserDomains': bid.adomain }, @@ -542,6 +659,7 @@ describe('Taboola Adapter', function () { ad: bid.adm, width: bid.w, height: bid.h, + nurl: 'http://win.example.com/', meta: { 'advertiserDomains': bid.adomain }, @@ -550,10 +668,139 @@ describe('Taboola Adapter', function () { const res = spec.interpretResponse(serverResponse, request); expect(res).to.deep.equal(expectedRes); }); + + it('should replace AUCTION_PRICE macro in adm', function () { + const multiRequest = { + bids: [ + { + ...createBidRequest(), + ...displayBidRequestParams + }, + { + ...createBidRequest(), + ...displayBidRequestParams + } + ] + } + const multiServerResponseWithMacro = { + body: { + 'id': '49ffg4d58ef9a163a69fhgfghd4fad03621b9e036f24f7_15', + 'seatbid': [ + { + 'bid': [ + { + 'id': '0b3dd94348-134b-435f-8db5-6bf5afgfc39e86c', + 'impid': '2', + 'price': 0.34, + 'adid': '2785119545551083381', + 'adm': 'ADM2,\\nwp:\'${AUCTION_PRICE}\'', + 'adomain': [ + 'example.xyz' + ], + 'cid': '15744349', + 'crid': '278195503434041083381', + 'w': 300, + 'h': 250, + 'exp': 60, + 'lurl': 'http://us-trc.taboola.com/sample', + 'nurl': 'http://win.example.com/' + }, + { + 'id': '0b3dd94348-134b-435f-8db5-6bf5afgfc39e86c', + 'impid': '1', + 'price': 0.35, + 'adid': '2785119545551083381', + 'adm': 'ADM2,\\nwp:\'${AUCTION_PRICE}\'', + 'adomain': [ + 'example.xyz' + ], + 'cid': '15744349', + 'crid': '278195503434041083381', + 'w': 300, + 'h': 250, + 'exp': 60, + 'lurl': 'http://us-trc.taboola.com/sample', + 'nurl': 'http://win.example.com/' + + } + ], + 'seat': '14204545260' + } + ], + 'bidid': 'da43860a-4644-442a-b5e0-93f268cf8d19', + 'cur': 'USD' + } + }; + const [bid] = multiServerResponseWithMacro.body.seatbid[0].bid; + const expectedRes = [ + { + requestId: multiRequest.bids[1].bidId, + cpm: multiServerResponseWithMacro.body.seatbid[0].bid[0].price, + creativeId: bid.crid, + ttl: 60, + netRevenue: true, + currency: multiServerResponseWithMacro.body.cur, + mediaType: 'banner', + ad: 'ADM2,\\nwp:\'0.34\'', + width: bid.w, + height: bid.h, + nurl: 'http://win.example.com/', + meta: { + 'advertiserDomains': bid.adomain + }, + }, + { + requestId: multiRequest.bids[0].bidId, + cpm: multiServerResponseWithMacro.body.seatbid[0].bid[1].price, + creativeId: bid.crid, + ttl: 60, + netRevenue: true, + currency: multiServerResponseWithMacro.body.cur, + mediaType: 'banner', + ad: 'ADM2,\\nwp:\'0.35\'', + width: bid.w, + height: bid.h, + nurl: 'http://win.example.com/', + meta: { + 'advertiserDomains': bid.adomain + }, + } + ]; + const res = spec.interpretResponse(multiServerResponseWithMacro, multiRequest); + expect(res).to.deep.equal(expectedRes); + }); }) - describe('userData', function () { - // todo: add UT for getUserSyncs + describe('getUserSyncs', function () { + const usersyncUrl = 'https://trc.taboola.com/sg/prebidJS/1/cm'; + + it('should not return user sync if pixelEnabled is false', function () { + const res = spec.getUserSyncs({pixelEnabled: false}); + expect(res).to.be.an('array').that.is.empty; + }); + + it('should return user sync if pixelEnabled is true', function () { + const res = spec.getUserSyncs({pixelEnabled: true}); + expect(res).to.deep.equal([{type: 'image', url: usersyncUrl}]); + }); + + it('should pass consent tokens values', function() { + expect(spec.getUserSyncs({ pixelEnabled: true }, {}, {gdprApplies: true, consentString: 'GDPR_CONSENT'}, 'USP_CONSENT')).to.deep.equal([{ + type: 'image', url: `${usersyncUrl}?gdpr=1&gdpr_consent=GDPR_CONSENT&us_privacy=USP_CONSENT` + }]); + expect(spec.getUserSyncs({ pixelEnabled: true }, {}, {gdprApplies: false, consentString: undefined}, undefined)).to.deep.equal([{ + type: 'image', url: `${usersyncUrl}?gdpr=0&gdpr_consent=` + }]); + expect(spec.getUserSyncs({ pixelEnabled: true }, {}, {gdprApplies: false, consentString: undefined}, undefined)).to.deep.equal([{ + type: 'image', url: `${usersyncUrl}?gdpr=0&gdpr_consent=` + }]); + expect(spec.getUserSyncs({ pixelEnabled: true }, {}, undefined, 'USP_CONSENT')).to.deep.equal([{ + type: 'image', url: `${usersyncUrl}?us_privacy=USP_CONSENT` + }]); + expect(spec.getUserSyncs({ pixelEnabled: true }, {}, undefined, 'USP_CONSENT', 'GPP_STRING')).to.deep.equal([{ + type: 'image', url: `${usersyncUrl}?us_privacy=USP_CONSENT&gpp=GPP_STRING` + }]); + }); }) describe('internal functions', function () { diff --git a/test/spec/modules/targetVideoBidAdapter_spec.js b/test/spec/modules/targetVideoBidAdapter_spec.js index adbc982e509..8180183e6d7 100644 --- a/test/spec/modules/targetVideoBidAdapter_spec.js +++ b/test/spec/modules/targetVideoBidAdapter_spec.js @@ -58,7 +58,7 @@ describe('TargetVideo Bid Adapter', function() { 'uuid': '84ab500420319d', 'ads': [{ 'ad_type': 'video', - 'cpm': 0.500000, + 'cpm': 0.675000, 'notify_url': 'https://www.target-video.com/', 'rtb': { 'video': { @@ -87,7 +87,7 @@ describe('TargetVideo Bid Adapter', function() { const bid = bidResponse[0]; expect(bid).to.not.be.empty; - expect(bid.cpm).to.equal(0.675); + expect(bid.cpm).to.equal(0.5); expect(bid.width).to.equal(300); expect(bid.height).to.equal(250); expect(bid.ad).to.include('') diff --git a/test/spec/modules/topicsFpdModule_spec.js b/test/spec/modules/topicsFpdModule_spec.js index 3781768497b..958489f2728 100644 --- a/test/spec/modules/topicsFpdModule_spec.js +++ b/test/spec/modules/topicsFpdModule_spec.js @@ -1,5 +1,15 @@ -import {getTopics, getTopicsData, processFpd} from '../../../modules/topicsFpdModule.js'; -import {deepClone} from '../../../src/utils.js'; +import { + getTopics, + getTopicsData, + processFpd, + hasGDPRConsent, + getCachedTopics, + receiveMessage, + topicStorageName +} from '../../../modules/topicsFpdModule.js'; +import {deepClone, safeJSONParse} from '../../../src/utils.js'; +import {gdprDataHandler} from 'src/adapterManager.js'; +import {getCoreStorageManager} from 'src/storageManager.js'; describe('getTopicsData', () => { function makeTopic(topic, modelv, taxv = '1') { @@ -7,14 +17,14 @@ describe('getTopicsData', () => { topic, taxonomyVersion: taxv, modelVersion: modelv - } + }; } function byTaxClass(segments) { return segments.reduce((memo, segment) => { memo[`${segment.ext.segtax}:${segment.ext.segclass}`] = segment; return memo; - }, {}) + }, {}); } [ @@ -144,16 +154,16 @@ describe('getTopicsData', () => { Object.entries(byTaxClass(actual)).forEach(([key, datum]) => { sinon.assert.match(datum, expected[key]); expect(datum.name).to.equal('mockName'); - }) + }); }); it('should not set name if null', () => { getTopicsData(null, topics).forEach((data) => { expect(data.hasOwnProperty('name')).to.be.false; - }) - }) - }) - }) + }); + }); + }); + }); }); describe('getTopics', () => { @@ -167,11 +177,13 @@ describe('getTopics', () => { 'document that throws on featurePolicy': { browsingTopics: sinon.stub(), get featurePolicy() { - throw new Error() + throw new Error(); } }, 'document that throws on browsingTopics': { - browsingTopics: sinon.stub().callsFake(() => { throw new Error(); }), + browsingTopics: sinon.stub().callsFake(() => { + throw new Error(); + }), featurePolicy: { allowsFeature: sinon.stub().returns(true) } @@ -181,11 +193,11 @@ describe('getTopics', () => { return getTopics(doc).then((topics) => { expect(topics).to.eql([]); }); - }) + }); }); it('should call `document.browsingTopics` when allowed', () => { - const topics = ['t1', 't2'] + const topics = ['t1', 't2']; return getTopics({ browsingTopics: sinon.stub().returns(Promise.resolve(topics)), featurePolicy: { @@ -193,9 +205,9 @@ describe('getTopics', () => { } }).then((actual) => { expect(actual).to.eql(topics); - }) - }) -}) + }); + }); +}); describe('processFpd', () => { const mockData = [ @@ -237,3 +249,184 @@ describe('processFpd', () => { }); }); }); + +describe('Topics Module GDPR consent check', () => { + let gdprDataHdlrStub; + beforeEach(() => { + gdprDataHdlrStub = sinon.stub(gdprDataHandler, 'getConsentData'); + }); + + afterEach(() => { + gdprDataHdlrStub.restore(); + }); + + it('should return false when GDPR is applied but consent string is not present', () => { + const consentString = ''; + const consentConfig = { + consentString: consentString, + gdprApplies: true, + vendorData: {} + }; + gdprDataHdlrStub.returns(consentConfig); + expect(hasGDPRConsent()).to.equal(false); + }); + + it('should return true when GDPR doesn\'t apply', () => { + const consentString = 'CPi8wgAPi8wgAADABBENCrCsAP_AAH_AAAAAISNB7D=='; + const consentConfig = { + consentString: consentString, + gdprApplies: false, + vendorData: {} + }; + + gdprDataHdlrStub.returns(consentConfig); + expect(hasGDPRConsent()).to.equal(true); + }); + + it('should return true when GDPR is applied and purpose consent is true for all purpose[1,2,3,4]', () => { + const consentString = 'CPi8wgAPi8wgAADABBENCrCsAP_AAH_AAAAAISNB7D=='; + const consentConfig = { + consentString: consentString, + gdprApplies: true, + vendorData: { + metadata: consentString, + gdprApplies: true, + purpose: { + consents: { + 1: true, + 2: true, + 3: true, + 4: true + } + } + } + }; + + gdprDataHdlrStub.returns(consentConfig); + expect(hasGDPRConsent()).to.equal(true); + }); + + it('should return false when GDPR is applied and purpose consent is false for one of the purpose[1,2,3,4]', () => { + const consentString = 'CPi8wgAPi8wgAADABBENCrCsAP_AAH_AAAAAISNB7D=='; + const consentConfig = { + consentString: consentString, + gdprApplies: true, + vendorData: { + metadata: consentString, + gdprApplies: true, + purpose: { + consents: { + 1: true, + 2: true, + 3: true, + 4: false + } + } + } + }; + + gdprDataHdlrStub.returns(consentConfig); + expect(hasGDPRConsent()).to.equal(false); + }); +}); + +describe('getCachedTopics()', () => { + const storage = getCoreStorageManager('topicsFpd'); + const expected = [{ + ext: { + segtax: 600, + segclass: '2206021246' + }, + segment: [{ + 'id': '243' + }, { + 'id': '265' + }], + name: 'ads.pubmatic.com' + }]; + const consentString = 'CPi8wgAPi8wgAADABBENCrCsAP_AAH_AAAAAISNB7D=='; + const consentConfig = { + consentString: consentString, + gdprApplies: true, + vendorData: { + metadata: consentString, + gdprApplies: true, + purpose: { + consents: { + 1: true, + 2: true, + 3: true, + 4: true + } + } + } + }; + const mockData = [ + { + name: 'domain', + segment: [{id: 123}] + }, + { + name: 'domain', + segment: [{id: 321}], + } + ]; + + const evt = { + data: '{"segment":{"domain":"ads.pubmatic.com","topics":[{"configVersion":"chrome.1","modelVersion":"2206021246","taxonomyVersion":"1","topic":165,"version":"chrome.1:1:2206021246"}],"bidder":"pubmatic"},"date":1669743901858}', + origin: 'https://ads.pubmatic.com' + }; + + let gdprDataHdlrStub; + beforeEach(() => { + gdprDataHdlrStub = sinon.stub(gdprDataHandler, 'getConsentData'); + }); + + afterEach(() => { + storage.removeDataFromLocalStorage(topicStorageName); + gdprDataHdlrStub.restore(); + }); + + it('should return segments for bidder if GDPR consent is true and there is cached segments stored which is not expired', () => { + const storedSegments = JSON.stringify( + [['pubmatic', { + '2206021246': { + 'ext': {'segtax': 600, 'segclass': '2206021246'}, + 'segment': [{'id': '243'}, {'id': '265'}], + 'name': 'ads.pubmatic.com' + }, + 'lastUpdated': new Date().getTime() + }]] + ); + storage.setDataInLocalStorage(topicStorageName, storedSegments); + gdprDataHdlrStub.returns(consentConfig); + assert.deepEqual(getCachedTopics(), expected); + }); + + it('should return empty segments for bidder if GDPR consent is true and there is cached segments stored which is expired', () => { + let storedSegments = '[["pubmatic",{"2206021246":{"ext":{"segtax":600,"segclass":"2206021246"},"segment":[{"id":"243"},{"id":"265"}],"name":"ads.pubmatic.com"},"lastUpdated":10}]]'; + storage.setDataInLocalStorage(topicStorageName, storedSegments); + gdprDataHdlrStub.returns(consentConfig); + assert.deepEqual(getCachedTopics(), []); + }); + + it('should stored segments if receiveMessage event is triggerred with segment data', () => { + return processFpd({}, {global: {}}, {data: Promise.resolve(mockData)}) + .then(({global}) => { + receiveMessage(evt); + let segments = new Map(safeJSONParse(storage.getDataFromLocalStorage(topicStorageName))); + expect(segments.has('pubmatic')).to.equal(true); + }); + }); + + it('should update stored segments if receiveMessage event is triggerred with segment data', () => { + let storedSegments = '[["pubmatic",{"2206021246":{"ext":{"segtax":600,"segclass":"2206021246"},"segment":[{"id":"243"},{"id":"265"}],"name":"ads.pubmatic.com"},"lastUpdated":1669719242027}]]'; + storage.setDataInLocalStorage(topicStorageName, storedSegments); + return processFpd({}, {global: {}}, {data: Promise.resolve(mockData)}) + .then(({global}) => { + receiveMessage(evt); + let segments = new Map(safeJSONParse(storage.getDataFromLocalStorage(topicStorageName))); + expect(segments.get('pubmatic')[2206021246].segment.length).to.equal(1); + }); + }); +}); diff --git a/test/spec/modules/tripleliftBidAdapter_spec.js b/test/spec/modules/tripleliftBidAdapter_spec.js index 5cfa64184f9..4debf516f89 100644 --- a/test/spec/modules/tripleliftBidAdapter_spec.js +++ b/test/spec/modules/tripleliftBidAdapter_spec.js @@ -167,7 +167,8 @@ describe('triplelift adapter', function () { mediaTypes: { video: { context: 'instream', - playerSize: [640, 480] + playerSize: [640, 480], + playbackmethod: 5 } }, adUnitCode: 'adunit-code-instream', @@ -292,7 +293,8 @@ describe('triplelift adapter', function () { mediaTypes: { video: { context: 'instream', - playerSize: [640, 480] + playerSize: [640, 480], + playbackmethod: [1, 2, 3] }, banner: { sizes: [ @@ -725,11 +727,11 @@ describe('triplelift adapter', function () { it('should add amxRtbId to the payload if included', function () { const id = 'Ok9JQkBM-UFlAXEZQ-UUNBQlZOQzgrUFhW-UUNBQkRQTUBPQVpVWVxNXlZUUF9AUFhAUF9PXFY/'; - bidRequests[0].userIdAsEids = [{ source: 'amxrtb.com', uids: [{ id }] }]; + bidRequests[0].userIdAsEids = [{ source: 'amxdt.net', uids: [{ id }] }]; const request = tripleliftAdapterSpec.buildRequests(bidRequests, bidderRequest); const payload = request.data; expect(payload).to.exist; - expect(payload.user).to.deep.equal({ext: {eids: [{source: 'amxrtb.com', uids: [{id, ext: {rtiPartner: 'amxrtb.com'}}]}]}}); + expect(payload.user).to.deep.equal({ext: {eids: [{source: 'amxdt.net', uids: [{id, ext: {rtiPartner: 'amxdt.net'}}]}]}}); }); it('should add tdid, idl_env and criteoId to the payload if both are included', function () { @@ -1168,6 +1170,29 @@ describe('triplelift adapter', function () { } }) }); + it('should add gpp consent data to bid request object if gpp data exists', function() { + bidderRequest.ortb2 = { + regs: { + 'gpp': 'BOJ/P2HOJ/P2HABABMAAAAAZ+A==', + 'gpp_sid': [7] + } + } + const request = tripleliftAdapterSpec.buildRequests(bidRequests, bidderRequest); + expect(request.data.regs).to.deep.equal({ + 'gpp': 'BOJ/P2HOJ/P2HABABMAAAAAZ+A==', + 'gpp_sid': [7] + }) + }); + it('should cast playbackmethod as an array if it is an integer and it exists', function() { + const request = tripleliftAdapterSpec.buildRequests(bidRequests, bidderRequest); + expect(request.data.imp[1].video.playbackmethod).to.be.a('array'); + expect(request.data.imp[1].video.playbackmethod).to.deep.equal([5]); + }); + it('should set playbackmethod as an array if it exists as an array', function() { + const request = tripleliftAdapterSpec.buildRequests(bidRequests, bidderRequest); + expect(request.data.imp[5].video.playbackmethod).to.be.a('array'); + expect(request.data.imp[5].video.playbackmethod).to.deep.equal([1, 2, 3]); + }); }); describe('interpretResponse', function () { @@ -1424,6 +1449,13 @@ describe('triplelift adapter', function () { expect(result[0].meta.advertiserDomains[1]).to.equal('internetalerts.org'); expect(result[1].meta).to.not.have.key('advertiserDomains'); }); + + it('should include networkId in the meta field if available', function () { + let result = tripleliftAdapterSpec.interpretResponse(response, {bidderRequest}); + expect(result[1].meta.networkId).to.equal('10092'); + expect(result[2].meta.networkId).to.equal('5989'); + expect(result[3].meta.networkId).to.equal('5989'); + }); }); describe('getUserSyncs', function() { diff --git a/test/spec/modules/ttdBidAdapter_spec.js b/test/spec/modules/ttdBidAdapter_spec.js index 9869d072657..8c0a9db8fbd 100644 --- a/test/spec/modules/ttdBidAdapter_spec.js +++ b/test/spec/modules/ttdBidAdapter_spec.js @@ -81,6 +81,18 @@ describe('ttdBidAdapter', function () { delete bid.mediaTypes expect(spec.isBidRequestValid(bid)).to.equal(false); }); + + it('should return false if bidfloor is passed incorrectly', function () { + let bid = makeBid(); + bid.params.bidfloor = 'invalid bidfloor'; + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + + it('should return true if bidfloor is passed correctly as a float', function () { + let bid = makeBid(); + bid.params.bidfloor = 3.01; + expect(spec.isBidRequestValid(bid)).to.equal(true); + }); }); describe('banner', function () { @@ -270,6 +282,21 @@ describe('ttdBidAdapter', function () { expect(requestBody.imp[0].ext.gpid).to.equal(gpid); }); + it('sends rwdd in imp.rwdd if present', function () { + let clonedBannerRequests = deepClone(baseBannerBidRequests); + const gpid = '/1111/home#header'; + const rwdd = 1; + clonedBannerRequests[0].ortb2Imp = { + rwdd: rwdd, + ext: { + gpid: gpid + } + }; + const requestBody = testBuildRequests(clonedBannerRequests, baseBidderRequest).data; + expect(requestBody.imp[0].rwdd).to.be.not.null; + expect(requestBody.imp[0].rwdd).to.equal(1); + }); + it('sends auction id in source.tid', function () { const requestBody = testBuildRequests(baseBannerBidRequests, baseBidderRequest).data; expect(requestBody.source).to.be.not.null; @@ -477,13 +504,7 @@ describe('ttdBidAdapter', function () { const TDID = '00000000-0000-0000-0000-000000000000'; const UID2 = '99999999-9999-9999-9999-999999999999'; let clonedBannerRequests = deepClone(baseBannerBidRequests); - clonedBannerRequests[0].userId = { - tdid: TDID, - uid2: { - id: UID2 - } - }; - const expectedEids = [ + clonedBannerRequests[0].userIdAsEids = [ { source: 'adserver.org', uids: [ @@ -506,6 +527,7 @@ describe('ttdBidAdapter', function () { ] } ]; + const expectedEids = clonedBannerRequests[0].userIdAsEids; const requestBody = testBuildRequests(clonedBannerRequests, baseBidderRequest).data; expect(requestBody.user.ext.eids).to.deep.equal(expectedEids); @@ -535,6 +557,17 @@ describe('ttdBidAdapter', function () { expect(requestBody.site.ref).to.equal('https://ref.example.com'); expect(requestBody.site.keywords).to.equal('power tools, drills'); }); + + it('should fallback to floor module if no bidfloor is sent ', function () { + let clonedBannerRequests = deepClone(baseBannerBidRequests); + const bidfloor = 5.00; + clonedBannerRequests[0].getFloor = () => { + return { currency: 'USD', floor: bidfloor }; + }; + const requestBody = testBuildRequests(clonedBannerRequests, baseBidderRequest).data; + config.resetConfig(); + expect(requestBody.imp[0].bidfloor).to.equal(bidfloor); + }); }); describe('buildRequests-banner-multiple', function () { @@ -859,6 +892,14 @@ describe('ttdBidAdapter', function () { const requestBody = testBuildRequests(clonedVideoRequests, baseBidderRequest).data; expect(requestBody.imp[0].video.placement).to.equal(3); }); + + it('sets plcmt correctly if sent', function () { + let clonedVideoRequests = deepClone(baseVideoBidRequests); + clonedVideoRequests[0].mediaTypes.video.plcmt = 3; + + const requestBody = testBuildRequests(clonedVideoRequests, baseBidderRequest).data; + expect(requestBody.imp[0].video.plcmt).to.equal(3); + }); }); describe('interpretResponse-empty', function () { diff --git a/test/spec/modules/uid2IdSystem_spec.js b/test/spec/modules/uid2IdSystem_spec.js new file mode 100644 index 00000000000..b33e8cda501 --- /dev/null +++ b/test/spec/modules/uid2IdSystem_spec.js @@ -0,0 +1,310 @@ +import {coreStorage, init, setSubmoduleRegistry, requestBidsHook} from 'modules/userId/index.js'; +import {config} from 'src/config.js'; +import * as utils from 'src/utils.js'; +import { uid2IdSubmodule } from 'modules/uid2IdSystem.js'; +import 'src/prebid.js'; +import { getGlobal } from 'src/prebidGlobal.js'; +import { server } from 'test/mocks/xhr.js'; +import { configureTimerInterceptors } from 'test/mocks/timers.js'; +import {hook} from 'src/hook.js'; +import {uninstall as uninstallGdprEnforcement} from 'modules/gdprEnforcement.js'; + +let expect = require('chai').expect; + +const clearTimersAfterEachTest = true; +const debugOutput = () => {}; + +const expireCookieDate = 'Thu, 01 Jan 1970 00:00:01 GMT'; +const msIn12Hours = 60 * 60 * 12 * 1000; +const moduleCookieName = '__uid2_advertising_token'; +const publisherCookieName = '__UID2_SERVER_COOKIE'; +const auctionDelayMs = 10; +const legacyConfigParams = null; +const serverCookieConfigParams = { uid2ServerCookie: publisherCookieName } +const getFutureCookieExpiry = () => new Date(Date.now() + msIn12Hours).toUTCString(); +const setPublisherCookie = (token) => coreStorage.setCookie(publisherCookieName, JSON.stringify(token), getFutureCookieExpiry()); + +const makePrebidIdentityContainer = (token) => ({uid2: {id: token}}); +const makePrebidConfig = (params = null, extraSettings = {}, debug = false) => ({ + userSync: { auctionDelay: auctionDelayMs, userIds: [{name: 'uid2', params}] }, debug, ...extraSettings +}); + +const initialToken = `initial-advertising-token`; +const legacyToken = 'legacy-advertising-token'; +const refreshedToken = 'refreshed-advertising-token'; +const makeUid2Token = (token = initialToken, shouldRefresh = false, expired = false) => ({ + advertising_token: token, + refresh_token: 'fake-refresh-token', + identity_expires: expired ? Date.now() - 1000 : Date.now() + 60 * 60 * 1000, + refresh_from: shouldRefresh ? Date.now() - 1000 : Date.now() + 60 * 1000, + refresh_expires: Date.now() + 24 * 60 * 60 * 1000, // 24 hours + refresh_response_key: 'wR5t6HKMfJ2r4J7fEGX9Gw==', +}); +const expectInitialToken = (bid) => expect(bid?.userId ?? {}).to.deep.include(makePrebidIdentityContainer(initialToken)); +const expectRefreshedToken = (bid) => expect(bid?.userId ?? {}).to.deep.include(makePrebidIdentityContainer(refreshedToken)); +const expectNoIdentity = (bid) => expect(bid).to.not.haveOwnProperty('userId'); +const expectGlobalToHaveRefreshedIdentity = () => expect(getGlobal().getUserIds()).to.deep.include(makePrebidIdentityContainer(refreshedToken)); +const expectGlobalToHaveNoUid2 = () => expect(getGlobal().getUserIds()).to.not.haveOwnProperty('uid2'); +const expectLegacyToken = (bid) => expect(bid.userId).to.deep.include(makePrebidIdentityContainer(legacyToken)); +const expectNoLegacyToken = (bid) => expect(bid.userId).to.not.deep.include(makePrebidIdentityContainer(legacyToken)); +const expectModuleCookieEmptyOrMissing = () => expect(coreStorage.getCookie(moduleCookieName)).to.be.null; +const expectModuleCookieToContain = (initialIdentity, latestIdentity) => { + const cookie = JSON.parse(coreStorage.getCookie(moduleCookieName)); + if (initialIdentity) expect(cookie.originalToken.advertising_token).to.equal(initialIdentity); + if (latestIdentity) expect(cookie.latestToken.advertising_token).to.equal(latestIdentity); +} + +const apiUrl = 'https://prod.uidapi.com/v2/token/refresh'; +const headers = { 'Content-Type': 'application/json' }; +const makeSuccessResponseBody = () => btoa(JSON.stringify({ status: 'success', body: { ...makeUid2Token(), advertising_token: refreshedToken } })); +const configureUid2Response = (httpStatus, response) => server.respondWith('POST', apiUrl, (xhr) => xhr.respond(httpStatus, headers, response)); +const configureUid2ApiSuccessResponse = () => configureUid2Response(200, makeSuccessResponseBody()); +const configureUid2ApiFailResponse = () => configureUid2Response(500, 'Error'); + +const respondAfterDelay = (delay) => new Promise((resolve) => setTimeout(() => { + server.respond(); + setTimeout(() => resolve()); +}, delay)); + +const runAuction = async () => { + const adUnits = [{ + code: 'adUnit-code', + mediaTypes: {banner: {}, native: {}}, + sizes: [[300, 200], [300, 600]], + bids: [{bidder: 'sampleBidder', params: {placementId: 'banner-only-bidder'}}] + }]; + return new Promise(function(resolve) { + requestBidsHook(function() { + resolve(adUnits[0].bids[0]); + }, {adUnits}); + }); +} + +// Runs the provided test twice - once with a successful API mock, once with one which returns a server error +const testApiSuccessAndFailure = (act, testDescription, failTestDescription, only = false) => { + const testFn = only ? it.only : it; + testFn(`API responds successfully: ${testDescription}`, async function() { + configureUid2ApiSuccessResponse(); + await act(true); + }); + testFn(`API responds with an error: ${failTestDescription ?? testDescription}`, async function() { + configureUid2ApiFailResponse(); + await act(false); + }); +} +describe(`UID2 module`, function () { + let suiteSandbox, testSandbox, timerSpy, fullTestTitle, restoreSubtleToUndefined = false; + before(function () { + timerSpy = configureTimerInterceptors(debugOutput); + hook.ready(); + uninstallGdprEnforcement(); + + suiteSandbox = sinon.sandbox.create(); + // I'm unable to find an authoritative source, but apparently subtle isn't available in some test stacks for security reasons. + // I've confirmed it's available in Firefox since v34 (it seems to be unavailable on BrowserStack in Firefox v106). + if (typeof window.crypto.subtle === 'undefined') { + restoreSubtleToUndefined = true; + window.crypto.subtle = { importKey: () => {}, decrypt: () => {} }; + } + suiteSandbox.stub(window.crypto.subtle, 'importKey').callsFake(() => Promise.resolve()); + suiteSandbox.stub(window.crypto.subtle, 'decrypt').callsFake((settings, key, data) => Promise.resolve(new Uint8Array([...settings.iv, ...data]))); + }); + + after(function () { + suiteSandbox.restore(); + timerSpy.restore(); + if (restoreSubtleToUndefined) window.crypto.subtle = undefined; + }); + + const getFullTestTitle = (test) => `${test.parent.title ? getFullTestTitle(test.parent) + ' | ' : ''}${test.title}`; + beforeEach(function () { + debugOutput(`----------------- START TEST ------------------`); + fullTestTitle = getFullTestTitle(this.test.ctx.currentTest); + debugOutput(fullTestTitle); + testSandbox = sinon.sandbox.create(); + testSandbox.stub(utils, 'logWarn'); + + init(config); + setSubmoduleRegistry([uid2IdSubmodule]); + }); + + afterEach(async function() { + $$PREBID_GLOBAL$$.requestBids.removeAll(); + config.resetConfig(); + testSandbox.restore(); + if (timerSpy.timers.length > 0) { + if (clearTimersAfterEachTest) { + debugOutput(`Cancelling ${timerSpy.timers.length} still-active timers.`); + timerSpy.clearAllActiveTimers(); + } else { + debugOutput(`Waiting on ${timerSpy.timers.length} still-active timers...`, timerSpy.timers); + await timerSpy.waitAllActiveTimers(); + } + } + coreStorage.setCookie(moduleCookieName, '', expireCookieDate); + coreStorage.setCookie(publisherCookieName, '', expireCookieDate); + + debugOutput('----------------- END TEST ------------------'); + }); + + describe('Configuration', function() { + it('When no baseUrl is provided in config, the module calls the production endpoint', function() { + const uid2Token = makeUid2Token(initialToken, true, true); + config.setConfig(makePrebidConfig({uid2Token})); + expect(server.requests[0]?.url).to.have.string('https://prod.uidapi.com/'); + }); + + it('When a baseUrl is provided in config, the module calls the provided endpoint', function() { + const uid2Token = makeUid2Token(initialToken, true, true); + config.setConfig(makePrebidConfig({uid2Token, uid2ApiBase: 'https://operator-integ.uidapi.com'})); + expect(server.requests[0]?.url).to.have.string('https://operator-integ.uidapi.com/'); + }); + }); + + it('When a legacy value is provided directly in configuration, it is passed on', async function() { + const valueConfig = makePrebidConfig(); + valueConfig.userSync.userIds[0].value = {uid2: {id: legacyToken}} + config.setConfig(valueConfig); + const bid = await runAuction(); + + expectLegacyToken(bid); + }); + + // These tests cover 'legacy' cookies - i.e. cookies set with just the uid2 advertising token, which was how some previous integrations worked. + // Some users might still have this cookie, and the module should use that token if a newer one isn't provided. + // This should cover older integrations where the server is setting this legacy cookie and expecting the module to pass it on. + describe('When a legacy cookie exists', function () { + // Creates a test which sets the legacy cookie, configures the UID2 module with provided params, runs an + const createLegacyTest = function(params, bidAssertions) { + return async function() { + coreStorage.setCookie(moduleCookieName, legacyToken, getFutureCookieExpiry()); + config.setConfig(makePrebidConfig(params)); + + const bid = await runAuction(); + bidAssertions.forEach(function(assertion) { assertion(bid); }); + } + }; + + it('and a legacy config is used, it should provide the legacy cookie', + createLegacyTest(legacyConfigParams, [expectLegacyToken])); + it('and a server cookie config is used without a valid server cookie, it should provide the legacy cookie', + createLegacyTest(serverCookieConfigParams, [expectLegacyToken])); + it('and a server cookie is used with a valid server cookie, it should provide the server cookie', + async function() { setPublisherCookie(makeUid2Token()); await createLegacyTest(serverCookieConfigParams, [expectInitialToken, expectNoLegacyToken])(); }); + it('and a token is provided in config, it should provide the config token', + createLegacyTest({uid2Token: makeUid2Token()}, [expectInitialToken, expectNoLegacyToken])); + }); + + // This setup runs all of the functional tests with both types of config - the full token response in params, or a server cookie with the cookie name provided + let scenarios = [ + { + name: 'Token provided in config call', + setConfig: (token, extraConfig = {}) => config.setConfig(makePrebidConfig({uid2Token: token}, extraConfig)), + }, + { + name: 'Token provided in server-set cookie', + setConfig: (token, extraConfig) => { + setPublisherCookie(token); + config.setConfig(makePrebidConfig(serverCookieConfigParams, extraConfig)); + }, + } + ] + + scenarios.forEach(function(scenario) { + describe(scenario.name, function() { + describe(`When an expired token which can be refreshed is provided`, function() { + describe('When the refresh is available in time', function() { + testApiSuccessAndFailure(async function(apiSucceeds) { + scenario.setConfig(makeUid2Token(initialToken, true, true)); + respondAfterDelay(auctionDelayMs / 10); + const bid = await runAuction(); + + if (apiSucceeds) expectRefreshedToken(bid); + else expectNoIdentity(bid); + }, 'it should be used in the auction', 'the auction should have no uid2'); + + testApiSuccessAndFailure(async function(apiSucceeds) { + scenario.setConfig(makeUid2Token(initialToken, true, true)); + respondAfterDelay(auctionDelayMs / 10); + + await runAuction(); + if (apiSucceeds) { + expectModuleCookieToContain(initialToken, refreshedToken); + } else { + expectModuleCookieEmptyOrMissing(); + } + }, 'the refreshed token should be stored in the module cookie', 'the module cookie should not be set'); + }); + describe(`when the response doesn't arrive before the auction timer`, function() { + testApiSuccessAndFailure(async function() { + scenario.setConfig(makeUid2Token(initialToken, true, true)); + const bid = await runAuction(); + expectNoIdentity(bid); + }, 'it should run the auction'); + + testApiSuccessAndFailure(async function(apiSucceeds) { + scenario.setConfig(makeUid2Token(initialToken, true, true)); + const promise = respondAfterDelay(auctionDelayMs * 2); + + const bid = await runAuction(); + expectNoIdentity(bid); + expectGlobalToHaveNoUid2(); + await promise; + if (apiSucceeds) expectGlobalToHaveRefreshedIdentity(); + else expectGlobalToHaveNoUid2(); + }, 'it should update the userId after the auction', 'there should be no global identity'); + }) + describe('and there is a refreshed token in the module cookie', function() { + it('the refreshed value from the cookie is used', async function() { + const initialIdentity = makeUid2Token(initialToken, true, true); + const refreshedIdentity = makeUid2Token(refreshedToken); + const moduleCookie = {originalToken: initialIdentity, latestToken: refreshedIdentity}; + coreStorage.setCookie(moduleCookieName, JSON.stringify(moduleCookie), getFutureCookieExpiry()); + scenario.setConfig(initialIdentity); + + const bid = await runAuction(); + expectRefreshedToken(bid); + }); + }) + }); + + describe(`When a current token is provided`, function() { + beforeEach(function() { + scenario.setConfig(makeUid2Token()); + }); + + it('it should use the token in the auction', async function() { + const bid = await runAuction(); + expectInitialToken(bid); + }); + }); + + describe(`When a current token which should be refreshed is provided, and the auction is set to run immediately`, function() { + beforeEach(function() { + scenario.setConfig(makeUid2Token(initialToken, true), {auctionDelay: 0, syncDelay: 1}); + }); + testApiSuccessAndFailure(async function() { + respondAfterDelay(10); + const bid = await runAuction(); + expectInitialToken(bid); + }, 'it should not be refreshed before the auction runs'); + + testApiSuccessAndFailure(async function(success) { + const promise = respondAfterDelay(1); + await runAuction(); + await promise; + if (success) { + expectModuleCookieToContain(initialToken, refreshedToken); + } else { + expectModuleCookieToContain(initialToken, initialToken); + } + }, 'the refreshed token should be stored in the module cookie after the auction runs', 'the module cookie should only have the original token'); + + it('it should use the current token in the auction', async function() { + const bid = await runAuction(); + expectInitialToken(bid); + }); + }); + }); + }); +}); diff --git a/test/spec/modules/underdogmediaBidAdapter_spec.js b/test/spec/modules/underdogmediaBidAdapter_spec.js index 70d09513f27..4201ecc2329 100644 --- a/test/spec/modules/underdogmediaBidAdapter_spec.js +++ b/test/spec/modules/underdogmediaBidAdapter_spec.js @@ -1,29 +1,38 @@ -import { expect } from 'chai'; -import { spec, resetUserSync } from 'modules/underdogmediaBidAdapter.js'; +import { + expect +} from 'chai'; +import { + spec, + resetUserSync +} from 'modules/underdogmediaBidAdapter.js'; describe('UnderdogMedia adapter', function () { let bidRequests; let bidderRequest; beforeEach(function () { - bidRequests = [ - { - bidder: 'underdogmedia', - params: { - siteId: 12143 - }, - adUnitCode: '/19968336/header-bid-tag-1', - mediaTypes: { - banner: { - sizes: [[300, 250], [300, 600], [728, 90], [160, 600], [320, 50]], - } - }, - bidId: '23acc48ad47af5', - auctionId: '0fb4905b-9456-4152-86be-c6f6d259ba99', - bidderRequestId: '1c56ad30b9b8ca8', - transactionId: '92489f71-1bf2-49a0-adf9-000cea934729' - } - ]; + bidRequests = [{ + bidder: 'underdogmedia', + params: { + siteId: 12143 + }, + adUnitCode: '/19968336/header-bid-tag-1', + mediaTypes: { + banner: { + sizes: [ + [300, 250], + [300, 600], + [728, 90], + [160, 600], + [320, 50] + ], + } + }, + bidId: '23acc48ad47af5', + auctionId: '0fb4905b-9456-4152-86be-c6f6d259ba99', + bidderRequestId: '1c56ad30b9b8ca8', + transactionId: '92489f71-1bf2-49a0-adf9-000cea934729' + }]; bidderRequest = { timeout: 3000, @@ -49,7 +58,10 @@ describe('UnderdogMedia adapter', function () { }, mediaTypes: { banner: { - sizes: [[300, 250], [300, 600]] + sizes: [ + [300, 250], + [300, 600] + ] } } }; @@ -76,7 +88,10 @@ describe('UnderdogMedia adapter', function () { params: {}, mediaTypes: { banner: { - sizes: [[300, 250], [300, 600]] + sizes: [ + [300, 250], + [300, 600] + ] } } }; @@ -86,90 +101,94 @@ describe('UnderdogMedia adapter', function () { }); it('request data should contain sid', function () { - let bidRequests = [ - { - bidId: '3c9408cdbf2f68', - bidder: 'underdogmedia', - mediaTypes: { - banner: { - sizes: [[300, 250]] - } - }, - params: { - siteId: '12143' - }, - auctionId: '10b327aa396609', - adUnitCode: '/123456/header-bid-tag-1' - } - ]; + let bidRequests = [{ + bidId: '3c9408cdbf2f68', + bidder: 'underdogmedia', + mediaTypes: { + banner: { + sizes: [ + [300, 250] + ] + } + }, + params: { + siteId: '12143' + }, + auctionId: '10b327aa396609', + adUnitCode: '/123456/header-bid-tag-1' + }]; const request = spec.buildRequests(bidRequests, bidderRequest); - expect(request.data.sid).to.equal('12143'); + expect(request.data.sid).to.equal(12143); }); it('request data should contain sizes', function () { - let bidRequests = [ - { - bidId: '3c9408cdbf2f68', - mediaTypes: { - banner: { - sizes: [[300, 250], [728, 90]] - } - }, - bidder: 'underdogmedia', - params: { - siteId: '12143' - }, - auctionId: '10b327aa396609', - adUnitCode: '/123456/header-bid-tag-1' - } - ]; + let bidRequests = [{ + bidId: '3c9408cdbf2f68', + mediaTypes: { + banner: { + sizes: [ + [300, 250], + [728, 90] + ] + } + }, + bidder: 'underdogmedia', + params: { + siteId: '12143' + }, + auctionId: '10b327aa396609', + adUnitCode: '/123456/header-bid-tag-1' + }]; const request = spec.buildRequests(bidRequests, bidderRequest); - expect(request.data.sizes).to.equal('300x250,728x90'); + expect(request.data.placements[0].sizes[0]).to.equal('300x250'); + expect(request.data.placements[0].sizes[1]).to.equal('728x90'); }); it('request data should contain gdpr info', function () { - let bidRequests = [ - { - bidId: '3c9408cdbf2f68', - mediaTypes: { - banner: { - sizes: [[300, 250], [728, 90]] - } - }, - bidder: 'underdogmedia', - params: { - siteId: '12143' - }, - auctionId: '10b327aa396609', - adUnitCode: '/123456/header-bid-tag-1' - } - ]; + let bidRequests = [{ + bidId: '3c9408cdbf2f68', + mediaTypes: { + banner: { + sizes: [ + [300, 250], + [728, 90] + ] + } + }, + bidder: 'underdogmedia', + params: { + siteId: '12143' + }, + auctionId: '10b327aa396609', + adUnitCode: '/123456/header-bid-tag-1' + }]; const request = spec.buildRequests(bidRequests, bidderRequest); - expect(request.data.gdprApplies).to.equal(true); - expect(request.data.consentGiven).to.equal(true); - expect(request.data.consentData).to.equal('consentDataString'); + expect(request.data.gdpr.gdprApplies).to.equal(true); + expect(request.data.gdpr.consentGiven).to.equal(true); + expect(request.data.gdpr.consentData).to.equal('consentDataString'); }); it('should not build a request if no vendorConsent', function () { - let bidRequests = [ - { - bidId: '3c9408cdbf2f68', - mediaTypes: { - banner: { - sizes: [[300, 250], [728, 90]] - } - }, - bidder: 'underdogmedia', - params: { - siteId: '12143' - }, - auctionId: '10b327aa396609', - adUnitCode: '/123456/header-bid-tag-1' - } - ]; + let bidRequests = [{ + bidId: '3c9408cdbf2f68', + mediaTypes: { + banner: { + sizes: [ + [300, 250], + [728, 90] + ] + } + }, + bidder: 'underdogmedia', + params: { + siteId: '12143' + }, + auctionId: '10b327aa396609', + adUnitCode: '/123456/header-bid-tag-1' + }]; let bidderRequest = { timeout: 3000, @@ -189,22 +208,23 @@ describe('UnderdogMedia adapter', function () { }); it('should properly build a request if no vendorConsent but no gdprApplies', function () { - let bidRequests = [ - { - bidId: '3c9408cdbf2f68', - mediaTypes: { - banner: { - sizes: [[300, 250], [728, 90]] - } - }, - bidder: 'underdogmedia', - params: { - siteId: '12143' - }, - auctionId: '10b327aa396609', - adUnitCode: '/123456/header-bid-tag-1' - } - ]; + let bidRequests = [{ + bidId: '3c9408cdbf2f68', + mediaTypes: { + banner: { + sizes: [ + [300, 250], + [728, 90] + ] + } + }, + bidder: 'underdogmedia', + params: { + siteId: '12143' + }, + auctionId: '10b327aa396609', + adUnitCode: '/123456/header-bid-tag-1' + }]; let bidderRequest = { timeout: 3000, @@ -220,30 +240,32 @@ describe('UnderdogMedia adapter', function () { } const request = spec.buildRequests(bidRequests, bidderRequest); - expect(request.data.sizes).to.equal('300x250,728x90'); - expect(request.data.sid).to.equal('12143'); - expect(request.data.gdprApplies).to.equal(false); - expect(request.data.consentGiven).to.equal(false); - expect(request.data.consentData).to.equal('consentDataString'); + expect(request.data.placements[0].sizes[0]).to.equal('300x250'); + expect(request.data.placements[0].sizes[1]).to.equal('728x90'); + expect(request.data.sid).to.equal(12143); + expect(request.data.gdpr.gdprApplies).to.equal(false); + expect(request.data.gdpr.consentGiven).to.equal(false); + expect(request.data.gdpr.consentData).to.equal('consentDataString'); }); it('should properly build a request if gdprConsent empty', function () { - let bidRequests = [ - { - bidId: '3c9408cdbf2f68', - mediaTypes: { - banner: { - sizes: [[300, 250], [728, 90]] - } - }, - bidder: 'underdogmedia', - params: { - siteId: '12143' - }, - auctionId: '10b327aa396609', - adUnitCode: '/123456/header-bid-tag-1' - } - ]; + let bidRequests = [{ + bidId: '3c9408cdbf2f68', + mediaTypes: { + banner: { + sizes: [ + [300, 250], + [728, 90] + ] + } + }, + bidder: 'underdogmedia', + params: { + siteId: '12143' + }, + auctionId: '10b327aa396609', + adUnitCode: '/123456/header-bid-tag-1' + }]; let bidderRequest = { timeout: 3000, @@ -251,21 +273,663 @@ describe('UnderdogMedia adapter', function () { } const request = spec.buildRequests(bidRequests, bidderRequest); - expect(request.data.sizes).to.equal('300x250,728x90'); - expect(request.data.sid).to.equal('12143'); + expect(request.data.placements[0].sizes[0]).to.equal('300x250'); + expect(request.data.placements[0].sizes[1]).to.equal('728x90'); + expect(request.data.sid).to.equal(12143); }); it('should have uspConsent if defined', function () { const uspConsent = '1YYN' bidderRequest.uspConsent = uspConsent const request = spec.buildRequests(bidRequests, bidderRequest); - expect(request.data.uspConsent).to.equal(uspConsent); + expect(request.data.usp.uspConsent).to.equal(uspConsent); + }); + + it('should have correct number of placements', function () { + let bidRequests = [{ + adUnitCode: 'div-gpt-ad-1460505748561-0', + auctionId: 'dfa93f1f-6ecc-4d75-8725-f5cb92307658', + bidId: '2dbc995ad299c', + bidder: 'underdogmedia', + crumbs: { + pubcid: 'ba6cbf43-abc0-4d61-b14f-e10f605b74d7' + }, + mediaTypes: { + banner: { + sizes: [ + [300, 250], + [160, 600] + ] + } + }, + ortb2Imp: { + ext: { + gpid: '/19968336/header-bid-tag-0' + } + }, + params: { + siteId: '12143' + }, + userId: { + tdid: '7a9fc5a2-346d-4502-826e-017a9badf5f3' + } + }, + { + adUnitCode: 'div-gpt-ad-2460505748561-0', + auctionId: 'dfa93f1f-6ecc-4d75-8725-f5cb92307658', + bidId: '3a378b833cdef4', + bidder: 'underdogmedia', + crumbs: { + pubcid: 'ba6cbf43-abc0-4d61-b14f-e10f605b74d7' + }, + mediaTypes: { + banner: { + sizes: [ + [300, 250] + ] + } + }, + ortb2Imp: { + ext: { + gpid: '/19968336/header-bid-tag-1' + } + }, + params: { + siteId: '12143' + }, + userId: { + tdid: '7a9fc5a2-346d-4502-826e-017a9badf5f3' + } + }, + { + adUnitCode: 'div-gpt-ad-3460505748561-0', + auctionId: 'dfa93f1f-6ecc-4d75-8725-f5cb92307658', + bidId: '4088f04e07c2a1', + bidder: 'underdogmedia', + crumbs: { + pubcid: 'ba6cbf43-abc0-4d61-b14f-e10f605b74d7' + }, + mediaTypes: { + banner: { + sizes: [ + [160, 600] + ] + } + }, + ortb2Imp: { + ext: { + gpid: '/19968336/header-bid-tag-2' + } + }, + params: { + siteId: '12143' + }, + userId: { + tdid: '7a9fc5a2-346d-4502-826e-017a9badf5f3' + } + } + ]; + + const request = spec.buildRequests(bidRequests, bidderRequest); + + expect(request.data.usp.uspConsent).to.be.undefined; + expect(request.data.placements.length).to.equal(3); + }); + + it('should have correct adUnitCode for each placement', function () { + let bidRequests = [{ + adUnitCode: 'div-gpt-ad-1460505748561-0', + auctionId: 'dfa93f1f-6ecc-4d75-8725-f5cb92307658', + bidId: '2dbc995ad299c', + bidder: 'underdogmedia', + crumbs: { + pubcid: 'ba6cbf43-abc0-4d61-b14f-e10f605b74d7' + }, + mediaTypes: { + banner: { + sizes: [ + [300, 250], + [160, 600] + ] + } + }, + ortb2Imp: { + ext: { + gpid: '/19968336/header-bid-tag-0' + } + }, + params: { + siteId: '12143' + }, + userId: { + tdid: '7a9fc5a2-346d-4502-826e-017a9badf5f3' + } + }, + { + adUnitCode: 'div-gpt-ad-2460505748561-0', + auctionId: 'dfa93f1f-6ecc-4d75-8725-f5cb92307658', + bidId: '3a378b833cdef4', + bidder: 'underdogmedia', + crumbs: { + pubcid: 'ba6cbf43-abc0-4d61-b14f-e10f605b74d7' + }, + mediaTypes: { + banner: { + sizes: [ + [300, 250] + ] + } + }, + ortb2Imp: { + ext: { + gpid: '/19968336/header-bid-tag-1' + } + }, + params: { + siteId: '12143' + }, + userId: { + tdid: '7a9fc5a2-346d-4502-826e-017a9badf5f3' + } + }, + { + adUnitCode: 'div-gpt-ad-3460505748561-0', + auctionId: 'dfa93f1f-6ecc-4d75-8725-f5cb92307658', + bidId: '4088f04e07c2a1', + bidder: 'underdogmedia', + crumbs: { + pubcid: 'ba6cbf43-abc0-4d61-b14f-e10f605b74d7' + }, + mediaTypes: { + banner: { + sizes: [ + [160, 600] + ] + } + }, + ortb2Imp: { + ext: { + gpid: '/19968336/header-bid-tag-2' + } + }, + params: { + siteId: '12143' + }, + userId: { + tdid: '7a9fc5a2-346d-4502-826e-017a9badf5f3' + } + } + ]; + + const request = spec.buildRequests(bidRequests, bidderRequest); + + expect(request.data.usp.uspConsent).to.be.undefined; + expect(request.data.placements[0].adUnitCode).to.equal('div-gpt-ad-1460505748561-0'); + expect(request.data.placements[1].adUnitCode).to.equal('div-gpt-ad-2460505748561-0'); + expect(request.data.placements[2].adUnitCode).to.equal('div-gpt-ad-3460505748561-0'); + }); + + it('should have gpid if it exists', function () { + let bidRequests = [{ + adUnitCode: 'div-gpt-ad-1460505748561-0', + auctionId: 'dfa93f1f-6ecc-4d75-8725-f5cb92307658', + bidId: '2dbc995ad299c', + bidder: 'underdogmedia', + crumbs: { + pubcid: 'ba6cbf43-abc0-4d61-b14f-e10f605b74d7' + }, + mediaTypes: { + banner: { + sizes: [ + [300, 250], + [160, 600] + ] + } + }, + ortb2Imp: { + ext: { + gpid: '/19968336/header-bid-tag-0' + } + }, + params: { + siteId: '12143' + }, + userId: { + tdid: '7a9fc5a2-346d-4502-826e-017a9badf5f3' + } + }]; + + const request = spec.buildRequests(bidRequests, bidderRequest); + + expect(request.data.placements[0].gpid).to.equal('/19968336/header-bid-tag-0'); + }); + + it('gpid should be undefined if it does not exists', function () { + let bidRequests = [{ + adUnitCode: 'div-gpt-ad-1460505748561-0', + auctionId: 'dfa93f1f-6ecc-4d75-8725-f5cb92307658', + bidId: '2dbc995ad299c', + bidder: 'underdogmedia', + crumbs: { + pubcid: 'ba6cbf43-abc0-4d61-b14f-e10f605b74d7' + }, + mediaTypes: { + banner: { + sizes: [ + [300, 250], + [160, 600] + ] + } + }, + params: { + siteId: '12143' + }, + userId: { + tdid: '7a9fc5a2-346d-4502-826e-017a9badf5f3' + } + }]; + + const request = spec.buildRequests(bidRequests, bidderRequest); + + expect(request.data.placements[0].gpid).to.equal(undefined); + }); + + it('should have productId equal to 1 if the productId is standard', function () { + let bidRequests = [{ + adUnitCode: 'div-gpt-ad-1460505748561-0', + auctionId: 'dfa93f1f-6ecc-4d75-8725-f5cb92307658', + bidId: '2dbc995ad299c', + bidder: 'underdogmedia', + crumbs: { + pubcid: 'ba6cbf43-abc0-4d61-b14f-e10f605b74d7' + }, + mediaTypes: { + banner: { + sizes: [ + [300, 250], + [160, 600] + ] + } + }, + params: { + siteId: '12143', + productId: 'standard' + }, + userId: { + tdid: '7a9fc5a2-346d-4502-826e-017a9badf5f3' + } + }]; + + const request = spec.buildRequests(bidRequests, bidderRequest); + + expect(request.data.placements[0].productId).to.equal(1); + }); + + it('should have productId equal to 2 if the productId is adhesion', function () { + let bidRequests = [{ + adUnitCode: 'div-gpt-ad-1460505748561-0', + auctionId: 'dfa93f1f-6ecc-4d75-8725-f5cb92307658', + bidId: '2dbc995ad299c', + bidder: 'underdogmedia', + crumbs: { + pubcid: 'ba6cbf43-abc0-4d61-b14f-e10f605b74d7' + }, + mediaTypes: { + banner: { + sizes: [ + [300, 250], + [160, 600] + ] + } + }, + params: { + siteId: '12143', + productId: 'adhesion' + }, + userId: { + tdid: '7a9fc5a2-346d-4502-826e-017a9badf5f3' + } + }]; + + const request = spec.buildRequests(bidRequests, bidderRequest); + + expect(request.data.placements[0].productId).to.equal(2); + }); + + it('productId should default to 1 if it is not defined', function () { + let bidRequests = [{ + adUnitCode: 'div-gpt-ad-1460505748561-0', + auctionId: 'dfa93f1f-6ecc-4d75-8725-f5cb92307658', + bidId: '2dbc995ad299c', + bidder: 'underdogmedia', + crumbs: { + pubcid: 'ba6cbf43-abc0-4d61-b14f-e10f605b74d7' + }, + mediaTypes: { + banner: { + sizes: [ + [300, 250], + [160, 600] + ] + } + }, + params: { + siteId: '12143' + }, + userId: { + tdid: '7a9fc5a2-346d-4502-826e-017a9badf5f3' + } + }]; + + const request = spec.buildRequests(bidRequests, bidderRequest); + + expect(request.data.placements[0].productId).to.equal(1); + }); + + it('should have correct sizes for multiple placements', function () { + let bidRequests = [{ + adUnitCode: 'div-gpt-ad-1460505748561-0', + auctionId: 'dfa93f1f-6ecc-4d75-8725-f5cb92307658', + bidId: '2dbc995ad299c', + bidder: 'underdogmedia', + crumbs: { + pubcid: 'ba6cbf43-abc0-4d61-b14f-e10f605b74d7' + }, + mediaTypes: { + banner: { + sizes: [ + [300, 250], + [160, 600] + ] + } + }, + ortb2Imp: { + ext: { + gpid: '/19968336/header-bid-tag-0' + } + }, + params: { + siteId: '12143' + }, + userId: { + tdid: '7a9fc5a2-346d-4502-826e-017a9badf5f3' + } + }, + { + adUnitCode: 'div-gpt-ad-2460505748561-0', + auctionId: 'dfa93f1f-6ecc-4d75-8725-f5cb92307658', + bidId: '3a378b833cdef4', + bidder: 'underdogmedia', + crumbs: { + pubcid: 'ba6cbf43-abc0-4d61-b14f-e10f605b74d7' + }, + mediaTypes: { + banner: { + sizes: [ + [300, 250] + ] + } + }, + ortb2Imp: { + ext: { + gpid: '/19968336/header-bid-tag-1' + } + }, + params: { + siteId: '12143' + }, + userId: { + tdid: '7a9fc5a2-346d-4502-826e-017a9badf5f3' + } + }, + { + adUnitCode: 'div-gpt-ad-3460505748561-0', + auctionId: 'dfa93f1f-6ecc-4d75-8725-f5cb92307658', + bidId: '4088f04e07c2a1', + bidder: 'underdogmedia', + crumbs: { + pubcid: 'ba6cbf43-abc0-4d61-b14f-e10f605b74d7' + }, + mediaTypes: { + banner: { + sizes: [ + [160, 600] + ] + } + }, + ortb2Imp: { + ext: { + gpid: '/19968336/header-bid-tag-2' + } + }, + params: { + siteId: '12143' + }, + userId: { + tdid: '7a9fc5a2-346d-4502-826e-017a9badf5f3' + } + } + ]; + + const request = spec.buildRequests(bidRequests, bidderRequest); + + expect(request.data.placements[0].sizes.length).to.equal(2); + expect(request.data.placements[0].sizes[0]).to.equal('300x250'); + expect(request.data.placements[0].sizes[1]).to.equal('160x600'); + expect(request.data.placements[1].sizes.length).to.equal(1); + expect(request.data.placements[1].sizes[0]).to.equal('300x250'); + expect(request.data.placements[2].sizes.length).to.equal(1); + expect(request.data.placements[2].sizes[0]).to.equal('160x600'); + }); + + it('should have ref if it exists', function () { + let bidderRequest = { + timeout: 3000, + gdprConsent: { + gdprApplies: 1, + consentString: 'consentDataString', + vendorData: { + vendorConsents: { + '159': 1 + }, + }, + }, + refererInfo: { + ref: 'www.example.com' + } + } + + const request = spec.buildRequests(bidRequests, bidderRequest); + + expect(request.data.ref).to.equal('www.example.com'); + }); + + it('ref should be undefined if it does not exist', function () { + let bidderRequest = { + timeout: 3000, + gdprConsent: { + gdprApplies: 1, + consentString: 'consentDataString', + vendorData: { + vendorConsents: { + '159': 1 + }, + }, + } + } + + const request = spec.buildRequests(bidRequests, bidderRequest); + + expect(request.data.ref).to.equal(undefined); + }); + + it('should have pubcid if it exists', function () { + let bidRequests = [{ + adUnitCode: 'div-gpt-ad-1460505748561-0', + auctionId: 'dfa93f1f-6ecc-4d75-8725-f5cb92307658', + bidId: '2dbc995ad299c', + bidder: 'underdogmedia', + crumbs: { + pubcid: 'ba6cbf43-abc0-4d61-b14f-e10f605b74d7' + }, + mediaTypes: { + banner: { + sizes: [ + [300, 250], + [160, 600] + ] + } + }, + ortb2Imp: { + ext: { + gpid: '/19968336/header-bid-tag-0' + } + }, + params: { + siteId: '12143' + }, + userId: { + tdid: '7a9fc5a2-346d-4502-826e-017a9badf5f3' + } + }]; + + const request = spec.buildRequests(bidRequests, bidderRequest); + + expect(request.data.userIds.pubcid).to.equal('ba6cbf43-abc0-4d61-b14f-e10f605b74d7'); + }); + + it('pubcid should be undefined if it does not exist', function () { + let bidRequests = [{ + adUnitCode: 'div-gpt-ad-1460505748561-0', + auctionId: 'dfa93f1f-6ecc-4d75-8725-f5cb92307658', + bidId: '2dbc995ad299c', + bidder: 'underdogmedia', + mediaTypes: { + banner: { + sizes: [ + [300, 250], + [160, 600] + ] + } + }, + ortb2Imp: { + ext: { + gpid: '/19968336/header-bid-tag-0' + } + }, + params: { + siteId: '12143' + }, + userId: { + tdid: '7a9fc5a2-346d-4502-826e-017a9badf5f3' + } + }]; + + const request = spec.buildRequests(bidRequests, bidderRequest); + + expect(request.data.userIds.pubcid).to.equal(undefined); + }); + + it('should have unifiedId if tdid if it exists', function () { + let bidRequests = [{ + adUnitCode: 'div-gpt-ad-1460505748561-0', + auctionId: 'dfa93f1f-6ecc-4d75-8725-f5cb92307658', + bidId: '2dbc995ad299c', + bidder: 'underdogmedia', + crumbs: { + pubcid: 'ba6cbf43-abc0-4d61-b14f-e10f605b74d7' + }, + mediaTypes: { + banner: { + sizes: [ + [300, 250], + [160, 600] + ] + } + }, + ortb2Imp: { + ext: { + gpid: '/19968336/header-bid-tag-0' + } + }, + params: { + siteId: '12143' + }, + userId: { + tdid: '7a9fc5a2-346d-4502-826e-017a9badf5f3' + } + }]; + + const request = spec.buildRequests(bidRequests, bidderRequest); + + expect(request.data.userIds.unifiedId).to.equal('7a9fc5a2-346d-4502-826e-017a9badf5f3'); + }); + + it('unifiedId should be undefined if tdid does not exist', function () { + let bidRequests = [{ + adUnitCode: 'div-gpt-ad-1460505748561-0', + auctionId: 'dfa93f1f-6ecc-4d75-8725-f5cb92307658', + bidId: '2dbc995ad299c', + bidder: 'underdogmedia', + crumbs: { + pubcid: 'ba6cbf43-abc0-4d61-b14f-e10f605b74d7' + }, + mediaTypes: { + banner: { + sizes: [ + [300, 250], + [160, 600] + ] + } + }, + ortb2Imp: { + ext: { + gpid: '/19968336/header-bid-tag-0' + } + }, + params: { + siteId: '12143' + } + }]; + + const request = spec.buildRequests(bidRequests, bidderRequest); + + expect(request.data.userIds.unifiedId).to.equal(undefined); }); - it('should not have uspConsent if not defined', function () { - bidderRequest.uspConsent = undefined + it('should have correct viewability information', function () { + let bidRequests = [{ + adUnitCode: 'div-gpt-ad-1460505748561-0', + auctionId: 'dfa93f1f-6ecc-4d75-8725-f5cb92307658', + bidId: '2dbc995ad299c', + bidder: 'underdogmedia', + crumbs: { + pubcid: 'ba6cbf43-abc0-4d61-b14f-e10f605b74d7' + }, + mediaTypes: { + banner: { + sizes: [ + [300, 250], + [160, 600] + ] + } + }, + ortb2Imp: { + ext: { + gpid: '/19968336/header-bid-tag-0' + } + }, + params: { + siteId: '12143' + }, + userId: { + tdid: '7a9fc5a2-346d-4502-826e-017a9badf5f3' + } + }]; + const request = spec.buildRequests(bidRequests, bidderRequest); - expect(request.data.uspConsent).to.be.undefined; + + expect(request.data.placements[0].viewability).to.equal(-1) }); }); @@ -273,26 +937,27 @@ describe('UnderdogMedia adapter', function () { it('should return complete bid response', function () { let serverResponse = { body: { - mids: [ - { - ad_code_html: 'ad_code_html', - advertiser_domains: ['domain1'], - cpm: 2.5, - height: '600', - mid: '32634', - notification_url: 'notification_url', - tid: '4', - width: '160' - }, - { - ad_code_html: 'ad_code_html', - cpm: 2.5, - height: '250', - mid: '32633', - notification_url: 'notification_url', - tid: '2', - width: '300' - }, + mids: [{ + ad_code_html: 'ad_code_html', + ad_unit_code: '/19968336/header-bid-tag-1', + advertiser_domains: ['domain1'], + cpm: 2.5, + height: '600', + mid: '32634', + notification_url: 'notification_url', + tid: '4', + width: '160', + }, + { + ad_code_html: 'ad_code_html', + ad_unit_code: '/19968336/header-bid-tag-1', + cpm: 3.0, + height: '250', + mid: '32633', + notification_url: 'notification_url', + tid: '2', + width: '300' + }, ] } }; @@ -326,17 +991,15 @@ describe('UnderdogMedia adapter', function () { it('should return empty bid response on incorrect size', function () { let serverResponse = { body: { - mids: [ - { - ad_code_html: 'ad_code_html', - cpm: 2.5, - height: '123', - mid: '32634', - notification_url: 'notification_url', - tid: '4', - width: '160' - } - ] + mids: [{ + ad_code_html: 'ad_code_html', + cpm: 2.5, + height: '123', + mid: '32634', + notification_url: 'notification_url', + tid: '4', + width: '160' + }] } }; const request = spec.buildRequests(bidRequests, bidderRequest); @@ -348,17 +1011,15 @@ describe('UnderdogMedia adapter', function () { it('should return empty bid response on 0 cpm', function () { let serverResponse = { body: { - mids: [ - { - ad_code_html: 'ad_code_html', - cpm: 0, - height: '600', - mid: '32634', - notification_url: 'notification_url', - tid: '4', - width: '160' - } - ] + mids: [{ + ad_code_html: 'ad_code_html', + cpm: 0, + height: '600', + mid: '32634', + notification_url: 'notification_url', + tid: '4', + width: '160' + }] } }; const request = spec.buildRequests(bidRequests, bidderRequest); @@ -370,17 +1031,15 @@ describe('UnderdogMedia adapter', function () { it('should return empty bid response if no ad in response', function () { let serverResponse = { body: { - mids: [ - { - ad_code_html: '', - cpm: 2.5, - height: '600', - mid: '32634', - notification_url: 'notification_url', - tid: '4', - width: '160' - } - ] + mids: [{ + ad_code_html: '', + cpm: 2.5, + height: '600', + mid: '32634', + notification_url: 'notification_url', + tid: '4', + width: '160' + }] } }; const request = spec.buildRequests(bidRequests, bidderRequest); @@ -392,17 +1051,16 @@ describe('UnderdogMedia adapter', function () { it('ad html string should contain the notification urls', function () { let serverResponse = { body: { - mids: [ - { - ad_code_html: 'ad_cod_html', - cpm: 2.5, - height: '600', - mid: '32634', - notification_url: 'notification_url', - tid: '4', - width: '160' - } - ] + mids: [{ + ad_code_html: 'ad_cod_html', + ad_unit_code: '/19968336/header-bid-tag-1', + cpm: 2.5, + height: '600', + mid: '32634', + notification_url: 'notification_url', + tid: '4', + width: '160' + }] } }; const request = spec.buildRequests(bidRequests, bidderRequest); @@ -430,15 +1088,14 @@ describe('UnderdogMedia adapter', function () { const responseWithUserSyncs = [{ body: { - userSyncs: [ - { - type: 'image', - url: 'https://test.url.com' - }, - { - type: 'iframe', - url: 'https://test.url.com' - } + userSyncs: [{ + type: 'image', + url: 'https://test.url.com' + }, + { + type: 'iframe', + url: 'https://test.url.com' + } ] } }]; diff --git a/test/spec/modules/userId_spec.js b/test/spec/modules/userId_spec.js index f4abcc4b9f6..a2f2bfd8713 100644 --- a/test/spec/modules/userId_spec.js +++ b/test/spec/modules/userId_spec.js @@ -27,7 +27,7 @@ import {britepoolIdSubmodule} from 'modules/britepoolIdSystem.js'; import {id5IdSubmodule} from 'modules/id5IdSystem.js'; import {identityLinkSubmodule} from 'modules/identityLinkIdSystem.js'; import {dmdIdSubmodule} from 'modules/dmdIdSystem.js'; -import {liveIntentIdSubmodule} from 'modules/liveIntentIdSystem.js'; +import {liveIntentIdSubmodule, setEventFiredFlag as liveIntentIdSubmoduleDoNotFireEvent} from 'modules/liveIntentIdSystem.js'; import {merkleIdSubmodule} from 'modules/merkleIdSystem.js'; import {netIdSubmodule} from 'modules/netIdSystem.js'; import {intentIqIdSubmodule} from 'modules/intentIqIdSystem.js'; @@ -53,6 +53,8 @@ import {hook} from '../../../src/hook.js'; import {mockGdprConsent} from '../../helpers/consentData.js'; import {getPPID} from '../../../src/adserver.js'; import {uninstall as uninstallGdprEnforcement} from 'modules/gdprEnforcement.js'; +import {GDPR_GVLIDS} from '../../../src/consentHandler.js'; +import {MODULE_TYPE_UID} from '../../../src/activities/modules.js'; let assert = require('chai').assert; let expect = require('chai').expect; @@ -147,6 +149,7 @@ describe('User ID', function () { hook.ready(); uninstallGdprEnforcement(); localStorage.removeItem(PBJS_USER_ID_OPTOUT_NAME); + liveIntentIdSubmoduleDoNotFireEvent(); }); beforeEach(function () { @@ -165,6 +168,20 @@ describe('User ID', function () { sandbox.restore(); }); + describe('GVL IDs', () => { + beforeEach(() => { + sinon.stub(GDPR_GVLIDS, 'register'); + }); + afterEach(() => { + GDPR_GVLIDS.register.restore(); + }); + + it('are registered when ID submodule is registered', () => { + attachIdSystem({name: 'gvlidMock', gvlid: 123}); + sinon.assert.calledWith(GDPR_GVLIDS.register, MODULE_TYPE_UID, 'gvlidMock', 123); + }) + }) + describe('Decorate Ad Units', function () { beforeEach(function () { // reset mockGpt so nothing else interferes @@ -425,6 +442,58 @@ describe('User ID', function () { }); }); + describe('submodule callback', () => { + const TEST_KEY = 'testKey'; + + function setVal(val) { + if (val) { + coreStorage.setDataInLocalStorage(TEST_KEY, val); + coreStorage.setDataInLocalStorage(TEST_KEY + '_exp', ''); + } else { + coreStorage.removeDataFromLocalStorage(TEST_KEY); + coreStorage.removeDataFromLocalStorage(TEST_KEY + '_exp'); + } + } + afterEach(() => { + setVal(null); + }) + + it('should be able to re-read ID changes', (done) => { + setVal(null); + init(config); + setSubmoduleRegistry([{ + name: 'mockId', + getId: function (_1, _2, storedId) { + expect(storedId).to.not.exist; + setVal('laterValue'); + return { + callback(_, readId) { + expect(readId()).to.eql('laterValue'); + done(); + } + } + }, + decode(d) { + return d + } + }]); + config.setConfig({ + userSync: { + auctionDelay: 10, + userIds: [ + { + name: 'mockId', + storage: { + type: 'html5', + name: TEST_KEY + } + } + ] + } + }); + }); + }); + it('should set PPID when the source needs to call out to the network', () => { let adUnits = [getAdUnitMock()]; init(config); @@ -897,7 +966,7 @@ describe('User ID', function () { storage: {name: 'intentIqId', type: 'cookie'} }, { name: 'hadronId', - storage: {name: 'hadronId', type: 'cookie'} + storage: {name: 'hadronId', type: 'html5'} }, { name: 'zeotapIdPlus' }, { @@ -1298,7 +1367,7 @@ describe('User ID', function () { expect(bid).to.have.deep.nested.property('userId.amxId'); expect(bid.userId.amxId).to.equal('test_amxid_id'); expect(bid.userIdAsEids[0]).to.deep.equal({ - source: 'amxrtb.com', + source: 'amxdt.net', uids: [{ id: 'test_amxid_id', atype: 1, @@ -1872,8 +1941,8 @@ describe('User ID', function () { it('test hook from hadronId html5', function (done) { // simulate existing browser local storage values - localStorage.setItem('hadronId', JSON.stringify({'hadronId': 'random-ls-identifier'})); - localStorage.setItem('hadronId_exp', ''); + localStorage.setItem('hadronId', JSON.stringify({'hadronId': 'testHadronId1'})); + localStorage.setItem('hadronId_exp', (new Date(Date.now() + 5000)).toUTCString()); init(config); setSubmoduleRegistry([hadronIdSubmodule]); @@ -1883,15 +1952,15 @@ describe('User ID', function () { adUnits.forEach(unit => { unit.bids.forEach(bid => { expect(bid).to.have.deep.nested.property('userId.hadronId'); - expect(bid.userId.hadronId).to.equal('random-ls-identifier'); + expect(bid.userId.hadronId).to.equal('testHadronId1'); expect(bid.userIdAsEids[0]).to.deep.equal({ source: 'audigent.com', - uids: [{id: 'random-ls-identifier', atype: 1}] + uids: [{id: 'testHadronId1', atype: 1}] }); }); }); localStorage.removeItem('hadronId'); - localStorage.removeItem('hadronId_exp', ''); + localStorage.removeItem('hadronId_exp'); done(); }, {adUnits}); }); @@ -2125,7 +2194,9 @@ describe('User ID', function () { coreStorage.setCookie('netId', JSON.stringify({'netId': 'testnetId'}), (new Date(Date.now() + 5000).toUTCString())); 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('hadronId', JSON.stringify({'hadronId': 'testHadronId'}), (new Date(Date.now() + 5000).toUTCString())); + // hadronId only supports localStorage + localStorage.setItem('hadronId', JSON.stringify({'hadronId': 'testHadronId1'})); + localStorage.setItem('hadronId_exp', (new Date(Date.now() + 5000)).toUTCString()); coreStorage.setCookie('storage_criteo', JSON.stringify({'criteoId': 'test_bidid'}), (new Date(Date.now() + 5000).toUTCString())); coreStorage.setCookie('mwol', JSON.stringify({eid: 'XX-YY-ZZ-123'}), (new Date(Date.now() + 5000).toUTCString())); coreStorage.setCookie('uid2id', 'Sample_AD_Token', (new Date(Date.now() + 5000).toUTCString())); @@ -2149,7 +2220,7 @@ describe('User ID', function () { ['netId', 'netId', 'cookie'], ['intentIqId', 'intentIqId', 'cookie'], ['zeotapIdPlus', 'IDP', 'cookie'], - ['hadronId', 'hadronId', 'cookie'], + ['hadronId', 'hadronId', 'html5'], ['criteo', 'storage_criteo', 'cookie'], ['mwOpenLinkId', 'mwol', 'cookie'], ['tapadId', 'tapad_id', 'cookie'], @@ -2192,7 +2263,7 @@ describe('User ID', function () { expect(bid.userId.IDP).to.equal('zeotapId'); // also check that hadronId id was copied to bid expect(bid).to.have.deep.nested.property('userId.hadronId'); - expect(bid.userId.hadronId).to.equal('testHadronId'); + expect(bid.userId.hadronId).to.equal('testHadronId1'); // 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'); @@ -2231,7 +2302,8 @@ describe('User ID', function () { coreStorage.setCookie('netId', '', EXPIRED_COOKIE_DATE); coreStorage.setCookie('intentIqId', '', EXPIRED_COOKIE_DATE); coreStorage.setCookie('IDP', '', EXPIRED_COOKIE_DATE); - coreStorage.setCookie('hadronId', '', EXPIRED_COOKIE_DATE); + localStorage.removeItem('hadronId'); + localStorage.removeItem('hadronId_exp'); coreStorage.setCookie('storage_criteo', '', EXPIRED_COOKIE_DATE); coreStorage.setCookie('mwol', '', EXPIRED_COOKIE_DATE); coreStorage.setCookie('uid2id', '', EXPIRED_COOKIE_DATE); @@ -2284,7 +2356,8 @@ describe('User ID', function () { coreStorage.setCookie('netId', JSON.stringify({'netId': 'testnetId'}), new Date(Date.now() + 5000).toUTCString()); 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('hadronId', JSON.stringify({'hadronId': 'testHadronId'}), (new Date(Date.now() + 5000).toUTCString())); + localStorage.setItem('hadronId', JSON.stringify({'hadronId': 'testHadronId1'})); + localStorage.setItem('hadronId_exp', (new Date(Date.now() + 5000)).toUTCString()); coreStorage.setCookie('admixerId', 'testadmixerId', new Date(Date.now() + 5000).toUTCString()); coreStorage.setCookie('deepintentId', 'testdeepintentId', new Date(Date.now() + 5000).toUTCString()); coreStorage.setCookie('MOCKID', JSON.stringify({'MOCKID': '123456778'}), new Date(Date.now() + 5000).toUTCString()); @@ -2320,7 +2393,7 @@ describe('User ID', function () { }, { name: 'zeotapIdPlus' }, { - name: 'hadronId', storage: {name: 'hadronId', type: 'cookie'} + name: 'hadronId', storage: {name: 'hadronId', type: 'html5'} }, { name: 'admixerId', storage: {name: 'admixerId', type: 'cookie'} }, { @@ -2388,7 +2461,7 @@ describe('User ID', function () { expect(bid.userId.IDP).to.equal('zeotapId'); // also check that hadronId id data was copied to bid expect(bid).to.have.deep.nested.property('userId.hadronId'); - expect(bid.userId.hadronId).to.equal('testHadronId'); + expect(bid.userId.hadronId).to.equal('testHadronId1'); expect(bid.userId.uid2).to.deep.equal({ id: 'Sample_AD_Token' }); @@ -2420,7 +2493,8 @@ describe('User ID', function () { coreStorage.setCookie('netId', '', EXPIRED_COOKIE_DATE); coreStorage.setCookie('intentIqId', '', EXPIRED_COOKIE_DATE); coreStorage.setCookie('IDP', '', EXPIRED_COOKIE_DATE); - coreStorage.setCookie('hadronId', '', EXPIRED_COOKIE_DATE); + localStorage.removeItem('hadronId'); + localStorage.removeItem('hadronId_exp'); coreStorage.setCookie('dmdId', '', EXPIRED_COOKIE_DATE); coreStorage.setCookie('admixerId', '', EXPIRED_COOKIE_DATE); coreStorage.setCookie('deepintentId', '', EXPIRED_COOKIE_DATE); @@ -2768,47 +2842,6 @@ describe('User ID', function () { }) }) }); - - describe('findRootDomain', function () { - let sandbox; - - beforeEach(function () { - init(config); - setSubmoduleRegistry([sharedIdSystemSubmodule]); - config.setConfig({ - userSync: { - syncDelay: 0, - userIds: [ - { - name: 'pubCommonId', - value: { pubcid: '11111' }, - }, - ], - }, - }); - sandbox = sinon.createSandbox(); - sandbox - .stub(coreStorage, 'getCookie') - .onFirstCall() - .returns(null) // .co.uk - .onSecondCall() - .returns('writeable'); // realdomain.co.uk; - }); - - afterEach(function () { - sandbox.restore(); - }); - - it('should just find the root domain', function () { - var domain = findRootDomain('sub.realdomain.co.uk'); - expect(domain).to.be.eq('realdomain.co.uk'); - }); - - it('should find the full domain when no subdomain is present', function () { - var domain = findRootDomain('realdomain.co.uk'); - expect(domain).to.be.eq('realdomain.co.uk'); - }); - }); }); describe('handles config with ESP configuration in user sync object', function() { diff --git a/test/spec/modules/vidazooBidAdapter_spec.js b/test/spec/modules/vidazooBidAdapter_spec.js index 2ddb65469af..0429a2a51bf 100644 --- a/test/spec/modules/vidazooBidAdapter_spec.js +++ b/test/spec/modules/vidazooBidAdapter_spec.js @@ -1,4 +1,4 @@ -import { expect } from 'chai'; +import {expect} from 'chai'; import { spec as adapter, SUPPORTED_ID_SYSTEMS, @@ -13,11 +13,13 @@ import { getUniqueDealId, getNextDealId, getVidazooSessionId, + webSessionId } from 'modules/vidazooBidAdapter.js'; import * as utils from 'src/utils.js'; -import { version } from 'package.json'; -import { useFakeTimers } from 'sinon'; -import { BANNER, VIDEO } from '../../../src/mediaTypes'; +import {version} from 'package.json'; +import {useFakeTimers} from 'sinon'; +import {BANNER, VIDEO} from '../../../src/mediaTypes'; +import {config} from '../../../src/config'; const SUB_DOMAIN = 'openrtb'; @@ -38,6 +40,10 @@ const BID = { 'transactionId': 'c881914b-a3b5-4ecf-ad9c-1c2f37c6aabf', 'sizes': [[300, 250], [300, 600]], 'bidderRequestId': '1fdb5ff1b6eaa7', + 'auctionId': 'auction_id', + 'bidRequestsCount': 4, + 'bidderRequestsCount': 3, + 'bidderWinsCount': 1, 'requestId': 'b0777d85-d061-450e-9bc7-260dd54bbb7a', 'schain': 'a0819c69-005b-41ed-af06-1be1e0aefefc', 'mediaTypes': [BANNER], @@ -53,6 +59,10 @@ const VIDEO_BID = { 'adUnitCode': '63550ad1ff6642d368cba59dh5884270560', 'bidderRequestId': '12a8ae9ada9c13', 'transactionId': '56e184c6-bde9-497b-b9b9-cf47a61381ee', + 'auctionId': 'auction_id', + 'bidRequestsCount': 4, + 'bidderRequestsCount': 3, + 'bidderWinsCount': 1, 'schain': 'a0819c69-005b-41ed-af06-1be1e0aefefc', 'params': { 'subDomain': SUB_DOMAIN, @@ -85,6 +95,8 @@ const BIDDER_REQUEST = { 'consentString': 'consent_string', 'gdprApplies': true }, + 'gppString': 'gpp_string', + 'gppSid': [7], 'uspConsent': 'consent_string', 'refererInfo': { 'page': 'https://www.greatsite.com', @@ -94,6 +106,28 @@ const BIDDER_REQUEST = { 'site': { 'cat': ['IAB2'], 'pagecat': ['IAB2-2'] + }, + 'regs': { + 'gpp': 'gpp_string', + 'gpp_sid': [7] + }, + 'device': { + 'sua': { + 'source': 2, + 'platform': { + 'brand': 'Android', + 'version': ['8', '0', '0'] + }, + 'browsers': [ + {'brand': 'Not_A Brand', 'version': ['99', '0', '0', '0']}, + {'brand': 'Google Chrome', 'version': ['109', '0', '5414', '119']}, + {'brand': 'Chromium', 'version': ['109', '0', '5414', '119']} + ], + 'mobile': 1, + 'model': 'SM-G955U', + 'bitness': '64', + 'architecture': '' + } } }, }; @@ -147,7 +181,7 @@ const REQUEST = { function getTopWindowQueryParams() { try { - const parsedUrl = utils.parseUrl(window.top.document.URL, { decodeSearchAsString: true }); + const parsedUrl = utils.parseUrl(window.top.document.URL, {decodeSearchAsString: true}); return parsedUrl.search; } catch (e) { return ''; @@ -226,6 +260,9 @@ describe('VidazooBidAdapter', function () { it('should build video request', function () { const hashUrl = hashCode(BIDDER_REQUEST.refererInfo.page); + config.setConfig({ + bidderTimeout: 3000 + }); const requests = adapter.buildRequests([VIDEO_BID], BIDDER_REQUEST); expect(requests).to.have.length(1); expect(requests[0]).to.deep.equal({ @@ -243,6 +280,15 @@ describe('VidazooBidAdapter', function () { gdpr: 1, gdprConsent: 'consent_string', usPrivacy: 'consent_string', + gppString: 'gpp_string', + gppSid: [7], + auctionId: 'auction_id', + bidRequestsCount: 4, + bidderRequestsCount: 3, + bidderWinsCount: 1, + bidderTimeout: 3000, + transactionId: '56e184c6-bde9-497b-b9b9-cf47a61381ee', + bidderRequestId: '12a8ae9ada9c13', gpid: '', prebidVersion: version, ptrace: '1000', @@ -253,9 +299,26 @@ describe('VidazooBidAdapter', function () { schain: VIDEO_BID.schain, sessionId: '', sizes: ['545x307'], + sua: { + 'source': 2, + 'platform': { + 'brand': 'Android', + 'version': ['8', '0', '0'] + }, + 'browsers': [ + {'brand': 'Not_A Brand', 'version': ['99', '0', '0', '0']}, + {'brand': 'Google Chrome', 'version': ['109', '0', '5414', '119']}, + {'brand': 'Chromium', 'version': ['109', '0', '5414', '119']} + ], + 'mobile': 1, + 'model': 'SM-G955U', + 'bitness': '64', + 'architecture': '' + }, uniqueDealId: `${hashUrl}_${Date.now().toString()}`, uqs: getTopWindowQueryParams(), isStorageAllowed: true, + webSessionId: webSessionId, mediaTypes: { video: { api: [2], @@ -278,6 +341,9 @@ describe('VidazooBidAdapter', function () { }); it('should build banner request for each size', function () { + config.setConfig({ + bidderTimeout: 3000 + }); const hashUrl = hashCode(BIDDER_REQUEST.refererInfo.page); const requests = adapter.buildRequests([BID], BIDDER_REQUEST); expect(requests).to.have.length(1); @@ -288,7 +354,32 @@ describe('VidazooBidAdapter', function () { gdprConsent: 'consent_string', gdpr: 1, usPrivacy: 'consent_string', + gppString: 'gpp_string', + gppSid: [7], + auctionId: 'auction_id', + bidRequestsCount: 4, + bidderRequestsCount: 3, + bidderWinsCount: 1, + bidderTimeout: 3000, + transactionId: 'c881914b-a3b5-4ecf-ad9c-1c2f37c6aabf', + bidderRequestId: '1fdb5ff1b6eaa7', sizes: ['300x250', '300x600'], + sua: { + 'source': 2, + 'platform': { + 'brand': 'Android', + 'version': ['8', '0', '0'] + }, + 'browsers': [ + {'brand': 'Not_A Brand', 'version': ['99', '0', '0', '0']}, + {'brand': 'Google Chrome', 'version': ['109', '0', '5414', '119']}, + {'brand': 'Chromium', 'version': ['109', '0', '5414', '119']} + ], + 'mobile': 1, + 'model': 'SM-G955U', + 'bitness': '64', + 'architecture': '' + }, url: 'https%3A%2F%2Fwww.greatsite.com', referrer: 'https://www.somereferrer.com', cb: 1000, @@ -311,7 +402,8 @@ describe('VidazooBidAdapter', function () { isStorageAllowed: true, gpid: '1234567890', cat: ['IAB2'], - pagecat: ['IAB2-2'] + pagecat: ['IAB2-2'], + webSessionId: webSessionId } }); }); @@ -324,7 +416,7 @@ describe('VidazooBidAdapter', function () { describe('getUserSyncs', function () { it('should have valid user sync with iframeEnabled', function () { - const result = adapter.getUserSyncs({ iframeEnabled: true }, [SERVER_RESPONSE]); + const result = adapter.getUserSyncs({iframeEnabled: true}, [SERVER_RESPONSE]); expect(result).to.deep.equal([{ type: 'iframe', @@ -333,7 +425,7 @@ describe('VidazooBidAdapter', function () { }); it('should have valid user sync with cid on response', function () { - const result = adapter.getUserSyncs({ iframeEnabled: true }, [SERVER_RESPONSE]); + const result = adapter.getUserSyncs({iframeEnabled: true}, [SERVER_RESPONSE]); expect(result).to.deep.equal([{ type: 'iframe', url: 'https://sync.cootlogix.com/api/sync/iframe/?cid=testcid123&gdpr=0&gdpr_consent=&us_privacy=' @@ -341,7 +433,7 @@ describe('VidazooBidAdapter', function () { }); it('should have valid user sync with pixelEnabled', function () { - const result = adapter.getUserSyncs({ pixelEnabled: true }, [SERVER_RESPONSE]); + const result = adapter.getUserSyncs({pixelEnabled: true}, [SERVER_RESPONSE]); expect(result).to.deep.equal([{ 'url': 'https://sync.cootlogix.com/api/sync/image/?cid=testcid123&gdpr=0&gdpr_consent=&us_privacy=', @@ -357,12 +449,12 @@ describe('VidazooBidAdapter', function () { }); it('should return empty array when there is no ad', function () { - const responses = adapter.interpretResponse({ price: 1, ad: '' }); + const responses = adapter.interpretResponse({price: 1, ad: ''}); expect(responses).to.be.empty; }); it('should return empty array when there is no price', function () { - const responses = adapter.interpretResponse({ price: null, ad: 'great ad' }); + const responses = adapter.interpretResponse({price: null, ad: 'great ad'}); expect(responses).to.be.empty; }); @@ -385,6 +477,19 @@ describe('VidazooBidAdapter', function () { }); }); + it('should get meta from response metaData', function () { + const serverResponse = utils.deepClone(SERVER_RESPONSE); + serverResponse.body.results[0].metaData = { + advertiserDomains: ['vidazoo.com'], + agencyName: 'Agency Name', + }; + const responses = adapter.interpretResponse(serverResponse, REQUEST); + expect(responses[0].meta).to.deep.equal({ + advertiserDomains: ['vidazoo.com'], + agencyName: 'Agency Name' + }); + }); + it('should return an array of interpreted video responses', function () { const responses = adapter.interpretResponse(VIDEO_SERVER_RESPONSE, REQUEST); expect(responses).to.have.length(1); @@ -422,11 +527,11 @@ describe('VidazooBidAdapter', function () { const userId = (function () { switch (idSystemProvider) { case 'lipb': - return { lipbid: id }; + return {lipbid: id}; case 'parrableId': - return { eid: id }; + return {eid: id}; case 'id5id': - return { uid: id }; + return {uid: id}; default: return id; } @@ -445,18 +550,18 @@ describe('VidazooBidAdapter', function () { describe('alternate param names extractors', function () { it('should return undefined when param not supported', function () { - const cid = extractCID({ 'c_id': '1' }); - const pid = extractPID({ 'p_id': '1' }); - const subDomain = extractSubDomain({ 'sub_domain': 'prebid' }); + const cid = extractCID({'c_id': '1'}); + const pid = extractPID({'p_id': '1'}); + const subDomain = extractSubDomain({'sub_domain': 'prebid'}); expect(cid).to.be.undefined; expect(pid).to.be.undefined; expect(subDomain).to.be.undefined; }); it('should return value when param supported', function () { - const cid = extractCID({ 'cID': '1' }); - const pid = extractPID({ 'Pid': '2' }); - const subDomain = extractSubDomain({ 'subDOMAIN': 'prebid' }); + const cid = extractCID({'cID': '1'}); + const pid = extractPID({'Pid': '2'}); + const subDomain = extractSubDomain({'subDOMAIN': 'prebid'}); expect(cid).to.be.equal('1'); expect(pid).to.be.equal('2'); expect(subDomain).to.be.equal('prebid'); @@ -569,7 +674,7 @@ describe('VidazooBidAdapter', function () { now }); setStorageItem('myKey', 2020); - const { value, created } = getStorageItem('myKey'); + const {value, created} = getStorageItem('myKey'); expect(created).to.be.equal(now); expect(value).to.be.equal(2020); expect(typeof value).to.be.equal('number'); @@ -585,8 +690,8 @@ describe('VidazooBidAdapter', function () { }); it('should parse JSON value', function () { - const data = JSON.stringify({ event: 'send' }); - const { event } = tryParseJSON(data); + const data = JSON.stringify({event: 'send'}); + const {event} = tryParseJSON(data); expect(event).to.be.equal('send'); }); diff --git a/test/spec/modules/videoModule/adQueue_spec.js b/test/spec/modules/videoModule/adQueue_spec.js new file mode 100644 index 00000000000..4002e0b6dcc --- /dev/null +++ b/test/spec/modules/videoModule/adQueue_spec.js @@ -0,0 +1,172 @@ +import { expect } from 'chai'; +import { AdQueueCoordinator } from '../../../../modules/videoModule/adQueue.js'; +import { AD_BREAK_END, SETUP_COMPLETE } from '../../../../libraries/video/constants/events.js' + +const testId = 'testId'; +describe('Ad Queue Coordinator', function () { + const mockVideoCoreFactory = function () { + return { + onEvents: sinon.spy(), + offEvents: sinon.spy(), + setAdTagUrl: sinon.spy(), + } + }; + + const mockEventsFactory = function () { + return { + emit: sinon.spy() + }; + }; + + describe('Before Provider Setup Complete', function () { + it('should push ad to queue', function () { + const mockVideoCore = mockVideoCoreFactory(); + const mockEvents = mockEventsFactory(); + const coordinator = AdQueueCoordinator(mockVideoCore, mockEvents); + coordinator.registerProvider(testId); + coordinator.queueAd('testAdTag', testId, { param: {} }); + + expect(mockEvents.emit.calledOnce).to.be.true; + let emitArgs = mockEvents.emit.firstCall.args; + expect(emitArgs[0]).to.be.equal('videoAuctionAdLoadQueued'); + expect(mockVideoCore.setAdTagUrl.called).to.be.false; + }); + }); + + describe('After Provider Setup Complete', function () { + it('should load from ad queue', function () { + const mockVideoCore = mockVideoCoreFactory(); + const mockEvents = mockEventsFactory(); + let setupComplete; + mockVideoCore.onEvents = function(events, callback, id) { + if (events[0] === SETUP_COMPLETE && id === testId) { + setupComplete = callback; + } + }; + const coordinator = AdQueueCoordinator(mockVideoCore, mockEvents); + coordinator.registerProvider(testId); + coordinator.queueAd('testAdTag', testId, { param: {} }); + + expect(mockEvents.emit.calledOnce).to.be.true; + let emitArgs = mockEvents.emit.firstCall.args; + expect(emitArgs[0]).to.be.equal('videoAuctionAdLoadQueued'); + + setupComplete('', { divId: testId }); + expect(mockEvents.emit.calledTwice).to.be.true; + emitArgs = mockEvents.emit.secondCall.args; + expect(emitArgs[0]).to.be.equal('videoAuctionAdLoadAttempt'); + expect(mockVideoCore.setAdTagUrl.calledOnce).to.be.true; + }); + + it('should load ads without queueing', function () { + const mockVideoCore = mockVideoCoreFactory(); + const mockEvents = mockEventsFactory(); + let setupComplete; + mockVideoCore.onEvents = function(events, callback, id) { + if (events[0] === SETUP_COMPLETE && id === testId) { + setupComplete = callback; + } + }; + const coordinator = AdQueueCoordinator(mockVideoCore, mockEvents); + coordinator.registerProvider(testId); + + setupComplete('', { divId: testId }); + + coordinator.queueAd('testAdTag', testId, { param: {} }); + expect(mockEvents.emit.calledOnce).to.be.true; + let emitArgs = mockEvents.emit.firstCall.args; + expect(emitArgs[0]).to.be.equal('videoAuctionAdLoadAttempt'); + expect(mockVideoCore.setAdTagUrl.calledOnce).to.be.true; + }); + }); + + describe('On Ad Break End', function () { + it('should load from queue', function () { + const mockVideoCore = mockVideoCoreFactory(); + const mockEvents = mockEventsFactory(); + let setupComplete; + let adBreakEnd; + + mockVideoCore.onEvents = function(events, callback, id) { + if (events[0] === SETUP_COMPLETE && id === testId) { + setupComplete = callback; + } + + if (events[0] === AD_BREAK_END && id === testId) { + adBreakEnd = callback; + } + }; + + const coordinator = AdQueueCoordinator(mockVideoCore, mockEvents); + coordinator.registerProvider(testId); + coordinator.queueAd('testAdTag', testId); + coordinator.queueAd('testAdTag2', testId); + coordinator.queueAd('testAdTag3', testId); + + mockEvents.emit.resetHistory(); + + setupComplete('', { divId: testId }); + + expect(mockEvents.emit.calledOnce).to.be.true; + let emitArgs = mockEvents.emit.firstCall.args; + expect(emitArgs[0]).to.be.equal('videoAuctionAdLoadAttempt'); + expect(mockVideoCore.setAdTagUrl.calledOnce).to.be.true; + let setAdTagArgs = mockVideoCore.setAdTagUrl.firstCall.args; + expect(setAdTagArgs[0]).to.be.equal('testAdTag'); + + adBreakEnd('', { divId: testId }); + + expect(mockEvents.emit.calledTwice).to.be.true; + emitArgs = mockEvents.emit.secondCall.args; + expect(emitArgs[0]).to.be.equal('videoAuctionAdLoadAttempt'); + expect(mockVideoCore.setAdTagUrl.calledTwice).to.be.true; + setAdTagArgs = mockVideoCore.setAdTagUrl.secondCall.args; + expect(setAdTagArgs[0]).to.be.equal('testAdTag2'); + + adBreakEnd('', { divId: testId }); + + expect(mockEvents.emit.calledThrice).to.be.true; + emitArgs = mockEvents.emit.thirdCall.args; + expect(emitArgs[0]).to.be.equal('videoAuctionAdLoadAttempt'); + expect(mockVideoCore.setAdTagUrl.calledThrice).to.be.true; + setAdTagArgs = mockVideoCore.setAdTagUrl.thirdCall.args; + expect(setAdTagArgs[0]).to.be.equal('testAdTag3'); + + adBreakEnd('', { divId: testId }); + + expect(mockEvents.emit.calledThrice).to.be.true; + expect(mockVideoCore.setAdTagUrl.calledThrice).to.be.true; + }); + + it('should stop responding to AdBreakEnd when queue is empty', function () { + const mockVideoCore = mockVideoCoreFactory(); + let setupComplete; + let adBreakEnd; + + mockVideoCore.onEvents = function(events, callback, id) { + if (events[0] === SETUP_COMPLETE && id === testId) { + setupComplete = callback; + } + + if (events[0] === AD_BREAK_END && id === testId) { + adBreakEnd = callback; + } + }; + + const coordinator = AdQueueCoordinator(mockVideoCore, mockEventsFactory()); + coordinator.registerProvider(testId); + coordinator.queueAd('testAdTag', testId); + coordinator.queueAd('testAdTag2', testId); + coordinator.queueAd('testAdTag3', testId); + + setupComplete('', { divId: testId }); + adBreakEnd('', { divId: testId }); + adBreakEnd('', { divId: testId }); + expect(mockVideoCore.setAdTagUrl.calledThrice).to.be.true; + adBreakEnd('', { divId: testId }); + expect(mockVideoCore.setAdTagUrl.calledThrice).to.be.true; + adBreakEnd('', { divId: testId }); + expect(mockVideoCore.setAdTagUrl.calledThrice).to.be.true; + }); + }); +}); diff --git a/test/spec/modules/videoModule/pbVideo_spec.js b/test/spec/modules/videoModule/pbVideo_spec.js index cfd34ebd706..2e26737da40 100644 --- a/test/spec/modules/videoModule/pbVideo_spec.js +++ b/test/spec/modules/videoModule/pbVideo_spec.js @@ -14,12 +14,15 @@ let gamSubmoduleMock; let gamSubmoduleFactoryMock; let videoImpressionVerifierFactoryMock; let videoImpressionVerifierMock; +let adQueueCoordinatorMock; +let adQueueCoordinatorFactoryMock; function resetTestVars() { ortbVideoMock = {}; ortbContentMock = {}; videoCoreMock = { registerProvider: sinon.spy(), + initProvider: sinon.spy(), onEvents: sinon.spy(), getOrtbVideo: () => ortbVideoMock, getOrtbContent: () => ortbContentMock, @@ -54,9 +57,16 @@ function resetTestVars() { }; videoImpressionVerifierFactoryMock = () => videoImpressionVerifierMock; + + adQueueCoordinatorMock = { + registerProvider: sinon.spy(), + queueAd: sinon.spy() + }; + + adQueueCoordinatorFactoryMock = () => adQueueCoordinatorMock; } -let pbVideoFactory = (videoCore, getConfig, pbGlobal, pbEvents, videoEvents, gamSubmoduleFactory, videoImpressionVerifierFactory) => { +let pbVideoFactory = (videoCore, getConfig, pbGlobal, pbEvents, videoEvents, gamSubmoduleFactory, videoImpressionVerifierFactory, adQueueCoordinator) => { const pbVideo = PbVideo( videoCore || videoCoreMock, getConfig || getConfigMock, @@ -64,7 +74,8 @@ let pbVideoFactory = (videoCore, getConfig, pbGlobal, pbEvents, videoEvents, gam pbEvents || pbEventsMock, videoEvents || videoEventsMock, gamSubmoduleFactory || gamSubmoduleFactoryMock, - videoImpressionVerifierFactory || videoImpressionVerifierFactoryMock + videoImpressionVerifierFactory || videoImpressionVerifierFactoryMock, + adQueueCoordinator || adQueueCoordinatorMock ); pbVideo.init(); return pbVideo; @@ -163,6 +174,37 @@ describe('Prebid Video', function () { expect(nextFn.calledOnce).to.be.true; expect(nextFn.getCall(0).args[0].ortb2).to.be.deep.equal({ site: { content: { test: 'contentTestValue' } } }); }); + + it('allows publishers to override video param', function () { + const getOrtbVideoSpy = videoCoreMock.getOrtbVideo = sinon.spy(() => ({ + test: 'videoTestValue', + test2: 'videoModuleValue' + })); + + let beforeBidRequestCallback; + const requestBids = { + before: callback_ => beforeBidRequestCallback = callback_ + }; + + pbVideoFactory(null, null, Object.assign({}, pbGlobalMock, { requestBids })); + expect(beforeBidRequestCallback).to.not.be.undefined; + const nextFn = sinon.spy(); + const adUnits = [{ + code: 'ad1', + mediaTypes: { + video: { + test2: 'publisherValue' + } + }, + video: { divId: 'divId' } + }]; + beforeBidRequestCallback(nextFn, { adUnits }); + expect(getOrtbVideoSpy.calledOnce).to.be.true; + const adUnit = adUnits[0]; + expect(adUnit.mediaTypes.video).to.have.property('test', 'videoTestValue'); + expect(adUnit.mediaTypes.video).to.have.property('test2', 'publisherValue'); + expect(nextFn.calledOnce).to.be.true; + }); }); describe('Ad tag injection', function () { @@ -207,6 +249,7 @@ describe('Prebid Video', function () { beforeEach(() => { gamSubmoduleMock.getAdTagUrl.resetHistory(); videoCoreMock.setAdTagUrl.resetHistory(); + adQueueCoordinatorMock.queueAd.resetHistory(); }); let beforeBidRequestCallback; @@ -250,10 +293,10 @@ describe('Prebid Video', function () { pbVideoFactory(null, getConfig, pbGlobal, pbEvents, null, gamSubmoduleFactory); beforeBidRequestCallback(() => {}, {}); auctionEndCallback(auctionResults); - expect(videoCoreMock.setAdTagUrl.calledOnce).to.be.true; - expect(videoCoreMock.setAdTagUrl.args[0][0]).to.be.equal(expectedAdTag); - expect(videoCoreMock.setAdTagUrl.args[0][1]).to.be.equal(expectedDivId); - expect(videoCoreMock.setAdTagUrl.args[0][2]).to.have.property('adUnitCode', expectedAdUnitCode); + expect(adQueueCoordinatorMock.queueAd.calledOnce).to.be.true; + expect(adQueueCoordinatorMock.queueAd.args[0][0]).to.be.equal(expectedAdTag); + expect(adQueueCoordinatorMock.queueAd.args[0][1]).to.be.equal(expectedDivId); + expect(adQueueCoordinatorMock.queueAd.args[0][2]).to.have.property('adUnitCode', expectedAdUnitCode); }); it('should load ad tag from highest bid when ad server is not configured', function () { @@ -275,11 +318,11 @@ describe('Prebid Video', function () { pbVideoFactory(null, () => ({ providers: [] }), pbGlobal, pbEvents); beforeBidRequestCallback(() => {}, {}); auctionEndCallback(auctionResults); - expect(videoCoreMock.setAdTagUrl.calledOnce).to.be.true; - expect(videoCoreMock.setAdTagUrl.args[0][0]).to.be.equal(expectedVastUrl); - expect(videoCoreMock.setAdTagUrl.args[0][1]).to.be.equal(expectedDivId); - expect(videoCoreMock.setAdTagUrl.args[0][2]).to.have.property('adUnitCode', expectedAdUnitCode); - expect(videoCoreMock.setAdTagUrl.args[0][2]).to.have.property('adXml', expectedVastXml); + expect(adQueueCoordinatorMock.queueAd.calledOnce).to.be.true; + expect(adQueueCoordinatorMock.queueAd.args[0][0]).to.be.equal(expectedVastUrl); + expect(adQueueCoordinatorMock.queueAd.args[0][1]).to.be.equal(expectedDivId); + expect(adQueueCoordinatorMock.queueAd.args[0][2]).to.have.property('adUnitCode', expectedAdUnitCode); + expect(adQueueCoordinatorMock.queueAd.args[0][2]).to.have.property('adXml', expectedVastXml); }); }); diff --git a/test/spec/modules/videoModule/shared/parentModule_spec.js b/test/spec/modules/videoModule/shared/parentModule_spec.js index 1e8e7fda380..e3e4cfb7f3f 100644 --- a/test/spec/modules/videoModule/shared/parentModule_spec.js +++ b/test/spec/modules/videoModule/shared/parentModule_spec.js @@ -62,7 +62,6 @@ describe('Submodule Builder', function () { it('should instantiate the submodule, when supported', function () { const submodule = submoduleBuilder.build(vendorCode2); - expect(initSpy.calledOnce).to.be.true; expect(submodule).to.be.equal(submodule2); }); diff --git a/test/spec/modules/videoModule/submodules/jwplayerVideoProvider_spec.js b/test/spec/modules/videoModule/submodules/jwplayerVideoProvider_spec.js index 399c115b820..3cede6c8eda 100644 --- a/test/spec/modules/videoModule/submodules/jwplayerVideoProvider_spec.js +++ b/test/spec/modules/videoModule/submodules/jwplayerVideoProvider_spec.js @@ -14,12 +14,12 @@ import { SETUP_COMPLETE, SETUP_FAILED, PLAY, AD_IMPRESSION, videoEvents } from 'libraries/video/constants/events.js'; -import { PLAYBACK_MODE } from 'libraries/video/constants/enums.js'; +import { PLAYBACK_MODE } from 'libraries/video/constants/constants.js'; function getPlayerMock() { return makePlayerFactoryMock({ getState: function () {}, - setup: function () {}, + setup: function () { return this; }, getViewable: function () {}, getPercentViewable: function () {}, getMute: function () {}, @@ -30,8 +30,8 @@ function getPlayerMock() { getFullscreen: function () {}, getPlaylistItem: function () {}, playAd: function () {}, - on: function () {}, - off: function () {}, + on: function () { return this; }, + off: function () { return this; }, remove: function () {}, getAudioTracks: function () {}, getCurrentAudioTrack: function () {}, @@ -66,7 +66,28 @@ function getUtilsMock() { const sharedUtils = { videoEvents }; +function addDiv() { + const div = document.createElement('div'); + div.setAttribute('id', 'test'); + document.body.appendChild(div); +} + +function removeDiv() { + const div = document.getElementById('test'); + if (div) { + div.remove(); + } +} + describe('JWPlayerProvider', function () { + beforeEach(() => { + addDiv(); + }); + + afterEach(() => { + removeDiv(); + }); + describe('init', function () { let config; let adState; @@ -75,7 +96,7 @@ describe('JWPlayerProvider', function () { let utilsMock; beforeEach(() => { - config = {}; + config = { divId: 'test' }; adState = adStateFactory(); timeState = timeStateFactory(); callbackStorage = callbackStorageFactory(); @@ -104,16 +125,30 @@ describe('JWPlayerProvider', function () { expect(payload.errorCode).to.be.equal(-2); }); - it('should instantiate the player when uninstantied', function () { + it('should trigger failure when div is missing', function () { + removeDiv(); + let jwplayerMock = () => {}; + const provider = JWPlayerProvider(config, jwplayerMock, adState, timeState, callbackStorage, utilsMock, sharedUtils); + const setupFailed = sinon.spy(); + provider.onEvent(SETUP_FAILED, setupFailed, {}); + provider.init(); + expect(setupFailed.calledOnce).to.be.true; + const payload = setupFailed.args[0][1]; + expect(payload.errorCode).to.be.equal(-3); + addDiv(); + addDiv(); + }); + + it('should instantiate the player when uninstantiated', function () { const player = getPlayerMock(); config.playerConfig = {}; - const setupSpy = player.setup = sinon.spy(); + const setupSpy = player.setup = sinon.spy(player.setup); const provider = JWPlayerProvider(config, makePlayerFactoryMock(player), adState, timeState, callbackStorage, utilsMock, sharedUtils); provider.init(); expect(setupSpy.calledOnce).to.be.true; }); - it('should trigger setup complete when player is already instantied', function () { + it('should trigger setup complete when player is already instantiated', function () { const player = getPlayerMock(); player.getState = () => 'idle'; const provider = JWPlayerProvider(config, makePlayerFactoryMock(player), adState, timeState, callbackStorage, utilsMock, sharedUtils); @@ -123,6 +158,19 @@ describe('JWPlayerProvider', function () { expect(setupComplete.calledOnce).to.be.true; }); + it('should support multiple setup complete event handlers', function () { + const player = getPlayerMock(); + player.getState = () => 'idle'; + const provider = JWPlayerProvider(config, makePlayerFactoryMock(player), adState, timeState, callbackStorage, utilsMock, sharedUtils); + const setupComplete = sinon.spy(); + const setupComplete2 = sinon.spy(); + provider.onEvent(SETUP_COMPLETE, setupComplete, {}); + provider.onEvent(SETUP_COMPLETE, setupComplete2, {}); + provider.init(); + expect(setupComplete.calledOnce).to.be.true; + expect(setupComplete2.calledOnce).to.be.true; + }); + it('should not reinstantiate player', function () { const player = getPlayerMock(); player.getState = () => 'idle'; @@ -151,7 +199,7 @@ describe('JWPlayerProvider', function () { const test_playback_method = PLAYBACK_METHODS.CLICK_TO_PLAY; const test_skip = 0; - const config = {}; + const config = { divId: 'test' }; const player = getPlayerMock(); const utils = getUtilsMock(); @@ -230,9 +278,9 @@ describe('JWPlayerProvider', function () { duration: test_duration, playbackMode: test_playback_mode }) - } + }; - const provider = JWPlayerProvider({}, makePlayerFactoryMock(player), adStateFactory(), timeState, {}, utils, sharedUtils); + const provider = JWPlayerProvider({ divId: 'test' }, makePlayerFactoryMock(player), adStateFactory(), timeState, {}, utils, sharedUtils); provider.init(); let content = provider.getOrtbContent(); @@ -260,7 +308,7 @@ describe('JWPlayerProvider', function () { it('should call playAd', function () { const player = getPlayerMock(); const playAdSpy = player.playAd = sinon.spy(); - const provider = JWPlayerProvider({}, makePlayerFactoryMock(player), {}, {}, {}, {}, sharedUtils); + const provider = JWPlayerProvider({ divId: 'test' }, makePlayerFactoryMock(player), {}, {}, {}, {}, sharedUtils); provider.init(); provider.setAdTagUrl('tag'); expect(playAdSpy.called).to.be.true; @@ -273,7 +321,7 @@ describe('JWPlayerProvider', function () { it('should register event listener on player', function () { const player = getPlayerMock(); const onSpy = player.on = sinon.spy(); - const provider = JWPlayerProvider({}, makePlayerFactoryMock(player), adStateFactory(), timeStateFactory(), callbackStorageFactory(), getUtilsMock(), sharedUtils); + const provider = JWPlayerProvider({ divId: 'test' }, makePlayerFactoryMock(player), adStateFactory(), timeStateFactory(), callbackStorageFactory(), getUtilsMock(), sharedUtils); provider.init(); const callback = () => {}; provider.onEvent(PLAY, callback, {}); @@ -285,7 +333,7 @@ describe('JWPlayerProvider', function () { it('should remove event listener on player', function () { const player = getPlayerMock(); const offSpy = player.off = sinon.spy(); - const provider = JWPlayerProvider({}, makePlayerFactoryMock(player), adStateFactory(), timeStateFactory(), callbackStorageFactory(), utils, sharedUtils); + const provider = JWPlayerProvider({ divId: 'test' }, makePlayerFactoryMock(player), adStateFactory(), timeStateFactory(), callbackStorageFactory(), utils, sharedUtils); provider.init(); const callback = () => {}; provider.onEvent(AD_IMPRESSION, callback, {}); @@ -301,7 +349,7 @@ describe('JWPlayerProvider', function () { const player = getPlayerMock(); const removeSpy = player.remove = sinon.spy(); player.remove = removeSpy; - const provider = JWPlayerProvider({}, makePlayerFactoryMock(player), adStateFactory(), timeStateFactory(), callbackStorageFactory(), getUtilsMock(), sharedUtils); + const provider = JWPlayerProvider({ divId: 'test' }, makePlayerFactoryMock(player), adStateFactory(), timeStateFactory(), callbackStorageFactory(), getUtilsMock(), sharedUtils); provider.init(); provider.destroy(); provider.destroy(); diff --git a/test/spec/modules/vidoomyBidAdapter_spec.js b/test/spec/modules/vidoomyBidAdapter_spec.js index 61b7f2fad7d..607fdf4b5b8 100644 --- a/test/spec/modules/vidoomyBidAdapter_spec.js +++ b/test/spec/modules/vidoomyBidAdapter_spec.js @@ -74,6 +74,33 @@ describe('vidoomyBidAdapter', function() { 'sizes': [[300, 250], [200, 100]] } }, + 'schain': { + ver: '1.0', + complete: 1, + nodes: [ + { + 'asi': 'exchange1.com', + 'sid': '1234!abcd', + 'hp': 1, + 'rid': 'bid-request-1', + 'name': 'publisher, Inc.', + 'domain': 'publisher.com' + }, + { + 'asi': 'exchange2.com', + 'sid': 'abcd', + 'hp': 1 + }, + { + 'asi': 'exchange2.com', + 'sid': 'abcd', + 'hp': 1, + 'rid': 'bid-request-2', + 'name': 'intermediary', + 'domain': 'intermediary.com' + } + ] + } }, { 'bidder': 'vidoomy', @@ -128,6 +155,47 @@ describe('vidoomyBidAdapter', function() { expect('' + request[1].data.id).to.equal('456456'); expect('' + request[1].data.pid).to.equal('456456'); }); + + it('should send schain parameter in serialized form', function () { + const serializedForm = '1.0,1!exchange1.com,1234%21abcd,1,bid-request-1,publisher%2C%20Inc.,publisher.com!exchange2.com,abcd,1,,,!exchange2.com,abcd,1,bid-request-2,intermediary,intermediary.com' + expect(request[0].data).to.include.any.keys('schain'); + expect(request[0].data.schain).to.eq(serializedForm); + }); + + it('should set the bidfloor if getFloor module is undefined but static bidfloor is present', function () { + const request = { ...bidRequests[0], params: { bidfloor: 2.5 } } + const req = spec.buildRequests([request], bidderRequest)[0]; + expect(req.data).to.include.any.keys('bidfloor'); + expect(req.data.bidfloor).to.equal(2.5); + }); + + describe('floorModule', function () { + const getFloordata = { + 'currency': 'USD', + 'floor': 1.60 + }; + bidRequests[0].getFloor = _ => { + return getFloordata; + }; + it('should return getFloor.floor if present', function () { + const request = spec.buildRequests(bidRequests, bidderRequest)[0]; + expect(request.data.bidfloor).to.equal(getFloordata.floor); + }); + it('should return the getFloor.floor if it is greater than static bidfloor', function () { + const bidfloor = 1.40; + const request = { ...bidRequests[0] }; + request.params.bidfloor = bidfloor; + const bidRequest = spec.buildRequests([request], bidderRequest)[0]; + expect(bidRequest.data.bidfloor).to.equal(getFloordata.floor); + }); + it('should return the static bidfloor if it is greater than getFloor.floor', function () { + const bidfloor = 1.90; + const request = { ...bidRequests[0] }; + request.params.bidfloor = bidfloor; + const bidRequest = spec.buildRequests([request], bidderRequest)[0]; + expect(bidRequest.data.bidfloor).to.equal(bidfloor); + }); + }); }); describe('interpretResponse', function () { diff --git a/test/spec/modules/visiblemeasuresBidAdapter_spec.js b/test/spec/modules/visiblemeasuresBidAdapter_spec.js new file mode 100644 index 00000000000..ad75e17699f --- /dev/null +++ b/test/spec/modules/visiblemeasuresBidAdapter_spec.js @@ -0,0 +1,401 @@ +import { expect } from 'chai'; +import { spec } from '../../../modules/visiblemeasuresBidAdapter.js'; +import { BANNER, VIDEO, NATIVE } from '../../../src/mediaTypes.js'; +import { getUniqueIdentifierStr } from '../../../src/utils.js'; + +const bidder = 'visiblemeasures' +const adUrl = 'https://us-e.visiblemeasures.com/pbjs'; +const syncUrl = 'https://cs.visiblemeasures.com'; + +describe('VisibleMeasuresBidAdapter', function () { + const bids = [ + { + bidId: getUniqueIdentifierStr(), + bidder: bidder, + mediaTypes: { + [BANNER]: { + sizes: [[300, 250]] + } + }, + params: { + placementId: 'testBanner', + } + }, + { + bidId: getUniqueIdentifierStr(), + bidder: bidder, + mediaTypes: { + [VIDEO]: { + playerSize: [[300, 300]], + minduration: 5, + maxduration: 60 + } + }, + params: { + placementId: 'testVideo', + } + }, + { + bidId: getUniqueIdentifierStr(), + bidder: bidder, + mediaTypes: { + [NATIVE]: { + native: { + title: { + required: true + }, + body: { + required: true + }, + icon: { + required: true, + size: [64, 64] + } + } + } + }, + params: { + placementId: 'testNative', + } + } + ]; + + const invalidBid = { + bidId: getUniqueIdentifierStr(), + bidder: bidder, + mediaTypes: { + [BANNER]: { + sizes: [[300, 250]] + } + }, + params: { + + } + } + + const bidderRequest = { + uspConsent: '1---', + gdprConsent: 'COvFyGBOvFyGBAbAAAENAPCAAOAAAAAAAAAAAEEUACCKAAA.IFoEUQQgAIQwgIwQABAEAAAAOIAACAIAAAAQAIAgEAACEAAAAAgAQBAAAAAAAGBAAgAAAAAAAFAAECAAAgAAQARAEQAAAAAJAAIAAgAAAYQEAAAQmAgBC3ZAYzUw', + refererInfo: { + referer: 'https://test.com' + }, + timeout: 500 + }; + + describe('isBidRequestValid', function () { + it('Should return true if there are bidId, params and key parameters present', function () { + expect(spec.isBidRequestValid(bids[0])).to.be.true; + }); + it('Should return false if at least one of parameters is not present', function () { + expect(spec.isBidRequestValid(invalidBid)).to.be.false; + }); + }); + + describe('buildRequests', function () { + let serverRequest = spec.buildRequests(bids, bidderRequest); + + it('Creates a ServerRequest object with method, URL and data', function () { + expect(serverRequest).to.exist; + expect(serverRequest.method).to.exist; + expect(serverRequest.url).to.exist; + expect(serverRequest.data).to.exist; + }); + + it('Returns POST method', function () { + expect(serverRequest.method).to.equal('POST'); + }); + + it('Returns valid URL', function () { + expect(serverRequest.url).to.equal(adUrl); + }); + + it('Returns general data valid', function () { + let data = serverRequest.data; + expect(data).to.be.an('object'); + expect(data).to.have.all.keys('deviceWidth', + 'deviceHeight', + 'language', + 'secure', + 'host', + 'page', + 'placements', + 'coppa', + 'ccpa', + 'gdpr', + 'tmax' + ); + expect(data.deviceWidth).to.be.a('number'); + expect(data.deviceHeight).to.be.a('number'); + expect(data.language).to.be.a('string'); + expect(data.secure).to.be.within(0, 1); + expect(data.host).to.be.a('string'); + expect(data.page).to.be.a('string'); + expect(data.coppa).to.be.a('number'); + expect(data.gdpr).to.be.a('string'); + expect(data.ccpa).to.be.a('string'); + expect(data.tmax).to.be.a('number'); + expect(data.placements).to.have.lengthOf(3); + }); + + it('Returns valid placements', function () { + const { placements } = serverRequest.data; + for (let i = 0, len = placements.length; i < len; i++) { + const placement = placements[i]; + expect(placement.placementId).to.be.oneOf(['testBanner', 'testVideo', 'testNative']); + expect(placement.adFormat).to.be.oneOf([BANNER, VIDEO, NATIVE]); + expect(placement.bidId).to.be.a('string'); + expect(placement.schain).to.be.an('object'); + expect(placement.bidfloor).to.exist.and.to.equal(0); + expect(placement.type).to.exist.and.to.equal('publisher'); + + if (placement.adFormat === BANNER) { + expect(placement.sizes).to.be.an('array'); + } + switch (placement.adFormat) { + case BANNER: + expect(placement.sizes).to.be.an('array'); + break; + case VIDEO: + expect(placement.playerSize).to.be.an('array'); + expect(placement.minduration).to.be.an('number'); + expect(placement.maxduration).to.be.an('number'); + break; + case NATIVE: + expect(placement.native).to.be.an('object'); + break; + } + } + }); + + it('Returns data with gdprConsent and without uspConsent', function () { + delete bidderRequest.uspConsent; + serverRequest = spec.buildRequests(bids, bidderRequest); + let data = serverRequest.data; + expect(data.gdpr).to.exist; + expect(data.gdpr).to.be.a('string'); + expect(data.gdpr).to.equal(bidderRequest.gdprConsent); + expect(data.ccpa).to.not.exist; + delete bidderRequest.gdprConsent; + }); + + it('Returns data with uspConsent and without gdprConsent', function () { + bidderRequest.uspConsent = '1---'; + delete bidderRequest.gdprConsent; + serverRequest = spec.buildRequests(bids, bidderRequest); + let data = serverRequest.data; + expect(data.ccpa).to.exist; + expect(data.ccpa).to.be.a('string'); + expect(data.ccpa).to.equal(bidderRequest.uspConsent); + expect(data.gdpr).to.not.exist; + }); + + it('Returns empty data if no valid requests are passed', function () { + serverRequest = spec.buildRequests([], bidderRequest); + let data = serverRequest.data; + expect(data.placements).to.be.an('array').that.is.empty; + }); + }); + + describe('interpretResponse', function () { + it('Should interpret banner response', function () { + const banner = { + body: [{ + mediaType: 'banner', + width: 300, + height: 250, + cpm: 0.4, + ad: 'Test', + requestId: '23fhj33i987f', + ttl: 120, + creativeId: '2', + netRevenue: true, + currency: 'USD', + dealId: '1', + meta: { + advertiserDomains: ['google.com'], + advertiserId: 1234 + } + }] + }; + let bannerResponses = spec.interpretResponse(banner); + expect(bannerResponses).to.be.an('array').that.is.not.empty; + let dataItem = bannerResponses[0]; + expect(dataItem).to.have.all.keys('requestId', 'cpm', 'width', 'height', 'ad', 'ttl', 'creativeId', + 'netRevenue', 'currency', 'dealId', 'mediaType', 'meta'); + expect(dataItem.requestId).to.equal(banner.body[0].requestId); + expect(dataItem.cpm).to.equal(banner.body[0].cpm); + expect(dataItem.width).to.equal(banner.body[0].width); + expect(dataItem.height).to.equal(banner.body[0].height); + expect(dataItem.ad).to.equal(banner.body[0].ad); + expect(dataItem.ttl).to.equal(banner.body[0].ttl); + expect(dataItem.creativeId).to.equal(banner.body[0].creativeId); + expect(dataItem.netRevenue).to.be.true; + expect(dataItem.currency).to.equal(banner.body[0].currency); + expect(dataItem.meta).to.be.an('object').that.has.any.key('advertiserDomains'); + }); + it('Should interpret video response', function () { + const video = { + body: [{ + vastUrl: 'test.com', + mediaType: 'video', + cpm: 0.5, + requestId: '23fhj33i987f', + ttl: 120, + creativeId: '2', + netRevenue: true, + currency: 'USD', + dealId: '1', + meta: { + advertiserDomains: ['google.com'], + advertiserId: 1234 + } + }] + }; + let videoResponses = spec.interpretResponse(video); + expect(videoResponses).to.be.an('array').that.is.not.empty; + + let dataItem = videoResponses[0]; + expect(dataItem).to.have.all.keys('requestId', 'cpm', 'vastUrl', 'ttl', 'creativeId', + 'netRevenue', 'currency', 'dealId', 'mediaType', 'meta'); + expect(dataItem.requestId).to.equal('23fhj33i987f'); + expect(dataItem.cpm).to.equal(0.5); + expect(dataItem.vastUrl).to.equal('test.com'); + expect(dataItem.ttl).to.equal(120); + expect(dataItem.creativeId).to.equal('2'); + expect(dataItem.netRevenue).to.be.true; + expect(dataItem.currency).to.equal('USD'); + expect(dataItem.meta).to.be.an('object').that.has.any.key('advertiserDomains'); + }); + it('Should interpret native response', function () { + const native = { + body: [{ + mediaType: 'native', + native: { + clickUrl: 'test.com', + title: 'Test', + image: 'test.com', + impressionTrackers: ['test.com'], + }, + ttl: 120, + cpm: 0.4, + requestId: '23fhj33i987f', + creativeId: '2', + netRevenue: true, + currency: 'USD', + meta: { + advertiserDomains: ['google.com'], + advertiserId: 1234 + } + }] + }; + let nativeResponses = spec.interpretResponse(native); + expect(nativeResponses).to.be.an('array').that.is.not.empty; + + let dataItem = nativeResponses[0]; + expect(dataItem).to.have.keys('requestId', 'cpm', 'ttl', 'creativeId', 'netRevenue', 'currency', 'mediaType', 'native', 'meta'); + expect(dataItem.native).to.have.keys('clickUrl', 'impressionTrackers', 'title', 'image') + expect(dataItem.requestId).to.equal('23fhj33i987f'); + expect(dataItem.cpm).to.equal(0.4); + expect(dataItem.native.clickUrl).to.equal('test.com'); + expect(dataItem.native.title).to.equal('Test'); + expect(dataItem.native.image).to.equal('test.com'); + expect(dataItem.native.impressionTrackers).to.be.an('array').that.is.not.empty; + expect(dataItem.native.impressionTrackers[0]).to.equal('test.com'); + expect(dataItem.ttl).to.equal(120); + expect(dataItem.creativeId).to.equal('2'); + expect(dataItem.netRevenue).to.be.true; + expect(dataItem.currency).to.equal('USD'); + expect(dataItem.meta).to.be.an('object').that.has.any.key('advertiserDomains'); + }); + it('Should return an empty array if invalid banner response is passed', function () { + const invBanner = { + body: [{ + width: 300, + cpm: 0.4, + ad: 'Test', + requestId: '23fhj33i987f', + ttl: 120, + creativeId: '2', + netRevenue: true, + currency: 'USD', + dealId: '1' + }] + }; + + let serverResponses = spec.interpretResponse(invBanner); + expect(serverResponses).to.be.an('array').that.is.empty; + }); + it('Should return an empty array if invalid video response is passed', function () { + const invVideo = { + body: [{ + mediaType: 'video', + cpm: 0.5, + requestId: '23fhj33i987f', + ttl: 120, + creativeId: '2', + netRevenue: true, + currency: 'USD', + dealId: '1' + }] + }; + let serverResponses = spec.interpretResponse(invVideo); + expect(serverResponses).to.be.an('array').that.is.empty; + }); + it('Should return an empty array if invalid native response is passed', function () { + const invNative = { + body: [{ + mediaType: 'native', + clickUrl: 'test.com', + title: 'Test', + impressionTrackers: ['test.com'], + ttl: 120, + requestId: '23fhj33i987f', + creativeId: '2', + netRevenue: true, + currency: 'USD', + }] + }; + let serverResponses = spec.interpretResponse(invNative); + expect(serverResponses).to.be.an('array').that.is.empty; + }); + it('Should return an empty array if invalid response is passed', function () { + const invalid = { + body: [{ + ttl: 120, + creativeId: '2', + netRevenue: true, + currency: 'USD', + dealId: '1' + }] + }; + let serverResponses = spec.interpretResponse(invalid); + expect(serverResponses).to.be.an('array').that.is.empty; + }); + }); + + describe('getUserSyncs', function() { + it('Should return array of objects with proper sync config , include GDPR', function() { + const syncData = spec.getUserSyncs({}, {}, { + consentString: 'ALL', + gdprApplies: true, + }, {}); + expect(syncData).to.be.an('array').which.is.not.empty; + expect(syncData[0]).to.be.an('object') + expect(syncData[0].type).to.be.a('string') + expect(syncData[0].type).to.equal('image') + expect(syncData[0].url).to.be.a('string') + expect(syncData[0].url).to.equal(`${syncUrl}/image?pbjs=1&gdpr=1&gdpr_consent=ALL&coppa=0`) + }); + it('Should return array of objects with proper sync config , include CCPA', function() { + const syncData = spec.getUserSyncs({}, {}, {}, { + consentString: '1---' + }); + expect(syncData).to.be.an('array').which.is.not.empty; + expect(syncData[0]).to.be.an('object') + expect(syncData[0].type).to.be.a('string') + expect(syncData[0].type).to.equal('image') + expect(syncData[0].url).to.be.a('string') + expect(syncData[0].url).to.equal(`${syncUrl}/image?pbjs=1&ccpa_consent=1---&coppa=0`) + }); + }); +}); diff --git a/test/spec/modules/visxBidAdapter_spec.js b/test/spec/modules/visxBidAdapter_spec.js index b8a66e7c3b9..9a486cd6c34 100755 --- a/test/spec/modules/visxBidAdapter_spec.js +++ b/test/spec/modules/visxBidAdapter_spec.js @@ -1273,7 +1273,7 @@ describe('VisxAdapter', function () { it('onTimeout', function () { const data = [{ timeout: 3000, adUnitCode: 'adunit-code-1', auctionId: '1cbd2feafe5e8b', bidder: 'visx', bidId: '23423', params: [{ uid: '1' }] }]; - const expectedData = [{ ...data[0], params: [{ uid: 1 }] }]; + const expectedData = [{ timeout: 3000, params: [{ uid: 1 }] }]; spec.onTimeout(data); expect(utils.triggerPixel.calledOnceWith('https://t.visx.net/track/bid_timeout//' + JSON.stringify(expectedData))).to.equal(true); }); diff --git a/test/spec/modules/vrtcalBidAdapter_spec.js b/test/spec/modules/vrtcalBidAdapter_spec.js index d911745d378..491c96df5e2 100644 --- a/test/spec/modules/vrtcalBidAdapter_spec.js +++ b/test/spec/modules/vrtcalBidAdapter_spec.js @@ -28,7 +28,8 @@ describe('vrtcalBidAdapter', function () { 'bidId': 'bidID0001', 'bidderRequestId': 'br0001', 'auctionId': 'auction0001', - 'userIdAsEids': {} + 'userIdAsEids': {}, + timeout: 435 } ]; @@ -95,7 +96,7 @@ describe('vrtcalBidAdapter', function () { w: 300, h: 250, crid: 'v2_1064_vrt_vrtcaltestdisplay2_300_250', - adomain: ['vrtcal.com'] + adomain: ['vrtcal.com'], }], seat: '16' }], diff --git a/test/spec/modules/yahoosspBidAdapter_spec.js b/test/spec/modules/yahoosspBidAdapter_spec.js index 32bc94b73fa..a326b22ecb4 100644 --- a/test/spec/modules/yahoosspBidAdapter_spec.js +++ b/test/spec/modules/yahoosspBidAdapter_spec.js @@ -3,6 +3,7 @@ import { config } from 'src/config.js'; import { BANNER, VIDEO } from 'src/mediaTypes.js'; import { spec } from 'modules/yahoosspBidAdapter.js'; import {createEidsArray} from '../../../modules/userId/eids'; +import {deepClone} from '../../../src/utils'; const DEFAULT_BID_ID = '84ab500420319d'; const DEFAULT_BID_DCN = '2093845709823475'; @@ -713,7 +714,7 @@ describe('YahooSSP Bid Adapter:', () => { }); }); - describe('GDPR & Consent:', () => { + describe('GDPR & Consent & GPP:', () => { it('should return request objects that do not send cookies if purpose 1 consent is not provided', () => { const { validBidRequests, bidderRequest } = generateBuildRequestMock({}); bidderRequest.gdprConsent = { @@ -731,6 +732,20 @@ describe('YahooSSP Bid Adapter:', () => { const options = spec.buildRequests(validBidRequests, bidderRequest)[0].options; expect(options.withCredentials).to.be.false; }); + + it('adds the ortb2 gpp consent info to the request', function () { + const { validBidRequests, bidderRequest } = generateBuildRequestMock({}); + const ortb2 = { + regs: { + gpp: 'somegppstring', + gpp_sid: [6, 7] + } + }; + let clonedBidderRequest = {...bidderRequest, ortb2}; + const data = spec.buildRequests(validBidRequests, clonedBidderRequest)[0].data; + expect(data.regs.ext.gpp).to.equal('somegppstring'); + expect(data.regs.ext.gpp_sid).to.eql([6, 7]); + }); }); describe('Endpoint & Impression Request Mode:', () => { @@ -857,7 +872,7 @@ describe('YahooSSP Bid Adapter:', () => { expect(data.user.ext.eids).to.deep.equal([ {source: 'admixer.net', uids: [{id: 'admixerId_FROM_USER_ID_MODULE', atype: 3}]}, {source: 'adtelligent.com', uids: [{id: 'adtelligentId_FROM_USER_ID_MODULE', atype: 3}]}, - {source: 'amxrtb.com', uids: [{id: 'amxId_FROM_USER_ID_MODULE', atype: 1}]}, + {source: 'amxdt.net', uids: [{id: 'amxId_FROM_USER_ID_MODULE', atype: 1}]}, {source: 'britepool.com', uids: [{id: 'britepoolid_FROM_USER_ID_MODULE', atype: 3}]}, {source: 'deepintent.com', uids: [{id: 'deepintentId_FROM_USER_ID_MODULE', atype: 3}]}, {source: 'epsilon.com', uids: [{id: 'publinkId_FROM_USER_ID_MODULE', atype: 3}]}, diff --git a/test/spec/modules/yandexBidAdapter_spec.js b/test/spec/modules/yandexBidAdapter_spec.js index df91100b966..12d413a9c93 100644 --- a/test/spec/modules/yandexBidAdapter_spec.js +++ b/test/spec/modules/yandexBidAdapter_spec.js @@ -1,34 +1,10 @@ import { assert, expect } from 'chai'; -import { spec } from 'modules/yandexBidAdapter.js'; +import { spec, NATIVE_ASSETS } from 'modules/yandexBidAdapter.js'; import { parseUrl } from 'src/utils.js'; -import { BANNER } from '../../../src/mediaTypes'; +import { BANNER, NATIVE } from '../../../src/mediaTypes'; +import {OPENRTB} from '../../../modules/rtbhouseBidAdapter'; describe('Yandex adapter', function () { - function getBidConfig() { - return { - bidder: 'yandex', - params: { - placementId: '123-1', - }, - }; - } - - function getBidRequest() { - return { - ...getBidConfig(), - bidId: 'bidid-1', - adUnitCode: 'adUnit-123', - mediaTypes: { - banner: { - sizes: [ - [300, 250], - [300, 600] - ], - }, - }, - }; - } - describe('isBidRequestValid', function () { it('should return true when required params found', function () { const bid = getBidRequest(); @@ -65,19 +41,17 @@ describe('Yandex adapter', function () { }); describe('buildRequests', function () { - const gdprConsent = { - gdprApplies: 1, - consentString: 'concent-string', - apiVersion: 1, - }; - const bidderRequest = { refererInfo: { domain: 'ya.ru', ref: 'https://ya.ru/', page: 'https://ya.ru/', }, - gdprConsent + gdprConsent: { + gdprApplies: 1, + consentString: 'concent-string', + apiVersion: 1, + }, }; it('creates a valid banner request', function () { @@ -101,7 +75,7 @@ describe('Yandex adapter', function () { const { search: query } = parsedRequestUrl expect(parsedRequestUrl.hostname).to.equal('bs.yandex.ru'); - expect(parsedRequestUrl.pathname).to.equal('/metadsp/123'); + expect(parsedRequestUrl.pathname).to.equal('/prebid/123'); expect(query['imp-id']).to.equal('1'); expect(query['target-ref']).to.equal('ya.ru'); @@ -112,21 +86,197 @@ describe('Yandex adapter', function () { expect(request.data).to.exist; expect(data.site).to.not.equal(null); - expect(data.site.page_url).to.equal('https://ya.ru/'); - expect(data.site.ref_url).to.equal('https://ya.ru/'); + expect(data.site.page).to.equal('https://ya.ru/'); + expect(data.site.ref).to.equal('https://ya.ru/'); + }); + + describe('banner', () => { + it('should create valid banner object', () => { + const bannerRequest = getBidRequest({ + mediaTypes: { + banner: { + sizes: [ + [300, 250], + [300, 600] + ], + }, + } + }); - // expect(data.device).to.not.equal(null); - // expect(data.device.w).to.equal(window.innerWidth); - // expect(data.device.h).to.equal(window.innerHeight); + const requests = spec.buildRequests([bannerRequest], bidderRequest); + expect(requests[0].data.imp).to.have.lengthOf(1); - expect(data.imp).to.have.lengthOf(1); - expect(data.imp[0].banner).to.not.equal(null); - expect(data.imp[0].banner.w).to.equal(300); - expect(data.imp[0].banner.h).to.equal(250); + const imp = requests[0].data.imp[0]; + expect(imp.banner).to.not.equal(null); + expect(imp.banner.w).to.equal(300); + expect(imp.banner.h).to.equal(250); + + expect(imp.banner.format).to.deep.equal([ + { w: 300, h: 250 }, + { w: 300, h: 600 }, + ]); + }); }); + + describe('native', () => { + function buildRequestAndGetNativeParams(extra) { + const bannerRequest = getBidRequest(extra); + const requests = spec.buildRequests([bannerRequest], bidderRequest); + + return JSON.parse(requests[0].data.imp[0].native.request); + } + + it('should extract native params', () => { + const nativeParams = buildRequestAndGetNativeParams({ + mediaTypes: { + native: { + title: { + required: true, + len: 100, + }, + body: { + len: 90 + }, + body2: { + len: 90 + }, + sponsoredBy: { + len: 25, + }, + icon: { + sizes: [32, 32], + }, + image: { + required: true, + sizes: [300, 250], + }, + }, + }, + }); + const sortedAssetsList = nativeParams.assets.sort((a, b) => a.id - b.id); + + expect(sortedAssetsList).to.deep.equal([ + { + id: NATIVE_ASSETS.title[0], + required: 1, + title: { + len: 100, + } + }, + { + id: NATIVE_ASSETS.body[0], + data: { + type: NATIVE_ASSETS.body[1], + len: 90, + }, + }, + { + id: NATIVE_ASSETS.body2[0], + data: { + type: NATIVE_ASSETS.body2[1], + len: 90, + }, + }, + { + id: NATIVE_ASSETS.sponsoredBy[0], + data: { + type: NATIVE_ASSETS.sponsoredBy[1], + len: 25, + }, + }, + { + id: NATIVE_ASSETS.icon[0], + img: { + type: NATIVE_ASSETS.icon[1], + w: 32, + h: 32, + }, + }, + { + id: NATIVE_ASSETS.image[0], + required: 1, + img: { + type: NATIVE_ASSETS.image[1], + w: 300, + h: 250, + }, + }, + ]); + }); + + it('should parse multiple image sizes', () => { + const nativeParams = buildRequestAndGetNativeParams({ + mediaTypes: { + native: { + image: { + sizes: [[300, 250], [100, 100]], + }, + }, + }, + }); + + expect(nativeParams.assets[0]).to.deep.equal({ + id: NATIVE_ASSETS.image[0], + img: { + type: NATIVE_ASSETS.image[1], + w: 300, + h: 250, + }, + }); + }); + + it('should parse aspect ratios with min_width', () => { + const nativeParams = buildRequestAndGetNativeParams({ + mediaTypes: { + native: { + image: { + aspect_ratios: [{ + min_width: 320, + ratio_width: 4, + ratio_height: 3, + }], + }, + }, + }, + }); + + expect(nativeParams.assets[0]).to.deep.equal({ + id: NATIVE_ASSETS.image[0], + img: { + type: NATIVE_ASSETS.image[1], + wmin: 320, + hmin: 240, + }, + }); + }); + + it('should parse aspect ratios without min_width', () => { + const nativeParams = buildRequestAndGetNativeParams({ + mediaTypes: { + native: { + image: { + aspect_ratios: [{ + ratio_width: 4, + ratio_height: 3, + }], + }, + }, + }, + }); + + expect(nativeParams.assets[0]).to.deep.equal({ + id: NATIVE_ASSETS.image[0], + img: { + type: NATIVE_ASSETS.image[1], + wmin: 100, + hmin: 75, + }, + }); + }); + }) }); - describe('response handler', function () { + describe('interpretResponse', function () { const bannerRequest = getBidRequest(); const bannerResponse = { @@ -172,5 +322,125 @@ describe('Yandex adapter', function () { expect(rtbBid.meta.advertiserDomains).to.deep.equal(['example.com']); }); + + describe('native', () => { + function getNativeAdmResponse() { + return { + native: { + link: { + url: 'https://example.com' + }, + imptrackers: [ + 'https://example.com/imptracker' + ], + assets: [ + { + title: { + text: 'title text', + }, + id: NATIVE_ASSETS.title[0], + }, + { + data: { + value: 'body text' + }, + id: NATIVE_ASSETS.body[0], + }, + { + data: { + value: 'sponsoredBy text' + }, + id: NATIVE_ASSETS.sponsoredBy[0], + }, + { + img: { + url: 'https://example.com/image', + w: 200, + h: 150, + }, + id: NATIVE_ASSETS.image[0], + }, + { + img: { + url: 'https://example.com/icon', + h: 32, + w: 32 + }, + id: NATIVE_ASSETS.icon[0], + }, + ] + } + }; + } + + it('handles native responses', function() { + bannerRequest.bidRequest = { + mediaType: NATIVE, + bidId: 'bidid-1', + }; + + const nativeAdmResponce = getNativeAdmResponse(); + const bannerResponse = { + body: { + seatbid: [{ + bid: [ + { + impid: 1, + price: 0.3, + adomain: [ + 'example.com' + ], + adid: 'yabs.123=', + adm: JSON.stringify(nativeAdmResponce), + }, + ], + }], + }, + }; + + const result = spec.interpretResponse(bannerResponse, bannerRequest); + + expect(result).to.have.lengthOf(1); + expect(result[0]).to.exist; + + const bid = result[0]; + expect(bid.meta.advertiserDomains).to.deep.equal(['example.com']); + expect(bid.native).to.deep.equal({ + clickUrl: 'https://example.com', + impressionTrackers: ['https://example.com/imptracker'], + title: 'title text', + body: 'body text', + sponsoredBy: 'sponsoredBy text', + image: { + url: 'https://example.com/image', + width: 200, + height: 150, + }, + icon: { + url: 'https://example.com/icon', + width: 32, + height: 32, + }, + }); + }); + }); }); }); + +function getBidConfig() { + return { + bidder: 'yandex', + params: { + placementId: '123-1', + }, + }; +} + +function getBidRequest(extra = {}) { + return { + ...getBidConfig(), + bidId: 'bidid-1', + adUnitCode: 'adUnit-123', + ...extra, + }; +} diff --git a/test/spec/modules/yieldlabBidAdapter_spec.js b/test/spec/modules/yieldlabBidAdapter_spec.js index 5df0e93d34e..e5151cf789c 100644 --- a/test/spec/modules/yieldlabBidAdapter_spec.js +++ b/test/spec/modules/yieldlabBidAdapter_spec.js @@ -1,7 +1,7 @@ import { config } from 'src/config.js'; -import { expect } from 'chai' -import { spec } from 'modules/yieldlabBidAdapter.js' -import { newBidder } from 'src/adapters/bidderFactory.js' +import { expect } from 'chai'; +import { spec } from 'modules/yieldlabBidAdapter.js'; +import { newBidder } from 'src/adapters/bidderFactory.js'; const DEFAULT_REQUEST = () => ({ bidder: 'yieldlab', @@ -11,11 +11,11 @@ const DEFAULT_REQUEST = () => ({ targeting: { key1: 'value1', key2: 'value2', - notDoubleEncoded: 'value3,value4' + notDoubleEncoded: 'value3,value4', }, customParams: { extraParam: true, - foo: 'bar' + foo: 'bar', }, extId: 'abc', iabContent: { @@ -31,8 +31,8 @@ const DEFAULT_REQUEST = () => ({ cat: ['cat1', 'cat2,ppp', 'cat3|||//'], context: '7', keywords: ['k1,', 'k2..'], - live: '0' - } + live: '0', + }, }, bidderRequestId: '143346cf0f1731', auctionId: '2e41f65424c87c', @@ -43,8 +43,14 @@ const DEFAULT_REQUEST = () => ({ source: 'netid.de', uids: [{ id: 'fH5A3n2O8_CZZyPoJVD-eabc6ECb7jhxCicsds7qSg', - atype: 1 - }] + atype: 1, + }], + }, { + source: 'digitrust.de', + uids: [{ + id: 'd8aa10fa-d86c-451d-aad8-5f16162a9e64', + atype: 2, + }], }], schain: { ver: '1.0', @@ -53,32 +59,32 @@ const DEFAULT_REQUEST = () => ({ { asi: 'indirectseller.com', sid: '1', - hp: 1 + hp: 1, }, { asi: 'indirectseller2.com', name: 'indirectseller2 name with comma , and bang !', sid: '2', - hp: 1 - } - ] - } -}) + hp: 1, + }, + ], + }, +}); const VIDEO_REQUEST = () => Object.assign(DEFAULT_REQUEST(), { mediaTypes: { video: { playerSize: [[640, 480]], - context: 'instream' - } - } -}) + context: 'instream', + }, + }, +}); const NATIVE_REQUEST = () => Object.assign(DEFAULT_REQUEST(), { mediaTypes: { - native: {} - } -}) + native: {}, + }, +}); const IAB_REQUEST = () => Object.assign(DEFAULT_REQUEST(), { params: { @@ -113,7 +119,7 @@ const IAB_REQUEST = () => Object.assign(DEFAULT_REQUEST(), { name: 'bar', cattax: 532, cat: [1, 'foo', true], - domain: 'producer.test' + domain: 'producer.test', }, data: { id: 'foo', @@ -123,8 +129,8 @@ const IAB_REQUEST = () => Object.assign(DEFAULT_REQUEST(), { value: 'bar', ext: { foo: { - bar: 'bar' - } + bar: 'bar', + }, }, }, { name: 'foo2', @@ -133,27 +139,27 @@ const IAB_REQUEST = () => Object.assign(DEFAULT_REQUEST(), { test: { nums: { int: 123, - float: 123.123 + float: 123.123, }, bool: true, - string: 'foo2' - } - } + string: 'foo2', + }, + }, }], }, network: { id: 'foo', name: 'bar', - domain: 'network.test' + domain: 'network.test', }, channel: { id: 'bar', name: 'foo', - domain: 'channel.test' - } - } - } -}) + domain: 'channel.test', + }, + }, + }, +}); const RESPONSE = { advertiser: 'yieldlab', @@ -163,169 +169,173 @@ const RESPONSE = { price: 1, pid: 2222, adsize: '728x90', - adtype: 'BANNER' -} + adtype: 'BANNER', +}; const NATIVE_RESPONSE = Object.assign({}, RESPONSE, { adtype: 'NATIVE', native: { link: { - url: 'https://www.yieldlab.de' + url: 'https://www.yieldlab.de', }, assets: [ { id: 1, title: { - text: 'This is a great headline' - } + text: 'This is a great headline', + }, }, { id: 2, img: { url: 'https://localhost:8080/yl-logo100x100.jpg', w: 100, - h: 100 - } + h: 100, + }, }, { id: 3, data: { - value: 'Native body value' - } - } + value: 'Native body value', + }, + }, ], imptrackers: [ 'http://localhost:8080/ve?d=ODE9ZSY2MTI1MjAzNjMzMzYxPXN0JjA0NWUwZDk0NTY5Yi05M2FiLWUwZTQtOWFjNy1hYWY0MzFiZj1kaXQmMj12', 'http://localhost:8080/md/1111/9efa4e76-2030-4f04-bb9f-322541f8d611?mdata=false&pvid=false&ids=x:1', - 'http://localhost:8080/imp?s=13216&d=2171514&a=12548955&ts=1633363025216&tid=fb134faa-7ca9-4e0e-ba39-b96549d0e540&l=0' - ] - } -}) + 'http://localhost:8080/imp?s=13216&d=2171514&a=12548955&ts=1633363025216&tid=fb134faa-7ca9-4e0e-ba39-b96549d0e540&l=0', + ], + }, +}); const VIDEO_RESPONSE = Object.assign({}, RESPONSE, { - adtype: 'VIDEO' -}) + adtype: 'VIDEO', +}); const PVID_RESPONSE = Object.assign({}, VIDEO_RESPONSE, { - pvid: '43513f11-55a0-4a83-94e5-0ebc08f54a2c' -}) + pvid: '43513f11-55a0-4a83-94e5-0ebc08f54a2c', +}); const REQPARAMS = { json: true, - ts: 1234567890 -} + ts: 1234567890, +}; const REQPARAMS_GDPR = Object.assign({}, REQPARAMS, { gdpr: true, - consent: 'BN5lERiOMYEdiAKAWXEND1AAAAE6DABACMA' -}) + consent: 'BN5lERiOMYEdiAKAWXEND1AAAAE6DABACMA', +}); const REQPARAMS_IAB_CONTENT = Object.assign({}, REQPARAMS, { - iab_content: 'id%3Afoo_id%2Cepisode%3A99%2Ctitle%3Afoo_title%252Cbar_title%2Cseries%3Afoo_series%2Cseason%3As1%2Cartist%3Afoo%2520bar%2Cgenre%3Abaz%2Cisrc%3ACC-XXX-YY-NNNNN%2Curl%3Ahttp%253A%252F%252Ffoo_url.de%2Ccat%3Acat1%7Ccat2%252Cppp%7Ccat3%257C%257C%257C%252F%252F%2Ccontext%3A7%2Ckeywords%3Ak1%252C%7Ck2..%2Clive%3A0' -}) + iab_content: 'id%3Afoo_id%2Cepisode%3A99%2Ctitle%3Afoo_title%252Cbar_title%2Cseries%3Afoo_series%2Cseason%3As1%2Cartist%3Afoo%2520bar%2Cgenre%3Abaz%2Cisrc%3ACC-XXX-YY-NNNNN%2Curl%3Ahttp%253A%252F%252Ffoo_url.de%2Ccat%3Acat1%7Ccat2%252Cppp%7Ccat3%257C%257C%257C%252F%252F%2Ccontext%3A7%2Ckeywords%3Ak1%252C%7Ck2..%2Clive%3A0', +}); describe('yieldlabBidAdapter', () => { describe('instantiation from spec', () => { it('is working properly', () => { - const yieldlabBidAdapter = newBidder(spec) - expect(yieldlabBidAdapter.callBids).to.exist.and.to.be.a('function') - }) - }) + const yieldlabBidAdapter = newBidder(spec); + expect(yieldlabBidAdapter.callBids).to.exist.and.to.be.a('function'); + }); + }); describe('isBidRequestValid', () => { it('should return true when all required parameters are found', () => { const request = { params: { adslotId: '1111', - supplyId: '2222' - } - } - expect(spec.isBidRequestValid(request)).to.equal(true) - }) + supplyId: '2222', + }, + }; + expect(spec.isBidRequestValid(request)).to.equal(true); + }); it('should return false when required parameters are missing', () => { - expect(spec.isBidRequestValid({})).to.equal(false) - }) - }) + expect(spec.isBidRequestValid({})).to.equal(false); + }); + }); describe('buildRequests', () => { - const bidRequests = [DEFAULT_REQUEST()] + const bidRequests = [DEFAULT_REQUEST()]; describe('default functionality', () => { - let request + let request; before(() => { - request = spec.buildRequests(bidRequests) - }) + request = spec.buildRequests(bidRequests); + }); it('sends bid request to ENDPOINT via GET', () => { - expect(request.method).to.equal('GET') - }) + expect(request.method).to.equal('GET'); + }); it('returns a list of valid requests', () => { - expect(request.validBidRequests).to.eql(bidRequests) - }) + expect(request.validBidRequests).to.eql(bidRequests); + }); it('passes single-encoded targeting to bid request', () => { - expect(request.url).to.include('t=key1%3Dvalue1%26key2%3Dvalue2%26notDoubleEncoded%3Dvalue3%2Cvalue4') - }) + expect(request.url).to.include('t=key1%3Dvalue1%26key2%3Dvalue2%26notDoubleEncoded%3Dvalue3%2Cvalue4'); + }); it('passes userids to bid request', () => { - expect(request.url).to.include('ids=netid.de%3AfH5A3n2O8_CZZyPoJVD-eabc6ECb7jhxCicsds7qSg') - }) + expect(request.url).to.include('ids=netid.de%3AfH5A3n2O8_CZZyPoJVD-eabc6ECb7jhxCicsds7qSg%2Cdigitrust.de%3Ad8aa10fa-d86c-451d-aad8-5f16162a9e64'); + }); + + it('passes atype to bid request', () => { + expect(request.url).to.include('atypes=netid.de%3A1%2Cdigitrust.de%3A2'); + }); it('passes extra params to bid request', () => { - expect(request.url).to.include('extraParam=true&foo=bar') - }) + expect(request.url).to.include('extraParam=true&foo=bar'); + }); it('passes unencoded schain string to bid request', () => { - expect(request.url).to.include('schain=1.0,1!indirectseller.com,1,1,,,,!indirectseller2.com,2,1,,indirectseller2%20name%20with%20comma%20%2C%20and%20bang%20%21,,') - }) + expect(request.url).to.include('schain=1.0,1!indirectseller.com,1,1,,,,!indirectseller2.com,2,1,,indirectseller2%20name%20with%20comma%20%2C%20and%20bang%20%21,,'); + }); it('passes iab_content string to bid request', () => { - expect(request.url).to.include('iab_content=id%3Afoo_id%2Cepisode%3A99%2Ctitle%3Afoo_title%252Cbar_title%2Cseries%3Afoo_series%2Cseason%3As1%2Cartist%3Afoo%2520bar%2Cgenre%3Abaz%2Cisrc%3ACC-XXX-YY-NNNNN%2Curl%3Ahttp%253A%252F%252Ffoo_url.de%2Ccat%3Acat1%7Ccat2%252Cppp%7Ccat3%257C%257C%257C%252F%252F%2Ccontext%3A7%2Ckeywords%3Ak1%252C%7Ck2..%2Clive%3A0') - }) + expect(request.url).to.include('iab_content=id%3Afoo_id%2Cepisode%3A99%2Ctitle%3Afoo_title%252Cbar_title%2Cseries%3Afoo_series%2Cseason%3As1%2Cartist%3Afoo%2520bar%2Cgenre%3Abaz%2Cisrc%3ACC-XXX-YY-NNNNN%2Curl%3Ahttp%253A%252F%252Ffoo_url.de%2Ccat%3Acat1%7Ccat2%252Cppp%7Ccat3%257C%257C%257C%252F%252F%2Ccontext%3A7%2Ckeywords%3Ak1%252C%7Ck2..%2Clive%3A0'); + }); it('passes correct size to bid request', () => { - expect(request.url).to.include('728x90') - }) + expect(request.url).to.include('728x90'); + }); it('passes external id to bid request', () => { - expect(request.url).to.include('id=abc') - }) - }) + expect(request.url).to.include('id=abc'); + }); + }); describe('iab_content handling', () => { const siteConfig = { ortb2: { site: { content: { - id: 'id_from_config' - } - } - } - } + id: 'id_from_config', + }, + }, + }, + }; beforeEach(() => { - config.setConfig(siteConfig) - }) + config.setConfig(siteConfig); + }); afterEach(() => { - config.resetConfig() - }) + config.resetConfig(); + }); it('generates iab_content string from bidder params', () => { - const request = spec.buildRequests(bidRequests) - expect(request.url).to.include('iab_content=id%3Afoo_id%2Cepisode%3A99%2Ctitle%3Afoo_title%252Cbar_title%2Cseries%3Afoo_series%2Cseason%3As1%2Cartist%3Afoo%2520bar%2Cgenre%3Abaz%2Cisrc%3ACC-XXX-YY-NNNNN%2Curl%3Ahttp%253A%252F%252Ffoo_url.de%2Ccat%3Acat1%7Ccat2%252Cppp%7Ccat3%257C%257C%257C%252F%252F%2Ccontext%3A7%2Ckeywords%3Ak1%252C%7Ck2..%2Clive%3A0') - }) + const request = spec.buildRequests(bidRequests); + expect(request.url).to.include('iab_content=id%3Afoo_id%2Cepisode%3A99%2Ctitle%3Afoo_title%252Cbar_title%2Cseries%3Afoo_series%2Cseason%3As1%2Cartist%3Afoo%2520bar%2Cgenre%3Abaz%2Cisrc%3ACC-XXX-YY-NNNNN%2Curl%3Ahttp%253A%252F%252Ffoo_url.de%2Ccat%3Acat1%7Ccat2%252Cppp%7Ccat3%257C%257C%257C%252F%252F%2Ccontext%3A7%2Ckeywords%3Ak1%252C%7Ck2..%2Clive%3A0'); + }); it('generates iab_content string from first party data if not provided in bidder params', () => { - const requestWithoutIabContent = DEFAULT_REQUEST() - delete requestWithoutIabContent.params.iabContent + const requestWithoutIabContent = DEFAULT_REQUEST(); + delete requestWithoutIabContent.params.iabContent; - const request = spec.buildRequests([{...requestWithoutIabContent, ...siteConfig}]) - expect(request.url).to.include('iab_content=id%3Aid_from_config') - }) + const request = spec.buildRequests([{...requestWithoutIabContent, ...siteConfig}]); + expect(request.url).to.include('iab_content=id%3Aid_from_config'); + }); it('flattens the iabContent, encodes the values, joins the keywords into one value, and than encodes the iab_content request param ', () => { const expectedIabContentValue = encodeURIComponent( @@ -372,18 +382,18 @@ describe('yieldlabBidAdapter', () => { 'channel.id:bar,' + 'channel.name:foo,' + 'channel.domain:channel.test' - ) - const request = spec.buildRequests([IAB_REQUEST()], REQPARAMS) - expect(request.url).to.include('iab_content=' + expectedIabContentValue) - }) - }) + ); + const request = spec.buildRequests([IAB_REQUEST()], REQPARAMS); + expect(request.url).to.include('iab_content=' + expectedIabContentValue); + }); + }); it('passes unencoded schain string to bid request when complete == 0', () => { - const schainRequest = DEFAULT_REQUEST() + const schainRequest = DEFAULT_REQUEST(); schainRequest.schain.complete = 0; // - const request = spec.buildRequests([schainRequest]) - expect(request.url).to.include('schain=1.0,0!indirectseller.com,1,1,,,,!indirectseller2.com,2,1,,indirectseller2%20name%20with%20comma%20%2C%20and%20bang%20%21,,') - }) + const request = spec.buildRequests([schainRequest]); + expect(request.url).to.include('schain=1.0,0!indirectseller.com,1,1,,,,!indirectseller2.com,2,1,,indirectseller2%20name%20with%20comma%20%2C%20and%20bang%20%21,,'); + }); it('passes encoded referer to bid request', () => { const refererRequest = spec.buildRequests(bidRequests, { @@ -392,123 +402,123 @@ describe('yieldlabBidAdapter', () => { numIframes: 0, reachedTop: true, page: 'https://www.yieldlab.de/test?with=querystring', - stack: ['https://www.yieldlab.de/test?with=querystring'] - } - }) + stack: ['https://www.yieldlab.de/test?with=querystring'], + }, + }); - expect(refererRequest.url).to.include('pubref=https%3A%2F%2Fwww.yieldlab.de%2Ftest%3Fwith%3Dquerystring') - }) + expect(refererRequest.url).to.include('pubref=https%3A%2F%2Fwww.yieldlab.de%2Ftest%3Fwith%3Dquerystring'); + }); it('passes gdpr flag and consent if present', () => { const gdprRequest = spec.buildRequests(bidRequests, { gdprConsent: { consentString: 'BN5lERiOMYEdiAKAWXEND1AAAAE6DABACMA', - gdprApplies: true - } - }) + gdprApplies: true, + }, + }); - expect(gdprRequest.url).to.include('consent=BN5lERiOMYEdiAKAWXEND1AAAAE6DABACMA') - expect(gdprRequest.url).to.include('gdpr=true') - }) + expect(gdprRequest.url).to.include('consent=BN5lERiOMYEdiAKAWXEND1AAAAE6DABACMA'); + expect(gdprRequest.url).to.include('gdpr=true'); + }); describe('sizes handling', () => { it('passes correct size to bid request for mediaType banner', () => { const bannerRequest = DEFAULT_REQUEST(); bannerRequest.mediaTypes = { banner: { - sizes: [[123, 456]] - } - } + sizes: [[123, 456]], + }, + }; // when mediaTypes is present it has precedence over the sizes field (728, 90) - let request = spec.buildRequests([bannerRequest], REQPARAMS) - expect(request.url).to.include('sizes') - expect(request.url).to.include('123x456') - - bannerRequest.mediaTypes.banner.sizes = [123, 456] - request = spec.buildRequests([bannerRequest], REQPARAMS) - expect(request.url).to.include('123x456') - - bannerRequest.mediaTypes.banner.sizes = [[123, 456], [320, 240]] - request = spec.buildRequests([bannerRequest], REQPARAMS) - expect(request.url).to.include('123x456') - expect(request.url).to.include('320x240') - }) + let request = spec.buildRequests([bannerRequest], REQPARAMS); + expect(request.url).to.include('sizes'); + expect(request.url).to.include('123x456'); + + bannerRequest.mediaTypes.banner.sizes = [123, 456]; + request = spec.buildRequests([bannerRequest], REQPARAMS); + expect(request.url).to.include('123x456'); + + bannerRequest.mediaTypes.banner.sizes = [[123, 456], [320, 240]]; + request = spec.buildRequests([bannerRequest], REQPARAMS); + expect(request.url).to.include('123x456'); + expect(request.url).to.include('320x240'); + }); it('passes correct sizes to bid request when mediaType is not present', () => { // information is taken from the top level sizes field const sizesRequest = DEFAULT_REQUEST(); - let request = spec.buildRequests([sizesRequest], REQPARAMS) - expect(request.url).to.include('sizes') - expect(request.url).to.include('728x90') + let request = spec.buildRequests([sizesRequest], REQPARAMS); + expect(request.url).to.include('sizes'); + expect(request.url).to.include('728x90'); - sizesRequest.sizes = [[728, 90]] - request = spec.buildRequests([sizesRequest], REQPARAMS) - expect(request.url).to.include('728x90') + sizesRequest.sizes = [[728, 90]]; + request = spec.buildRequests([sizesRequest], REQPARAMS); + expect(request.url).to.include('728x90'); - sizesRequest.sizes = [[728, 90], [320, 240]] - request = spec.buildRequests([sizesRequest], REQPARAMS) - expect(request.url).to.include('728x90') - }) + sizesRequest.sizes = [[728, 90], [320, 240]]; + request = spec.buildRequests([sizesRequest], REQPARAMS); + expect(request.url).to.include('728x90'); + }); it('does not pass the sizes parameter for mediaType video', () => { const videoRequest = VIDEO_REQUEST(); - let request = spec.buildRequests([videoRequest], REQPARAMS) - expect(request.url).to.not.include('sizes') - }) + let request = spec.buildRequests([videoRequest], REQPARAMS); + expect(request.url).to.not.include('sizes'); + }); it('does not pass the sizes parameter for mediaType native', () => { const nativeRequest = NATIVE_REQUEST(); - let request = spec.buildRequests([nativeRequest], REQPARAMS) - expect(request.url).to.not.include('sizes') - }) - }) - }) + let request = spec.buildRequests([nativeRequest], REQPARAMS); + expect(request.url).to.not.include('sizes'); + }); + }); + }); describe('interpretResponse', () => { - let bidRequest + let bidRequest; before(() => { - bidRequest = DEFAULT_REQUEST() - }) + bidRequest = DEFAULT_REQUEST(); + }); it('handles nobid responses', () => { - expect(spec.interpretResponse({body: {}}, {validBidRequests: []}).length).to.equal(0) - expect(spec.interpretResponse({body: []}, {validBidRequests: []}).length).to.equal(0) - }) + expect(spec.interpretResponse({body: {}}, {validBidRequests: []}).length).to.equal(0); + expect(spec.interpretResponse({body: []}, {validBidRequests: []}).length).to.equal(0); + }); it('should get correct bid response', () => { - const result = spec.interpretResponse({body: [RESPONSE]}, {validBidRequests: [bidRequest], queryParams: REQPARAMS}) - - expect(result[0].requestId).to.equal('2d925f27f5079f') - expect(result[0].cpm).to.equal(0.01) - expect(result[0].width).to.equal(728) - expect(result[0].height).to.equal(90) - expect(result[0].creativeId).to.equal('1111') - expect(result[0].dealId).to.equal(2222) - expect(result[0].currency).to.equal('EUR') - expect(result[0].netRevenue).to.equal(false) - expect(result[0].ttl).to.equal(300) - expect(result[0].referrer).to.equal('') - expect(result[0].meta.advertiserDomains).to.equal('yieldlab') - expect(result[0].ad).to.include('