diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 104d9a38132..d4c34929569 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -9,7 +9,9 @@ }, "postCreateCommand": "bash .devcontainer/postCreate.sh", - + // Make is-docker work again + "postStartCommand": "test -f /.dockerenv || sudo touch /.dockerenv", + // Set *default* container specific settings.json values on container create. "settings": {}, diff --git a/.eslintrc.js b/.eslintrc.js index fc3ad3afe66..511e78048e4 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -11,12 +11,25 @@ module.exports = { node: { moduleDirectory: ['node_modules', './'] } + }, + 'jsdoc': { + mode: 'typescript', + tagNamePreference: { + 'tag constructor': 'constructor', + extends: 'extends', + method: 'method', + return: 'return', + } } }, - extends: 'standard', + extends: [ + 'standard', + 'plugin:jsdoc/recommended' + ], plugins: [ 'prebid', - 'import' + 'import', + 'jsdoc' ], globals: { 'BROWSERSTACK_USERNAME': false, @@ -46,6 +59,24 @@ module.exports = { 'no-undef': 2, 'no-useless-escape': 'off', 'no-console': 'error', + 'jsdoc/check-types': 'off', + 'jsdoc/newline-after-description': 'off', + 'jsdoc/require-jsdoc': 'off', + 'jsdoc/require-param': 'off', + 'jsdoc/require-param-description': 'off', + 'jsdoc/require-param-name': 'off', + 'jsdoc/require-param-type': 'off', + 'jsdoc/require-property': 'off', + 'jsdoc/require-property-description': 'off', + 'jsdoc/require-property-name': 'off', + 'jsdoc/require-property-type': 'off', + 'jsdoc/require-returns': 'off', + 'jsdoc/require-returns-check': 'off', + 'jsdoc/require-returns-description': 'off', + 'jsdoc/require-returns-type': 'off', + 'jsdoc/require-yields': 'off', + 'jsdoc/require-yields-check': 'off', + 'jsdoc/tag-lines': 'off' }, overrides: Object.keys(allowedModules).map((key) => ({ files: key + '/**/*.js', diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 584f6f8894a..3bee8f7c947 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -38,11 +38,11 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@v2 + uses: github/codeql-action/init@v3 with: languages: ${{ matrix.language }} config-file: ./.github/codeql/codeql-config.yml @@ -57,7 +57,7 @@ jobs: # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild - uses: github/codeql-action/autobuild@v2 + uses: github/codeql-action/autobuild@v3 # ℹī¸ Command-line programs to run using the OS shell. # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun @@ -70,4 +70,4 @@ jobs: # ./location_of_script_within_repo/buildscript.sh - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v2 + uses: github/codeql-action/analyze@v3 diff --git a/.github/workflows/issue_tracker.yml b/.github/workflows/issue_tracker.yml index 69cf4c5fc7f..b5c59c85160 100644 --- a/.github/workflows/issue_tracker.yml +++ b/.github/workflows/issue_tracker.yml @@ -14,7 +14,7 @@ jobs: steps: - name: Generate token id: generate_token - uses: tibdex/github-app-token@b62528385c34dbc9f38e5f4225ac829252d1ea92 + uses: tibdex/github-app-token@3beb63f4bd073e61482598c45c71c1019b59b73a with: app_id: ${{ secrets.ISSUE_APP_ID }} private_key: ${{ secrets.ISSUE_APP_PEM }} diff --git a/PR_REVIEW.md b/PR_REVIEW.md index 45ca30a7a3d..9deac9963fb 100644 --- a/PR_REVIEW.md +++ b/PR_REVIEW.md @@ -55,7 +55,6 @@ Follow steps above for general review process. In addition, please verify the fo - Adapters that accept a floor parameter must also support the [floors module](https://docs.prebid.org/dev-docs/modules/floors.html) -- look for a call to the `getFloor()` function. - Adapters cannot accept an schain parameter. Rather, they must look for the schain parameter at bidRequest.schain. - The bidderRequest.refererInfo.referer must be checked in addition to any bidder-specific parameter. - - If they're getting the COPPA flag, it must come from config.getConfig('coppa'); - Page position must come from bidrequest.mediaTypes.banner.pos or bidrequest.mediaTypes.video.pos - Global OpenRTB fields should come from [getConfig('ortb2');](https://docs.prebid.org/dev-docs/publisher-api-reference/setConfig.html#setConfig-fpd): - bcat, battr, badv diff --git a/features.json b/features.json index ccb2166a05f..4d8377cda7d 100644 --- a/features.json +++ b/features.json @@ -1,4 +1,5 @@ [ "NATIVE", - "VIDEO" + "VIDEO", + "UID2_CSTG" ] diff --git a/gulpfile.js b/gulpfile.js index 09de874e389..5e16af8b0c1 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -158,6 +158,20 @@ function makeWebpackPkg(extraConfig = {}) { } } +function buildCreative() { + return gulp.src(['**/*']) + .pipe(webpackStream(require('./webpack.creative.js'))) + .pipe(gulp.dest('build/creative')) +} + +function updateCreativeExample(cb) { + const CREATIVE_EXAMPLE = 'integrationExamples/gpt/x-domain/creative.html'; + const root = require('node-html-parser').parse(fs.readFileSync(CREATIVE_EXAMPLE)); + root.querySelectorAll('script')[0].textContent = fs.readFileSync('build/creative/creative.js') + fs.writeFileSync(CREATIVE_EXAMPLE, root.toString()) + cb(); +} + function getModulesListToAddInBanner(modules) { if (!modules || modules.length === helpers.getModuleNames().length) { return 'All available modules for this version.' @@ -405,6 +419,7 @@ function watchTaskMaker(options = {}) { return function watch(done) { var mainWatcher = gulp.watch([ 'src/**/*.js', + 'libraries/**/*.js', 'modules/**/*.js', ].concat(options.alsoWatch)); @@ -415,8 +430,8 @@ function watchTaskMaker(options = {}) { } } -const watch = watchTaskMaker({alsoWatch: ['test/**/*.js'], task: () => gulp.series(clean, gulp.parallel(lint, 'build-bundle-dev', test))}); -const watchFast = watchTaskMaker({livereload: false, task: () => gulp.series('build-bundle-dev')}); +const watch = watchTaskMaker({alsoWatch: ['test/**/*.js'], task: () => gulp.series(clean, gulp.parallel(lint, 'build-bundle-dev', test, buildCreative))}); +const watchFast = watchTaskMaker({livereload: false, task: () => gulp.parallel('build-bundle-dev', buildCreative)}); // support tasks gulp.task(lint); @@ -447,21 +462,23 @@ gulp.task('build-bundle-verbose', gulp.series(makeWebpackPkg({ } }), gulpBundle.bind(null, false))); +gulp.task('build-creative', gulp.series(buildCreative, updateCreativeExample)); + // public tasks (dependencies are needed for each task since they can be ran on their own) gulp.task('test-only', test); gulp.task('test-all-features-disabled', testTaskMaker({disableFeatures: require('./features.json'), oneBrowser: 'chrome', watch: false})); -gulp.task('test', gulp.series(clean, lint, gulp.series('test-all-features-disabled', 'test-only'))); +gulp.task('test', gulp.series(clean, lint, gulp.parallel('build-creative', gulp.series('test-all-features-disabled', 'test-only')))); gulp.task('test-coverage', gulp.series(clean, testCoverage)); gulp.task(viewCoverage); gulp.task('coveralls', gulp.series('test-coverage', coveralls)); -gulp.task('build', gulp.series(clean, 'build-bundle-prod')); +gulp.task('build', gulp.series(clean, 'build-bundle-prod', 'build-creative')); gulp.task('build-postbid', gulp.series(escapePostbidConfig, buildPostbid)); gulp.task('serve', gulp.series(clean, lint, gulp.parallel('build-bundle-dev', watch, test))); -gulp.task('serve-fast', gulp.series(clean, gulp.parallel('build-bundle-dev', watchFast))); +gulp.task('serve-fast', gulp.series(clean, gulp.parallel('build-bundle-dev', buildCreative, watchFast))); gulp.task('serve-prod', gulp.series(clean, gulp.parallel('build-bundle-prod', startLocalServer))); gulp.task('serve-and-test', gulp.series(clean, gulp.parallel('build-bundle-dev', watchFast, testTaskMaker({watch: true})))); gulp.task('serve-e2e', gulp.series(clean, 'build-bundle-prod', gulp.parallel(() => startIntegServer(), startLocalServer))); diff --git a/integrationExamples/gpt/adUnitFloors.html b/integrationExamples/gpt/adUnitFloors.html index bb48a20ef78..a80e1b2380b 100644 --- a/integrationExamples/gpt/adUnitFloors.html +++ b/integrationExamples/gpt/adUnitFloors.html @@ -109,4 +109,3 @@
Div-1
- diff --git a/integrationExamples/gpt/adnuntius_example.html b/integrationExamples/gpt/adnuntius_example.html new file mode 100644 index 00000000000..b61c4e0674e --- /dev/null +++ b/integrationExamples/gpt/adnuntius_example.html @@ -0,0 +1,95 @@ + + + + + + + +

Adnuntius Prebid Adaptor Test

+
Ad Slot 1
+ + +
+ +
+ + + diff --git a/integrationExamples/gpt/contxtfulRtdProvider_example.html b/integrationExamples/gpt/contxtfulRtdProvider_example.html new file mode 100644 index 00000000000..29284de81a2 --- /dev/null +++ b/integrationExamples/gpt/contxtfulRtdProvider_example.html @@ -0,0 +1,91 @@ + + + + + + + + + +

Contxtful RTD Provider

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

Prebid.js Test

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

Prebid.js Test

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

Prebid.js TPMN Banner Example

+ +
+

Prebid.js TPMN Video Example

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

Ad Serverless Test Page

+ + +
+
+ + diff --git a/integrationExamples/gpt/x-domain/creative.html b/integrationExamples/gpt/x-domain/creative.html index 2216d0ed6ae..f1c0c647e72 100644 --- a/integrationExamples/gpt/x-domain/creative.html +++ b/integrationExamples/gpt/x-domain/creative.html @@ -1,105 +1,13 @@ - -} - - -function requestAdFromPrebid() { - const message = JSON.stringify({ - message: 'Prebid Request', - adId - }); - const channel = new MessageChannel(); - channel.port1.onmessage = renderAd; - window.parent.postMessage(message, publisherDomain, [channel.port2]); -} - -function listenAdFromPrebid() { - window.addEventListener('message', receiveMessage, false); -} - -listenAdFromPrebid(); -requestAdFromPrebid(); + diff --git a/integrationExamples/noadserver/native_noadserver.html b/integrationExamples/noadserver/native_noadserver.html new file mode 100755 index 00000000000..81c71d2acfd --- /dev/null +++ b/integrationExamples/noadserver/native_noadserver.html @@ -0,0 +1,173 @@ + + + + + + + + + + + + + +

Prebid Native

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

JW Player with Bids Back Handler override

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

VideoJS with Bids Back Handler override

+
Div-1: Player placeholder div
+ + + + + + diff --git a/integrationExamples/videoModule/videojs/eventListeners.html b/integrationExamples/videoModule/videojs/eventListeners.html index 16edaf4da41..1966f134e02 100644 --- a/integrationExamples/videoModule/videojs/eventListeners.html +++ b/integrationExamples/videoModule/videojs/eventListeners.html @@ -225,7 +225,7 @@ console.log('videos pb bid Error: ', e); }); - pbjs.requestBids(adUnits); + pbjs.requestBids(); }); diff --git a/integrationExamples/videoModule/videojs/eventsUI.html b/integrationExamples/videoModule/videojs/eventsUI.html index 04e0ca9eaf8..9eba09f7a52 100644 --- a/integrationExamples/videoModule/videojs/eventsUI.html +++ b/integrationExamples/videoModule/videojs/eventsUI.html @@ -301,7 +301,7 @@ eventHandler(e, "bidImpression", "auction", ); }); - pbjs.requestBids(adUnits); + pbjs.requestBids(); }); diff --git a/integrationExamples/videoModule/videojs/gamAdServerMediation.html b/integrationExamples/videoModule/videojs/gamAdServerMediation.html index 9efd77e9681..6ffc1a67c03 100644 --- a/integrationExamples/videoModule/videojs/gamAdServerMediation.html +++ b/integrationExamples/videoModule/videojs/gamAdServerMediation.html @@ -123,7 +123,7 @@ console.log('An Ad Impression came from a Bid: ', e); }); - pbjs.requestBids(adUnits); + pbjs.requestBids(); }); diff --git a/integrationExamples/videoModule/videojs/mediaMetadata.html b/integrationExamples/videoModule/videojs/mediaMetadata.html index eef12b0e19d..ede076fd814 100644 --- a/integrationExamples/videoModule/videojs/mediaMetadata.html +++ b/integrationExamples/videoModule/videojs/mediaMetadata.html @@ -87,7 +87,7 @@ console.log('videos pb contentLoaded: ', e); }); - pbjs.requestBids(adUnits); + pbjs.requestBids(); }); diff --git a/integrationExamples/videoModule/videojs/playlist.html b/integrationExamples/videoModule/videojs/playlist.html index a9e3d1bc99b..eb813f095f7 100644 --- a/integrationExamples/videoModule/videojs/playlist.html +++ b/integrationExamples/videoModule/videojs/playlist.html @@ -134,7 +134,7 @@ console.log('videos pb complete: ', e); }); - pbjs.requestBids(adUnits); + pbjs.requestBids(); }); diff --git a/libraries/appnexusKeywords/anKeywords.js b/libraries/appnexusUtils/anKeywords.js similarity index 91% rename from libraries/appnexusKeywords/anKeywords.js rename to libraries/appnexusUtils/anKeywords.js index 5dc0b453253..d6714dacc21 100644 --- a/libraries/appnexusKeywords/anKeywords.js +++ b/libraries/appnexusUtils/anKeywords.js @@ -1,4 +1,4 @@ -import {_each, deepAccess, getValueString, isArray, isStr, mergeDeep, isNumber} from '../../src/utils.js'; +import {_each, deepAccess, isArray, isNumber, isStr, mergeDeep, logWarn} from '../../src/utils.js'; import {getAllOrtbKeywords} from '../keywords/keywords.js'; import {CLIENT_SECTIONS} from '../../src/fpd/oneClient.js'; @@ -12,6 +12,19 @@ const ORTB_SEG_PATHS = ['user.data'].concat( CLIENT_SECTIONS.map((prefix) => `${prefix}.content.data`) ); +function getValueString(param, val, defaultValue) { + if (val === undefined || val === null) { + return defaultValue; + } + if (isStr(val)) { + return val; + } + if (isNumber(val)) { + return val.toString(); + } + logWarn('Unsuported type for param: ' + param + ' required type: String'); +} + /** * Converts an object of arrays (either strings or numbers) into an array of objects containing key and value properties * normally read from bidder params diff --git a/libraries/appnexusUtils/anUtils.js b/libraries/appnexusUtils/anUtils.js new file mode 100644 index 00000000000..9b55cd5c2a4 --- /dev/null +++ b/libraries/appnexusUtils/anUtils.js @@ -0,0 +1,25 @@ +/** + * Converts a string value in camel-case to underscore eg 'placementId' becomes 'placement_id' + * @param {string} value string value to convert + */ +import {deepClone, isPlainObject} from '../../src/utils.js'; + +export function convertCamelToUnderscore(value) { + return value.replace(/(?:^|\.?)([A-Z])/g, function (x, y) { + return '_' + y.toLowerCase(); + }).replace(/^_/, ''); +} + +/** + * Creates an array of n length and fills each item with the given value + */ +export function fill(value, length) { + let newArray = []; + + for (let i = 0; i < length; i++) { + let valueToPush = isPlainObject(value) ? deepClone(value) : value; + newArray.push(valueToPush); + } + + return newArray; +} diff --git a/libraries/chunk/chunk.js b/libraries/chunk/chunk.js new file mode 100644 index 00000000000..57be7bd5016 --- /dev/null +++ b/libraries/chunk/chunk.js @@ -0,0 +1,19 @@ +/** + * http://npm.im/chunk + * Returns an array with *size* chunks from given array + * + * Example: + * ['a', 'b', 'c', 'd', 'e'] chunked by 2 => + * [['a', 'b'], ['c', 'd'], ['e']] + */ +export function chunk(array, size) { + let newArray = []; + + for (let i = 0; i < Math.ceil(array.length / size); i++) { + let start = i * size; + let end = start + size; + newArray.push(array.slice(start, end)); + } + + return newArray; +} diff --git a/libraries/cmp/cmpClient.js b/libraries/cmp/cmpClient.js new file mode 100644 index 00000000000..1d0b327cee4 --- /dev/null +++ b/libraries/cmp/cmpClient.js @@ -0,0 +1,169 @@ +import {GreedyPromise} from '../../src/utils/promise.js'; + +/** + * @typedef {function} CMPClient + * + * @param {{}} params CMP parameters. Currently this is a subset of {command, callback, parameter, version}. + * @param {boolean} once if true, discard cross-frame event listeners once a reply message is received. + * @returns {Promise<*>} a promise to the API's "result" - see the `mode` argument to `cmpClient` on how that's determined. + * @property {boolean} isDirect true if the CMP is directly accessible (no postMessage required) + * @property {() => void} close close the client; currently, this just stops listening for cross-frame messages. + */ + +export const MODE_MIXED = 0; +export const MODE_RETURN = 1; +export const MODE_CALLBACK = 2; + +/** + * Returns a client function that can interface with a CMP regardless of where it's located. + * + * @param {object} obj + * @param obj.apiName name of the CMP api, e.g. "__gpp" + * @param [obj.apiVersion] CMP API version + * @param [obj.apiArgs] names of the arguments taken by the api function, in order. + * @param [obj.callbackArgs] names of the cross-frame response payload properties that should be passed as callback arguments, in order + * @param [obj.mode] controls the callbacks passed to the underlying API, and how the promises returned by the client are resolved. + * + * The client behaves differently when it's provided a `callback` argument vs when it's not - for short, let's name these + * cases "subscriptions" and "one-shot calls" respectively: + * + * With `mode: MODE_MIXED` (the default), promises returned on subscriptions are resolved to undefined when the callback + * is first run (that is, the promise resolves when the CMP replies, but what it replies with is discarded and + * left for the callback to deal with). For one-shot calls, the returned promise is resolved to the API's + * return value when it's directly accessible, or with the result from the first (and, presumably, the only) + * cross-frame reply when it's not; + * + * With `mode: MODE_RETURN`, the returned promise always resolves to the API's return value - which is taken to be undefined + * when cross-frame; + * + * With `mode: MODE_CALLBACK`, the underlying API is expected to never directly return anything significant; instead, + * it should always accept a callback and - for one-shot calls - invoke it only once with the result. The client will + * automatically generate an appropriate callback for one-shot calls and use the result it's given to resolve + * the returned promise. Subscriptions are treated in the same way as MODE_MIXED. + * + * @param win + * @returns {CMPClient} CMP invocation function (or null if no CMP was found). + */ +export function cmpClient( + { + apiName, + apiVersion, + apiArgs = ['command', 'callback', 'parameter', 'version'], + callbackArgs = ['returnValue', 'success'], + mode = MODE_MIXED, + }, + win = window +) { + const cmpCallbacks = {}; + const callName = `${apiName}Call`; + const cmpDataPkgName = `${apiName}Return`; + + function handleMessage(event) { + const json = (typeof event.data === 'string' && event.data.includes(cmpDataPkgName)) ? JSON.parse(event.data) : event.data; + if (json?.[cmpDataPkgName]?.callId) { + const payload = json[cmpDataPkgName]; + + if (cmpCallbacks.hasOwnProperty(payload.callId)) { + cmpCallbacks[payload.callId](...callbackArgs.map(name => payload[name])); + } + } + } + + function findCMP() { + let f = win; + let cmpFrame; + let isDirect = false; + while (f != null) { + try { + if (typeof f[apiName] === 'function') { + cmpFrame = f; + isDirect = true; + break; + } + } catch (e) { + } + + // need separate try/catch blocks due to the exception errors thrown when trying to check for a frame that doesn't exist in 3rd party env + try { + if (f.frames[`${apiName}Locator`]) { + cmpFrame = f; + break; + } + } catch (e) { + } + + if (f === win.top) break; + f = f.parent; + } + + return [ + cmpFrame, + isDirect + ]; + } + + const [cmpFrame, isDirect] = findCMP(); + + if (!cmpFrame) { + return; + } + + function resolveParams(params) { + params = Object.assign({version: apiVersion}, params); + return apiArgs.map(arg => [arg, params[arg]]) + } + + function wrapCallback(callback, resolve, reject, preamble) { + const haveCb = typeof callback === 'function'; + + return function (result, success) { + preamble && preamble(); + if (mode !== MODE_RETURN) { + const resolver = success == null || success ? resolve : reject; + resolver(haveCb ? undefined : result); + } + haveCb && callback.apply(this, arguments); + } + } + + let client; + + if (isDirect) { + client = function invokeCMPDirect(params = {}) { + return new GreedyPromise((resolve, reject) => { + const ret = cmpFrame[apiName](...resolveParams({ + ...params, + callback: (params.callback || mode === MODE_CALLBACK) ? wrapCallback(params.callback, resolve, reject) : undefined, + }).map(([_, val]) => val)); + if (mode === MODE_RETURN || (params.callback == null && mode === MODE_MIXED)) { + resolve(ret); + } + }); + }; + } else { + win.addEventListener('message', handleMessage, false); + + client = function invokeCMPFrame(params, once = false) { + return new GreedyPromise((resolve, reject) => { + // call CMP via postMessage + const callId = Math.random().toString(); + const msg = { + [callName]: { + ...Object.fromEntries(resolveParams(params).filter(([param]) => param !== 'callback')), + callId: callId + } + }; + + cmpCallbacks[callId] = wrapCallback(params?.callback, resolve, reject, (once || params?.callback == null) && (() => { delete cmpCallbacks[callId] })); + cmpFrame.postMessage(msg, '*'); + if (mode === MODE_RETURN) resolve(); + }); + }; + } + return Object.assign(client, { + isDirect, + close() { + !isDirect && win.removeEventListener('message', handleMessage); + } + }) +} diff --git a/libraries/creativeRender/constants.js b/libraries/creativeRender/constants.js new file mode 100644 index 00000000000..7b67f8ed5cd --- /dev/null +++ b/libraries/creativeRender/constants.js @@ -0,0 +1,10 @@ +import events from '../../src/constants.json'; + +export const PREBID_NATIVE = 'Prebid Native'; +export const PREBID_REQUEST = 'Prebid Request'; +export const PREBID_RESPONSE = 'Prebid Response'; +export const PREBID_EVENT = 'Prebid Event'; +export const AD_RENDER_SUCCEEDED = events.EVENTS.AD_RENDER_SUCCEEDED; +export const AD_RENDER_FAILED = events.EVENTS.AD_RENDER_FAILED; +export const NO_AD = events.AD_RENDER_FAILED_REASON.NO_AD; +export const EXCEPTION = events.AD_RENDER_FAILED_REASON.EXCEPTION; diff --git a/libraries/creativeRender/crossDomain.js b/libraries/creativeRender/crossDomain.js new file mode 100644 index 00000000000..ffa8b468f12 --- /dev/null +++ b/libraries/creativeRender/crossDomain.js @@ -0,0 +1,57 @@ +import {mkFrame, writeAd} from './writer.js'; +import { + AD_RENDER_FAILED, + AD_RENDER_SUCCEEDED, + PREBID_EVENT, + PREBID_RESPONSE, + PREBID_REQUEST, + EXCEPTION +} from './constants.js'; + +export function renderer(win = window) { + return function ({adId, pubUrl, clickUrl}) { + const pubDomain = (function() { + const a = win.document.createElement('a'); + a.href = pubUrl; + return a.protocol + '//' + a.host; + })(); + function sendMessage(type, payload, transfer) { + win.parent.postMessage(JSON.stringify(Object.assign({message: type, adId}, payload)), pubDomain, transfer); + } + function cb(err) { + sendMessage(PREBID_EVENT, { + event: err == null ? AD_RENDER_SUCCEEDED : AD_RENDER_FAILED, + info: err + }); + } + function onMessage(ev) { + let data = {}; + try { + data = JSON.parse(ev[ev.message ? 'message' : 'data']); + } catch (e) { + return; + } + if (data.message === PREBID_RESPONSE && data.adId === adId) { + try { + let doc = win.document + if (data.ad) { + doc = mkFrame(doc, {width: data.width, height: data.height}).contentDocument; + doc.open(); + } + writeAd(data, cb, doc); + } catch (e) { + // eslint-disable-next-line standard/no-callback-literal + cb({ reason: EXCEPTION, message: e.message }) + } + } + } + + const channel = new MessageChannel(); + channel.port1.onmessage = onMessage; + sendMessage(PREBID_REQUEST, { + options: {clickUrl} + }, [channel.port2]); + win.addEventListener('message', onMessage, false); + } +} +window.renderAd = renderer(); diff --git a/libraries/creativeRender/direct.js b/libraries/creativeRender/direct.js new file mode 100644 index 00000000000..19d34e16844 --- /dev/null +++ b/libraries/creativeRender/direct.js @@ -0,0 +1,62 @@ +import {emitAdRenderFail, emitAdRenderSucceeded, handleRender} from '../../src/adRendering.js'; +import {writeAd} from './writer.js'; +import {auctionManager} from '../../src/auctionManager.js'; +import CONSTANTS from '../../src/constants.json'; +import {inIframe, insertElement} from '../../src/utils.js'; +import {getGlobal} from '../../src/prebidGlobal.js'; +import {EXCEPTION} from './constants.js'; + +export function renderAdDirect(doc, adId, options) { + let bid; + function cb(err) { + if (err != null) { + emitAdRenderFail(Object.assign({id: adId, bid}, err)); + } else { + emitAdRenderSucceeded({doc, bid, adId}) + } + } + function renderFn(adData) { + writeAd(adData, cb, doc); + if (doc.defaultView && doc.defaultView.frameElement) { + doc.defaultView.frameElement.width = adData.width; + doc.defaultView.frameElement.height = adData.height; + } + // TODO: this is almost certainly the wrong way to do this + const creativeComment = document.createComment(`Creative ${bid.creativeId} served by ${bid.bidder} Prebid.js Header Bidding`); + insertElement(creativeComment, doc, 'html'); + } + try { + if (!adId || !doc) { + // eslint-disable-next-line standard/no-callback-literal + cb({ + reason: CONSTANTS.AD_RENDER_FAILED_REASON.MISSING_DOC_OR_ADID, + message: `missing ${adId ? 'doc' : 'adId'}` + }); + } else { + bid = auctionManager.findBidByAdId(adId); + + if (FEATURES.VIDEO) { + // TODO: could the video module implement this as a custom renderer, rather than a special case in here? + const adUnit = bid && auctionManager.index.getAdUnit(bid); + const videoModule = getGlobal().videoModule; + if (adUnit?.video && videoModule) { + videoModule.renderBid(adUnit.video.divId, bid); + return; + } + } + + if ((doc === document && !inIframe())) { + // eslint-disable-next-line standard/no-callback-literal + cb({ + reason: CONSTANTS.AD_RENDER_FAILED_REASON.PREVENT_WRITING_ON_MAIN_DOCUMENT, + message: `renderAd was prevented from writing to the main document.` + }) + } else { + handleRender(renderFn, {adId, options: {clickUrl: options?.clickThrough}, bidResponse: bid, doc}); + } + } + } catch (e) { + // eslint-disable-next-line standard/no-callback-literal + cb({reason: EXCEPTION, message: e.message}) + } +} diff --git a/libraries/creativeRender/writer.js b/libraries/creativeRender/writer.js new file mode 100644 index 00000000000..80bb0592a1f --- /dev/null +++ b/libraries/creativeRender/writer.js @@ -0,0 +1,34 @@ +import {NO_AD} from './constants.js'; + +const IFRAME_ATTRS = { + frameBorder: 0, + scrolling: 'no', + marginHeight: 0, + marginWidth: 0, + topMargin: 0, + leftMargin: 0, + allowTransparency: 'true', +}; + +export function mkFrame(doc, attrs) { + const frame = doc.createElement('iframe'); + attrs = Object.assign({}, attrs, IFRAME_ATTRS); + Object.entries(attrs).forEach(([k, v]) => frame.setAttribute(k, v)); + doc.body.appendChild(frame); + return frame; +} + +export function writeAd({ad, adUrl, width, height}, cb, doc = document) { + if (!ad && !adUrl) { + // eslint-disable-next-line standard/no-callback-literal + cb({reason: NO_AD, message: 'Missing ad markup or URL'}); + } else { + if (adUrl && !ad) { + mkFrame(doc, {width, height, src: adUrl}) + } else { + doc.write(ad); + doc.close(); + } + cb(); + } +} diff --git a/libraries/currencyUtils/currency.js b/libraries/currencyUtils/currency.js new file mode 100644 index 00000000000..924f8f200d8 --- /dev/null +++ b/libraries/currencyUtils/currency.js @@ -0,0 +1,31 @@ +import {getGlobal} from '../../src/prebidGlobal.js'; +import {keyCompare} from '../../src/utils/reducers.js'; + +/** + * Attempt to convert `amount` from the currency `fromCur` to the currency `toCur`. + * + * By default, when the conversion is not possible (currency module not present or + * throwing errors), the amount is returned unchanged. This behavior can be + * toggled off with bestEffort = false. + */ +export function convertCurrency(amount, fromCur, toCur, bestEffort = true) { + if (fromCur === toCur) return amount; + let result = amount; + try { + result = getGlobal().convertCurrency(amount, fromCur, toCur); + } catch (e) { + if (!bestEffort) throw e; + } + return result; +} + +export function currencyNormalizer(toCurrency = null, bestEffort = true, convert = convertCurrency) { + return function (amount, currency) { + if (toCurrency == null) toCurrency = currency; + return convert(amount, currency, toCurrency, bestEffort); + } +} + +export function currencyCompare(get = (obj) => [obj.cpm, obj.currency], normalize = currencyNormalizer()) { + return keyCompare(obj => normalize.apply(null, get(obj))) +} diff --git a/libraries/gptUtils/gptUtils.js b/libraries/gptUtils/gptUtils.js new file mode 100644 index 00000000000..950f28c618f --- /dev/null +++ b/libraries/gptUtils/gptUtils.js @@ -0,0 +1,37 @@ +import {find} from '../../src/polyfill.js'; +import {compareCodeAndSlot, isGptPubadsDefined} from '../../src/utils.js'; + +/** + * Returns filter function to match adUnitCode in slot + * @param {string} adUnitCode AdUnit code + * @return {function} filter function + */ +export function isSlotMatchingAdUnitCode(adUnitCode) { + return (slot) => compareCodeAndSlot(slot, adUnitCode); +} + +/** + * @summary Uses the adUnit's code in order to find a matching gpt slot object on the page + */ +export function getGptSlotForAdUnitCode(adUnitCode) { + let matchingSlot; + if (isGptPubadsDefined()) { + // find the first matching gpt slot on the page + matchingSlot = find(window.googletag.pubads().getSlots(), isSlotMatchingAdUnitCode(adUnitCode)); + } + return matchingSlot; +} + +/** + * @summary Uses the adUnit's code in order to find a matching gptSlot on the page + */ +export function getGptSlotInfoForAdUnitCode(adUnitCode) { + const matchingSlot = getGptSlotForAdUnitCode(adUnitCode); + if (matchingSlot) { + return { + gptSlot: matchingSlot.getAdUnitPath(), + divId: matchingSlot.getSlotElementId() + }; + } + return {}; +} diff --git a/libraries/htmlEscape/htmlEscape.js b/libraries/htmlEscape/htmlEscape.js new file mode 100644 index 00000000000..f0952c02e3c --- /dev/null +++ b/libraries/htmlEscape/htmlEscape.js @@ -0,0 +1,26 @@ +/** + * Encode a string for inclusion in HTML. + * See https://pragmaticwebsecurity.com/articles/spasecurity/json-stringify-xss.html and + * https://codeql.github.com/codeql-query-help/javascript/js-bad-code-sanitization/ + * @return {string} + */ +export const escapeUnsafeChars = (() => { + const escapes = { + '<': '\\u003C', + '>': '\\u003E', + '/': '\\u002F', + '\\': '\\\\', + '\b': '\\b', + '\f': '\\f', + '\n': '\\n', + '\r': '\\r', + '\t': '\\t', + '\0': '\\0', + '\u2028': '\\u2028', + '\u2029': '\\u2029' + }; + + return function (str) { + return str.replace(/[<>\b\f\n\r\t\0\u2028\u2029\\]/g, x => escapes[x]); + }; +})(); diff --git a/libraries/mspa/activityControls.js b/libraries/mspa/activityControls.js new file mode 100644 index 00000000000..eaf515e2385 --- /dev/null +++ b/libraries/mspa/activityControls.js @@ -0,0 +1,127 @@ +import {registerActivityControl} from '../../src/activities/rules.js'; +import { + ACTIVITY_ENRICH_EIDS, + ACTIVITY_ENRICH_UFPD, + ACTIVITY_SYNC_USER, + ACTIVITY_TRANSMIT_PRECISE_GEO +} from '../../src/activities/activities.js'; +import {gppDataHandler} from '../../src/adapterManager.js'; +import {logInfo} from '../../src/utils.js'; + +// default interpretation for MSPA consent(s): +// https://docs.prebid.org/features/mspa-usnat.html + +const SENSITIVE_DATA_GEO = 7; + +function isApplicable(val) { + return val != null && val !== 0 +} + +export function isBasicConsentDenied(cd) { + // service provider mode is always consent denied + return ['MspaServiceProviderMode', 'Gpc'].some(prop => cd[prop] === 1) || + // you cannot consent to what you were not notified of + cd.PersonalDataConsents === 2 || + // minors 13+ who have not given consent + cd.KnownChildSensitiveDataConsents[0] === 1 || + // minors under 13 cannot consent + isApplicable(cd.KnownChildSensitiveDataConsents[1]) || + // covered cannot be zero + cd.MspaCoveredTransaction === 0; +} + +export function sensitiveNoticeIs(cd, value) { + return ['SensitiveDataProcessingOptOutNotice', 'SensitiveDataLimitUseNotice'].some(prop => cd[prop] === value) +} + +export function isConsentDenied(cd) { + return isBasicConsentDenied(cd) || + ['Sale', 'Sharing', 'TargetedAdvertising'].some(scope => { + const oo = cd[`${scope}OptOut`]; + const notice = cd[`${scope}OptOutNotice`]; + // user opted out + return oo === 1 || + // opt-out notice was not given + notice === 2 || + // do not trust CMP if it signals opt-in but no opt-out notice was given + (oo === 2 && notice === 0); + }) || + // no sharing notice was given ... + cd.SharingNotice === 2 || + // ... or the CMP says it was not applicable, while also claiming it got consent + (cd.SharingOptOut === 2 && cd.SharingNotice === 0); +} + +export const isTransmitUfpdConsentDenied = (() => { + // deny anything that smells like: genetic, biometric, state/national ID, financial, union membership, + // or personal communication data + const cannotBeInScope = [6, 7, 9, 10, 12].map(el => --el); + // require consent for everything else (except geo, which is treated separately) + const allExceptGeo = Array.from(Array(12).keys()).filter((el) => el !== SENSITIVE_DATA_GEO) + const mustHaveConsent = allExceptGeo.filter(el => !cannotBeInScope.includes(el)); + + return function (cd) { + return isConsentDenied(cd) || + // no notice about sensitive data was given + sensitiveNoticeIs(cd, 2) || + // extra-sensitive data is applicable + cannotBeInScope.some(i => isApplicable(cd.SensitiveDataProcessing[i])) || + // user opted out for not-as-sensitive data + mustHaveConsent.some(i => cd.SensitiveDataProcessing[i] === 1) || + // CMP says it has consent, but did not give notice about it + (sensitiveNoticeIs(cd, 0) && allExceptGeo.some(i => cd.SensitiveDataProcessing[i] === 2)) + } +})(); + +export function isTransmitGeoConsentDenied(cd) { + const geoConsent = cd.SensitiveDataProcessing[SENSITIVE_DATA_GEO]; + return geoConsent === 1 || + isBasicConsentDenied(cd) || + // no sensitive data notice was given + sensitiveNoticeIs(cd, 2) || + // do not trust CMP if it says it has consent for geo but didn't show a sensitive data notice + (sensitiveNoticeIs(cd, 0) && geoConsent === 2) +} + +const CONSENT_RULES = { + [ACTIVITY_SYNC_USER]: isConsentDenied, + [ACTIVITY_ENRICH_EIDS]: isConsentDenied, + [ACTIVITY_ENRICH_UFPD]: isTransmitUfpdConsentDenied, + [ACTIVITY_TRANSMIT_PRECISE_GEO]: isTransmitGeoConsentDenied +}; + +export function mspaRule(sids, getConsent, denies, applicableSids = () => gppDataHandler.getConsentData()?.applicableSections) { + return function () { + if (applicableSids().some(sid => sids.includes(sid))) { + const consent = getConsent(); + if (consent == null) { + return {allow: false, reason: 'consent data not available'}; + } + if (denies(consent)) { + return {allow: false}; + } + } + }; +} + +function flatSection(subsections) { + if (subsections == null) return subsections; + return subsections.reduceRight((subsection, consent) => { + return Object.assign(consent, subsection); + }, {}); +} + +export function setupRules(api, sids, normalizeConsent = (c) => c, rules = CONSENT_RULES, registerRule = registerActivityControl, getConsentData = () => gppDataHandler.getConsentData()) { + const unreg = []; + const ruleName = `MSPA (GPP '${api}' for section${sids.length > 1 ? 's' : ''} ${sids.join(', ')})`; + logInfo(`Enabling activity controls for ${ruleName}`) + Object.entries(rules).forEach(([activity, denies]) => { + unreg.push(registerRule(activity, ruleName, mspaRule( + sids, + () => normalizeConsent(flatSection(getConsentData()?.parsedSections?.[api])), + denies, + () => getConsentData()?.applicableSections || [] + ))); + }); + return () => unreg.forEach(ur => ur()); +} diff --git a/libraries/ortbConverter/README.md b/libraries/ortbConverter/README.md index 31f56b4c754..751971eebdc 100644 --- a/libraries/ortbConverter/README.md +++ b/libraries/ortbConverter/README.md @@ -80,8 +80,7 @@ However, there are two restrictions (to avoid them, use the [other customization ) ``` - -### Fine grained customization - imp, request, bidResponse, response +### Fine grained customization - imp, request, bidResponse, response When invoked, `toORTB({bidRequests, bidderRequest})` first loops through each request in `bidRequests`, converting them into ORTB `imp` objects. It then packages them into a single ORTB request, adding other parameters that are not imp-specific (such as for example `request.tmax`). @@ -91,7 +90,7 @@ a single return value. You can customize each of these steps using the `ortbConverter` arguments `imp`, `request`, `bidResponse` and `response`: -### Customizing imps: `imp(buildImp, bidRequest, context)` +### Customizing imps: `imp(buildImp, bidRequest, context)` Invoked once for each input `bidRequest`; should return the ORTB `imp` object to include in the request. The arguments are: @@ -101,7 +100,7 @@ The arguments are: - `context`: a [context object](#context) that contains at least: - `bidderRequest`: the `bidderRequest` argument passed to `toORTB`. -#### Example: attaching custom bid params +#### Example: attaching custom bid params ```javascript const converter = ortbConverter({ @@ -351,7 +350,7 @@ const converter = ortbConverter({ - the `context` argument of `ortbConverter`: e.g. `ortbConverter({context: {ttl: 30}})`. This will set `context.ttl = 30` globally for the converter. - the `context` argument of `toORTB`: e.g. `converter.toORTB({bidRequests, bidderRequest, context: {ttl: 30}})`. This will set `context.ttl = 30` only for this request. -### Special `context` properties +### Special `context` properties For ease of use, the conversion logic gives special meaning to some context properties: diff --git a/libraries/sizeUtils/sizeUtils.js b/libraries/sizeUtils/sizeUtils.js new file mode 100644 index 00000000000..41cdd71df89 --- /dev/null +++ b/libraries/sizeUtils/sizeUtils.js @@ -0,0 +1,29 @@ +/** + * Read an adUnit object and return the sizes used in an [[728, 90]] format (even if they had [728, 90] defined) + * Preference is given to the `adUnit.mediaTypes.banner.sizes` object over the `adUnit.sizes` + * @param {object} adUnit one adUnit object from the normal list of adUnits + * @returns {Array.} array of arrays containing numeric sizes + */ +export function getAdUnitSizes(adUnit) { + if (!adUnit) { + return; + } + + let sizes = []; + if (adUnit.mediaTypes && adUnit.mediaTypes.banner && Array.isArray(adUnit.mediaTypes.banner.sizes)) { + let bannerSizes = adUnit.mediaTypes.banner.sizes; + if (Array.isArray(bannerSizes[0])) { + sizes = bannerSizes; + } else { + sizes.push(bannerSizes); + } + // TODO - remove this else block when we're ready to deprecate adUnit.sizes for bidders + } else if (Array.isArray(adUnit.sizes)) { + if (Array.isArray(adUnit.sizes[0])) { + sizes = adUnit.sizes; + } else { + sizes.push(adUnit.sizes); + } + } + return sizes; +} diff --git a/libraries/transformParamsUtils/convertTypes.js b/libraries/transformParamsUtils/convertTypes.js new file mode 100644 index 00000000000..813d8e6e693 --- /dev/null +++ b/libraries/transformParamsUtils/convertTypes.js @@ -0,0 +1,36 @@ +import {isFn} from '../../src/utils.js'; + +/** + * Try to convert a value to a type. + * If it can't be done, the value will be returned. + * + * @param {string} typeToConvert The target type. e.g. "string", "number", etc. + * @param {*} value The value to be converted into typeToConvert. + */ +function tryConvertType(typeToConvert, value) { + if (typeToConvert === 'string') { + return value && value.toString(); + } else if (typeToConvert === 'number') { + return Number(value); + } else { + return value; + } +} + +export function convertTypes(types, params) { + Object.keys(types).forEach(key => { + if (params[key]) { + if (isFn(types[key])) { + params[key] = types[key](params[key]); + } else { + params[key] = tryConvertType(types[key], params[key]); + } + + // don't send invalid values + if (isNaN(params[key])) { + delete params.key; + } + } + }); + return params; +} diff --git a/libraries/uid2Eids/uid2Eids.js b/libraries/uid2Eids/uid2Eids.js new file mode 100644 index 00000000000..ce4f4fa3b2a --- /dev/null +++ b/libraries/uid2Eids/uid2Eids.js @@ -0,0 +1,14 @@ +export const UID2_EIDS = { + 'uid2': { + source: 'uidapi.com', + atype: 3, + getValue: function(data) { + return data.id; + }, + getUidExt: function(data) { + if (data.ext) { + return data.ext; + } + } + } +} diff --git a/libraries/urlUtils/urlUtils.js b/libraries/urlUtils/urlUtils.js new file mode 100644 index 00000000000..f0c5823aab1 --- /dev/null +++ b/libraries/urlUtils/urlUtils.js @@ -0,0 +1,7 @@ +export function tryAppendQueryString(existingUrl, key, value) { + if (value) { + return existingUrl + key + '=' + encodeURIComponent(value) + '&'; + } + + return existingUrl; +} diff --git a/modules/.submodules.json b/modules/.submodules.json index b45fb7f2303..d2a13a57330 100644 --- a/modules/.submodules.json +++ b/modules/.submodules.json @@ -46,7 +46,9 @@ "zeotapIdPlusIdSystem", "adqueryIdSystem", "gravitoIdSystem", - "freepassIdSystem" + "freepassIdSystem", + "operaadsIdSystem", + "mygaruIdSystem" ], "adpod": [ "freeWheelAdserverVideo", @@ -54,30 +56,44 @@ ], "rtdModule": [ "1plusXRtdProvider", + "a1MediaRtdProvider", "aaxBlockmeterRtdProvider", + "adlooxRtdProvider", + "adnuntiusRtdProvider", "airgridRtdProvider", "akamaiDapRtdProvider", "arcspanRtdProvider", "blueconicRtdProvider", + "brandmetricsRtdProvider", "browsiRtdProvider", "captifyRtdProvider", + "mediafilterRtdProvider", "confiantRtdProvider", "dgkeywordRtdProvider", + "experianRtdProvider", "geoedgeRtdProvider", + "geolocationRtdProvider", + "greenbidsRtdProvider", + "growthCodeRtdProvider", "hadronRtdProvider", - "haloRtdProvider", "iasRtdProvider", + "idWardRtdProvider", + "imRtdProvider", + "intersectionRtdProvider", "jwplayerRtdProvider", "medianetRtdProvider", "mgidRtdProvider", + "neuwoRtdProvider", "oneKeyRtdProvider", "optimeraRtdProvider", + "oxxionRtdProvider", "permutiveRtdProvider", + "qortexRtdProvider", "reconciliationRtdProvider", + "relevadRtdProvider", "sirdataRtdProvider", "timeoutRtdProvider", - "weboramaRtdProvider", - "zeusPrimeRtdProvider" + "weboramaRtdProvider" ], "fpdModule": [ "validationFpdModule", diff --git a/modules/33acrossAnalyticsAdapter.js b/modules/33acrossAnalyticsAdapter.js new file mode 100644 index 00000000000..e3539906b13 --- /dev/null +++ b/modules/33acrossAnalyticsAdapter.js @@ -0,0 +1,656 @@ +import { deepAccess, logInfo, logWarn, logError, deepClone } from '../src/utils.js'; +import buildAdapter from '../libraries/analyticsAdapter/AnalyticsAdapter.js'; +import adapterManager, { coppaDataHandler, gdprDataHandler, gppDataHandler, uspDataHandler } from '../src/adapterManager.js'; +import CONSTANTS from '../src/constants.json'; + +/** + * @typedef {typeof import('../src/constants.json').EVENTS} EVENTS + */ +const { EVENTS } = CONSTANTS; + +/** @typedef {'pending'|'available'|'targetingSet'|'rendered'|'timeout'|'rejected'|'noBid'|'error'} BidStatus */ +/** + * @type {Object} + */ +const BidStatus = { + PENDING: 'pending', + AVAILABLE: 'available', + TARGETING_SET: 'targetingSet', + RENDERED: 'rendered', + TIMEOUT: 'timeout', + REJECTED: 'rejected', + NOBID: 'noBid', + ERROR: 'error', +} + +const ANALYTICS_VERSION = '1.0.0'; +const PROVIDER_NAME = '33across'; +const GVLID = 58; +/** Time to wait for all transactions in an auction to complete before sending the report */ +const DEFAULT_TRANSACTION_TIMEOUT = 10000; +/** Time to wait after all GAM slots have registered before sending the report */ +export const POST_GAM_TIMEOUT = 500; +export const DEFAULT_ENDPOINT = 'https://analytics.33across.com/api/v1/event'; + +export const log = getLogger(); + +/** + * @typedef {Object} AnalyticsReport - Sent when all bids are complete (as determined by `bidWon` and `slotRenderEnded` events) + * @property {string} analyticsVersion - Version of the Prebid.js 33Across Analytics Adapter + * @property {string} pid - Partner ID + * @property {string} src - Source of the report ('pbjs') + * @property {string} pbjsVersion - Version of Prebid.js + * @property {Auction[]} auctions + */ + +/** + * @typedef {Object} AnalyticsCache + * @property {string} pid Partner ID + * @property {Object} auctions + * @property {string} [usPrivacy] + */ + +/** + * @typedef {Object} Auction - Parsed auction data + * @property {AdUnit[]} adUnits + * @property {string} auctionId + * @property {string[]} userIds + */ + +/** + * @typedef {`${number}x${number}`} AdUnitSize + */ + +/** + * @typedef {('banner'|'native'|'video')} AdUnitMediaType + */ + +/** + * @typedef {Object} BidResponse + * @property {number} cpm + * @property {string} cur + * @property {number} [cpmOrig] + * @property {number} cpmFloor + * @property {AdUnitMediaType} mediaType + * @property {AdUnitSize} size + */ + +/** + * @typedef {Object} Bid - Parsed bid data + * @property {string} bidder + * @property {string} bidId + * @property {string} source + * @property {string} status + * @property {BidResponse} [bidResponse] + * @property {1|0} [hasWon] + */ + +/** + * @typedef {Object} AdUnit - Parsed adUnit data + * @property {string} transactionId - Primary key for *this* auction/adUnit combination + * @property {string} adUnitCode + * @property {string} slotId - Equivalent to GPID. (Note that + * GPID supports adUnits where multiple units have the same `code` values + * by appending a `#UNIQUIFIER`. The value of the UNIQUIFIER is likely to be the div-id, + * but, if div-id is randomized / unavailable, may be something else like the media size) + * @property {Array} mediaTypes + * @property {Array} sizes + * @property {Array} bids + */ + +/** + * After the first transaction begins, wait until all transactions are complete + * before calling `onComplete`. If the timeout is reached before all transactions + * are complete, send the report anyway. + * + * Use this to track all transactions per auction, and send the report as soon + * as all adUnits have been won (or after timeout) even if other bid/auction + * activity is still happening. + */ +class TransactionManager { + /** + * Milliseconds between activity to allow until this collection automatically completes. + * @type {number} + */ + #sendTimeout; + #sendTimeoutId; + #transactionsPending = new Set(); + #transactionsCompleted = new Set(); + #onComplete; + + constructor({ timeout, onComplete }) { + this.#sendTimeout = timeout; + this.#onComplete = onComplete; + } + + status() { + return { + pending: [...this.#transactionsPending], + completed: [...this.#transactionsCompleted], + }; + } + + initiate(transactionId) { + this.#transactionsPending.add(transactionId); + this.#restartSendTimeout(); + } + + complete(transactionId) { + if (!this.#transactionsPending.has(transactionId)) { + log.warn(`transactionId "${transactionId}" was not found. No transaction to mark as complete.`); + return; + } + + this.#transactionsPending.delete(transactionId); + this.#transactionsCompleted.add(transactionId); + + if (this.#transactionsPending.size === 0) { + this.#flushTransactions(); + } + } + + #flushTransactions() { + this.#clearSendTimeout(); + this.#transactionsPending = new Set(); + this.#onComplete(); + } + + // gulp-eslint is using eslint 6, a version that doesn't support private method syntax + // eslint-disable-next-line no-dupe-class-members + #clearSendTimeout() { + return clearTimeout(this.#sendTimeoutId); + } + + // eslint-disable-next-line no-dupe-class-members + #restartSendTimeout() { + this.#clearSendTimeout(); + + this.#sendTimeoutId = setTimeout(() => { + if (this.#sendTimeout !== 0) { + log.warn(`Timed out waiting for ad transactions to complete. Sending report.`); + } + + this.#flushTransactions(); + }, this.#sendTimeout); + } +} + +/** + * Initialized during `enableAnalytics`. Exported for testing purposes. + */ +export const locals = { + /** @type {Object} - one manager per auction */ + transactionManagers: {}, + /** @type {AnalyticsCache} */ + cache: { + auctions: {}, + pid: '', + }, + /** @type {Object} */ + adUnitMap: {}, + reset() { + this.transactionManagers = {}; + this.cache = { + auctions: {}, + pid: '', + }; + this.adUnitMap = {}; + } +} + +/** + * @typedef {Object} AnalyticsAdapter + * @property {function} track + * @property {function} enableAnalytics + * @property {function} disableAnalytics + * @property {function} [originEnableAnalytics] + * @property {function} [originDisableAnalytics] + * @property {function} [_oldEnable] + */ + +/** + * @type {AnalyticsAdapter} + */ +const analyticsAdapter = Object.assign( + buildAdapter({ analyticsType: 'endpoint' }), + { track: analyticEventHandler } +); + +analyticsAdapter.originEnableAnalytics = analyticsAdapter.enableAnalytics; +analyticsAdapter.enableAnalytics = enableAnalyticsWrapper; + +/** + * @typedef {Object} AnalyticsConfig + * @property {string} provider - set by pbjs at module registration time + * @property {Object} options + * @property {string} options.pid - Publisher/Partner ID + * @property {string} [options.endpoint=DEFAULT_ENDPOINT] - Endpoint to send analytics data + * @property {number} [options.timeout=DEFAULT_TRANSACTION_TIMEOUT] - Timeout for sending analytics data + */ + +/** + * @param {AnalyticsConfig} config Analytics module configuration + */ +function enableAnalyticsWrapper(config) { + const { options } = config; + + const pid = options.pid; + if (!pid) { + log.error('No partnerId provided for "options.pid". No analytics will be sent.'); + + return; + } + + const endpoint = calculateEndpoint(options.endpoint); + this.getUrl = () => endpoint; + + const timeout = calculateTransactionTimeout(options.timeout); + this.getTimeout = () => timeout; + + locals.cache = { + pid, + auctions: {}, + }; + + window.googletag = window.googletag || { cmd: [] }; + window.googletag.cmd.push(subscribeToGamSlots); + + analyticsAdapter.originEnableAnalytics(config); +} + +/** + * @param {string} [endpoint] + * @returns {string} + */ +function calculateEndpoint(endpoint = DEFAULT_ENDPOINT) { + if (typeof endpoint === 'string' && endpoint.startsWith('http')) { + return endpoint; + } + + log.info(`Invalid endpoint provided for "options.endpoint". Using default endpoint.`); + + return DEFAULT_ENDPOINT; +} +/** + * @param {number} [configTimeout] + * @returns {number} Transaction Timeout + */ +function calculateTransactionTimeout(configTimeout = DEFAULT_TRANSACTION_TIMEOUT) { + if (typeof configTimeout === 'number' && configTimeout >= 0) { + return configTimeout; + } + + log.info(`Invalid timeout provided for "options.timeout". Using default timeout of ${DEFAULT_TRANSACTION_TIMEOUT}ms.`); + + return DEFAULT_TRANSACTION_TIMEOUT; +} + +function subscribeToGamSlots() { + window.googletag.pubads().addEventListener('slotRenderEnded', event => { + setTimeout(() => { + const { transactionId, auctionId } = + getAdUnitMetadata(event.slot.getAdUnitPath(), event.slot.getSlotElementId()); + if (!transactionId || !auctionId) { + const slotName = `${event.slot.getAdUnitPath()} - ${event.slot.getSlotElementId()}`; + log.warn('Could not find configured ad unit matching GAM render of slot:', { slotName }); + return; + } + + locals.transactionManagers[auctionId] && + locals.transactionManagers[auctionId].complete(transactionId); + }, POST_GAM_TIMEOUT); + }); +} + +function getAdUnitMetadata(adUnitPath, adSlotElementId) { + const adUnitMeta = locals.adUnitMap[adUnitPath] || locals.adUnitMap[adSlotElementId]; + if (adUnitMeta && adUnitMeta.length > 0) { + return adUnitMeta[adUnitMeta.length - 1]; + } + return {}; +} + +/** necessary for testing */ +analyticsAdapter.originDisableAnalytics = analyticsAdapter.disableAnalytics; +analyticsAdapter.disableAnalytics = function () { + analyticsAdapter._oldEnable = enableAnalyticsWrapper; + locals.reset(); + analyticsAdapter.originDisableAnalytics(); +}; + +adapterManager.registerAnalyticsAdapter({ + adapter: analyticsAdapter, + code: PROVIDER_NAME, + gvlid: GVLID, +}); + +export default analyticsAdapter; + +/** + * @param {AnalyticsCache} analyticsCache + * @param {string} completedAuctionId value of auctionId + * @return {AnalyticsReport} Analytics report + */ +function createReportFromCache(analyticsCache, completedAuctionId) { + const { pid, auctions } = analyticsCache; + + const report = { + pid, + src: 'pbjs', + analyticsVersion: ANALYTICS_VERSION, + pbjsVersion: '$prebid.version$', // Replaced by build script + auctions: [ auctions[completedAuctionId] ], + } + if (uspDataHandler.getConsentData()) { + report.usPrivacy = uspDataHandler.getConsentData(); + } + + if (gdprDataHandler.getConsentData()) { + report.gdpr = Number(Boolean(gdprDataHandler.getConsentData().gdprApplies)); + report.gdprConsent = gdprDataHandler.getConsentData().consentString || ''; + } + + if (gppDataHandler.getConsentData()) { + report.gpp = gppDataHandler.getConsentData().gppString; + report.gppSid = gppDataHandler.getConsentData().applicableSections; + } + + if (coppaDataHandler.getCoppa()) { + report.coppa = Number(coppaDataHandler.getCoppa()); + } + + return report; +} + +function getCachedBid(auctionId, bidId) { + const auction = locals.cache.auctions[auctionId]; + for (let adUnit of auction.adUnits) { + for (let bid of adUnit.bids) { + if (bid.bidId === bidId) { + return bid; + } + } + } + log.error(`Cannot find bid "${bidId}" in auction "${auctionId}".`); +}; + +/** + * @param {Object} args + * @param {Object} args.args Event data + * @param {EVENTS[keyof EVENTS]} args.eventType + */ +function analyticEventHandler({ eventType, args }) { + if (!locals.cache) { + log.error('Something went wrong. Analytics cache is not initialized.'); + return; + } + + switch (eventType) { + case EVENTS.AUCTION_INIT: + onAuctionInit(args); + break; + case EVENTS.BID_REQUESTED: // BidStatus.PENDING + onBidRequested(args); + break; + case EVENTS.BID_TIMEOUT: + for (let bid of args) { + setCachedBidStatus(bid.auctionId, bid.bidId, BidStatus.TIMEOUT); + } + break; + case EVENTS.BID_RESPONSE: + onBidResponse(args); + break; + case EVENTS.BID_REJECTED: + onBidRejected(args); + break; + case EVENTS.NO_BID: + case EVENTS.SEAT_NON_BID: + setCachedBidStatus(args.auctionId, args.bidId, BidStatus.NOBID); + break; + case EVENTS.BIDDER_ERROR: + if (args.bidderRequest && args.bidderRequest.bids) { + for (let bid of args.bidderRequest.bids) { + setCachedBidStatus(args.bidderRequest.auctionId, bid.bidId, BidStatus.ERROR); + } + } + break; + case EVENTS.AUCTION_END: + onAuctionEnd(args); + break; + case EVENTS.BID_WON: // BidStatus.TARGETING_SET | BidStatus.RENDERED | BidStatus.ERROR + onBidWon(args); + break; + default: + break; + } +} + +/**************** + * AUCTION_INIT * + ***************/ +function onAuctionInit({ adUnits, auctionId, bidderRequests }) { + if (typeof auctionId !== 'string' || !Array.isArray(bidderRequests)) { + log.error('Analytics adapter failed to parse auction.'); + return; + } + + locals.cache.auctions[auctionId] = { + auctionId, + adUnits: adUnits.map(au => { + setAdUnitMap(au.code, auctionId, au.transactionId); + + return { + transactionId: au.transactionId, + adUnitCode: au.code, + // Note: GPID supports adUnits that have matching `code` values by appending a `#UNIQUIFIER`. + // The value of the UNIQUIFIER is likely to be the div-id, + // but, if div-id is randomized / unavailable, may be something else like the media size) + slotId: deepAccess(au, 'ortb2Imp.ext.gpid') || deepAccess(au, 'ortb2Imp.ext.data.pbadslot', au.code), + mediaTypes: Object.keys(au.mediaTypes), + sizes: au.sizes.map(size => size.join('x')), + bids: [], + } + }), + userIds: Object.keys(deepAccess(bidderRequests, '0.bids.0.userId', {})), + }; + + locals.transactionManagers[auctionId] ||= + new TransactionManager({ + timeout: analyticsAdapter.getTimeout(), + onComplete() { + sendReport( + createReportFromCache(locals.cache, auctionId), + analyticsAdapter.getUrl() + ); + delete locals.transactionManagers[auctionId]; + } + }); +} + +function setAdUnitMap(adUnitCode, auctionId, transactionId) { + if (!locals.adUnitMap[adUnitCode]) { + locals.adUnitMap[adUnitCode] = []; + } + + locals.adUnitMap[adUnitCode].push({ auctionId, transactionId }); +} + +/***************** + * BID_REQUESTED * + ****************/ +function onBidRequested({ auctionId, bids }) { + for (let { bidder, bidId, transactionId, src } of bids) { + const auction = locals.cache.auctions[auctionId]; + const adUnit = auction.adUnits.find(adUnit => adUnit.transactionId === transactionId); + if (!adUnit) return; + adUnit.bids.push({ + bidder, + bidId, + status: BidStatus.PENDING, + hasWon: 0, + source: src, + }); + + // if there is no manager for this auction, then the auction has already been completed + locals.transactionManagers[auctionId] && + locals.transactionManagers[auctionId].initiate(transactionId); + } +} + +/**************** + * BID_RESPONSE * + ***************/ +function onBidResponse({ requestId, auctionId, cpm, currency, originalCpm, floorData, mediaType, size, status, source }) { + const bid = getCachedBid(auctionId, requestId); + if (!bid) return; + + setBidStatus(bid, status); + Object.assign(bid, + { + bidResponse: { + cpm, + cur: currency, + cpmOrig: originalCpm, + cpmFloor: floorData?.floorValue, + mediaType, + size + }, + source + } + ); +} + +/**************** + * BID_REJECTED * + ***************/ +function onBidRejected({ requestId, auctionId, cpm, currency, originalCpm, floorData, mediaType, width, height, source }) { + const bid = getCachedBid(auctionId, requestId); + if (!bid) return; + + setBidStatus(bid, BidStatus.REJECTED); + Object.assign(bid, + { + bidResponse: { + cpm, + cur: currency, + cpmOrig: originalCpm, + cpmFloor: floorData?.floorValue, + mediaType, + size: `${width}x${height}` + }, + source + } + ); +} + +/*************** + * AUCTION_END * + **************/ +/** + * @param {Object} args + * @param {{requestId: string, status: string}[]} args.bidsReceived + * @param {string} args.auctionId + * @returns {void} + */ +function onAuctionEnd({ bidsReceived, auctionId }) { + for (let bid of bidsReceived) { + setCachedBidStatus(auctionId, bid.requestId, bid.status); + } +} + +/*********** + * BID_WON * + **********/ +function onBidWon(bidWon) { + const { auctionId, requestId, transactionId } = bidWon; + const bid = getCachedBid(auctionId, requestId); + if (!bid) { + return; + } + + setBidStatus(bid, bidWon.status ?? BidStatus.ERROR); + + locals.transactionManagers[auctionId] && + locals.transactionManagers[auctionId].complete(transactionId); +} + +/** + * @param {Bid} bid + * @param {BidStatus} [status] + * @returns {void} + */ +function setBidStatus(bid, status = BidStatus.AVAILABLE) { + const statusStates = { + pending: { + next: [BidStatus.AVAILABLE, BidStatus.TARGETING_SET, BidStatus.RENDERED, BidStatus.TIMEOUT, BidStatus.REJECTED, BidStatus.NOBID, BidStatus.ERROR], + }, + available: { + next: [BidStatus.TARGETING_SET, BidStatus.RENDERED, BidStatus.TIMEOUT, BidStatus.REJECTED, BidStatus.NOBID, BidStatus.ERROR], + }, + targetingSet: { + next: [BidStatus.RENDERED, BidStatus.ERROR, BidStatus.TIMEOUT], + }, + rendered: { + next: [], + }, + timeout: { + next: [], + }, + rejected: { + next: [], + }, + noBid: { + next: [], + }, + error: { + next: [BidStatus.TARGETING_SET, BidStatus.RENDERED, BidStatus.TIMEOUT, BidStatus.REJECTED, BidStatus.NOBID, BidStatus.ERROR], + }, + } + + const winningStatuses = [BidStatus.RENDERED]; + + if (statusStates[bid.status].next.includes(status)) { + bid.status = status; + if (winningStatuses.includes(status)) { + // occassionally we can detect a bidWon before prebid reports it as such + bid.hasWon = 1; + } + } +} + +function setCachedBidStatus(auctionId, bidId, status) { + const bid = getCachedBid(auctionId, bidId); + if (!bid) return; + setBidStatus(bid, status); +} + +/** + * Guarantees sending of data without waiting for response, even after page is left/closed + * + * @param {AnalyticsReport} report Request payload + * @param {string} endpoint URL + */ +function sendReport(report, endpoint) { + if (navigator.sendBeacon(endpoint, JSON.stringify(report))) { + log.info(`Analytics report sent to ${endpoint}`, report); + + return; + } + + log.error('Analytics report exceeded User-Agent data limits and was not sent.', report); +} + +/** + * Encapsulate certain logger functions and add a prefix to the final messages. + * + * @return {Object} New logger functions + */ +function getLogger() { + const LPREFIX = `${PROVIDER_NAME} Analytics: `; + + return { + info: (msg, ...args) => logInfo(`${LPREFIX}${msg}`, ...deepClone(args)), + warn: (msg, ...args) => logWarn(`${LPREFIX}${msg}`, ...deepClone(args)), + error: (msg, ...args) => logError(`${LPREFIX}${msg}`, ...deepClone(args)), + } +} diff --git a/modules/33acrossAnalyticsAdapter.md b/modules/33acrossAnalyticsAdapter.md new file mode 100644 index 00000000000..c56059e5526 --- /dev/null +++ b/modules/33acrossAnalyticsAdapter.md @@ -0,0 +1,76 @@ +# Overview + +```txt +Module Name: 33Across Analytics Adapter +Module Type: Analytics Adapter +Maintainer: analytics_support@33across.com +``` + +#### About + +This analytics adapter collects data about the performance of your ad slots +for each auction run on your site. It also provides insight into how identifiers +from the +[33Across User ID Sub-module](https://docs.prebid.org/dev-docs/modules/userid-submodules/33across.html) +and other user ID sub-modules improve your monetization. The data is sent at +the earliest opportunity for each auction to provide a more complete picture of +your ad performance. + +The analytics adapter is free to use! +However, the publisher must work with our account management team to obtain a +Publisher/Partner ID (PID) and enable Analytics for their account. +To get a PID and to have the publisher account enabled for Analytics, +you can reach out to our team at the following email - analytics_support@33across.com + +If you are an existing publisher and you already use a 33Across PID, +you can reach out to analytics_support@33across.com +to have your account enabled for analytics. + +The 33Across privacy policy is at . + +#### Analytics Options + +| Name | Scope | Example | Type | Description | +|-----------|----------|---------|----------|-------------| +| `pid` | required | abc123 | `string` | 33Across Publisher ID | +| `timeout` | optional | 10000 | `int` | Milliseconds to wait after last seen auction transaction before sending report (default 10000). | + +#### Configuration + +The data is sent at the earliest opportunity for each auction to provide +a more complete picture of your ad performance, even if the auction is interrupted +by a page navigation. At the latest, the adapter will always send the report +when the page is unloaded, at the end of the auction, or after the timeout, +whichever comes first. + +In order to guarantee consistent reports of your ad slot behavior, we recommend +including the GPT Pre-Auction Module, `gptPreAuction`. This module is included +by default when Prebid is downloaded. If you are compiling from source, +this might look something like: + +```sh +gulp bundle --modules=gptPreAuction,consentManagement,consentManagementGpp,consentManagementUsp,enrichmentFpdModule,gdprEnforcement,33acrossBidAdapter,33acrossIdSystem,33acrossAnalyticsAdapter +``` + +Enable the 33Across Analytics Adapter in Prebid.js using the analytics provider `33across` +and options as seen in the example below. + +#### Example Configuration + +```js +pbjs.enableAnalytics({ + provider: '33across', + options: { + /** + * The 33Across Publisher ID. + */ + pid: 'abc123', + /** + * Timeout in milliseconds after which an auction report + * will be sent regardless of auction state. + * [optional] + */ + timeout: 10000 + } +}); +``` diff --git a/modules/33acrossBidAdapter.js b/modules/33acrossBidAdapter.js index 26fc0a11fd5..0e9beb22013 100644 --- a/modules/33acrossBidAdapter.js +++ b/modules/33acrossBidAdapter.js @@ -6,7 +6,6 @@ import { getWindowTop, isArray, isGptPubadsDefined, - isSlotMatchingAdUnitCode, logInfo, logWarn, mergeDeep, @@ -14,6 +13,7 @@ import { uniques } from '../src/utils.js'; import {BANNER, VIDEO} from '../src/mediaTypes.js'; +import {isSlotMatchingAdUnitCode} from '../libraries/gptUtils/gptUtils.js'; // **************************** UTILS *************************** // const BIDDER_CODE = '33across'; @@ -167,6 +167,7 @@ function buildRequests(bidRequests, bidderRequest) { ttxSettings, gdprConsent, uspConsent, + gppConsent, pageUrl, referer } = _buildRequestParams(bidRequests, bidderRequest); @@ -181,6 +182,7 @@ function buildRequests(bidRequests, bidderRequest) { bidRequests: groupedRequests[key], gdprConsent, uspConsent, + gppConsent, pageUrl, referer, ttxSettings, @@ -200,20 +202,15 @@ function _buildRequestParams(bidRequests, bidderRequest) { gdprApplies: false }, bidderRequest && bidderRequest.gdprConsent); - const uspConsent = bidderRequest && bidderRequest.uspConsent; - - const pageUrl = bidderRequest?.refererInfo?.page; - - const referer = bidderRequest?.refererInfo?.ref; - adapterState.uniqueSiteIds = bidRequests.map(req => req.params.siteId).filter(uniques); return { ttxSettings, gdprConsent, - uspConsent, - pageUrl, - referer + uspConsent: bidderRequest?.uspConsent, + gppConsent: bidderRequest?.gppConsent, + pageUrl: bidderRequest?.refererInfo?.page, + referer: bidderRequest?.refererInfo?.ref } } @@ -247,10 +244,11 @@ function _getMRAKey(bidRequest) { } // Infer the necessary data from valid bid for a minimal ttxRequest and create HTTP request -function _createServerRequest({ bidRequests, gdprConsent = {}, uspConsent, pageUrl, referer, ttxSettings, bidderRequest }) { +function _createServerRequest({ bidRequests, gdprConsent = {}, uspConsent, gppConsent = {}, pageUrl, referer, ttxSettings, bidderRequest }) { const ttxRequest = {}; const firstBidRequest = bidRequests[0]; const { siteId, test } = firstBidRequest.params; + const coppaValue = config.getConfig('coppa'); /* * Infer data for the request payload @@ -296,6 +294,17 @@ function _createServerRequest({ bidRequests, gdprConsent = {}, uspConsent, pageU }); } + if (gppConsent.gppString) { + Object.assign(ttxRequest.regs, { + 'gpp': gppConsent.gppString, + 'gpp_sid': gppConsent.applicableSections + }); + } + + if (coppaValue !== undefined) { + ttxRequest.regs.coppa = Number(!!coppaValue); + } + ttxRequest.ext = { ttx: { prebidStartedAt: Date.now(), @@ -717,10 +726,10 @@ function _createBidResponse(bid, cur) { // Else no syncs // For logic on how we handle gdpr data see _createSyncs and module's unit tests // '33acrossBidAdapter#getUserSyncs' -function getUserSyncs(syncOptions, responses, gdprConsent, uspConsent) { +function getUserSyncs(syncOptions, responses, gdprConsent, uspConsent, gppConsent) { const syncUrls = ( (syncOptions.iframeEnabled) - ? adapterState.uniqueSiteIds.map((siteId) => _createSync({ gdprConsent, uspConsent, siteId })) + ? adapterState.uniqueSiteIds.map((siteId) => _createSync({ gdprConsent, uspConsent, gppConsent, siteId })) : ([]) ); @@ -731,15 +740,16 @@ function getUserSyncs(syncOptions, responses, gdprConsent, uspConsent) { } // Sync object will always be of type iframe for TTX -function _createSync({ siteId = 'zzz000000000003zzz', gdprConsent = {}, uspConsent }) { +function _createSync({ siteId = 'zzz000000000003zzz', gdprConsent = {}, uspConsent, gppConsent = {} }) { const ttxSettings = config.getConfig('ttxSettings'); const syncUrl = (ttxSettings && ttxSettings.syncUrl) || SYNC_ENDPOINT; const { consentString, gdprApplies } = gdprConsent; + const { gppString = '', applicableSections = [] } = gppConsent; const sync = { type: 'iframe', - url: `${syncUrl}&id=${siteId}&gdpr_consent=${encodeURIComponent(consentString)}&us_privacy=${encodeURIComponent(uspConsent)}` + url: `${syncUrl}&id=${siteId}&gdpr_consent=${encodeURIComponent(consentString)}&us_privacy=${encodeURIComponent(uspConsent)}&gpp=${encodeURIComponent(gppString)}&gpp_sid=${encodeURIComponent(applicableSections.join(','))}` }; if (typeof gdprApplies === 'boolean') { diff --git a/modules/33acrossIdSystem.js b/modules/33acrossIdSystem.js index be81b26b110..0cb1b1f3382 100644 --- a/modules/33acrossIdSystem.js +++ b/modules/33acrossIdSystem.js @@ -8,7 +8,13 @@ import { logMessage, logError } from '../src/utils.js'; import { ajaxBuilder } from '../src/ajax.js'; import { submodule } from '../src/hook.js'; -import { uspDataHandler } from '../src/adapterManager.js'; +import { uspDataHandler, coppaDataHandler, gppDataHandler } from '../src/adapterManager.js'; + +/** + * @typedef {import('../modules/userId/index.js').Submodule} Submodule + * @typedef {import('../modules/userId/index.js').SubmoduleConfig} SubmoduleConfig + * @typedef {import('../modules/userId/index.js').IdResponse} IdResponse + */ const MODULE_NAME = '33acrossId'; const API_URL = 'https://lexicon.33across.com/v1/envelope'; @@ -37,19 +43,30 @@ function getEnvelope(response) { function calculateQueryStringParams(pid, gdprConsentData) { const uspString = uspDataHandler.getConsentData(); const gdprApplies = Boolean(gdprConsentData?.gdprApplies); + const coppaValue = coppaDataHandler.getCoppa(); + const gppConsent = gppDataHandler.getConsentData(); + const params = { pid, gdpr: Number(gdprApplies), src: CALLER_NAME, - ver: '$prebid.version$' + ver: '$prebid.version$', + coppa: Number(coppaValue) }; if (uspString) { params.us_privacy = uspString; } - if (gdprApplies) { - params.gdpr_consent = gdprConsentData.consentString || ''; + if (gppConsent) { + const { gppString = '', applicableSections = [] } = gppConsent; + + params.gpp = gppString; + params.gpp_sid = encodeURIComponent(applicableSections.join(',')) + } + + if (gdprConsentData?.consentString) { + params.gdpr_consent = gdprConsentData.consentString; } return params; @@ -115,6 +132,15 @@ export const thirthyThreeAcrossIdSubmodule = { }, calculateQueryStringParams(pid, gdprConsentData), { method: 'GET', withCredentials: true }); } }; + }, + eids: { + '33acrossId': { + source: '33across.com', + atype: 1, + getValue: function(data) { + return data.envelope; + } + }, } }; diff --git a/modules/a1MediaBidAdapter.js b/modules/a1MediaBidAdapter.js new file mode 100644 index 00000000000..d640bbfe2d7 --- /dev/null +++ b/modules/a1MediaBidAdapter.js @@ -0,0 +1,104 @@ +import { ortbConverter } from '../libraries/ortbConverter/converter.js'; +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { BANNER, VIDEO, NATIVE } from '../src/mediaTypes.js'; +import { replaceAuctionPrice } from '../src/utils.js'; + +const BIDDER_CODE = 'a1media'; +const END_POINT = 'https://d11.contentsfeed.com/dsp/breq/a1'; +const DEFAULT_CURRENCY = 'JPY'; + +const converter = ortbConverter({ + context: { + netRevenue: true, + ttl: 30, + }, + imp(buildImp, bidRequest, context) { + const imp = buildImp(bidRequest, context); + if (!imp.bidfloor) { + imp.bidfloor = bidRequest.params.bidfloor || 0; + imp.bidfloorcur = bidRequest.params.currency || DEFAULT_CURRENCY; + } + if (bidRequest.params.battr) { + Object.keys(bidRequest.mediaTypes).forEach(mType => { + imp[mType].battr = bidRequest.params.battr; + }) + } + return imp; + }, + request(buildRequest, imps, bidderRequest, context) { + const request = buildRequest(imps, bidderRequest, context); + const bid = context.bidRequests[0]; + if (!request.cur) { + request.cur = [bid.params.currency || DEFAULT_CURRENCY]; + } + if (bid.params.bcat) { + request.bcat = bid.params.bcat; + } + return request; + }, + bidResponse(buildBidResponse, bid, context) { + const { bidRequest } = context; + + let resMediaType; + const reqMediaTypes = Object.keys(bidRequest.mediaTypes); + if (reqMediaTypes.length === 1) { + resMediaType = reqMediaTypes[0]; + } else { + if (bid.adm.search(/^(<\?xml| { + const parsedBid = seatbidItem.bid.map((bidItem) => ({ + ...bidItem, + adm: replaceAuctionPrice(bidItem.adm, bidItem.price), + nurl: replaceAuctionPrice(bidItem.nurl, bidItem.price) + })); + return {...seatbidItem, bid: parsedBid}; + }); + + const responseBody = {...serverResponse.body, seatbid: parsedSeatbid}; + const bids = converter.fromORTB({ + response: responseBody, + request: bidRequest.data, + }).bids; + return bids; + }, + +}; +registerBidder(spec); diff --git a/modules/a1MediaBidAdapter.md b/modules/a1MediaBidAdapter.md new file mode 100644 index 00000000000..304b7e1bb5a --- /dev/null +++ b/modules/a1MediaBidAdapter.md @@ -0,0 +1,93 @@ +# Overview + +```markdown +Module Name: A1Media Bid Adapter +Module Type: Bidder Adapter +Maintainer: dev@a1mediagroup.co.kr +``` + +# Description + +Connects to A1Media exchange for bids. + +# Test Parameters + +## Sample Banner Ad Unit + +```javascript +var adUnits = [ + { + code: 'test-div', + mediaTypes: { + banner: { + sizes: [[320, 100]], + } + }, + bids: [ + { + bidder: "a1media", + params: { + bidfloor: 0.9, //optional + currency: 'JPY' //optional + battr: [ 13 ], //optional + bcat: ['IAB1-1'] //optional + } + } + ] + } +] +``` + +## Sample Video Ad Unit + +```javascript +var adUnits = [ + { + code: 'test-div', + mediaTypes: { + video: { + mimes: ['video/mp4'], + } + }, + bids: [ + { + bidder: "a1media", + params: { + bidfloor: 0.9, //optional + currency: 'JPY' //optional + battr: [ 13 ], //optional + bcat: ['IAB1-1'] //optional + } + } + ] + } +] +``` + +## Sample Native Ad Unit + +```javascript +var adUnits = [ + { + code: 'test-div', + mediaTypes: { + native: { + title: { + len: 140 + }, + } + }, + bids: [ + { + bidder: "a1media", + params: { + bidfloor: 0.9, //optional + currency: 'JPY' //optional + battr: [ 13 ], //optional + bcat: ['IAB1-1'] //optional + } + } + ] + } +] +``` diff --git a/modules/a1MediaRtdProvider.js b/modules/a1MediaRtdProvider.js new file mode 100644 index 00000000000..445ed47181d --- /dev/null +++ b/modules/a1MediaRtdProvider.js @@ -0,0 +1,96 @@ +import { MODULE_TYPE_RTD } from '../src/activities/modules.js'; +import { loadExternalScript } from '../src/adloader.js'; +import { submodule } from '../src/hook.js'; +import { getStorageManager } from '../src/storageManager.js'; +import { isEmptyStr, mergeDeep } from '../src/utils.js'; + +/** + * @typedef {import('../modules/rtdModule/index.js').RtdSubmodule} RtdSubmodule + */ + +const REAL_TIME_MODULE = 'realTimeData'; +const MODULE_NAME = 'a1Media'; +const SCRIPT_URL = 'https://linkback.contentsfeed.com/src'; +export const A1_SEG_KEY = '__a1tg'; +export const A1_AUD_KEY = 'a1_gid'; + +export const storage = getStorageManager({moduleType: MODULE_TYPE_RTD, moduleName: MODULE_NAME}); + +/** @type {RtdSubmodule} */ +export const subModuleObj = { + name: MODULE_NAME, + init: init, + getBidRequestData: alterBidRequests, +}; + +export function getStorageData(key) { + let storageValue = ''; + if (storage.getDataFromLocalStorage(key)) { + storageValue = storage.getDataFromLocalStorage(key); + } else if (storage.getCookie(key)) { + storageValue = storage.getCookie(key); + } + return storageValue; +} + +function loadLbScript(tagname) { + const linkback = window.linkback = window.linkback || {}; + if (!linkback.l) { + linkback.l = true; + + const scriptUrl = `${SCRIPT_URL}/${tagname}`; + loadExternalScript(scriptUrl, MODULE_NAME); + } +} + +function init(config, userConsent) { + const tagId = config.params.tagId; + if (tagId && !isEmptyStr(tagId)) { + loadLbScript(config.params.tagId); + return true; + } + if (!isEmptyStr(getStorageData(A1_SEG_KEY))) { + return true; + } + return false; +} + +function alterBidRequests(reqBidsConfigObj, callback, config, userConsent) { + const a1seg = getStorageData(A1_SEG_KEY); + const a1gid = getStorageData(A1_AUD_KEY); + + const a1UserSegData = { + name: 'a1mediagroup.com', + ext: { + segtax: 900 + }, + segment: a1seg.split(',').map(x => ({id: x})) + }; + + const a1UserEid = { + source: 'a1mediagroup.com', + uids: [ + { + id: a1gid, + atype: 1 + } + ] + }; + + const a1Ortb2 = { + user: { + data: [ + a1UserSegData + ], + ext: { + eids: [ + a1UserEid + ] + } + } + }; + mergeDeep(reqBidsConfigObj.ortb2Fragments.global, a1Ortb2); + callback(); +} + +submodule(REAL_TIME_MODULE, subModuleObj); diff --git a/modules/a1MediaRtdProvider.md b/modules/a1MediaRtdProvider.md new file mode 100644 index 00000000000..ab2077ebbbb --- /dev/null +++ b/modules/a1MediaRtdProvider.md @@ -0,0 +1,46 @@ +# Overview + +Module Name: A1Media Rtd Provider +Module Type: Rtd Provider +Maintainer: dev@a1mediagroup.co.kr + +# Description + +This module loads external code using the passed parameter (params.tagId). + +The A1Media RTD module loads A1Media script for obtains user segments, and provides user segment data to bid-requests.
+to get user segments, you will need a1media script customized for site. + +To use this module, you’ll need to work with [A1MediaGroup](https://www.a1mediagroup.com/) to get an account and receive instructions on how to set up your pages and ad server. + +Contact dev@a1mediagroup.co.kr for information. + +### Integration + +1) Build the A1Media RTD Module into the Prebid.js package with: + +``` +gulp build --modules=a1MediaRtdProvider,... +``` + +2) Use `setConfig` to instruct Prebid.js to initilaize the A1Media RTD module, as specified below. + +### Configuration + +```javascript +pbjs.setConfig({ + realTimeData: { + auctionDelay: 1000, + dataProviders: [ + { + name: "a1Media", + waitForIt: true, + params: { + // 'tagId' is unique value for each account. + tagId: 'lb4test' + } + } + ] + } +}); +``` diff --git a/modules/ablidaBidAdapter.js b/modules/ablidaBidAdapter.js index 805a2020fb4..175d5ff7c72 100644 --- a/modules/ablidaBidAdapter.js +++ b/modules/ablidaBidAdapter.js @@ -3,6 +3,12 @@ import {registerBidder} from '../src/adapters/bidderFactory.js'; import {BANNER, NATIVE, VIDEO} from '../src/mediaTypes.js'; import {convertOrtbRequestToProprietaryNative} from '../src/native.js'; +/** + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid + * @typedef {import('../src/adapters/bidderFactory.js').ServerResponse} ServerResponse + */ + const BIDDER_CODE = 'ablida'; const ENDPOINT_URL = 'https://bidder.ablida.net/prebid'; diff --git a/modules/acuityAdsBidAdapter.js b/modules/acuityadsBidAdapter.js similarity index 94% rename from modules/acuityAdsBidAdapter.js rename to modules/acuityadsBidAdapter.js index b0bb132ddae..5b12eb2133b 100644 --- a/modules/acuityAdsBidAdapter.js +++ b/modules/acuityadsBidAdapter.js @@ -153,6 +153,15 @@ export const spec = { tmax: bidderRequest.timeout }; + // Add GPP consent + if (bidderRequest.gppConsent) { + request.gpp = bidderRequest.gppConsent.gppString; + request.gpp_sid = bidderRequest.gppConsent.applicableSections; + } else if (bidderRequest.ortb2?.regs?.gpp) { + request.gpp = bidderRequest.ortb2.regs.gpp; + request.gpp_sid = bidderRequest.ortb2.regs.gpp_sid; + } + const len = validBidRequests.length; for (let i = 0; i < len; i++) { const bid = validBidRequests[i]; diff --git a/modules/acuityAdsBidAdapter.md b/modules/acuityadsBidAdapter.md similarity index 98% rename from modules/acuityAdsBidAdapter.md rename to modules/acuityadsBidAdapter.md index a19e0a6b0ba..7f001cd9376 100644 --- a/modules/acuityAdsBidAdapter.md +++ b/modules/acuityadsBidAdapter.md @@ -3,7 +3,7 @@ ``` Module Name: AcuityAds Bidder Adapter Module Type: AcuityAds Bidder Adapter -Maintainer: sa-support@brightcom.com +Maintainer: rafi.babler@acuityads.com ``` # Description diff --git a/modules/ad2ictionBidAdapter.js b/modules/ad2ictionBidAdapter.js new file mode 100644 index 00000000000..0f7cea45d14 --- /dev/null +++ b/modules/ad2ictionBidAdapter.js @@ -0,0 +1,59 @@ +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { BANNER } from '../src/mediaTypes.js'; +import { getStorageManager } from '../src/storageManager.js'; + +export const BIDDER_CODE = 'ad2iction'; +export const SUPPORTED_AD_TYPES = [BANNER]; +export const API_ENDPOINT = 'https://ads.ad2iction.com/html/prebid/'; +export const API_VERSION_NUMBER = 3; +export const COOKIE_NAME = 'ad2udid'; + +export const storage = getStorageManager({ bidderCode: BIDDER_CODE }); + +export const spec = { + code: BIDDER_CODE, + aliases: ['ad2'], + supportedMediaTypes: SUPPORTED_AD_TYPES, + isBidRequestValid: (bid) => { + return !!bid.params.id && typeof bid.params.id === 'string'; + }, + buildRequests: (validBidRequests, bidderRequest) => { + const ids = validBidRequests.map((bid) => { + return { bannerId: bid.params.id, bidId: bid.bidId }; + }); + + const options = { + contentType: 'application/json', + withCredentials: false, + }; + + const udid = storage.cookiesAreEnabled() && storage.getCookie(COOKIE_NAME); + + const data = { + ids: JSON.stringify(ids), + ortb2: bidderRequest.ortb2, + refererInfo: bidderRequest.refererInfo, + v: API_VERSION_NUMBER, + udid: udid || '', + _: Math.round(new Date().getTime()), + }; + + return { + method: 'POST', + url: API_ENDPOINT, + data, + options, + }; + }, + interpretResponse: (serverResponse, bidRequest) => { + if (!Array.isArray(serverResponse.body)) { + return []; + } + + const bidResponses = serverResponse.body; + + return bidResponses; + }, +}; + +registerBidder(spec); diff --git a/modules/ad2ictionBidAdapter.md b/modules/ad2ictionBidAdapter.md new file mode 100644 index 00000000000..47e355aa795 --- /dev/null +++ b/modules/ad2ictionBidAdapter.md @@ -0,0 +1,30 @@ +# Overview + +**Module Name**: Ad2iction Bidder Adapter +**Module Type**: Bidder Adapter +**Maintainer**: prebid@ad2iction.com + +# Description + +The Ad2iction Bidding adapter requires setup before beginning. Please contact us on https://www.ad2iction.com. + +# Sample Ad Unit Config +``` +var adUnits = [ + // Banner adUnit + { + code: 'banner-div', + mediaTypes: { + banner: { + sizes: [[300, 250], [336, 280]] + } + }, + bids: [{ + bidder: 'ad2iction', + params: { + id: 'accepted-uuid' + } + }] + } +]; +``` diff --git a/modules/adWMGBidAdapter.js b/modules/adWMGBidAdapter.js index 36935e80d3b..d268c4cafa8 100644 --- a/modules/adWMGBidAdapter.js +++ b/modules/adWMGBidAdapter.js @@ -1,9 +1,9 @@ 'use strict'; -import { tryAppendQueryString } from '../src/utils.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; import { config } from '../src/config.js'; import { BANNER } from '../src/mediaTypes.js'; +import {tryAppendQueryString} from '../libraries/urlUtils/urlUtils.js'; const BIDDER_CODE = 'adWMG'; const ENDPOINT = 'https://hb.adwmg.com/hb'; diff --git a/modules/adagioAnalyticsAdapter.js b/modules/adagioAnalyticsAdapter.js index 96f3f089cbd..de0aa1cb5d7 100644 --- a/modules/adagioAnalyticsAdapter.js +++ b/modules/adagioAnalyticsAdapter.js @@ -5,16 +5,50 @@ import adapter from '../libraries/analyticsAdapter/AnalyticsAdapter.js'; import adapterManager from '../src/adapterManager.js'; import CONSTANTS from '../src/constants.json'; -import { getWindowTop } from '../src/utils.js'; +import { ajax } from '../src/ajax.js'; +import { BANNER } from '../src/mediaTypes.js'; +import { getWindowTop, getWindowSelf, deepAccess, logInfo, logError } from '../src/utils.js'; +import { getGlobal } from '../src/prebidGlobal.js'; const emptyUrl = ''; const analyticsType = 'endpoint'; const events = Object.keys(CONSTANTS.EVENTS).map(key => CONSTANTS.EVENTS[key]); -const VERSION = '2.0.0'; +const ADAGIO_GVLID = 617; +const VERSION = '3.0.0'; +const PREBID_VERSION = '$prebid.version$'; +const ENDPOINT = 'https://c.4dex.io/pba.gif'; +const cache = { + auctions: {}, + getAuction: function(auctionId, adUnitCode) { + return this.auctions[auctionId][adUnitCode]; + }, + getBiddersFromAuction: function(auctionId, adUnitCode) { + return this.getAuction(auctionId, adUnitCode).bdrs.split(','); + }, + getAllAdUnitCodes: function(auctionId) { + return Object.keys(this.auctions[auctionId]); + }, + updateAuction: function(auctionId, adUnitCode, values) { + this.auctions[auctionId][adUnitCode] = { + ...this.auctions[auctionId][adUnitCode], + ...values + }; + }, -const adagioEnqueue = function adagioEnqueue(action, data) { - getWindowTop().ADAGIO.queue.push({ action, data, ts: Date.now() }); -} + // Map prebid auction id to adagio auction id + auctionIdReferences: {}, + addPrebidAuctionIdRef(auctionId, adagioAuctionId) { + this.auctionIdReferences[auctionId] = adagioAuctionId; + }, + getAdagioAuctionId(auctionId) { + return this.auctionIdReferences[auctionId]; + } +}; +const enc = window.encodeURIComponent; + +/** +/* BEGIN ADAGIO.JS CODE + */ function canAccessTopWindow() { try { @@ -24,12 +58,356 @@ function canAccessTopWindow() { } catch (error) { return false; } +}; + +function getCurrentWindow() { + return currentWindow; +}; + +let currentWindow; + +const adagioEnqueue = function adagioEnqueue(action, data) { + getCurrentWindow().ADAGIO.queue.push({ action, data, ts: Date.now() }); +}; + +/** + * END ADAGIO.JS CODE + */ + +/** + * UTILS FUNCTIONS + */ + +const guard = { + adagio: (value) => isAdagio(value), + bidTracked: (auctionId, adUnitCode) => deepAccess(cache, `auctions.${auctionId}.${adUnitCode}`, false), + auctionTracked: (auctionId) => deepAccess(cache, `auctions.${auctionId}`, false) +}; + +function removeDuplicates(arr, getKey) { + const seen = {}; + return arr.filter(item => { + const key = getKey(item); + return seen.hasOwnProperty(key) ? false : (seen[key] = true); + }); +}; + +function getAdapterNameForAlias(aliasName) { + return adapterManager.aliasRegistry[aliasName] || aliasName; +}; + +function isAdagio(value) { + if (!value) { + return false + } + + return value.toLowerCase().includes('adagio') || + getAdapterNameForAlias(value).toLowerCase().includes('adagio'); +}; + +function getMediaTypeAlias(mediaType) { + const mediaTypesMap = { + banner: 'ban', + outstream: 'vidout', + instream: 'vidin', + adpod: 'vidadpod', + native: 'nat' + }; + return mediaTypesMap[mediaType] || mediaType; +}; + +function addKeyPrefix(obj, prefix) { + return Object.keys(obj).reduce((acc, key) => { + // We don't want to prefix already prefixed keys. + if (key.startsWith(prefix)) { + acc[key] = obj[key]; + return acc; + } + + acc[`${prefix}${key}`] = obj[key]; + return acc; + }, {}); +} + +/** + * sendRequest to Adagio. It filter null values and encode each query param. + * @param {Object} qp + */ +function sendRequest(qp) { + // Removing null values + qp = Object.keys(qp).reduce((acc, key) => { + if (qp[key] !== null) { + acc[key] = qp[key]; + } + return acc; + }, {}); + + const url = `${ENDPOINT}?${Object.keys(qp).map(key => `${key}=${enc(qp[key])}`).join('&')}`; + ajax(url, null, null, {method: 'GET'}); +}; + +/** + * Send a new beacon to Adagio. It increment the version of the beacon. + * @param {string} auctionId + * @param {string} adUnitCode + */ +function sendNewBeacon(auctionId, adUnitCode) { + cache.updateAuction(auctionId, adUnitCode, { + v: (cache.getAuction(auctionId, adUnitCode).v || 0) + 1 + }); + sendRequest(cache.getAuction(auctionId, adUnitCode)); +}; + +function getTargetedAuctionId(bid) { + return deepAccess(bid, 'latestTargetedAuctionId') || deepAccess(bid, 'auctionId'); } +/** + * END UTILS FUNCTIONS + */ + +/** + * HANDLERS + * - handlerAuctionInit + * - handlerBidResponse + * - handlerAuctionEnd + * - handlerBidWon + * - handlerAdRender + * + * Each handler is called when the event is fired. + */ + +function handlerAuctionInit(event) { + const w = getCurrentWindow(); + + const prebidAuctionId = event.auctionId; + const adUnitCodes = removeDuplicates(event.adUnitCodes, adUnitCode => adUnitCode); + + // Check if Adagio is on the bid requests. + // If not, we don't need to track the auction. + const adagioBidRequest = event.bidderRequests.find(bidRequest => isAdagio(bidRequest.bidderCode)); + if (!adagioBidRequest) { + logInfo(`Adagio is not on the bid requests for auction '${prebidAuctionId}'`) + return; + } + + cache.auctions[prebidAuctionId] = {}; + + adUnitCodes.forEach(adUnitCode => { + const adUnits = event.adUnits.filter(adUnit => adUnit.code === adUnitCode); + + // Get all bidders configures for the ad unit. + const bidders = removeDuplicates( + adUnits.map(adUnit => adUnit.bids.map(bid => ({bidder: bid.bidder, params: bid.params}))).flat(), + bidder => bidder.bidder + ); + + // Check if Adagio is configured for the ad unit. + // If not, we don't need to track the ad unit. + const adagioBidder = bidders.find(bidder => isAdagio(bidder.bidder)); + if (!adagioBidder) { + logInfo(`Adagio is not configured for ad unit '${adUnitCode}'`); + return; + } + + // Get all media types and banner sizes configured for the ad unit. + const mediaTypes = adUnits.map(adUnit => adUnit.mediaTypes); + const mediaTypesKeys = removeDuplicates( + mediaTypes.map(mediaTypeObj => Object.keys(mediaTypeObj)).flat(), + mediaTypeKey => mediaTypeKey + ).map(mediaType => getMediaTypeAlias(mediaType)).sort(); + const bannerSizes = removeDuplicates( + mediaTypes.filter(mediaType => mediaType.hasOwnProperty(BANNER)) + .map(mediaType => mediaType[BANNER].sizes.map(size => size.join('x'))) + .flat(), + bannerSize => bannerSize + ).sort(); + + // Get all Adagio bids for the ad unit from the bidRequest. + // If no bids, we don't need to track the ad unit. + const adagioAdUnitBids = adagioBidRequest.bids.filter(bid => bid.adUnitCode === adUnitCode); + if (deepAccess(adagioAdUnitBids, 'length', 0) <= 0) { + logInfo(`Adagio is not on the bid requests for ad unit '${adUnitCode}' and auction '${prebidAuctionId}'`) + return; + } + // Get Adagio params from the first bid. + // We assume that all Adagio bids for a same adunit have the same params. + const params = adagioAdUnitBids[0].params; + + const adagioAuctionId = params.adagioAuctionId; + cache.addPrebidAuctionIdRef(prebidAuctionId, adagioAuctionId); + + // Get all media types requested for Adagio. + const adagioMediaTypes = removeDuplicates( + adagioAdUnitBids.map(bid => Object.keys(bid.mediaTypes)).flat(), + mediaTypeKey => mediaTypeKey + ).flat().map(mediaType => getMediaTypeAlias(mediaType)).sort(); + + const qp = { + v: 0, + pbjsv: PREBID_VERSION, + org_id: params.organizationId, + site: params.site, + pv_id: params.pageviewId, + auct_id: adagioAuctionId, + adu_code: adUnitCode, + url_dmn: w.location.hostname, + pgtyp: params.pagetype, + plcmt: params.placement, + t_n: params.testName || null, + t_v: params.testVersion || null, + mts: mediaTypesKeys.join(','), + ban_szs: bannerSizes.join(','), + bdrs: bidders.map(bidder => getAdapterNameForAlias(bidder.bidder)).sort().join(','), + adg_mts: adagioMediaTypes.join(',') + }; + + cache.auctions[prebidAuctionId][adUnitCode] = qp; + sendNewBeacon(prebidAuctionId, adUnitCode); + }); +}; + +/** + * handlerBidResponse allow to track the adagio bid response + * and to update the auction cache with the seat ID. + * No beacon is sent here. + */ +function handlerBidResponse(event) { + if (!guard.adagio(event.bidder)) { + return; + } + + if (!guard.bidTracked(event.auctionId, event.adUnitCode)) { + return; + } + + if (!event.pba) { + return; + } + + cache.updateAuction(event.auctionId, event.adUnitCode, { + ...addKeyPrefix(event.pba, 'e_') + }); +}; + +function handlerAuctionEnd(event) { + const { auctionId } = event; + + if (!guard.auctionTracked(auctionId)) { + return; + } + + const adUnitCodes = cache.getAllAdUnitCodes(auctionId); + adUnitCodes.forEach(adUnitCode => { + const mapper = (bidder) => event.bidsReceived.find(bid => bid.adUnitCode === adUnitCode && bid.bidder === bidder) ? '1' : '0'; + + cache.updateAuction(auctionId, adUnitCode, { + bdrs_bid: cache.getBiddersFromAuction(auctionId, adUnitCode).map(mapper).join(',') + }); + sendNewBeacon(auctionId, adUnitCode); + }); +} + +function handlerBidWon(event) { + let auctionId = getTargetedAuctionId(event); + + if (!guard.bidTracked(auctionId, event.adUnitCode)) { + return; + } + + let adsCurRateToUSD = (event.currency === 'USD') ? 1 : null; + let ogCurRateToUSD = (event.originalCurrency === 'USD') ? 1 : null; + try { + if (typeof getGlobal().convertCurrency === 'function') { + // Currency module is loaded, we can calculate the conversion rate. + + // Get the conversion rate from the original currency to USD. + ogCurRateToUSD = getGlobal().convertCurrency(1, event.originalCurrency, 'USD'); + // Get the conversion rate from the ad server currency to USD. + adsCurRateToUSD = getGlobal().convertCurrency(1, event.currency, 'USD'); + } + } catch (error) { + logError('Error on Adagio Analytics Adapter - handlerBidWon', error); + } + + const adagioAuctionCacheId = ( + (event.latestTargetedAuctionId && event.latestTargetedAuctionId !== event.auctionId) + ? cache.getAdagioAuctionId(event.auctionId) + : null); + + cache.updateAuction(auctionId, event.adUnitCode, { + win_bdr: getAdapterNameForAlias(event.bidder), + win_mt: getMediaTypeAlias(event.mediaType), + win_ban_sz: event.mediaType === BANNER ? `${event.width}x${event.height}` : null, + + // ad server currency + win_cpm: event.cpm, + cur: event.currency, + cur_rate: adsCurRateToUSD, + + // original currency from bidder + og_cpm: event.originalCpm, + og_cur: event.originalCurrency, + og_cur_rate: ogCurRateToUSD, + + // cache bid id + auct_id_c: adagioAuctionCacheId, + }); + sendNewBeacon(auctionId, event.adUnitCode); +}; + +function handlerAdRender(event, isSuccess) { + const { adUnitCode } = event.bid; + let auctionId = getTargetedAuctionId(event.bid); + + if (!guard.bidTracked(auctionId, adUnitCode)) { + return; + } + + cache.updateAuction(auctionId, adUnitCode, { + rndr: isSuccess ? 1 : 0 + }); + sendNewBeacon(auctionId, adUnitCode); +}; + +/** + * END HANDLERS + */ + let adagioAdapter = Object.assign(adapter({ emptyUrl, analyticsType }), { - track: function({ eventType, args }) { - if (typeof args !== 'undefined' && events.indexOf(eventType) !== -1) { - adagioEnqueue('pb-analytics-event', { eventName: eventType, args }); + track: function(event) { + const { eventType, args } = event; + + try { + switch (eventType) { + case CONSTANTS.EVENTS.AUCTION_INIT: + handlerAuctionInit(args); + break; + case CONSTANTS.EVENTS.BID_RESPONSE: + handlerBidResponse(args); + break; + case CONSTANTS.EVENTS.AUCTION_END: + handlerAuctionEnd(args); + break; + case CONSTANTS.EVENTS.BID_WON: + handlerBidWon(args); + break; + // AD_RENDER_SUCCEEDED seems redundant with BID_WON. + // case CONSTANTS.EVENTS.AD_RENDER_SUCCEEDED: + case CONSTANTS.EVENTS.AD_RENDER_FAILED: + handlerAdRender(args, eventType === CONSTANTS.EVENTS.AD_RENDER_SUCCEEDED); + break; + } + } catch (error) { + logError('Error on Adagio Analytics Adapter', error); + } + + try { + if (typeof args !== 'undefined' && events.indexOf(eventType) !== -1) { + adagioEnqueue('pb-analytics-event', { eventName: eventType, args }); + } + } catch (error) { + logError('Error on Adagio Analytics Adapter - adagio.js', error); } } }); @@ -37,11 +415,8 @@ let adagioAdapter = Object.assign(adapter({ emptyUrl, analyticsType }), { adagioAdapter.originEnableAnalytics = adagioAdapter.enableAnalytics; adagioAdapter.enableAnalytics = config => { - if (!canAccessTopWindow()) { - return; - } - - const w = getWindowTop(); + const w = (canAccessTopWindow()) ? getWindowTop() : getWindowSelf(); + currentWindow = w; w.ADAGIO = w.ADAGIO || {}; w.ADAGIO.queue = w.ADAGIO.queue || []; @@ -53,7 +428,8 @@ adagioAdapter.enableAnalytics = config => { adapterManager.registerAnalyticsAdapter({ adapter: adagioAdapter, - code: 'adagio' + code: 'adagio', + gvlid: ADAGIO_GVLID, }); export default adagioAdapter; diff --git a/modules/adagioAnalyticsAdapter.md b/modules/adagioAnalyticsAdapter.md index 312a26ea8da..9fc2cb0bb88 100644 --- a/modules/adagioAnalyticsAdapter.md +++ b/modules/adagioAnalyticsAdapter.md @@ -8,10 +8,10 @@ Maintainer: dev@adagio.io Analytics adapter for Adagio -# Test Parameters +# Settings -``` -{ - provider: 'adagio' -} +```js + pbjs.enableAnalytics({ + provider: 'adagio', + }); ``` diff --git a/modules/adagioBidAdapter.js b/modules/adagioBidAdapter.js index 57dd80aa9b1..112722383a9 100644 --- a/modules/adagioBidAdapter.js +++ b/modules/adagioBidAdapter.js @@ -1,12 +1,10 @@ import {find} from '../src/polyfill.js'; import { - _map, cleanObj, deepAccess, deepClone, generateUUID, getDNT, - getGptSlotInfoForAdUnitCode, getUniqueIdentifierStr, getWindowSelf, getWindowTop, @@ -34,6 +32,7 @@ import {OUTSTREAM} from '../src/video.js'; import { getGlobal } from '../src/prebidGlobal.js'; import { convertOrtbRequestToProprietaryNative } from '../src/native.js'; import { userSync } from '../src/userSync.js'; +import {getGptSlotInfoForAdUnitCode} from '../libraries/gptUtils/gptUtils.js'; const BIDDER_CODE = 'adagio'; const LOG_PREFIX = 'Adagio:'; @@ -54,8 +53,9 @@ const ADAGIO_PUBKEY = 'AL16XT44Sfp+8SHVF1UdC7hydPSMVLMhsYknKDdwqq+0ToDSJrP0+Qh0k const ADAGIO_PUBKEY_E = 65537; const CURRENCY = 'USD'; -// This provide a whitelist and a basic validation of OpenRTB 2.6 options used by the Adagio SSP. -// https://iabtechlab.com/wp-content/uploads/2022/04/OpenRTB-2-6_FINAL.pdf +// This provide a whitelist and a basic validation of OpenRTB 2.5 options used by the Adagio SSP. +// Accept all options but 'protocol', 'companionad', 'companiontype', 'ext' +// https://www.iab.com/wp-content/uploads/2016/03/OpenRTB-API-Specification-Version-2-5-FINAL.pdf export const ORTB_VIDEO_PARAMS = { 'mimes': (value) => Array.isArray(value) && value.length > 0 && value.every(v => typeof v === 'string'), 'minduration': (value) => isInteger(value), @@ -399,6 +399,17 @@ function _getUspConsent(bidderRequest) { return (deepAccess(bidderRequest, 'uspConsent')) ? { uspConsent: bidderRequest.uspConsent } : false; } +function _getGppConsent(bidderRequest) { + let gpp = deepAccess(bidderRequest, 'gppConsent.gppString') + let gppSid = deepAccess(bidderRequest, 'gppConsent.applicableSections') + + if (!gpp || !gppSid) { + gpp = deepAccess(bidderRequest, 'ortb2.regs.gpp', '') + gppSid = deepAccess(bidderRequest, 'ortb2.regs.gpp_sid', []) + } + return { gpp, gppSid } +} + function _getSchain(bidRequest) { return deepAccess(bidRequest, 'schain'); } @@ -558,6 +569,7 @@ function _parseNativeBidResponse(bid) { bid.native = native } +// bidRequest param must be the `bidRequest` object with the original `auctionId` value. function _getFloors(bidRequest) { if (!isFn(bidRequest.getFloor)) { return false; @@ -688,9 +700,9 @@ function getPageDimensions() { } /** -* @todo Move to prebid Core as Utils. -* @returns -*/ + * @todo Move to prebid Core as Utils. + * @returns + */ function getViewPortDimensions() { if (!isSafeFrameWindow() && !canAccessTopWindow()) { return ''; @@ -820,8 +832,8 @@ function getPrintNumber(adUnitCode, bidderRequest) { } /** - * domLoading feature is computed on window.top if reachable. - */ + * domLoading feature is computed on window.top if reachable. + */ function getDomLoadingDuration() { let domLoadingDuration = -1; let performance; @@ -976,6 +988,7 @@ export const spec = { const gdprConsent = _getGdprConsent(bidderRequest) || {}; const uspConsent = _getUspConsent(bidderRequest) || {}; const coppa = _getCoppa(); + const gppConsent = _getGppConsent(bidderRequest) const schain = _getSchain(validBidRequests[0]); const eids = _getEids(validBidRequests[0]) || []; const syncEnabled = deepAccess(config.getConfig('userSync'), 'syncEnabled') @@ -983,8 +996,8 @@ export const spec = { const aucId = generateUUID() - const adUnits = _map(validBidRequests, (rawBidRequest) => { - const bidRequest = {...rawBidRequest} + const adUnits = validBidRequests.map(rawBidRequest => { + const bidRequest = deepClone(rawBidRequest); // Fix https://github.com/prebid/Prebid.js/issues/9781 bidRequest.auctionId = aucId @@ -1007,6 +1020,9 @@ export const spec = { } } + // Enforce the organizationId param to be a string + bidRequest.params.organizationId = bidRequest.params.organizationId.toString(); + // 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)) { @@ -1055,7 +1071,10 @@ export const spec = { }); // Handle priceFloors module - const computedFloors = _getFloors(bidRequest); + // We need to use `rawBidRequest` as param because: + // - adagioBidAdapter generates its own auctionId due to transmitTid activity limitation (see https://github.com/prebid/Prebid.js/pull/10079) + // - the priceFloors.getFloor() uses a `_floorDataForAuction` map to store the floors based on the auctionId. + const computedFloors = _getFloors(rawBidRequest); if (isArray(computedFloors) && computedFloors.length) { bidRequest.floors = computedFloors @@ -1099,41 +1118,59 @@ export const spec = { _buildVideoBidRequest(bidRequest); } + const gpid = deepAccess(bidRequest, 'ortb2Imp.ext.gpid') || deepAccess(bidRequest, 'ortb2Imp.ext.data.pbadslot'); + if (gpid) { + bidRequest.gpid = gpid; + } + + // store the whole bidRequest (adUnit) object in the ADAGIO namespace. storeRequestInAdagioNS(bidRequest); - // Remove these fields at the very end, so we can still use them before. - delete bidRequest.transactionId; - delete bidRequest.ortb2Imp; - delete bidRequest.ortb2; - delete bidRequest.sizes; + // Remove some params that are not needed on the server side. + delete bidRequest.params.siteId; + + // whitelist the fields that are allowed to be sent to the server. + const adUnit = { + adUnitCode: bidRequest.adUnitCode, + auctionId: bidRequest.auctionId, + bidder: bidRequest.bidder, + bidId: bidRequest.bidId, + params: bidRequest.params, + features: bidRequest.features, + gpid: bidRequest.gpid, + mediaTypes: bidRequest.mediaTypes, + nativeParams: bidRequest.nativeParams, + score: bidRequest.score, + transactionId: bidRequest.transactionId, + } - return bidRequest; + return adUnit; }); // Group ad units by organizationId const groupedAdUnits = adUnits.reduce((groupedAdUnits, adUnit) => { - const adUnitCopy = deepClone(adUnit); - adUnitCopy.params.organizationId = adUnitCopy.params.organizationId.toString(); + const organizationId = adUnit.params.organizationId - // remove useless props - delete adUnitCopy.floorData; - delete adUnitCopy.params.siteId; - delete adUnitCopy.userId - delete adUnitCopy.userIdAsEids - - groupedAdUnits[adUnitCopy.params.organizationId] = groupedAdUnits[adUnitCopy.params.organizationId] || []; - groupedAdUnits[adUnitCopy.params.organizationId].push(adUnitCopy); + groupedAdUnits[organizationId] = groupedAdUnits[organizationId] || []; + groupedAdUnits[organizationId].push(adUnit); return groupedAdUnits; }, {}); + // Adding more params on the original bid object. + // Those params are not sent to the server. + // They are used for further operations on analytics adapter. + validBidRequests.forEach(rawBidRequest => { + rawBidRequest.params.adagioAuctionId = aucId + rawBidRequest.params.pageviewId = pageviewId + }); + // Build one request per organizationId - const requests = _map(Object.keys(groupedAdUnits), organizationId => { + const requests = Object.keys(groupedAdUnits).map(organizationId => { return { method: 'POST', url: ENDPOINT, data: { - id: generateUUID(), organizationId: organizationId, secure: secure, device: device, @@ -1144,7 +1181,9 @@ export const spec = { regs: { gdpr: gdprConsent, coppa: coppa, - ccpa: uspConsent + ccpa: uspConsent, + gpp: gppConsent.gpp, + gppSid: gppConsent.gppSid }, schain: schain, user: { @@ -1152,7 +1191,8 @@ export const spec = { }, prebidVersion: '$prebid.version$', featuresVersion: FEATURES_VERSION, - usIfr: usIfr + usIfr: usIfr, + adgjs: storage.localStorageIsEnabled() }, options: { contentType: 'text/plain' diff --git a/modules/adagioBidAdapter.md b/modules/adagioBidAdapter.md index 45f39fc6f2d..19673571982 100644 --- a/modules/adagioBidAdapter.md +++ b/modules/adagioBidAdapter.md @@ -107,10 +107,11 @@ var adUnits = [ cpm: 3.00 // default to 1.00 }, video: { - api: [2, 7], // Required - Your video player must at least support the value 2 and/or 7. + api: [2], // Required - Your video player must at least support the value 2 playbackMethod: [6], // Highly recommended skip: 0 - // OpenRTB video options defined here override ones defined in mediaTypes. + // OpenRTB 2.5 video options defined here override ones defined in mediaTypes. + // Not supported: 'protocol', 'companionad', 'companiontype', 'ext' }, native: { // Optional OpenRTB Native 1.2 request object. Only `context`, `plcmttype` fields are supported. @@ -193,6 +194,8 @@ If the FPD value is an array, the 1st value of this array will be used. placement: 'in_article', adUnitElementId: 'article_outstream', video: { + api: [2], + playbackMethod: [6], skip: 0 }, debug: { diff --git a/modules/adfBidAdapter.js b/modules/adfBidAdapter.js index 5c4b03c3bb2..e5b40f66176 100644 --- a/modules/adfBidAdapter.js +++ b/modules/adfBidAdapter.js @@ -69,8 +69,11 @@ export const spec = { bid.netRevenue = pt; const floorInfo = bid.getFloor ? bid.getFloor({ - currency: currency || 'USD' + currency: currency || 'USD', + size: '*', + mediaType: '*' }) : {}; + const bidfloor = floorInfo.floor; const bidfloorcur = floorInfo.currency; const { mid, inv, mname } = bid.params; diff --git a/modules/adfusionBidAdapter.js b/modules/adfusionBidAdapter.js new file mode 100644 index 00000000000..a206ee5e899 --- /dev/null +++ b/modules/adfusionBidAdapter.js @@ -0,0 +1,120 @@ +import { ortbConverter } from '../libraries/ortbConverter/converter.js'; +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { BANNER, VIDEO } from '../src/mediaTypes.js'; +import * as utils from '../src/utils.js'; + +const adpterVersion = '1.0'; +export const REQUEST_URL = 'https://spicyrtb.com/auction/prebid'; +export const DEFAULT_CURRENCY = 'USD'; + +export const spec = { + code: 'adfusion', + gvlid: 844, + supportedMediaTypes: [BANNER, VIDEO], + isBidRequestValid, + buildRequests, + interpretResponse, + isBannerBid, + isVideoBid, +}; + +registerBidder(spec); + +const converter = ortbConverter({ + context: { + netRevenue: true, + ttl: 300, + currency: DEFAULT_CURRENCY, + }, + imp(buildImp, bidRequest, context) { + const imp = buildImp(bidRequest, context); + const floor = getBidFloor(bidRequest); + if (floor) { + imp.bidfloor = floor; + imp.bidfloorcur = DEFAULT_CURRENCY; + } + + return imp; + }, + request(buildRequest, imps, bidderRequest, context) { + const req = buildRequest(imps, bidderRequest, context); + const bid = context.bidRequests[0]; + utils.mergeDeep(req, { + at: 1, + ext: { + prebid: { + accountid: bid.params.accountId, + adapterVersion: `${adpterVersion}`, + }, + }, + }); + return req; + }, + response(buildResponse, bidResponses, ortbResponse, context) { + const response = buildResponse(bidResponses, ortbResponse, context); + return response.bids; + }, +}); + +function isBidRequestValid(bidRequest) { + const isValid = bidRequest.params.accountId; + if (!isValid) { + utils.logError('AdFusion adapter bidRequest has no accountId'); + return false; + } + return true; +} + +function buildRequests(bids, bidderRequest) { + let videoBids = bids.filter((bid) => isVideoBid(bid)); + let bannerBids = bids.filter((bid) => isBannerBid(bid)); + let requests = bannerBids.length + ? [createRequest(bannerBids, bidderRequest, BANNER)] + : []; + videoBids.forEach((bid) => { + requests.push(createRequest([bid], bidderRequest, VIDEO)); + }); + return requests; +} + +function createRequest(bidRequests, bidderRequest, mediaType) { + return { + method: 'POST', + url: REQUEST_URL, + data: converter.toORTB({ + bidRequests, + bidderRequest, + context: { mediaType }, + }), + }; +} + +function isVideoBid(bid) { + return utils.deepAccess(bid, 'mediaTypes.video'); +} + +function isBannerBid(bid) { + return utils.deepAccess(bid, 'mediaTypes.banner'); +} + +function interpretResponse(resp, req) { + return converter.fromORTB({ request: req.data, response: resp.body }); +} + +function getBidFloor(bid) { + if (utils.isFn(bid.getFloor)) { + let floor = bid.getFloor({ + currency: DEFAULT_CURRENCY, + mediaType: '*', + size: '*', + }); + if ( + utils.isPlainObject(floor) && + !isNaN(floor.floor) && + floor.currency === DEFAULT_CURRENCY + ) { + return floor.floor; + } + } + return null; +} diff --git a/modules/adfusionBidAdapter.md b/modules/adfusionBidAdapter.md new file mode 100644 index 00000000000..803a03ba1a1 --- /dev/null +++ b/modules/adfusionBidAdapter.md @@ -0,0 +1,61 @@ +# Overview + +``` +Module Name: AdFusion Bid Adapter +Module Type: Bidder Adapter +Maintainer: prebid@adfusion.pl +``` + +# Description + +Module that connects to AdFusion demand sources + +# Banner Test Parameters + +```js +var adUnits = [ + { + code: "test-banner", + mediaTypes: { + banner: { + sizes: [ + [300, 250], + [300, 600], + [320, 480], + ], + }, + }, + bids: [ + { + bidder: "adfusion", + params: { + accountId: 1234, // required + }, + }, + ], + }, +]; +``` + +# Video Test Parameters + +```js +var videoAdUnit = { + code: "video1", + mediaTypes: { + video: { + context: "instream", + playerSize: [640, 480], + mimes: ["video/mp4"], + }, + }, + bids: [ + { + bidder: "adfusion", + params: { + accountId: 1234, // required + }, + }, + ], +}; +``` diff --git a/modules/adgenerationBidAdapter.js b/modules/adgenerationBidAdapter.js index f18b7629d8f..e0538fe2815 100644 --- a/modules/adgenerationBidAdapter.js +++ b/modules/adgenerationBidAdapter.js @@ -1,8 +1,18 @@ -import {tryAppendQueryString, getBidIdParameter, escapeUnsafeChars, deepAccess} from '../src/utils.js'; +import {deepAccess, getBidIdParameter} from '../src/utils.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import {BANNER, NATIVE} from '../src/mediaTypes.js'; import {config} from '../src/config.js'; import {convertOrtbRequestToProprietaryNative} from '../src/native.js'; +import {tryAppendQueryString} from '../libraries/urlUtils/urlUtils.js'; +import {escapeUnsafeChars} from '../libraries/htmlEscape/htmlEscape.js'; + +/** + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid + * @typedef {import('../src/adapters/bidderFactory.js').ServerResponse} ServerResponse + * @typedef {import('../src/adapters/bidderFactory.js').SyncOptions} SyncOptions + * @typedef {import('../src/adapters/bidderFactory.js').UserSync} UserSync + */ const ADG_BIDDER_CODE = 'adgeneration'; @@ -28,7 +38,7 @@ export const spec = { buildRequests: function (validBidRequests, bidderRequest) { // convert Native ORTB definition to old-style prebid native definition validBidRequests = convertOrtbRequestToProprietaryNative(validBidRequests); - const ADGENE_PREBID_VERSION = '1.6.0'; + const ADGENE_PREBID_VERSION = '1.6.2'; let serverRequests = []; for (let i = 0, len = validBidRequests.length; i < len; i++) { const validReq = validBidRequests[i]; @@ -41,6 +51,7 @@ export const spec = { const imuid = deepAccess(validReq, 'userId.imuid'); const gpid = deepAccess(validReq, 'ortb2Imp.ext.gpid'); const sua = deepAccess(validReq, 'ortb2.device.sua'); + const uid2 = deepAccess(validReq, 'userId.uid2.id'); let data = ``; data = tryAppendQueryString(data, 'posall', 'SSPLOC'); const id = getBidIdParameter('id', validReq.params); @@ -58,10 +69,10 @@ export const spec = { data = tryAppendQueryString(data, 'adgext_id5_id', id5id); data = tryAppendQueryString(data, 'adgext_id5_id_link_type', id5LinkType); data = tryAppendQueryString(data, 'adgext_imuid', imuid); - data = tryAppendQueryString(data, 'adgext_uid2', validReq.userId ? validReq.userId.uid2 : null); - data = tryAppendQueryString(data, 'gpid', gpid ? encodeURIComponent(gpid) : null); + data = tryAppendQueryString(data, 'adgext_uid2', uid2); + data = tryAppendQueryString(data, 'gpid', gpid); data = tryAppendQueryString(data, 'uach', sua ? JSON.stringify(sua) : null); - data = tryAppendQueryString(data, 'schain', validReq.schain ? encodeURIComponent(JSON.stringify(validReq.schain)) : null); + data = tryAppendQueryString(data, 'schain', validReq.schain ? JSON.stringify(validReq.schain) : null); // nativeäģĨ外ãĢvideoį­‰ãŽå¯žåŋœãŒå…ĨãŖた場合はčĻäŋŽæ­Ŗ if (!validReq.mediaTypes || !validReq.mediaTypes.native) { diff --git a/modules/adkernelAdnBidAdapter.js b/modules/adkernelAdnBidAdapter.js index 4db46aca3c6..81067a3efcf 100644 --- a/modules/adkernelAdnBidAdapter.js +++ b/modules/adkernelAdnBidAdapter.js @@ -159,7 +159,7 @@ export const spec = { code: 'adkernelAdn', gvlid: GVLID, supportedMediaTypes: [BANNER, VIDEO], - aliases: ['engagesimply'], + aliases: ['engagesimply', 'adpluto_dsp'], isBidRequestValid: function(bidRequest) { return 'params' in bidRequest && diff --git a/modules/adkernelBidAdapter.js b/modules/adkernelBidAdapter.js index 64567832dbd..d6a4030057a 100644 --- a/modules/adkernelBidAdapter.js +++ b/modules/adkernelBidAdapter.js @@ -5,7 +5,6 @@ import { createTrackPixelHtml, deepAccess, deepSetValue, - getAdUnitSizes, getDefinedParams, getDNT, isArray, @@ -22,25 +21,31 @@ import {registerBidder} from '../src/adapters/bidderFactory.js'; import {find} from '../src/polyfill.js'; import {config} from '../src/config.js'; import {convertOrtbRequestToProprietaryNative} from '../src/native.js'; +import {getAdUnitSizes} from '../libraries/sizeUtils/sizeUtils.js'; -/* +/** * In case you're AdKernel whitelable platform's client who needs branded adapter to * work with Adkernel platform - DO NOT COPY THIS ADAPTER UNDER NEW NAME * - * Please contact prebid@adkernel.com and we'll add your adapter as an alias. + * Please contact prebid@adkernel.com and we'll add your adapter as an alias + * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid + * @typedef {import('../src/adapters/bidderFactory.js').ServerRequest} ServerRequest + * @typedef {import('../src/adapters/bidderFactory.js').UserSync} UserSync */ -const VIDEO_PARAMS = ['pos', 'context', 'placement', 'api', 'mimes', 'protocols', 'playbackmethod', 'minduration', 'maxduration', + +const VIDEO_PARAMS = ['pos', 'context', 'placement', 'plcmt', 'api', 'mimes', 'protocols', 'playbackmethod', 'minduration', 'maxduration', 'startdelay', 'linearity', 'skip', 'skipmin', 'skipafter', 'minbitrate', 'maxbitrate', 'delivery', 'playbackend', 'boxingallowed']; const VIDEO_FPD = ['battr', 'pos']; const NATIVE_FPD = ['battr', 'api']; +const BANNER_PARAMS = ['pos']; const BANNER_FPD = ['btype', 'battr', 'pos', 'api']; const VERSION = '1.6'; const SYNC_IFRAME = 1; const SYNC_IMAGE = 2; -const SYNC_TYPES = Object.freeze({ +const SYNC_TYPES = { 1: 'iframe', 2: 'image' -}); +}; const GVLID = 14; const NATIVE_MODEL = [ @@ -66,6 +71,11 @@ const NATIVE_INDEX = NATIVE_MODEL.reduce((acc, val, idx) => { return acc; }, {}); +const MULTI_FORMAT_SUFFIX = '__mf'; +const MULTI_FORMAT_SUFFIX_BANNER = 'b' + MULTI_FORMAT_SUFFIX; +const MULTI_FORMAT_SUFFIX_VIDEO = 'v' + MULTI_FORMAT_SUFFIX; +const MULTI_FORMAT_SUFFIX_NATIVE = 'n' + MULTI_FORMAT_SUFFIX; + /** * Adapter for requesting bids from AdKernel white-label display platform */ @@ -98,9 +108,10 @@ export const spec = { {code: 'displayioads'}, {code: 'rtbdemand_com'}, {code: 'bidbuddy'}, - {code: 'adliveconnect'}, {code: 'didnadisplay'}, - {code: 'qortex'} + {code: 'qortex'}, + {code: 'adpluto'}, + {code: 'headbidder'} ], supportedMediaTypes: [BANNER, VIDEO, NATIVE], @@ -171,6 +182,9 @@ export const spec = { ttl: 360, netRevenue: true }; + if (prBid.requestId.endsWith(MULTI_FORMAT_SUFFIX)) { + prBid.requestId = stripMultiformatSuffix(prBid.requestId); + } if ('banner' in imp) { prBid.mediaType = BANNER; prBid.width = rtbBid.w; @@ -237,13 +251,13 @@ registerBidder(spec); function groupImpressionsByHostZone(bidRequests, refererInfo) { let secure = (refererInfo && refererInfo.page?.indexOf('https:') === 0); return Object.values( - bidRequests.map(bidRequest => buildImp(bidRequest, secure)) + bidRequests.map(bidRequest => buildImps(bidRequest, secure)) .reduce((acc, curr, index) => { let bidRequest = bidRequests[index]; let {zoneId, host} = bidRequest.params; let key = `${host}_${zoneId}`; acc[key] = acc[key] || {host: host, zoneId: zoneId, imps: []}; - acc[key].imps.push(curr); + acc[key].imps.push(...curr); return acc; }, {}) ); @@ -262,57 +276,97 @@ function getBidFloor(bid, mediaType, sizes) { } /** - * Builds rtb imp object for single adunit + * Builds rtb imp object(s) for single adunit * @param bidRequest {BidRequest} * @param secure {boolean} */ -function buildImp(bidRequest, secure) { - const imp = { +function buildImps(bidRequest, secure) { + let imp = { 'id': bidRequest.bidId, 'tagid': bidRequest.adUnitCode }; - var mediaType; + if (secure) { + imp.secure = 1; + } var sizes = []; - - if (deepAccess(bidRequest, 'mediaTypes.banner')) { + let mediaTypes = bidRequest.mediaTypes; + let isMultiformat = (~~!!mediaTypes?.banner + ~~!!mediaTypes?.video + ~~!!mediaTypes?.native) > 1; + let result = []; + let typedImp; + + if (mediaTypes?.banner) { + if (isMultiformat) { + typedImp = {...imp}; + typedImp.id = imp.id + MULTI_FORMAT_SUFFIX_BANNER; + } else { + typedImp = imp; + } sizes = getAdUnitSizes(bidRequest); - imp.banner = { + let pbBanner = mediaTypes.banner; + typedImp.banner = { + ...getDefinedParamsOrEmpty(bidRequest.ortb2Imp, BANNER_FPD), + ...getDefinedParamsOrEmpty(pbBanner, BANNER_PARAMS), format: sizes.map(wh => parseGPTSingleSizeArrayToRtbSize(wh)), topframe: 0 }; - populateImpFpd(imp.banner, bidRequest, BANNER_FPD); - mediaType = BANNER; - } else if (deepAccess(bidRequest, 'mediaTypes.video')) { - let video = deepAccess(bidRequest, 'mediaTypes.video'); - imp.video = getDefinedParams(video, VIDEO_PARAMS); - populateImpFpd(imp.video, bidRequest, VIDEO_FPD); - if (video.playerSize) { - sizes = video.playerSize[0]; - imp.video = Object.assign(imp.video, parseGPTSingleSizeArrayToRtbSize(sizes) || {}); - } else if (video.w && video.h) { - imp.video.w = video.w; - imp.video.h = video.h; + initImpBidfloor(typedImp, bidRequest, sizes, isMultiformat ? '*' : BANNER); + result.push(typedImp); + } + + if (mediaTypes?.video) { + if (isMultiformat) { + typedImp = {...imp}; + typedImp.id = typedImp.id + MULTI_FORMAT_SUFFIX_VIDEO; + } else { + typedImp = imp; } - mediaType = VIDEO; - } else if (deepAccess(bidRequest, 'mediaTypes.native')) { - let nativeRequest = buildNativeRequest(bidRequest.mediaTypes.native); - imp.native = { + let pbVideo = mediaTypes.video; + typedImp.video = { + ...getDefinedParamsOrEmpty(bidRequest.ortb2Imp, VIDEO_FPD), + ...getDefinedParamsOrEmpty(pbVideo, VIDEO_PARAMS) + }; + if (pbVideo.playerSize) { + sizes = pbVideo.playerSize[0]; + typedImp.video = Object.assign(typedImp.video, parseGPTSingleSizeArrayToRtbSize(sizes) || {}); + } else if (pbVideo.w && pbVideo.h) { + typedImp.video.w = pbVideo.w; + typedImp.video.h = pbVideo.h; + } + initImpBidfloor(typedImp, bidRequest, sizes, isMultiformat ? '*' : VIDEO); + result.push(typedImp); + } + + if (mediaTypes?.native) { + if (isMultiformat) { + typedImp = {...imp}; + typedImp.id = typedImp.id + MULTI_FORMAT_SUFFIX_NATIVE; + } else { + typedImp = imp; + } + let nativeRequest = buildNativeRequest(mediaTypes.native); + typedImp.native = { + ...getDefinedParamsOrEmpty(bidRequest.ortb2Imp, NATIVE_FPD), ver: '1.1', request: JSON.stringify(nativeRequest) }; - populateImpFpd(imp.native, bidRequest, NATIVE_FPD); - mediaType = NATIVE; - } else { - throw new Error('Unsupported bid received'); + initImpBidfloor(typedImp, bidRequest, sizes, isMultiformat ? '*' : NATIVE); + result.push(typedImp); } - let floor = getBidFloor(bidRequest, mediaType, sizes); - if (floor) { - imp.bidfloor = floor; + return result; +} + +function initImpBidfloor(imp, bid, sizes, mediaType) { + let bidfloor = getBidFloor(bid, mediaType, sizes); + if (bidfloor) { + imp.bidfloor = bidfloor; } - if (secure) { - imp.secure = 1; +} + +function getDefinedParamsOrEmpty(object, params) { + if (object === undefined) { + return {}; } - return imp; + return getDefinedParams(object, params); } /** @@ -344,19 +398,6 @@ function buildNativeRequest(nativeReq) { return request; } -/** - * Populate impression-level FPD from bid request - * @param target {Object} - * @param bidRequest {BidRequest} - * @param props {String[]} - */ -function populateImpFpd(target, bidRequest, props) { - if (bidRequest.ortb2Imp === undefined) { - return; - } - Object.assign(target, getDefinedParams(bidRequest.ortb2Imp, props)); -} - /** * Builds image asset request */ @@ -456,7 +497,9 @@ function makeUser(bidderRequest, fpd) { if (eids) { deepSetValue(user, 'ext.eids', eids); } - if (!isEmpty(user)) { return {user: user}; } + if (!isEmpty(user)) { + return {user: user}; + } } /** @@ -465,13 +508,17 @@ function makeUser(bidderRequest, fpd) { * @returns {{regs: Object} | undefined} */ function makeRegulations(bidderRequest) { - let {gdprConsent, uspConsent} = bidderRequest; + let {gdprConsent, uspConsent, gppConsent} = bidderRequest; let regs = {}; if (gdprConsent) { if (gdprConsent.gdprApplies !== undefined) { deepSetValue(regs, 'regs.ext.gdpr', ~~gdprConsent.gdprApplies); } } + if (gppConsent) { + deepSetValue(regs, 'regs.gpp', gppConsent.gppString); + deepSetValue(regs, 'regs.gpp_sid', gppConsent.applicableSections); + } if (uspConsent) { deepSetValue(regs, 'regs.ext.us_privacy', uspConsent); } @@ -637,3 +684,7 @@ function buildNativeAd(nativeResp) { }); return cleanObj(nativeAd); } + +function stripMultiformatSuffix(impid) { + return impid.substr(0, impid.length - MULTI_FORMAT_SUFFIX.length - 1); +} diff --git a/modules/adlooxAnalyticsAdapter.js b/modules/adlooxAnalyticsAdapter.js index 5db75c656bb..9284d543298 100644 --- a/modules/adlooxAnalyticsAdapter.js +++ b/modules/adlooxAnalyticsAdapter.js @@ -14,7 +14,6 @@ import {find} from '../src/polyfill.js'; import {getRefererInfo} from '../src/refererDetection.js'; import { deepAccess, - getGptSlotInfoForAdUnitCode, getUniqueIdentifierStr, insertElement, isFn, @@ -28,6 +27,7 @@ import { mergeDeep, parseUrl } from '../src/utils.js'; +import {getGptSlotInfoForAdUnitCode} from '../libraries/gptUtils/gptUtils.js'; const MODULE = 'adlooxAnalyticsAdapter'; diff --git a/modules/adlooxRtdProvider.js b/modules/adlooxRtdProvider.js index c2037429185..727dc84e399 100644 --- a/modules/adlooxRtdProvider.js +++ b/modules/adlooxRtdProvider.js @@ -25,7 +25,6 @@ import { deepAccess, deepClone, deepSetValue, - getGptSlotInfoForAdUnitCode, isArray, isBoolean, isInteger, @@ -37,6 +36,7 @@ import { parseUrl, safeJSONParse } from '../src/utils.js'; +import {getGptSlotInfoForAdUnitCode} from '../libraries/gptUtils/gptUtils.js'; const MODULE_NAME = 'adloox'; const MODULE = `${MODULE_NAME}RtdProvider`; diff --git a/modules/admanBidAdapter.js b/modules/admanBidAdapter.js index ff8e6b02ad5..b78737722bd 100644 --- a/modules/admanBidAdapter.js +++ b/modules/admanBidAdapter.js @@ -4,6 +4,7 @@ import { isFn, deepAccess, logMessage } from '../src/utils.js'; import {config} from '../src/config.js'; import { convertOrtbRequestToProprietaryNative } from '../src/native.js'; +const GVLID = 149; const BIDDER_CODE = 'adman'; const AD_URL = 'https://pub.admanmedia.com/?c=o&m=multi'; const URL_SYNC = 'https://sync.admanmedia.com'; @@ -57,6 +58,7 @@ function getUserId(eids, id, source, uidExt) { export const spec = { code: BIDDER_CODE, + gvlid: GVLID, supportedMediaTypes: [BANNER, VIDEO, NATIVE], isBidRequestValid: (bid) => { @@ -66,6 +68,7 @@ export const spec = { buildRequests: (validBidRequests = [], bidderRequest) => { // convert Native ORTB definition to old-style prebid native definition validBidRequests = convertOrtbRequestToProprietaryNative(validBidRequests); + const content = deepAccess(bidderRequest, 'ortb2.site.content', config.getAnyConfig('ortb2.site.content')); let winTop = window; let location; @@ -93,7 +96,12 @@ export const spec = { request.ccpa = bidderRequest.uspConsent; } if (bidderRequest.gdprConsent) { - request.gdpr = bidderRequest.gdprConsent + request.gdpr = { + consentString: bidderRequest.gdprConsent.consentString + }; + } + if (content) { + request.content = content; } } const len = validBidRequests.length; @@ -109,6 +117,11 @@ export const spec = { bidFloor: getBidFloor(bid) } + if (bid.transactionId) { + placement.ext = placement.ext || {}; + placement.ext.tid = bid.transactionId; + } + if (bid.schain) { placement.schain = bid.schain; } diff --git a/modules/admaticBidAdapter.js b/modules/admaticBidAdapter.js index 027f924ac5d..3f87476def7 100644 --- a/modules/admaticBidAdapter.js +++ b/modules/admaticBidAdapter.js @@ -1,16 +1,47 @@ -import { getValue, logError, isEmpty, deepAccess, getBidIdParameter, isArray } from '../src/utils.js'; +import {getValue, formatQS, logError, deepAccess, isArray, getBidIdParameter} from '../src/utils.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; import { config } from '../src/config.js'; -import { BANNER, VIDEO } from '../src/mediaTypes.js'; +import { BANNER, VIDEO, NATIVE } from '../src/mediaTypes.js'; + +/** + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid + * @typedef {import('../src/adapters/bidderFactory.js').ServerRequest} ServerRequest + */ + +export const OPENRTB = { + NATIVE: { + IMAGE_TYPE: { + ICON: 1, + MAIN: 3, + }, + ASSET_ID: { + TITLE: 1, + IMAGE: 2, + ICON: 3, + BODY: 4, + SPONSORED: 5, + CTA: 6 + }, + DATA_ASSET_TYPE: { + SPONSORED: 1, + DESC: 2, + CTA_TEXT: 12, + }, + } +}; + let SYNC_URL = ''; const BIDDER_CODE = 'admatic'; + export const spec = { code: BIDDER_CODE, aliases: [ {code: 'pixad'} ], - supportedMediaTypes: [BANNER, VIDEO], - /** f + supportedMediaTypes: [BANNER, VIDEO, NATIVE], + /** + * f * @param {object} bid * @return {boolean} */ @@ -33,23 +64,21 @@ export const spec = { * @return {ServerRequest} */ buildRequests: (validBidRequests, bidderRequest) => { + const tmax = bidderRequest.timeout; const bids = validBidRequests.map(buildRequestObject); - const blacklist = bidderRequest.ortb2; + const ortb = bidderRequest.ortb2; const networkId = getValue(validBidRequests[0].params, 'networkId'); const host = getValue(validBidRequests[0].params, 'host'); const currency = config.getConfig('currency.adServerCurrency') || 'TRY'; const bidderName = validBidRequests[0].bidder; const payload = { - user: { - ua: navigator.userAgent - }, - blacklist: [], + ortb, site: { - page: location.href, - ref: location.origin, + page: bidderRequest.refererInfo.page, + ref: bidderRequest.refererInfo.page, publisher: { - name: location.hostname, + name: bidderRequest.refererInfo.domain, publisherId: networkId } }, @@ -57,17 +86,59 @@ export const spec = { ext: { cur: currency, bidder: bidderName - } + }, + schain: {}, + regs: { + ext: { + } + }, + user: { + ext: {} + }, + at: 1, + tmax: parseInt(tmax) }; - if (!isEmpty(blacklist.badv)) { - payload.blacklist = blacklist.badv; - }; + if (bidderRequest && bidderRequest.gdprConsent && bidderRequest.gdprConsent.gdprApplies) { + const consentStr = (bidderRequest.gdprConsent.consentString) + ? bidderRequest.gdprConsent.consentString.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '') : ''; + const gdpr = bidderRequest.gdprConsent.gdprApplies ? 1 : 0; + payload.regs.ext.gdpr = gdpr; + payload.regs.ext.consent = consentStr; + } + + if (bidderRequest && bidderRequest.coppa) { + payload.regs.ext.coppa = bidderRequest.coppa === true ? 1 : (bidderRequest.coppa === false ? 0 : undefined); + } + + if (bidderRequest && bidderRequest.ortb2?.regs?.gpp) { + payload.regs.ext.gpp = bidderRequest.ortb2?.regs?.gpp; + } + + if (bidderRequest && bidderRequest.ortb2?.regs?.gpp_sid) { + payload.regs.ext.gpp_sid = bidderRequest.ortb2?.regs?.gpp_sid; + } + + if (bidderRequest && bidderRequest.uspConsent) { + payload.regs.ext.uspIab = bidderRequest.uspConsent; + } + + if (validBidRequests[0].schain) { + const schain = mapSchain(validBidRequests[0].schain); + if (schain) { + payload.schain = schain; + } + } + + if (validBidRequests[0].userIdAsEids) { + const eids = { eids: validBidRequests[0].userIdAsEids }; + payload.user.ext = { ...payload.user.ext, ...eids }; + } if (payload) { switch (bidderName) { case 'pixad': - SYNC_URL = 'https://static.pixad.com.tr/sync.html'; + SYNC_URL = 'https://static.cdn.pixad.com.tr/sync.html'; break; default: SYNC_URL = 'https://cdn.serve.admatic.com.tr/showad/sync.html'; @@ -78,12 +149,36 @@ export const spec = { } }, - getUserSyncs: function (syncOptions, responses) { - if (syncOptions.iframeEnabled) { - return [{ + getUserSyncs: function (syncOptions, responses, gdprConsent, uspConsent, gppConsent) { + if (!hasSynced && syncOptions.iframeEnabled) { + // data is only assigned if params are available to pass to syncEndpoint + let params = {}; + + if (gdprConsent) { + if (typeof gdprConsent.gdprApplies === 'boolean') { + params['gdpr'] = Number(gdprConsent.gdprApplies); + } + if (typeof gdprConsent.consentString === 'string') { + params['gdpr_consent'] = gdprConsent.consentString; + } + } + + if (uspConsent) { + params['us_privacy'] = encodeURIComponent(uspConsent); + } + + if (gppConsent?.gppString) { + params['gpp'] = gppConsent.gppString; + params['gpp_sid'] = gppConsent.applicableSections?.toString(); + } + + params = Object.keys(params).length ? `?${formatQS(params)}` : ''; + + hasSynced = true; + return { type: 'iframe', - url: SYNC_URL - }]; + url: SYNC_URL + params + }; } }, @@ -106,6 +201,7 @@ export const spec = { netRevenue: true, creativeId: bid.creative_id, meta: { + model: bid.mime_type, advertiserDomains: bid && bid.adomain ? bid.adomain : [] }, bidder: bid.bidder, @@ -121,6 +217,8 @@ export const spec = { resbid.vastImpUrl = bid.iurl; } else if (resbid.mediaType === 'banner') { resbid.ad = bid.party_tag; + } else if (resbid.mediaType === 'native') { + resbid.native = interpretNativeAd(bid.party_tag) }; bidResponses.push(resbid); @@ -130,6 +228,41 @@ export const spec = { } }; +var hasSynced = false; + +export function resetUserSync() { + hasSynced = false; +} + +/** + * @param {object} schain object set by Publisher + * @returns {object} OpenRTB SupplyChain object + */ +function mapSchain(schain) { + if (!schain) { + return null; + } + if (!validateSchain(schain)) { + logError('AdMatic: required schain params missing'); + return null; + } + return schain; +} + +/** + * @param {object} schain object set by Publisher + * @returns {object} bool + */ +function validateSchain(schain) { + if (!schain.nodes) { + return false; + } + const requiredFields = ['asi', 'sid', 'hp']; + return schain.nodes.every(node => { + return requiredFields.every(field => node[field]); + }); +} + function isUrl(str) { try { URL(str); @@ -156,6 +289,11 @@ function enrichSlotWithFloors(slot, bidRequest) { videoSizes.forEach(videoSize => slotFloors.video[parseSize(videoSize).toString()] = bidRequest.getFloor({ size: videoSize, mediaType: VIDEO })); } + if (bidRequest.mediaTypes?.native) { + slotFloors.native = {}; + slotFloors.native['*'] = bidRequest.getFloor({ size: '*', mediaType: NATIVE }); + } + if (Object.keys(slotFloors).length > 0) { if (!slot) { slot = {} @@ -195,6 +333,16 @@ function buildRequestObject(bid) { reqObj.type = 'video'; reqObj.mediatype = bid.mediaTypes.video; } + if (bid.mediaTypes?.native) { + reqObj.type = 'native'; + reqObj.size = [{w: 1, h: 1}]; + reqObj.mediatype = bid.mediaTypes.native; + } + + if (deepAccess(bid, 'ortb2Imp.ext')) { + reqObj.ext = bid.ortb2Imp.ext; + } + reqObj.id = getBidIdParameter('bidId', bid); enrichSlotWithFloors(reqObj, bid); @@ -209,10 +357,11 @@ function getSizes(bid) { function concatSizes(bid) { let playerSize = deepAccess(bid, 'mediaTypes.video.playerSize'); let videoSizes = deepAccess(bid, 'mediaTypes.video.sizes'); + let nativeSizes = deepAccess(bid, 'mediaTypes.native.sizes'); let bannerSizes = deepAccess(bid, 'mediaTypes.banner.sizes'); if (isArray(bannerSizes) || isArray(playerSize) || isArray(videoSizes)) { - let mediaTypesSizes = [bannerSizes, videoSizes, playerSize]; + let mediaTypesSizes = [bannerSizes, videoSizes, nativeSizes, playerSize]; return mediaTypesSizes .reduce(function(acc, currSize) { if (isArray(currSize)) { @@ -227,6 +376,45 @@ function concatSizes(bid) { } } +function interpretNativeAd(adm) { + const native = JSON.parse(adm).native; + const result = { + clickUrl: encodeURI(native.link.url), + impressionTrackers: native.imptrackers + }; + native.assets.forEach(asset => { + switch (asset.id) { + case OPENRTB.NATIVE.ASSET_ID.TITLE: + result.title = asset.title.text; + break; + case OPENRTB.NATIVE.ASSET_ID.IMAGE: + result.image = { + url: encodeURI(asset.img.url), + width: asset.img.w, + height: asset.img.h + }; + break; + case OPENRTB.NATIVE.ASSET_ID.ICON: + result.icon = { + url: encodeURI(asset.img.url), + width: asset.img.w, + height: asset.img.h + }; + break; + case OPENRTB.NATIVE.ASSET_ID.BODY: + result.body = asset.data.value; + break; + case OPENRTB.NATIVE.ASSET_ID.SPONSORED: + result.sponsoredBy = asset.data.value; + break; + case OPENRTB.NATIVE.ASSET_ID.CTA: + result.cta = asset.data.value; + break; + } + }); + return result; +} + function _validateId(id) { return (parseInt(id) > 0); } diff --git a/modules/admediaBidAdapter.js b/modules/admediaBidAdapter.js index 42593a36159..5ea3e27b0d9 100644 --- a/modules/admediaBidAdapter.js +++ b/modules/admediaBidAdapter.js @@ -1,6 +1,12 @@ import {registerBidder} from '../src/adapters/bidderFactory.js'; import {BANNER} from '../src/mediaTypes.js'; +/** + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid + * @typedef {import('../src/adapters/bidderFactory.js').ServerResponse} ServerResponse + */ + const BIDDER_CODE = 'admedia'; const ENDPOINT_URL = 'https://prebid.admedia.com/bidder/'; diff --git a/modules/admixerBidAdapter.js b/modules/admixerBidAdapter.js index 1006fef631c..f5f0b5bf665 100644 --- a/modules/admixerBidAdapter.js +++ b/modules/admixerBidAdapter.js @@ -14,6 +14,7 @@ const ALIASES = [ {code: 'futureads', endpoint: 'https://ads.futureads.io/prebid.1.2.aspx'}, {code: 'smn', endpoint: 'https://ads.smn.rs/prebid.1.2.aspx'}, {code: 'admixeradx', endpoint: 'https://inv-nets.admixer.net/adxprebid.1.2.aspx'}, + {code: 'admixerwl', endpoint: 'https://inv-nets-adxwl.admixer.com/adxwlprebid.aspx'}, ]; export const spec = { code: BIDDER_CODE, @@ -23,7 +24,9 @@ export const spec = { * Determines whether or not the given bid request is valid. */ isBidRequestValid: function (bid) { - return !!bid.params.zone; + return bid.bidder === 'admixerwl' + ? !!bid.params.clientId && !!bid.params.endpointId + : !!bid.params.zone; }, /** * Make a server request from the list of BidRequests. @@ -73,12 +76,14 @@ export const spec = { validRequest.forEach((bid) => { let imp = {}; Object.keys(bid).forEach(key => imp[key] = bid[key]); + imp.ortb2 && delete imp.ortb2; payload.imps.push(imp); }); + + let urlForRequest = endpointUrl || getEndpointUrl(bidderRequest.bidderCode) return { method: 'POST', - url: - endpointUrl || getEndpointUrl(bidderRequest.bidderCode), + url: bidderRequest.bidderCode === 'admixerwl' ? `${urlForRequest}?client=${payload.imps[0]?.params?.clientId}` : urlForRequest, data: payload, }; }, diff --git a/modules/admixerBidAdapter.md b/modules/admixerBidAdapter.md index 682f5629115..64f8dd64ee4 100644 --- a/modules/admixerBidAdapter.md +++ b/modules/admixerBidAdapter.md @@ -50,3 +50,48 @@ Please use ```admixer``` as the bidder code. }, ]; ``` + +### AdmixerWL Test Parameters +``` + var adUnits = [ + { + code: 'desktop-banner-ad-div', + sizes: [[300, 250]], // a display size + bids: [ + { + bidder: "admixer", + params: { + endpointId: 41512, + clientId: 62 + } + } + ] + },{ + code: 'mobile-banner-ad-div', + sizes: [[300, 50]], // a mobile size + bids: [ + { + bidder: "admixer", + params: { + endpointId: 41512, + clientId: 62 + } + } + ] + },{ + code: 'video-ad', + sizes: [[300, 50]], + mediaType: 'video', + bids: [ + { + bidder: "admixer", + params: { + endpointId: 41512, + clientId: 62 + } + } + ] + }, + ]; +``` + diff --git a/modules/admixerIdSystem.js b/modules/admixerIdSystem.js index 7fbebecfc12..cb7248c9537 100644 --- a/modules/admixerIdSystem.js +++ b/modules/admixerIdSystem.js @@ -11,6 +11,13 @@ import { submodule } from '../src/hook.js'; import {getStorageManager} from '../src/storageManager.js'; import {MODULE_TYPE_UID} from '../src/activities/modules.js'; +/** + * @typedef {import('../modules/userId/index.js').Submodule} Submodule + * @typedef {import('../modules/userId/index.js').SubmoduleConfig} SubmoduleConfig + * @typedef {import('../modules/userId/index.js').ConsentData} ConsentData + * @typedef {import('../modules/userId/index.js').IdResponse} IdResponse + */ + const NAME = 'admixerId'; export const storage = getStorageManager({moduleType: MODULE_TYPE_UID, moduleName: NAME}); @@ -72,6 +79,12 @@ export const admixerIdSubmodule = { }; return { callback: resp }; + }, + eids: { + 'admixerId': { + source: 'admixer.net', + atype: 3 + }, } }; function retrieveVisitorId(url, callback) { diff --git a/modules/adnowBidAdapter.js b/modules/adnowBidAdapter.js index f83dbf68a1f..99f56df58b2 100644 --- a/modules/adnowBidAdapter.js +++ b/modules/adnowBidAdapter.js @@ -9,6 +9,10 @@ const ENDPOINT = 'https://n.ads3-adnow.com/a'; /** * @typedef {object} CommonBidData + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid + * @typedef {import('../src/adapters/bidderFactory.js').ServerRequest} ServerRequest + * @typedef {import('../src/adapters/bidderFactory.js').BidderSpec} BidderSpec * * @property {string} requestId The specific BidRequest which this bid is aimed at. * This should match the BidRequest.bidId which this Bid targets. diff --git a/modules/adnuntiusBidAdapter.js b/modules/adnuntiusBidAdapter.js index d1192c2eb64..a498d056513 100644 --- a/modules/adnuntiusBidAdapter.js +++ b/modules/adnuntiusBidAdapter.js @@ -6,61 +6,165 @@ import { getStorageManager } from '../src/storageManager.js'; const BIDDER_CODE = 'adnuntius'; const BIDDER_CODE_DEAL_ALIAS_BASE = 'adndeal'; -const BIDDER_CODE_DEAL_ALIAS_1 = BIDDER_CODE_DEAL_ALIAS_BASE + '1'; -const BIDDER_CODE_DEAL_ALIAS_2 = BIDDER_CODE_DEAL_ALIAS_BASE + '2'; -const BIDDER_CODE_DEAL_ALIAS_3 = BIDDER_CODE_DEAL_ALIAS_BASE + '3'; -const BIDDER_CODE_DEAL_ALIAS_4 = BIDDER_CODE_DEAL_ALIAS_BASE + '4'; -const BIDDER_CODE_DEAL_ALIAS_5 = BIDDER_CODE_DEAL_ALIAS_BASE + '5'; +const BIDDER_CODE_DEAL_ALIASES = [1, 2, 3, 4, 5].map(num => { + return BIDDER_CODE_DEAL_ALIAS_BASE + num; +}); const ENDPOINT_URL = 'https://ads.adnuntius.delivery/i'; const ENDPOINT_URL_EUROPE = 'https://europe.delivery.adnuntius.com/i'; const GVLID = 855; const DEFAULT_VAST_VERSION = 'vast4' const MAXIMUM_DEALS_LIMIT = 5; +const VALID_BID_TYPES = ['netBid', 'grossBid']; +const META_DATA_KEY = 'adn.metaData'; -const checkSegment = function (segment) { - if (isStr(segment)) return segment; - if (segment.id) return segment.id -} +export const misc = { + getUnixTimestamp: function (addDays, asMinutes) { + const multiplication = addDays / (asMinutes ? 1440 : 1); + return Date.now() + (addDays && addDays > 0 ? (1000 * 60 * 60 * 24 * multiplication) : 0); + } +}; + +const storageTool = (function () { + const storage = getStorageManager({ bidderCode: BIDDER_CODE }); + let metaInternal; + + const getMetaInternal = function () { + if (!storage.localStorageIsEnabled()) { + return []; + } + + let parsedJson; + try { + parsedJson = JSON.parse(storage.getDataFromLocalStorage(META_DATA_KEY)); + } catch (e) { + return []; + } + + let filteredEntries = parsedJson ? parsedJson.filter((datum) => { + if (datum.key === 'voidAuIds' && Array.isArray(datum.value)) { + return true; + } + return datum.key && datum.value && datum.exp && datum.exp > misc.getUnixTimestamp(); + }) : []; + const voidAuIdsEntry = filteredEntries.find(entry => entry.key === 'voidAuIds'); + if (voidAuIdsEntry) { + const now = misc.getUnixTimestamp(); + voidAuIdsEntry.value = voidAuIdsEntry.value.filter(voidAuId => voidAuId.auId && voidAuId.exp > now); + if (!voidAuIdsEntry.value.length) { + filteredEntries = filteredEntries.filter(entry => entry.key !== 'voidAuIds'); + } + } + return filteredEntries; + }; + + const setMetaInternal = function (apiResponse) { + if (!storage.localStorageIsEnabled()) { + return; + } + + const updateVoidAuIds = function (currentVoidAuIds, auIdsAsString) { + const newAuIds = isStr(auIdsAsString) ? auIdsAsString.split(';') : []; + const notNewExistingAuIds = currentVoidAuIds.filter(auIdObj => { + return newAuIds.indexOf(auIdObj.value) < -1; + }) || []; + const oneDayFromNow = misc.getUnixTimestamp(1); + const apiIdsArray = newAuIds.map(auId => { + return { exp: oneDayFromNow, auId: auId }; + }) || []; + return notNewExistingAuIds.concat(apiIdsArray) || []; + } -const getSegmentsFromOrtb = function (ortb2) { - const userData = deepAccess(ortb2, 'user.data'); - let segments = []; - if (userData) { - userData.forEach(userdat => { - if (userdat.segment) { - segments.push(...userdat.segment.filter(checkSegment).map(checkSegment)); + const metaAsObj = getMetaInternal().reduce((a, entry) => ({ ...a, [entry.key]: { value: entry.value, exp: entry.exp } }), {}); + for (const key in apiResponse) { + if (key !== 'voidAuIds') { + metaAsObj[key] = { + value: apiResponse[key], + exp: misc.getUnixTimestamp(100) + } + } + } + const currentAuIds = updateVoidAuIds(metaAsObj.voidAuIds || [], apiResponse.voidAuIds); + if (currentAuIds.length > 0) { + metaAsObj.voidAuIds = { value: currentAuIds }; + } + const metaDataForSaving = Object.entries(metaAsObj).map((entrySet) => { + if (entrySet[0] === 'voidAuIds') { + return { + key: entrySet[0], + value: entrySet[1].value + }; + } + return { + key: entrySet[0], + value: entrySet[1].value, + exp: entrySet[1].exp } }); + storage.setDataInLocalStorage(META_DATA_KEY, JSON.stringify(metaDataForSaving)); + }; + + const getUsi = function (meta, ortb2) { + let usi = (meta && meta.usi) ? meta.usi : false; + if (ortb2 && ortb2.user && ortb2.user.id) { + usi = ortb2.user.id + } + return usi; } - return segments -} -const handleMeta = function () { - const storage = getStorageManager({ bidderCode: BIDDER_CODE }) - let adnMeta = null - if (storage.localStorageIsEnabled()) { - adnMeta = JSON.parse(storage.getDataFromLocalStorage('adn.metaData')) + const getSegmentsFromOrtb = function (ortb2) { + const userData = deepAccess(ortb2, 'user.data'); + let segments = []; + if (userData) { + userData.forEach(userdat => { + if (userdat.segment) { + segments.push(...userdat.segment.map((segment) => { + if (isStr(segment)) return segment; + if (isStr(segment.id)) return segment.id; + }).filter((seg) => !!seg)); + } + }); + } + return segments } - return (adnMeta !== null) ? adnMeta.reduce((acc, cur) => { return { ...acc, [cur.key]: cur.value } }, {}) : {} -} -const getUsi = function (meta, ortb2, bidderRequest) { - let usi = (meta !== null && meta.usi) ? meta.usi : false; - if (ortb2 && ortb2.user && ortb2.user.id) { usi = ortb2.user.id } - return usi + return { + refreshStorage: function (bidderRequest) { + const ortb2 = bidderRequest.ortb2 || {}; + metaInternal = getMetaInternal().reduce((a, entry) => ({ ...a, [entry.key]: entry.value }), {}); + metaInternal.usi = getUsi(metaInternal, ortb2); + if (!metaInternal.usi) { + delete metaInternal.usi; + } + if (metaInternal.voidAuIds) { + metaInternal.voidAuIdsArray = metaInternal.voidAuIds.map((voidAuId) => { + return voidAuId.auId; + }); + } + metaInternal.segments = getSegmentsFromOrtb(ortb2); + }, + saveToStorage: function (serverData) { + setMetaInternal(serverData); + }, + getUrlRelatedData: function () { + const { segments, usi, voidAuIdsArray } = metaInternal; + return { segments, usi, voidAuIdsArray }; + }, + getPayloadRelatedData: function () { + const { segments, usi, userId, voidAuIdsArray, voidAuIds, ...payloadRelatedData } = metaInternal; + return payloadRelatedData; + } + }; +})(); + +const validateBidType = function (bidTypeOption) { + return VALID_BID_TYPES.indexOf(bidTypeOption || '') > -1 ? bidTypeOption : 'bid'; } const AU_ID_REGEX = new RegExp('^[0-9A-Fa-f]{1,20}$'); export const spec = { code: BIDDER_CODE, - aliases: [ - BIDDER_CODE_DEAL_ALIAS_1, - BIDDER_CODE_DEAL_ALIAS_2, - BIDDER_CODE_DEAL_ALIAS_3, - BIDDER_CODE_DEAL_ALIAS_4, - BIDDER_CODE_DEAL_ALIAS_5 - ], + aliases: BIDDER_CODE_DEAL_ALIASES, gvlid: GVLID, supportedMediaTypes: [BANNER, VIDEO], isBidRequestValid: function (bid) { @@ -70,34 +174,38 @@ export const spec = { }, buildRequests: function (validBidRequests, bidderRequest) { - const networks = {}; - const bidRequests = {}; - const requests = []; - const request = []; - const ortb2 = bidderRequest.ortb2 || {}; - const bidderConfig = config.getConfig(); - - const adnMeta = handleMeta() - const usi = getUsi(adnMeta, ortb2, bidderRequest) - const segments = getSegmentsFromOrtb(ortb2); - const tzo = new Date().getTimezoneOffset(); + const queryParamsAndValues = []; + queryParamsAndValues.push('tzo=' + new Date().getTimezoneOffset()) + queryParamsAndValues.push('format=json') const gdprApplies = deepAccess(bidderRequest, 'gdprConsent.gdprApplies'); const consentString = deepAccess(bidderRequest, 'gdprConsent.consentString'); + if (gdprApplies !== undefined) { + const flag = gdprApplies ? '1' : '0' + queryParamsAndValues.push('consentString=' + consentString); + queryParamsAndValues.push('gdpr=' + flag); + } + + storageTool.refreshStorage(bidderRequest); - request.push('tzo=' + tzo) - request.push('format=json') + const urlRelatedMetaData = storageTool.getUrlRelatedData(); + if (urlRelatedMetaData.segments.length > 0) queryParamsAndValues.push('segments=' + urlRelatedMetaData.segments.join(',')); + if (urlRelatedMetaData.usi) queryParamsAndValues.push('userId=' + urlRelatedMetaData.usi); + + const bidderConfig = config.getConfig(); + if (bidderConfig.useCookie === false) queryParamsAndValues.push('noCookies=true'); + if (bidderConfig.maxDeals > 0) queryParamsAndValues.push('ds=' + Math.min(bidderConfig.maxDeals, MAXIMUM_DEALS_LIMIT)); + + const bidRequests = {}; + const networks = {}; - if (gdprApplies !== undefined) request.push('consentString=' + consentString); - if (segments.length > 0) request.push('segments=' + segments.join(',')); - if (usi) request.push('userId=' + usi); - if (bidderConfig.useCookie === false) request.push('noCookies=true'); - if (bidderConfig.maxDeals > 0) request.push('ds=' + Math.min(bidderConfig.maxDeals, MAXIMUM_DEALS_LIMIT)); for (let i = 0; i < validBidRequests.length; i++) { - const bid = validBidRequests[i] - let network = bid.params.network || 'network'; - const maxDeals = Math.max(0, Math.min(bid.params.maxDeals || 0, MAXIMUM_DEALS_LIMIT)); - const targeting = bid.params.targeting || {}; + const bid = validBidRequests[i]; + if ((urlRelatedMetaData.voidAuIdsArray && (urlRelatedMetaData.voidAuIdsArray.indexOf(bid.params.auId) > -1 || urlRelatedMetaData.voidAuIdsArray.indexOf(bid.params.auId.padStart(16, '0')) > -1))) { + // This auId is void. Do NOT waste time and energy sending a request to the server + continue; + } + let network = bid.params.network || 'network'; if (bid.mediaTypes && bid.mediaTypes.video && bid.mediaTypes.video.context !== 'outstream') { network += '_video' } @@ -108,21 +216,31 @@ export const spec = { networks[network] = networks[network] || {}; networks[network].adUnits = networks[network].adUnits || []; if (bidderRequest && bidderRequest.refererInfo) networks[network].context = bidderRequest.refererInfo.page; - if (adnMeta) networks[network].metaData = adnMeta; - const adUnit = { ...targeting, auId: bid.params.auId, targetId: bid.bidId, maxDeals: maxDeals } + + const payloadRelatedData = storageTool.getPayloadRelatedData(); + if (Object.keys(payloadRelatedData).length > 0) { + networks[network].metaData = payloadRelatedData; + } + + const targeting = bid.params.targeting || {}; + const adUnit = { ...targeting, auId: bid.params.auId, targetId: bid.params.targetId || bid.bidId }; + const maxDeals = Math.max(0, Math.min(bid.params.maxDeals || 0, MAXIMUM_DEALS_LIMIT)); + if (maxDeals > 0) { + adUnit.maxDeals = maxDeals; + } if (bid.mediaTypes && bid.mediaTypes.banner && bid.mediaTypes.banner.sizes) adUnit.dimensions = bid.mediaTypes.banner.sizes networks[network].adUnits.push(adUnit); } + const requests = []; const networkKeys = Object.keys(networks) for (let j = 0; j < networkKeys.length; j++) { const network = networkKeys[j]; - const networkRequest = [...request] - if (network.indexOf('_video') > -1) { networkRequest.push('tt=' + DEFAULT_VAST_VERSION) } + if (network.indexOf('_video') > -1) { queryParamsAndValues.push('tt=' + DEFAULT_VAST_VERSION) } const requestURL = gdprApplies ? ENDPOINT_URL_EUROPE : ENDPOINT_URL requests.push({ method: 'POST', - url: requestURL + '?' + networkRequest.join('&'), + url: requestURL + '?' + queryParamsAndValues.join('&'), data: JSON.stringify(networks[network]), bid: bidRequests[network] }); @@ -132,8 +250,20 @@ export const spec = { }, interpretResponse: function (serverResponse, bidRequest) { + if (serverResponse.body.metaData) { + storageTool.saveToStorage(serverResponse.body.metaData); + } const adUnits = serverResponse.body.adUnits; + let validatedBidType = validateBidType(config.getConfig().bidType); + if (bidRequest.bid) { + bidRequest.bid.forEach(b => { + if (b.params && b.params.bidType) { + validatedBidType = validateBidType(b.params.bidType); + } + }); + } + function buildAdResponse(bidderCode, ad, adUnit, dealCount) { const destinationUrls = ad.destinationUrls || {}; const advertiserDomains = []; @@ -143,7 +273,7 @@ export const spec = { const adResponse = { bidderCode: bidderCode, requestId: adUnit.targetId, - cpm: (ad.bid) ? ad.bid.amount * 1000 : 0, + cpm: ad[validatedBidType] ? ad[validatedBidType].amount * 1000 : 0, width: Number(ad.creativeWidth), height: Number(ad.creativeHeight), creativeId: ad.creativeId, @@ -178,7 +308,7 @@ export const spec = { const hasBidAdUnits = adUnits.filter((au) => { const bid = bidsById[au.targetId]; - if (bid && bid.bidder === BIDDER_CODE) { + if (bid && bid.bidder && BIDDER_CODE_DEAL_ALIASES.indexOf(bid.bidder) < 0) { return au.matchedAdCount > 0; } else { // We do NOT accept bids when using this adaptor via one of the diff --git a/modules/adnuntiusRtdProvider.js b/modules/adnuntiusRtdProvider.js index 9234a30aa33..1d5d639aa55 100644 --- a/modules/adnuntiusRtdProvider.js +++ b/modules/adnuntiusRtdProvider.js @@ -5,6 +5,10 @@ import { ajax } from '../src/ajax.js'; import { config as sourceConfig } from '../src/config.js'; +/** + * @typedef {import('../modules/rtdModule/index.js').RtdSubmodule} RtdSubmodule + */ + const GVLID = 855; function init(config, userConsent) { diff --git a/modules/adotBidAdapter.js b/modules/adotBidAdapter.js index c34af4d3d17..b48a7ec43b0 100644 --- a/modules/adotBidAdapter.js +++ b/modules/adotBidAdapter.js @@ -9,6 +9,7 @@ import { convertOrtbRequestToProprietaryNative } from '../src/native.js'; const BIDDER_CODE = 'adot'; const ADAPTER_VERSION = 'v2.0.0'; +const GVLID = 272; const BID_METHOD = 'POST'; const BIDDER_URL = 'https://dsp.adotmob.com/headerbidding{PUBLISHER_PATH}/bidrequest'; const REQUIRED_VIDEO_PARAMS = ['mimes', 'protocols']; @@ -635,7 +636,8 @@ export const spec = { isBidRequestValid, buildRequests, interpretResponse, - getFloor + getFloor, + gvlid: GVLID }; registerBidder(spec); diff --git a/modules/adpod.js b/modules/adpod.js index 4ab8e4e5ab9..f6d8309cd9f 100644 --- a/modules/adpod.js +++ b/modules/adpod.js @@ -13,7 +13,6 @@ */ import { - compareOn, deepAccess, generateUUID, groupBy, @@ -28,7 +27,6 @@ import { import { addBidToAuction, AUCTION_IN_PROGRESS, - doCallbacksIfTimedout, getPriceByGranularity, getPriceGranularity } from '../src/auction.js'; @@ -212,9 +210,6 @@ function firePrebidCacheCall(auctionInstance, bidList, afterBidAdded) { store(bidList, function (error, cacheIds) { if (error) { logWarn(`Failed to save to the video cache: ${error}. Video bid(s) must be discarded.`); - for (let i = 0; i < bidList.length; i++) { - doCallbacksIfTimedout(auctionInstance, bidList[i]); - } } else { for (let i = 0; i < cacheIds.length; i++) { // when uuid in response is empty string then the key already existed, so this bid wasn't cached @@ -324,7 +319,7 @@ export function checkAdUnitSetupHook(fn, adUnits) { * @param {Object} videoMediaType 'mediaTypes.video' associated to bidResponse * @param {Object} bidResponse incoming bidResponse being evaluated by bidderFactory * @returns {boolean} return false if bid duration is deemed invalid as per adUnit configuration; return true if fine -*/ + */ function checkBidDuration(videoMediaType, bidResponse) { const buffer = 2; let bidDuration = deepAccess(bidResponse, 'video.durationSeconds'); @@ -595,6 +590,23 @@ function getAdPodAdUnits(codes) { .filter((adUnit) => (codes.length > 0) ? codes.indexOf(adUnit.code) != -1 : true); } +/** + * This function will create compare function to sort on object property + * @param {string} property + * @returns {function} compare function to be used in sorting + */ +function compareOn(property) { + return function compare(a, b) { + if (a[property] < b[property]) { + return 1; + } + if (a[property] > b[property]) { + return -1; + } + return 0; + } +} + /** * This function removes bids of same category. It will be used when competitive exclusion is enabled. * @param {Array[Object]} bidsReceived diff --git a/modules/adqueryBidAdapter.js b/modules/adqueryBidAdapter.js index 8a953f0d97f..d5a8ba6da84 100644 --- a/modules/adqueryBidAdapter.js +++ b/modules/adqueryBidAdapter.js @@ -1,7 +1,14 @@ import {registerBidder} from '../src/adapters/bidderFactory.js'; import {BANNER} from '../src/mediaTypes.js'; -import { logInfo, buildUrl, triggerPixel, parseSizesInput } from '../src/utils.js'; -import { getStorageManager } from '../src/storageManager.js'; +import {buildUrl, logInfo, parseSizesInput, triggerPixel} from '../src/utils.js'; + +/** + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid + * @typedef {import('../src/adapters/bidderFactory.js').ServerRequest} ServerRequest + * @typedef {import('../src/adapters/bidderFactory.js').BidderSpec} BidderSpec + * @typedef {import('../src/adapters/bidderFactory.js').TimedOutBid} TimedOutBid + */ const ADQUERY_GVLID = 902; const ADQUERY_BIDDER_CODE = 'adquery'; @@ -11,7 +18,6 @@ const ADQUERY_USER_SYNC_DOMAIN = ADQUERY_BIDDER_DOMAIN_PROTOCOL + '://' + ADQUER const ADQUERY_DEFAULT_CURRENCY = 'PLN'; const ADQUERY_NET_REVENUE = true; const ADQUERY_TTL = 360; -const storage = getStorageManager({bidderCode: ADQUERY_BIDDER_CODE}); /** @type {BidderSpec} */ export const spec = { @@ -19,7 +25,8 @@ export const spec = { gvlid: ADQUERY_GVLID, supportedMediaTypes: [BANNER], - /** f + /** + * f * @param {object} bid * @return {boolean} */ @@ -58,7 +65,6 @@ export const spec = { logInfo(request); logInfo(response); - let qid = null; const res = response && response.body && response.body.data; let bidResponses = []; @@ -87,17 +93,6 @@ export const spec = { bidResponses.push(bidResponse); logInfo('bidResponses', bidResponses); - if (res && res.qid) { - if (storage.getDataFromLocalStorage('qid')) { - qid = storage.getDataFromLocalStorage('qid'); - if (qid && qid.includes('%7B%22')) { - storage.setDataInLocalStorage('qid', res.qid); - } - } else { - storage.setDataInLocalStorage('qid', res.qid); - } - } - return bidResponses; }, @@ -130,8 +125,12 @@ export const spec = { */ onBidWon: (bid) => { logInfo('onBidWon', bid); + const bidString = JSON.stringify(bid); - const encodedBuf = window.btoa(bidString); + let copyOfBid = JSON.parse(bidString); + delete copyOfBid.ad; + const shortBidString = JSON.stringify(bid); + const encodedBuf = window.btoa(shortBidString); let params = { q: encodedBuf, @@ -187,10 +186,29 @@ export const spec = { url: syncUrl }]; } - }; + function buildRequest(validBidRequests, bidderRequest) { let bid = validBidRequests; + logInfo('buildRequest: ', bid); + + let userId = null; + if (window.qid) { + userId = window.qid; + } + + if (bid.userId && bid.userId.qid) { + userId = bid.userId.qid + } + + if (!userId) { + // onetime User ID + const ramdomValues = Array.from(window.crypto.getRandomValues(new Uint32Array(4))); + userId = ramdomValues.map(val => val.toString(36)).join('').substring(0, 20); + logInfo('generated onetime User ID: ', userId); + window.qid = userId; + } + let pageUrl = ''; if (bidderRequest && bidderRequest.refererInfo) { pageUrl = bidderRequest.refererInfo.page || ''; @@ -199,11 +217,10 @@ function buildRequest(validBidRequests, bidderRequest) { return { v: '$prebid.version$', placementCode: bid.params.placementId, - // TODO: fix auctionId leak: https://github.com/prebid/Prebid.js/issues/9781 - auctionId: bid.auctionId, + auctionId: null, type: bid.params.type, adUnitCode: bid.adUnitCode, - bidQid: storage.getDataFromLocalStorage('qid') || null, + bidQid: userId, bidId: bid.bidId, bidder: bid.bidder, bidPageUrl: pageUrl, diff --git a/modules/adqueryIdSystem.js b/modules/adqueryIdSystem.js index 5171802caba..eb00011593d 100644 --- a/modules/adqueryIdSystem.js +++ b/modules/adqueryIdSystem.js @@ -8,9 +8,15 @@ import {ajax} from '../src/ajax.js'; import {getStorageManager} from '../src/storageManager.js'; import {submodule} from '../src/hook.js'; -import { isFn, isStr, isPlainObject, logError } from '../src/utils.js'; +import {isFn, isPlainObject, isStr, logError, logInfo} from '../src/utils.js'; import {MODULE_TYPE_UID} from '../src/activities/modules.js'; +/** + * @typedef {import('../modules/userId/index.js').Submodule} Submodule + * @typedef {import('../modules/userId/index.js').SubmoduleConfig} SubmoduleConfig + * @typedef {import('../modules/userId/index.js').IdResponse} IdResponse + */ + const MODULE_NAME = 'qid'; const AU_GVLID = 902; @@ -51,11 +57,7 @@ export const adqueryIdSubmodule = { * @returns {{qid:Object}} */ decode(value) { - let qid = storage.getDataFromLocalStorage('qid'); - if (isStr(qid)) { - return {qid: qid}; - } - return (value && typeof value['qid'] === 'string') ? { 'qid': value['qid'] } : undefined; + return {qid: value} }, /** * performs action to obtain id and return a value in the callback's response argument @@ -64,40 +66,59 @@ export const adqueryIdSubmodule = { * @returns {IdResponse|undefined} */ getId(config) { + logInfo('adqueryIdSubmodule getId'); if (!isPlainObject(config.params)) { config.params = {}; } - const url = paramOrDefault(config.params.url, + + const url = paramOrDefault( + config.params.url, `https://bidder.adquery.io/prebid/qid`, - config.params.urlArg); + config.params.urlArg + ); const resp = function (callback) { - let qid = storage.getDataFromLocalStorage('qid'); - if (isStr(qid)) { - const responseObj = {qid: qid}; - callback(responseObj); - } else { - const callbacks = { - success: response => { - let responseObj; - if (response) { - try { - responseObj = JSON.parse(response); - } catch (error) { - logError(error); - } + let qid = window.qid; + + if (!qid) { + const ramdomValues = Array.from(window.crypto.getRandomValues(new Uint32Array(4))); + qid = ramdomValues.map(val => val.toString(36)).join('').substring(0, 20); + + logInfo('adqueryIdSubmodule ID QID GENERTAED:', qid); + } + logInfo('adqueryIdSubmodule ID QID:', qid); + + const callbacks = { + success: response => { + let responseObj; + if (response) { + try { + responseObj = JSON.parse(response); + } catch (error) { + logError(error); } - callback(responseObj); - }, - error: error => { - logError(`${MODULE_NAME}: ID fetch encountered an error`, error); - callback(); } - }; - ajax(url, callbacks, undefined, {method: 'GET'}); - } + if (responseObj.qid) { + let myQid = responseObj.qid; + storage.setDataInLocalStorage('qid', myQid); + return callback(myQid); + } + callback(); + }, + error: error => { + logError(`${MODULE_NAME}: ID fetch encountered an error`, error); + callback(); + } + }; + ajax(url + '?qid=' + qid, callbacks, undefined, {method: 'GET'}); }; return {callback: resp}; + }, + eids: { + 'qid': { + source: 'adquery.io', + atype: 1 + }, } }; diff --git a/modules/adrelevantisBidAdapter.js b/modules/adrelevantisBidAdapter.js index cf785a1fc87..68cd859e24e 100644 --- a/modules/adrelevantisBidAdapter.js +++ b/modules/adrelevantisBidAdapter.js @@ -1,8 +1,5 @@ import {Renderer} from '../src/Renderer.js'; import { - chunk, - convertCamelToUnderscore, - convertTypes, createTrackPixelHtml, deepAccess, deepClone, @@ -21,7 +18,15 @@ import {BANNER, NATIVE, VIDEO} from '../src/mediaTypes.js'; import {find, includes} from '../src/polyfill.js'; import {INSTREAM, OUTSTREAM} from '../src/video.js'; import { convertOrtbRequestToProprietaryNative } from '../src/native.js'; -import {getANKeywordParam, transformBidderParamKeywords} from '../libraries/appnexusKeywords/anKeywords.js'; +import {getANKeywordParam, transformBidderParamKeywords} from '../libraries/appnexusUtils/anKeywords.js'; +import {convertCamelToUnderscore} from '../libraries/appnexusUtils/anUtils.js'; +import {convertTypes} from '../libraries/transformParamsUtils/convertTypes.js'; +import {chunk} from '../libraries/chunk/chunk.js'; + +/** + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid + */ const BIDDER_CODE = 'adrelevantis'; const URL = 'https://ssp.adrelevantis.com/prebid'; @@ -597,6 +602,8 @@ function parseMediaType(rtbBid) { const adType = rtbBid.ad_type; if (adType === VIDEO) { return VIDEO; + } else if (adType === NATIVE) { + return NATIVE; } else { return BANNER; } diff --git a/modules/adriverBidAdapter.js b/modules/adriverBidAdapter.js index 1af0cffa700..5bce315f572 100644 --- a/modules/adriverBidAdapter.js +++ b/modules/adriverBidAdapter.js @@ -1,5 +1,5 @@ // ADRIVER BID ADAPTER for Prebid 1.13 -import { logInfo, getWindowLocation, getBidIdParameter, _each } from '../src/utils.js'; +import {logInfo, getWindowLocation, _each, getBidIdParameter} from '../src/utils.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; import { getStorageManager } from '../src/storageManager.js'; diff --git a/modules/adriverIdSystem.js b/modules/adriverIdSystem.js index c04ebf48028..1da75f2063d 100644 --- a/modules/adriverIdSystem.js +++ b/modules/adriverIdSystem.js @@ -11,6 +11,13 @@ import { submodule } from '../src/hook.js'; import {getStorageManager} from '../src/storageManager.js'; import {MODULE_TYPE_UID} from '../src/activities/modules.js'; +/** + * @typedef {import('../modules/userId/index.js').Submodule} Submodule + * @typedef {import('../modules/userId/index.js').SubmoduleConfig} SubmoduleConfig + * @typedef {import('../modules/userId/index.js').ConsentData} ConsentData + * @typedef {import('../modules/userId/index.js').IdResponse} IdResponse + */ + const MODULE_NAME = 'adriverId'; export const storage = getStorageManager({moduleType: MODULE_TYPE_UID, moduleName: MODULE_NAME}); diff --git a/modules/adsinteractiveBidAdapter.js b/modules/adsinteractiveBidAdapter.js index 304b8bcade0..ad6bdfeb299 100644 --- a/modules/adsinteractiveBidAdapter.js +++ b/modules/adsinteractiveBidAdapter.js @@ -6,12 +6,14 @@ import { registerBidder } from '../src/adapters/bidderFactory.js'; import { BANNER } from '../src/mediaTypes.js'; const ADSINTERACTIVE_CODE = 'adsinteractive'; -const USER_SYNC_URL_IMAGE = 'https://pb.adsinteractive.com/img'; -const USER_SYNC_URL_IFRAME = 'https://pb.adsinteractive.com/sync'; +const USER_SYNC_URL_IMAGE = 'https://sync.adsinteractive.com/img'; +const USER_SYNC_URL_IFRAME = 'https://sync.adsinteractive.com/sync'; +const GVLID = 1212; export const spec = { code: ADSINTERACTIVE_CODE, supportedMediaTypes: [BANNER], + gvlid: GVLID, isBidRequestValid: (bid) => { return ( diff --git a/modules/adstirBidAdapter.js b/modules/adstirBidAdapter.js new file mode 100644 index 00000000000..4b22d568785 --- /dev/null +++ b/modules/adstirBidAdapter.js @@ -0,0 +1,91 @@ +import * as utils from '../src/utils.js'; +import { config } from '../src/config.js'; +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { BANNER } from '../src/mediaTypes.js'; + +const BIDDER_CODE = 'adstir'; +const ENDPOINT = 'https://ad.ad-stir.com/prebid' + +export const spec = { + code: BIDDER_CODE, + supportedMediaTypes: [BANNER], + + isBidRequestValid: function (bid) { + return !!(utils.isStr(bid.params.appId) && !utils.isEmptyStr(bid.params.appId) && utils.isInteger(bid.params.adSpaceNo)); + }, + + buildRequests: function (validBidRequests, bidderRequest) { + const sua = utils.deepAccess(validBidRequests[0], 'ortb2.device.sua', null); + + const requests = validBidRequests.map((r) => { + return { + method: 'POST', + url: ENDPOINT, + data: JSON.stringify({ + appId: r.params.appId, + adSpaceNo: r.params.adSpaceNo, + auctionId: r.auctionId, + transactionId: r.transactionId, + bidId: r.bidId, + mediaTypes: r.mediaTypes, + sizes: r.sizes, + ref: { + page: bidderRequest.refererInfo.page, + tloc: bidderRequest.refererInfo.topmostLocation, + referrer: bidderRequest.refererInfo.ref, + topurl: config.getConfig('pageUrl') ? false : bidderRequest.refererInfo.reachedTop, + }, + sua, + gdpr: utils.deepAccess(bidderRequest, 'gdprConsent.gdprApplies', false), + usp: (bidderRequest.uspConsent || '1---') !== '1---', + eids: utils.deepAccess(r, 'userIdAsEids', []), + schain: serializeSchain(utils.deepAccess(r, 'schain', null)), + pbVersion: '$prebid.version$', + }), + } + }); + + return requests; + }, + + interpretResponse: function (serverResponse, bidRequest) { + const seatbid = serverResponse.body.seatbid; + if (!utils.isArray(seatbid)) { + return []; + } + const bids = []; + seatbid.forEach((b) => { + const bid = b.bid || null; + if (!bid) { + return; + } + bids.push(bid); + }); + return bids; + }, +} + +function serializeSchain(schain) { + if (!schain) { + return null; + } + + let serializedSchain = `${schain.ver},${schain.complete}`; + + schain.nodes.map(node => { + serializedSchain += `!${encodeURIComponentForRFC3986(node.asi || '')},`; + serializedSchain += `${encodeURIComponentForRFC3986(node.sid || '')},`; + serializedSchain += `${encodeURIComponentForRFC3986(node.hp || '')},`; + serializedSchain += `${encodeURIComponentForRFC3986(node.rid || '')},`; + serializedSchain += `${encodeURIComponentForRFC3986(node.name || '')},`; + serializedSchain += `${encodeURIComponentForRFC3986(node.domain || '')}`; + }); + + return serializedSchain; +} + +function encodeURIComponentForRFC3986(str) { + return encodeURIComponent(str).replace(/[!'()*]/g, c => `%${c.charCodeAt(0).toString(16)}`); +} + +registerBidder(spec); diff --git a/modules/adstirBidAdapter.md b/modules/adstirBidAdapter.md new file mode 100644 index 00000000000..7485375a09d --- /dev/null +++ b/modules/adstirBidAdapter.md @@ -0,0 +1,36 @@ +# Overview + +``` +Module Name: adstir Bidder Adapter +Module Type: Bidder Adapter +Maintainer: support@ad-stir.com +``` + +# Description + +Module that connects to adstir's demand sources + +# Test Parameters + +``` + var adUnits = [ + // Banner adUnit + { + code: 'test-div', + mediaTypes: { + banner: { + sizes: [[300, 250]], + } + }, + bids: [ + { + bidder: 'adstir', + params: { + appId: 'TEST-MEDIA', + adSpaceNo: 1, + } + } + ] + } + ]; +``` diff --git a/modules/adtargetBidAdapter.js b/modules/adtargetBidAdapter.js index 89ba4878acf..a1dec5a420f 100644 --- a/modules/adtargetBidAdapter.js +++ b/modules/adtargetBidAdapter.js @@ -1,8 +1,9 @@ -import {_map, chunk, deepAccess, flatten, isArray, logError, parseSizesInput} from '../src/utils.js'; +import {_map, deepAccess, flatten, isArray, logError, parseSizesInput} from '../src/utils.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import {BANNER, VIDEO} from '../src/mediaTypes.js'; import {config} from '../src/config.js'; import {find} from '../src/polyfill.js'; +import {chunk} from '../libraries/chunk/chunk.js'; const ENDPOINT = 'https://ghb.console.adtarget.com.tr/v2/auction/'; const BIDDER_CODE = 'adtarget'; diff --git a/modules/adtelligentBidAdapter.js b/modules/adtelligentBidAdapter.js index cbba1bb49eb..cadba499b5c 100644 --- a/modules/adtelligentBidAdapter.js +++ b/modules/adtelligentBidAdapter.js @@ -1,9 +1,15 @@ -import {_map, chunk, convertTypes, deepAccess, flatten, isArray, parseSizesInput} from '../src/utils.js'; +import {_map, deepAccess, flatten, isArray, parseSizesInput} from '../src/utils.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import {ADPOD, BANNER, VIDEO} from '../src/mediaTypes.js'; import {config} from '../src/config.js'; import {Renderer} from '../src/Renderer.js'; import {find} from '../src/polyfill.js'; +import {convertTypes} from '../libraries/transformParamsUtils/convertTypes.js'; +import {chunk} from '../libraries/chunk/chunk.js'; + +/** + * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid + */ const subdomainSuffixes = ['', 1, 2]; const AUCTION_PATH = '/v2/auction/'; @@ -15,17 +21,12 @@ const HOST_GETTERS = { return 'ghb' + subdomainSuffixes[num++ % subdomainSuffixes.length] + '.adtelligent.com'; } }()), - navelix: () => 'ghb.hb.navelix.com', - appaloosa: () => 'ghb.hb.appaloosa.media', - onefiftytwomedia: () => 'ghb.ads.152media.com', - bidsxchange: () => 'ghb.hbd.bidsxchange.com', streamkey: () => 'ghb.hb.streamkey.net', janet: () => 'ghb.bidder.jmgads.com', - pgam: () => 'ghb.pgamssp.com', ocm: () => 'ghb.cenarius.orangeclickmedia.com', - vidcrunchllc: () => 'ghb.platform.vidcrunch.com', '9dotsmedia': () => 'ghb.platform.audiodots.com', - copper6: () => 'ghb.app.copper6.com' + copper6: () => 'ghb.app.copper6.com', + indicue: () => 'ghb.console.indicue.com', } const getUri = function (bidderCode) { let bidderWithoutSuffix = bidderCode.split('_')[0]; @@ -42,18 +43,13 @@ export const spec = { code: BIDDER_CODE, gvlid: 410, aliases: [ - 'onefiftytwomedia', - 'appaloosa', - 'bidsxchange', 'streamkey', 'janet', { code: 'selectmedia', gvlid: 775 }, - { code: 'navelix', gvlid: 380 }, - 'pgam', { code: 'ocm', gvlid: 1148 }, - { code: 'vidcrunchllc', gvlid: 1145 }, '9dotsmedia', 'copper6', + 'indicue', ], supportedMediaTypes: [VIDEO, BANNER], isBidRequestValid: function (bid) { @@ -200,6 +196,14 @@ function bidToTag(bidRequests, adapterRequest) { tag.DMPId = window.adtDmp.getUID(); } + if (adapterRequest.gppConsent) { + tag.GPP = adapterRequest.gppConsent.gppString; + tag.GPPSid = adapterRequest.gppConsent.applicableSections?.toString(); + } else if (adapterRequest.ortb2?.regs?.gpp) { + tag.GPP = adapterRequest.ortb2.regs.gpp; + tag.GPPSid = adapterRequest.ortb2.regs.gpp_sid; + } + // end publisher env const bids = []; diff --git a/modules/adtelligentIdSystem.js b/modules/adtelligentIdSystem.js index fb3b5f6fe2a..76713f29775 100644 --- a/modules/adtelligentIdSystem.js +++ b/modules/adtelligentIdSystem.js @@ -8,6 +8,13 @@ import * as ajax from '../src/ajax.js'; import { submodule } from '../src/hook.js'; +/** + * @typedef {import('../modules/userId/index.js').Submodule} Submodule + * @typedef {import('../modules/userId/index.js').SubmoduleConfig} SubmoduleConfig + * @typedef {import('../modules/userId/index.js').ConsentData} ConsentData + * @typedef {import('../modules/userId/index.js').IdResponse} IdResponse + */ + const gvlid = 410; const moduleName = 'adtelligent'; const syncUrl = 'https://idrs.adtelligent.com/get'; @@ -85,6 +92,12 @@ export const adtelligentIdModule = { } } + }, + eids: { + 'adtelligentId': { + source: 'adtelligent.com', + atype: 3 + }, } }; diff --git a/modules/aduptechBidAdapter.js b/modules/aduptechBidAdapter.js index 1ea5f1a0096..fdc1249ded4 100644 --- a/modules/aduptechBidAdapter.js +++ b/modules/aduptechBidAdapter.js @@ -1,7 +1,13 @@ -import {deepClone, getAdUnitSizes, isArray, isBoolean, isEmpty, isFn, isPlainObject} from '../src/utils.js'; +import {deepClone, isArray, isBoolean, isEmpty, isFn, isPlainObject} from '../src/utils.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import {BANNER, NATIVE} from '../src/mediaTypes.js'; import { convertOrtbRequestToProprietaryNative } from '../src/native.js'; +import {getAdUnitSizes} from '../libraries/sizeUtils/sizeUtils.js'; + +/** + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + * @typedef {import('../src/adapters/bidderFactory.js').BidderRequest} BidderRequest + */ export const BIDDER_CODE = 'aduptech'; export const GVLID = 647; diff --git a/modules/adxcgBidAdapter.js b/modules/adxcgBidAdapter.js index 5930f3adb67..dda88575ff5 100644 --- a/modules/adxcgBidAdapter.js +++ b/modules/adxcgBidAdapter.js @@ -1,307 +1,65 @@ // jshint esversion: 6, es3: false, node: true -'use strict'; - -import {registerBidder} from '../src/adapters/bidderFactory.js'; -import {BANNER, NATIVE, VIDEO} from '../src/mediaTypes.js'; +import { ortbConverter } from '../libraries/ortbConverter/converter.js'; +import { convertTypes } from '../libraries/transformParamsUtils/convertTypes.js'; +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { BANNER, NATIVE, VIDEO } from '../src/mediaTypes.js'; import { - _map, - deepAccess, - deepSetValue, - getDNT, isArray, - isPlainObject, - isStr, - mergeDeep, - parseSizesInput, replaceAuctionPrice, - triggerPixel + triggerPixel, + logMessage, + deepSetValue, + getBidIdParameter } from '../src/utils.js'; -import {config} from '../src/config.js'; -import { convertOrtbRequestToProprietaryNative } from '../src/native.js'; - -const { getConfig } = config; +import { config } from '../src/config.js'; const BIDDER_CODE = 'adxcg'; const SECURE_BID_URL = 'https://pbc.adxcg.net/rtb/ortb/pbc?adExchangeId=1'; -const NATIVE_ASSET_IDS = { 0: 'title', 2: 'icon', 3: 'image', 5: 'sponsoredBy', 4: 'body', 1: 'cta' }; -const NATIVE_PARAMS = { - title: { - id: 0, - name: 'title' - }, - icon: { - id: 2, - type: 1, - name: 'img' - }, - image: { - id: 3, - type: 3, - name: 'img' - }, - sponsoredBy: { - id: 5, - name: 'data', - type: 1 - }, - body: { - id: 4, - name: 'data', - type: 2 - }, - cta: { - id: 1, - type: 12, - name: 'data' - } -}; +const DEFAULT_CURRENCY = 'EUR'; +const KNOWN_PARAMS = ['cp', 'ct', 'cf', 'battr', 'deals']; +const DEFAULT_TMAX = 500; +/** + * Adxcg Bid Adapter. + * + */ export const spec = { + code: BIDDER_CODE, - supportedMediaTypes: [ NATIVE, BANNER, VIDEO ], + + aliases: ['mediaopti'], + + supportedMediaTypes: [BANNER, NATIVE, VIDEO], + isBidRequestValid: (bid) => { + logMessage('adxcg - validating isBidRequestValid'); const params = bid.params || {}; const { adzoneid } = params; return !!(adzoneid); }, - buildRequests: (validBidRequests, bidderRequest) => { - // convert Native ORTB definition to old-style prebid native definition - validBidRequests = convertOrtbRequestToProprietaryNative(validBidRequests); - - let app, site; - - const commonFpd = bidderRequest.ortb2 || {}; - let { user } = commonFpd; - - if (typeof getConfig('app') === 'object') { - app = getConfig('app') || {}; - if (commonFpd.app) { - mergeDeep(app, commonFpd.app); - } - } else { - site = getConfig('site') || {}; - if (commonFpd.site) { - mergeDeep(site, commonFpd.site); - } - - if (!site.page) { - site.page = bidderRequest.refererInfo.page; - site.domain = bidderRequest.refererInfo.domain; - } - } - - const device = getConfig('device') || {}; - device.w = device.w || window.innerWidth; - device.h = device.h || window.innerHeight; - device.ua = device.ua || navigator.userAgent; - device.dnt = getDNT() ? 1 : 0; - device.language = (navigator && navigator.language) ? navigator.language.split('-')[0] : ''; - - const tid = bidderRequest.ortb2?.source?.tid; - const test = setOnAny(validBidRequests, 'params.test'); - const currency = getConfig('currency.adServerCurrency'); - const cur = currency && [ currency ]; - const eids = setOnAny(validBidRequests, 'userIdAsEids'); - const schain = setOnAny(validBidRequests, 'schain'); - - const imp = validBidRequests.map((bid, id) => { - const floorInfo = bid.getFloor ? bid.getFloor({ - currency: currency || 'USD' - }) : {}; - const bidfloor = floorInfo.floor; - const bidfloorcur = floorInfo.currency; - const { adzoneid } = bid.params; - - const imp = { - id: id + 1, - tagid: adzoneid, - secure: 1, - bidfloor, - bidfloorcur, - ext: { - } - }; - - const assets = _map(bid.nativeParams, (bidParams, key) => { - const props = NATIVE_PARAMS[key]; - const asset = { - required: bidParams.required & 1, - }; - if (props) { - asset.id = props.id; - let wmin, hmin, w, h; - let aRatios = bidParams.aspect_ratios; - - if (aRatios && aRatios[0]) { - aRatios = aRatios[0]; - wmin = aRatios.min_width || 0; - hmin = aRatios.ratio_height * wmin / aRatios.ratio_width | 0; - } - - if (bidParams.sizes) { - const sizes = flatten(bidParams.sizes); - w = sizes[0]; - h = sizes[1]; - } - - asset[props.name] = { - len: bidParams.len, - type: props.type, - wmin, - hmin, - w, - h - }; - - return asset; - } - }).filter(Boolean); - - if (assets.length) { - imp.native = { - request: JSON.stringify({assets: assets}) - }; - } - - const bannerParams = deepAccess(bid, 'mediaTypes.banner'); - - if (bannerParams && bannerParams.sizes) { - const sizes = parseSizesInput(bannerParams.sizes); - const format = sizes.map(size => { - const [ width, height ] = size.split('x'); - const w = parseInt(width, 10); - const h = parseInt(height, 10); - return { w, h }; - }); - - imp.banner = { - format - }; - } - - const videoParams = deepAccess(bid, 'mediaTypes.video'); - if (videoParams) { - imp.video = videoParams; - } - - return imp; - }); - - const request = { - id: bidderRequest.auctionId, - site, - app, - user, - geo: { utcoffset: new Date().getTimezoneOffset() }, - device, - source: { tid, fd: 1 }, - ext: { - prebid: { - channel: { - name: 'pbjs', - version: '$prebid.version$' - } - } - }, - cur, - imp - }; - - if (test) { - request.is_debug = !!test; - request.test = 1; - } - if (deepAccess(bidderRequest, 'gdprConsent.gdprApplies') !== undefined) { - deepSetValue(request, 'user.ext.consent', bidderRequest.gdprConsent.consentString); - deepSetValue(request, 'regs.ext.gdpr', bidderRequest.gdprConsent.gdprApplies & 1); - } - - if (bidderRequest.uspConsent) { - deepSetValue(request, 'regs.ext.us_privacy', bidderRequest.uspConsent); - } - - if (eids) { - deepSetValue(request, 'user.ext.eids', eids); - } - - if (schain) { - deepSetValue(request, 'source.ext.schain', schain); - } + buildRequests: (bidRequests, bidderRequest) => { + const data = converter.toORTB({ bidRequests, bidderRequest }); return { method: 'POST', url: SECURE_BID_URL, - data: JSON.stringify(request), + data, options: { contentType: 'application/json' }, - bids: validBidRequests + bidderRequest }; }, - interpretResponse: function(serverResponse, { bids }) { - if (!serverResponse.body) { - return; - } - const { seatbid, cur } = serverResponse.body; - - const bidResponses = flatten(seatbid.map(seat => seat.bid)).reduce((result, bid) => { - result[bid.impid - 1] = bid; - return result; - }, []); - - return bids.map((bid, id) => { - const bidResponse = bidResponses[id]; - if (bidResponse) { - const mediaType = deepAccess(bidResponse, 'ext.crType'); - const result = { - requestId: bid.bidId, - cpm: bidResponse.price, - creativeId: bidResponse.crid, - ttl: bidResponse.ttl ? bidResponse.ttl : 300, - netRevenue: bid.netRevenue === 'net', - currency: cur, - burl: bid.burl || '', - mediaType: mediaType, - width: bidResponse.w, - height: bidResponse.h, - dealId: bidResponse.dealid, - }; - deepSetValue(result, 'meta.mediaType', mediaType); - if (isArray(bidResponse.adomain)) { - deepSetValue(result, 'meta.advertiserDomains', bidResponse.adomain); - } - - if (isPlainObject(bidResponse.ext)) { - if (isStr(bidResponse.ext.mediaType)) { - deepSetValue(result, 'meta.mediaType', mediaType); - } - if (isStr(bidResponse.ext.advertiser_id)) { - deepSetValue(result, 'meta.advertiserId', bidResponse.ext.advertiser_id); - } - if (isStr(bidResponse.ext.advertiser_name)) { - deepSetValue(result, 'meta.advertiserName', bidResponse.ext.advertiser_name); - } - if (isStr(bidResponse.ext.agency_name)) { - deepSetValue(result, 'meta.agencyName', bidResponse.ext.agency_name); - } - } - if (mediaType === BANNER) { - result.ad = bidResponse.adm; - } else if (mediaType === NATIVE) { - result.native = parseNative(bidResponse); - result.width = 0; - result.height = 0; - } else if (mediaType === VIDEO) { - result.vastUrl = bidResponse.nurl; - result.vastXml = bidResponse.adm; - } - - return result; - } - }).filter(Boolean); + interpretResponse: (response, request) => { + if (response.body) { + const bids = converter.fromORTB({ response: response.body, request: request.data }).bids; + return bids; + } + return []; }, + getUserSyncs: (syncOptions, responses, gdprConsent, uspConsent) => { const syncs = []; let syncUrl = config.getConfig('adxcg.usersyncUrl'); @@ -323,44 +81,95 @@ export const spec = { } return syncs; }, + onBidWon: (bid) => { // for native requests we put the nurl as an imp tracker, otherwise if the auction takes place on prebid server // the server JS adapter puts the nurl in the adm as a tracking pixel and removes the attribute if (bid.nurl) { triggerPixel(replaceAuctionPrice(bid.nurl, bid.originalCpm)) } + }, + transformBidParams: function (params) { + return convertTypes({ + 'cf': 'string', + 'cp': 'number', + 'ct': 'number', + 'adzoneid': 'string' + }, params); } }; -registerBidder(spec); +const converter = ortbConverter({ + context: { + netRevenue: true, + ttl: 300, + currency: 'EUR' + }, -function parseNative(bid) { - const { assets, link, imptrackers, jstracker } = JSON.parse(bid.adm); - const result = { - clickUrl: link.url, - clickTrackers: link.clicktrackers || undefined, - impressionTrackers: imptrackers || undefined, - javascriptTrackers: jstracker ? [ jstracker ] : undefined - }; - assets.forEach(asset => { - const kind = NATIVE_ASSET_IDS[asset.id]; - const content = kind && asset[NATIVE_PARAMS[kind].name]; - if (content) { - result[kind] = content.text || content.value || { url: content.url, width: content.w, height: content.h }; + imp(buildImp, bidRequest, context) { + const imp = buildImp(bidRequest, context); + // tagid + imp.tagid = bidRequest.params.adzoneid.toString(); + // unknown params + const unknownParams = slotUnknownParams(bidRequest); + if (imp.ext || unknownParams) { + imp.ext = Object.assign({}, imp.ext, unknownParams); + } + // battr + if (bidRequest.params.battr) { + ['banner', 'video', 'audio', 'native'].forEach(k => { + if (imp[k]) { + imp[k].battr = bidRequest.params.battr; + } + }); + } + // deals + if (bidRequest.params.deals && isArray(bidRequest.params.deals)) { + imp.pmp = { + private_auction: 0, + deals: bidRequest.params.deals + }; } - }); - return result; -} -function setOnAny(collection, key) { - for (let i = 0, result; i < collection.length; i++) { - result = deepAccess(collection[i], key); - if (result) { - return result; + imp.secure = Number(window.location.protocol === 'https:'); + + if (!imp.bidfloor && bidRequest.params.bidFloor) { + imp.bidfloor = bidRequest.params.bidFloor; + imp.bidfloorcur = getBidIdParameter('bidFloorCur', bidRequest.params).toUpperCase() || 'USD' } - } -} + return imp; + }, + + request(buildRequest, imps, bidderRequest, context) { + const request = buildRequest(imps, bidderRequest, context); + request.tmax = request.tmax || DEFAULT_TMAX; + request.test = config.getConfig('debug') ? 1 : 0; + request.at = 1; + deepSetValue(request, 'ext.prebid.channel.name', 'pbjs'); + deepSetValue(request, 'ext.prebid.channel.version', '$prebid.version$'); + return request; + }, -function flatten(arr) { - return [].concat(...arr); + bidResponse(buildBidResponse, bid, context) { + const bidResponse = buildBidResponse(bid, context); + bidResponse.cur = bid.cur || DEFAULT_CURRENCY; + return bidResponse; + }, +}); + +/** + * Unknown params are captured and sent on ext + */ +function slotUnknownParams(slot) { + const ext = {}; + const knownParamsMap = {}; + KNOWN_PARAMS.forEach(value => knownParamsMap[value] = 1); + Object.keys(slot.params).forEach(key => { + if (!knownParamsMap[key]) { + ext[key] = slot.params[key]; + } + }); + return Object.keys(ext).length > 0 ? { prebid: ext } : null; } + +registerBidder(spec); diff --git a/modules/adyoulikeBidAdapter.js b/modules/adyoulikeBidAdapter.js index 4080d9f25cd..9bc24b11ac3 100644 --- a/modules/adyoulikeBidAdapter.js +++ b/modules/adyoulikeBidAdapter.js @@ -1,9 +1,15 @@ import {buildUrl, deepAccess, parseSizesInput} from '../src/utils.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; +import { config } from '../src/config.js'; import {find} from '../src/polyfill.js'; import {BANNER, NATIVE, VIDEO} from '../src/mediaTypes.js'; import { convertOrtbRequestToProprietaryNative } from '../src/native.js'; +/** + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid + */ + const VERSION = '1.0'; const BIDDER_CODE = 'adyoulike'; const DEFAULT_DC = 'hb-api'; @@ -63,6 +69,7 @@ export const spec = { // convert Native ORTB definition to old-style prebid native definition bidRequests = convertOrtbRequestToProprietaryNative(bidRequests); let hasVideo = false; + let eids; const payload = { Version: VERSION, Bids: bidRequests.reduce((accumulator, bidReq) => { @@ -81,6 +88,9 @@ export const spec = { if (bidReq.schain) { accumulator[bidReq.bidId].SChain = bidReq.schain; } + if (!eids && bidReq.userIdAsEids && bidReq.userIdAsEids.length) { + eids = bidReq.userIdAsEids; + } if (mediatype === NATIVE) { let nativeReq = bidReq.mediaTypes.native; if (nativeReq.type === 'image') { @@ -120,9 +130,8 @@ export const spec = { if (bidderRequest.ortb2) { payload.ortb2 = bidderRequest.ortb2; } - - if (deepAccess(bidderRequest, 'userIdAsEids')) { - payload.userId = bidderRequest.userIdAsEids; + if (eids) { + payload.eids = eids; } payload.pbjs_version = '$prebid.version$'; @@ -163,6 +172,50 @@ export const spec = { } }); return bidResponses; + }, + + /** + * List user sync endpoints. + * Legal information have to be added to the request. + * Only iframe syncs are supported. + * + * @param {*} syncOptions Publisher prebid configuration. + * @param {*} serverResponses A successful response from the server. + * @return {syncs[]} An array of syncs that should be executed. + */ + getUserSyncs: function (syncOptions, serverResponses, gdprConsent, uspConsent, gppConsent) { + if (!syncOptions.iframeEnabled) { + return []; + } + + let params = ''; + + // GDPR + if (gdprConsent) { + params += '&gdpr=' + (gdprConsent.gdprApplies ? 1 : 0); + params += '&gdpr_consent=' + encodeURIComponent(gdprConsent.consentString || ''); + } + + // coppa compliance + if (config.getConfig('coppa') === true) { + params += '&coppa=1'; + } + + // CCPA + if (uspConsent) { + params += '&us_privacy=' + encodeURIComponent(uspConsent); + } + + // GPP + if (gppConsent?.gppString && gppConsent?.applicableSections?.length) { + params += '&gpp=' + encodeURIComponent(gppConsent.gppString); + params += '&gpp_sid=' + encodeURIComponent(gppConsent?.applicableSections?.join(',')); + } + + return [{ + type: 'iframe', + url: `https://visitor.omnitagjs.com/visitor/isync?uid=19340f4f097d16f41f34fc0274981ca4${params}` + }]; } } diff --git a/modules/agmaAnalyticsAdapter.js b/modules/agmaAnalyticsAdapter.js new file mode 100644 index 00000000000..e43dee063c5 --- /dev/null +++ b/modules/agmaAnalyticsAdapter.js @@ -0,0 +1,225 @@ +import { ajax } from '../src/ajax.js'; +import { + generateUUID, + logInfo, + logError, + getPerformanceNow, + isEmpty, + isEmptyStr, +} from '../src/utils.js'; +import { getGlobal } from '../src/prebidGlobal.js'; +import adapter from '../libraries/analyticsAdapter/AnalyticsAdapter.js'; +import CONSTANTS from '../src/constants.json'; +import adapterManager, { gdprDataHandler } from '../src/adapterManager.js'; +import { getRefererInfo } from '../src/refererDetection.js'; +import { config } from '../src/config.js'; + +const GVLID = 1122; +const ModuleCode = 'agma'; +const analyticsType = 'endpoint'; +const scriptVersion = '1.7.1'; +const batchDelayInMs = 1000; +const agmaURL = 'https://pbc.agma-analytics.de/v1'; +const pageViewId = generateUUID(); + +const { + EVENTS: { AUCTION_INIT }, +} = CONSTANTS; + +// Helper functions +const getScreen = () => { + const w = window; + const d = document; + const e = d.documentElement; + const g = d.getElementsByTagName('body')[0]; + const x = w.innerWidth || e.clientWidth || g.clientWidth; + const y = w.innerHeight || e.clientHeight || g.clientHeight; + return { x, y }; +}; + +const getUserIDs = () => { + try { + return getGlobal().getUserIdsAsEids(); + } catch (e) {} + return []; +}; + +export const getOrtb2Data = (options) => { + let site = null; + let user = null; + + // check if data is provided via config + if (options.ortb2) { + if (options.ortb2.user) { + user = options.ortb2.user; + } + if (options.ortb2.site) { + site = options.ortb2.site; + } + if (site && user) { + return { site, user }; + } + } + try { + const configData = config.getConfig(); + // try to fallback to global config + if (configData.ortb2) { + site = site || configData.ortb2.site; + user = user || configData.ortb2.user; + } + } catch (e) {} + + return { site, user }; +}; + +export const getTiming = () => { + // Timing API V2 + let ttfb = 0; + try { + const entry = performance.getEntriesByType('navigation')[0]; + ttfb = Math.round(entry.responseStart - entry.startTime); + } catch (e) { + // Timing API V1 + try { + const entry = performance.timing; + ttfb = Math.round(entry.responseStart - entry.fetchStart); + } catch (e) { + // Timing API not available + return null; + } + } + const elapsedTime = getPerformanceNow(); + ttfb = ttfb >= 0 && ttfb <= elapsedTime ? ttfb : 0; + return { + ttfb, + elapsedTime, + }; +}; + +export const getPayload = (auctionIds, options) => { + if (!options || !auctionIds || auctionIds.length === 0) { + return false; + } + const consentData = gdprDataHandler.getConsentData(); + let gdprApplies = true; // we assume gdpr applies + let useExtendedPayload = false; + if (consentData) { + gdprApplies = consentData.gdprApplies; + const consents = consentData.vendorData?.vendor?.consents || {}; + useExtendedPayload = consents[GVLID]; + } + const ortb2 = getOrtb2Data(options); + const ri = getRefererInfo() || {}; + + let payload = { + auctionIds: auctionIds, + triggerEvent: options.triggerEvent, + pageViewId, + domain: ri.domain, + gdprApplies, + code: options.code, + ortb2: { site: ortb2.site }, + pageUrl: ri.page, + prebidVersion: '$prebid.version$', + scriptVersion, + debug: options.debug, + timing: getTiming(), + }; + + if (useExtendedPayload) { + const { x, y } = getScreen(); + const userIdsAsEids = getUserIDs(); + payload = { + ...payload, + ortb2, + extended: true, + timestamp: Date.now(), + gdprConsentString: consentData.consentString, + timezoneOffset: new Date().getTimezoneOffset(), + language: window.navigator.language, + referrer: ri.topmostLocation, + pageUrl: ri.page, + screenWidth: x, + screenHeight: y, + userIdsAsEids, + }; + } + return payload; +}; + +const agmaAnalytics = Object.assign(adapter({ analyticsType }), { + auctionIds: [], + timer: null, + track(data) { + const { eventType, args } = data; + if (eventType === this.options.triggerEvent && args && args.auctionId) { + this.auctionIds.push(args.auctionId); + if (this.timer === null) { + this.timer = setTimeout(() => { + this.processBatch(); + }, batchDelayInMs); + } + } + }, + processBatch() { + const currentBatch = [...this.auctionIds]; + const payload = getPayload(currentBatch, this.options); + this.auctionIds = []; + if (this.timer) { + clearTimeout(this.timer); + this.timer = null; + } + this.send(payload); + }, + send(payload) { + if (!payload) { + return; + } + return ajax( + agmaURL, + () => { + logInfo(ModuleCode, 'flushed', payload); + }, + JSON.stringify(payload), + { + contentType: 'text/plain', + method: 'POST', + } + ); + }, +}); + +agmaAnalytics.originEnableAnalytics = agmaAnalytics.enableAnalytics; +agmaAnalytics.enableAnalytics = function (config = {}) { + const { options } = config; + + if (isEmpty(options)) { + logError(ModuleCode, 'Please set options'); + return false; + } + + if (options.site && !options.code) { + logError(ModuleCode, 'Please set `code` - `site` is deprecated'); + options.code = options.site; + } + + if (!options.code || isEmptyStr(options.code)) { + logError(ModuleCode, 'Please set `code` option - agma Analytics is disabled'); + return false; + } + + agmaAnalytics.options = { + triggerEvent: AUCTION_INIT, + ...options, + }; + + agmaAnalytics.originEnableAnalytics(config); +}; + +adapterManager.registerAnalyticsAdapter({ + adapter: agmaAnalytics, + code: ModuleCode, + gvlid: GVLID, +}); + +export default agmaAnalytics; diff --git a/modules/agmaAnalyticsAdapter.md b/modules/agmaAnalyticsAdapter.md new file mode 100644 index 00000000000..30c88fb92ec --- /dev/null +++ b/modules/agmaAnalyticsAdapter.md @@ -0,0 +1,28 @@ +# Overview + Module Name: Agma Analytics + Module Type: Analytics Adapter + Maintainer: [www.agma-mmc.de](https://www.agma-mmc.de) + Technical Support: [info@mllrsohn.com](mailto:info@mllrsohn.com) + +# Description + +Agma Analytics adapter. Please contact [team-internet@agma-mmc.de](mailto:team-internet@agma-mmc.de) for signup and access to [futher documentation](https://docs.agma-analytics.de). + +# Usage + +Add the `agmaAnalyticsAdapter` to your build: + +``` +gulp build --modules=...,agmaAnalyticsAdapter... +``` + +Configure the analytics module: + +```javascript +pbjs.enableAnalytics({ + provider: 'agma', + options: { + code: 'provided-by-agma' // change to the code you received from agma + } +}); +``` diff --git a/modules/aidemBidAdapter.js b/modules/aidemBidAdapter.js index 7469f26156b..c6a5cd96fb6 100644 --- a/modules/aidemBidAdapter.js +++ b/modules/aidemBidAdapter.js @@ -1,27 +1,15 @@ -import { - _each, - contains, - deepAccess, - deepSetValue, - getDNT, - isBoolean, - isNumber, - isStr, - logError, - logInfo -} from '../src/utils.js'; +import {deepAccess, deepSetValue, isBoolean, isNumber, isStr, logError, logInfo} from '../src/utils.js'; import {config} from '../src/config.js'; import {BANNER, VIDEO} from '../src/mediaTypes.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import {getRefererInfo} from '../src/refererDetection.js'; import {ajax} from '../src/ajax.js'; +import {ortbConverter} from '../libraries/ortbConverter/converter.js'; const BIDDER_CODE = 'aidem'; const BASE_URL = 'https://zero.aidemsrv.com'; const LOCAL_BASE_URL = 'http://127.0.0.1:8787'; -const AVAILABLE_CURRENCIES = ['USD']; -const DEFAULT_CURRENCY = ['USD']; // NOTE - USD is the only supported currency right now; Hardcoded for bids const SUPPORTED_MEDIA_TYPES = [BANNER, VIDEO]; const REQUIRED_VIDEO_PARAMS = [ 'mimes', 'protocols', 'context' ]; @@ -37,34 +25,63 @@ export const ERROR_CODES = { }; const endpoints = { - request: `${BASE_URL}/bid/request`, - notice: { - win: `${BASE_URL}/notice/win`, - timeout: `${BASE_URL}/notice/timeout`, - error: `${BASE_URL}/notice/error`, - } + request: `${BASE_URL}/prebidjs/ortb/v2.6/bid/request`, + // notice: { + // win: `${BASE_URL}/notice/win`, + // timeout: `${BASE_URL}/notice/timeout`, + // error: `${BASE_URL}/notice/error`, + // } }; -export function setEndPoints(env = null, path = '', mediaType = BANNER) { +export function setEndPoints(env = null, path = '') { switch (env) { case 'local': - endpoints.request = mediaType === BANNER ? `${LOCAL_BASE_URL}${path}/bid/request` : `${LOCAL_BASE_URL}${path}/bid/videorequest`; - endpoints.notice.win = `${LOCAL_BASE_URL}${path}/notice/win`; - endpoints.notice.error = `${LOCAL_BASE_URL}${path}/notice/error`; - endpoints.notice.timeout = `${LOCAL_BASE_URL}${path}/notice/timeout`; + endpoints.request = `${LOCAL_BASE_URL}${path}/prebidjs/ortb/v2.6/bid/request`; break; case 'main': - endpoints.request = mediaType === BANNER ? `${BASE_URL}${path}/bid/request` : `${BASE_URL}${path}/bid/videorequest`; - endpoints.notice.win = `${BASE_URL}${path}/notice/win`; - endpoints.notice.error = `${BASE_URL}${path}/notice/error`; - endpoints.notice.timeout = `${BASE_URL}${path}/notice/timeout`; + endpoints.request = `${BASE_URL}${path}/prebidjs/ortb/v2.6/bid/request`; break; } return endpoints; } config.getConfig('aidem', function (config) { - if (config.aidem.env) { setEndPoints(config.aidem.env, config.aidem.path, config.aidem.mediaType); } + if (config.aidem.env) { setEndPoints(config.aidem.env, config.aidem.path); } +}); + +const converter = ortbConverter({ + context: { + netRevenue: true, + ttl: 30 + }, + request(buildRequest, imps, bidderRequest, context) { + logInfo('Building request'); + const request = buildRequest(imps, bidderRequest, context); + deepSetValue(request, 'at', 1); + setPrebidRequestEnvironment(request); + deepSetValue(request, 'regs', getRegs()); + deepSetValue(request, 'site.publisher.id', bidderRequest.bids[0].params.publisherId); + deepSetValue(request, 'site.id', bidderRequest.bids[0].params.siteId); + return request; + }, + imp(buildImp, bidRequest, context) { + logInfo('Building imp bidRequest', bidRequest); + const imp = buildImp(bidRequest, context); + deepSetValue(imp, 'tagId', bidRequest.params.placementId); + return imp; + }, + bidResponse(buildBidResponse, bid, context) { + const {bidRequest} = context; + const bidResponse = buildBidResponse(bid, context); + logInfo('Building bidResponse'); + logInfo('bid', bid); + logInfo('bidRequest', bidRequest); + logInfo('bidResponse', bidResponse); + if (bidResponse.mediaType === VIDEO) { + deepSetValue(bidResponse, 'vastUrl', bid.adm); + } + return bidResponse; + } }); // AIDEM Custom FN @@ -89,49 +106,6 @@ function recur(obj) { return result; } -// ================================================================================= -function getConnectionType() { - const connection = navigator.connection || navigator.webkitConnection; - if (!connection) { - return 0; - } - switch (connection.type) { - case 'ethernet': - return 1; - case 'wifi': - return 2; - case 'cellular': - switch (connection.effectiveType) { - case 'slow-2g': - return 4; - case '2g': - return 4; - case '3g': - return 5; - case '4g': - return 6; - case '5g': - return 7; - default: - return 3; - } - default: - return 0; - } -} - -function getDevice() { - const language = navigator.language || navigator.browserLanguage || navigator.userLanguage || navigator.systemLanguage; - return { - ua: navigator.userAgent, - dnt: !!getDNT(), - language: language, - connectiontype: getConnectionType(), - screen_width: screen.width, - screen_height: screen.height - }; -} - function getRegs() { let regs = {}; const consentManagement = config.getConfig('consentManagement'); @@ -157,129 +131,6 @@ function getRegs() { return regs; } -function getPageUrl(bidderRequest) { - return bidderRequest?.refererInfo?.page; -} - -function buildWinNotice(bid) { - const params = bid.params[0]; - const app = deepAccess(bid, 'meta.ext.app') - const winNoticeExt = deepAccess(bid, 'meta.ext.win_notice_ext') - return { - publisherId: params.publisherId, - siteId: params.siteId, - placementId: params.placementId, - burl: deepAccess(bid, 'meta.burl'), - cpm: bid.cpm, - currency: bid.currency, - impid: deepAccess(bid, 'meta.impid'), - dsp_id: deepAccess(bid, 'meta.dsp_id'), - adUnitCode: bid.adUnitCode, - // TODO: fix auctionId/transactionId leak: https://github.com/prebid/Prebid.js/issues/9781 - auctionId: bid.auctionId, - transactionId: bid.transactionId, - ttl: bid.ttl, - requestTimestamp: bid.requestTimestamp, - responseTimestamp: bid.responseTimestamp, - mediatype: bid.mediaType, - environment: app ? 'app' : 'web', - ...app, - ext: winNoticeExt, - }; -} - -function buildErrorNotice(prebidErrorResponse) { - return { - message: `Prebid.js: Server call for ${prebidErrorResponse.bidderCode} failed.`, - url: encodeURIComponent(getPageUrl(prebidErrorResponse)), - auctionId: prebidErrorResponse.auctionId, - bidderRequestId: prebidErrorResponse.bidderRequestId, - metrics: {} - }; -} - -function hasValidFloor(obj) { - if (!obj) return false; - const hasValue = !isNaN(Number(obj.value)); - const hasCurrency = contains(AVAILABLE_CURRENCIES, obj.currency); - return hasValue && hasCurrency; -} - -function getMediaType(bidRequest) { - if ((bidRequest.mediaTypes && bidRequest.mediaTypes.hasOwnProperty('video')) || bidRequest.params.hasOwnProperty('video')) { return VIDEO; } - return BANNER; -} - -function getPrebidRequestFields(bidderRequest, bidRequests) { - const payload = {}; - // Base Payload Data - deepSetValue(payload, 'id', bidderRequest.bidderRequestId); - // Impressions - setPrebidImpressionObject(bidRequests, payload); - // Device - deepSetValue(payload, 'device', getDevice()); - // Timeout - deepSetValue(payload, 'tmax', bidderRequest.timeout); - // Currency - deepSetValue(payload, 'cur', DEFAULT_CURRENCY); - // Timezone - deepSetValue(payload, 'tz', new Date().getTimezoneOffset()); - // Privacy Regs - deepSetValue(payload, 'regs', getRegs()); - // Site - setPrebidSiteObject(bidderRequest, payload); - // Environment - setPrebidRequestEnvironment(payload); - // AT auction type - deepSetValue(payload, 'at', 1); - - return payload; -} - -function setPrebidImpressionObject(bidRequests, payload) { - payload.imp = []; - _each(bidRequests, function (bidRequest) { - const impressionObject = {}; - // Placement or ad tag used to initiate the auction - deepSetValue(impressionObject, 'id', bidRequest.bidId); - // Transaction id - // TODO: `imp.tid` is not ORTB, is this intentional? - deepSetValue(impressionObject, 'tid', deepAccess(bidRequest, 'ortb2Imp.ext.tid')); - // placement id - deepSetValue(impressionObject, 'tagid', deepAccess(bidRequest, 'params.placementId', null)); - // Publisher id - deepSetValue(payload, 'site.publisher.id', deepAccess(bidRequest, 'params.publisherId')); - // Site id - deepSetValue(payload, 'site.id', deepAccess(bidRequest, 'params.siteId')); - const mediaType = getMediaType(bidRequest); - switch (mediaType) { - case 'banner': - setPrebidImpressionObjectBanner(bidRequest, impressionObject); - break; - case 'video': - setPrebidImpressionObjectVideo(bidRequest, impressionObject); - break; - } - - // Floor (optional) - setPrebidImpressionObjectFloor(bidRequest, impressionObject); - - impressionObject.imp_ext = {}; - - payload.imp.push(impressionObject); - }); -} - -function setPrebidSiteObject(bidderRequest, payload) { - deepSetValue(payload, 'site.domain', deepAccess(bidderRequest, 'refererInfo.domain')); - deepSetValue(payload, 'site.page', deepAccess(bidderRequest, 'refererInfo.page')); - deepSetValue(payload, 'site.referer', deepAccess(bidderRequest, 'refererInfo.ref')); - deepSetValue(payload, 'site.cat', deepAccess(bidderRequest, 'ortb2.site.cat')); - deepSetValue(payload, 'site.sectioncat', deepAccess(bidderRequest, 'ortb2.site.sectioncat')); - deepSetValue(payload, 'site.keywords', deepAccess(bidderRequest, 'ortb2.site.keywords')); - deepSetValue(payload, 'site.site_ext', deepAccess(bidderRequest, 'ortb2.site.ext')); // see https://docs.prebid.org/features/firstPartyData.html -} - function setPrebidRequestEnvironment(payload) { const __navigator = JSON.parse(JSON.stringify(recur(navigator))); delete __navigator.plugins; @@ -296,92 +147,6 @@ function setPrebidRequestEnvironment(payload) { deepSetValue(payload, 'environment.wpar.innerHeight', window.innerHeight); } -function setPrebidImpressionObjectFloor(bidRequest, impressionObject) { - const floor = deepAccess(bidRequest, 'params.floor'); - if (hasValidFloor(floor)) { - deepSetValue(impressionObject, 'floor.value', floor.value); - deepSetValue(impressionObject, 'floor.currency', floor.currency); - } -} - -function setPrebidImpressionObjectBanner(bidRequest, impressionObject) { - deepSetValue(impressionObject, 'mediatype', BANNER); - deepSetValue(impressionObject, 'banner.topframe', 1); - deepSetValue(impressionObject, 'banner.format', []); - _each(bidRequest.mediaTypes.banner.sizes, function (bannerFormat) { - const format = {}; - deepSetValue(format, 'w', bannerFormat[0]); - deepSetValue(format, 'h', bannerFormat[1]); - deepSetValue(format, 'format_ext', {}); - impressionObject.banner.format.push(format); - }); -} - -function setPrebidImpressionObjectVideo(bidRequest, impressionObject) { - deepSetValue(impressionObject, 'mediatype', VIDEO); - deepSetValue(impressionObject, 'video.format', []); - deepSetValue(impressionObject, 'video.mimes', bidRequest.mediaTypes.video.mimes); - deepSetValue(impressionObject, 'video.minDuration', bidRequest.mediaTypes.video.minduration); - deepSetValue(impressionObject, 'video.maxDuration', bidRequest.mediaTypes.video.maxduration); - deepSetValue(impressionObject, 'video.protocols', bidRequest.mediaTypes.video.protocols); - deepSetValue(impressionObject, 'video.context', bidRequest.mediaTypes.video.context); - deepSetValue(impressionObject, 'video.playbackmethod', bidRequest.mediaTypes.video.playbackmethod); - deepSetValue(impressionObject, 'skip', bidRequest.mediaTypes.video.skip); - deepSetValue(impressionObject, 'skipafter', bidRequest.mediaTypes.video.skipafter); - deepSetValue(impressionObject, 'video.pos', bidRequest.mediaTypes.video.pos); - _each(bidRequest.mediaTypes.video.playerSize, function (videoPlayerSize) { - const format = {}; - deepSetValue(format, 'w', videoPlayerSize[0]); - deepSetValue(format, 'h', videoPlayerSize[1]); - deepSetValue(format, 'format_ext', {}); - impressionObject.video.format.push(format); - }); -} - -function getPrebidResponseBidObject(openRTBResponseBidObject) { - const prebidResponseBidObject = {}; - // Common properties - deepSetValue(prebidResponseBidObject, 'requestId', openRTBResponseBidObject.impid); - deepSetValue(prebidResponseBidObject, 'cpm', parseFloat(openRTBResponseBidObject.price)); - deepSetValue(prebidResponseBidObject, 'creativeId', openRTBResponseBidObject.crid); - deepSetValue(prebidResponseBidObject, 'currency', openRTBResponseBidObject.cur ? openRTBResponseBidObject.cur.toUpperCase() : DEFAULT_CURRENCY); - deepSetValue(prebidResponseBidObject, 'width', openRTBResponseBidObject.w); - deepSetValue(prebidResponseBidObject, 'height', openRTBResponseBidObject.h); - deepSetValue(prebidResponseBidObject, 'dealId', openRTBResponseBidObject.dealid); - deepSetValue(prebidResponseBidObject, 'netRevenue', true); - deepSetValue(prebidResponseBidObject, 'ttl', 60000); - - if (openRTBResponseBidObject.mediatype === VIDEO) { - logInfo('bidObject.mediatype == VIDEO'); - deepSetValue(prebidResponseBidObject, 'mediaType', VIDEO); - deepSetValue(prebidResponseBidObject, 'vastUrl', openRTBResponseBidObject.adm); - } else { - logInfo('bidObject.mediatype == BANNER'); - deepSetValue(prebidResponseBidObject, 'mediaType', BANNER); - deepSetValue(prebidResponseBidObject, 'ad', openRTBResponseBidObject.adm); - } - setPrebidResponseBidObjectMeta(prebidResponseBidObject, openRTBResponseBidObject); - return prebidResponseBidObject; -} - -function setPrebidResponseBidObjectMeta(prebidResponseBidObject, openRTBResponseBidObject) { - logInfo('AIDEM Bid Adapter meta', openRTBResponseBidObject); - deepSetValue(prebidResponseBidObject, 'meta.advertiserDomains', deepAccess(openRTBResponseBidObject, 'meta.advertiserDomains')); - deepSetValue(prebidResponseBidObject, 'meta.ext', deepAccess(openRTBResponseBidObject, 'meta.ext')); - if (openRTBResponseBidObject.cat && Array.isArray(openRTBResponseBidObject.cat)) { - const primaryCatId = openRTBResponseBidObject.cat.shift(); - deepSetValue(prebidResponseBidObject, 'meta.primaryCatId', primaryCatId); - deepSetValue(prebidResponseBidObject, 'meta.secondaryCatIds', openRTBResponseBidObject.cat); - } - deepSetValue(prebidResponseBidObject, 'meta.id', openRTBResponseBidObject.id); - deepSetValue(prebidResponseBidObject, 'meta.dsp_id', openRTBResponseBidObject.dsp_id); - deepSetValue(prebidResponseBidObject, 'meta.adid', openRTBResponseBidObject.adid); - deepSetValue(prebidResponseBidObject, 'meta.burl', openRTBResponseBidObject.burl); - deepSetValue(prebidResponseBidObject, 'meta.impid', openRTBResponseBidObject.impid); - deepSetValue(prebidResponseBidObject, 'meta.cat', openRTBResponseBidObject.cat); - deepSetValue(prebidResponseBidObject, 'meta.cid', openRTBResponseBidObject.cid); -} - function hasValidMediaType(bidRequest) { const supported = hasBannerMediaType(bidRequest) || hasVideoMediaType(bidRequest); if (!supported) { @@ -494,48 +259,38 @@ export const spec = { return passesRateLimit(bidRequest); }, - buildRequests: function(validBidRequests, bidderRequest) { - logInfo('validBidRequests: ', validBidRequests); + buildRequests: function(bidRequests, bidderRequest) { + logInfo('bidRequests: ', bidRequests); logInfo('bidderRequest: ', bidderRequest); - const prebidRequest = getPrebidRequestFields(bidderRequest, validBidRequests); - const payloadString = JSON.stringify(prebidRequest); - + const data = converter.toORTB({bidRequests, bidderRequest}); + logInfo('request payload', data); return { method: 'POST', url: endpoints.request, - data: payloadString, + data, options: { withCredentials: true } }; }, - interpretResponse: function (serverResponse) { - const bids = []; - logInfo('serverResponse: ', serverResponse); - _each(serverResponse.body.bid, function (bidObject) { - logInfo('bidObject: ', bidObject); - if (!bidObject.price || !bidObject.adm) { - return; - } - logInfo('CPM OK'); - const bid = getPrebidResponseBidObject(bidObject); - bids.push(bid); - }); - return bids; + interpretResponse: function (serverResponse, request) { + logInfo('serverResponse body: ', serverResponse.body); + logInfo('request data: ', request.data); + const ortbBids = converter.fromORTB({response: serverResponse.body, request: request.data}).bids; + logInfo('ortbBids: ', ortbBids); + return ortbBids; }, onBidWon: function(bid) { // Bidder specific code logInfo('onBidWon bid: ', bid); - const notice = buildWinNotice(bid); - ajax(endpoints.notice.win, null, JSON.stringify(notice), { method: 'POST', withCredentials: true }); + ajax(bid.burl); }, - onBidderError: function({ bidderRequest }) { - // Bidder specific code - const notice = buildErrorNotice(bidderRequest); - ajax(endpoints.notice.error, null, JSON.stringify(notice), { method: 'POST', withCredentials: true }); - }, + // onBidderError: function({ bidderRequest }) { + // const notice = buildErrorNotice(bidderRequest); + // ajax(endpoints.notice.error, null, JSON.stringify(notice), { method: 'POST', withCredentials: true }); + // }, }; registerBidder(spec); diff --git a/modules/airgridRtdProvider.js b/modules/airgridRtdProvider.js index 7c6cf1f5de0..2184544807e 100644 --- a/modules/airgridRtdProvider.js +++ b/modules/airgridRtdProvider.js @@ -11,6 +11,10 @@ import {getStorageManager} from '../src/storageManager.js'; import {loadExternalScript} from '../src/adloader.js'; import {MODULE_TYPE_RTD} from '../src/activities/modules.js'; +/** + * @typedef {import('../modules/rtdModule/index.js').RtdSubmodule} RtdSubmodule + */ + const MODULE_NAME = 'realTimeData'; const SUBMODULE_NAME = 'airgrid'; const AG_TCF_ID = 782; diff --git a/modules/ajaBidAdapter.js b/modules/ajaBidAdapter.js index ffab41611ef..fcbe0c12c6e 100644 --- a/modules/ajaBidAdapter.js +++ b/modules/ajaBidAdapter.js @@ -1,7 +1,12 @@ -import { getBidIdParameter, tryAppendQueryString, createTrackPixelHtml, logError, logWarn, deepAccess } from '../src/utils.js'; -import { Renderer } from '../src/Renderer.js'; +import {createTrackPixelHtml, logError, getBidIdParameter} from '../src/utils.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; -import { VIDEO, BANNER, NATIVE } from '../src/mediaTypes.js'; +import { BANNER } from '../src/mediaTypes.js'; +import {tryAppendQueryString} from '../libraries/urlUtils/urlUtils.js'; + +/** + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + * @typedef {import('../src/adapters/bidderFactory.js').ServerRequest} ServerRequest + */ const BidderCode = 'aja'; const URL = 'https://ad.as.amanad.adtdp.com/v2/prebid'; @@ -24,7 +29,7 @@ const BannerSizeMap = { export const spec = { code: BidderCode, - supportedMediaTypes: [VIDEO, BANNER, NATIVE], + supportedMediaTypes: [BANNER], /** * Determines whether or not the given bid has all the params needed to make a valid request. @@ -50,12 +55,20 @@ export const spec = { for (let i = 0, len = validBidRequests.length; i < len; i++) { const bidRequest = validBidRequests[i]; + if ( + (bidRequest.mediaTypes?.native || bidRequest.mediaTypes?.video) && + bidRequest.mediaTypes?.banner) { + continue + } + let queryString = ''; const asi = getBidIdParameter('asi', bidRequest.params); queryString = tryAppendQueryString(queryString, 'asi', asi); queryString = tryAppendQueryString(queryString, 'skt', SDKType); + queryString = tryAppendQueryString(queryString, 'gpid', bidRequest.ortb2Imp?.ext?.gpid) queryString = tryAppendQueryString(queryString, 'tid', bidRequest.ortb2Imp?.ext?.tid) + queryString = tryAppendQueryString(queryString, 'cdep', bidRequest.ortb2?.device?.ext?.cdep) queryString = tryAppendQueryString(queryString, 'prebid_id', bidRequest.bidId); queryString = tryAppendQueryString(queryString, 'prebid_ver', '$prebid.version$'); @@ -63,19 +76,8 @@ 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); - } - } + const adFormatIDs = pickAdFormats(bidRequest) + if (adFormatIDs && adFormatIDs.length > 0) { queryString = tryAppendQueryString(queryString, 'ad_format_ids', adFormatIDs.join(',')); } @@ -86,7 +88,7 @@ export const spec = { })); } - const sua = deepAccess(bidRequest, 'ortb2.device.sua'); + const sua = bidRequest.ortb2?.device?.sua if (sua) { queryString = tryAppendQueryString(queryString, 'sua', JSON.stringify(sua)); } @@ -109,9 +111,17 @@ export const spec = { } const ad = bidderResponseBody.ad; + if (AdType.Banner !== ad.ad_type) { + return [] + } + const bannerAd = bidderResponseBody.ad.banner; const bid = { requestId: ad.prebid_id, + mediaType: BANNER, + ad: bannerAd.tag, + width: bannerAd.w, + height: bannerAd.h, cpm: ad.price, creativeId: ad.creative_id, dealId: ad.deal_id, @@ -119,80 +129,16 @@ export const spec = { netRevenue: true, ttl: 300, // 5 minutes meta: { - advertiserDomains: [] + advertiserDomains: bannerAd.adomain, }, } - - if (AdType.Video === ad.ad_type) { - const videoAd = bidderResponseBody.ad.video; - Object.assign(bid, { - vastXml: videoAd.vtag, - width: videoAd.w, - height: videoAd.h, - renderer: newRenderer(bidderResponseBody), - adResponse: bidderResponseBody, - mediaType: VIDEO - }); - - Array.prototype.push.apply(bid.meta.advertiserDomains, videoAd.adomain) - } else if (AdType.Banner === ad.ad_type) { - const bannerAd = bidderResponseBody.ad.banner; - Object.assign(bid, { - width: bannerAd.w, - height: bannerAd.h, - ad: bannerAd.tag, - mediaType: BANNER - }); - try { - bannerAd.imps.forEach(impTracker => { - const tracker = createTrackPixelHtml(impTracker); - bid.ad += tracker; - }); - } catch (error) { - logError('Error appending tracking pixel', error); - } - - Array.prototype.push.apply(bid.meta.advertiserDomains, bannerAd.adomain) - } else if (AdType.Native === ad.ad_type) { - const nativeAds = ad.native.template_and_ads.ads; - if (nativeAds.length === 0) { - return []; - } - - const nativeAd = nativeAds[0]; - const assets = nativeAd.assets; - - Object.assign(bid, { - mediaType: NATIVE + try { + bannerAd.imps.forEach(impTracker => { + const tracker = createTrackPixelHtml(impTracker); + bid.ad += tracker; }); - - bid.native = { - title: assets.title, - body: assets.description, - cta: assets.cta_text, - sponsoredBy: assets.sponsor, - clickUrl: assets.lp_link, - impressionTrackers: nativeAd.imps, - privacyLink: assets.adchoice_url - }; - - if (assets.img_main !== undefined) { - bid.native.image = { - url: assets.img_main, - width: parseInt(assets.img_main_width, 10), - height: parseInt(assets.img_main_height, 10) - }; - } - - if (assets.img_icon !== undefined) { - bid.native.icon = { - url: assets.img_icon, - width: parseInt(assets.img_icon_width, 10), - height: parseInt(assets.img_icon_height, 10) - }; - } - - Array.prototype.push.apply(bid.meta.advertiserDomains, nativeAd.adomain) + } catch (error) { + logError('Error appending tracking pixel', error); } return [bid]; @@ -228,34 +174,23 @@ export const spec = { }, } -function newRenderer(bidderResponse) { - const renderer = Renderer.install({ - id: bidderResponse.ad.prebid_id, - url: bidderResponse.ad.video.purl, - loaded: false, - }); +function pickAdFormats(bidRequest) { + let sizes = bidRequest.sizes || [] + sizes.push(...(bidRequest.mediaTypes?.banner?.sizes || [])) - try { - renderer.setRender(outstreamRender); - } catch (err) { - logWarn('Prebid Error calling setRender on newRenderer', err); - } + const adFormatIDs = []; + for (const size of sizes) { + if (size.length !== 2) { + continue + } - return renderer; -} + const adFormatID = BannerSizeMap[`${size[0]}x${size[1]}`]; + if (adFormatID) { + adFormatIDs.push(adFormatID); + } + } -function outstreamRender(bid) { - bid.renderer.push(() => { - window['aja_vast_player'].init({ - vast_tag: bid.adResponse.ad.video.vtag, - ad_unit_code: bid.adUnitCode, // target div id to render video - width: bid.width, - height: bid.height, - progress: bid.adResponse.ad.video.progress, - loop: bid.adResponse.ad.video.loop, - inread: bid.adResponse.ad.video.inread - }); - }); + return [...new Set(adFormatIDs)] } registerBidder(spec); diff --git a/modules/ajaBidAdapter.md b/modules/ajaBidAdapter.md index 66155875f4d..92ffecaeb9f 100644 --- a/modules/ajaBidAdapter.md +++ b/modules/ajaBidAdapter.md @@ -8,7 +8,7 @@ Maintainer: ssp_support@aja-kk.co.jp # Description Connects to Aja exchange for bids. -Aja bid adapter supports Banner and Outstream Video. +Aja bid adapter supports Banner. # Test Parameters ```js @@ -29,64 +29,6 @@ var adUnits = [ asi: 'tk82gbLmg' } }] - }, - // Video outstream adUnit - { - code: 'prebid_video', - mediaTypes: { - video: { - context: 'outstream', - playerSize: [300, 250] - } - }, - bids: [{ - bidder: 'aja', - params: { - asi: '1-KwEG_iR' - } - }] - }, - // Native adUnit - { - code: 'prebid_native', - mediaTypes: { - native: { - image: { - required: true, - sendId: false - }, - title: { - required: true, - sendId: true - }, - sponsoredBy: { - required: false, - sendId: true - }, - clickUrl: { - required: false, - sendId: true - }, - body: { - required: false, - sendId: true - }, - icon: { - required: false, - sendId: false - }, - privacyLink: { - required: true, - sendId: true - }, - } - }, - bids: [{ - bidder: 'aja', - params: { - asi: 'qxueUGliR' - } - }] } ]; ``` diff --git a/modules/alkimiBidAdapter.js b/modules/alkimiBidAdapter.js index c087b3061a0..53f6460434f 100644 --- a/modules/alkimiBidAdapter.js +++ b/modules/alkimiBidAdapter.js @@ -1,18 +1,20 @@ import {registerBidder} from '../src/adapters/bidderFactory.js'; -import {deepAccess, deepClone} from '../src/utils.js'; +import {deepAccess, deepClone, getDNT, generateUUID, replaceAuctionPrice} from '../src/utils.js'; import {ajax} from '../src/ajax.js'; -import {VIDEO} from '../src/mediaTypes.js'; +import {VIDEO, BANNER} from '../src/mediaTypes.js'; import {config} from '../src/config.js'; const BIDDER_CODE = 'alkimi'; +const GVLID = 1169; export const ENDPOINT = 'https://exchange.alkimi-onboarding.com/bid?prebid=true'; export const spec = { code: BIDDER_CODE, + gvlid: GVLID, supportedMediaTypes: ['banner', 'video'], isBidRequestValid: function (bid) { - return !!(bid.params && bid.params.bidFloor && bid.params.token); + return !!(bid.params && bid.params.token); }, buildRequests: function (validBidRequests, bidderRequest) { @@ -28,12 +30,15 @@ export const spec = { bids.push({ token: bidRequest.params.token, - pos: bidRequest.params.pos, + instl: bidRequest.params.instl, + exp: bidRequest.params.exp, bidFloor: getBidFloor(bidRequest, formatTypes), sizes: prepareSizes(deepAccess(bidRequest, 'mediaTypes.banner.sizes')), playerSizes: prepareSizes(deepAccess(bidRequest, 'mediaTypes.video.playerSize')), impMediaTypes: formatTypes, - adUnitCode: bidRequest.adUnitCode + adUnitCode: bidRequest.adUnitCode, + video: deepAccess(bidRequest, 'mediaTypes.video'), + banner: deepAccess(bidRequest, 'mediaTypes.banner') }) bidIds.push(bidRequest.bidId) }) @@ -41,14 +46,26 @@ export const spec = { const alkimiConfig = config.getConfig('alkimi'); let payload = { - // TODO: fix auctionId leak: https://github.com/prebid/Prebid.js/issues/9781 - requestId: bidderRequest.auctionId, + requestId: generateUUID(), signRequest: {bids, randomUUID: alkimiConfig && alkimiConfig.randomUUID}, bidIds, referer: bidderRequest.refererInfo.page, signature: alkimiConfig && alkimiConfig.signature, schain: validBidRequests[0].schain, - cpp: config.getConfig('coppa') ? 1 : 0 + cpp: config.getConfig('coppa') ? 1 : 0, + device: { + dnt: getDNT() ? 1 : 0, + w: screen.width, + h: screen.height + }, + ortb2: { + site: { + keywords: bidderRequest.ortb2?.site?.keywords + }, + at: bidderRequest.ortb2?.at, + bcat: bidderRequest.ortb2?.bcat, + wseat: bidderRequest.ortb2?.wseat + } } if (bidderRequest && bidderRequest.gdprConsent) { @@ -99,7 +116,7 @@ export const spec = { // banner or video if (VIDEO === bid.mediaType) { - bid.vastXml = bid.ad; + bid.vastUrl = replaceAuctionPrice(bid.winUrl, bid.cpm); } bid.meta = {}; @@ -112,21 +129,12 @@ export const spec = { }, onBidWon: function (bid) { - let winUrl; - if (bid.winUrl || bid.vastUrl) { - winUrl = bid.winUrl ? bid.winUrl : bid.vastUrl; - winUrl = winUrl.replace(/\$\{AUCTION_PRICE}/, bid.cpm); - } else if (bid.ad) { - let trackImg = bid.ad.match(/(?!^)/); - bid.ad = bid.ad.replace(trackImg[0], ''); - winUrl = trackImg[0].split('"')[1]; - winUrl = winUrl.replace(/\$%7BAUCTION_PRICE%7D/, bid.cpm); - } else { - return false; + if (BANNER == bid.mediaType && bid.winUrl) { + const winUrl = replaceAuctionPrice(bid.winUrl, bid.cpm); + ajax(winUrl, null); + return true; } - - ajax(winUrl, null); - return true; + return false; } } diff --git a/modules/ampliffyBidAdapter.js b/modules/ampliffyBidAdapter.js new file mode 100644 index 00000000000..bcd28e5bcf1 --- /dev/null +++ b/modules/ampliffyBidAdapter.js @@ -0,0 +1,419 @@ +import {registerBidder} from '../src/adapters/bidderFactory.js'; +import {logError, logInfo, triggerPixel} from '../src/utils.js'; + +const BIDDER_CODE = 'ampliffy'; +const GVLID = 1258; +const DEFAULT_ENDPOINT = 'bidder.ampliffy.com'; +const TTL = 600; // Time-to-Live - how long (in seconds) Prebid can use this bid. +const LOG_PREFIX = 'AmpliffyBidder: '; + +function isBidRequestValid(bid) { + logInfo(LOG_PREFIX + 'isBidRequestValid: Code: ' + bid.adUnitCode + ': Param' + JSON.stringify(bid.params), bid.adUnitCode); + if (bid.params) { + if (!bid.params.placementId || !bid.params.format) return false; + + if (bid.params.format.toLowerCase() !== 'video' && bid.params.format.toLowerCase() !== 'display' && bid.params.format.toLowerCase() !== 'all') return false; + if (bid.params.format.toLowerCase() === 'video' && !bid.mediaTypes['video']) return false; + if (bid.params.format.toLowerCase() === 'display' && !bid.mediaTypes['banner']) return false; + + if (!bid.params.server || bid.params.server === '') { + const server = bid.params.type + bid.params.region + bid.params.adnetwork; + if (server && server !== '') bid.params.server = server; + else bid.params.server = DEFAULT_ENDPOINT; + } + return true; + } + return false; +} + +function manageConsentArguments(bidderRequest) { + let consent = null; + if (bidderRequest?.gdprConsent) { + consent = { + gdpr: bidderRequest.gdprConsent.gdprApplies ? '1' : '0', + }; + if (bidderRequest.gdprConsent.consentString) { + consent.consent_string = bidderRequest.gdprConsent.consentString; + } + if (bidderRequest.gdprConsent.addtlConsent && bidderRequest.gdprConsent.addtlConsent.indexOf('~') !== -1) { + consent.addtl_consent = bidderRequest.gdprConsent.addtlConsent; + } + } + return consent; +} + +function buildRequests(validBidRequests, bidderRequest) { + const bidRequests = []; + for (const bidRequest of validBidRequests) { + for (const sizes of bidRequest.sizes) { + let extraParams = mergeParams(getDefaultParams(), bidRequest.params.extraParams); + // Apply GDPR parameters to request. + extraParams = mergeParams(extraParams, manageConsentArguments(bidderRequest)); + const serverURL = getServerURL(bidRequest.params.server, sizes, bidRequest.params.placementId, extraParams); + logInfo(LOG_PREFIX + serverURL, 'requests'); + extraParams.bidId = bidRequest.bidId; + bidRequests.push({ + method: 'GET', + url: serverURL, + data: extraParams, + bidRequest, + }); + } + logInfo(LOG_PREFIX + 'Building request from: ' + bidderRequest.url + ': ' + JSON.stringify(bidRequests), bidRequest.adUnitCode); + } + return bidRequests; +} +export function getDefaultParams() { + return { + ciu_szs: '1x1', + gdfp_req: '1', + env: 'vp', + output: 'xml_vast4', + unviewed_position_start: '1' + }; +} +export function mergeParams(params, extraParams) { + if (extraParams) { + for (const k in extraParams) { + params[k] = extraParams[k]; + } + } + return params; +} +export function paramsToQueryString(params) { + return Object.entries(params).filter(e => typeof e[1] !== 'undefined').map(e => { + if (e[1]) return encodeURIComponent(e[0]) + '=' + encodeURIComponent(e[1]); + else return encodeURIComponent(e[0]); + }).join('&'); +} +const getCacheBuster = () => Math.floor(Math.random() * (9999999999 - 1000000000)); + +// For testing purposes +let currentUrl = null; +export function getCurrentURL() { + if (!currentUrl) currentUrl = top.location.href; + return currentUrl; +} +export function setCurrentURL(url) { + currentUrl = url; +} +const getCurrentURLEncoded = () => encodeURIComponent(getCurrentURL()); +function getServerURL(server, sizes, iu, queryParams) { + const random = getCacheBuster(); + const size = sizes[0] + 'x' + sizes[1]; + let serverURL = '//' + server + '/gampad/ads'; + queryParams.sz = size; + queryParams.iu = iu; + queryParams.url = getCurrentURL(); + queryParams.description_url = getCurrentURL(); + queryParams.correlator = random; + + return serverURL; +} +function interpretResponse(serverResponse, bidRequest) { + const bidResponses = []; + + const bidResponse = {}; + let mediaType = 'video'; + if ( + bidRequest.bidRequest?.mediaTypes && + !bidRequest.bidRequest.mediaTypes['video'] + ) { + mediaType = 'banner'; + } + bidResponse.requestId = bidRequest.bidRequest.bidId; + bidResponse.width = bidRequest.bidRequest?.sizes[0][0]; + bidResponse.height = bidRequest.bidRequest?.sizes[0][1]; + bidResponse.ttl = TTL; + bidResponse.creativeId = 'ampCreativeID134'; + bidResponse.netRevenue = true; + bidResponse.mediaType = mediaType; + bidResponse.meta = { + advertiserDomains: [], + }; + let xmlStr = serverResponse.body; + const xml = new window.DOMParser().parseFromString(xmlStr, 'text/xml'); + const xmlData = parseXML(xml, bidResponse); + logInfo(LOG_PREFIX + 'Response from: ' + bidRequest.url + ': ' + JSON.stringify(xmlData), bidRequest.bidRequest.adUnitCode); + if (xmlData.cpm < 0 || !xmlData.creativeURL || !xmlData.bidUp) { + return []; + } + bidResponse.cpm = xmlData.cpm; + bidResponse.currency = xmlData.currency; + + if (mediaType === 'video') { + logInfo(LOG_PREFIX + xmlData.creativeURL, 'requests'); + bidResponse.vastUrl = xmlData.creativeURL; + } else { + bidResponse.adUrl = xmlData.creativeURL; + } + if (xmlData.trackingUrl) { + bidResponse.vastImpUrl = xmlData.trackingUrl; + bidResponse.trackingUrl = xmlData.trackingUrl; + } + bidResponses.push(bidResponse); + return bidResponses; +} +const replaceMacros = (txt, cpm, bid) => { + const size = bid.width + 'x' + bid.height; + txt = txt.replaceAll('%%CACHEBUSTER%%', getCacheBuster()); + txt = txt.replaceAll('@@CACHEBUSTER@@', getCacheBuster()); + txt = txt.replaceAll('%%REFERER%%', getCurrentURLEncoded()); + txt = txt.replaceAll('@@REFERER@@', getCurrentURLEncoded()); + txt = txt.replaceAll('%%REFERRER_URL_UNESC%%', getCurrentURLEncoded()); + txt = txt.replaceAll('@@REFERRER_URL_UNESC@@', getCurrentURLEncoded()); + txt = txt.replaceAll('%%PRICE_ESC%%', encodePrice(cpm)); + txt = txt.replaceAll('@@PRICE_ESC@@', encodePrice(cpm)); + txt = txt.replaceAll('%%SIZES%%', size); + txt = txt.replaceAll('@@SIZES@@', size); + return txt; +} +const encodePrice = (price) => { + price = parseFloat(price); + const s = 116.54; + const c = 1; + const a = 1; + let encodedPrice = s * Math.log10(price + a) + c; + encodedPrice = Math.min(200, encodedPrice); + encodedPrice = Math.round(Math.max(1, encodedPrice)); + + // Format the encoded price with leading zeros if necessary + const formattedEncodedPrice = encodedPrice.toString().padStart(3, '0'); + + // Build the encoding key + const encodingKey = `H--${formattedEncodedPrice}`; + + return encodeURIComponent(`vch=${encodingKey}`); +}; + +function extractCT(xml) { + let ct = null; + try { + try { + const vastAdTagURI = xml.getElementsByTagName('VASTAdTagURI')[0] + if (vastAdTagURI) { + let url = null; + for (const childNode of vastAdTagURI.childNodes) { + if (childNode.nodeValue.trim().includes('http')) { + url = decodeURIComponent(childNode.nodeValue); + } + } + const urlParams = new URLSearchParams(url); + ct = urlParams.get('ct') + } + } catch (e) { + } + if (!ct) { + const geoExtensions = xml.querySelectorAll('Extension[type="geo"]'); + geoExtensions.forEach((geoExtension) => { + const countryElement = geoExtension.querySelector('Country'); + if (countryElement) { + ct = countryElement.textContent; + } + }); + } + } catch (e) {} + return ct; +} + +function extractCPM(htmlContent, ct, cpm) { + const cpmMapDiv = htmlContent.querySelectorAll('[cpmMap]')[0]; + if (cpmMapDiv) { + let cpmMapJSON = JSON.parse(cpmMapDiv.getAttribute('cpmMap')); + if ((cpmMapJSON)) { + if (cpmMapJSON[ct]) { + cpm = cpmMapJSON[ct]; + } else if (cpmMapJSON['default']) { + cpm = cpmMapJSON['default']; + } + } + } + return cpm; +} + +function extractCurrency(htmlContent, currency) { + const currencyDiv = htmlContent.querySelectorAll('[cpmCurrency]')[0]; + if (currencyDiv) { + const currencyValue = currencyDiv.getAttribute('cpmCurrency'); + if (currencyValue && currencyValue !== '') { + currency = currencyValue; + } + } + return currency; +} + +function extractCreativeURL(htmlContent, ct, cpm, bid) { + let creativeURL = null; + const creativeMap = htmlContent.querySelectorAll('[creativeMap]')[0]; + if (creativeMap) { + const creativeMapString = creativeMap.getAttribute('creativeMap'); + + const creativeMapJSON = JSON.parse(creativeMapString); + let defaultURL = null; + for (const url of Object.keys(creativeMapJSON)) { + const geo = creativeMapJSON[url]; + if (geo.includes(ct)) { + creativeURL = replaceMacros(url, cpm, bid); + } else if (geo.includes('default')) { + defaultURL = url; + } + } + if (!creativeURL && defaultURL) creativeURL = replaceMacros(defaultURL, cpm, bid); + } + return creativeURL; +} + +function extractSyncs(htmlContent) { + let userSyncsJSON = null; + const userSyncs = htmlContent.querySelectorAll('[userSyncs]')[0]; + if (userSyncs) { + const userSyncsString = userSyncs.getAttribute('userSyncs'); + + userSyncsJSON = JSON.parse(userSyncsString); + } + return userSyncsJSON; +} + +function extractTrackingURL(htmlContent, ret) { + const trackingUrlDiv = htmlContent.querySelectorAll('[bidder-tracking-url]')[0]; + if (trackingUrlDiv) { + const trackingUrl = trackingUrlDiv.getAttribute('bidder-tracking-url'); + // eslint-disable-next-line no-console + logInfo(LOG_PREFIX + 'parseXML: trackingUrl: ', trackingUrl) + ret.trackingUrl = trackingUrl; + } +} + +export function parseXML(xml, bid) { + const ret = { cpm: 0.001, currency: 'EUR', creativeURL: null, bidUp: false }; + const ct = extractCT(xml); + if (!ct) return ret; + + try { + if (ct) { + const companion = xml.getElementsByTagName('Companion')[0]; + const htmlResource = companion.getElementsByTagName('HTMLResource')[0]; + const htmlContent = document.createElement('html'); + htmlContent.innerHTML = htmlResource.textContent; + + ret.cpm = extractCPM(htmlContent, ct, ret.cpm); + ret.currency = extractCurrency(htmlContent, ret.currency); + ret.creativeURL = extractCreativeURL(htmlContent, ct, ret.cpm, bid); + extractTrackingURL(htmlContent, ret); + ret.bidUp = isAllowedToBidUp(htmlContent, getCurrentURL()); + ret.userSyncs = extractSyncs(htmlContent); + } + } catch (e) { + // eslint-disable-next-line no-console + logError(LOG_PREFIX + 'Error parsing XML', e); + } + // eslint-disable-next-line no-console + logInfo(LOG_PREFIX + 'parseXML RET:', ret); + + return ret; +} +export function isAllowedToBidUp(html, currentURL) { + currentURL = currentURL.split('?')[0]; // Remove parameters + let allowedToPush = false; + try { + const domainsMap = html.querySelectorAll('[domainMap]')[0]; + if (domainsMap) { + let domains = JSON.parse(domainsMap.getAttribute('domainMap')); + if (domains.domainMap) { + domains = domains.domainMap; + } + domains.forEach((d) => { + if (currentURL.includes(d) || d === 'all' || d === '*') allowedToPush = true; + }) + } else { + allowedToPush = true; + } + if (allowedToPush) { + const excludedURL = html.querySelectorAll('[excludedURLs]')[0]; + if (excludedURL) { + const excludedURLsString = domainsMap.getAttribute('excludedURLs'); + if (excludedURLsString !== '') { + let excluded = JSON.parse(excludedURLsString); + excluded.forEach((d) => { + if (currentURL.includes(d)) allowedToPush = false; + }) + } + } + } + } catch (e) { + // eslint-disable-next-line no-console + logError(LOG_PREFIX + 'isAllowedToBidUp', e); + } + return allowedToPush; +} + +function getSyncData(options, syncs) { + const ret = []; + if (syncs?.length) { + for (const sync of syncs) { + if (sync.type === 'syncImage' && options.pixelEnabled) { + ret.push({url: sync.url, type: 'image'}); + } else if (sync.type === 'syncIframe' && options.iframeEnabled) { + ret.push({url: sync.url, type: 'iframe'}); + } + } + } + return ret; +} + +function getUserSyncs(syncOptions, serverResponses) { + const userSyncs = []; + for (const serverResponse of serverResponses) { + if (serverResponse.body) { + try { + const xmlStr = serverResponse.body; + const xml = new window.DOMParser().parseFromString(xmlStr, 'text/xml'); + const xmlData = parseXML(xml, {}); + if (xmlData.userSyncs) { + userSyncs.push(...getSyncData(syncOptions, xmlData.userSyncs)); + } + } catch (e) {} + } + } + return userSyncs; +} + +function onBidWon(bid) { + logInfo(`${LOG_PREFIX} WON AMPLIFFY`); + if (bid.trackingUrl) { + let url = bid.trackingUrl; + + // Replace macros with URL-encoded bid parameters + Object.keys(bid).forEach(key => { + const macroKey = `%%${key.toUpperCase()}%%`; + const value = encodeURIComponent(JSON.stringify(bid[key])); + url = url.split(macroKey).join(value); + }); + + triggerPixel(url, () => { + logInfo(`${LOG_PREFIX} send data success`); + }, + (e) => { + logError(`${LOG_PREFIX} send data error`, e); + }); + } +} +function onTimeOut() { + // eslint-disable-next-line no-console + logInfo(LOG_PREFIX + 'TIMEOUT'); +} + +export const spec = { + code: BIDDER_CODE, + gvlid: GVLID, + aliases: ['ampliffy', 'amp', 'videoffy', 'publiffy'], + supportedMediaTypes: ['video', 'banner'], + isBidRequestValid, + buildRequests, + interpretResponse, + getUserSyncs, + onTimeOut, + onBidWon, +}; + +registerBidder(spec); diff --git a/modules/ampliffyBidAdapter.md b/modules/ampliffyBidAdapter.md new file mode 100644 index 00000000000..a425d910582 --- /dev/null +++ b/modules/ampliffyBidAdapter.md @@ -0,0 +1,39 @@ +# Overview + +``` +Module Name: Ampliffy Bidder Adapter +Module Type: Bidder Adapter +Maintainer: bidder@ampliffy.com +``` + +# Description + +Connects to Ampliffy Ad server for bids. + +Ampliffy bid adapter supports Video currently, and has initial support for Banner. + +For more information about [Ampliffy](https://www.ampliffy.com/en/), please contact [info@ampliffy.com](info@ampliffy.com). + +# Sample Ad Unit: For Publishers +```javascript +var videoAdUnit = [ +{ + code: 'video1', + mediaTypes: { + video: { + playerSize: [[640, 480]], + context: 'instream' + }, + }, + bids: [{ + bidder: 'ampliffy', + params: { + server: 'bidder.ampliffy.com', + placementId: '1213213/example/vrutal_/', + format: 'video' + } + }] +}]; +``` + +``` diff --git a/modules/amxBidAdapter.js b/modules/amxBidAdapter.js index 7286001b888..6e14f65b0c8 100644 --- a/modules/amxBidAdapter.js +++ b/modules/amxBidAdapter.js @@ -1,5 +1,5 @@ -import {registerBidder} from '../src/adapters/bidderFactory.js'; -import {BANNER, VIDEO} from '../src/mediaTypes.js'; +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { BANNER, VIDEO } from '../src/mediaTypes.js'; import { _each, deepAccess, @@ -10,23 +10,35 @@ import { logError, parseUrl, triggerPixel, + generateUUID, } from '../src/utils.js'; -import {config} from '../src/config.js'; -import {getStorageManager} from '../src/storageManager.js'; +import { config } from '../src/config.js'; +import { getStorageManager } from '../src/storageManager.js'; +import { fetch } from '../src/ajax.js'; const BIDDER_CODE = 'amx'; const storage = getStorageManager({ bidderCode: BIDDER_CODE }); const SIMPLE_TLD_TEST = /\.com?\.\w{2,4}$/; const DEFAULT_ENDPOINT = 'https://prebid.a-mo.net/a/c'; -const VERSION = 'pba1.3.2'; +const VERSION = 'pba1.3.4'; const VAST_RXP = /^\s*<\??(?:vast|xml)/i; -const TRACKING_ENDPOINT = 'https://1x1.a-mo.net/hbx/'; +const TRACKING_BASE = 'https://1x1.a-mo.net/'; +const TRACKING_ENDPOINT = TRACKING_BASE + 'hbx/'; +const POST_TRACKING_ENDPOINT = TRACKING_BASE + 'e'; const AMUID_KEY = '__amuidpb'; function getLocation(request) { return parseUrl(request.refererInfo?.topmostLocation || window.location.href); } +function getTimeoutSize(timeoutData) { + if (timeoutData.sizes == null || timeoutData.sizes.length === 0) { + return [0, 0]; + } + + return timeoutData.sizes[0]; +} + const largestSize = (sizes, mediaTypes) => { const allSizes = sizes .concat(deepAccess(mediaTypes, `${BANNER}.sizes`, []) || []) @@ -148,7 +160,9 @@ function convertRequest(bid) { const tid = deepAccess(bid, 'params.tagId'); const au = - bid.params != null && typeof bid.params.adUnitId === 'string' && bid.params.adUnitId !== '' + bid.params != null && + typeof bid.params.adUnitId === 'string' && + bid.params.adUnitId !== '' ? bid.params.adUnitId : bid.adUnitCode; @@ -201,7 +215,10 @@ function isSyncEnabled(syncConfigP, syncType) { return false; } - if (syncConfig.bidders === '*' || (isArray(syncConfig.bidders) && syncConfig.bidders.indexOf('amx') !== -1)) { + if ( + syncConfig.bidders === '*' || + (isArray(syncConfig.bidders) && syncConfig.bidders.indexOf('amx') !== -1) + ) { return syncConfig.filter == null || syncConfig.filter === 'include'; } @@ -218,12 +235,17 @@ function getSyncSettings() { d: 0, l: 0, t: 0, - e: true + e: true, }; } - const settings = { d: syncConfig.syncDelay, l: syncConfig.syncsPerBidder, t: 0, e: syncConfig.syncEnabled } - const all = isSyncEnabled(syncConfig.filterSettings, 'all') + 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; @@ -255,12 +277,14 @@ function getGpp(bidderRequest) { return bidderRequest.gppConsent; } - return bidderRequest?.ortb2?.regs?.gpp ?? {gppString: '', applicableSections: ''}; + return ( + bidderRequest?.ortb2?.regs?.gpp ?? { gppString: '', applicableSections: '' } + ); } function buildReferrerInfo(bidderRequest) { if (bidderRequest.refererInfo == null) { - return {r: '', t: false, c: '', l: 0, s: []} + return { r: '', t: false, c: '', l: 0, s: [] }; } const re = bidderRequest.refererInfo; @@ -271,7 +295,7 @@ function buildReferrerInfo(bidderRequest) { l: re.numIframes, s: re.stack, c: re.canonicalUrl, - } + }; } const isTrue = (boolValue) => @@ -303,8 +327,7 @@ export const spec = { }; const payload = { - // TODO: fix auctionId leak: https://github.com/prebid/Prebid.js/issues/9781 - a: bidderRequest.auctionId, + a: generateUUID(), B: 0, b: loc.host, brc: fbid.bidderRequestsCount || 0, @@ -358,28 +381,35 @@ export const spec = { return { data: payload, method: 'POST', + browsingTopics: true, url: deepAccess(bidRequests[0], 'params.endpoint', DEFAULT_ENDPOINT), withCredentials: true, }; }, - getUserSyncs(syncOptions, serverResponses, gdprConsent, uspConsent, gppConsent) { + 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 || '') + gpp_sid: enc(gppConsent?.applicableSections || ''), }; const iframeSync = { url: `https://prebid.a-mo.net/isyn?${formatQS(qp)}`, - type: 'iframe' + type: 'iframe', }; if (serverResponses == null || serverResponses.length === 0) { if (syncOptions.iframeEnabled) { - return [iframeSync] + return [iframeSync]; } return []; @@ -388,13 +418,16 @@ export const spec = { const output = []; let hasFrame = false; - _each(serverResponses, function ({ body: response }) { + _each(serverResponses, function({ body: response }) { if (response != null && response.p != null && response.p.hreq) { - _each(response.p.hreq, function (syncPixel) { + _each(response.p.hreq, function(syncPixel) { const pixelType = syncPixel.indexOf('__st=iframe') !== -1 ? 'iframe' : 'image'; if (syncOptions.iframeEnabled || pixelType === 'image') { - hasFrame = hasFrame || (pixelType === 'iframe') || (syncPixel.indexOf('cchain') !== -1) + hasFrame = + hasFrame || + pixelType === 'iframe' || + syncPixel.indexOf('cchain') !== -1; output.push({ url: syncPixel, type: pixelType, @@ -405,7 +438,7 @@ export const spec = { }); if (!hasFrame && output.length < 2) { - output.push(iframeSync) + output.push(iframeSync); } return output; @@ -470,20 +503,58 @@ export const spec = { aud: targetingData.requestId, a: targetingData.adUnitCode, c2: nestedQs(targetingData.adserverTargeting), + cn3: targetingData.timeToRespond, }); }, onTimeout(timeoutData) { - if (timeoutData == null) { + if (timeoutData == null || !timeoutData.length) { return; } - trackEvent('pbto', { - A: timeoutData.bidder, - bid: timeoutData.bidId, - a: timeoutData.adUnitCode, - cn: timeoutData.timeout, - aud: timeoutData.auctionId, + let common = null; + const events = timeoutData.map((timeout) => { + const params = timeout.params || {}; + const size = getTimeoutSize(timeout); + const { domain, page, ref } = + timeout.ortb2 != null && timeout.ortb2.site != null + ? timeout.ortb2.site + : {}; + + if (common == null) { + common = { + do: domain, + u: page, + U: getUIDSafe(), + re: ref, + V: '$prebid.version$', + vg: '$$PREBID_GLOBAL$$', + }; + } + + return { + A: timeout.bidder, + mid: params.tagId, + a: params.adunitId || timeout.adUnitCode, + bid: timeout.bidId, + n: 'g_pbto', + aud: timeout.transactionId, + w: size[0], + h: size[1], + cn: timeout.timeout, + cn2: timeout.bidderRequestsCount, + cn3: timeout.bidderWinsCount, + }; + }); + + const payload = JSON.stringify({ c: common, e: events }); + fetch(POST_TRACKING_ENDPOINT, { + body: payload, + keepalive: true, + withCredentials: true, + method: 'POST' + }).catch((_e) => { + // do nothing; ignore errors }); }, diff --git a/modules/amxIdSystem.js b/modules/amxIdSystem.js index 5deffa00dfe..184eff76c34 100644 --- a/modules/amxIdSystem.js +++ b/modules/amxIdSystem.js @@ -152,6 +152,12 @@ export const amxIdSubmodule = { return { callback }; }, + eids: { + amxId: { + source: 'amxdt.net', + atype: 1, + }, + } }; submodule('userId', amxIdSubmodule); diff --git a/modules/apacdexBidAdapter.js b/modules/apacdexBidAdapter.js index e1557d9c6d3..834df134c2e 100644 --- a/modules/apacdexBidAdapter.js +++ b/modules/apacdexBidAdapter.js @@ -96,8 +96,8 @@ export const spec = { payload.device = {}; payload.device.ua = navigator.userAgent; - payload.device.height = window.screen.width; - payload.device.width = window.screen.height; + payload.device.height = window.screen.height; + payload.device.width = window.screen.width; payload.device.dnt = _getDoNotTrack(); payload.device.language = navigator.language; diff --git a/modules/appierBidAdapter.js b/modules/appierBidAdapter.js index 12346d15130..fa314f0bd5f 100644 --- a/modules/appierBidAdapter.js +++ b/modules/appierBidAdapter.js @@ -2,6 +2,11 @@ import { registerBidder } from '../src/adapters/bidderFactory.js'; import { BANNER } from '../src/mediaTypes.js'; import { config } from '../src/config.js'; +/** + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid + */ + export const ADAPTER_VERSION = '1.0.0'; const SUPPORTED_AD_TYPES = [BANNER]; diff --git a/modules/appnexusBidAdapter.js b/modules/appnexusBidAdapter.js index 0660f4f4b10..e62824cb2aa 100644 --- a/modules/appnexusBidAdapter.js +++ b/modules/appnexusBidAdapter.js @@ -1,17 +1,10 @@ import { - chunk, - convertCamelToUnderscore, - convertTypes, createTrackPixelHtml, deepAccess, deepClone, - fill, getBidRequest, - getMaxValueFromArray, - getMinValueFromArray, getParameterByName, getUniqueIdentifierStr, - getWindowFromDocument, isArray, isArrayOfNums, isEmpty, @@ -40,7 +33,15 @@ import { getANKewyordParamFromMaps, getANKeywordParam, transformBidderParamKeywords -} from '../libraries/appnexusKeywords/anKeywords.js'; +} from '../libraries/appnexusUtils/anKeywords.js'; +import {convertCamelToUnderscore, fill} from '../libraries/appnexusUtils/anUtils.js'; +import {convertTypes} from '../libraries/transformParamsUtils/convertTypes.js'; +import {chunk} from '../libraries/chunk/chunk.js'; + +/** + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid + */ const BIDDER_CODE = 'appnexus'; const URL = 'https://ib.adnxs.com/ut/v3/prebid'; @@ -114,6 +115,7 @@ export const spec = { { code: 'beintoo', gvlid: 618 }, { code: 'projectagora', gvlid: 1032 }, { code: 'uol', gvlid: 32 }, + { code: 'adzymic', gvlid: 32 }, ], supportedMediaTypes: [BANNER, VIDEO, NATIVE], @@ -349,6 +351,36 @@ export const spec = { } } + if (bidderRequest?.ortb2?.regs?.ext?.dsa) { + const pubDsaObj = bidderRequest.ortb2.regs.ext.dsa; + const dsaObj = {}; + ['required', 'pubrender', 'datatopub'].forEach((dsaKey) => { + if (isNumber(pubDsaObj[dsaKey])) { + if (dsaKey === 'required') { + // our client-side endpoint has a different name for the 'required' field + // ...this is the only exception to the openrtb spec for these fields + dsaObj.dsainfo = pubDsaObj[dsaKey]; + } else { + dsaObj[dsaKey] = pubDsaObj[dsaKey]; + } + } + }); + + if (isArray(pubDsaObj.transparency) && pubDsaObj.transparency.every((v) => isPlainObject(v))) { + const tpData = []; + pubDsaObj.transparency.forEach((tpObj) => { + if (isStr(tpObj.domain) && tpObj.domain != '' && isArray(tpObj.params) && tpObj.params.every((v) => isNumber(v))) { + tpData.push(tpObj); + } + }); + if (tpData.length > 0) { + dsaObj.transparency = tpData; + } + } + + if (!isEmpty(dsaObj)) payload.dsa = dsaObj; + } + if (tags[0].publisher_id) { payload.publisher_id = tags[0].publisher_id; } @@ -588,6 +620,10 @@ function newBid(serverBid, rtbBid, bidderRequest) { bid.meta = Object.assign({}, bid.meta, { advertiserId: rtbBid.advertiser_id }); } + if (rtbBid.dsa) { + bid.meta = Object.assign({}, bid.meta, { dsa: rtbBid.dsa }); + } + // temporary function; may remove at later date if/when adserver fully supports dchain function setupDChain(rtbBid) { let dchain = { @@ -788,7 +824,7 @@ function bidToTag(bid) { tag.keywords = auKeywords; } - let gpid = deepAccess(bid, 'ortb2Imp.ext.data.pbadslot'); + let gpid = deepAccess(bid, 'ortb2Imp.ext.gpid') || deepAccess(bid, 'ortb2Imp.ext.data.pbadslot'); if (gpid) { tag.gpid = gpid; } @@ -1031,7 +1067,7 @@ function createAdPodRequest(tags, adPodBid) { const { durationRangeSec, requireExactDuration } = adPodBid.mediaTypes.video; const numberOfPlacements = getAdPodPlacementNumber(adPodBid.mediaTypes.video); - const maxDuration = getMaxValueFromArray(durationRangeSec); + const maxDuration = Math.max(...durationRangeSec); const tagToDuplicate = tags.filter(tag => tag.uuid === adPodBid.bidId); let request = fill(...tagToDuplicate, numberOfPlacements); @@ -1057,7 +1093,7 @@ function createAdPodRequest(tags, adPodBid) { function getAdPodPlacementNumber(videoParams) { const { adPodDurationSec, durationRangeSec, requireExactDuration } = videoParams; - const minAllowedDuration = getMinValueFromArray(durationRangeSec); + const minAllowedDuration = Math.min(...durationRangeSec); const numberOfPlacements = Math.floor(adPodDurationSec / minAllowedDuration); return requireExactDuration @@ -1142,7 +1178,7 @@ function outstreamRender(bid, doc) { hideSASIframe(bid.adUnitCode); // push to render queue because ANOutstreamVideo may not be loaded yet bid.renderer.push(() => { - const win = getWindowFromDocument(doc) || window; + const win = doc?.defaultView || window; win.ANOutstreamVideo.renderAd({ tagId: bid.adResponse.tag_id, sizes: [bid.getSize().split('x')], diff --git a/modules/arcspanRtdProvider.js b/modules/arcspanRtdProvider.js index a7ffa059279..8ccf3d160b9 100644 --- a/modules/arcspanRtdProvider.js +++ b/modules/arcspanRtdProvider.js @@ -2,6 +2,10 @@ import { submodule } from '../src/hook.js'; import { mergeDeep } from '../src/utils.js'; import {loadExternalScript} from '../src/adloader.js'; +/** + * @typedef {import('../modules/rtdModule/index.js').RtdSubmodule} RtdSubmodule + */ + /** @type {string} */ const MODULE_NAME = 'realTimeData'; const SUBMODULE_NAME = 'arcspan'; diff --git a/modules/asoBidAdapter.js b/modules/asoBidAdapter.js index e569f04a2a8..704cffefb39 100644 --- a/modules/asoBidAdapter.js +++ b/modules/asoBidAdapter.js @@ -7,14 +7,14 @@ import { isArray, isFn, logWarn, - parseSizesInput, - tryAppendQueryString + parseSizesInput } from '../src/utils.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; import { config } from '../src/config.js'; import { BANNER, VIDEO } from '../src/mediaTypes.js'; import { Renderer } from '../src/Renderer.js'; import { parseDomain } from '../src/refererDetection.js'; +import {tryAppendQueryString} from '../libraries/urlUtils/urlUtils.js'; const BIDDER_CODE = 'aso'; const DEFAULT_SERVER_URL = 'https://srv.aso1.net'; diff --git a/modules/asteriobidAnalyticsAdapter.js b/modules/asteriobidAnalyticsAdapter.js new file mode 100644 index 00000000000..516a3a65667 --- /dev/null +++ b/modules/asteriobidAnalyticsAdapter.js @@ -0,0 +1,336 @@ +import { generateUUID, getParameterByName, logError, logInfo, parseUrl } from '../src/utils.js' +import { ajaxBuilder } from '../src/ajax.js' +import adapter from '../libraries/analyticsAdapter/AnalyticsAdapter.js' +import adapterManager from '../src/adapterManager.js' +import { getStorageManager } from '../src/storageManager.js' +import CONSTANTS from '../src/constants.json' +import { MODULE_TYPE_ANALYTICS } from '../src/activities/modules.js' +import {getRefererInfo} from '../src/refererDetection.js'; + +/** + * asteriobidAnalyticsAdapter.js - analytics adapter for AsterioBid + */ +export const storage = getStorageManager({ moduleType: MODULE_TYPE_ANALYTICS, moduleName: 'asteriobid' }) +const DEFAULT_EVENT_URL = 'https://endpt.asteriobid.com/endpoint' +const analyticsType = 'endpoint' +const analyticsName = 'AsterioBid Analytics' +const utmTags = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content'] +const _VERSION = 1 + +let ajax = ajaxBuilder(20000) +let initOptions +let auctionStarts = {} +let auctionTimeouts = {} +let sampling +let pageViewId +let flushInterval +let eventQueue = [] +let asteriobidAnalyticsEnabled = false + +let asteriobidAnalytics = Object.assign(adapter({ url: DEFAULT_EVENT_URL, analyticsType }), { + track({ eventType, args }) { + handleEvent(eventType, args) + } +}) + +asteriobidAnalytics.originEnableAnalytics = asteriobidAnalytics.enableAnalytics +asteriobidAnalytics.enableAnalytics = function (config) { + initOptions = config.options || {} + + pageViewId = initOptions.pageViewId || generateUUID() + sampling = initOptions.sampling || 1 + + if (Math.floor(Math.random() * sampling) === 0) { + asteriobidAnalyticsEnabled = true + flushInterval = setInterval(flush, 1000) + } else { + logInfo(`${analyticsName} isn't enabled because of sampling`) + } + + asteriobidAnalytics.originEnableAnalytics(config) +} + +asteriobidAnalytics.originDisableAnalytics = asteriobidAnalytics.disableAnalytics +asteriobidAnalytics.disableAnalytics = function () { + if (!asteriobidAnalyticsEnabled) { + return + } + flush() + clearInterval(flushInterval) + asteriobidAnalytics.originDisableAnalytics() +} + +function collectUtmTagData() { + let newUtm = false + let pmUtmTags = {} + try { + utmTags.forEach(function (utmKey) { + let utmValue = getParameterByName(utmKey) + if (utmValue !== '') { + newUtm = true + } + pmUtmTags[utmKey] = utmValue + }) + if (newUtm === false) { + utmTags.forEach(function (utmKey) { + let itemValue = storage.getDataFromLocalStorage(`pm_${utmKey}`) + if (itemValue && itemValue.length !== 0) { + pmUtmTags[utmKey] = itemValue + } + }) + } else { + utmTags.forEach(function (utmKey) { + storage.setDataInLocalStorage(`pm_${utmKey}`, pmUtmTags[utmKey]) + }) + } + } catch (e) { + logError(`${analyticsName} Error`, e) + pmUtmTags['error_utm'] = 1 + } + return pmUtmTags +} + +function collectPageInfo() { + const pageInfo = { + domain: window.location.hostname, + } + if (document.referrer) { + pageInfo.referrerDomain = parseUrl(document.referrer).hostname + } + + const refererInfo = getRefererInfo() + pageInfo.page = refererInfo.page + pageInfo.ref = refererInfo.ref + + return pageInfo +} + +function flush() { + if (!asteriobidAnalyticsEnabled) { + return + } + + if (eventQueue.length > 0) { + const data = { + pageViewId: pageViewId, + ver: _VERSION, + bundleId: initOptions.bundleId, + events: eventQueue, + utmTags: collectUtmTagData(), + pageInfo: collectPageInfo(), + sampling: sampling + } + eventQueue = [] + + if ('version' in initOptions) { + data.version = initOptions.version + } + if ('tcf_compliant' in initOptions) { + data.tcf_compliant = initOptions.tcf_compliant + } + if ('adUnitDict' in initOptions) { + data.adUnitDict = initOptions.adUnitDict; + } + if ('customParam' in initOptions) { + data.customParam = initOptions.customParam; + } + + const url = initOptions.url ? initOptions.url : DEFAULT_EVENT_URL + ajax( + url, + () => logInfo(`${analyticsName} sent events batch`), + _VERSION + ':' + JSON.stringify(data), + { + contentType: 'text/plain', + method: 'POST', + withCredentials: true + } + ) + } +} + +function trimAdUnit(adUnit) { + if (!adUnit) return adUnit + const res = {} + res.code = adUnit.code + res.sizes = adUnit.sizes + return res +} + +function trimBid(bid) { + if (!bid) return bid + const res = {} + res.auctionId = bid.auctionId + res.bidder = bid.bidder + res.bidderRequestId = bid.bidderRequestId + res.bidId = bid.bidId + res.crumbs = bid.crumbs + res.cpm = bid.cpm + res.currency = bid.currency + res.mediaTypes = bid.mediaTypes + res.sizes = bid.sizes + res.transactionId = bid.transactionId + res.adUnitCode = bid.adUnitCode + res.bidRequestsCount = bid.bidRequestsCount + res.serverResponseTimeMs = bid.serverResponseTimeMs + return res +} + +function trimBidderRequest(bidderRequest) { + if (!bidderRequest) return bidderRequest + const res = {} + res.auctionId = bidderRequest.auctionId + res.auctionStart = bidderRequest.auctionStart + res.bidderRequestId = bidderRequest.bidderRequestId + res.bidderCode = bidderRequest.bidderCode + res.bids = bidderRequest.bids && bidderRequest.bids.map(trimBid) + return res +} + +function handleEvent(eventType, eventArgs) { + if (!asteriobidAnalyticsEnabled) { + return + } + + try { + eventArgs = eventArgs ? JSON.parse(JSON.stringify(eventArgs)) : {} + } catch (e) { + // keep eventArgs as is + } + + const pmEvent = {} + pmEvent.timestamp = eventArgs.timestamp || Date.now() + pmEvent.eventType = eventType + + switch (eventType) { + case CONSTANTS.EVENTS.AUCTION_INIT: { + pmEvent.auctionId = eventArgs.auctionId + pmEvent.timeout = eventArgs.timeout + pmEvent.adUnits = eventArgs.adUnits && eventArgs.adUnits.map(trimAdUnit) + pmEvent.bidderRequests = eventArgs.bidderRequests && eventArgs.bidderRequests.map(trimBidderRequest) + auctionStarts[pmEvent.auctionId] = pmEvent.timestamp + auctionTimeouts[pmEvent.auctionId] = pmEvent.timeout + break + } + case CONSTANTS.EVENTS.AUCTION_END: { + pmEvent.auctionId = eventArgs.auctionId + pmEvent.end = eventArgs.end + pmEvent.start = eventArgs.start + pmEvent.adUnitCodes = eventArgs.adUnitCodes + pmEvent.bidsReceived = eventArgs.bidsReceived && eventArgs.bidsReceived.map(trimBid) + pmEvent.start = auctionStarts[pmEvent.auctionId] + pmEvent.end = Date.now() + break + } + case CONSTANTS.EVENTS.BID_ADJUSTMENT: { + break + } + case CONSTANTS.EVENTS.BID_TIMEOUT: { + pmEvent.bidders = eventArgs && eventArgs.map ? eventArgs.map(trimBid) : eventArgs + pmEvent.duration = auctionTimeouts[pmEvent.auctionId] + break + } + case CONSTANTS.EVENTS.BID_REQUESTED: { + pmEvent.auctionId = eventArgs.auctionId + pmEvent.bidderCode = eventArgs.bidderCode + pmEvent.doneCbCallCount = eventArgs.doneCbCallCount + pmEvent.start = eventArgs.start + pmEvent.bidderRequestId = eventArgs.bidderRequestId + pmEvent.bids = eventArgs.bids && eventArgs.bids.map(trimBid) + pmEvent.auctionStart = eventArgs.auctionStart + pmEvent.timeout = eventArgs.timeout + break + } + case CONSTANTS.EVENTS.BID_RESPONSE: { + pmEvent.bidderCode = eventArgs.bidderCode + pmEvent.width = eventArgs.width + pmEvent.height = eventArgs.height + pmEvent.adId = eventArgs.adId + pmEvent.mediaType = eventArgs.mediaType + pmEvent.cpm = eventArgs.cpm + pmEvent.currency = eventArgs.currency + pmEvent.requestId = eventArgs.requestId + pmEvent.adUnitCode = eventArgs.adUnitCode + pmEvent.auctionId = eventArgs.auctionId + pmEvent.timeToRespond = eventArgs.timeToRespond + pmEvent.requestTimestamp = eventArgs.requestTimestamp + pmEvent.responseTimestamp = eventArgs.responseTimestamp + pmEvent.netRevenue = eventArgs.netRevenue + pmEvent.size = eventArgs.size + pmEvent.adserverTargeting = eventArgs.adserverTargeting + break + } + case CONSTANTS.EVENTS.BID_WON: { + pmEvent.auctionId = eventArgs.auctionId + pmEvent.adId = eventArgs.adId + pmEvent.adserverTargeting = eventArgs.adserverTargeting + pmEvent.adUnitCode = eventArgs.adUnitCode + pmEvent.bidderCode = eventArgs.bidderCode + pmEvent.height = eventArgs.height + pmEvent.mediaType = eventArgs.mediaType + pmEvent.netRevenue = eventArgs.netRevenue + pmEvent.cpm = eventArgs.cpm + pmEvent.requestTimestamp = eventArgs.requestTimestamp + pmEvent.responseTimestamp = eventArgs.responseTimestamp + pmEvent.size = eventArgs.size + pmEvent.width = eventArgs.width + pmEvent.currency = eventArgs.currency + pmEvent.bidder = eventArgs.bidder + break + } + case CONSTANTS.EVENTS.BIDDER_DONE: { + pmEvent.auctionId = eventArgs.auctionId + pmEvent.auctionStart = eventArgs.auctionStart + pmEvent.bidderCode = eventArgs.bidderCode + pmEvent.bidderRequestId = eventArgs.bidderRequestId + pmEvent.bids = eventArgs.bids && eventArgs.bids.map(trimBid) + pmEvent.doneCbCallCount = eventArgs.doneCbCallCount + pmEvent.start = eventArgs.start + pmEvent.timeout = eventArgs.timeout + pmEvent.tid = eventArgs.tid + pmEvent.src = eventArgs.src + break + } + case CONSTANTS.EVENTS.SET_TARGETING: { + break + } + case CONSTANTS.EVENTS.REQUEST_BIDS: { + break + } + case CONSTANTS.EVENTS.ADD_AD_UNITS: { + break + } + case CONSTANTS.EVENTS.AD_RENDER_FAILED: { + pmEvent.bid = eventArgs.bid + pmEvent.message = eventArgs.message + pmEvent.reason = eventArgs.reason + break + } + default: + return + } + + sendEvent(pmEvent) +} + +function sendEvent(event) { + eventQueue.push(event) + logInfo(`${analyticsName} Event ${event.eventType}:`, event) + + if (event.eventType === CONSTANTS.EVENTS.AUCTION_END) { + flush() + } +} + +adapterManager.registerAnalyticsAdapter({ + adapter: asteriobidAnalytics, + code: 'asteriobid' +}) + +asteriobidAnalytics.getOptions = function () { + return initOptions +} + +asteriobidAnalytics.flush = flush + +export default asteriobidAnalytics diff --git a/modules/asteriobidAnalyticsAdapter.md b/modules/asteriobidAnalyticsAdapter.md new file mode 100644 index 00000000000..524cf6e2721 --- /dev/null +++ b/modules/asteriobidAnalyticsAdapter.md @@ -0,0 +1,41 @@ +# Overview + +Module Name: AsterioBid Analytics Adapter +Module Type: Analytics Adapter +Maintainer: admin@asteriobid.com + +# Description +Analytics adapter for
AsterioBid. Contact admin@asteriobid.com for information. + +# Test Parameters + +``` +pbjs.enableAnalytics({ + provider: 'asteriobid', + options: { + bundleId: '04bcf17b-9733-4675-9f67-d475f881ab78' + } +}); + +``` + +# Advanced Parameters + +``` +pbjs.enableAnalytics({ + provider: 'asteriobid', + options: { + bundleId: '04bcf17b-9733-4675-9f67-d475f881ab78', + version: 'v1', // configuration version for the comparison + adUnitDict: { // provide names of the ad units for better reporting + adunitid1: 'Top Banner', + adunitid2: 'Bottom Banner' + }, + customParam: { // provide custom parameters values that you want to collect and report + param1: 'value1', + param2: 'value2' + } + } +}); + +``` diff --git a/modules/astraoneBidAdapter.js b/modules/astraoneBidAdapter.js index d7f92bb5fac..9645b8e68bd 100644 --- a/modules/astraoneBidAdapter.js +++ b/modules/astraoneBidAdapter.js @@ -2,6 +2,12 @@ import { _map } from '../src/utils.js'; import { registerBidder } from '../src/adapters/bidderFactory.js' import { BANNER } from '../src/mediaTypes.js' +/** + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid + * @typedef {import('../src/adapters/bidderFactory.js').ServerResponse} ServerResponse + */ + const BIDDER_CODE = 'astraone'; const SSP_ENDPOINT = 'https://ssp.astraone.io/auction/prebid'; const TTL = 60; diff --git a/modules/audiencerunBidAdapter.js b/modules/audiencerunBidAdapter.js index 9beb20d4f77..92a4343b3ed 100644 --- a/modules/audiencerunBidAdapter.js +++ b/modules/audiencerunBidAdapter.js @@ -1,8 +1,7 @@ import { _each, deepAccess, - formatQS, - getBidIdParameter, + formatQS, getBidIdParameter, getValue, isArray, isFn, @@ -13,6 +12,15 @@ import {config} from '../src/config.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import {BANNER} from '../src/mediaTypes.js'; +/** + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid + * @typedef {import('../src/adapters/bidderFactory.js').ServerRequest} ServerRequest + * @typedef {import('../src/adapters/bidderFactory.js').ServerResponse} ServerResponse + * @typedef {import('../src/adapters/bidderFactory.js').SyncOptions} SyncOptions + * @typedef {import('../src/adapters/bidderFactory.js').UserSync} UserSync + */ + const BIDDER_CODE = 'audiencerun'; const BASE_URL = 'https://d.audiencerun.com'; const AUCTION_URL = `${BASE_URL}/prebid`; diff --git a/modules/automatadAnalyticsAdapter.js b/modules/automatadAnalyticsAdapter.js new file mode 100644 index 00000000000..7d7bd8cb34c --- /dev/null +++ b/modules/automatadAnalyticsAdapter.js @@ -0,0 +1,325 @@ +import { + logError, + logInfo, + logMessage +} from '../src/utils.js'; + +import CONSTANTS from '../src/constants.json'; +import adapter from '../libraries/analyticsAdapter/AnalyticsAdapter.js'; +import adapterManager from '../src/adapterManager.js'; +import { config } from '../src/config.js' + +/** Prebid Event Handlers */ + +const ADAPTER_CODE = 'automatadAnalytics' +const trialCountMilsMapping = [1500, 3000, 5000, 10000]; + +var isLoggingEnabled; var queuePointer = 0; var retryCount = 0; var timer = null; var __atmtdAnalyticsQueue = []; + +const prettyLog = (level, text, isGroup = false, cb = () => {}) => { + if (self.isLoggingEnabled === undefined) { + if (window.localStorage.getItem('__aggLoggingEnabled')) { + self.isLoggingEnabled = true + } else { + const queryParams = new URLSearchParams(new URL(window.location.href).search) + self.isLoggingEnabled = queryParams.has('aggLoggingEnabled') + } + } + + if (self.isLoggingEnabled) { + if (isGroup) { + logInfo(`ATD Analytics Adapter: ${level.toUpperCase()}: ${text} --- Group Start ---`) + try { + cb(); + } catch (error) { + logError(`ATD Analytics Adapter: ERROR: ${'Error during cb function in prettyLog'}`) + } + logInfo(`ATD Analytics Adapter: ${level.toUpperCase()}: ${text} --- Group End ---`) + } else { + logInfo(`ATD Analytics Adapter: ${level.toUpperCase()}: ${text}`) + } + } +} + +const processEvents = () => { + if (self.retryCount === trialCountMilsMapping.length) { + self.prettyLog('error', `Aggregator still hasn't loaded. Processing que stopped`, trialCountMilsMapping, self.retryCount) + return; + } + + self.prettyLog('status', `Que has been inactive for a while. Adapter starting to process que now... Trial Count = ${self.retryCount + 1}`) + + let shouldTryAgain = false + + while (self.queuePointer < self.__atmtdAnalyticsQueue.length) { + const eventType = self.__atmtdAnalyticsQueue[self.queuePointer][0] + const args = self.__atmtdAnalyticsQueue[self.queuePointer][1] + + try { + switch (eventType) { + case CONSTANTS.EVENTS.AUCTION_INIT: + if (window.atmtdAnalytics && window.atmtdAnalytics.auctionInitHandler) { + window.atmtdAnalytics.auctionInitHandler(args); + } else { + shouldTryAgain = true + } + break; + case CONSTANTS.EVENTS.BID_REQUESTED: + if (window.atmtdAnalytics && window.atmtdAnalytics.bidRequestedHandler) { + window.atmtdAnalytics.bidRequestedHandler(args); + } + break; + case CONSTANTS.EVENTS.BID_RESPONSE: + if (window.atmtdAnalytics && window.atmtdAnalytics.bidResponseHandler) { + window.atmtdAnalytics.bidResponseHandler(args); + } + break; + case CONSTANTS.EVENTS.BID_REJECTED: + if (window.atmtdAnalytics && window.atmtdAnalytics.bidRejectedHandler) { + window.atmtdAnalytics.bidRejectedHandler(args); + } + break; + case CONSTANTS.EVENTS.BIDDER_DONE: + if (window.atmtdAnalytics && window.atmtdAnalytics.bidderDoneHandler) { + window.atmtdAnalytics.bidderDoneHandler(args); + } + break; + case CONSTANTS.EVENTS.BID_WON: + if (window.atmtdAnalytics && window.atmtdAnalytics.bidWonHandler) { + window.atmtdAnalytics.bidWonHandler(args); + } + break; + case CONSTANTS.EVENTS.NO_BID: + if (window.atmtdAnalytics && window.atmtdAnalytics.noBidHandler) { + window.atmtdAnalytics.noBidHandler(args); + } + break; + case CONSTANTS.EVENTS.BID_TIMEOUT: + if (window.atmtdAnalytics && window.atmtdAnalytics.bidderTimeoutHandler) { + window.atmtdAnalytics.bidderTimeoutHandler(args); + } + break; + case CONSTANTS.EVENTS.AUCTION_DEBUG: + if (window.atmtdAnalytics && window.atmtdAnalytics.auctionDebugHandler) { + window.atmtdAnalytics.auctionDebugHandler(args); + } + break; + case 'slotRenderEnded': + if (window.atmtdAnalytics && window.atmtdAnalytics.slotRenderEndedGPTHandler) { + window.atmtdAnalytics.slotRenderEndedGPTHandler(args); + } else { + shouldTryAgain = true + } + break; + case 'impressionViewable': + if (window.atmtdAnalytics && window.atmtdAnalytics.impressionViewableHandler) { + window.atmtdAnalytics.impressionViewableHandler(args); + } else { + shouldTryAgain = true + } + break; + } + + if (shouldTryAgain) break; + } catch (error) { + self.prettyLog('error', `Unhandled Error while processing ${eventType} of ${self.queuePointer}th index in the que. Will not be retrying this raw event ...`, true, () => { + logError(`The error is `, error) + }) + } + + self.queuePointer = self.queuePointer + 1 + } + + if (shouldTryAgain) { + if (trialCountMilsMapping[self.retryCount]) self.prettyLog('warn', `Adapter failed to process event as aggregator has not loaded. Retrying in ${trialCountMilsMapping[self.retryCount]}ms ...`); + setTimeout(self.processEvents, trialCountMilsMapping[self.retryCount]) + self.retryCount = self.retryCount + 1 + } +} + +const addGPTHandlers = () => { + const googletag = window.googletag || {} + googletag.cmd = googletag.cmd || [] + googletag.cmd.push(() => { + googletag.pubads().addEventListener('slotRenderEnded', (event) => { + if (window.atmtdAnalytics && window.atmtdAnalytics.slotRenderEndedGPTHandler) { + if (window.__atmtdAggregatorFirstAuctionInitialized === true) { + window.atmtdAnalytics.slotRenderEndedGPTHandler(event) + return; + } + } + self.__atmtdAnalyticsQueue.push(['slotRenderEnded', event]) + self.prettyLog(`warn`, `Aggregator not initialised at auctionInit, exiting slotRenderEnded handler and pushing to que instead`) + }) + + googletag.pubads().addEventListener('impressionViewable', (event) => { + if (window.atmtdAnalytics && window.atmtdAnalytics.impressionViewableHandler) { + if (window.__atmtdAggregatorFirstAuctionInitialized === true) { + window.atmtdAnalytics.impressionViewableHandler(event) + return; + } + } + self.__atmtdAnalyticsQueue.push(['impressionViewable', event]) + self.prettyLog(`warn`, `Aggregator not initialised at auctionInit, exiting impressionViewable handler and pushing to que instead`) + }) + }) +} + +const initializeQueue = () => { + self.__atmtdAnalyticsQueue.push = (args) => { + Array.prototype.push.apply(self.__atmtdAnalyticsQueue, [args]); + if (timer) { + clearTimeout(timer); + timer = null; + } + + if (args[0] === CONSTANTS.EVENTS.AUCTION_INIT) { + const timeout = parseInt(config.getConfig('bidderTimeout')) + 1500 + timer = setTimeout(() => { + self.processEvents() + }, timeout); + } else { + timer = setTimeout(() => { + self.processEvents() + }, 1500); + } + }; +} + +// ANALYTICS ADAPTER + +let baseAdapter = adapter({analyticsType: 'bundle'}); +let atmtdAdapter = Object.assign({}, baseAdapter, { + + disableAnalytics() { + baseAdapter.disableAnalytics.apply(this, arguments); + }, + + track({eventType, args}) { + switch (eventType) { + case CONSTANTS.EVENTS.AUCTION_INIT: + if (window.atmtdAnalytics && window.atmtdAnalytics.auctionInitHandler) { + self.prettyLog('status', 'Aggregator loaded, initialising auction through handlers'); + window.atmtdAnalytics.auctionInitHandler(args); + } else { + self.prettyLog('warn', 'Aggregator not loaded, initialising auction through que ...'); + self.__atmtdAnalyticsQueue.push([eventType, args]) + } + break; + case CONSTANTS.EVENTS.BID_REQUESTED: + if (window.atmtdAnalytics && window.atmtdAnalytics.bidRequestedHandler) { + window.atmtdAnalytics.bidRequestedHandler(args); + } else { + self.prettyLog('warn', `Aggregator not loaded, pushing ${eventType} to que instead ...`); + self.__atmtdAnalyticsQueue.push([eventType, args]) + } + break; + case CONSTANTS.EVENTS.BID_REJECTED: + if (window.atmtdAnalytics && window.atmtdAnalytics.bidRejectedHandler) { + window.atmtdAnalytics.bidRejectedHandler(args); + } else { + self.prettyLog('warn', `Aggregator not loaded, pushing ${eventType} to que instead ...`); + self.__atmtdAnalyticsQueue.push([eventType, args]) + } + break; + case CONSTANTS.EVENTS.BID_RESPONSE: + if (window.atmtdAnalytics && window.atmtdAnalytics.bidResponseHandler) { + window.atmtdAnalytics.bidResponseHandler(args); + } else { + self.prettyLog('warn', `Aggregator not loaded, pushing ${eventType} to que instead ...`); + self.__atmtdAnalyticsQueue.push([eventType, args]) + } + break; + case CONSTANTS.EVENTS.BIDDER_DONE: + if (window.atmtdAnalytics && window.atmtdAnalytics.bidderDoneHandler) { + window.atmtdAnalytics.bidderDoneHandler(args); + } else { + self.prettyLog('warn', `Aggregator not loaded, pushing ${eventType} to que instead ...`); + self.__atmtdAnalyticsQueue.push([eventType, args]) + } + break; + case CONSTANTS.EVENTS.BID_WON: + if (window.atmtdAnalytics && window.atmtdAnalytics.bidWonHandler) { + window.atmtdAnalytics.bidWonHandler(args); + } else { + self.prettyLog('warn', `Aggregator not loaded, pushing ${eventType} to que instead ...`); + self.__atmtdAnalyticsQueue.push([eventType, args]) + } + break; + case CONSTANTS.EVENTS.NO_BID: + if (window.atmtdAnalytics && window.atmtdAnalytics.noBidHandler) { + window.atmtdAnalytics.noBidHandler(args); + } else { + self.prettyLog('warn', `Aggregator not loaded, pushing ${eventType} to que instead ...`); + self.__atmtdAnalyticsQueue.push([eventType, args]) + } + break; + case CONSTANTS.EVENTS.AUCTION_DEBUG: + if (window.atmtdAnalytics && window.atmtdAnalytics.auctionDebugHandler) { + window.atmtdAnalytics.auctionDebugHandler(args); + } else { + self.prettyLog('warn', `Aggregator not loaded, pushing ${eventType} to que instead ...`); + self.__atmtdAnalyticsQueue.push([eventType, args]) + } + break; + case CONSTANTS.EVENTS.BID_TIMEOUT: + if (window.atmtdAnalytics && window.atmtdAnalytics.bidderTimeoutHandler) { + window.atmtdAnalytics.bidderTimeoutHandler(args); + } else { + self.prettyLog('warn', `Aggregator not loaded, pushing ${eventType} to que instead ...`); + self.__atmtdAnalyticsQueue.push([eventType, args]) + } + break; + } + } +}); + +atmtdAdapter.originEnableAnalytics = atmtdAdapter.enableAnalytics + +atmtdAdapter.enableAnalytics = function (configuration) { + if ((configuration === undefined && typeof configuration !== 'object') || configuration.options === undefined) { + logError('A valid configuration must be passed to the Atmtd Analytics Adapter.'); + return; + } + + const conf = configuration.options + + if (conf === undefined || typeof conf !== 'object' || conf.siteID === undefined || conf.publisherID === undefined) { + logError('A valid publisher ID and siteID must be passed to the Atmtd Analytics Adapter.'); + return; + } + + self.initializeQueue() + self.addGPTHandlers() + + window.__atmtdSDKConfig = { + publisherID: conf.publisherID, + siteID: conf.siteID, + collectDebugMessages: conf.logDebug ? conf.logDebug : false + } + + logMessage(`Automatad Analytics Adapter enabled with sdk config`, window.__atmtdSDKConfig) + + // eslint-disable-next-line + atmtdAdapter.originEnableAnalytics(configuration) +}; + +/// /////////// ADAPTER REGISTRATION ////////////// + +adapterManager.registerAnalyticsAdapter({ + adapter: atmtdAdapter, + code: ADAPTER_CODE +}); + +export var self = { + __atmtdAnalyticsQueue, + processEvents, + initializeQueue, + addGPTHandlers, + prettyLog, + queuePointer, + retryCount, + isLoggingEnabled +} + +export default atmtdAdapter; diff --git a/modules/automatadAnalyticsAdapter.md b/modules/automatadAnalyticsAdapter.md new file mode 100644 index 00000000000..2be1af87f20 --- /dev/null +++ b/modules/automatadAnalyticsAdapter.md @@ -0,0 +1,23 @@ + +# Overview + +Module Name: Automatad Analytics Adapter +Module Type: Analytics Adapter +Maintainer: tech@automatad.com + +# Description + +Analytics adapter for automatad.com. Contact tech@automatad.com for information. + +# Test Parameters + +``` +{ + provider: 'automatadAnalytics', + options: { + publisherID: 'N8vZLx', + siteID: 'PXfvBq' + } +} + +``` \ No newline at end of file diff --git a/modules/axisBidAdapter.js b/modules/axisBidAdapter.js new file mode 100644 index 00000000000..8d7f2dd04fd --- /dev/null +++ b/modules/axisBidAdapter.js @@ -0,0 +1,210 @@ +import { isFn, deepAccess, logMessage, logError } from '../src/utils.js'; +import { convertOrtbRequestToProprietaryNative } from '../src/native.js'; + +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { BANNER, NATIVE, VIDEO } from '../src/mediaTypes.js'; +import { config } from '../src/config.js'; + +const BIDDER_CODE = 'axis'; +const AD_URL = 'https://prebid.axis-marketplace.com/pbjs'; +const SYNC_URL = 'https://cs.axis-marketplace.com'; + +function isBidResponseValid(bid) { + if (!bid.requestId || !bid.cpm || !bid.creativeId || !bid.ttl || !bid.currency) { + return false; + } + + switch (bid.mediaType) { + case BANNER: + return Boolean(bid.width && bid.height && bid.ad); + case VIDEO: + return Boolean(bid.vastUrl || bid.vastXml); + case NATIVE: + return Boolean(bid.native && bid.native.impressionTrackers && bid.native.impressionTrackers.length); + default: + return false; + } +} + +function getPlacementReqData(bid) { + const { params, bidId, mediaTypes } = bid; + const schain = bid.schain || {}; + const { integration, token } = params; + const bidfloor = getBidFloor(bid); + + const placement = { + integration, + token, + bidId, + schain, + bidfloor + }; + + if (mediaTypes && mediaTypes[BANNER]) { + placement.adFormat = BANNER; + placement.pos = mediaTypes[BANNER].pos; + placement.sizes = mediaTypes[BANNER].sizes; + } else if (mediaTypes && mediaTypes[VIDEO]) { + placement.adFormat = VIDEO; + placement.pos = mediaTypes[VIDEO].pos; + placement.playerSize = mediaTypes[VIDEO].playerSize; + placement.minduration = mediaTypes[VIDEO].minduration; + placement.maxduration = mediaTypes[VIDEO].maxduration; + placement.mimes = mediaTypes[VIDEO].mimes; + placement.protocols = mediaTypes[VIDEO].protocols; + placement.startdelay = mediaTypes[VIDEO].startdelay; + placement.placement = mediaTypes[VIDEO].placement; + placement.skip = mediaTypes[VIDEO].skip; + placement.skipafter = mediaTypes[VIDEO].skipafter; + placement.minbitrate = mediaTypes[VIDEO].minbitrate; + placement.maxbitrate = mediaTypes[VIDEO].maxbitrate; + placement.delivery = mediaTypes[VIDEO].delivery; + placement.playbackmethod = mediaTypes[VIDEO].playbackmethod; + placement.api = mediaTypes[VIDEO].api; + placement.linearity = mediaTypes[VIDEO].linearity; + placement.context = mediaTypes[VIDEO].context; + } else if (mediaTypes && mediaTypes[NATIVE]) { + placement.native = mediaTypes[NATIVE]; + placement.adFormat = NATIVE; + } + + return placement; +} + +function getBidFloor(bid) { + if (!isFn(bid.getFloor)) { + return deepAccess(bid, 'params.bidfloor', 0); + } + + try { + const bidFloor = bid.getFloor({ + currency: 'USD', + mediaType: '*', + size: '*', + }); + return bidFloor.floor; + } catch (e) { + logError(e); + return 0; + } +} + +export const spec = { + code: BIDDER_CODE, + supportedMediaTypes: [BANNER, VIDEO, NATIVE], + + isBidRequestValid: (bid = {}) => { + const { params, bidId, mediaTypes } = bid; + let valid = Boolean(bidId && params && params.integration && params.token); + + if (mediaTypes && mediaTypes[BANNER]) { + valid = valid && Boolean(mediaTypes[BANNER] && mediaTypes[BANNER].sizes); + } else if (mediaTypes && mediaTypes[VIDEO]) { + valid = valid && Boolean(mediaTypes[VIDEO] && mediaTypes[VIDEO].playerSize); + } else if (mediaTypes && mediaTypes[NATIVE]) { + valid = valid && Boolean(mediaTypes[NATIVE]); + } else { + valid = false; + } + return valid; + }, + + buildRequests: (validBidRequests = [], bidderRequest = {}) => { + // convert Native ORTB definition to old-style prebid native definition + validBidRequests = convertOrtbRequestToProprietaryNative(validBidRequests); + + let deviceWidth = 0; + let deviceHeight = 0; + + let winLocation; + try { + const winTop = window.top; + deviceWidth = winTop.screen.width; + deviceHeight = winTop.screen.height; + winLocation = winTop.location; + } catch (e) { + logMessage(e); + winLocation = window.location; + } + + const refferUrl = bidderRequest.refererInfo && bidderRequest.refererInfo.page; + let refferLocation; + try { + refferLocation = refferUrl && new URL(refferUrl); + } catch (e) { + logMessage(e); + } + + let location = refferLocation || winLocation; + const language = (navigator && navigator.language) ? navigator.language.split('-')[0] : ''; + const host = location.host; + const page = location.pathname; + const secure = location.protocol === 'https:' ? 1 : 0; + const placements = []; + const request = { + deviceWidth, + deviceHeight, + language, + secure, + host, + page, + placements, + iabCat: deepAccess(bidderRequest, 'ortb2.site.cat'), + coppa: deepAccess(bidderRequest, 'ortb2.regs.coppa') ? 1 : 0, + ccpa: bidderRequest.uspConsent || undefined, + gdpr: bidderRequest.gdprConsent || undefined, + tmax: bidderRequest.timeout || 3000, + }; + + const len = validBidRequests.length; + for (let i = 0; i < len; i++) { + const bid = validBidRequests[i]; + placements.push(getPlacementReqData(bid)); + } + + return { + method: 'POST', + url: AD_URL, + data: request + }; + }, + + interpretResponse: (serverResponse) => { + let response = []; + for (let i = 0; i < serverResponse.body.length; i++) { + let resItem = serverResponse.body[i]; + if (isBidResponseValid(resItem)) { + const advertiserDomains = resItem.adomain && resItem.adomain.length ? resItem.adomain : []; + resItem.meta = { ...resItem.meta, advertiserDomains }; + + response.push(resItem); + } + } + return response; + }, + + getUserSyncs: (syncOptions, serverResponses, gdprConsent, uspConsent) => { + let syncType = syncOptions.iframeEnabled ? 'iframe' : 'image'; + let syncUrl = SYNC_URL + `/${syncType}?pbjs=1`; + if (gdprConsent && gdprConsent.consentString) { + if (typeof gdprConsent.gdprApplies === 'boolean') { + syncUrl += `&gdpr=${Number(gdprConsent.gdprApplies)}&gdpr_consent=${gdprConsent.consentString}`; + } else { + syncUrl += `&gdpr=0&gdpr_consent=${gdprConsent.consentString}`; + } + } + if (uspConsent && uspConsent.consentString) { + syncUrl += `&ccpa=${uspConsent.consentString}`; + } + + const coppa = config.getConfig('coppa') ? 1 : 0; + syncUrl += `&coppa=${coppa}`; + + return [{ + type: syncType, + url: syncUrl + }]; + } +}; + +registerBidder(spec); diff --git a/modules/axisBidAdapter.md b/modules/axisBidAdapter.md new file mode 100644 index 00000000000..d1625a56176 --- /dev/null +++ b/modules/axisBidAdapter.md @@ -0,0 +1,83 @@ +# Overview + +``` +Module Name: Axis Bidder Adapter +Module Type: Axis Bidder Adapter +Maintainer: help@axis-marketplace.com +``` + +# Description + +Connects to Axis exchange for bids. +Axis bid adapter supports Banner, Video (instream and outstream) and Native. + +# Test Parameters +``` + var adUnits = [ + // Will return static test banner + { + code: 'adunit1', + mediaTypes: { + banner: { + sizes: [ [300, 250], [320, 50] ], + pos: 1 + } + }, + bids: [ + { + bidder: 'axis', + params: { + integration: '000000', + token: '000000' + } + } + ] + }, + { + code: 'addunit2', + mediaTypes: { + video: { + playerSize: [ [640, 480] ], + minduration: 5, + maxduration: 60, + pos: 1 + } + }, + bids: [ + { + bidder: 'axis', + params: { + integration: '000000', + token: '000000' + } + } + ] + }, + { + code: 'addunit3', + mediaTypes: { + native: { + title: { + required: true + }, + body: { + required: true + }, + icon: { + required: true, + size: [64, 64] + } + } + }, + bids: [ + { + bidder: 'axis', + params: { + integration: '000000', + token: '000000' + } + } + ] + } + ]; +``` diff --git a/modules/beachfrontBidAdapter.js b/modules/beachfrontBidAdapter.js index 37de8e637a9..658fc30b43b 100644 --- a/modules/beachfrontBidAdapter.js +++ b/modules/beachfrontBidAdapter.js @@ -7,14 +7,15 @@ import { isFn, logWarn, parseSizesInput, - parseUrl + parseUrl, + formatQS } from '../src/utils.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import {Renderer} from '../src/Renderer.js'; import {BANNER, VIDEO} from '../src/mediaTypes.js'; import {find, includes} from '../src/polyfill.js'; -const ADAPTER_VERSION = '1.19'; +const ADAPTER_VERSION = '1.20'; const ADAPTER_NAME = 'BFIO_PREBID'; const OUTSTREAM = 'outstream'; const CURRENCY = 'USD'; @@ -22,6 +23,8 @@ const CURRENCY = 'USD'; export const VIDEO_ENDPOINT = 'https://reachms.bfmio.com/bid.json?exchange_id='; export const BANNER_ENDPOINT = 'https://display.bfmio.com/prebid_display'; export const OUTSTREAM_SRC = 'https://player-cdn.beachfrontmedia.com/playerapi/loader/outstream.js'; +export const SYNC_IFRAME_ENDPOINT = 'https://sync.bfmio.com/sync_iframe'; +export const SYNC_IMAGE_ENDPOINT = 'https://sync.bfmio.com/syncb'; export const VIDEO_TARGETING = ['mimes', 'playbackmethod', 'maxduration', 'placement', 'skip', 'skipmin', 'skipafter']; export const DEFAULT_MIMES = ['video/mp4', 'application/javascript']; @@ -152,11 +155,22 @@ export const spec = { } }, - getUserSyncs(syncOptions, serverResponses = [], gdprConsent = {}, uspConsent = '') { - let syncs = []; + getUserSyncs(syncOptions, serverResponses = [], gdprConsent = {}, uspConsent = '', gppConsent = {}) { let { gdprApplies, consentString = '' } = gdprConsent; + let { gppString = '', applicableSections = [] } = gppConsent; let bannerResponse = find(serverResponses, (res) => isArray(res.body)); + let syncs = []; + let params = { + id: appId, + gdpr: gdprApplies ? 1 : 0, + gc: consentString, + gce: 1, + us_privacy: uspConsent, + gpp: gppString, + gpp_sid: Array.isArray(applicableSections) ? applicableSections.join(',') : '' + }; + if (bannerResponse) { if (syncOptions.iframeEnabled) { bannerResponse.body @@ -171,12 +185,12 @@ export const spec = { } else if (syncOptions.iframeEnabled) { syncs.push({ type: 'iframe', - url: `https://sync.bfmio.com/sync_iframe?ifg=1&id=${appId}&gdpr=${gdprApplies ? 1 : 0}&gc=${consentString}&gce=1&us_privacy=${uspConsent}` + url: `${SYNC_IFRAME_ENDPOINT}?ifg=1&${formatQS(params)}` }); } else if (syncOptions.pixelEnabled) { syncs.push({ type: 'image', - url: `https://sync.bfmio.com/syncb?pid=144&id=${appId}&gdpr=${gdprApplies ? 1 : 0}&gc=${consentString}&gce=1&us_privacy=${uspConsent}` + url: `${SYNC_IMAGE_ENDPOINT}?pid=144&${formatQS(params)}` }); } @@ -404,6 +418,12 @@ function createVideoRequestData(bid, bidderRequest) { deepSetValue(payload, 'user.ext.consent', consentString); } + if (bidderRequest && bidderRequest.gppConsent) { + let { gppString, applicableSections } = bidderRequest.gppConsent; + deepSetValue(payload, 'regs.gpp', gppString); + deepSetValue(payload, 'regs.gpp_sid', applicableSections); + } + if (bid.schain) { deepSetValue(payload, 'source.ext.schain', bid.schain); } @@ -459,6 +479,12 @@ function createBannerRequestData(bids, bidderRequest) { payload.gdprConsent = consentString; } + if (bidderRequest && bidderRequest.gppConsent) { + let { gppString, applicableSections } = bidderRequest.gppConsent; + payload.gpp = gppString; + payload.gppSid = applicableSections; + } + if (bids[0] && bids[0].schain) { payload.schain = bids[0].schain; } diff --git a/modules/beopBidAdapter.js b/modules/beopBidAdapter.js index 6348854ec2a..ad52af824ee 100644 --- a/modules/beopBidAdapter.js +++ b/modules/beopBidAdapter.js @@ -1,7 +1,6 @@ import { buildUrl, - deepAccess, - getBidIdParameter, + deepAccess, getBidIdParameter, getValue, isArray, logInfo, @@ -24,11 +23,11 @@ export const spec = { gvlid: TCF_VENDOR_ID, aliases: ['bp'], /** - * Test if the bid request is valid. - * - * @param {bid} : The Bid params - * @return boolean true if the bid request is valid (aka contains a valid accountId or networkId and is open for BANNER), false otherwise. - */ + * Test if the bid request is valid. + * + * @param {bid} : The Bid params + * @return boolean true if the bid request is valid (aka contains a valid accountId or networkId and is open for BANNER), false otherwise. + */ isBidRequestValid: function(bid) { const id = bid.params.accountId || bid.params.networkId; if (id === null || typeof id === 'undefined') { @@ -40,16 +39,18 @@ export const spec = { return bid.mediaTypes.banner !== null && typeof bid.mediaTypes.banner !== 'undefined'; }, /** - * Create a BeOp server request from a list of BidRequest - * - * @param {validBidRequests[], ...} : The array of validated bidRequests - * @param {... , bidderRequest} : Common params for each bidRequests - * @return ServerRequest Info describing the request to the BeOp's server - */ + * Create a BeOp server request from a list of BidRequest + * + * @param {validBidRequests[], ...} : The array of validated bidRequests + * @param {... , bidderRequest} : Common params for each bidRequests + * @return ServerRequest Info describing the request to the BeOp's server + */ buildRequests: function(validBidRequests, bidderRequest) { const slots = validBidRequests.map(beOpRequestSlotsMaker); - const firstPartyData = bidderRequest.ortb2; - const psegs = (firstPartyData && firstPartyData.user && firstPartyData.user.ext && firstPartyData.user.ext.data) ? firstPartyData.user.ext.data.permutive : undefined; + const firstPartyData = bidderRequest.ortb2 || {}; + const psegs = firstPartyData.user?.ext?.permutive || firstPartyData.user?.ext?.data?.permutive || []; + const userBpSegs = firstPartyData.user?.ext?.bpsegs || firstPartyData.user?.ext?.data?.bpsegs || []; + const siteBpSegs = firstPartyData.site?.ext?.bpsegs || firstPartyData.site?.ext?.data?.bpsegs || []; const pageUrl = getPageUrl(bidderRequest.refererInfo, window); const gdpr = bidderRequest.gdprConsent; const firstSlot = slots[0]; @@ -61,6 +62,8 @@ export const spec = { nid: firstSlot.nid, nptnid: firstSlot.nptnid, pid: firstSlot.pid, + psegs: psegs, + bpsegs: (userBpSegs.concat(siteBpSegs)).map(item => item.toString()), url: pageUrl, lang: (window.navigator.language || window.navigator.languages[0]), kwds: keywords, @@ -71,10 +74,6 @@ export const spec = { tc_string: (gdpr && gdpr.gdprApplies) ? gdpr.consentString : null, }; - if (psegs) { - Object.assign(payloadObject, {psegs: psegs}); - } - const payloadString = JSON.stringify(payloadObject); return { method: 'POST', @@ -129,8 +128,6 @@ function buildTrackingParams(data, info, value) { nptnid: params.networkPartnerId, bid: data.bidId || data.requestId, sl_n: data.adUnitCode, - // TODO: fix auctionId leak: https://github.com/prebid/Prebid.js/issues/9781 - aid: data.auctionId, se_ca: 'bid', se_ac: info, se_va: value, @@ -158,8 +155,6 @@ function beOpRequestSlotsMaker(bid) { bid: getBidIdParameter('bidId', bid), brid: getBidIdParameter('bidderRequestId', bid), name: getBidIdParameter('adUnitCode', bid), - // TODO: fix auctionId leak: https://github.com/prebid/Prebid.js/issues/9781 - aid: getBidIdParameter('auctionId', bid), tid: bid.ortb2Imp?.ext?.tid || '', brc: getBidIdParameter('bidRequestsCount', bid), bdrc: getBidIdParameter('bidderRequestCount', bid), diff --git a/modules/beopBidAdapter.md b/modules/beopBidAdapter.md index c0e88cb1ceb..53d2542b69c 100644 --- a/modules/beopBidAdapter.md +++ b/modules/beopBidAdapter.md @@ -9,13 +9,14 @@ Module that connects to BeOp's demand sources # Test Parameters + ``` var adUnits = [ { code: 'in-article', mediaTypes: { banner: { - sizes: [[1,1]], + sizes: [[1,1]], } }, bids: [ @@ -31,3 +32,36 @@ Module that connects to BeOp's demand sources ]; ``` +# Custom Bidder data + +If you want to pass your first party data to BeOp, you can set your bidder `config.ortb2` object with + +```json +{ + "site": { + "ext": { + "bpsegs": ["Your", 1, "ST", "party", "data"], + "data": { + "bpsegs": ["Your", 1, "ST", "party", "data"] + } + } + }, + "user": { + "ext": { + "bpsegs": ["Your", 1, "ST", "party", "data"], + "data": { + "bpsegs": ["Your", 1, "ST", "party", "data"] + } + } + } +} +``` + +You can choose the location between: + +- `site.ext` +- `site.ext.data` +- `user.ext` +- `user.ext.data` + +and our BidAdapter will be able to find them diff --git a/modules/betweenBidAdapter.js b/modules/betweenBidAdapter.js index d615e433cc0..4a953875d99 100644 --- a/modules/betweenBidAdapter.js +++ b/modules/betweenBidAdapter.js @@ -1,6 +1,15 @@ import {registerBidder} from '../src/adapters/bidderFactory.js'; -import {getAdUnitSizes, parseSizesInput} from '../src/utils.js'; +import {parseSizesInput} from '../src/utils.js'; import {includes} from '../src/polyfill.js'; +import {getAdUnitSizes} from '../libraries/sizeUtils/sizeUtils.js'; + +/** + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid + * @typedef {import('../src/adapters/bidderFactory.js').ServerResponse} ServerResponse + * @typedef {import('../src/adapters/bidderFactory.js').SyncOptions} SyncOptions + * @typedef {import('../src/adapters/bidderFactory.js').UserSync} UserSync + */ const BIDDER_CODE = 'between'; let ENDPOINT = 'https://ads.betweendigital.com/adjson?t=prebid'; diff --git a/modules/bidViewability.js b/modules/bidViewability.js index a5cab99b1a7..be18095e369 100644 --- a/modules/bidViewability.js +++ b/modules/bidViewability.js @@ -67,6 +67,8 @@ export let logWinningBidNotFound = (slot) => { export let impressionViewableHandler = (globalModuleConfig, slot, event) => { let respectiveBid = getMatchingWinningBidForGPTSlot(globalModuleConfig, slot); + let respectiveDeferredAdUnit = getGlobal().adUnits.find(adUnit => adUnit.deferBilling && respectiveBid.adUnitCode === adUnit.code); + if (respectiveBid === null) { logWinningBidNotFound(slot); } else { @@ -74,6 +76,11 @@ export let impressionViewableHandler = (globalModuleConfig, slot, event) => { fireViewabilityPixels(globalModuleConfig, respectiveBid); // trigger respective bidder's onBidViewable handler adapterManager.callBidViewableBidder(respectiveBid.adapterCode || respectiveBid.bidder, respectiveBid); + + if (respectiveDeferredAdUnit) { + adapterManager.callBidBillableBidder(respectiveBid); + } + // emit the BID_VIEWABLE event with bid details, this event can be consumed by bidders and analytics pixels events.emit(CONSTANTS.EVENTS.BID_VIEWABLE, respectiveBid); } diff --git a/modules/bidViewability.md b/modules/bidViewability.md index 78a1539fb1a..922a4a9def4 100644 --- a/modules/bidViewability.md +++ b/modules/bidViewability.md @@ -2,19 +2,20 @@ Module Name: bidViewability -Purpose: Track when a bid is viewable +Purpose: Track when a bid is viewable (and also ready for billing) Maintainer: harshad.mane@pubmatic.com # Description -- This module, when included, will trigger a BID_VIEWABLE event which can be consumed by Analytics adapters, bidders will need to implement `onBidViewable` method to capture this event -- Bidderes can check if this module is part of the final build and whether it is enabled or not by accessing ```pbjs.getConfig('bidViewability')``` +- This module, when included, will trigger a BID_VIEWABLE event which can be consumed by Analytics adapters, bidders will need to implement the `onBidViewable` method to capture this event +- Bidders can check if this module is part of the final build and whether it is enabled or not by accessing ```pbjs.getConfig('bidViewability')``` - GPT API is used to find when a bid is viewable, https://developers.google.com/publisher-tag/reference#googletag.events.impressionviewableevent . This event is fired when an impression becomes viewable, according to the Active View criteria. Refer: https://support.google.com/admanager/answer/4524488 -- The module does not work with adserver other than GAM with GPT integration +- This module does not work with any adserver's other than GAM with GPT integration - Logic used to find a matching pbjs-bid for a GPT slot is ``` (slot.getAdUnitPath() === bid.adUnitCode || slot.getSlotElementId() === bid.adUnitCode) ``` this logic can be changed by using param ```customMatchFunction``` -- When a rendered PBJS bid is viewable the module will trigger BID_VIEWABLE event, which can be consumed by bidders and analytics adapters -- For the viewable bid if ```bid.vurls type array``` param is and module config ``` firePixels: true ``` is set then the URLs mentioned in bid.vurls will be executed. Please note that GDPR and USP related parameters will be added to the given URLs +- When a rendered PBJS bid is viewable the module will trigger a BID_VIEWABLE event, which can be consumed by bidders and analytics adapters +- If the viewable bid contains a ```vurls``` param containing URL's and the Bid Viewability module is configured with ``` firePixels: true ``` then the URLs mentioned in bid.vurls will be called. Please note that GDPR and USP related parameters will be added to the given URLs +- This module is also compatible with Prebid core's billing deferral logic, this means that bids linked to an ad unit marked with `deferBilling: true` will trigger a bid adapter's `onBidBillable` function (if present) indicating an ad slot was viewed and also billing ready (if it were deferred). # Params - enabled [required] [type: boolean, default: false], when set to true, the module will emit BID_VIEWABLE when applicable @@ -44,6 +45,6 @@ Refer: https://support.google.com/admanager/answer/4524488 ``` # Please Note: -- Doesn't seems to work with Instream Video, https://docs.prebid.org/dev-docs/examples/instream-banner-mix.html as GPT's impressionViewable event is not triggered for instream-video-creative -- Works with Banner, Outsteam, Native creatives +- This module doesn't seem to work with Instream Video, https://docs.prebid.org/dev-docs/examples/instream-banner-mix.html as GPT's impressionViewable event is not triggered for instream-video-creative +- Works with Banner, Outsteam and Native creatives diff --git a/modules/biddoBidAdapter.js b/modules/biddoBidAdapter.js index 5512ca60f8e..cf39c572629 100644 --- a/modules/biddoBidAdapter.js +++ b/modules/biddoBidAdapter.js @@ -1,6 +1,11 @@ import {registerBidder} from '../src/adapters/bidderFactory.js'; import {BANNER} from '../src/mediaTypes.js'; +/** + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + * @typedef {import('../src/adapters/bidderFactory.js').ServerResponse} ServerResponse + */ + const BIDDER_CODE = 'biddo'; const ENDPOINT_URL = 'https://ad.adopx.net/delivery/impress'; diff --git a/modules/bidglassBidAdapter.js b/modules/bidglassBidAdapter.js index 3184372881b..6074d1a0e4c 100644 --- a/modules/bidglassBidAdapter.js +++ b/modules/bidglassBidAdapter.js @@ -1,7 +1,12 @@ -import { _each, isArray, getBidIdParameter, deepClone, getUniqueIdentifierStr } from '../src/utils.js'; -// import {config} from 'src/config.js'; +import {_each, isArray, deepClone, getUniqueIdentifierStr, getBidIdParameter} from '../src/utils.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; +/** + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid + * @typedef {import('../src/adapters/bidderFactory.js').ServerResponse} ServerResponse + */ + const BIDDER_CODE = 'bidglass'; export const spec = { diff --git a/modules/big-richmediaBidAdapter.js b/modules/big-richmediaBidAdapter.js index 8a03aac1ace..ecb1724c2a1 100644 --- a/modules/big-richmediaBidAdapter.js +++ b/modules/big-richmediaBidAdapter.js @@ -3,6 +3,11 @@ import {config} from '../src/config.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import {spec as baseAdapter} from './appnexusBidAdapter.js'; // eslint-disable-line prebid/validate-imports +/** + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid + */ + const BIDDER_CODE = 'big-richmedia'; const metadataByRequestId = {}; diff --git a/modules/bizzclickBidAdapter.js b/modules/bizzclickBidAdapter.js index dc7731231ab..d2eba3f0f81 100644 --- a/modules/bizzclickBidAdapter.js +++ b/modules/bizzclickBidAdapter.js @@ -1,322 +1,77 @@ -import {_map, deepAccess, deepSetValue, getDNT, logMessage, logWarn} from '../src/utils.js'; -import {registerBidder} from '../src/adapters/bidderFactory.js'; -import {BANNER, NATIVE, VIDEO} from '../src/mediaTypes.js'; -import {config} from '../src/config.js'; -import {convertOrtbRequestToProprietaryNative} from '../src/native.js'; +import { ortbConverter } from '../libraries/ortbConverter/converter.js'; +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { config } from '../src/config.js'; +import { BANNER, NATIVE, VIDEO } from '../src/mediaTypes.js'; const BIDDER_CODE = 'bizzclick'; -const ACCOUNTID_MACROS = '[account_id]'; -const URL_ENDPOINT = `https://us-e-node1.bizzclick.com/bid?rtb_seat_id=prebidjs&secret_key=${ACCOUNTID_MACROS}`; -const NATIVE_ASSET_IDS = { 0: 'title', 2: 'icon', 3: 'image', 5: 'sponsoredBy', 4: 'body', 1: 'cta' }; -const NATIVE_PARAMS = { - title: { - id: 0, - name: 'title' - }, - icon: { - id: 2, - type: 1, - name: 'img' - }, - image: { - id: 3, - type: 3, - name: 'img' +const SOURCE_ID_MACRO = '[sourceid]'; +const ACCOUNT_ID_MACRO = '[accountid]'; +const HOST_MACRO = '[host]'; +const URL = `https://${HOST_MACRO}.bizzclick.com/bid?rtb_seat_id=${SOURCE_ID_MACRO}&secret_key=${ACCOUNT_ID_MACRO}&integration_type=prebidjs`; +const DEFAULT_CURRENCY = 'USD'; +const DEFAULT_HOST = 'us-e-node1'; + +const converter = ortbConverter({ + context: { + netRevenue: true, + ttl: 20, }, - sponsoredBy: { - id: 5, - name: 'data', - type: 1 + imp(buildImp, bidRequest, context) { + const imp = buildImp(bidRequest, context); + if (!imp.bidfloor) imp.bidfloor = bidRequest.params.bidfloor || 0; + imp.ext = { + [BIDDER_CODE]: { + accountId: bidRequest.params.accountId, + sourceId: bidRequest.params.sourceId, + host: bidRequest.params.host || DEFAULT_HOST, + } + } + return imp; }, - body: { - id: 4, - name: 'data', - type: 2 + request(buildRequest, imps, bidderRequest, context) { + const request = buildRequest(imps, bidderRequest, context); + const bid = context.bidRequests[0]; + request.test = config.getConfig('debug') ? 1 : 0; + if (!request.cur) request.cur = [bid.params.currency || DEFAULT_CURRENCY]; + return request; }, - cta: { - id: 1, - type: 12, - name: 'data' + bidResponse(buildBidResponse, bid, context) { + const bidResponse = buildBidResponse(bid, context); + bidResponse.cur = bid.cur || DEFAULT_CURRENCY; + return bidResponse; } -}; -const NATIVE_VERSION = '1.2'; +}); + export const spec = { code: BIDDER_CODE, supportedMediaTypes: [BANNER, VIDEO, NATIVE], - /** - * Determines whether or not the given bid request is valid. - * - * @param {object} bid The bid to validate. - * @return boolean True if this is a valid bid, and false otherwise. - */ + isBidRequestValid: (bid) => { - return Boolean(bid.params.accountId) && Boolean(bid.params.placementId) + return Boolean(bid.params.sourceId) && Boolean(bid.params.accountId); }, - /** - * Make a server request from the list of BidRequests. - * - * @param {BidRequest[]} validBidRequests A non-empty list of valid bid requests that should be sent to the Server. - * @return ServerRequest Info describing the request to the server. - */ - buildRequests: (validBidRequests, bidderRequest) => { - // convert Native ORTB definition to old-style prebid native definition - validBidRequests = convertOrtbRequestToProprietaryNative(validBidRequests); - if (validBidRequests && validBidRequests.length === 0) return [] - let accuontId = validBidRequests[0].params.accountId; - const endpointURL = URL_ENDPOINT.replace(ACCOUNTID_MACROS, accuontId); - let winTop = window; - let location; - try { - location = new URL(bidderRequest.refererInfo.page) - winTop = window.top; - } catch (e) { - location = winTop.location; - logMessage(e); - }; - let bids = []; - for (let bidRequest of validBidRequests) { - let impObject = prepareImpObject(bidRequest); - let data = { - id: bidRequest.bidId, - test: config.getConfig('debug') ? 1 : 0, - at: 1, - cur: ['USD'], - device: { - w: winTop.screen.width, - h: winTop.screen.height, - dnt: getDNT() ? 1 : 0, - language: (navigator && navigator.language) ? navigator.language.indexOf('-') != -1 ? navigator.language.split('-')[0] : navigator.language : '', - }, - site: { - page: location.pathname, - host: location.host - }, - source: { - tid: bidRequest.ortb2Imp?.ext?.tid, - ext: { - schain: {} - } - }, - regs: { - coppa: config.getConfig('coppa') === true ? 1 : 0, - ext: {} - }, - user: { - ext: {} - }, - ext: { - ts: Date.now() - }, - tmax: bidRequest.timeout, - imp: [impObject], - }; - - let connection = navigator.connection || navigator.webkitConnection; - if (connection && connection.effectiveType) { - data.device.connectiontype = connection.effectiveType; - } - if (bidRequest) { - if (bidRequest.schain) { - deepSetValue(data, 'source.ext.schain', bidRequest.schain); - } - - if (bidRequest.gdprConsent && bidRequest.gdprConsent.gdprApplies) { - deepSetValue(data, 'regs.ext.gdpr', bidRequest.gdprConsent.gdprApplies ? 1 : 0); - deepSetValue(data, 'user.ext.consent', bidRequest.gdprConsent.consentString); - } - - if (bidRequest.uspConsent !== undefined) { - deepSetValue(data, 'regs.ext.us_privacy', bidRequest.uspConsent); - } - } - bids.push(data) - } + buildRequests: (validBidRequests, bidderRequest) => { + if (validBidRequests && validBidRequests.length === 0) return []; + const { sourceId, accountId } = validBidRequests[0].params; + const host = validBidRequests[0].params.host || 'USE'; + const endpointURL = URL.replace(HOST_MACRO, host || DEFAULT_HOST) + .replace(ACCOUNT_ID_MACRO, accountId) + .replace(SOURCE_ID_MACRO, sourceId); + const request = converter.toORTB({ bidRequests: validBidRequests, bidderRequest }); return { method: 'POST', url: endpointURL, - data: bids + data: request }; }, - /** - * Unpack the response from the server into a list of bids. - * - * @param {*} serverResponse A successful response from the server. - * @return {Bid[]} An array of bids which were nested inside the server. - */ - interpretResponse: (serverResponse) => { - if (!serverResponse || !serverResponse.body) return [] - let bizzclickResponse = serverResponse.body; - let bids = []; - for (let response of bizzclickResponse) { - let mediaType = response.seatbid[0].bid[0].ext && response.seatbid[0].bid[0].ext.mediaType ? response.seatbid[0].bid[0].ext.mediaType : BANNER; - let bid = { - requestId: response.id, - cpm: response.seatbid[0].bid[0].price, - width: response.seatbid[0].bid[0].w, - height: response.seatbid[0].bid[0].h, - ttl: response.ttl || 1200, - currency: response.cur || 'USD', - netRevenue: true, - creativeId: response.seatbid[0].bid[0].crid, - dealId: response.seatbid[0].bid[0].dealid, - mediaType: mediaType - }; - - bid.meta = {}; - if (response.seatbid[0].bid[0].adomain && response.seatbid[0].bid[0].adomain.length > 0) { - bid.meta.advertiserDomains = response.seatbid[0].bid[0].adomain; - } - switch (mediaType) { - case VIDEO: - bid.vastXml = response.seatbid[0].bid[0].adm - bid.vastUrl = response.seatbid[0].bid[0].ext.vastUrl - break - case NATIVE: - bid.native = parseNative(response.seatbid[0].bid[0].adm) - break - default: - bid.ad = response.seatbid[0].bid[0].adm - } - bids.push(bid); + interpretResponse: (response, request) => { + if (response?.body) { + const bids = converter.fromORTB({ response: response.body, request: request.data }).bids; + return bids; } - return bids; + return []; }, }; -/** - * Determine type of request - * - * @param bidRequest - * @param type - * @returns {boolean} - */ -const checkRequestType = (bidRequest, type) => { - return (typeof deepAccess(bidRequest, `mediaTypes.${type}`) !== 'undefined'); -} -const parseNative = admObject => { - const { assets, link, imptrackers, jstracker } = admObject.native; - const result = { - clickUrl: link.url, - clickTrackers: link.clicktrackers || undefined, - impressionTrackers: imptrackers || undefined, - javascriptTrackers: jstracker ? [ jstracker ] : undefined - }; - assets.forEach(asset => { - const kind = NATIVE_ASSET_IDS[asset.id]; - const content = kind && asset[NATIVE_PARAMS[kind].name]; - if (content) { - result[kind] = content.text || content.value || { url: content.url, width: content.w, height: content.h }; - } - }); - return result; -} -const prepareImpObject = (bidRequest) => { - let impObject = { - id: bidRequest.transactionId, - secure: 1, - ext: { - placementId: bidRequest.params.placementId - } - }; - if (checkRequestType(bidRequest, BANNER)) { - impObject.banner = addBannerParameters(bidRequest); - } - if (checkRequestType(bidRequest, VIDEO)) { - impObject.video = addVideoParameters(bidRequest); - } - if (checkRequestType(bidRequest, NATIVE)) { - impObject.native = { - ver: NATIVE_VERSION, - request: addNativeParameters(bidRequest) - }; - } - return impObject -}; -const addNativeParameters = bidRequest => { - let impObject = { - // TODO: top-level ID is not in ORTB native 1.2, is this intentional? - // (despite the name, this appears to be an ORTB native request - not an imp - object) - id: bidRequest.bidId, - ver: NATIVE_VERSION, - }; - const assets = _map(bidRequest.mediaTypes.native, (bidParams, key) => { - const props = NATIVE_PARAMS[key]; - const asset = { - required: bidParams.required & 1, - }; - if (props) { - asset.id = props.id; - let wmin, hmin; - let aRatios = bidParams.aspect_ratios; - if (aRatios && aRatios[0]) { - aRatios = aRatios[0]; - wmin = aRatios.min_width || 0; - hmin = aRatios.ratio_height * wmin / aRatios.ratio_width | 0; - } - if (bidParams.sizes) { - const sizes = flatten(bidParams.sizes); - wmin = sizes[0]; - hmin = sizes[1]; - } - asset[props.name] = {}; - if (bidParams.len) asset[props.name]['len'] = bidParams.len; - if (props.type) asset[props.name]['type'] = props.type; - if (wmin) asset[props.name]['wmin'] = wmin; - if (hmin) asset[props.name]['hmin'] = hmin; - return asset; - } - }).filter(Boolean); - impObject.assets = assets; - return impObject -} -const addBannerParameters = (bidRequest) => { - let bannerObject = {}; - const size = parseSizes(bidRequest, 'banner'); - bannerObject.w = size[0]; - bannerObject.h = size[1]; - return bannerObject; -}; -const parseSizes = (bid, mediaType) => { - let mediaTypes = bid.mediaTypes; - if (mediaType === 'video') { - let size = []; - if (mediaTypes.video && mediaTypes.video.w && mediaTypes.video.h) { - size = [ - mediaTypes.video.w, - mediaTypes.video.h - ]; - } else if (Array.isArray(deepAccess(bid, 'mediaTypes.video.playerSize')) && bid.mediaTypes.video.playerSize.length === 1) { - size = bid.mediaTypes.video.playerSize[0]; - } else if (Array.isArray(bid.sizes) && bid.sizes.length > 0 && Array.isArray(bid.sizes[0]) && bid.sizes[0].length > 1) { - size = bid.sizes[0]; - } - return size; - } - let sizes = []; - if (Array.isArray(mediaTypes.banner.sizes)) { - sizes = mediaTypes.banner.sizes[0]; - } else if (Array.isArray(bid.sizes) && bid.sizes.length > 0) { - sizes = bid.sizes - } else { - logWarn('no sizes are setup or found'); - } - return sizes -} -const addVideoParameters = (bidRequest) => { - let videoObj = {}; - let supportParamsList = ['mimes', 'minduration', 'maxduration', 'protocols', 'startdelay', 'placement', 'skip', 'skipafter', 'minbitrate', 'maxbitrate', 'delivery', 'playbackmethod', 'api', 'linearity'] - for (let param of supportParamsList) { - if (bidRequest.mediaTypes.video[param] !== undefined) { - videoObj[param] = bidRequest.mediaTypes.video[param]; - } - } - const size = parseSizes(bidRequest, 'video'); - videoObj.w = size[0]; - videoObj.h = size[1]; - return videoObj; -} -const flatten = arr => { - return [].concat(...arr); -} + registerBidder(spec); diff --git a/modules/bizzclickBidAdapter.md b/modules/bizzclickBidAdapter.md index 6fc1bebf546..ad342f34e07 100644 --- a/modules/bizzclickBidAdapter.md +++ b/modules/bizzclickBidAdapter.md @@ -11,94 +11,99 @@ Maintainer: support@bizzclick.com Module that connects to BizzClick SSP demand sources # Test Parameters -``` - var adUnits = [{ - code: 'placementId', - mediaTypes: { - banner: { - sizes: [[300, 250], [300,600]] - } - }, - bids: [{ - bidder: 'bizzclick', - params: { - placementId: 'hash', - accountId: 'accountId' - } - }] - }, - { - code: 'native_example', - // sizes: [[1, 1]], - mediaTypes: { - native: { - title: { - required: true, - len: 800 - }, - image: { - required: true, - len: 80 - }, - sponsoredBy: { - required: true - }, - clickUrl: { - required: true - }, - privacyLink: { - required: false - }, - body: { - required: true - }, - icon: { - required: true, - sizes: [50, 50] - } - } - }, - bids: [ { - bidder: 'bizzclick', - params: { - placementId: 'hash', - accountId: 'accountId' - } - }] - }, - { - code: 'video1', - sizes: [640,480], - mediaTypes: { video: { - minduration:0, - maxduration:999, - boxingallowed:1, - skip:0, - mimes:[ - 'application/javascript', - 'video/mp4' - ], - w:1920, - h:1080, - protocols:[ - 2 - ], - linearity:1, - api:[ - 1, - 2 - ] - } }, +```js +const adUnits = [ + { + code: "placementId", + mediaTypes: { + banner: { + sizes: [ + [300, 250], + [300, 600], + ], + }, + }, + bids: [ + { + bidder: "bizzclick", + params: { + placementId: "hash", + accountId: "accountId", + host: "host", + }, + }, + ], + }, + { + code: "native_example", + // sizes: [[1, 1]], + mediaTypes: { + native: { + title: { + required: true, + len: 800, + }, + image: { + required: true, + len: 80, + }, + sponsoredBy: { + required: true, + }, + clickUrl: { + required: true, + }, + privacyLink: { + required: false, + }, + body: { + required: true, + }, + icon: { + required: true, + sizes: [50, 50], + }, + }, + }, bids: [ - { - bidder: 'bizzclick', - params: { - placementId: 'hash', - accountId: 'accountId' - } - } - ] - } - ]; -``` \ No newline at end of file + { + bidder: "bizzclick", + params: { + placementId: "hash", + accountId: "accountId", + host: "host", + }, + }, + ], + }, + { + code: "video1", + sizes: [640, 480], + mediaTypes: { + video: { + minduration: 0, + maxduration: 999, + boxingallowed: 1, + skip: 0, + mimes: ["application/javascript", "video/mp4"], + w: 1920, + h: 1080, + protocols: [2], + linearity: 1, + api: [1, 2], + }, + }, + bids: [ + { + bidder: "bizzclick", + params: { + placementId: "hash", + accountId: "accountId", + host: "host", + }, + }, + ], + }, +]; +``` diff --git a/modules/bliinkBidAdapter.js b/modules/bliinkBidAdapter.js index 9a9d74d14c1..debe956d7eb 100644 --- a/modules/bliinkBidAdapter.js +++ b/modules/bliinkBidAdapter.js @@ -2,8 +2,9 @@ // eslint-disable-next-line prebid/validate-imports import { registerBidder } from '../src/adapters/bidderFactory.js' import { config } from '../src/config.js' -import {_each, deepAccess, deepSetValue} from '../src/utils.js' +import { _each, deepAccess, deepSetValue, getWindowSelf, getWindowTop } from '../src/utils.js' export const BIDDER_CODE = 'bliink' +export const GVL_ID = 658 export const BLIINK_ENDPOINT_ENGINE = 'https://engine.bliink.io/prebid' export const BLIINK_ENDPOINT_COOKIE_SYNC_IFRAME = 'https://tag.bliink.io/usersync.html' @@ -12,9 +13,10 @@ export const META_DESCRIPTION = 'description' const VIDEO = 'video' const BANNER = 'banner' - +window.bliinkBid = window.bliinkBid || {}; const supportedMediaTypes = [BANNER, VIDEO] const aliasBidderCode = ['bk'] +const CURRENCY = 'EUR'; /** * @description get coppa value from config @@ -23,6 +25,36 @@ function getCoppa() { return config.getConfig('coppa') === true ? 1 : 0; } +/** + * Retrieves the effective connection type from the browser's Navigator API. + * @returns {string} The effective connection type or 'unsupported' if unavailable. + */ +export function getEffectiveConnectionType() { + /** + * The effective connection type obtained from the browser's Navigator API. + * @type {string|undefined} + */ + const navigatorEffectiveType = navigator?.connection?.effectiveType; + + if (navigatorEffectiveType) { + return navigatorEffectiveType; + } + + return 'unsupported'; +} + +/** + * Retrieves the user IDs as EIDs from the first valid bid request. + * + * @param {Array} validBidRequests - Array of valid bid requests + * @returns {Array|undefined} - Array of user IDs as EIDs, or undefined if not found + */ +export function getUserIds(validBidRequests) { + /** @type {Object} */ + if (validBidRequests?.[0]?.userIdAsEids) { + return validBidRequests[0].userIdAsEids; + } +} export function getMetaList(name) { if (!name || name.length === 0) return [] @@ -91,6 +123,35 @@ export function getKeywords() { return []; } +function canAccessTopWindow() { + try { + if (getWindowTop().location.href) { + return true; + } + } catch (error) { + return false; + } +} + +/** + * domLoading feature is computed on window.top if reachable. + */ +export function getDomLoadingDuration() { + let domLoadingDuration = -1; + let performance; + + performance = (canAccessTopWindow()) ? getWindowTop().performance : getWindowSelf().performance; + + if (performance && performance.timing && performance.timing.navigationStart > 0) { + const val = performance.timing.domLoading - performance.timing.navigationStart; + if (val > 0) { + domLoadingDuration = val; + } + } + + return domLoadingDuration; +} + /** * @param bidRequest * @return {({cpm, netRevenue: boolean, requestId, width: number, currency, ttl: number, creativeId, height: number}&{mediaType: string, vastXml})|null} @@ -120,7 +181,7 @@ export const buildBid = (bidResponse) => { } return Object.assign(bid, { cpm: bidResponse.price, - currency: bidResponse.currency || 'EUR', + currency: bidResponse.currency || CURRENCY, creativeId: deepAccess(bidResponse, 'extras.deal_id'), requestId: deepAccess(bidResponse, 'extras.transaction_id'), width: deepAccess(bidResponse, `creative.${bid.mediaType}.width`) || 1, @@ -149,29 +210,59 @@ export const isBidRequestValid = (bid) => { */ export const buildRequests = (validBidRequests, bidderRequest) => { if (!validBidRequests || !bidderRequest || !bidderRequest.bids) return null - + const domLoadingDuration = getDomLoadingDuration().toString(); const tags = bidderRequest.bids.map((bid) => { - return { + let bidFloor; + const sizes = bid.sizes.map((size) => ({ w: size[0], h: size[1] })); + const mediaTypes = Object.keys(bid.mediaTypes) + if (typeof bid.getFloor === 'function') { + bidFloor = bid.getFloor({ + currency: CURRENCY, + mediaType: mediaTypes[0], + size: sizes[0] + }); + } + const id = bid.params.tagId + const request = { sizes: bid.sizes.map((size) => ({ w: size[0], h: size[1] })), - id: bid.params.tagId, + id, // TODO: bidId is globally unique, is it a good choice for transaction ID (vs ortb2Imp.ext.tid)? transactionId: bid.bidId, - mediaTypes: Object.keys(bid.mediaTypes), + mediaTypes: mediaTypes, imageUrl: deepAccess(bid, 'params.imageUrl', ''), - }; + videoUrl: deepAccess(bid, 'params.videoUrl', ''), + refresh: (window.bliinkBid[id] = (window.bliinkBid[id] ?? -1) + 1) || undefined, + } + if (bidFloor) { + request.bidFloor = bidFloor + } + return request; }); let request = { tags, pageTitle: document.title, - pageUrl: deepAccess(bidderRequest, 'refererInfo.page'), + pageUrl: deepAccess(bidderRequest, 'refererInfo.page').replace(/\?.*$/, ''), pageDescription: getMetaValue(META_DESCRIPTION), keywords: getKeywords().join(','), + ect: getEffectiveConnectionType(), }; + const schain = deepAccess(validBidRequests[0], 'schain') + const eids = getUserIds(validBidRequests) + const device = bidderRequest.ortb2?.device if (schain) { request.schain = schain } + if (domLoadingDuration > -1) { + request.domLoadingDuration = domLoadingDuration + } + if (device) { + request.device = device + } + if (eids) { + request.eids = eids + } const gdprConsent = deepAccess(bidderRequest, 'gdprConsent'); if (!!gdprConsent && gdprConsent.gdprApplies) { request.gdpr = true @@ -183,7 +274,6 @@ export const buildRequests = (validBidRequests, bidderRequest) => { if (bidderRequest.uspConsent) { deepSetValue(request, 'uspConsent', bidderRequest.uspConsent); } - return { method: 'POST', url: BLIINK_ENDPOINT_ENGINE, @@ -254,6 +344,7 @@ const getUserSyncs = (syncOptions, serverResponses, gdprConsent, uspConsent) => */ export const spec = { code: BIDDER_CODE, + gvlid: GVL_ID, aliases: aliasBidderCode, supportedMediaTypes: supportedMediaTypes, isBidRequestValid, diff --git a/modules/blueconicRtdProvider.js b/modules/blueconicRtdProvider.js index b6eb9374671..6b10e79b94c 100644 --- a/modules/blueconicRtdProvider.js +++ b/modules/blueconicRtdProvider.js @@ -11,6 +11,10 @@ import {submodule} from '../src/hook.js'; import {mergeDeep, isPlainObject, logMessage, logError} from '../src/utils.js'; import {MODULE_TYPE_RTD} from '../src/activities/modules.js'; +/** + * @typedef {import('../modules/rtdModule/index.js').RtdSubmodule} RtdSubmodule + */ + const MODULE_NAME = 'realTimeData'; const SUBMODULE_NAME = 'blueconic'; @@ -19,9 +23,9 @@ export const RTD_LOCAL_NAME = 'bcPrebidData'; export const storage = getStorageManager({moduleType: MODULE_TYPE_RTD, moduleName: SUBMODULE_NAME}); /** -* Try parsing stringified array of data. -* @param {String} data -*/ + * Try parsing stringified array of data. + * @param {String} data + */ function parseJson(data) { try { return JSON.parse(data); diff --git a/modules/boldwinBidAdapter.js b/modules/boldwinBidAdapter.js index 3915df8b976..c7def383b5e 100644 --- a/modules/boldwinBidAdapter.js +++ b/modules/boldwinBidAdapter.js @@ -5,7 +5,7 @@ import { convertOrtbRequestToProprietaryNative } from '../src/native.js'; const BIDDER_CODE = 'boldwin'; const AD_URL = 'https://ssp.videowalldirect.com/pbjs'; -const SYNC_URL = 'https://cs.videowalldirect.com' +const SYNC_URL = 'https://sync.videowalldirect.com'; function isBidResponseValid(bid) { if (!bid.requestId || !bid.cpm || !bid.creativeId || @@ -80,6 +80,15 @@ export const spec = { if (bidderRequest.gdprConsent) { request.gdpr = bidderRequest.gdprConsent; } + + // Add GPP consent + if (bidderRequest.gppConsent) { + request.gpp = bidderRequest.gppConsent.gppString; + request.gpp_sid = bidderRequest.gppConsent.applicableSections; + } else if (bidderRequest.ortb2?.regs?.gpp) { + request.gpp = bidderRequest.ortb2.regs.gpp; + request.gpp_sid = bidderRequest.ortb2.regs.gpp_sid; + } } const len = validBidRequests.length; diff --git a/modules/brandmetricsRtdProvider.js b/modules/brandmetricsRtdProvider.js index 53868eccc4c..17336baa76c 100644 --- a/modules/brandmetricsRtdProvider.js +++ b/modules/brandmetricsRtdProvider.js @@ -6,8 +6,14 @@ * @requires module:modules/realTimeData */ import {submodule} from '../src/hook.js'; -import {deepAccess, deepSetValue, logError, mergeDeep} from '../src/utils.js'; +import {deepAccess, deepSetValue, logError, mergeDeep, generateUUID} from '../src/utils.js'; import {loadExternalScript} from '../src/adloader.js'; +import * as events from '../src/events.js'; +import CONSTANTS from '../src/constants.json'; + +/** + * @typedef {import('../modules/rtdModule/index.js').RtdSubmodule} RtdSubmodule + */ const MODULE_NAME = 'brandmetrics' const MODULE_CODE = MODULE_NAME @@ -15,14 +21,18 @@ const RECEIVED_EVENTS = [] const GVL_ID = 422 const TCF_PURPOSES = [1, 7] +let billableEventsInitialized = false + function init (config, userConsent) { const hasConsent = checkConsent(userConsent) + const initialize = hasConsent !== false - if (hasConsent) { + if (initialize) { const moduleConfig = getMergedConfig(config) initializeBrandmetrics(moduleConfig.params.scriptId) + initializeBillableEvents() } - return hasConsent + return initialize } /** @@ -31,43 +41,45 @@ function init (config, userConsent) { * @returns {boolean} */ function checkConsent (userConsent) { - let consent = false - - if (userConsent && userConsent.gdpr && userConsent.gdpr.gdprApplies) { - const gdpr = userConsent.gdpr - - if (gdpr.vendorData) { - const vendor = gdpr.vendorData.vendor - const purpose = gdpr.vendorData.purpose - - let vendorConsent = false - if (vendor.consents) { - vendorConsent = vendor.consents[GVL_ID] + let consent + + if (userConsent) { + if (userConsent.gdpr && userConsent.gdpr.gdprApplies) { + const gdpr = userConsent.gdpr + + if (gdpr.vendorData) { + const vendor = gdpr.vendorData.vendor + const purpose = gdpr.vendorData.purpose + + let vendorConsent = false + if (vendor.consents) { + vendorConsent = vendor.consents[GVL_ID] + } + + if (vendor.legitimateInterests) { + vendorConsent = vendorConsent || vendor.legitimateInterests[GVL_ID] + } + + const purposes = TCF_PURPOSES.map(id => { + return (purpose.consents && purpose.consents[id]) || (purpose.legitimateInterests && purpose.legitimateInterests[id]) + }) + const purposesValid = purposes.filter(p => p === true).length === TCF_PURPOSES.length + consent = vendorConsent && purposesValid } - - if (vendor.legitimateInterests) { - vendorConsent = vendorConsent || vendor.legitimateInterests[GVL_ID] - } - - const purposes = TCF_PURPOSES.map(id => { - return (purpose.consents && purpose.consents[id]) || (purpose.legitimateInterests && purpose.legitimateInterests[id]) - }) - const purposesValid = purposes.filter(p => p === true).length === TCF_PURPOSES.length - consent = vendorConsent && purposesValid + } else if (userConsent.usp) { + const usp = userConsent.usp + consent = usp[1] !== 'N' && usp[2] !== 'Y' } - } else if (userConsent.usp) { - const usp = userConsent.usp - consent = usp[1] !== 'N' && usp[2] !== 'Y' } return consent } /** -* Add event- listeners to hook in to brandmetrics events -* @param {Object} reqBidsConfigObj -* @param {function} callback -*/ + * Add event- listeners to hook in to brandmetrics events + * @param {Object} reqBidsConfigObj + * @param {function} callback + */ function processBrandmetricsEvents (reqBidsConfigObj, moduleConfig, callback) { const callBidTargeting = (event) => { if (event.available && event.conf) { @@ -82,7 +94,6 @@ function processBrandmetricsEvents (reqBidsConfigObj, moduleConfig, callback) { if (RECEIVED_EVENTS.length > 0) { callBidTargeting(RECEIVED_EVENTS[RECEIVED_EVENTS.length - 1]) } else { - window._brandmetrics = window._brandmetrics || [] window._brandmetrics.push({ cmd: '_addeventlistener', val: { @@ -120,6 +131,8 @@ function setBidderTargeting (reqBidsConfigObj, moduleConfig, key, val) { * @param {string} scriptId - The script- id provided by brandmetrics or brandmetrics partner */ function initializeBrandmetrics(scriptId) { + window._brandmetrics = window._brandmetrics || [] + if (scriptId) { const path = 'https://cdn.brandmetrics.com/survey/script/' const file = scriptId + '.js' @@ -129,6 +142,34 @@ function initializeBrandmetrics(scriptId) { } } +/** + * Hook in to brandmetrics creative_in_view- event and emit billable- event for creatives measured by brandmetrics. + */ +function initializeBillableEvents() { + if (!billableEventsInitialized) { + window._brandmetrics.push({ + cmd: '_addeventlistener', + val: { + event: 'creative_in_view', + handler: (ev) => { + if (ev.source && ev.source.type === 'pbj') { + const bid = ev.source.data; + events.emit(CONSTANTS.EVENTS.BILLABLE_EVENT, { + vendor: 'brandmetrics', + type: 'creative_in_view', + measurementId: ev.mid, + billingId: generateUUID(), + auctionId: bid.auctionId, + transactionId: bid.transactionId, + }); + } + }, + } + }) + billableEventsInitialized = true + } +} + /** * Merges a provided config with default values * @param {Object} customConfig diff --git a/modules/brandmetricsRtdProvider.md b/modules/brandmetricsRtdProvider.md index 89ee6bb75cf..d6304f9ae12 100644 --- a/modules/brandmetricsRtdProvider.md +++ b/modules/brandmetricsRtdProvider.md @@ -16,10 +16,10 @@ Enable the Brandmetrics RTD in your Prebid configuration, using the below format pbjs.setConfig({ ..., realTimeData: { - auctionDelay: 500, // auction delay + auctionDelay: 500, dataProviders: [{ name: 'brandmetrics', - waitForIt: true // should be true if there's an `auctionDelay`, + waitForIt: true, params: { scriptId: '00000000-0000-0000-0000-000000000000', bidders: ['ozone'] @@ -29,6 +29,7 @@ pbjs.setConfig({ ... }) ``` +The scriptId- parameter is provided by brandmetrics or a brandmetrics partner. ## Parameters | Name | Type | Description | Default | @@ -38,3 +39,17 @@ pbjs.setConfig({ | params | Object | | - | | params.bidders | String[] | An array of bidders which should receive targeting keys. | `[]` | | params.scriptId | String | A script- id GUID if the brandmetrics- script should be initialized. | `undefined` | + +## Billable events +The module emits a billable event for creatives that are measured by brandmetrics and are considered in- view. + +```javascript +{ + vendor: 'brandmetrics', + type: 'creative_in_view', + measurementId: string, // UUID, brandmetrics measurement id + billingId: string, // UUID, unique billing id + auctionId: string, // Prebid auction id + transactionId: string, //Prebid transaction id +} +``` \ No newline at end of file diff --git a/modules/braveBidAdapter.js b/modules/braveBidAdapter.js index d954522ae24..4c5448482db 100644 --- a/modules/braveBidAdapter.js +++ b/modules/braveBidAdapter.js @@ -4,6 +4,11 @@ import {BANNER, NATIVE, VIDEO} from '../src/mediaTypes.js'; import {config} from '../src/config.js'; import {convertOrtbRequestToProprietaryNative} from '../src/native.js'; +/** + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid + */ + const BIDDER_CODE = 'brave'; const DEFAULT_CUR = 'USD'; const ENDPOINT_URL = `https://point.bravegroup.tv/?t=2&partner=hash`; diff --git a/modules/bridBidAdapter.js b/modules/bridBidAdapter.js new file mode 100644 index 00000000000..e784ea517ac --- /dev/null +++ b/modules/bridBidAdapter.js @@ -0,0 +1,229 @@ +import {createTrackPixelHtml, _each, deepAccess, getDefinedParams, parseGPTSingleSizeArrayToRtbSize} from '../src/utils.js'; +import {VIDEO} from '../src/mediaTypes.js'; +import {registerBidder} from '../src/adapters/bidderFactory.js'; +import {getRefererInfo} from '../src/refererDetection.js'; + +/** + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid + * @typedef {import('../src/adapters/bidderFactory.js').BidderRequest} BidderRequest + */ + +const SOURCE = 'pbjs'; +const BIDDER_CODE = 'brid'; +const ENDPOINT_URL = 'https://pbs.prebrid.tv/openrtb2/auction'; +const GVLID = 934; +const TIME_TO_LIVE = 300; +const VIDEO_PARAMS = [ + 'api', 'linearity', 'maxduration', 'mimes', 'minduration', 'placement', + 'playbackmethod', 'protocols', 'startdelay' +]; + +export const spec = { + + code: BIDDER_CODE, + gvlid: GVLID, + supportedMediaTypes: [VIDEO], + + /** + * Determines whether or not the given bid request is valid. + * + * @param {object} bid The bid to validate. + * @return boolean True if this is a valid bid, and false otherwise. + */ + isBidRequestValid: function(bid) { + return !!(bid && bid.params && bid.params.placementId); + }, + + /** + * Make a server request from the list of BidRequests. + * + * @param {BidRequest[]} bidRequests A non-empty list of bid requests which should be sent to the Server. + * @param {BidderRequest} bidderRequest bidder request object. + * @return ServerRequest Info describing the request to the server. + */ + buildRequests: function(bidRequests, bidderRequest) { + const requests = []; + + _each(bidRequests, function(bid) { + const placementId = bid.params.placementId; + const bidId = bid.bidId; + let sizes = bid.sizes; + if (sizes && !Array.isArray(sizes[0])) sizes = [sizes]; + + const site = getSiteObj(); + + const postBody = { + sdk: { + source: SOURCE, + version: '$prebid.version$' + }, + id: bidderRequest.bidderRequestId, + site, + imp: [] + }; + + const imp = { + ext: { + prebid: { + storedrequest: {'id': placementId} + } + } + }; + + const video = deepAccess(bid, 'mediaTypes.video'); + if (video) { + imp.video = getDefinedParams(video, VIDEO_PARAMS); + if (video.playerSize) { + imp.video = Object.assign( + imp.video, parseGPTSingleSizeArrayToRtbSize(video.playerSize[0]) || {} + ); + } else if (video.w && video.h) { + imp.video.w = video.w; + imp.video.h = video.h; + }; + }; + + postBody.imp.push(imp); + + const gdprConsent = bidderRequest && bidderRequest.gdprConsent; + const uspConsent = bidderRequest && bidderRequest.uspConsent; + + if (gdprConsent || uspConsent) { + postBody.regs = { ext: {} }; + + if (uspConsent) { + postBody.regs.ext.us_privacy = uspConsent; + }; + + if (gdprConsent) { + if (typeof gdprConsent.gdprApplies !== 'undefined') { + postBody.regs.ext.gdpr = gdprConsent.gdprApplies ? 1 : 0; + }; + + if (typeof gdprConsent.consentString !== 'undefined') { + postBody.user = { + ext: { consent: gdprConsent.consentString } + }; + }; + }; + }; + + if (bidRequests[0].schain) { + postBody.schain = bidRequests[0].schain; + } + + const params = bid.params; + + requests.push({ + method: 'POST', + url: ENDPOINT_URL, + data: JSON.stringify(postBody), + options: { + withCredentials: true + }, + bidId, + params + }); + }); + + return requests; + }, + + /** + * Unpack the response from the server into a list of bids. + * + * @param {*} serverResponse A successful response from the server. + * @return {Bid[]} An array of bids which were nested inside the server. + */ + interpretResponse: function(serverResponse, bidRequest) { + const response = serverResponse.body; + const bidResponses = []; + + _each(response.seatbid, (resp) => { + _each(resp.bid, (bid) => { + const requestId = bidRequest.bidId; + const params = bidRequest.params; + + const {ad, adUrl, vastUrl, vastXml} = getAd(bid); + + const bidResponse = { + requestId, + params, + cpm: bid.price, + width: bid.w, + height: bid.h, + creativeId: bid.adid, + currency: response.cur, + netRevenue: false, + ttl: TIME_TO_LIVE, + meta: { + advertiserDomains: bid.adomain || [] + } + }; + + if (vastUrl || vastXml) { + bidResponse.mediaType = VIDEO; + if (vastUrl) bidResponse.vastUrl = vastUrl; + if (vastXml) bidResponse.vastXml = vastXml; + } else { + bidResponse.ad = ad; + bidResponse.adUrl = adUrl; + }; + + bidResponses.push(bidResponse); + }); + }); + + return bidResponses; + }, + +} + +/** + * Helper function to get ad + * + * @param {object} bid The bid. + * @return {object} ad object. + */ +function getAd(bid) { + let ad, adUrl, vastXml, vastUrl; + + switch (deepAccess(bid, 'ext.prebid.type')) { + case VIDEO: + if (bid.adm.substr(0, 4) === 'http') { + vastUrl = bid.adm; + } else { + vastXml = bid.adm; + }; + break; + default: + if (bid.adm && bid.nurl) { + ad = bid.adm; + ad += createTrackPixelHtml(decodeURIComponent(bid.nurl)); + } else if (bid.adm) { + ad = bid.adm; + } else if (bid.nurl) { + adUrl = bid.nurl; + }; + } + + return {ad, adUrl, vastXml, vastUrl}; +} + +/** + * Helper function to get site object + * + * @return {object} siteObj. + */ +function getSiteObj() { + const refInfo = (getRefererInfo && getRefererInfo()) || {}; + + return { + page: refInfo.page, + ref: refInfo.ref, + domain: refInfo.domain + }; +} + +registerBidder(spec); diff --git a/modules/bridgewellBidAdapter.js b/modules/bridgewellBidAdapter.js index 6088cefaa55..578acf8a358 100644 --- a/modules/bridgewellBidAdapter.js +++ b/modules/bridgewellBidAdapter.js @@ -4,6 +4,11 @@ import {BANNER, NATIVE} from '../src/mediaTypes.js'; import {find} from '../src/polyfill.js'; import { convertOrtbRequestToProprietaryNative } from '../src/native.js'; +/** + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid + */ + const BIDDER_CODE = 'bridgewell'; const REQUEST_ENDPOINT = 'https://prebid.scupio.com/recweb/prebid.aspx?cb='; const BIDDER_VERSION = '1.1.0'; diff --git a/modules/brightcomBidAdapter.js b/modules/brightcomBidAdapter.js index c4cc5394a03..1fa1dac4e95 100644 --- a/modules/brightcomBidAdapter.js +++ b/modules/brightcomBidAdapter.js @@ -1,4 +1,17 @@ -import { getBidIdParameter, _each, isArray, getWindowTop, getUniqueIdentifierStr, deepSetValue, logError, logWarn, createTrackPixelHtml, getWindowSelf, isFn, isPlainObject } from '../src/utils.js'; +import { + _each, + isArray, + getWindowTop, + getUniqueIdentifierStr, + deepSetValue, + logError, + logWarn, + createTrackPixelHtml, + getWindowSelf, + isFn, + isPlainObject, + getBidIdParameter +} from '../src/utils.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; import { BANNER } from '../src/mediaTypes.js'; import { config } from '../src/config.js'; diff --git a/modules/brightcomSSPBidAdapter.js b/modules/brightcomSSPBidAdapter.js index b85a01c8fc7..4750881da40 100644 --- a/modules/brightcomSSPBidAdapter.js +++ b/modules/brightcomSSPBidAdapter.js @@ -1,5 +1,4 @@ import { - getBidIdParameter, isArray, getWindowTop, getUniqueIdentifierStr, @@ -9,7 +8,7 @@ import { createTrackPixelHtml, getWindowSelf, isFn, - isPlainObject, + isPlainObject, getBidIdParameter, } from '../src/utils.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import {BANNER} from '../src/mediaTypes.js'; diff --git a/modules/britepoolIdSystem.js b/modules/britepoolIdSystem.js index 2316fbb732d..37ace544dc7 100644 --- a/modules/britepoolIdSystem.js +++ b/modules/britepoolIdSystem.js @@ -10,6 +10,13 @@ import {ajax} from '../src/ajax.js'; import {submodule} from '../src/hook.js'; const PIXEL = 'https://px.britepool.com/new?partner_id=t'; +/** + * @typedef {import('../modules/userId/index.js').Submodule} Submodule + * @typedef {import('../modules/userId/index.js').SubmoduleConfig} SubmoduleConfig + * @typedef {import('../modules/userId/index.js').ConsentData} ConsentData + * @typedef {import('../modules/userId/index.js').SubmoduleParams} SubmoduleParams + */ + /** @type {Submodule} */ export const britepoolIdSubmodule = { /** @@ -136,6 +143,12 @@ export const britepoolIdSubmodule = { } } return valueObj; + }, + eids: { + 'britepoolid': { + source: 'britepool.com', + atype: 3 + }, } }; diff --git a/modules/browsiBidAdapter.js b/modules/browsiBidAdapter.js index 03b6b2a8f3d..fa1cacaa568 100644 --- a/modules/browsiBidAdapter.js +++ b/modules/browsiBidAdapter.js @@ -3,6 +3,11 @@ import {config} from '../src/config.js'; import {VIDEO} from '../src/mediaTypes.js'; import {logError, logInfo, isArray, isStr} from '../src/utils.js'; +/** + * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid + * @typedef {import('../src/adapters/bidderFactory.js').UserSync} UserSync + */ + const BIDDER_CODE = 'browsi'; const DATA = 'brwvidtag'; const ADAPTER = '__bad'; diff --git a/modules/browsiRtdProvider.js b/modules/browsiRtdProvider.js index 4a61f40600d..5281274616a 100644 --- a/modules/browsiRtdProvider.js +++ b/modules/browsiRtdProvider.js @@ -25,6 +25,11 @@ 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'; + +/** + * @typedef {import('../modules/rtdModule/index.js').RtdSubmodule} RtdSubmodule + */ + const MODULE_NAME = 'browsi'; const storage = getStorageManager({moduleType: MODULE_TYPE_RTD, moduleName: MODULE_NAME}); @@ -88,6 +93,7 @@ export function collectData() { let predictorData = { ...{ sk: _moduleParams.siteKey, + pk: _moduleParams.pubKey, sw: (win.screen && win.screen.width) || -1, sh: (win.screen && win.screen.height) || -1, url: `${doc.location.protocol}//${doc.location.host}${doc.location.pathname}`, @@ -134,7 +140,6 @@ function getRTD(auc) { const adSlot = getSlotByCode(uc); const identifier = adSlot ? getMacroId(_browsiData['pmd'], adSlot) : uc; const _pd = _bp[identifier]; - rp[uc] = getKVObject(-1); if (!_pd) { return rp } @@ -275,7 +280,7 @@ function getPredictionsFromServer(url) { if (req.status === 200) { try { const data = JSON.parse(response); - if (data && data.p && data.kn) { + if (data) { setData({p: data.p, kn: data.kn, pmd: data.pmd, bet: data.bet}); } else { setData({}); diff --git a/modules/bucksenseBidAdapter.js b/modules/bucksenseBidAdapter.js index 7b6c3911ea1..5aa14f2a53b 100644 --- a/modules/bucksenseBidAdapter.js +++ b/modules/bucksenseBidAdapter.js @@ -2,12 +2,18 @@ import { logInfo } from '../src/utils.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; import { BANNER } from '../src/mediaTypes.js'; +/** + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid + */ + const WHO = 'BKSHBID-005'; const BIDDER_CODE = 'bucksense'; const URL = 'https://directo.prebidserving.com/prebidjs/'; export const spec = { code: BIDDER_CODE, + gvlid: 235, supportedMediaTypes: [BANNER], /** @@ -15,7 +21,7 @@ export const spec = { * * @param {object} bid The bid to validate. * @return boolean True if this is a valid bid, and false otherwise. - */ + */ isBidRequestValid: function (bid) { logInfo(WHO + ' isBidRequestValid() - INPUT bid:', bid); if (bid.bidder !== BIDDER_CODE || typeof bid.params === 'undefined') { @@ -28,10 +34,10 @@ export const spec = { }, /** - * Make a server request from the list of BidRequests. - * - * @param {BidRequest[]} validBidRequests A non-empty list of valid bid requests that should be sent to the Server. - * @return ServerRequest Info describing the request to the server. + * Make a server request from the list of BidRequests. + * + * @param {BidRequest[]} validBidRequests A non-empty list of valid bid requests that should be sent to the Server. + * @return ServerRequest Info describing the request to the server. */ buildRequests: function (validBidRequests, bidderRequest) { logInfo(WHO + ' buildRequests() - INPUT validBidRequests:', validBidRequests, 'INPUT bidderRequest:', bidderRequest); @@ -73,7 +79,7 @@ export const spec = { * * @param {*} serverResponse A successful response from the server. * @return {Bid[]} An array of bids which were nested inside the server. - */ + */ interpretResponse: function (serverResponse, request) { logInfo(WHO + ' interpretResponse() - INPUT serverResponse:', serverResponse, 'INPUT request:', request); diff --git a/modules/buzzoolaBidAdapter.js b/modules/buzzoolaBidAdapter.js index b5ea6227f58..14a26203484 100644 --- a/modules/buzzoolaBidAdapter.js +++ b/modules/buzzoolaBidAdapter.js @@ -5,6 +5,12 @@ import {Renderer} from '../src/Renderer.js'; import {OUTSTREAM} from '../src/video.js'; import { convertOrtbRequestToProprietaryNative } from '../src/native.js'; +/** + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid + * @typedef {import('../src/adapters/bidderFactory.js').ServerResponse} ServerResponse + */ + const BIDDER_CODE = 'buzzoola'; const ENDPOINT = 'https://exchange.buzzoola.com/ssp/prebidjs'; const RENDERER_SRC = 'https://tube.buzzoola.com/new/build/buzzlibrary.js'; diff --git a/modules/c1xBidAdapter.js b/modules/c1xBidAdapter.js index 8c9407825ba..79ba8cf499d 100644 --- a/modules/c1xBidAdapter.js +++ b/modules/c1xBidAdapter.js @@ -2,6 +2,10 @@ import { registerBidder } from '../src/adapters/bidderFactory.js'; import { logInfo, logError } from '../src/utils.js'; import { BANNER } from '../src/mediaTypes.js'; +/** + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + */ + const BIDDER_CODE = 'c1x'; const URL = 'https://hb-stg.c1exchange.com/ht'; // const PIXEL_ENDPOINT = '//px.c1exchange.com/pubpixel/'; diff --git a/modules/cadentApertureMXBidAdapter.js b/modules/cadentApertureMXBidAdapter.js index 26e8639154c..e73564dacdb 100644 --- a/modules/cadentApertureMXBidAdapter.js +++ b/modules/cadentApertureMXBidAdapter.js @@ -1,7 +1,6 @@ import { _each, - deepAccess, - getBidIdParameter, + deepAccess, getBidIdParameter, isArray, isFn, isPlainObject, @@ -170,6 +169,22 @@ export const cadentAdapter = { return cadentData; }, + + getGpp: (bidRequest, cadentData) => { + if (bidRequest.gppConsent) { + const {gppString: gpp, applicableSections: gppSid} = bidRequest.gppConsent; + if (cadentData.regs) { + cadentData.regs.gpp = gpp; + cadentData.regs.gpp_sid = gppSid; + } else { + cadentData.regs = { + gpp: gpp, + gpp_sid: gppSid + } + } + } + return cadentData; + }, getSupplyChain: (bidderRequest, cadentData) => { if (bidderRequest.bids[0] && bidderRequest.bids[0].schain) { cadentData.source = { @@ -261,7 +276,7 @@ export const spec = { let isVideo = !!bid.mediaTypes.video; let data = { id: bid.bidId, - tid: bid.transactionId, + tid: bid.ortb2Imp?.ext?.tid, tagid, secure }; @@ -281,7 +296,7 @@ export const spec = { }); let cadentData = { - id: bidderRequest.auctionId, + id: bidderRequest.auctionId ?? bidderRequest.bidderRequestId, imp: cadentImps, device, site, @@ -290,6 +305,7 @@ export const spec = { }; cadentData = cadentAdapter.getGdpr(bidderRequest, Object.assign({}, cadentData)); + cadentData = cadentAdapter.getGpp(bidderRequest, Object.assign({}, cadentData)); cadentData = cadentAdapter.getSupplyChain(bidderRequest, Object.assign({}, cadentData)); if (bidderRequest && bidderRequest.uspConsent) { cadentData.us_privacy = bidderRequest.uspConsent; @@ -357,7 +373,7 @@ export const spec = { } return cadentBidResponses; }, - getUserSyncs: function (syncOptions, responses, gdprConsent, uspConsent) { + getUserSyncs: function (syncOptions, responses, gdprConsent, uspConsent, gppConsent) { const syncs = []; const consentParams = []; if (syncOptions.iframeEnabled) { @@ -373,6 +389,14 @@ export const spec = { if (uspConsent && typeof uspConsent.consentString === 'string') { consentParams.push(`usp=${uspConsent.consentString}`); } + if (gppConsent && typeof gppConsent === 'object') { + if (gppConsent.gppString && typeof gppConsent.gppString === 'string') { + consentParams.push(`gpp=${gppConsent.gppString}`); + } + if (gppConsent.applicableSections && typeof gppConsent.applicableSections === 'object') { + consentParams.push(`gpp_sid=${gppConsent.applicableSections}`); + } + } if (consentParams.length > 0) { url = url + '?' + consentParams.join('&'); } diff --git a/modules/cadentApertureMXBidAdapter.md b/modules/cadentApertureMXBidAdapter.md index 11d63134587..d924f904be4 100644 --- a/modules/cadentApertureMXBidAdapter.md +++ b/modules/cadentApertureMXBidAdapter.md @@ -3,14 +3,14 @@ ``` Module Name: Cadent Aperture MX Adapter Module Type: Bidder Adapter -Maintainer: git@emxdigital.com +Maintainer: contactaperturemx@cadent.tv ``` # Description The Cadent Aperture MX adapter provides publishers with access to the Cadent Aperture MX SSP. The adapter is GDPR compliant. Please note that the adapter supports Banner and Video (Instream & Outstream) media types. -Note: The Cadent Aperture MX adapter requires approval and implementation guidelines from the Cadent team, including existing publishers that work with Cadent. Please reach out to your account manager or prebid@emxdigital.com for more information. +Note: The Cadent Aperture MX adapter requires approval and implementation guidelines from the Cadent team, including existing publishers that work with Cadent. Please reach out to your account manager or contactaperturemx@cadent.tv for more information. The bidder code should be ```cadent_aperture_mx``` The params used by the bidder are : diff --git a/modules/categoryTranslation.js b/modules/categoryTranslation.js index ba73aa01b85..eb6cb83730a 100644 --- a/modules/categoryTranslation.js +++ b/modules/categoryTranslation.js @@ -12,7 +12,7 @@ */ import {config} from '../src/config.js'; -import {hook, setupBeforeHookFnOnce} from '../src/hook.js'; +import {hook, setupBeforeHookFnOnce, ready} from '../src/hook.js'; import {ajax} from '../src/ajax.js'; import {logError, timestamp} from '../src/utils.js'; import {addBidResponse} from '../src/auction.js'; @@ -32,7 +32,8 @@ export const registerAdserver = hook('async', function(adServer) { initTranslation(url, DEFAULT_IAB_TO_FW_MAPPING_KEY); } }, 'registerAdserver'); -registerAdserver(); + +ready.then(() => registerAdserver()); export const getAdserverCategoryHook = timedBidResponseHook('categoryTranslation', function getAdserverCategoryHook(fn, adUnitCode, bid, reject) { if (!bid) { diff --git a/modules/cleanioRtdProvider.js b/modules/cleanioRtdProvider.js index 7d0f461108b..f9bed5357ee 100644 --- a/modules/cleanioRtdProvider.js +++ b/modules/cleanioRtdProvider.js @@ -12,6 +12,10 @@ import { logError, generateUUID, insertElement } from '../src/utils.js'; import * as events from '../src/events.js'; import CONSTANTS from '../src/constants.json'; +/** + * @typedef {import('../modules/rtdModule/index.js').RtdSubmodule} RtdSubmodule + */ + // ============================ MODULE STATE =============================== /** diff --git a/modules/clickforceBidAdapter.js b/modules/clickforceBidAdapter.js index 92bc9b1bad2..be81ff1885c 100644 --- a/modules/clickforceBidAdapter.js +++ b/modules/clickforceBidAdapter.js @@ -2,6 +2,12 @@ import { _each } from '../src/utils.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import {BANNER, NATIVE} from '../src/mediaTypes.js'; import { convertOrtbRequestToProprietaryNative } from '../src/native.js'; + +/** + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid + */ + const BIDDER_CODE = 'clickforce'; const ENDPOINT_URL = 'https://ad.holmesmind.com/adserver/prebid.json?cb=' + new Date().getTime() + '&hb=1&ver=1.21'; diff --git a/modules/codefuelBidAdapter.js b/modules/codefuelBidAdapter.js index 2548b20189b..a289e29bd19 100644 --- a/modules/codefuelBidAdapter.js +++ b/modules/codefuelBidAdapter.js @@ -2,6 +2,14 @@ import {deepAccess, isArray} from '../src/utils.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import {BANNER} from '../src/mediaTypes.js'; +/** + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid + * @typedef {import('../src/adapters/bidderFactory.js').ServerResponse} ServerResponse + * @typedef {import('../src/adapters/bidderFactory.js').SyncOptions} SyncOptions + * @typedef {import('../src/adapters/bidderFactory.js').UserSync} UserSync + */ + const BIDDER_CODE = 'codefuel'; const CURRENCY = 'USD'; @@ -10,11 +18,11 @@ export const spec = { supportedMediaTypes: [ BANNER ], aliases: ['ex'], // short code /** - * Determines whether or not the given bid request is valid. - * - * @param {BidRequest} bid The bid params to validate. - * @return boolean True if this is a valid bid, and false otherwise. - */ + * 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.nativeParams) { return false; @@ -22,11 +30,11 @@ export const spec = { return !!(bid.params.placementId || (bid.params.member && bid.params.invCode)); }, /** - * Make a server request from the list of BidRequests. - * - * @param {validBidRequests[]} - an array of bids - * @return ServerRequest Info describing the request to the server. - */ + * 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) { const page = bidderRequest.refererInfo.page; const domain = bidderRequest.refererInfo.domain; @@ -78,11 +86,11 @@ export const spec = { }; }, /** - * 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. - */ + * 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: (serverResponse, { bids }) => { if (!serverResponse.body) { return []; @@ -116,12 +124,12 @@ export const spec = { }, /** - * 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. + */ getUserSyncs: function(syncOptions, serverResponses, gdprConsent, uspConsent) { return []; } diff --git a/modules/cointrafficBidAdapter.js b/modules/cointrafficBidAdapter.js index 380e1f5fc77..3b90529b6cc 100644 --- a/modules/cointrafficBidAdapter.js +++ b/modules/cointrafficBidAdapter.js @@ -3,6 +3,13 @@ import { registerBidder } from '../src/adapters/bidderFactory.js'; import { BANNER } from '../src/mediaTypes.js' import { config } from '../src/config.js' +/** + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid + * @typedef {import('../src/adapters/bidderFactory.js').ServerResponse} ServerResponse + * @typedef {import('../src/adapters/bidderFactory.js').BidderSpec} BidderSpec + */ + const BIDDER_CODE = 'cointraffic'; const ENDPOINT_URL = 'https://apps-pbd.ctraffic.io/pb/tmp'; const DEFAULT_CURRENCY = 'EUR'; diff --git a/modules/coinzillaBidAdapter.js b/modules/coinzillaBidAdapter.js index 15731423c49..9ae2c74547d 100644 --- a/modules/coinzillaBidAdapter.js +++ b/modules/coinzillaBidAdapter.js @@ -1,6 +1,12 @@ import { parseSizesInput } from '../src/utils.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; +/** + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid + * @typedef {import('../src/adapters/bidderFactory.js').ServerResponse} ServerResponse + */ + const BIDDER_CODE = 'coinzilla'; const ENDPOINT_URL = 'https://request.czilladx.com/serve/request.php'; @@ -77,7 +83,7 @@ export const spec = { dealId: dealId, currency: currency, netRevenue: netRevenue, - ttl: bidRequest.timeout, + ttl: response.timeout, referrer: referrer, ad: response.ad, mediaType: response.mediaType, diff --git a/modules/colossussspBidAdapter.js b/modules/colossussspBidAdapter.js index b1ee8875422..cc3e452f20c 100644 --- a/modules/colossussspBidAdapter.js +++ b/modules/colossussspBidAdapter.js @@ -5,6 +5,11 @@ import { ajax } from '../src/ajax.js'; import { config } from '../src/config.js'; import { convertOrtbRequestToProprietaryNative } from '../src/native.js'; +/** + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid + */ + const BIDDER_CODE = 'colossusssp'; const G_URL = 'https://colossusssp.com/?c=o&m=multi'; const G_URL_SYNC = 'https://sync.colossusssp.com'; diff --git a/modules/conceptxBidAdapter.js b/modules/conceptxBidAdapter.js index 1147a50e71f..87ac96f2131 100644 --- a/modules/conceptxBidAdapter.js +++ b/modules/conceptxBidAdapter.js @@ -3,23 +3,23 @@ import { BANNER } from '../src/mediaTypes.js'; // import { logError, logInfo, logWarn, parseUrl } from '../src/utils.js'; const BIDDER_CODE = 'conceptx'; -let ENDPOINT_URL = 'https://conceptx.cncpt-central.com/openrtb'; +const ENDPOINT_URL = 'https://conceptx.cncpt-central.com/openrtb'; // const LOG_PREFIX = 'ConceptX: '; export const spec = { code: BIDDER_CODE, supportedMediaTypes: [BANNER], isBidRequestValid: function (bid) { - return !!(bid.bidId); + return !!(bid.bidId && bid.params.site && bid.params.adunit); }, buildRequests: function (validBidRequests, bidderRequest) { // logWarn(LOG_PREFIX + 'all native assets containing URL should be sent as placeholders with sendId(icon, image, clickUrl, displayUrl, privacyLink, privacyIcon)'); const requests = []; - + let requestUrl = `${ENDPOINT_URL}` if (bidderRequest && bidderRequest.gdprConsent && bidderRequest.gdprConsent.gdprApplies) { - ENDPOINT_URL += '?gdpr_applies=' + bidderRequest.gdprConsent.gdprApplies; - ENDPOINT_URL += '&consentString=' + bidderRequest.gdprConsent.consentString; + requestUrl += '?gdpr_applies=' + bidderRequest.gdprConsent.gdprApplies; + requestUrl += '&consentString=' + bidderRequest.gdprConsent.consentString; } for (var i = 0; i < validBidRequests.length; i++) { const requestParent = { adUnits: [], meta: {} }; @@ -33,7 +33,7 @@ export const spec = { requestParent.adUnits.push(adUnit); requests.push({ method: 'POST', - url: ENDPOINT_URL, + url: requestUrl, options: { withCredentials: false, }, @@ -45,10 +45,16 @@ export const spec = { }, interpretResponse: function (serverResponse, bidRequest) { - const bidResponsesFromServer = serverResponse.body.bidResponses; - const firstDummy = bidResponsesFromServer[0] - const firstSeat = firstDummy.ads[0] const bidResponses = []; + const bidResponsesFromServer = serverResponse.body.bidResponses; + if (Array.isArray(bidResponsesFromServer) && bidResponsesFromServer.length === 0) { + return bidResponses + } + const firstBid = bidResponsesFromServer[0] + if (!firstBid) { + return bidResponses + } + const firstSeat = firstBid.ads[0] const bidResponse = { requestId: firstSeat.requestId, cpm: firstSeat.cpm, diff --git a/modules/concertBidAdapter.js b/modules/concertBidAdapter.js index bf4079322ff..bd738a39bba 100644 --- a/modules/concertBidAdapter.js +++ b/modules/concertBidAdapter.js @@ -3,6 +3,13 @@ import { registerBidder } from '../src/adapters/bidderFactory.js'; import { getStorageManager } from '../src/storageManager.js'; import { hasPurpose1Consent } from '../src/utils/gpdr.js'; +/** + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid + * @typedef {import('../src/adapters/bidderFactory.js').ServerResponse} ServerResponse + * @typedef {import('../src/adapters/bidderFactory.js').validBidRequests} validBidRequests + */ + const BIDDER_CODE = 'concert'; const CONCERT_ENDPOINT = 'https://bids.concert.io'; @@ -41,6 +48,7 @@ export const spec = { prebidVersion: '$prebid.version$', pageUrl: bidderRequest.refererInfo.page, screen: [window.screen.width, window.screen.height].join('x'), + browserLanguage: window.navigator.language, debug: debugTurnedOn(), uid: getUid(bidderRequest, validBidRequests), optedOut: hasOptedOutOfPersonalization(), @@ -48,6 +56,7 @@ export const spec = { uspConsent: bidderRequest.uspConsent, gdprConsent: bidderRequest.gdprConsent, gppConsent: bidderRequest.gppConsent, + tdid: getTdid(bidderRequest, validBidRequests), } }; @@ -262,3 +271,11 @@ function getOffset(el) { }; } } + +function getTdid(bidderRequest, validBidRequests) { + if (hasOptedOutOfPersonalization() || !consentAllowsPpid(bidderRequest)) { + return null; + } + + return deepAccess(validBidRequests[0], 'userId.tdid') || null; +} diff --git a/modules/connatixBidAdapter.js b/modules/connatixBidAdapter.js new file mode 100644 index 00000000000..7524cd4e194 --- /dev/null +++ b/modules/connatixBidAdapter.js @@ -0,0 +1,184 @@ +import { + registerBidder +} from '../src/adapters/bidderFactory.js'; + +import { + deepAccess, + isFn, + logError, + isArray, + formatQS +} from '../src/utils.js'; + +import { + BANNER, +} from '../src/mediaTypes.js'; + +const BIDDER_CODE = 'connatix'; +const AD_URL = 'https://capi.connatix.com/rtb/hba'; +const DEFAULT_MAX_TTL = '3600'; +const DEFAULT_CURRENCY = 'USD'; + +/* + * Get the bid floor value from the bid object, either using the getFloor function or by accessing the 'params.bidfloor' property. + * If the bid floor cannot be determined, return 0 as a fallback value. + */ +export function getBidFloor(bid) { + if (!isFn(bid.getFloor)) { + return deepAccess(bid, 'params.bidfloor', 0); + } + + try { + const bidFloor = bid.getFloor({ + currency: DEFAULT_CURRENCY, + mediaType: '*', + size: '*', + }); + return bidFloor.floor; + } catch (err) { + logError(err); + return 0; + } +} + +export const spec = { + code: BIDDER_CODE, + gvlid: 143, + supportedMediaTypes: [BANNER], + + /* + * Validate the bid request. + * If the request is valid, Connatix is trying to obtain at least one bid. + * Otherwise, the request to the Connatix server is not made + */ + isBidRequestValid: (bid = {}) => { + const bidId = deepAccess(bid, 'bidId'); + const mediaTypes = deepAccess(bid, 'mediaTypes', {}); + const params = deepAccess(bid, 'params', {}); + const bidder = deepAccess(bid, 'bidder'); + + const banner = deepAccess(mediaTypes, BANNER, {}); + + const hasBidId = Boolean(bidId); + const isValidBidder = (bidder === BIDDER_CODE); + const isValidSize = (Boolean(banner.sizes) && isArray(mediaTypes[BANNER].sizes) && mediaTypes[BANNER].sizes.length > 0); + const hasSizes = mediaTypes[BANNER] ? isValidSize : false; + const hasRequiredBidParams = Boolean(params.placementId); + + const isValid = isValidBidder && hasBidId && hasSizes && hasRequiredBidParams; + if (!isValid) { + logError(`Invalid bid request: isValidBidder: ${isValidBidder} hasBidId: ${hasBidId}, hasSizes: ${hasSizes}, hasRequiredBidParams: ${hasRequiredBidParams}`); + } + return isValid; + }, + + /* + * Build the request payload by processing valid bid requests and extracting the necessary information. + * Determine the host and page from the bidderRequest's refferUrl, and include ccpa and gdpr consents. + * Return an object containing the request method, url, and the constructed payload. + */ + buildRequests: (validBidRequests = [], bidderRequest = {}) => { + const bidRequests = validBidRequests.map(bid => { + const { + bidId, + mediaTypes, + params, + sizes, + } = bid; + return { + bidId, + mediaTypes, + sizes, + placementId: params.placementId, + floor: getBidFloor(bid), + }; + }); + + const requestPayload = { + ortb2: bidderRequest.ortb2, + gdprConsent: bidderRequest.gdprConsent, + uspConsent: bidderRequest.uspConsent, + refererInfo: bidderRequest.refererInfo, + bidRequests, + }; + + return { + method: 'POST', + url: AD_URL, + data: requestPayload + }; + }, + + /* + * Interpret the server response and create an array of bid responses by extracting and formatting + * relevant information such as requestId, cpm, ttl, width, height, creativeId, referrer and ad + * Returns an array of bid responses by extracting and formatting the server response + */ + interpretResponse: (serverResponse) => { + const responseBody = serverResponse.body; + const bids = responseBody.Bids; + + if (!isArray(bids)) { + return []; + } + + const referrer = responseBody.Referrer; + return bids.map(bidResponse => ({ + requestId: bidResponse.RequestId, + cpm: bidResponse.Cpm, + ttl: bidResponse.Ttl || DEFAULT_MAX_TTL, + currency: 'USD', + mediaType: BANNER, + netRevenue: true, + width: bidResponse.Width, + height: bidResponse.Height, + creativeId: bidResponse.CreativeId, + ad: bidResponse.Ad, + referrer: referrer, + })); + }, + + /* + * Determine the user sync type (either 'iframe' or 'image') based on syncOptions. + * Construct the sync URL by appending required query parameters such as gdpr, ccpa, and coppa consents. + * Return an array containing an object with the sync type and the constructed URL. + */ + getUserSyncs: (syncOptions, serverResponses, gdprConsent, uspConsent, gppConsent) => { + if (!syncOptions.iframeEnabled) { + return []; + } + + if (!serverResponses || !serverResponses.length) { + return []; + } + + const params = {}; + + if (gdprConsent) { + if (typeof gdprConsent.gdprApplies === 'boolean') { + params['gdpr'] = Number(gdprConsent.gdprApplies); + } else { + params['gdpr'] = 0; + } + + if (typeof gdprConsent.consentString === 'string') { + params['gdpr_consent'] = encodeURIComponent(gdprConsent.consentString); + } + } + + if (typeof uspConsent === 'string') { + params['us_privacy'] = encodeURIComponent(uspConsent); + } + + const syncUrl = serverResponses[0].body.UserSyncEndpoint; + const queryParams = Object.keys(params).length > 0 ? formatQS(params) : ''; + + const url = queryParams ? `${syncUrl}?${queryParams}` : syncUrl; + return [{ + type: 'iframe', + url + }]; + } +}; + +registerBidder(spec); diff --git a/modules/connatixBidAdapter.md b/modules/connatixBidAdapter.md new file mode 100644 index 00000000000..595c294e311 --- /dev/null +++ b/modules/connatixBidAdapter.md @@ -0,0 +1,54 @@ + +# Overview + +``` +Module Name: Connatix Bidder Adapter +Module Type: Bidder Adapter +Maintainer: prebid_integration@connatix.com +``` + +# Description +Connects to Connatix demand source to fetch bids. +Please use ```connatix``` as the bidder code. + +# Configuration +Connatix requires that ```iframe``` is used for user syncing. + +Example configuration: +``` +pbjs.setConfig({ + userSync: { + filterSettings: { + iframe: { + bidders: '*', // represents all bidders + filter: 'include' + } + } + } +}); +``` + +# Test Parameters +``` +var adUnits = [ + { + code: '1', + mediaTypes: { + banner: { + sizes: [[640, 480], [320, 180]], + }, + }, + bids: [ + { + bidder: 'connatix', + params: { + placementId: 'e4984e88-9ff4-45a3-8b9d-33aabcad634e', // required + bidfloor: 2.5, // optional + }, + }, + // Add more bidders and their parameters as needed + ], + }, + // Define more ad units here if necessary +]; +``` \ No newline at end of file diff --git a/modules/connectIdSystem.js b/modules/connectIdSystem.js index 1caff49199d..2ebc68baa84 100644 --- a/modules/connectIdSystem.js +++ b/modules/connectIdSystem.js @@ -10,10 +10,17 @@ import {submodule} from '../src/hook.js'; import {includes} from '../src/polyfill.js'; import {getRefererInfo} from '../src/refererDetection.js'; import {getStorageManager} from '../src/storageManager.js'; -import {formatQS, isPlainObject, logError, parseUrl} from '../src/utils.js'; +import {formatQS, isNumber, isPlainObject, logError, parseUrl} from '../src/utils.js'; import {uspDataHandler, gppDataHandler} from '../src/adapterManager.js'; import {MODULE_TYPE_UID} from '../src/activities/modules.js'; +/** + * @typedef {import('../modules/userId/index.js').Submodule} Submodule + * @typedef {import('../modules/userId/index.js').SubmoduleConfig} SubmoduleConfig + * @typedef {import('../modules/userId/index.js').ConsentData} ConsentData + * @typedef {import('../modules/userId/index.js').IdResponse} IdResponse + */ + const MODULE_NAME = 'connectId'; const STORAGE_EXPIRY_DAYS = 365; const STORAGE_DURATION = 60 * 60 * 24 * 1000 * STORAGE_EXPIRY_DAYS; @@ -26,6 +33,16 @@ const PLACEHOLDER = '__PIXEL_ID__'; const UPS_ENDPOINT = `https://ups.analytics.yahoo.com/ups/${PLACEHOLDER}/fed`; const OVERRIDE_OPT_OUT_KEY = 'connectIdOptOut'; const INPUT_PARAM_KEYS = ['pixelId', 'he', 'puid']; +const O_AND_O_DOMAINS = [ + 'yahoo.com', + 'aol.com', + 'aol.ca', + 'aol.de', + 'aol.co.uk', + 'engadget.com', + 'techcrunch.com', + 'autoblog.com', +]; export const storage = getStorageManager({moduleType: MODULE_TYPE_UID, moduleName: MODULE_NAME}); /** @@ -104,9 +121,11 @@ function syncLocalStorageToCookie() { } function isStale(storedIdData) { - if (isPlainObject(storedIdData) && storedIdData.lastSynced && - (storedIdData.lastSynced + VALID_ID_DURATION) <= Date.now()) { + if (isOAndOTraffic()) { return true; + } else if (isPlainObject(storedIdData) && storedIdData.lastSynced) { + const validTTL = storedIdData.ttl || VALID_ID_DURATION; + return storedIdData.lastSynced + validTTL <= Date.now(); } return false; } @@ -127,6 +146,17 @@ function getSiteHostname() { return pageInfo.hostname; } +function isOAndOTraffic() { + let referer = getRefererInfo().ref; + + if (referer) { + referer = parseUrl(referer).hostname; + const subDomains = referer.split('.'); + referer = subDomains.slice(subDomains.length - 2, subDomains.length).join('.'); + } + return O_AND_O_DOMAINS.indexOf(referer) >= 0; +} + /** @type {Submodule} */ export const connectIdSubmodule = { /** @@ -238,6 +268,13 @@ export const connectIdSubmodule = { responseObj.puid = params.puid || responseObj.puid; responseObj.lastSynced = Date.now(); responseObj.lastUsed = Date.now(); + if (isNumber(responseObj.ttl)) { + let validTTLMiliseconds = responseObj.ttl * 60 * 60 * 1000; + if (validTTLMiliseconds > VALID_ID_DURATION) { + validTTLMiliseconds = VALID_ID_DURATION; + } + responseObj.ttl = validTTLMiliseconds; + } storeObject(responseObj); } else { logError(`${MODULE_NAME} module: UPS response returned an invalid payload ${response}`); @@ -294,6 +331,12 @@ export const connectIdSubmodule = { */ getAjaxFn() { return ajax; + }, + eids: { + 'connectId': { + source: 'yahoo.com', + atype: 3 + }, } }; diff --git a/modules/connectadBidAdapter.js b/modules/connectadBidAdapter.js index d5665b318be..b40ef30f6bc 100644 --- a/modules/connectadBidAdapter.js +++ b/modules/connectadBidAdapter.js @@ -1,7 +1,9 @@ -import { deepSetValue, convertTypes, tryAppendQueryString, logWarn } from '../src/utils.js'; +import { deepSetValue, logWarn } from '../src/utils.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; import { BANNER } from '../src/mediaTypes.js' import {config} from '../src/config.js'; +import {tryAppendQueryString} from '../libraries/urlUtils/urlUtils.js'; +import {convertTypes} from '../libraries/transformParamsUtils/convertTypes.js'; const BIDDER_CODE = 'connectad'; const BIDDER_CODE_ALIAS = 'connectadrealtime'; const ENDPOINT_URL = 'https://i.connectad.io/api/v2'; diff --git a/modules/consentManagement.js b/modules/consentManagement.js index 6c22b3b9da4..05447a890cb 100644 --- a/modules/consentManagement.js +++ b/modules/consentManagement.js @@ -12,6 +12,7 @@ import {timedAuctionHook} from '../src/utils/perfMetrics.js'; import {registerOrtbProcessor, REQUEST} from '../src/pbjsORTB.js'; import {enrichFPD} from '../src/fpd/enrichment.js'; import {getGlobal} from '../src/prebidGlobal.js'; +import {cmpClient} from '../libraries/cmp/cmpClient.js'; const DEFAULT_CMP = 'iab'; const DEFAULT_CONSENT_TIMEOUT = 10000; @@ -48,36 +49,6 @@ function lookupStaticConsentData({onSuccess, onError}) { * @param {function(string, ...{}?)} cmpError acts as an error callback while interacting with CMP; pass along an error message (string) and any extra error arguments (purely for logging) */ function lookupIabConsent({onSuccess, onError, onEvent}) { - function findCMP() { - let f = window; - let cmpFrame; - let cmpFunction; - while (true) { - try { - if (typeof f.__tcfapi === 'function') { - cmpFunction = f.__tcfapi; - cmpFrame = f; - break; - } - } catch (e) { } - - // need separate try/catch blocks due to the exception errors thrown when trying to check for a frame that doesn't exist in 3rd party env - try { - if (f.frames['__tcfapiLocator']) { - cmpFrame = f; - break; - } - } catch (e) { } - - if (f === window.top) break; - f = f.parent; - } - return { - cmpFrame, - cmpFunction - }; - } - function cmpResponseCallback(tcfData, success) { logInfo('Received a response from CMP', tcfData); if (success) { @@ -90,70 +61,25 @@ function lookupIabConsent({onSuccess, onError, onEvent}) { } } - const cmpCallbacks = {}; - const { cmpFrame, cmpFunction } = findCMP(); + const cmp = cmpClient({ + apiName: '__tcfapi', + apiVersion: CMP_VERSION, + apiArgs: ['command', 'version', 'callback', 'parameter'], + }); - if (!cmpFrame) { + if (!cmp) { return onError('TCF2 CMP not found.'); } - // to collect the consent information from the user, we perform two calls to the CMP in parallel: - // first to collect the user's consent choices represented in an encoded string (via getConsentData) - // second to collect the user's full unparsed consent information (via getVendorConsents) - - // the following code also determines where the CMP is located and uses the proper workflow to communicate with it: - // check to see if CMP is found on the same window level as prebid and call it directly if so - // check to see if prebid is in a safeframe (with CMP support) - // else assume prebid may be inside an iframe and use the IAB CMP locator code to see if CMP's located in a higher parent window. this works in cross domain iframes - // if the CMP is not found, the iframe function will call the cmpError exit callback to abort the rest of the CMP workflow - - if (typeof cmpFunction === 'function') { + if (cmp.isDirect) { logInfo('Detected CMP API is directly accessible, calling it now...'); - cmpFunction('addEventListener', CMP_VERSION, cmpResponseCallback); } else { logInfo('Detected CMP is outside the current iframe where Prebid.js is located, calling it now...'); - callCmpWhileInIframe('addEventListener', cmpFrame, cmpResponseCallback); } - function callCmpWhileInIframe(commandName, cmpFrame, moduleCallback) { - const apiName = '__tcfapi'; - - const callName = `${apiName}Call`; - - /* Setup up a __cmp function to do the postMessage and stash the callback. - This function behaves (from the caller's perspective identicially to the in-frame __cmp call */ - window[apiName] = function (cmd, cmpVersion, callback, arg) { - const callId = Math.random() + ''; - const msg = { - [callName]: { - command: cmd, - version: cmpVersion, - parameter: arg, - callId: callId - } - }; - - cmpCallbacks[callId] = callback; - cmpFrame.postMessage(msg, '*'); - } - - /** when we get the return message, call the stashed callback */ - window.addEventListener('message', readPostMessageResponse, false); - - // call CMP - window[apiName](commandName, CMP_VERSION, moduleCallback); - - function readPostMessageResponse(event) { - const cmpDataPkgName = `${apiName}Return`; - const json = (typeof event.data === 'string' && includes(event.data, cmpDataPkgName)) ? JSON.parse(event.data) : event.data; - if (json[cmpDataPkgName] && json[cmpDataPkgName].callId) { - const payload = json[cmpDataPkgName]; - // TODO - clean up this logic (move listeners?); we have duplicate messages responses because 2 eventlisteners are active from the 2 cmp requests running in parallel - if (cmpCallbacks.hasOwnProperty(payload.callId)) { - cmpCallbacks[payload.callId](payload.returnValue, payload.success); - } - } - } - } + cmp({ + command: 'addEventListener', + callback: cmpResponseCallback + }) } /** diff --git a/modules/consentManagementGpp.js b/modules/consentManagementGpp.js index 3b0e1c25c6a..f696ce25902 100644 --- a/modules/consentManagementGpp.js +++ b/modules/consentManagementGpp.js @@ -4,180 +4,319 @@ * and make it available for any GPP supported adapters to read/pass this information to * their system and for various other features/modules in Prebid.js. */ -import {deepSetValue, isNumber, isPlainObject, isStr, logError, logInfo, logWarn} from '../src/utils.js'; +import {deepSetValue, isEmpty, isNumber, isPlainObject, isStr, logError, logInfo, logWarn} from '../src/utils.js'; import {config} from '../src/config.js'; import {gppDataHandler} from '../src/adapterManager.js'; -import {includes} from '../src/polyfill.js'; import {timedAuctionHook} from '../src/utils/perfMetrics.js'; -import { enrichFPD } from '../src/fpd/enrichment.js'; +import {enrichFPD} from '../src/fpd/enrichment.js'; import {getGlobal} from '../src/prebidGlobal.js'; +import {cmpClient, MODE_CALLBACK, MODE_MIXED, MODE_RETURN} from '../libraries/cmp/cmpClient.js'; +import {GreedyPromise} from '../src/utils/promise.js'; +import {buildActivityParams} from '../src/activities/params.js'; const DEFAULT_CMP = 'iab'; const DEFAULT_CONSENT_TIMEOUT = 10000; -const CMP_VERSION = 1; export let userCMP; export let consentTimeout; -export let staticConsentData; +let staticConsentData; let consentData; let addedConsentHook = false; -// add new CMPs here, with their dedicated lookup function -const cmpCallMap = { - 'iab': lookupIabConsent, - 'static': lookupStaticConsentData -}; - -/** - * This function checks the state of the IAB gppData's applicableSection field (to ensure it's populated and has a valid value). - * section === 0 represents a CMP's default value when CMP is loading, it shoud not be used a real user's section. - * - * TODO --- The initial version of the GPP CMP API spec used this naming convention, but it was later changed as an update to the spec. - * CMPs should adjust their logic to use the new format (applicableSecctions), but that may not be the case with the initial release. - * Added support just in case for this transition period, can likely be removed at a later date... - * @param gppData represents the IAB gppData object - * @returns true|false - */ -function checkApplicableSectionIsReady(gppData) { - return gppData && Array.isArray(gppData.applicableSection) && gppData.applicableSection.length > 0 && gppData.applicableSection[0] !== 0; +function pipeCallbacks(fn, {onSuccess, onError}) { + new GreedyPromise((resolve) => resolve(fn())).then(onSuccess, (err) => { + if (err instanceof GPPError) { + onError(err.message, ...err.args); + } else { + onError(`GPP error:`, err); + } + }); } -/** - * This function checks the state of the IAB gppData's applicableSections field (to ensure it's populated and has a valid value). - * section === 0 represents a CMP's default value when CMP is loading, it shoud not be used a real user's section. - * @param gppData represents the IAB gppData object - * @returns true|false - */ -function checkApplicableSectionsIsReady(gppData) { - return gppData && Array.isArray(gppData.applicableSections) && gppData.applicableSections.length > 0 && gppData.applicableSections[0] !== 0; +function lookupStaticConsentData(callbacks) { + return pipeCallbacks(() => processCmpData(staticConsentData), callbacks); } -/** - * This function reads the consent string from the config to obtain the consent information of the user. - * @param {function({})} onSuccess acts as a success callback when the value is read from config; pass along consentObject from CMP - */ -function lookupStaticConsentData({onSuccess, onError}) { - processCmpData(staticConsentData, {onSuccess, onError}); +const GPP_10 = '1.0'; +const GPP_11 = '1.1'; + +class GPPError { + constructor(message, arg) { + this.message = message; + this.args = arg == null ? [] : [arg]; + } } -/** - * This function handles interacting with an IAB compliant CMP to obtain the consent information of the user. - * Given the async nature of the CMP's API, we pass in acting success/error callback functions to exit this function - * based on the appropriate result. - * @param {function({})} onSuccess acts as a success callback when CMP returns a value; pass along consentObjectfrom CMP - * @param {function(string, ...{}?)} cmpError acts as an error callback while interacting with CMP; pass along an error message (string) and any extra error arguments (purely for logging) - */ -function lookupIabConsent({onSuccess, onError}) { - const cmpApiName = '__gpp'; - const cmpCallbacks = {}; - let registeredPostMessageResponseListener = false; - - function findCMP() { - let f = window; - let cmpFrame; - let cmpDirectAccess = false; - while (true) { - try { - if (typeof f[cmpApiName] === 'function') { - cmpFrame = f; - cmpDirectAccess = true; - break; - } - } catch (e) {} +export class GPPClient { + static CLIENTS = {}; - // need separate try/catch blocks due to the exception errors thrown when trying to check for a frame that doesn't exist in 3rd party env - try { - if (f.frames['__gppLocator']) { - cmpFrame = f; - break; - } - } catch (e) {} + static register(apiVersion, defaultVersion = false) { + this.apiVersion = apiVersion; + this.CLIENTS[apiVersion] = this; + if (defaultVersion) { + this.CLIENTS.default = this; + } + } - if (f === window.top) break; - f = f.parent; + static INST; + + /** + * Ping the CMP to set up an appropriate client for it, and initialize it. + * + * @param mkCmp + * @returns {Promise<[GPPClient,Promise<{}>]>} a promise to two objects: + * - a GPPClient that talks the best GPP dialect we know for the CMP's version; + * - a promise to GPP data. + */ + static init(mkCmp = cmpClient) { + let inst = this.INST; + if (!inst) { + let err; + const reset = () => err && (this.INST = null); + inst = this.INST = this.ping(mkCmp).catch(e => { + err = true; + reset(); + throw e; + }); + reset(); } + return inst.then(([client, pingData]) => [ + client, + client.initialized ? client.refresh() : client.init(pingData) + ]); + } - return { - cmpFrame, - cmpDirectAccess + /** + * Ping the CMP to determine its version and set up a client appropriate for it. + * + * @param mkCmp + * @returns {Promise<[GPPClient, {}]>} a promise to two objects: + * - a GPPClient that talks the best GPP dialect we know for the CMP's version; + * - the result from pinging the CMP. + */ + static ping(mkCmp = cmpClient) { + const cmpOptions = { + apiName: '__gpp', + apiArgs: ['command', 'callback', 'parameter'], // do not pass version - not clear what it's for (or what we should use) }; - } - const {cmpFrame, cmpDirectAccess} = findCMP(); + // in 1.0, 'ping' should return pingData but ignore callback; + // in 1.1 it should not return anything but run the callback + // the following looks for either - but once the version is known, produce a client that knows whether the + // rest of the interactions should pick return values or pass callbacks - if (!cmpFrame) { - return onError('GPP CMP not found.'); + const probe = mkCmp({...cmpOptions, mode: MODE_RETURN}); + return new GreedyPromise((resolve, reject) => { + if (probe == null) { + reject(new GPPError('GPP CMP not found')); + return; + } + let done = false; // some CMPs do both return value and callbacks - avoid repeating log messages + const pong = (result, success) => { + if (done) return; + if (success != null && !success) { + reject(result); + return; + } + if (result == null) return; + done = true; + const cmpVersion = result?.gppVersion; + const Client = this.getClient(cmpVersion); + if (cmpVersion !== Client.apiVersion) { + logWarn(`Unrecognized GPP CMP version: ${cmpVersion}. Continuing using GPP API version ${Client}...`); + } else { + logInfo(`Using GPP version ${cmpVersion}`); + } + const mode = Client.apiVersion === GPP_10 ? MODE_MIXED : MODE_CALLBACK; + const client = new Client( + cmpVersion, + mkCmp({...cmpOptions, mode}) + ); + resolve([client, result]); + }; + + probe({ + command: 'ping', + callback: pong + }).then((res) => pong(res, true), reject); + }).finally(() => { + probe && probe.close(); + }); } - const invokeCMP = (cmpDirectAccess) ? invokeCMPDirect : invokeCMPFrame; + static getClient(cmpVersion) { + return this.CLIENTS.hasOwnProperty(cmpVersion) ? this.CLIENTS[cmpVersion] : this.CLIENTS.default; + } - function invokeCMPDirect({command, callback, parameter, version = CMP_VERSION}, resultCb) { - if (typeof resultCb === 'function') { - resultCb(cmpFrame[cmpApiName](command, callback, parameter, version)); - } else { - cmpFrame[cmpApiName](command, callback, parameter, version); - } + #resolve; + #reject; + #pending = []; + + initialized = false; + + constructor(cmpVersion, cmp) { + this.apiVersion = this.constructor.apiVersion; + this.cmpVersion = cmp; + this.cmp = cmp; + [this.#resolve, this.#reject] = [0, 1].map(slot => (result) => { + while (this.#pending.length) { + this.#pending.pop()[slot](result); + } + }); } - function invokeCMPFrame({command, callback, parameter, version = CMP_VERSION}, resultCb) { - const callName = `${cmpApiName}Call`; - if (!registeredPostMessageResponseListener) { - // when we get the return message, call the stashed callback; - window.addEventListener('message', readPostMessageResponse, false); - registeredPostMessageResponseListener = true; + /** + * initialize this client - update consent data if already available, + * and set up event listeners to also update on CMP changes + * + * @param pingData + * @returns {Promise<{}>} a promise to GPP consent data + */ + init(pingData) { + const ready = this.updateWhenReady(pingData); + if (!this.initialized) { + this.initialized = true; + this.cmp({ + command: 'addEventListener', + callback: (event, success) => { + if (success != null && !success) { + this.#reject(new GPPError('Received error response from CMP', event)); + } else if (event?.pingData?.cmpStatus === 'error') { + this.#reject(new GPPError('CMP status is "error"; please check CMP setup', event)); + } else if (this.isCMPReady(event?.pingData || {}) && this.events.includes(event?.eventName)) { + this.#resolve(this.updateConsent(event.pingData)); + } + } + }); } + return ready; + } - // call CMP via postMessage - const callId = Math.random().toString(); - const msg = { - [callName]: { - command: command, - parameter, - version, - callId: callId + refresh() { + return this.cmp({command: 'ping'}).then(this.updateWhenReady.bind(this)); + } + + /** + * Retrieve and store GPP consent data. + * + * @param pingData + * @returns {Promise<{}>} a promise to GPP consent data + */ + updateConsent(pingData) { + return this.getGPPData(pingData).then((data) => { + if (data == null || isEmpty(data)) { + throw new GPPError('Received empty response from CMP', data); } - }; + return processCmpData(data); + }).then((data) => { + logInfo('Retrieved GPP consent from CMP:', data); + return data; + }); + } - // TODO? - add logic to check if random was already used in the same session, and roll another if so? - cmpCallbacks[callId] = (typeof callback === 'function') ? callback : resultCb; - cmpFrame.postMessage(msg, '*'); + /** + * Return a promise to GPP consent data, to be retrieved the next time the CMP signals it's ready. + * + * @returns {Promise<{}>} + */ + nextUpdate() { + return new GreedyPromise((resolve, reject) => { + this.#pending.push([resolve, reject]); + }); + } - function readPostMessageResponse(event) { - const cmpDataPkgName = `${cmpApiName}Return`; - const json = (typeof event.data === 'string' && event.data.includes(cmpDataPkgName)) ? JSON.parse(event.data) : event.data; - if (json[cmpDataPkgName] && json[cmpDataPkgName].callId) { - const payload = json[cmpDataPkgName]; + /** + * Return a promise to GPP consent data, to be retrieved immediately if the CMP is ready according to `pingData`, + * or as soon as it signals that it's ready otherwise. + * + * @param pingData + * @returns {Promise<{}>} + */ + updateWhenReady(pingData) { + return this.isCMPReady(pingData) ? this.updateConsent(pingData) : this.nextUpdate(); + } +} - if (cmpCallbacks.hasOwnProperty(payload.callId)) { - cmpCallbacks[payload.callId](payload.returnValue); - } - } - } +// eslint-disable-next-line no-unused-vars +class GPP10Client extends GPPClient { + static { + super.register(GPP_10); } - const startupMsg = (cmpDirectAccess) ? 'Detected GPP CMP API is directly accessible, calling it now...' - : 'Detected GPP CMP is outside the current iframe where Prebid.js is located, calling it now...'; - logInfo(startupMsg); - - invokeCMP({ - command: 'addEventListener', - callback: function (evt) { - if (evt) { - logInfo(`Received a ${(cmpDirectAccess ? 'direct' : 'postmsg')} response from GPP CMP for event`, evt); - if (evt.eventName === 'sectionChange' || evt.pingData.cmpStatus === 'loaded') { - invokeCMP({command: 'getGPPData'}, function (gppData) { - logInfo(`Received a ${cmpDirectAccess ? 'direct' : 'postmsg'} response from GPP CMP for getGPPData`, gppData); - processCmpData(gppData, {onSuccess, onError}); - }); - } else if (evt.pingData.cmpStatus === 'error') { - onError('CMP returned with a cmpStatus:error response. Please check CMP setup.'); - } - } - } - }); + events = ['sectionChange', 'cmpStatus']; + + isCMPReady(pingData) { + return pingData.cmpStatus === 'loaded'; + } + + getGPPData(pingData) { + const parsedSections = GreedyPromise.all( + (pingData.supportedAPIs || pingData.apiSupport || []).map((api) => this.cmp({ + command: 'getSection', + parameter: api + }).catch(err => { + logWarn(`Could not retrieve GPP section '${api}'`, err); + }).then((section) => [api, section])) + ).then(sections => { + // parse single section object into [core, gpc] to uniformize with 1.1 parsedSections + return Object.fromEntries( + sections.filter(([_, val]) => val != null) + .map(([api, section]) => { + const subsections = [ + Object.fromEntries(Object.entries(section).filter(([k]) => k !== 'Gpc')) + ]; + if (section.Gpc != null) { + subsections.push({ + SubsectionType: 1, + Gpc: section.Gpc + }); + } + return [api, subsections]; + }) + ); + }); + return GreedyPromise.all([ + this.cmp({command: 'getGPPData'}), + parsedSections + ]).then(([gppData, parsedSections]) => Object.assign({}, gppData, {parsedSections})); + } +} + +// eslint-disable-next-line no-unused-vars +class GPP11Client extends GPPClient { + static { + super.register(GPP_11, true); + } + + events = ['sectionChange', 'signalStatus']; + + isCMPReady(pingData) { + return pingData.signalStatus === 'ready'; + } + + getGPPData(pingData) { + return GreedyPromise.resolve(pingData); + } } +/** + * This function handles interacting with an IAB compliant CMP to obtain the consent information of the user. + * Given the async nature of the CMP's API, we pass in acting success/error callback functions to exit this function + * based on the appropriate result. + * @param {function({})} onSuccess acts as a success callback when CMP returns a value; pass along consentObjectfrom CMP + * @param {function(string, ...{}?)} cmpError acts as an error callback while interacting with CMP; pass along an error message (string) and any extra error arguments (purely for logging) + */ +export function lookupIabConsent({onSuccess, onError}, mkCmp = cmpClient) { + pipeCallbacks(() => GPPClient.init(mkCmp).then(([client, gppDataPm]) => gppDataPm), {onSuccess, onError}); +} + +// add new CMPs here, with their dedicated lookup function +const cmpCallMap = { + 'iab': lookupIabConsent, + 'static': lookupStaticConsentData +}; + /** * Look up consent data and store it in the `consentData` global as well as `adapterManager.js`' gdprDataHandler. * @@ -199,7 +338,7 @@ function loadConsentData(cb) { } } - if (!includes(Object.keys(cmpCallMap), userCMP)) { + if (!cmpCallMap.hasOwnProperty(userCMP)) { done(null, false, `GPP CMP framework (${userCMP}) is not a supported framework. Aborting consentManagement module and resuming auction.`); return; } @@ -209,19 +348,19 @@ function loadConsentData(cb) { onError: function (msg, ...extraArgs) { done(null, true, msg, ...extraArgs); } - } + }; cmpCallMap[userCMP](callbacks); if (!isDone) { const onTimeout = () => { const continueToAuction = (data) => { done(data, false, 'GPP CMP did not load, continuing auction...'); - } - processCmpData(consentData, { + }; + pipeCallbacks(() => processCmpData(consentData), { onSuccess: continueToAuction, - onError: () => continueToAuction(storeConsentData(undefined)) - }) - } + onError: () => continueToAuction(storeConsentData()) + }); + }; if (consentTimeout === 0) { onTimeout(); } else { @@ -276,43 +415,30 @@ export const requestBidsHook = timedAuctionHook('gpp', function requestBidsHook( }); }); -/** - * This function checks the consent data provided by CMP to ensure it's in an expected state. - * If it's bad, we call `onError` - * If it's good, then we store the value and call `onSuccess` - */ -function processCmpData(consentObject, {onSuccess, onError}) { - function checkData() { - const gppString = consentObject && consentObject.gppString; - const gppSection = (checkApplicableSectionsIsReady(consentObject)) ? consentObject.applicableSections - : (checkApplicableSectionIsReady(consentObject)) ? consentObject.applicableSection : []; - - return !!( - (!Array.isArray(gppSection)) || - (Array.isArray(gppSection) && (!gppString || !isStr(gppString))) - ); - } - - if (checkData()) { - onError(`CMP returned unexpected value during lookup process.`, consentObject); - } else { - onSuccess(storeConsentData(consentObject)); +function processCmpData(consentData) { + if ( + (consentData?.applicableSections != null && !Array.isArray(consentData.applicableSections)) || + (consentData?.gppString != null && !isStr(consentData.gppString)) || + (consentData?.parsedSections != null && !isPlainObject(consentData.parsedSections)) + ) { + throw new GPPError('CMP returned unexpected value during lookup process.', consentData); } + return storeConsentData(consentData); } /** * Stores CMP data locally in module to make information available in adaptermanager.js for later in the auction - * @param {object} cmpConsentObject required; an object representing user's consent choices (can be undefined in certain use-cases for this function only) + * @param {{}} gppData the result of calling a CMP's `getGPPData` (or equivalent) + * @param {{}} sectionData map from GPP section name to the result of calling a CMP's `getSection` (or equivalent) */ -function storeConsentData(cmpConsentObject) { +export function storeConsentData(gppData = {}) { consentData = { - gppString: (cmpConsentObject) ? cmpConsentObject.gppString : undefined, - - fullGppData: (cmpConsentObject) || undefined, + gppString: gppData?.gppString, + applicableSections: gppData?.applicableSections || [], + parsedSections: gppData?.parsedSections || {}, + gppData: gppData }; - consentData.applicableSections = (checkApplicableSectionsIsReady(cmpConsentObject)) ? cmpConsentObject.applicableSections - : (checkApplicableSectionIsReady(cmpConsentObject)) ? cmpConsentObject.applicableSection : []; - consentData.apiVersion = CMP_VERSION; + gppDataHandler.setConsentData(gppData); return consentData; } @@ -324,6 +450,7 @@ export function resetConsentData() { userCMP = undefined; consentTimeout = undefined; gppDataHandler.reset(); + GPPClient.INST = null; } /** @@ -364,11 +491,15 @@ export function setConsentConfig(config) { if (!addedConsentHook) { getGlobal().requestBids.before(requestBidsHook, 50); + buildActivityParams.before((next, params) => { + return next(Object.assign({gppConsent: gppDataHandler.getConsentData()}, params)); + }); } addedConsentHook = true; gppDataHandler.enable(); loadConsentData(); // immediately look up consent data to make it available without requiring an auction } + config.getConfig('consentManagement', config => setConsentConfig(config.consentManagement)); export function enrichFPDHook(next, fpd) { diff --git a/modules/consentManagementUsp.js b/modules/consentManagementUsp.js index fcc13152aa9..1218c3724f4 100644 --- a/modules/consentManagementUsp.js +++ b/modules/consentManagementUsp.js @@ -4,12 +4,13 @@ * information and make it available for any USP (CCPA) supported adapters to * read/pass this information to their system. */ -import {deepSetValue, isFn, isNumber, isPlainObject, isStr, logError, logInfo, logWarn} from '../src/utils.js'; +import {deepSetValue, isNumber, isPlainObject, isStr, logError, logInfo, logWarn} from '../src/utils.js'; import {config} from '../src/config.js'; import adapterManager, {uspDataHandler} from '../src/adapterManager.js'; import {timedAuctionHook} from '../src/utils/perfMetrics.js'; import {getHook} from '../src/hook.js'; import {enrichFPD} from '../src/fpd/enrichment.js'; +import {cmpClient} from '../libraries/cmp/cmpClient.js'; const DEFAULT_CONSENT_API = 'iab'; const DEFAULT_CONSENT_TIMEOUT = 50; @@ -41,35 +42,6 @@ function lookupStaticConsentData({onSuccess, onError}) { * based on the appropriate result. */ function lookupUspConsent({onSuccess, onError}) { - function findUsp() { - let f = window; - let uspapiFrame; - let uspapiFunction; - - while (true) { - try { - if (typeof f.__uspapi === 'function') { - uspapiFunction = f.__uspapi; - uspapiFrame = f; - break; - } - } catch (e) {} - - try { - if (f.frames['__uspapiLocator']) { - uspapiFrame = f; - break; - } - } catch (e) {} - if (f === window.top) break; - f = f.parent; - } - return { - uspapiFrame, - uspapiFunction, - }; - } - function handleUspApiResponseCallbacks() { const uspResponse = {}; @@ -92,86 +64,36 @@ function lookupUspConsent({onSuccess, onError}) { } let callbackHandler = handleUspApiResponseCallbacks(); - let uspapiCallbacks = {}; - let { uspapiFrame, uspapiFunction } = findUsp(); + const cmp = cmpClient({ + apiName: '__uspapi', + apiVersion: USPAPI_VERSION, + apiArgs: ['command', 'version', 'callback'], + }); - if (!uspapiFrame) { + if (!cmp) { return onError('USP CMP not found.'); } - function registerDataDelHandler(invoker, arg2) { - try { - invoker('registerDeletion', arg2, adapterManager.callDataDeletionRequest); - } catch (e) { - logError('Error invoking CMP `registerDeletion`:', e); - } - } - - // to collect the consent information from the user, we perform a call to USPAPI - // to collect the user's consent choices represented as a string (via getUSPData) - - // the following code also determines where the USPAPI is located and uses the proper workflow to communicate with it: - // - use the USPAPI locator code to see if USP's located in the current window or an ancestor window. - // - else assume prebid is in an iframe, and use the locator to see if the CMP is located in a higher parent window. This works in cross domain iframes. - // - if USPAPI is not found, the iframe function will call the uspError exit callback to abort the rest of the USPAPI workflow - - if (isFn(uspapiFunction)) { + if (cmp.isDirect) { logInfo('Detected USP CMP is directly accessible, calling it now...'); - uspapiFunction( - 'getUSPData', - USPAPI_VERSION, - callbackHandler.consentDataCallback - ); - registerDataDelHandler(uspapiFunction, USPAPI_VERSION); } else { logInfo( 'Detected USP CMP is outside the current iframe where Prebid.js is located, calling it now...' ); - callUspApiWhileInIframe( - 'getUSPData', - uspapiFrame, - callbackHandler.consentDataCallback - ); - registerDataDelHandler(callUspApiWhileInIframe, uspapiFrame); } - let listening = false; - - function callUspApiWhileInIframe(commandName, uspapiFrame, moduleCallback) { - function callUsp(cmd, ver, callback) { - let callId = Math.random() + ''; - let msg = { - __uspapiCall: { - command: cmd, - version: ver, - callId: callId, - }, - }; - - uspapiCallbacks[callId] = callback; - uspapiFrame.postMessage(msg, '*'); - }; - - /** when we get the return message, call the stashed callback */ - if (!listening) { - window.addEventListener('message', readPostMessageResponse, false); - listening = true; - } - - // call uspapi - callUsp(commandName, USPAPI_VERSION, moduleCallback); + cmp({ + command: 'getUSPData', + callback: callbackHandler.consentDataCallback + }); - function readPostMessageResponse(event) { - const res = event && event.data && event.data.__uspapiReturn; - if (res && res.callId) { - if (uspapiCallbacks.hasOwnProperty(res.callId)) { - uspapiCallbacks[res.callId](res.returnValue, res.success); - delete uspapiCallbacks[res.callId]; - } - } - } - } + cmp({ + command: 'registerDeletion', + callback: (res, success) => (success == null || success) && adapterManager.callDataDeletionRequest(res) + }).catch(e => { + logError('Error invoking CMP `registerDeletion`:', e); + }); } /** diff --git a/modules/consumableBidAdapter.js b/modules/consumableBidAdapter.js index 4e2a92fb594..30b081e53d3 100644 --- a/modules/consumableBidAdapter.js +++ b/modules/consumableBidAdapter.js @@ -3,6 +3,12 @@ import {config} from '../src/config.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; import { BANNER, VIDEO } from '../src/mediaTypes.js'; +/** + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid + * @typedef {import('../src/adapters/bidderFactory.js').validBidRequests} validBidRequests + */ + const BIDDER_CODE = 'consumable'; const BASE_URI = 'https://e.serverbid.com/api/v2'; @@ -65,6 +71,11 @@ export const spec = { }; } + if (bidderRequest && bidderRequest.gppConsent && bidderRequest.gppConsent.gppString) { + data.gpp = bidderRequest.gppConsent.gppString; + data.gpp_sid = bidderRequest.gppConsent.applicableSections; + } + if (bidderRequest && bidderRequest.uspConsent) { data.ccpa = bidderRequest.uspConsent; } @@ -180,20 +191,26 @@ export const spec = { return bidResponses; }, - getUserSyncs: function(syncOptions, serverResponses, gdprConsent, uspConsent) { + getUserSyncs: function(syncOptions, serverResponses, gdprConsent, uspConsent, gppConsent) { let syncUrl = 'https://sync.serverbid.com/ss/' + siteId + '.html'; if (syncOptions.iframeEnabled) { if (gdprConsent && gdprConsent.consentString) { if (typeof gdprConsent.gdprApplies === 'boolean') { - syncUrl = appendUrlParam(syncUrl, `gdpr=${Number(gdprConsent.gdprApplies)}&gdpr_consent=${gdprConsent.consentString}`); + syncUrl = appendUrlParam(syncUrl, `gdpr=${Number(gdprConsent.gdprApplies)}&gdpr_consent=${encodeURIComponent(gdprConsent.consentString) || ''}`); } else { - syncUrl = appendUrlParam(syncUrl, `gdpr=0&gdpr_consent=${gdprConsent.consentString}`); + syncUrl = appendUrlParam(syncUrl, `gdpr=0&gdpr_consent=${encodeURIComponent(gdprConsent.consentString) || ''}`); + } + } + if (gppConsent && gppConsent.gppString) { + syncUrl = appendUrlParam(syncUrl, `gpp=${encodeURIComponent(gppConsent.gppString)}`); + if (gppConsent.applicableSections && gppConsent.applicableSections.length > 0) { + syncUrl = appendUrlParam(syncUrl, `gpp_sid=${encodeURIComponent(gppConsent.applicableSections.join(','))}`); } } - if (uspConsent && uspConsent.consentString) { - syncUrl = appendUrlParam(syncUrl, `us_privacy=${uspConsent.consentString}`); + if (uspConsent) { + syncUrl = appendUrlParam(syncUrl, `us_privacy=${encodeURIComponent(uspConsent)}`); } if (!serverResponses || serverResponses.length === 0 || !serverResponses[0].body.bdr || serverResponses[0].body.bdr !== 'cx') { diff --git a/modules/contxtfulRtdProvider.js b/modules/contxtfulRtdProvider.js new file mode 100644 index 00000000000..6d4b2a2ce29 --- /dev/null +++ b/modules/contxtfulRtdProvider.js @@ -0,0 +1,150 @@ +/** + * Contxtful Technologies Inc + * This RTD module provides receptivity feature that can be accessed using the + * getReceptivity() function. The value returned by this function enriches the ad-units + * that are passed within the `getTargetingData` functions and GAM. + */ + +import { submodule } from '../src/hook.js'; +import { + logInfo, + logError, + isStr, + isEmptyStr, + buildUrl, +} from '../src/utils.js'; +import { loadExternalScript } from '../src/adloader.js'; + +const MODULE_NAME = 'contxtful'; +const MODULE = `${MODULE_NAME}RtdProvider`; + +const CONTXTFUL_RECEPTIVITY_DOMAIN = 'api.receptivity.io'; + +let initialReceptivity = null; +let contxtfulModule = null; + +/** + * Init function used to start sub module + * @param { { params: { version: String, customer: String, hostname: String } } } config + * @return { Boolean } + */ +function init(config) { + logInfo(MODULE, 'init', config); + initialReceptivity = null; + contxtfulModule = null; + + try { + const {version, customer, hostname} = extractParameters(config); + initCustomer(version, customer, hostname); + return true; + } catch (error) { + logError(MODULE, error); + return false; + } +} + +/** + * Extract required configuration for the sub module. + * validate that all required configuration are present and are valid. + * Throws an error if any config is missing of invalid. + * @param { { params: { version: String, customer: String, hostname: String } } } config + * @return { { version: String, customer: String, hostname: String } } + * @throws params.{name} should be a non-empty string + */ +function extractParameters(config) { + const version = config?.params?.version; + if (!isStr(version) || isEmptyStr(version)) { + throw Error(`${MODULE}: params.version should be a non-empty string`); + } + + const customer = config?.params?.customer; + if (!isStr(customer) || isEmptyStr(customer)) { + throw Error(`${MODULE}: params.customer should be a non-empty string`); + } + + const hostname = config?.params?.hostname || CONTXTFUL_RECEPTIVITY_DOMAIN; + + return {version, customer, hostname}; +} + +/** + * Initialize sub module for a customer. + * This will load the external resources for the sub module. + * @param { String } version + * @param { String } customer + * @param { String } hostname + */ +function initCustomer(version, customer, hostname) { + const CONNECTOR_URL = buildUrl({ + protocol: 'https', + host: hostname, + pathname: `/${version}/prebid/${customer}/connector/p.js`, + }); + + const externalScript = loadExternalScript(CONNECTOR_URL, MODULE_NAME); + addExternalScriptEventListener(externalScript); +} + +/** + * Add event listener to the script tag for the expected events from the external script. + * @param { HTMLScriptElement } script + */ +function addExternalScriptEventListener(script) { + if (!script) { + return; + } + + script.addEventListener('initialReceptivity', ({ detail }) => { + let receptivityState = detail?.ReceptivityState; + if (isStr(receptivityState) && !isEmptyStr(receptivityState)) { + initialReceptivity = receptivityState; + } + }); + + script.addEventListener('rxEngineIsReady', ({ detail: api }) => { + contxtfulModule = api; + }); +} + +/** + * Return current receptivity. + * @return { { ReceptivityState: String } } + */ +function getReceptivity() { + return { + ReceptivityState: contxtfulModule?.GetReceptivity()?.ReceptivityState || initialReceptivity + }; +} + +/** + * Set targeting data for ad server + * @param { [String] } adUnits + * @param {*} _config + * @param {*} _userConsent + * @return {{ code: { ReceptivityState: String } }} + */ +function getTargetingData(adUnits, _config, _userConsent) { + logInfo(MODULE, 'getTargetingData'); + if (!adUnits) { + return {}; + } + + const receptivity = getReceptivity(); + if (!receptivity?.ReceptivityState) { + return {}; + } + + return adUnits.reduce((targets, code) => { + targets[code] = receptivity; + return targets; + }, {}); +} + +export const contxtfulSubmodule = { + name: MODULE_NAME, + init, + extractParameters, + getTargetingData, +}; + +submodule('realTimeData', contxtfulSubmodule); diff --git a/modules/contxtfulRtdProvider.md b/modules/contxtfulRtdProvider.md new file mode 100644 index 00000000000..dfefca2067a --- /dev/null +++ b/modules/contxtfulRtdProvider.md @@ -0,0 +1,65 @@ +# Overview + +**Module Name:** Contxtful RTD Provider +**Module Type:** RTD Provider +**Maintainer:** [prebid@contxtful.com](mailto:prebid@contxtful.com) + +# Description + +The Contxtful RTD module offers a unique feature—Receptivity. Receptivity is an efficiency metric, enabling the qualification of any instant in a session in real time based on attention. The core idea is straightforward: the likelihood of an ad’s success increases when it grabs attention and is presented in the right context at the right time. + +To utilize this module, you need to register for an account with [Contxtful](https://contxtful.com). For inquiries, please contact [prebid@contxtful.com](mailto:prebid@contxtful.com). + +# Configuration + +## Build Instructions + +To incorporate this module into your `prebid.js`, compile the module using the following command: + +```sh +gulp build --modules=contxtfulRtdProvider, +``` + +## Module Configuration + +Configure the `contxtfulRtdProvider` by passing the required settings through the `setConfig` function in `prebid.js`. + +```js +import pbjs from 'prebid.js'; + +pbjs.setConfig({ + "realTimeData": { + "auctionDelay": 1000, + "dataProviders": [ + { + "name": "contxtful", + "waitForIt": true, + "params": { + "version": "", + "customer": "" + } + } + ] + } +}); +``` + +### Configuration Parameters + +| Name | Type | Scope | Description | +|------------|----------|----------|-------------------------------------------| +| `version` | `string` | Required | Specifies the API version of Contxtful. | +| `customer` | `string` | Required | Your unique customer identifier. | + +# Usage + +The `contxtfulRtdProvider` module loads an external JavaScript file and authenticates with Contxtful APIs. The `getTargetingData` function then adds a `ReceptivityState` to each ad slot, which can have one of two values: `Receptive` or `NonReceptive`. + +```json +{ + "adUnitCode1": { "ReceptivityState": "Receptive" }, + "adUnitCode2": { "ReceptivityState": "NonReceptive" } +} +``` + +This module also integrates seamlessly with Google Ad Manager, ensuring that the `ReceptivityState` is available as early as possible in the ad serving process. \ No newline at end of file diff --git a/modules/conversantBidAdapter.js b/modules/conversantBidAdapter.js index fd436e51461..dffc1a9f33b 100644 --- a/modules/conversantBidAdapter.js +++ b/modules/conversantBidAdapter.js @@ -1,33 +1,125 @@ import { - logWarn, - isStr, + buildUrl, deepAccess, - isArray, - getBidIdParameter, deepSetValue, - isEmpty, - _each, - convertTypes, - parseUrl, - mergeDeep, - buildUrl, - _map, - logError, + getBidIdParameter, + isArray, isFn, isPlainObject, + isStr, + logError, + logWarn, + mergeDeep, + parseUrl, } from '../src/utils.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import {BANNER, VIDEO} from '../src/mediaTypes.js'; import {getStorageManager} from '../src/storageManager.js'; +import {convertTypes} from '../libraries/transformParamsUtils/convertTypes.js'; +import {ortbConverter} from '../libraries/ortbConverter/converter.js'; +import {ORTB_MTYPES} from '../libraries/ortbConverter/processors/mediaType.js'; // Maintainer: mediapsr@epsilon.com +/** + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid + * @typedef {import('../src/adapters/bidderFactory.js').ServerRequest} ServerRequest + * @typedef {import('../src/adapters/bidderFactory.js').Device} Device + */ + const GVLID = 24; const BIDDER_CODE = 'conversant'; export const storage = getStorageManager({gvlid: GVLID, bidderCode: BIDDER_CODE}); const URL = 'https://web.hb.ad.cpe.dotomi.com/cvx/client/hb/ortb/25'; +function setSiteId(bidRequest, request) { + if (bidRequest.params.site_id) { + if (request.site) { + request.site.id = bidRequest.params.site_id; + } + if (request.app) { + request.app.id = bidRequest.params.site_id; + } + } +} + +function setPubcid(bidRequest, request) { + // Add common id if available + const pubcid = getPubcid(bidRequest); + if (pubcid) { + deepSetValue(request, 'user.ext.fpc', pubcid); + } +} + +const converter = ortbConverter({ + context: { + netRevenue: true, + ttl: 300 + }, + request: function (buildRequest, imps, bidderRequest, context) { + const request = buildRequest(imps, bidderRequest, context); + request.at = 1; + if (context.bidRequests) { + const bidRequest = context.bidRequests[0]; + setSiteId(bidRequest, request); + setPubcid(bidRequest, request); + } + + return request; + }, + imp(buildImp, bidRequest, context) { + const imp = buildImp(bidRequest, context); + const data = { + secure: 1, + bidfloor: getBidFloor(bidRequest) || 0, + displaymanager: 'Prebid.js', + displaymanagerver: '$prebid.version$' + }; + copyOptProperty(bidRequest.params.tag_id, data, 'tagid'); + mergeDeep(imp, data, imp); + return imp; + }, + bidResponse: function (buildBidResponse, bid, context) { + if (!bid.price) return; + + // ensure that context.mediaType is set to banner or video otherwise + if (!context.mediaType && context.bidRequest.mediaTypes) { + const [type] = Object.keys(context.bidRequest.mediaTypes); + if (Object.values(ORTB_MTYPES).includes(type)) { + context.mediaType = type; + } + } + const bidResponse = buildBidResponse(bid, context); + return bidResponse; + }, + + overrides: { + imp: { + banner(fillBannerImp, imp, bidRequest, context) { + if (bidRequest.mediaTypes && !bidRequest.mediaTypes.banner) return; + if (bidRequest.params.position) { + // fillBannerImp looks for mediaTypes.banner.pos so put it under the right name here + mergeDeep(bidRequest, {mediaTypes: {banner: {pos: bidRequest.params.position}}}); + } + fillBannerImp(imp, bidRequest, context); + }, + video(fillVideoImp, imp, bidRequest, context) { + if (bidRequest.mediaTypes && !bidRequest.mediaTypes.video) return; + const videoData = {}; + copyOptProperty(bidRequest.params?.position, videoData, 'pos'); + copyOptProperty(bidRequest.params?.mimes, videoData, 'mimes'); + copyOptProperty(bidRequest.params?.maxduration, videoData, 'maxduration'); + copyOptProperty(bidRequest.params?.protocols, videoData, 'protocols'); + copyOptProperty(bidRequest.params?.api, videoData, 'api'); + imp.video = mergeDeep(videoData, imp.video); + fillVideoImp(imp, bidRequest, context); + } + }, + } +}); + export const spec = { code: BIDDER_CODE, gvlid: GVLID, @@ -65,148 +157,14 @@ export const spec = { return true; }, - /** - * Make a server request from the list of BidRequests. - * - * @param {BidRequest[]} validBidRequests - an array of bids - * @param bidderRequest - * @return {ServerRequest} Info describing the request to the server. - */ - buildRequests: function(validBidRequests, bidderRequest) { - const page = (bidderRequest && bidderRequest.refererInfo) ? bidderRequest.refererInfo.page : ''; - let siteId = ''; - let pubcid = null; - let pubcidName = '_pubcid'; - let bidurl = URL; - - const conversantImps = validBidRequests.map(function(bid) { - const bidfloor = getBidFloor(bid); - - siteId = getBidIdParameter('site_id', bid.params) || siteId; - pubcidName = getBidIdParameter('pubcid_name', bid.params) || pubcidName; - - const imp = { - id: bid.bidId, - secure: 1, - bidfloor: bidfloor || 0, - displaymanager: 'Prebid.js', - displaymanagerver: '$prebid.version$' - }; - if (bid.ortb2Imp) { - mergeDeep(imp, bid.ortb2Imp); - } - - copyOptProperty(bid.params.tag_id, imp, 'tagid'); - - if (isVideoRequest(bid)) { - const videoData = deepAccess(bid, 'mediaTypes.video') || {}; - const format = convertSizes(videoData.playerSize || bid.sizes); - const video = {}; - - if (format && format[0]) { - copyOptProperty(format[0].w, video, 'w'); - copyOptProperty(format[0].h, video, 'h'); - } - - copyOptProperty(bid.params.position || videoData.pos, video, 'pos'); - copyOptProperty(bid.params.mimes || videoData.mimes, video, 'mimes'); - copyOptProperty(bid.params.maxduration || videoData.maxduration, video, 'maxduration'); - copyOptProperty(bid.params.protocols || videoData.protocols, video, 'protocols'); - copyOptProperty(bid.params.api || videoData.api, video, 'api'); - - imp.video = video; - } else { - const bannerData = deepAccess(bid, 'mediaTypes.banner') || {}; - const format = convertSizes(bannerData.sizes || bid.sizes); - const banner = {format: format}; - - copyOptProperty(bid.params.position || bannerData.pos, banner, 'pos'); - - imp.banner = banner; - } - - if (bid.userId && bid.userId.pubcid) { - pubcid = bid.userId.pubcid; - } else if (bid.crumbs && bid.crumbs.pubcid) { - pubcid = bid.crumbs.pubcid; - } - if (bid.params.white_label_url) { - bidurl = bid.params.white_label_url; - } - - return imp; - }); - - const payload = { - id: bidderRequest.bidderRequestId, - imp: conversantImps, - source: { - tid: bidderRequest.ortb2?.source?.tid, - }, - site: { - id: siteId, - mobile: document.querySelector('meta[name="viewport"][content*="width=device-width"]') !== null ? 1 : 0, - page: page - }, - device: getDevice(), - at: 1 - }; - - let userExt = {}; - - // pass schain object if it is present - const schain = deepAccess(validBidRequests, '0.schain'); - if (schain) { - deepSetValue(payload, 'source.ext.schain', schain); - } - - if (bidderRequest) { - if (bidderRequest.timeout) { - deepSetValue(payload, 'tmax', bidderRequest.timeout); - } - - // Add GDPR flag and consent string - if (bidderRequest.gdprConsent) { - userExt.consent = bidderRequest.gdprConsent.consentString; - - if (typeof bidderRequest.gdprConsent.gdprApplies === 'boolean') { - deepSetValue(payload, 'regs.ext.gdpr', bidderRequest.gdprConsent.gdprApplies ? 1 : 0); - } - } - - if (bidderRequest.uspConsent) { - deepSetValue(payload, 'regs.ext.us_privacy', bidderRequest.uspConsent); - } - } - - if (!pubcid) { - pubcid = readStoredValue(pubcidName); - } - - // Add common id if available - if (pubcid) { - userExt.fpc = pubcid; - } - - // Add Eids if available - const eids = collectEids(validBidRequests); - if (eids.length > 0) { - userExt.eids = eids; - } - - // Only add the user object if it's not empty - if (!isEmpty(userExt)) { - payload.user = {ext: userExt}; - } - - const firstPartyData = bidderRequest.ortb2 || {}; - mergeDeep(payload, firstPartyData); - - return { + buildRequests: function(bidRequests, bidderRequest) { + const payload = converter.toORTB({bidderRequest, bidRequests}); + const result = { method: 'POST', - url: bidurl, + url: makeBidUrl(bidRequests[0]), data: payload, }; + return result; }, /** * Unpack the response from the server into a list of bids. @@ -216,59 +174,7 @@ export const spec = { * @return {Bid[]} An array of bids which were nested inside the server. */ interpretResponse: function(serverResponse, bidRequest) { - const bidResponses = []; - const requestMap = {}; - serverResponse = serverResponse.body; - - if (bidRequest && bidRequest.data && bidRequest.data.imp) { - _each(bidRequest.data.imp, imp => requestMap[imp.id] = imp); - } - - if (serverResponse && isArray(serverResponse.seatbid)) { - _each(serverResponse.seatbid, function(bidList) { - _each(bidList.bid, function(conversantBid) { - const responseCPM = parseFloat(conversantBid.price); - if (responseCPM > 0.0 && conversantBid.impid) { - const responseAd = conversantBid.adm || ''; - const responseNurl = conversantBid.nurl || ''; - const request = requestMap[conversantBid.impid]; - - const bid = { - requestId: conversantBid.impid, - currency: serverResponse.cur || 'USD', - cpm: responseCPM, - creativeId: conversantBid.crid || '', - ttl: 300, - netRevenue: true - }; - bid.meta = {}; - if (conversantBid.adomain && conversantBid.adomain.length > 0) { - bid.meta.advertiserDomains = conversantBid.adomain; - } - - if (request.video) { - if (responseAd.charAt(0) === '<') { - bid.vastXml = responseAd; - } else { - bid.vastUrl = responseAd; - } - - bid.mediaType = 'video'; - bid.width = request.video.w; - bid.height = request.video.h; - } else { - bid.ad = responseAd + ''; - bid.width = conversantBid.w; - bid.height = conversantBid.h; - } - - bidResponses.push(bid); - } - }) - }); - } - - return bidResponses; + return converter.fromORTB({request: bidRequest.data, response: serverResponse.body}); }, /** @@ -328,51 +234,18 @@ export const spec = { } }; -/** - * Determine do-not-track state - * - * @returns {boolean} - */ -function getDNT() { - return navigator.doNotTrack === '1' || window.doNotTrack === '1' || navigator.msDoNoTrack === '1' || navigator.doNotTrack === 'yes'; -} - -/** - * Return openrtb device object that includes ua, width, and height. - * - * @returns {Device} Openrtb device object - */ -function getDevice() { - const language = navigator.language ? 'language' : 'userLanguage'; - return { - h: screen.height, - w: screen.width, - dnt: getDNT() ? 1 : 0, - language: navigator[language].split('-')[0], - make: navigator.vendor ? navigator.vendor : '', - ua: navigator.userAgent - }; -} - -/** - * Convert arrays of widths and heights to an array of objects with w and h properties. - * - * [[300, 250], [300, 600]] => [{w: 300, h: 250}, {w: 300, h: 600}] - * - * @param {Array.>} bidSizes - arrays of widths and heights - * @returns {object[]} Array of objects with w and h - */ -function convertSizes(bidSizes) { - let format; - if (Array.isArray(bidSizes)) { - if (bidSizes.length === 2 && typeof bidSizes[0] === 'number' && typeof bidSizes[1] === 'number') { - format = [{w: bidSizes[0], h: bidSizes[1]}]; - } else { - format = _map(bidSizes, d => { return {w: d[0], h: d[1]}; }); - } +function getPubcid(bidRequest) { + let pubcid = null; + if (bidRequest.userId && bidRequest.userId.pubcid) { + pubcid = bidRequest.userId.pubcid; + } else if (bidRequest.crumbs && bidRequest.crumbs.pubcid) { + pubcid = bidRequest.crumbs.pubcid; } - - return format; + if (!pubcid) { + const pubcidName = getBidIdParameter('pubcid_name', bidRequest.params) || '_pubcid'; + pubcid = readStoredValue(pubcidName); + } + return pubcid; } /** @@ -398,33 +271,6 @@ function copyOptProperty(src, dst, dstName) { } } -/** - * Collect IDs from validBidRequests and store them as an extended id array - * @param bidRequests valid bid requests - */ -function collectEids(bidRequests) { - const request = bidRequests[0]; // bidRequests have the same userId object - const eids = []; - if (isArray(request.userIdAsEids) && request.userIdAsEids.length > 0) { - // later following white-list can be converted to block-list if needed - const requiredSourceValues = { - 'epsilon.com': 1, - 'adserver.org': 1, - 'liveramp.com': 1, - 'criteo.com': 1, - 'id5-sync.com': 1, - 'parrable.com': 1, - 'liveintent.com': 1 - }; - request.userIdAsEids.forEach(function(eid) { - if (requiredSourceValues.hasOwnProperty(eid.source)) { - eids.push(eid); - } - }); - } - return eids; -} - /** * Look for a stored value from both cookie and local storage and return the first value found. * @param key Key for the search @@ -480,4 +326,12 @@ function getBidFloor(bid) { return floor } +function makeBidUrl(bid) { + let bidurl = URL; + if (bid.params.white_label_url) { + bidurl = bid.params.white_label_url; + } + return bidurl; +} + registerBidder(spec); diff --git a/modules/cpmstarBidAdapter.js b/modules/cpmstarBidAdapter.js index 9e237ef2558..e076fb4b0bb 100755 --- a/modules/cpmstarBidAdapter.js +++ b/modules/cpmstarBidAdapter.js @@ -1,8 +1,8 @@ - import * as utils from '../src/utils.js'; -import { registerBidder } from '../src/adapters/bidderFactory.js'; -import { VIDEO, BANNER } from '../src/mediaTypes.js'; -import { config } from '../src/config.js'; +import {registerBidder} from '../src/adapters/bidderFactory.js'; +import {BANNER, VIDEO} from '../src/mediaTypes.js'; +import {config} from '../src/config.js'; +import {getBidIdParameter} from '../src/utils.js'; const BIDDER_CODE = 'cpmstar'; @@ -49,13 +49,13 @@ export const spec = { var bidRequest = validBidRequests[i]; var referer = bidderRequest.refererInfo.page ? bidderRequest.refererInfo.page : bidderRequest.refererInfo.domain; referer = encodeURIComponent(referer); - var e = utils.getBidIdParameter('endpoint', bidRequest.params); + var e = getBidIdParameter('endpoint', bidRequest.params); var ENDPOINT = e == 'dev' ? ENDPOINT_DEV : e == 'staging' ? ENDPOINT_STAGING : ENDPOINT_PRODUCTION; var mediaType = spec.getMediaType(bidRequest); var playerSize = spec.getPlayerSize(bidRequest); var videoArgs = '&fv=0' + (playerSize ? ('&w=' + playerSize[0] + '&h=' + playerSize[1]) : ''); var url = ENDPOINT + '?media=' + mediaType + (mediaType == VIDEO ? videoArgs : '') + - '&json=c_b&mv=1&poolid=' + utils.getBidIdParameter('placementId', bidRequest.params) + + '&json=c_b&mv=1&poolid=' + getBidIdParameter('placementId', bidRequest.params) + '&reachedTop=' + encodeURIComponent(bidderRequest.refererInfo.reachedTop) + '&requestid=' + bidRequest.bidId + '&referer=' + encodeURIComponent(referer); diff --git a/modules/craftBidAdapter.js b/modules/craftBidAdapter.js index 8f7821173c1..a2a054d7659 100644 --- a/modules/craftBidAdapter.js +++ b/modules/craftBidAdapter.js @@ -1,4 +1,4 @@ -import {convertCamelToUnderscore, convertTypes, getBidRequest, logError} from '../src/utils.js'; +import {getBidRequest, logError} from '../src/utils.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import {BANNER, NATIVE, VIDEO} from '../src/mediaTypes.js'; import {auctionManager} from '../src/auctionManager.js'; @@ -7,7 +7,9 @@ import {getStorageManager} from '../src/storageManager.js'; import {ajax} from '../src/ajax.js'; import {hasPurpose1Consent} from '../src/utils/gpdr.js'; import {convertOrtbRequestToProprietaryNative} from '../src/native.js'; -import {getANKeywordParam, transformBidderParamKeywords} from '../libraries/appnexusKeywords/anKeywords.js'; +import {getANKeywordParam, transformBidderParamKeywords} from '../libraries/appnexusUtils/anKeywords.js'; +import {convertCamelToUnderscore} from '../libraries/appnexusUtils/anUtils.js'; +import {convertTypes} from '../libraries/transformParamsUtils/convertTypes.js'; const BIDDER_CODE = 'craft'; const URL_BASE = 'https://gacraft.jp/prebid-v3'; diff --git a/modules/criteoBidAdapter.js b/modules/criteoBidAdapter.js index 993346df849..a8a1416e1f3 100644 --- a/modules/criteoBidAdapter.js +++ b/modules/criteoBidAdapter.js @@ -3,19 +3,30 @@ import { loadExternalScript } from '../src/adloader.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; import { config } from '../src/config.js'; import { BANNER, NATIVE, VIDEO } from '../src/mediaTypes.js'; -import { find } from '../src/polyfill.js'; import { verify } from 'criteo-direct-rsa-validate/build/verify.js'; // ref#2 import { getStorageManager } from '../src/storageManager.js'; import { getRefererInfo } from '../src/refererDetection.js'; import { hasPurpose1Consent } from '../src/utils/gpdr.js'; import { Renderer } from '../src/Renderer.js'; import { OUTSTREAM } from '../src/video.js'; +import { ajax } from '../src/ajax.js'; + +/** + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid + * @typedef {import('../src/adapters/bidderFactory.js').ServerRequest} ServerRequest + * @typedef {import('../src/adapters/bidderFactory.js').BidderSpec} BidderSpec + * @typedef {import('../src/adapters/bidderFactory.js').TimedOutBid} TimedOutBid + */ const GVLID = 91; export const ADAPTER_VERSION = 36; const BIDDER_CODE = 'criteo'; const CDB_ENDPOINT = 'https://bidder.criteo.com/cdb'; const PROFILE_ID_INLINE = 207; +const FLEDGE_SELLER_DOMAIN = 'https://grid-mercury.criteo.com'; +const FLEDGE_SELLER_TIMEOUT = 500; +const FLEDGE_DECISION_LOGIC_URL = 'https://grid-mercury.criteo.com/fledge/decision'; export const PROFILE_ID_PUBLISHERTAG = 185; export const storage = getStorageManager({ bidderCode: BIDDER_CODE }); const LOG_PREFIX = 'Criteo: '; @@ -28,7 +39,7 @@ const LOG_PREFIX = 'Criteo: '; Unminified source code can be found in the privately shared repo: https://github.com/Prebid-org/prebid-js-external-js-criteo/blob/master/dist/prod.js */ const FAST_BID_VERSION_PLACEHOLDER = '%FAST_BID_VERSION%'; -export const FAST_BID_VERSION_CURRENT = 136; +export const FAST_BID_VERSION_CURRENT = 144; 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'; @@ -122,7 +133,8 @@ export const spec = { return []; }, - /** f + /** + * f * @param {object} bid * @return {boolean} */ @@ -201,7 +213,7 @@ export const spec = { /** * @param {*} response * @param {ServerRequest} request - * @return {Bid[]} + * @return {Bid[] | {bids: Bid[], fledgeAuctionConfigs: object[]}} */ interpretResponse: (response, request) => { const body = response.body || response; @@ -215,57 +227,105 @@ export const spec = { } const bids = []; + const fledgeAuctionConfigs = []; if (body && body.slots && isArray(body.slots)) { body.slots.forEach(slot => { - const bidRequest = find(request.bidRequests, b => b.adUnitCode === slot.impid && (!b.params.zoneId || parseInt(b.params.zoneId) === slot.zoneid)); - const bidId = bidRequest.bidId; - const bid = { - requestId: bidId, - cpm: slot.cpm, - currency: slot.currency, - netRevenue: true, - ttl: slot.ttl || 60, - creativeId: slot.creativecode, - width: slot.width, - height: slot.height, - dealId: slot.deal, - }; - if (body.ext?.paf?.transmission && slot.ext?.paf?.content_id) { - const pafResponseMeta = { - content_id: slot.ext.paf.content_id, - transmission: response.ext.paf.transmission + const bidRequest = getAssociatedBidRequest(request.bidRequests, slot); + if (bidRequest) { + const bidId = bidRequest.bidId; + const bid = { + requestId: bidId, + cpm: slot.cpm, + currency: slot.currency, + netRevenue: true, + ttl: slot.ttl || 60, + creativeId: slot.creativecode, + width: slot.width, + height: slot.height, + dealId: slot.deal, }; - bid.meta = Object.assign({}, bid.meta, { paf: pafResponseMeta }); + if (body.ext?.paf?.transmission && slot.ext?.paf?.content_id) { + const pafResponseMeta = { + content_id: slot.ext.paf.content_id, + transmission: response.ext.paf.transmission + }; + bid.meta = Object.assign({}, bid.meta, { paf: pafResponseMeta }); + } + if (slot.adomain) { + bid.meta = Object.assign({}, bid.meta, { advertiserDomains: [slot.adomain].flat() }); + } + if (slot.ext?.meta?.networkName) { + bid.meta = Object.assign({}, bid.meta, { networkName: slot.ext.meta.networkName }) + } + if (slot.native) { + if (bidRequest.params.nativeCallback) { + bid.ad = createNativeAd(bidId, slot.native, bidRequest.params.nativeCallback); + } else { + bid.native = createPrebidNativeAd(slot.native); + bid.mediaType = NATIVE; + } + } else if (slot.video) { + bid.vastUrl = slot.displayurl; + bid.mediaType = VIDEO; + const context = deepAccess(bidRequest, 'mediaTypes.video.context'); + // if outstream video, add a default render for it. + if (context === OUTSTREAM) { + bid.renderer = createOutstreamVideoRenderer(slot); + } + } else { + bid.ad = slot.creative; + } + bids.push(bid); } - if (slot.adomain) { - bid.meta = Object.assign({}, bid.meta, { advertiserDomains: [slot.adomain].flat() }); + }); + } + + if (isArray(body.ext?.igbid)) { + const seller = body.ext.seller || FLEDGE_SELLER_DOMAIN; + const sellerTimeout = body.ext.sellerTimeout || FLEDGE_SELLER_TIMEOUT; + body.ext.igbid.forEach((igbid) => { + const perBuyerSignals = {}; + igbid.igbuyer.forEach(buyerItem => { + perBuyerSignals[buyerItem.origin] = buyerItem.buyerdata; + }); + const bidRequest = request.bidRequests.find(b => b.bidId === igbid.impid); + const bidId = bidRequest.bidId; + let sellerSignals = body.ext.sellerSignals || {}; + if (!sellerSignals.floor && bidRequest.params.bidFloor) { + sellerSignals.floor = bidRequest.params.bidFloor; } - if (slot.ext?.meta?.networkName) { - bid.meta = Object.assign({}, bid.meta, { networkName: slot.ext.meta.networkName }) + if (!sellerSignals.sellerCurrency && bidRequest.params.bidFloorCur) { + sellerSignals.sellerCurrency = bidRequest.params.bidFloorCur; } - if (slot.native) { - if (bidRequest.params.nativeCallback) { - bid.ad = createNativeAd(bidId, slot.native, bidRequest.params.nativeCallback); - } else { - bid.native = createPrebidNativeAd(slot.native); - bid.mediaType = NATIVE; + if (body?.ext?.sellerSignalsPerImp !== undefined) { + const sellerSignalsPerImp = body.ext.sellerSignalsPerImp[bidId]; + if (sellerSignalsPerImp !== undefined) { + sellerSignals = {...sellerSignals, ...sellerSignalsPerImp}; } - } else if (slot.video) { - bid.vastUrl = slot.displayurl; - bid.mediaType = VIDEO; - const context = deepAccess(bidRequest, 'mediaTypes.video.context'); - // if outstream video, add a default render for it. - if (context === OUTSTREAM) { - bid.renderer = createOutstreamVideoRenderer(slot); - } - } else { - bid.ad = slot.creative; } - bids.push(bid); + fledgeAuctionConfigs.push({ + bidId, + config: { + seller, + sellerSignals, + sellerTimeout, + perBuyerSignals, + auctionSignals: {}, + decisionLogicUrl: FLEDGE_DECISION_LOGIC_URL, + interestGroupBuyers: Object.keys(perBuyerSignals), + }, + }); }); } + if (fledgeAuctionConfigs.length) { + return { + bids, + fledgeAuctionConfigs, + }; + } + return bids; }, /** @@ -306,6 +366,23 @@ export const spec = { adapter.handleSetTargeting(bid); } }, + + /** + * @param {BidRequest[]} bidRequests + */ + onDataDeletionRequest: (bidRequests) => { + const id = readFromAllStorages(BUNDLE_COOKIE_NAME); + if (id) { + deleteFromAllStorages(BUNDLE_COOKIE_NAME); + ajax('https://privacy.criteo.com/api/privacy/datadeletionrequest', + null, + JSON.stringify({ publisherUserId: id }), + { + contentType: 'application/json', + method: 'POST' + }); + } + } }; function readFromAllStorages(name) { @@ -444,6 +521,7 @@ function buildCdbRequest(context, bidRequests, bidderRequest) { networkId = bidRequest.params.networkId || networkId; schain = bidRequest.schain || schain; const slot = { + slotid: bidRequest.bidId, impid: bidRequest.adUnitCode, transactionid: bidRequest.ortb2Imp?.ext?.tid }; @@ -483,6 +561,7 @@ function buildCdbRequest(context, bidRequests, bidderRequest) { if (hasVideoMediaType(bidRequest)) { const video = { + context: bidRequest.mediaTypes.video.context, playersizes: parseSizes(deepAccess(bidRequest, 'mediaTypes.video.playerSize'), parseSize), mimes: bidRequest.mediaTypes.video.mimes, protocols: bidRequest.mediaTypes.video.protocols, @@ -493,7 +572,19 @@ function buildCdbRequest(context, bidRequests, bidderRequest) { minduration: bidRequest.mediaTypes.video.minduration, playbackmethod: bidRequest.mediaTypes.video.playbackmethod, startdelay: bidRequest.mediaTypes.video.startdelay, - plcmt: bidRequest.mediaTypes.video.plcmt + plcmt: bidRequest.mediaTypes.video.plcmt, + w: bidRequest.mediaTypes.video.w, + h: bidRequest.mediaTypes.video.h, + linearity: bidRequest.mediaTypes.video.linearity, + skipmin: bidRequest.mediaTypes.video.skipmin, + skipafter: bidRequest.mediaTypes.video.skipafter, + minbitrate: bidRequest.mediaTypes.video.minbitrate, + maxbitrate: bidRequest.mediaTypes.video.maxbitrate, + delivery: bidRequest.mediaTypes.video.delivery, + pos: bidRequest.mediaTypes.video.pos, + playbackend: bidRequest.mediaTypes.video.playbackend, + adPodDurationSec: bidRequest.mediaTypes.video.adPodDurationSec, + durationRangeSec: bidRequest.mediaTypes.video.durationRangeSec, }; const paramsVideo = bidRequest.params.video; if (paramsVideo !== undefined) { @@ -509,6 +600,10 @@ function buildCdbRequest(context, bidRequests, bidderRequest) { enrichSlotWithFloors(slot, bidRequest); + if (!bidderRequest.fledgeEnabled && slot.ext?.ae) { + delete slot.ext.ae; + } + return slot; }), }; @@ -527,6 +622,8 @@ function buildCdbRequest(context, bidRequests, bidderRequest) { }; request.user = bidderRequest.ortb2?.user || {}; request.site = bidderRequest.ortb2?.site || {}; + request.app = bidderRequest.ortb2?.app || {}; + request.device = bidderRequest.ortb2?.device || {}; if (bidderRequest && bidderRequest.ceh) { request.user.ceh = bidderRequest.ceh; } @@ -601,17 +698,7 @@ function hasValidVideoMediaType(bidRequest) { } }); - if (isValid) { - const videoPlacement = bidRequest.mediaTypes.video.placement || bidRequest.params.video.placement; - // We do not support long form for now, also we have to check that context & placement are consistent - if (bidRequest.mediaTypes.video.context == 'instream' && videoPlacement === 1) { - return true; - } else if (bidRequest.mediaTypes.video.context == 'outstream' && videoPlacement !== 1) { - return true; - } - } - - return false; + return isValid; } /** @@ -768,6 +855,27 @@ function createOutstreamVideoRenderer(slot) { return renderer; } +function getAssociatedBidRequest(bidRequests, slot) { + for (const request of bidRequests) { + if (request.adUnitCode === slot.impid) { + if (request.params.zoneId && parseInt(request.params.zoneId) === slot.zoneid) { + return request; + } else if (slot.native) { + if (request.mediaTypes?.native || request.nativeParams) { + return request; + } + } else if (slot.video) { + if (request.mediaTypes?.video) { + return request; + } + } else if (request.mediaTypes?.banner || request.sizes) { + return request; + } + } + } + return undefined; +} + export function tryGetCriteoFastBid() { // begin ref#1 try { diff --git a/modules/criteoIdSystem.js b/modules/criteoIdSystem.js index b48612203f8..0c42858a0fb 100644 --- a/modules/criteoIdSystem.js +++ b/modules/criteoIdSystem.js @@ -13,6 +13,12 @@ import { getStorageManager } from '../src/storageManager.js'; import { MODULE_TYPE_UID } from '../src/activities/modules.js'; import { gdprDataHandler, uspDataHandler, gppDataHandler } from '../src/adapterManager.js'; +/** + * @typedef {import('../modules/userId/index.js').Submodule} Submodule + * @typedef {import('../modules/userId/index.js').SubmoduleConfig} SubmoduleConfig + * @typedef {import('../modules/userId/index.js').ConsentData} ConsentData + */ + const gvlid = 91; const bidderCode = 'criteo'; export const storage = getStorageManager({ moduleType: MODULE_TYPE_UID, moduleName: bidderCode }); @@ -22,6 +28,9 @@ const bundleStorageKey = 'cto_bundle'; const dnaBundleStorageKey = 'cto_dna_bundle'; const cookiesMaxAge = 13 * 30 * 24 * 60 * 60 * 1000; +const STORAGE_TYPE_LOCALSTORAGE = 'html5'; +const STORAGE_TYPE_COOKIES = 'cookie'; + const pastDateString = new Date(0).toString(); const expirationString = new Date(timestamp() + cookiesMaxAge).toString(); @@ -32,14 +41,26 @@ function extractProtocolHost(url, returnOnlyHost = false) { : `${parsedUrl.protocol}://${parsedUrl.hostname}${parsedUrl.port ? ':' + parsedUrl.port : ''}/`; } -function getFromAllStorages(key) { +function getFromStorage(submoduleConfig, key) { + if (submoduleConfig?.storage?.type === STORAGE_TYPE_LOCALSTORAGE) { + return storage.getDataFromLocalStorage(key); + } else if (submoduleConfig?.storage?.type === STORAGE_TYPE_COOKIES) { + return storage.getCookie(key); + } + return storage.getCookie(key) || storage.getDataFromLocalStorage(key); } -function saveOnAllStorages(key, value, hostname) { +function saveOnStorage(submoduleConfig, key, value, hostname) { if (key && value) { - storage.setDataInLocalStorage(key, value); - setCookieOnAllDomains(key, value, expirationString, hostname, true); + if (submoduleConfig?.storage?.type === STORAGE_TYPE_LOCALSTORAGE) { + storage.setDataInLocalStorage(key, value); + } else if (submoduleConfig?.storage?.type === STORAGE_TYPE_COOKIES) { + setCookieOnAllDomains(key, value, expirationString, hostname, true); + } else { + storage.setDataInLocalStorage(key, value); + setCookieOnAllDomains(key, value, expirationString, hostname, true); + } } } @@ -70,11 +91,11 @@ function deleteFromAllStorages(key, hostname) { storage.removeDataFromLocalStorage(key); } -function getCriteoDataFromAllStorages() { +function getCriteoDataFromStorage(submoduleConfig) { return { - bundle: getFromAllStorages(bundleStorageKey), - dnaBundle: getFromAllStorages(dnaBundleStorageKey), - bidId: getFromAllStorages(bididStorageKey), + bundle: getFromStorage(submoduleConfig, bundleStorageKey), + dnaBundle: getFromStorage(submoduleConfig, dnaBundleStorageKey), + bidId: getFromStorage(submoduleConfig, bididStorageKey), } } @@ -108,7 +129,7 @@ function buildCriteoUsersyncUrl(topUrl, domain, bundle, dnaBundle, areCookiesWri return url; } -function callSyncPixel(domain, pixel) { +function callSyncPixel(submoduleConfig, domain, pixel) { if (pixel.writeBundleInStorage && pixel.bundlePropertyName && pixel.storageKeyName) { ajax( pixel.pixelUrl, @@ -117,7 +138,7 @@ function callSyncPixel(domain, pixel) { if (response) { const jsonResponse = JSON.parse(response); if (jsonResponse && jsonResponse[pixel.bundlePropertyName]) { - saveOnAllStorages(pixel.storageKeyName, jsonResponse[pixel.bundlePropertyName], domain); + saveOnStorage(submoduleConfig, pixel.storageKeyName, jsonResponse[pixel.bundlePropertyName], domain); } } }, @@ -133,9 +154,9 @@ function callSyncPixel(domain, pixel) { } } -function callCriteoUserSync(parsedCriteoData, callback) { - const cw = storage.cookiesAreEnabled(); - const lsw = storage.localStorageIsEnabled(); +function callCriteoUserSync(submoduleConfig, parsedCriteoData, callback) { + const cw = (submoduleConfig?.storage?.type === undefined || submoduleConfig?.storage?.type === STORAGE_TYPE_COOKIES) && storage.cookiesAreEnabled(); + const lsw = (submoduleConfig?.storage?.type === undefined || submoduleConfig?.storage?.type === STORAGE_TYPE_LOCALSTORAGE) && storage.localStorageIsEnabled(); const topUrl = extractProtocolHost(getRefererInfo().page); // TODO: should domain really be extracted from the current frame? const domain = extractProtocolHost(document.location.href, true); @@ -156,18 +177,18 @@ function callCriteoUserSync(parsedCriteoData, callback) { const jsonResponse = JSON.parse(response); if (jsonResponse.pixels) { - jsonResponse.pixels.forEach(pixel => callSyncPixel(domain, pixel)); + jsonResponse.pixels.forEach(pixel => callSyncPixel(submoduleConfig, domain, pixel)); } if (jsonResponse.acwsUrl) { const urlsToCall = typeof jsonResponse.acwsUrl === 'string' ? [jsonResponse.acwsUrl] : jsonResponse.acwsUrl; urlsToCall.forEach(url => triggerPixel(url)); } else if (jsonResponse.bundle) { - saveOnAllStorages(bundleStorageKey, jsonResponse.bundle, domain); + saveOnStorage(submoduleConfig, bundleStorageKey, jsonResponse.bundle, domain); } if (jsonResponse.bidId) { - saveOnAllStorages(bididStorageKey, jsonResponse.bidId, domain); + saveOnStorage(submoduleConfig, bididStorageKey, jsonResponse.bidId, domain); const criteoId = { criteoId: jsonResponse.bidId }; callback(criteoId); } else { @@ -207,15 +228,21 @@ export const criteoIdSubmodule = { * @param {ConsentData} [consentData] * @returns {{id: {criteoId: string} | undefined}}} */ - getId() { - let localData = getCriteoDataFromAllStorages(); + getId(submoduleConfig) { + let localData = getCriteoDataFromStorage(submoduleConfig); - const result = (callback) => callCriteoUserSync(localData, callback); + const result = (callback) => callCriteoUserSync(submoduleConfig, localData, callback); return { id: localData.bidId ? { criteoId: localData.bidId } : undefined, callback: result } + }, + eids: { + 'criteoId': { + source: 'criteo.com', + atype: 1 + }, } }; diff --git a/modules/currency.js b/modules/currency.js index 3da0cfe73e8..eaed4c50df2 100644 --- a/modules/currency.js +++ b/modules/currency.js @@ -7,29 +7,24 @@ import {getHook} from '../src/hook.js'; import {defer} from '../src/utils/promise.js'; import {registerOrtbProcessor, REQUEST} from '../src/pbjsORTB.js'; import {timedBidResponseHook} from '../src/utils/perfMetrics.js'; +import {on as onEvent, off as offEvent} from '../src/events.js'; const DEFAULT_CURRENCY_RATE_URL = 'https://cdn.jsdelivr.net/gh/prebid/currency-file@1/latest.json?date=$$TODAY$$'; const CURRENCY_RATE_PRECISION = 4; -var bidResponseQueue = []; -var conversionCache = {}; -var currencyRatesLoaded = false; -var needToCallForCurrencyFile = true; -var adServerCurrency = 'USD'; +let ratesURL; +let bidResponseQueue = []; +let conversionCache = {}; +let currencyRatesLoaded = false; +let needToCallForCurrencyFile = true; +let adServerCurrency = 'USD'; export var currencySupportEnabled = false; export var currencyRates = {}; -var bidderCurrencyDefault = {}; -var defaultRates; +let bidderCurrencyDefault = {}; +let defaultRates; -export const ready = (() => { - let ctl; - function reset() { - ctl = defer(); - } - reset(); - return {done: () => ctl.resolve(), reset, promise: () => ctl.promise} -})(); +export let responseReady = defer(); /** * Configuration function for currency @@ -64,7 +59,7 @@ export const ready = (() => { * there is an error loading the config.conversionRateFile. */ export function setConfig(config) { - let url = DEFAULT_CURRENCY_RATE_URL; + ratesURL = DEFAULT_CURRENCY_RATE_URL; if (typeof config.rates === 'object') { currencyRates.conversions = config.rates; @@ -86,14 +81,14 @@ export function setConfig(config) { adServerCurrency = config.adServerCurrency; if (config.conversionRateFile) { logInfo('currency using override conversionRateFile:', config.conversionRateFile); - url = config.conversionRateFile; + ratesURL = config.conversionRateFile; } // see if the url contains a date macro // this is a workaround to the fact that jsdelivr doesn't currently support setting a 24-hour HTTP cache header // So this is an approach to let the browser cache a copy of the file each day // We should remove the macro once the CDN support a day-level HTTP cache setting - const macroLocation = url.indexOf('$$TODAY$$'); + const macroLocation = ratesURL.indexOf('$$TODAY$$'); if (macroLocation !== -1) { // get the date to resolve the macro const d = new Date(); @@ -104,10 +99,10 @@ export function setConfig(config) { const todaysDate = `${d.getFullYear()}${month}${day}`; // replace $$TODAY$$ with todaysDate - url = `${url.substring(0, macroLocation)}${todaysDate}${url.substring(macroLocation + 9, url.length)}`; + ratesURL = `${ratesURL.substring(0, macroLocation)}${todaysDate}${ratesURL.substring(macroLocation + 9, ratesURL.length)}`; } - initCurrency(url); + initCurrency(); } else { // currency support is disabled, setting defaults logInfo('disabling currency support'); @@ -128,20 +123,11 @@ function errorSettingsRates(msg) { } } -function initCurrency(url) { - conversionCache = {}; - currencySupportEnabled = true; - - logInfo('Installing addBidResponse decorator for currency module', arguments); - - // Adding conversion function to prebid global for external module and on page use - getGlobal().convertCurrency = (cpm, fromCurrency, toCurrency) => parseFloat(cpm) * getCurrencyConversion(fromCurrency, toCurrency); - getHook('addBidResponse').before(addBidResponseHook, 100); - - // call for the file if we haven't already +function loadRates() { if (needToCallForCurrencyFile) { needToCallForCurrencyFile = false; - ajax(url, + currencyRatesLoaded = false; + ajax(ratesURL, { success: function (response) { try { @@ -150,26 +136,45 @@ function initCurrency(url) { conversionCache = {}; currencyRatesLoaded = true; processBidResponseQueue(); - ready.done(); } catch (e) { errorSettingsRates('Failed to parse currencyRates response: ' + response); } }, error: function (...args) { errorSettingsRates(...args); - ready.done(); + currencyRatesLoaded = true; + processBidResponseQueue(); + needToCallForCurrencyFile = true; } } ); } else { - ready.done(); + processBidResponseQueue(); } } +function initCurrency() { + conversionCache = {}; + currencySupportEnabled = true; + + logInfo('Installing addBidResponse decorator for currency module', arguments); + + // Adding conversion function to prebid global for external module and on page use + getGlobal().convertCurrency = (cpm, fromCurrency, toCurrency) => parseFloat(cpm) * getCurrencyConversion(fromCurrency, toCurrency); + getHook('addBidResponse').before(addBidResponseHook, 100); + getHook('responsesReady').before(responsesReadyHook); + onEvent(CONSTANTS.EVENTS.AUCTION_TIMEOUT, rejectOnAuctionTimeout); + onEvent(CONSTANTS.EVENTS.AUCTION_INIT, loadRates); + loadRates(); +} + function resetCurrency() { logInfo('Uninstalling addBidResponse decorator for currency module', arguments); getHook('addBidResponse').getHooks({hook: addBidResponseHook}).remove(); + getHook('responsesReady').getHooks({hook: responsesReadyHook}).remove(); + offEvent(CONSTANTS.EVENTS.AUCTION_TIMEOUT, rejectOnAuctionTimeout); + offEvent(CONSTANTS.EVENTS.AUCTION_INIT, loadRates); delete getGlobal().convertCurrency; adServerCurrency = 'USD'; @@ -179,6 +184,11 @@ function resetCurrency() { needToCallForCurrencyFile = true; currencyRates = {}; bidderCurrencyDefault = {}; + responseReady = defer(); +} + +function responsesReadyHook(next, ready) { + next(ready.then(() => responseReady.promise)); } export const addBidResponseHook = timedBidResponseHook('currency', function addBidResponseHook(fn, adUnitCode, bid, reject) { @@ -211,24 +221,25 @@ export const addBidResponseHook = timedBidResponseHook('currency', function addB if (bid.currency === adServerCurrency) { return fn.call(this, adUnitCode, bid, reject); } - - bidResponseQueue.push(wrapFunction(fn, this, [adUnitCode, bid, reject])); + bidResponseQueue.push([fn, this, adUnitCode, bid, reject]); if (!currencySupportEnabled || currencyRatesLoaded) { processBidResponseQueue(); - } else { - fn.untimed.bail(ready.promise()); } }); -function processBidResponseQueue() { - while (bidResponseQueue.length > 0) { - (bidResponseQueue.shift())(); - } +function rejectOnAuctionTimeout({auctionId}) { + bidResponseQueue = bidResponseQueue.filter(([fn, ctx, adUnitCode, bid, reject]) => { + if (bid.auctionId === auctionId) { + reject(CONSTANTS.REJECTION_REASON.CANNOT_CONVERT_CURRENCY) + } else { + return true; + } + }); } -function wrapFunction(fn, context, params) { - return function() { - let bid = params[1]; +function processBidResponseQueue() { + while (bidResponseQueue.length > 0) { + const [fn, ctx, adUnitCode, bid, reject] = bidResponseQueue.shift(); if (bid !== undefined && 'currency' in bid && 'cpm' in bid) { let fromCurrency = bid.currency; try { @@ -239,12 +250,13 @@ function wrapFunction(fn, context, params) { } } catch (e) { logWarn('getCurrencyConversion threw error: ', e); - params[2](CONSTANTS.REJECTION_REASON.CANNOT_CONVERT_CURRENCY); - return; + reject(CONSTANTS.REJECTION_REASON.CANNOT_CONVERT_CURRENCY); + continue; } } - return fn.apply(context, params); - }; + fn.call(ctx, adUnitCode, bid, reject); + } + responseReady.resolve(); } function getCurrencyConversion(fromCurrency, toCurrency = adServerCurrency) { diff --git a/modules/cwireBidAdapter.js b/modules/cwireBidAdapter.js index 604d7235d0f..d36948d162d 100644 --- a/modules/cwireBidAdapter.js +++ b/modules/cwireBidAdapter.js @@ -2,6 +2,13 @@ import {registerBidder} from '../src/adapters/bidderFactory.js'; import {getStorageManager} from '../src/storageManager.js'; import {BANNER} from '../src/mediaTypes.js'; import {generateUUID, getParameterByName, isNumber, logError, logInfo} from '../src/utils.js'; +import {hasPurpose1Consent} from '../src/utils/gpdr.js'; + +/** + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid + * @typedef {import('../src/adapters/bidderFactory.js').ServerResponse} ServerResponse + */ // ------------------------------------ const BIDDER_CODE = 'cwire'; @@ -230,5 +237,23 @@ export const spec = { navigator.sendBeacon(EVENT_ENDPOINT, JSON.stringify(event)) }, + getUserSyncs: function(syncOptions, serverResponses, gdprConsent, uspConsent) { + logInfo('Collecting user-syncs: ', JSON.stringify({syncOptions, gdprConsent, uspConsent, serverResponses})); + + const syncs = [] + if (hasPurpose1Consent(gdprConsent)) { + logInfo('GDPR purpose 1 consent was given, adding user-syncs') + let type = (syncOptions.pixelEnabled) ? 'image' : null ?? (syncOptions.iframeEnabled) ? 'iframe' : null + if (type) { + syncs.push({ + type: type, + url: 'https://ib.adnxs.com/getuid?https://prebid.cwi.re/v1/cookiesync?xandrId=$UID' + }) + } + } + logInfo('Collected user-syncs: ', JSON.stringify({syncs})) + return syncs + } + }; registerBidder(spec); diff --git a/modules/czechAdIdSystem.js b/modules/czechAdIdSystem.js index 957b3ed30bd..7fdf462183a 100644 --- a/modules/czechAdIdSystem.js +++ b/modules/czechAdIdSystem.js @@ -9,6 +9,11 @@ import { submodule } from '../src/hook.js' import {getStorageManager} from '../src/storageManager.js'; import {MODULE_TYPE_UID} from '../src/activities/modules.js'; +/** + * @typedef {import('../modules/userId/index.js').Submodule} Submodule + * @typedef {import('../modules/userId/index.js').IdResponse} IdResponse + */ + // Returns StorageManager export const storage = getStorageManager({ moduleType: MODULE_TYPE_UID, moduleName: 'czechAdId' }) @@ -42,6 +47,12 @@ export const czechAdIdSubmodule = { getId () { const id = readId() return id ? { id: id } : undefined + }, + eids: { + 'czechAdId': { + source: 'czechadid.cz', + atype: 1 + }, } } diff --git a/modules/dacIdSystem.js b/modules/dacIdSystem.js index 5adca074c87..ffdadef18e8 100644 --- a/modules/dacIdSystem.js +++ b/modules/dacIdSystem.js @@ -170,6 +170,12 @@ export const dacIdSystemSubmodule = { } return fetchAoneId(configParams.oid, cookie.fuuid); + }, + eids: { + 'dacId': { + source: 'impact-ad.jp', + atype: 1 + }, } }; diff --git a/modules/datablocksBidAdapter.js b/modules/datablocksBidAdapter.js index 11d3ebb1589..395706994fe 100644 --- a/modules/datablocksBidAdapter.js +++ b/modules/datablocksBidAdapter.js @@ -1,10 +1,11 @@ -import {deepAccess, getAdUnitSizes, getWindowTop, isEmpty, isGptPubadsDefined} from '../src/utils.js'; +import {deepAccess, getWindowTop, isEmpty, isGptPubadsDefined} from '../src/utils.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import {config} from '../src/config.js'; import {BANNER, NATIVE} from '../src/mediaTypes.js'; import {getStorageManager} from '../src/storageManager.js'; import {ajax} from '../src/ajax.js'; import {convertOrtbRequestToProprietaryNative} from '../src/native.js'; +import {getAdUnitSizes} from '../libraries/sizeUtils/sizeUtils.js'; export const storage = getStorageManager({bidderCode: 'datablocks'}); diff --git a/modules/datawrkzBidAdapter.js b/modules/datawrkzBidAdapter.js index 2cf28c36330..db795c89155 100644 --- a/modules/datawrkzBidAdapter.js +++ b/modules/datawrkzBidAdapter.js @@ -1,4 +1,12 @@ -import { deepAccess, getBidIdParameter, isArray, getUniqueIdentifierStr, contains, isFn, isPlainObject } from '../src/utils.js'; +import { + deepAccess, + isArray, + getUniqueIdentifierStr, + contains, + isFn, + isPlainObject, + getBidIdParameter +} from '../src/utils.js'; import { config } from '../src/config.js'; import { Renderer } from '../src/Renderer.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; @@ -7,6 +15,11 @@ import { BANNER, NATIVE, VIDEO } from '../src/mediaTypes.js'; import CONSTANTS from '../src/constants.json'; import { OUTSTREAM, INSTREAM } from '../src/video.js'; +/** + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid + */ + const BIDDER_CODE = 'datawrkz'; const ALIASES = []; const ENDPOINT_URL = 'https://at.datawrkz.com/exchange/openrtb23/'; diff --git a/modules/dchain.js b/modules/dchain.js index daf97a7551f..7f84282b81e 100644 --- a/modules/dchain.js +++ b/modules/dchain.js @@ -1,7 +1,7 @@ import {includes} from '../src/polyfill.js'; import {config} from '../src/config.js'; import {getHook} from '../src/hook.js'; -import {_each, deepAccess, deepClone, hasOwn, isArray, isPlainObject, isStr, logError, logWarn} from '../src/utils.js'; +import {_each, deepAccess, deepClone, isArray, isPlainObject, isStr, logError, logWarn} from '../src/utils.js'; import {timedBidResponseHook} from '../src/utils/perfMetrics.js'; const shouldBeAString = ' should be a string'; @@ -49,7 +49,7 @@ export function checkDchainSyntax(bid, mode) { appendFailMsg(`dchain.ver` + shouldBeAString); } - if (hasOwn(dchainObj, 'ext')) { + if (dchainObj.hasOwnProperty('ext')) { if (!isPlainObject(dchainObj.ext)) { appendFailMsg(`dchain.ext` + shouldBeAnObject); } diff --git a/modules/deepintentBidAdapter.js b/modules/deepintentBidAdapter.js index e062686b320..7c24cd6a8f6 100644 --- a/modules/deepintentBidAdapter.js +++ b/modules/deepintentBidAdapter.js @@ -2,6 +2,7 @@ import { generateUUID, deepSetValue, deepAccess, isArray, isInteger, logError, l import {registerBidder} from '../src/adapters/bidderFactory.js'; import {BANNER, VIDEO} from '../src/mediaTypes.js'; const BIDDER_CODE = 'deepintent'; +const GVL_ID = 541; const BIDDER_ENDPOINT = 'https://prebid.deepintent.com/prebid'; const USER_SYNC_URL = 'https://cdn.deepintent.com/syncpixel.html'; const DI_M_V = '1.0.0'; @@ -32,6 +33,7 @@ export const ORTB_VIDEO_PARAMS = { }; export const spec = { code: BIDDER_CODE, + gvlid: GVL_ID, supportedMediaTypes: [BANNER, VIDEO], aliases: [], diff --git a/modules/deepintentDpesIdSystem.js b/modules/deepintentDpesIdSystem.js index f2b50d535eb..2d3eae980cd 100644 --- a/modules/deepintentDpesIdSystem.js +++ b/modules/deepintentDpesIdSystem.js @@ -9,6 +9,12 @@ import { submodule } from '../src/hook.js'; import {getStorageManager} from '../src/storageManager.js'; import {MODULE_TYPE_UID} from '../src/activities/modules.js'; +/** + * @typedef {import('../modules/userId/index.js').Submodule} Submodule + * @typedef {import('../modules/userId/index.js').SubmoduleConfig} SubmoduleConfig + * @typedef {import('../modules/userId/index.js').ConsentData} ConsentData + */ + const MODULE_NAME = 'deepintentId'; export const storage = getStorageManager({moduleType: MODULE_TYPE_UID, moduleName: MODULE_NAME}); @@ -39,8 +45,13 @@ export const deepintentDpesSubmodule = { */ getId(config, consentData, cacheIdObj) { return cacheIdObj; - } - + }, + eids: { + 'deepintentId': { + source: 'deepintent.com', + atype: 3 + }, + }, }; submodule('userId', deepintentDpesSubmodule); diff --git a/modules/deltaprojectsBidAdapter.js b/modules/deltaprojectsBidAdapter.js index c66e381b8f1..870378a13dd 100644 --- a/modules/deltaprojectsBidAdapter.js +++ b/modules/deltaprojectsBidAdapter.js @@ -16,7 +16,7 @@ export const BIDDER_CODE = 'deltaprojects'; export const BIDDER_ENDPOINT_URL = 'https://d5p.de17a.com/dogfight/prebid'; export const USERSYNC_URL = 'https://userservice.de17a.com/getuid/prebid'; -/** -- isBidRequestValid --**/ +/** -- isBidRequestValid -- */ function isBidRequestValid(bid) { if (!bid) return false; @@ -32,9 +32,9 @@ function isBidRequestValid(bid) { return true; } -/** -- Build requests --**/ +/** -- Build requests -- */ function buildRequests(validBidRequests, bidderRequest) { - /** == shared ==**/ + /** == shared == */ // -- build id const id = bidderRequest.bidderRequestId; @@ -146,7 +146,7 @@ function buildImpressionBanner(bid, bannerMediaType) { }; } -/** -- Interpret response --**/ +/** -- Interpret response -- */ function interpretResponse(serverResponse) { if (!serverResponse.body) { logWarn('Response body is invalid, return !!'); @@ -189,7 +189,7 @@ function interpretResponse(serverResponse) { return bidResponses; } -/** -- On Bid Won -- **/ +/** -- On Bid Won -- */ function onBidWon(bid) { let cpm = bid.cpm; if (bid.currency && bid.currency !== bid.originalCurrency && typeof bid.getCpmInNewCurrency === 'function') { @@ -200,7 +200,7 @@ function onBidWon(bid) { bid.ad = bid.ad.replace(wonPriceMacroPatten, wonPrice); } -/** -- Get user syncs --**/ +/** -- Get user syncs -- */ function getUserSyncs(syncOptions, serverResponses, gdprConsent) { const syncs = [] @@ -223,7 +223,7 @@ function getUserSyncs(syncOptions, serverResponses, gdprConsent) { return syncs; } -/** -- Get bid floor --**/ +/** -- Get bid floor -- */ export function getBidFloor(bid, mediaType, size, currency) { if (isFn(bid.getFloor)) { const bidFloorCurrency = currency || 'USD'; @@ -234,7 +234,7 @@ export function getBidFloor(bid, mediaType, size, currency) { } } -/** -- Helper methods --**/ +/** -- Helper methods -- */ function setOnAny(collection, key) { for (let i = 0, result; i < collection.length; i++) { result = deepAccess(collection[i], key); diff --git a/modules/dfpAdServerVideo.js b/modules/dfpAdServerVideo.js index 3394fd8b3f4..7f275992210 100644 --- a/modules/dfpAdServerVideo.js +++ b/modules/dfpAdServerVideo.js @@ -2,17 +2,28 @@ * This module adds [DFP support]{@link https://www.doubleclickbygoogle.com/} for Video to Prebid. */ -import { registerVideoSupport } from '../src/adServerManager.js'; -import { targeting } from '../src/targeting.js'; -import { deepAccess, isEmpty, logError, parseSizesInput, formatQS, parseUrl, buildUrl } from '../src/utils.js'; -import { config } from '../src/config.js'; -import { getHook, submodule } from '../src/hook.js'; -import { auctionManager } from '../src/auctionManager.js'; -import { gdprDataHandler, uspDataHandler, gppDataHandler } from '../src/adapterManager.js'; +import {registerVideoSupport} from '../src/adServerManager.js'; +import {targeting} from '../src/targeting.js'; +import { + isNumber, + buildUrl, + deepAccess, + formatQS, + isEmpty, + logError, + parseSizesInput, + parseUrl, + uniques +} from '../src/utils.js'; +import {config} from '../src/config.js'; +import {getHook, submodule} from '../src/hook.js'; +import {auctionManager} from '../src/auctionManager.js'; +import {gdprDataHandler} 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'; +import {CLIENT_SECTIONS} from '../src/fpd/oneClient.js'; /** * @typedef {Object} DfpVideoParams @@ -113,7 +124,6 @@ export function buildDfpVideoUrl(options) { const descriptionUrl = getDescriptionUrl(bid, options, 'params'); if (descriptionUrl) { queryParams.description_url = descriptionUrl; } - const gdprConsent = gdprDataHandler.getConsentData(); if (gdprConsent) { if (typeof gdprConsent.gdprApplies === 'boolean') { queryParams.gdpr = Number(gdprConsent.gdprApplies); } @@ -121,14 +131,6 @@ export function buildDfpVideoUrl(options) { if (gdprConsent.addtlConsent) { queryParams.addtl_consent = gdprConsent.addtlConsent; } } - 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) { @@ -136,6 +138,70 @@ export function buildDfpVideoUrl(options) { } } + const video = options.adUnit?.mediaTypes?.video; + Object.entries({ + plcmt: () => video?.plcmt, + min_ad_duration: () => isNumber(video?.minduration) ? video.minduration * 1000 : null, + max_ad_duration: () => isNumber(video?.maxduration) ? video.maxduration * 1000 : null, + vpos() { + const startdelay = video?.startdelay; + if (isNumber(startdelay)) { + if (startdelay === -2) return 'postroll'; + if (startdelay === -1 || startdelay > 0) return 'midroll'; + return 'preroll'; + } + }, + vconp: () => Array.isArray(video?.playbackmethod) && video.playbackmethod.every(m => m === 7) ? '2' : undefined, + vpa() { + // playbackmethod = 3 is play on click; 1, 2, 4, 5, 6 are autoplay + if (Array.isArray(video?.playbackmethod)) { + const click = video.playbackmethod.some(m => m === 3); + const auto = video.playbackmethod.some(m => [1, 2, 4, 5, 6].includes(m)); + if (click && !auto) return 'click'; + if (auto && !click) return 'auto'; + } + }, + vpmute() { + // playbackmethod = 2, 6 are muted; 1, 3, 4, 5 are not + if (Array.isArray(video?.playbackmethod)) { + const muted = video.playbackmethod.some(m => [2, 6].includes(m)); + const talkie = video.playbackmethod.some(m => [1, 3, 4, 5].includes(m)); + if (muted && !talkie) return '1'; + if (talkie && !muted) return '0'; + } + } + }).forEach(([param, getter]) => { + if (!queryParams.hasOwnProperty(param)) { + const val = getter(); + if (val != null) { + queryParams[param] = val; + } + } + }); + const fpd = auctionManager.index.getBidRequest(options.bid || {})?.ortb2 ?? + auctionManager.index.getAuction(options.bid || {})?.getFPD()?.global; + + function getSegments(sections, segtax) { + return sections + .flatMap(section => deepAccess(fpd, section) || []) + .filter(datum => datum.ext?.segtax === segtax) + .flatMap(datum => datum.segment?.map(seg => seg.id)) + .filter(ob => ob) + .filter(uniques) + } + + const signals = Object.entries({ + IAB_AUDIENCE_1_1: getSegments(['user.data'], 4), + IAB_CONTENT_2_2: getSegments(CLIENT_SECTIONS.map(section => `${section}.content.data`), 6) + }).map(([taxonomy, values]) => values.length ? {taxonomy, values} : null) + .filter(ob => ob); + + if (signals.length) { + queryParams.ppsj = btoa(JSON.stringify({ + PublisherProvidedTaxonomySignals: signals + })) + } + return buildUrl(Object.assign({ protocol: 'https', host: 'securepubads.g.doubleclick.net', @@ -164,6 +230,8 @@ if (config.getConfig('brandCategoryTranslation.translationFile')) { getHook('reg * @returns {string} A URL which calls DFP with custom adpod targeting key values to compete with rest of the demand in DFP */ export function buildAdpodVideoUrl({code, params, callback} = {}) { + // TODO: the public API for this does not take in enough info to fill all DFP params (adUnit/bid), + // and is marked "alpha": https://docs.prebid.org/dev-docs/publisher-api-reference/adServers.dfp.buildAdpodVideoUrl.html if (!params || !callback) { logError(`A params object and a callback is required to use pbjs.adServers.dfp.buildAdpodVideoUrl`); return; @@ -225,9 +293,6 @@ export function buildAdpodVideoUrl({code, params, callback} = {}) { if (gdprConsent.addtlConsent) { queryParams.addtl_consent = gdprConsent.addtlConsent; } } - const uspConsent = uspDataHandler.getConsentData(); - if (uspConsent) { queryParams.us_privacy = uspConsent; } - const masterTag = buildUrl({ protocol: 'https', host: 'securepubads.g.doubleclick.net', @@ -249,7 +314,9 @@ export function buildAdpodVideoUrl({code, params, callback} = {}) { */ function buildUrlFromAdserverUrlComponents(components, bid, options) { const descriptionUrl = getDescriptionUrl(bid, components, 'search'); - if (descriptionUrl) { components.search.description_url = descriptionUrl; } + if (descriptionUrl) { + components.search.description_url = descriptionUrl; + } components.search.cust_params = getCustParams(bid, options, components.search.cust_params); return buildUrl(components); @@ -264,7 +331,7 @@ function buildUrlFromAdserverUrlComponents(components, bid, options) { * @return {string | undefined} The encoded vast url if it exists, or undefined */ function getDescriptionUrl(bid, components, prop) { - return deepAccess(components, `${prop}.description_url`) || dep.ri().page; + return deepAccess(components, `${prop}.description_url`) || encodeURIComponent(dep.ri().page); } /** diff --git a/modules/dgkeywordRtdProvider.js b/modules/dgkeywordRtdProvider.js index 99df3b18a39..14519ae2713 100644 --- a/modules/dgkeywordRtdProvider.js +++ b/modules/dgkeywordRtdProvider.js @@ -6,12 +6,15 @@ * @module modules/dgkeywordProvider * @requires module:modules/realTimeData */ - -import {logMessage, deepSetValue, logError, logInfo, mergeDeep} from '../src/utils.js'; +import { logMessage, deepSetValue, logError, logInfo, isStr, isArray } from '../src/utils.js'; import { ajax } from '../src/ajax.js'; import { submodule } from '../src/hook.js'; import { getGlobal } from '../src/prebidGlobal.js'; +/** + * @typedef {import('../modules/rtdModule/index.js').RtdSubmodule} RtdSubmodule + */ + /** * get keywords from api server. and set keywords. * @param {Object} reqBidsConfigObj @@ -57,20 +60,12 @@ export function getDgKeywordsAndSet(reqBidsConfigObj, callback, moduleConfig, us if (Object.keys(keywords).length > 0) { const targetBidKeys = {}; for (let bid of setKeywordTargetBidders) { - // set keywords to params - bid.params.keywords = keywords; + // set keywords to ortb2Imp + deepSetValue(bid, 'ortb2Imp.ext.data.keywords', convertKeywordsToString(keywords)); if (!targetBidKeys[bid.bidder]) { targetBidKeys[bid.bidder] = true; } } - - if (!reqBidsConfigObj._ignoreSetOrtb2) { - // set keywrods to ortb2 - let addOrtb2 = {}; - deepSetValue(addOrtb2, 'site.keywords', keywords); - deepSetValue(addOrtb2, 'user.keywords', keywords); - mergeDeep(reqBidsConfigObj.ortb2Fragments.bidder, Object.fromEntries(Object.keys(targetBidKeys).map(bidder => [bidder, addOrtb2]))); - } } } isFinish = true; @@ -156,4 +151,37 @@ function init(moduleConfig) { function registerSubModule() { submodule('realTimeData', dgkeywordSubmodule); } + +// keywords: { 'genre': ['rock', 'pop'], 'pets': ['dog'] } goes to 'genre=rock,genre=pop,pets=dog' +export function convertKeywordsToString(keywords) { + let result = ''; + Object.keys(keywords).forEach(key => { + // if 'text' or '' + if (isStr(keywords[key])) { + if (keywords[key] !== '') { + result += `${key}=${keywords[key]},` + } else { + result += `${key},`; + } + } else if (isArray(keywords[key])) { + let isValSet = false + keywords[key].forEach(val => { + if (isStr(val) && val) { + result += `${key}=${val},` + isValSet = true + } + }); + if (!isValSet) { + result += `${key},` + } + } else { + result += `${key},` + } + }); + + // remove last trailing comma + result = result.substring(0, result.length - 1); + return result; +} + registerSubModule(); diff --git a/modules/discoveryBidAdapter.js b/modules/discoveryBidAdapter.js index 675ede273a6..ad8f5616d44 100644 --- a/modules/discoveryBidAdapter.js +++ b/modules/discoveryBidAdapter.js @@ -3,17 +3,27 @@ import { getStorageManager } from '../src/storageManager.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; import { BANNER, NATIVE } from '../src/mediaTypes.js'; +/** + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid + * @typedef {import('../src/adapters/bidderFactory.js').ServerResponse} ServerResponse + */ + const BIDDER_CODE = 'discovery'; const ENDPOINT_URL = 'https://rtb-jp.mediago.io/api/bid?tn='; const TIME_TO_LIVE = 500; -const storage = getStorageManager({bidderCode: BIDDER_CODE}); +export const storage = getStorageManager({bidderCode: BIDDER_CODE}); let globals = {}; let itemMaps = {}; const MEDIATYPE = [BANNER, NATIVE]; /* ----- _ss_pp_id:start ------ */ const COOKIE_KEY_SSPPID = '_ss_pp_id'; -const COOKIE_KEY_MGUID = '__mguid_'; +export const COOKIE_KEY_MGUID = '__mguid_'; +const COOKIE_KEY_PMGUID = '__pmguid_'; +const COOKIE_RETENTION_TIME = 365 * 24 * 60 * 60 * 1000; // 1 year +const COOKY_SYNC_IFRAME_URL = 'https://asset.popin.cc/js/cookieSync.html'; +export const THIRD_PARTY_COOKIE_ORIGIN = 'https://asset.popin.cc'; const NATIVERET = { id: 'id', @@ -55,24 +65,79 @@ const NATIVERET = { }; /** - * čŽˇå–į”¨æˆˇid + * get page title + * @returns {string} + */ + +export function getPageTitle(win = window) { + try { + const ogTitle = win.top.document.querySelector('meta[property="og:title"]') + return win.top.document.title || (ogTitle && ogTitle.content) || ''; + } catch (e) { + const ogTitle = document.querySelector('meta[property="og:title"]') + return document.title || (ogTitle && ogTitle.content) || ''; + } +} + +/** + * get page description + * @returns {string} + */ +export function getPageDescription(win = window) { + let element; + + try { + element = win.top.document.querySelector('meta[name="description"]') || + win.top.document.querySelector('meta[property="og:description"]') + } catch (e) { + element = document.querySelector('meta[name="description"]') || + document.querySelector('meta[property="og:description"]') + } + + return (element && element.content) || ''; +} + +/** + * get page keywords + * @returns {string} + */ +export function getPageKeywords(win = window) { + let element; + + try { + element = win.top.document.querySelector('meta[name="keywords"]'); + } catch (e) { + element = document.querySelector('meta[name="keywords"]'); + } + + return (element && element.content) || ''; +} + +/** + * get connection downlink + * @returns {number} + */ +export function getConnectionDownLink(win = window) { + const nav = win.navigator || {}; + return nav && nav.connection && nav.connection.downlink >= 0 ? nav.connection.downlink.toString() : undefined; +} + +/** + * get pmg uid + * čŽˇå–åšļį”Ÿæˆį”¨æˆˇįš„id * @return {string} */ -const getUserID = () => { - let idd = storage.getCookie(COOKIE_KEY_SSPPID); - let idm = storage.getCookie(COOKIE_KEY_MGUID); - - if (idd && !idm) { - idm = idd; - } else if (idm && !idd) { - idd = idm; - } else if (!idd && !idm) { - const uuid = utils.generateUUID(); - storage.setCookie(COOKIE_KEY_MGUID, uuid); - storage.setCookie(COOKIE_KEY_SSPPID, uuid); - return uuid; +export const getPmgUID = () => { + if (!storage.cookiesAreEnabled()) return; + + let pmgUid = storage.getCookie(COOKIE_KEY_PMGUID); + if (!pmgUid) { + pmgUid = utils.generateUUID(); + try { + storage.setCookie(COOKIE_KEY_PMGUID, pmgUid, getCurrentTimeToUTCString()); + } catch (e) {} } - return idd; + return pmgUid; }; /* ----- _ss_pp_id:end ------ */ @@ -211,6 +276,74 @@ const popInAdSize = [ { w: 336, h: 280 }, ]; +/** + * get screen size + * + * @returns {Array} eg: "['widthxheight']" + */ +function getScreenSize() { + return utils.parseSizesInput([window.screen.width, window.screen.height]); +} + +/** + * @param {BidRequest} bidRequest + * @param bidderRequest + * @returns {string} + */ +function getReferrer(bidRequest = {}, bidderRequest = {}) { + let pageUrl; + if (bidRequest.params && bidRequest.params.referrer) { + pageUrl = bidRequest.params.referrer; + } else { + pageUrl = utils.deepAccess(bidderRequest, 'refererInfo.page'); + } + return pageUrl; +} + +/** + * get current time to UTC string + * @returns utc string + */ +export function getCurrentTimeToUTCString() { + const date = new Date(); + date.setTime(date.getTime() + COOKIE_RETENTION_TIME); + return date.toUTCString(); +} + +/** + * format imp ad test ext params + * + * @param validBidRequest sigleBidRequest + * @param bidderRequest + */ +function addImpExtParams(bidRequest = {}, bidderRequest = {}) { + const { deepAccess } = utils; + const { params = {}, adUnitCode, bidId } = bidRequest; + const ext = { + bidId: bidId || '', + adUnitCode: adUnitCode || '', + token: params.token || '', + siteId: params.siteId || '', + zoneId: params.zoneId || '', + publisher: params.publisher || '', + p_pos: params.position || '', + screenSize: getScreenSize(), + referrer: getReferrer(bidRequest, bidderRequest), + stack: deepAccess(bidRequest, 'refererInfo.stack', []), + b_pos: deepAccess(bidRequest, 'mediaTypes.banner.pos', '', ''), + ortbUser: deepAccess(bidRequest, 'ortb2.user', {}, {}), + ortbSite: deepAccess(bidRequest, 'ortb2.site', {}, {}), + tid: deepAccess(bidRequest, 'ortb2Imp.ext.tid', '', ''), + browsiViewability: deepAccess(bidRequest, 'ortb2Imp.ext.data.browsi.browsiViewability', '', ''), + adserverName: deepAccess(bidRequest, 'ortb2Imp.ext.data.adserver.name', '', ''), + adslot: deepAccess(bidRequest, 'ortb2Imp.ext.data.adserver.adslot', '', ''), + keywords: deepAccess(bidRequest, 'ortb2Imp.ext.data.keywords', '', ''), + gpid: deepAccess(bidRequest, 'ortb2Imp.ext.gpid', '', ''), + pbadslot: deepAccess(bidRequest, 'ortb2Imp.ext.data.pbadslot', '', ''), + }; + return ext; +} + /** * get aditem setting * @param {Array} validBidRequests an an array of bids @@ -258,9 +391,14 @@ function getItems(validBidRequests, bidderRequest) { format: sizes, }, ext: {}, - tagid: globals['tagid'], + tagid: req.params && req.params.tagid }; } + + try { + ret.ext = addImpExtParams(req, bidderRequest); + } catch (e) {} + itemMaps[id] = { req, ret, @@ -298,6 +436,10 @@ function getParam(validBidRequests, bidderRequest) { const page = utils.deepAccess(bidderRequest, 'refererInfo.page'); const referer = utils.deepAccess(bidderRequest, 'refererInfo.ref'); const firstPartyData = bidderRequest.ortb2; + const topWindow = window.top; + const title = getPageTitle(); + const desc = getPageDescription(); + const keywords = getPageKeywords(); if (items && items.length) { let c = { @@ -318,12 +460,24 @@ function getParam(validBidRequests, bidderRequest) { ext: { eids, firstPartyData, + ssppid: storage.getCookie(COOKIE_KEY_SSPPID) || undefined, + pmguid: getPmgUID(), + page: { + title: title ? title.slice(0, 100) : undefined, + desc: desc ? desc.slice(0, 300) : undefined, + keywords: keywords ? keywords.slice(0, 100) : undefined, + hLen: topWindow.history?.length || undefined, + }, + device: { + nbw: getConnectionDownLink(), + hc: topWindow.navigator?.hardwareConcurrency || undefined, + dm: topWindow.navigator?.deviceMemory || undefined, + } }, user: { - buyeruid: getUserID(), + buyeruid: storage.getCookie(COOKIE_KEY_MGUID) || undefined, id: sharedid || pubcid, }, - eids, tmax: timeout, site: { name: domain, @@ -372,7 +526,7 @@ export const spec = { if (bid.params.badv) { globals['badv'] = Array.isArray(bid.params.badv) ? bid.params.badv : []; } - return !!(bid.params.token && bid.params.publisher && bid.params.tagid); + return true; }, /** @@ -383,6 +537,8 @@ export const spec = { * @return ServerRequest Info describing the request to the server. */ buildRequests: function (validBidRequests, bidderRequest) { + if (!globals['token']) return; + let payload = getParam(validBidRequests, bidderRequest); const payloadString = JSON.stringify(payload); @@ -485,6 +641,45 @@ export const spec = { return bidResponses; }, + getUserSyncs: function (syncOptions, serverResponse, gdprConsent, uspConsent, gppConsent) { + const origin = encodeURIComponent(location.origin || `https://${location.host}`); + let syncParamUrl = `dm=${origin}`; + + if (gdprConsent && gdprConsent.consentString) { + if (typeof gdprConsent.gdprApplies === 'boolean') { + syncParamUrl += `&gdpr=${Number(gdprConsent.gdprApplies)}&gdpr_consent=${gdprConsent.consentString}`; + } else { + syncParamUrl += `&gdpr=0&gdpr_consent=${gdprConsent.consentString}`; + } + } + if (uspConsent && uspConsent.consentString) { + syncParamUrl += `&ccpa_consent=${uspConsent.consentString}`; + } + + if (syncOptions.iframeEnabled) { + window.addEventListener('message', function handler(event) { + if (!event.data || event.origin != THIRD_PARTY_COOKIE_ORIGIN) { + return; + } + + this.removeEventListener('message', handler); + + event.stopImmediatePropagation(); + + const response = event.data; + if (!response.optout && response.mguid) { + storage.setCookie(COOKIE_KEY_MGUID, response.mguid, getCurrentTimeToUTCString()); + } + }, true); + return [ + { + type: 'iframe', + url: `${COOKY_SYNC_IFRAME_URL}?${syncParamUrl}` + } + ]; + } + }, + /** * Register bidder specific code, which will execute if bidder timed out after an auction * @param {data} Containing timeout specific data diff --git a/modules/displayioBidAdapter.js b/modules/displayioBidAdapter.js index 3d34f2c8b26..3cdfd3a77cd 100644 --- a/modules/displayioBidAdapter.js +++ b/modules/displayioBidAdapter.js @@ -1,7 +1,7 @@ import {registerBidder} from '../src/adapters/bidderFactory.js'; import {BANNER, VIDEO} from '../src/mediaTypes.js'; import {Renderer} from '../src/Renderer.js'; -import {getWindowFromDocument, logWarn} from '../src/utils.js'; +import {logWarn} from '../src/utils.js'; import {getStorageManager} from '../src/storageManager.js'; import {getAllOrtbKeywords} from '../libraries/keywords/keywords.js'; @@ -156,7 +156,7 @@ function newRenderer(bid) { function webisRender(bid, doc) { bid.renderer.push(() => { - const win = getWindowFromDocument(doc) || window; + const win = doc?.defaultView || window; win.webis.init(bid.adData, bid.adUnitCode, bid.params); }) } diff --git a/modules/dmdIdSystem.js b/modules/dmdIdSystem.js index b42315d66ee..3575e658a2a 100644 --- a/modules/dmdIdSystem.js +++ b/modules/dmdIdSystem.js @@ -9,6 +9,13 @@ import { logError, getWindowLocation } from '../src/utils.js'; import { submodule } from '../src/hook.js'; import { ajax } from '../src/ajax.js'; +/** + * @typedef {import('../modules/userId/index.js').Submodule} Submodule + * @typedef {import('../modules/userId/index.js').SubmoduleConfig} SubmoduleConfig + * @typedef {import('../modules/userId/index.js').ConsentData} ConsentData + * @typedef {import('../modules/userId/index.js').IdResponse} IdResponse + */ + const MODULE_NAME = 'dmdId'; /** @type {Submodule} */ @@ -85,6 +92,12 @@ export const dmdIdSubmodule = { }; return { callback: resp }; } + }, + eids: { + 'dmdId': { + source: 'hcn.health', + atype: 3 + }, } }; diff --git a/modules/docereeAdManagerBidAdapter.js b/modules/docereeAdManagerBidAdapter.js new file mode 100644 index 00000000000..d3765f5a130 --- /dev/null +++ b/modules/docereeAdManagerBidAdapter.js @@ -0,0 +1,128 @@ +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { config } from '../src/config.js'; +import { BANNER } from '../src/mediaTypes.js'; +const BIDDER_CODE = 'docereeadmanager'; +const END_POINT = 'https://dai.doceree.com/drs/quest'; + +export const spec = { + code: BIDDER_CODE, + url: '', + supportedMediaTypes: [BANNER], + + isBidRequestValid: (bid) => { + const { placementId } = bid.params; + return !!placementId; + }, + isGdprConsentPresent: (bid) => { + const { gdpr, gdprconsent } = bid.params; + if (gdpr == '1') { + return !!gdprconsent; + } + return true; + }, + buildRequests: (validBidRequests) => { + const serverRequests = []; + const { data } = config.getConfig('docereeadmanager.user') || {}; + + validBidRequests.forEach(function (validBidRequest) { + const payload = getPayload(validBidRequest, data); + + if (!payload) { + return; + } + + serverRequests.push({ + method: 'POST', + url: END_POINT, + data: JSON.stringify(payload.data), + options: { + contentType: 'application/json', + withCredentials: true, + }, + }); + }); + + return serverRequests; + }, + interpretResponse: (serverResponse) => { + const responseJson = serverResponse ? serverResponse.body : {}; + const bidResponse = { + ad: responseJson.ad, + width: Number(responseJson.width), + height: Number(responseJson.height), + requestId: responseJson.requestId, + netRevenue: true, + ttl: 30, + cpm: responseJson.cpm, + currency: responseJson.currency, + mediaType: BANNER, + creativeId: responseJson.creativeId, + meta: { + advertiserDomains: + Array.isArray(responseJson.meta.advertiserDomains) && + responseJson.meta.advertiserDomains.length > 0 + ? responseJson.meta.advertiserDomains + : [], + }, + }; + + return [bidResponse]; + }, +}; + +function getPayload(bid, userData) { + if (!userData || !bid) { + return false; + } + + const { bidId, params } = bid; + const { placementId } = params; + const { + userid, + email, + firstname, + lastname, + specialization, + hcpid, + gender, + city, + state, + zipcode, + hashedNPI, + hashedhcpid, + hashedemail, + hashedmobile, + country, + organization, + dob, + } = userData; + + const data = { + userid: userid || '', + email: email || '', + firstname: firstname || '', + lastname: lastname || '', + specialization: specialization || '', + hcpid: hcpid || '', + gender: gender || '', + city: city || '', + state: state || '', + zipcode: zipcode || '', + hashedNPI: hashedNPI || '', + pb: 1, + adunit: placementId || '', + requestId: bidId || '', + hashedhcpid: hashedhcpid || '', + hashedemail: hashedemail || '', + hashedmobile: hashedmobile || '', + country: country || '', + organization: organization || '', + dob: dob || '', + userconsent: 1, + }; + return { + data, + }; +} + +registerBidder(spec); diff --git a/modules/docereeAdManagerBidAdapter.md b/modules/docereeAdManagerBidAdapter.md new file mode 100644 index 00000000000..bedbf57b179 --- /dev/null +++ b/modules/docereeAdManagerBidAdapter.md @@ -0,0 +1,68 @@ +# Overview + +``` +Module Name: Doceree AdManager Bidder Adapter +Module Type: Bidder Adapter +Maintainer: tech.stack@doceree.com +``` + + + +Connects to Doceree demand source to fetch bids. +Please use `docereeadmanager` as the bidder code. + +# Test Parameters + +``` +var adUnits = [ + { + code: 'DOC-397-1', + sizes: [ + [300, 250] + ], + bids: [ + { + bidder: 'docereeadmanager', + params: { + placementId: 'DOC-19-1', //required + publisherUrl: document.URL || window.location.href, //optional + gdpr: '1', //optional + gdprconsent:'CPQfU1jPQfU1jG0AAAENAwCAAAAAAAAAAAAAAAAAAAAA.IGLtV_T9fb2vj-_Z99_tkeYwf95y3p-wzhheMs-8NyZeH_B4Wv2MyvBX4JiQKGRgksjLBAQdtHGlcTQgBwIlViTLMYk2MjzNKJrJEilsbO2dYGD9Pn8HT3ZCY70-vv__7v3ff_3g', //optional + } + } + ] + } +]; +``` + +```javascript +pbjs.setBidderConfig({ + bidders: ['docereeadmanager'], + config: { + docereeadmanager: { + user: { + data: { + email: 'XXX.XXX@GMAIL.COM', + firstname: 'DR. XXX', + lastname: 'XXX', + mobile: '981234XXXX', + specialization: 'Internal Medicine', + organization: 'Max Lifecare', + hcpid: '199291XXXX', + dob: '1987-08-27', + gender: 'Female', + city: 'Oildale', + state: 'California', + country: 'California', + hashedhcpid: '', + hashedemail: '', + hashedmobile: '', + userid: '7d26d8ca-233a-46c2-9d36-7c5d261e151d', + zipcode: '', + userconsent: '1', + }, + }, + }, + }, +}); +``` diff --git a/modules/docereeBidAdapter.js b/modules/docereeBidAdapter.js index 524f464cee3..2731e1ff397 100644 --- a/modules/docereeBidAdapter.js +++ b/modules/docereeBidAdapter.js @@ -1,9 +1,11 @@ -import { tryAppendQueryString } from '../src/utils.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { triggerPixel } from '../src/utils.js'; import { config } from '../src/config.js'; import { BANNER } from '../src/mediaTypes.js'; +import {tryAppendQueryString} from '../libraries/urlUtils/urlUtils.js'; const BIDDER_CODE = 'doceree'; const END_POINT = 'https://bidder.doceree.com' +const TRACKING_END_POINT = 'https://tracking.doceree.com' export const spec = { code: BIDDER_CODE, @@ -69,6 +71,33 @@ export const spec = { } }; return [bidResponse]; + }, + onTimeout: function(timeoutData) { + if (timeoutData == null || !timeoutData.length) { + return; + } + timeoutData.forEach(td => { + const encodedBuf = window.btoa(encodeURIComponent(JSON.stringify({ + bidId: td.bidId, + timeout: td.timeout, + }))); + triggerPixel(TRACKING_END_POINT + '/v1/hbTimeout?adp=prebidjs&data=' + encodedBuf); + }) + }, + onBidWon: function (bidWon) { + if (bidWon == null) { + return; + } + const encodedBuf = window.btoa(encodeURIComponent(JSON.stringify({ + requestId: bidWon.requestId, + cpm: bidWon.cpm, + adId: bidWon.adId, + currency: bidWon.currency, + netRevenue: bidWon.netRevenue, + status: bidWon.status, + hb_pb: bidWon.adserverTargeting && bidWon.adserverTargeting.hb_pb, + }))); + triggerPixel(TRACKING_END_POINT + '/v1/hbBidWon?adp=prebidjs&data=' + encodedBuf); } }; diff --git a/modules/dsp_genieeBidAdapter.js b/modules/dsp_genieeBidAdapter.js new file mode 100644 index 00000000000..17865e6793a --- /dev/null +++ b/modules/dsp_genieeBidAdapter.js @@ -0,0 +1,123 @@ +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { BANNER } from '../src/mediaTypes.js'; +import { ortbConverter } from '../libraries/ortbConverter/converter.js'; +import { deepAccess, deepSetValue } from '../src/utils.js'; +import { config } from '../src/config.js'; +const BIDDER_CODE = 'dsp_geniee'; +const ENDPOINT_URL = 'https://rt.gsspat.jp/prebid_auction'; +const ENDPOINT_URL_UNCOMFORTABLE = 'https://rt.gsspat.jp/prebid_uncomfortable'; +const ENDPOINT_USERSYNC = 'https://rt.gsspat.jp/prebid_cs'; +const VALID_CURRENCIES = ['USD', 'JPY']; +const converter = ortbConverter({ + context: { ttl: 300, netRevenue: true }, + // set optional parameters + imp(buildImp, bidRequest, context) { + const imp = buildImp(bidRequest, context); + deepSetValue(imp, 'ext', bidRequest.params); + return imp; + } +}); + +function USPConsent(consent) { + return typeof consent === 'string' && consent[0] === '1' && consent.toUpperCase()[2] === 'Y'; +} + +function invalidCurrency(currency) { + return typeof currency === 'string' && VALID_CURRENCIES.indexOf(currency.toUpperCase()) === -1; +} + +function hasTest(imp) { + if (typeof imp !== 'object') { + return false; + } + for (let i = 0; i < imp.length; i++) { + if (deepAccess(imp[i], 'ext.test') === 1) { + return true; + } + } + return false; +} + +export const spec = { + code: BIDDER_CODE, + supportedMediaTypes: [BANNER], + /** + * Determines whether or not the given bid request is valid. + * + * @param {BidRequest} - The bid params to validate. + * @return boolean True if this is a valid bid, and false otherwise. + */ + isBidRequestValid: function (_) { + return true; + }, + /** + * Make a server request from the list of BidRequests. + * + * @param {validBidRequests[]} - an array of bids + * @param {bidderRequest} - the master bidRequest object + * @return ServerRequest Info describing the request to the server. + */ + buildRequests: function (validBidRequests, bidderRequest) { + if (deepAccess(bidderRequest, 'gdprConsent.gdprApplies') || // gdpr + USPConsent(bidderRequest.uspConsent) || // usp + config.getConfig('coppa') || // coppa + invalidCurrency(config.getConfig('currency.adServerCurrency')) // currency validation + ) { + return { + method: 'GET', + url: ENDPOINT_URL_UNCOMFORTABLE + }; + } + + const payload = converter.toORTB({ validBidRequests, bidderRequest }); + + if (hasTest(deepAccess(payload, 'imp'))) { + deepSetValue(payload, 'test', 1); + } + + deepSetValue(payload, 'at', 1); // first price auction only + + 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} bidRequest - the master bidRequest object + * @return {bids} - An array of bids which were nested inside the server. + */ + interpretResponse: function (serverResponse, bidRequest) { + if (!serverResponse.body) { // empty response (no bids) + return []; + } + const bids = converter.fromORTB({ response: serverResponse.body, request: bidRequest.data }).bids; + return bids; + }, + + /** + * 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) { + const syncs = []; + // gdpr & usp + if (deepAccess(gdprConsent, 'gdprApplies') || USPConsent(uspConsent)) { + return syncs; + } + if (syncOptions.pixelEnabled) { + syncs.push({ + type: 'image', + url: ENDPOINT_USERSYNC + }); + } + return syncs; + } +}; +registerBidder(spec); diff --git a/modules/dsp_genieeBidAdapter.md b/modules/dsp_genieeBidAdapter.md new file mode 100644 index 00000000000..d51d66884af --- /dev/null +++ b/modules/dsp_genieeBidAdapter.md @@ -0,0 +1,39 @@ +# Overview + +```markdown +Module Name: Geniee Bid Adapter +Module Type: Bidder Adapter +Maintainer: dsp_back@geniee.co.jp +``` + +# Description +This is [Geniee](https://geniee.co.jp) Bidder Adapter for Prebid.js. + +Please contact us before using the adapter. + +We will provide ads when satisfy the following conditions: + +- There are a certain number bid requests by zone +- The request is a Banner ad +- Payment is possible in Japanese yen or US dollars +- The request is not for GDPR or COPPA users + +Thus, even if the following test, it will be no bids if the request does not reach a certain requests. + +# Test AdUnits +```javascript +var adUnits={ + code: 'geniee-test-ad', + bids: [{ + bidder: 'dsp_geniee', + params: { + test: 1, + } + }], + mediaTypes: { + banner: { + sizes: [[300, 250]] + } + } +}; +``` diff --git a/modules/dspxBidAdapter.js b/modules/dspxBidAdapter.js index b8e812f581a..b4490095894 100644 --- a/modules/dspxBidAdapter.js +++ b/modules/dspxBidAdapter.js @@ -4,6 +4,10 @@ import {BANNER, VIDEO} from '../src/mediaTypes.js'; import {Renderer} from '../src/Renderer.js'; import {includes} from '../src/polyfill.js'; +/** + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + */ + const BIDDER_CODE = 'dspx'; const ENDPOINT_URL = 'https://buyer.dspx.tv/request/'; const ENDPOINT_URL_DEV = 'https://dcbuyer.dspx.tv/request/'; diff --git a/modules/dxkultureBidAdapter.js b/modules/dxkultureBidAdapter.js new file mode 100644 index 00000000000..c167baef6ea --- /dev/null +++ b/modules/dxkultureBidAdapter.js @@ -0,0 +1,340 @@ +import { + logInfo, + logWarn, + logError, + logMessage, + deepAccess, + deepSetValue, + mergeDeep +} from '../src/utils.js'; +import {registerBidder} from '../src/adapters/bidderFactory.js'; +import {BANNER, VIDEO} from '../src/mediaTypes.js'; +import { Renderer } from '../src/Renderer.js'; +import {ortbConverter} from '../libraries/ortbConverter/converter.js' + +/** + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid + */ + +const BIDDER_CODE = 'dxkulture'; +const DEFAULT_BID_TTL = 300; +const DEFAULT_NET_REVENUE = true; +const DEFAULT_CURRENCY = 'USD'; +const DEFAULT_OUTSTREAM_RENDERER_URL = 'https://cdn.dxkulture.com/players/dxOutstreamPlayer.js'; + +const converter = ortbConverter({ + context: { + netRevenue: DEFAULT_NET_REVENUE, + ttl: DEFAULT_BID_TTL + }, + imp(buildImp, bidRequest, context) { + const imp = buildImp(bidRequest, context); + + if (!imp.bidfloor) { + imp.bidfloor = bidRequest.params.bidfloor || 0; + imp.bidfloorcur = bidRequest.params.currency || DEFAULT_CURRENCY; + } + return imp; + }, + request(buildRequest, imps, bidderRequest, context) { + const req = buildRequest(imps, bidderRequest, context); + mergeDeep(req, { + ext: { + hb: 1, + prebidver: '$prebid.version$', + adapterver: '1.0.0', + } + }) + + // Attaching GDPR Consent Params + if (bidderRequest.gdprConsent) { + deepSetValue(req, 'user.ext.consent', bidderRequest.gdprConsent.consentString); + deepSetValue(req, 'regs.ext.gdpr', (bidderRequest.gdprConsent.gdprApplies ? 1 : 0)); + } + + // CCPA + if (bidderRequest.uspConsent) { + deepSetValue(req, 'regs.ext.us_privacy', bidderRequest.uspConsent); + } + + return req; + }, + bidResponse(buildBidResponse, bid, context) { + let resMediaType; + const {bidRequest} = context; + + if (bid.adm?.trim().startsWith(' { + 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 => { + let queryParamStrings = []; + let syncUrl = syncDetails.url; + + if (syncDetails.type === 'iframe') { + if (gdprConsent) { + queryParamStrings.push('gdpr=' + (gdprConsent.gdprApplies ? 1 : 0)); + queryParamStrings.push('gdpr_consent=' + encodeURIComponent(gdprConsent.consentString || '')); + } + if (uspConsent) { + queryParamStrings.push('us_privacy=' + encodeURIComponent(uspConsent)); + } + syncUrl = `${syncDetails.url}${queryParamStrings.length > 0 ? '?' + queryParamStrings.join('&') : ''}` + } + + syncs.push({ + type: syncDetails.type === 'iframe' ? 'iframe' : 'image', + url: syncUrl + }); + }); + + if (syncOptions.iframeEnabled) { + syncs = syncs.filter(s => s.type == 'iframe'); + } else if (syncOptions.pixelEnabled) { + syncs = syncs.filter(s => s.type == 'image'); + } + } + }); + logInfo('dxkulture.getUserSyncs result=%o', syncs); + return syncs; + }, + +}; + +function outstreamRenderer(bid) { + const rendererConfig = { + width: bid.width, + height: bid.height, + vastTimeout: 5000, + maxAllowedVastTagRedirects: 3, + allowVpaid: false, + autoPlay: true, + preload: true, + mute: false + } + + const renderer = Renderer.install({ + id: bid.adId, + url: DEFAULT_OUTSTREAM_RENDERER_URL, + config: rendererConfig, + loaded: false, + targetId: bid.adUnitCode, + adUnitCode: bid.adUnitCode + }); + + try { + renderer.setRender(function (bid) { + bid.renderer.push(() => { + const { id, config } = bid.renderer; + window.dxOutstreamPlayer(bid, id, config); + }); + }); + } catch (err) { + logWarn('dxkulture: Prebid Error calling setRender on renderer', err); + } + + return renderer; +} + +/* ======================================= + * Util Functions + *======================================= */ + +function hasBannerMediaType(bidRequest) { + return !!deepAccess(bidRequest, 'mediaTypes.banner'); +} + +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('dxkulture: Validation failed: publisherId not declared'); + return false; + } + + if (!bidRequest.params.placementId) { + logError('dxkulture: 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('dxkulture: Validation failed: mimes are invalid'); + return false; + } + + if (!Array.isArray(videoParams.protocols) || videoParams.protocols.length === 0) { + logError('dxkulture: Validation failed: protocols are invalid'); + return false; + } + + if (!videoParams.context) { + logError('dxkulture: Validation failed: context id not declared'); + return false; + } + + if (videoParams.context !== 'instream') { + logError('dxkulture: Validation failed: only context instream is supported '); + return false; + } + + if (typeof videoParams.playerSize === 'undefined' || !Array.isArray(videoParams.playerSize) || !Array.isArray(videoParams.playerSize[0])) { + logError('dxkulture: Validation failed: player size not declared or is not in format [[w,h]]'); + return false; + } + + return true; +} + +registerBidder(spec); diff --git a/modules/kulturemediaBidAdapter.md b/modules/dxkultureBidAdapter.md similarity index 86% rename from modules/kulturemediaBidAdapter.md rename to modules/dxkultureBidAdapter.md index 0bd17e97982..e31794ef6c6 100644 --- a/modules/kulturemediaBidAdapter.md +++ b/modules/dxkultureBidAdapter.md @@ -1,15 +1,15 @@ # Overview ``` -Module Name: Kulture Media Bid Adapter +Module Name: DXKulture 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. +Module that connects to DXKulture's demand sources. +DXKulture bid adapter supports Banner and Video. # Test Parameters @@ -26,10 +26,12 @@ var adUnits = [ } }, bids: [{ - bidder: 'kulturemedia', + bidder: 'dxkulture', params: { placementId: 'test', publisherId: 'test', + bidfloor: 2.7, + bidfloorcur: 'USD' } }] } @@ -42,7 +44,7 @@ We support the following OpenRTB params that can be specified in `mediaTypes.vid - 'mimes', - 'minduration', - 'maxduration', -- 'placement', +- 'plcmt', - 'protocols', - 'startdelay', - 'skip', @@ -73,13 +75,13 @@ We support the following OpenRTB params that can be specified in `mediaTypes.vid delivery: [2], minduration: 10, maxduration: 30, - placement: 1, + plcmt: 1, playbackmethod: [1,5], } }, bids: [ { - bidder: 'kulturemedia', + bidder: 'dxkulture', params: { bidfloor: 0.5, publisherId: '12345', @@ -105,7 +107,7 @@ var adUnits = [ } }, bids: [{ - bidder: 'kulturemedia', + bidder: 'dxkulture', params: { e2etest: true } @@ -129,7 +131,7 @@ var adUnits = [ }, bids: [ { - bidder: 'kulturemedia', + bidder: 'dxkulture', params: { e2etest: true } diff --git a/modules/dynamicAdBoostRtdProvider.js b/modules/dynamicAdBoostRtdProvider.js new file mode 100644 index 00000000000..fe08795f313 --- /dev/null +++ b/modules/dynamicAdBoostRtdProvider.js @@ -0,0 +1,114 @@ +/** + * The {@link module:modules/realTimeData} module is required + * @module modules/dynamicAdBoost + * @requires module:modules/realTimeData + */ + +import { submodule } from '../src/hook.js' +import { loadExternalScript } from '../src/adloader.js'; +import { getGlobal } from '../src/prebidGlobal.js'; +import { deepAccess, deepSetValue, isEmptyStr } from '../src/utils.js'; + +const MODULE_NAME = 'dynamicAdBoost'; +const SCRIPT_URL = 'https://adxbid.info'; +const CLIENT_SUPPORTS_IO = window.IntersectionObserver && window.IntersectionObserverEntry && window.IntersectionObserverEntry.prototype && + 'intersectionRatio' in window.IntersectionObserverEntry.prototype; +// Options for the Intersection Observer +const dabOptions = { + threshold: 0.5 // Trigger callback when 50% of the element is visible +}; +let observer; +let dabStartDate; +let dabStartTime; + +// Array of div IDs to track +let dynamicAdBoostAdUnits = {}; + +function init(config, userConsent) { + dabStartDate = new Date(); + dabStartTime = dabStartDate.getTime(); + if (!CLIENT_SUPPORTS_IO) { + return false; + } + // Create an Intersection Observer instance + observer = new IntersectionObserver(dabHandleIntersection, dabOptions); + if (config.params.keyId) { + let keyId = config.params.keyId; + if (keyId && !isEmptyStr(keyId)) { + let dabDivIdsToTrack = config.params.adUnits; + let dabInterval = setInterval(function() { + // Observe each div by its ID + dabDivIdsToTrack.forEach(divId => { + let div = document.getElementById(divId); + if (div) { + observer.observe(div); + } + }); + + let dabDateNow = new Date(); + let dabTimeNow = dabDateNow.getTime(); + let dabElapsedSeconds = Math.floor((dabTimeNow - dabStartTime) / 1000); + let elapsedThreshold = 30; + if (config.params.threshold) { + elapsedThreshold = config.params.threshold; + } + if (dabElapsedSeconds >= elapsedThreshold) { + clearInterval(dabInterval); // Stop + loadLmScript(keyId); + } + }, 1000); + + return true; + } + } + return false; +} + +function loadLmScript(keyId) { + let viewableAdUnits = Object.keys(dynamicAdBoostAdUnits); + let viewableAdUnitsCSV = viewableAdUnits.join(','); + const scriptUrl = `${SCRIPT_URL}/${keyId}.js?viewableAdUnits=${viewableAdUnitsCSV}`; + loadExternalScript(scriptUrl, MODULE_NAME); + observer.disconnect(); +} + +function getBidRequestData(reqBidsConfigObj, callback, config, userConsent) { + const reqAdUnits = reqBidsConfigObj.adUnits || getGlobal().adUnits; + + if (Array.isArray(reqAdUnits)) { + reqAdUnits.forEach(adunit => { + let gptCode = deepAccess(adunit, 'code'); + if (dynamicAdBoostAdUnits.hasOwnProperty(gptCode)) { + // AdUnits has reached target viewablity at some point + deepSetValue(adunit, `ortb2Imp.ext.data.${MODULE_NAME}.${gptCode}`, dynamicAdBoostAdUnits[gptCode]); + } + }); + } + callback(); +} + +let markViewed = (entry, observer) => { + return () => { + observer.unobserve(entry.target); + } +} + +// Callback function when an observed element becomes visible +function dabHandleIntersection(entries) { + entries.forEach(entry => { + if (entry.isIntersecting && entry.intersectionRatio > 0.5) { + dynamicAdBoostAdUnits[entry.target.id] = entry.intersectionRatio; + markViewed(entry, observer) + } + }); +} + +/** @type {RtdSubmodule} */ +export const subModuleObj = { + name: MODULE_NAME, + init, + getBidRequestData, + markViewed +}; + +submodule('realTimeData', subModuleObj); diff --git a/modules/dynamicAdBoostRtdProvider.md b/modules/dynamicAdBoostRtdProvider.md new file mode 100644 index 00000000000..93efe3b3f97 --- /dev/null +++ b/modules/dynamicAdBoostRtdProvider.md @@ -0,0 +1,40 @@ +# Overview + +Module Name: Dynamic Ad Boost +Module Type: Track when a adunit is viewable +Maintainer: info@luponmedia.com + +# Description + +Enhance your revenue with the cutting-edge DynamicAdBoost module! By seamlessly integrating the powerful LuponMedia technology, our module retrieves adunits viewability data, providing publishers with valuable insights to optimize their revenue streams. To unlock the full potential of this technology, we provide a customized LuponMedia module tailored to your specific site requirements. Boost your ad revenue and gain unprecedented visibility into your performance with our advanced solution. + +In order to utilize this module, it is essential to collaborate with [LuponMedia](https://www.luponmedia.com/) to create an account and obtain detailed guidelines on configuring your sites. Working hand in hand with LuponMedia will ensure a smooth integration process, enabling you to fully leverage the capabilities of this module on your website. Take the first step towards optimizing your ad revenue and enhancing your site's performance by partnering with LuponMedia for a seamless experience. +Contact info@luponmedia.com for information. + +## Building Prebid with Real-time Data Support + +First, make sure to add the Dynamic AdBoost submodule to your Prebid.js package with: + +`gulp build --modules=rtdModule,dynamicAdBoostRtdProvider` + +The following configuration parameters are available: + +``` +pbjs.setConfig( + ... + realTimeData: { + auctionDelay: 2000, + dataProviders: [ + { + name: "dynamicAdBoost", + params: { + keyId: "[PROVIDED_KEY]", // Your provided Dynamic AdBoost keyId + adUnits: ["allowedAdUnit1", "allowedAdUnit2"], + threshold: 35 // optional + } + } + ] + } + ... +} +``` diff --git a/modules/ebdrBidAdapter.js b/modules/ebdrBidAdapter.js index a03a1ec12ca..e830f8a94f7 100644 --- a/modules/ebdrBidAdapter.js +++ b/modules/ebdrBidAdapter.js @@ -1,4 +1,4 @@ -import { logInfo, getBidIdParameter } from '../src/utils.js'; +import {getBidIdParameter, logInfo} from '../src/utils.js'; import { VIDEO, BANNER } from '../src/mediaTypes.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; const BIDDER_CODE = 'ebdr'; diff --git a/modules/edge226BidAdapter.js b/modules/edge226BidAdapter.js new file mode 100644 index 00000000000..6d1e2466abe --- /dev/null +++ b/modules/edge226BidAdapter.js @@ -0,0 +1,188 @@ +import { isFn, deepAccess, logMessage, logError } from '../src/utils.js'; +import { convertOrtbRequestToProprietaryNative } from '../src/native.js'; + +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { BANNER, NATIVE, VIDEO } from '../src/mediaTypes.js'; +import { config } from '../src/config.js'; + +const BIDDER_CODE = 'edge226'; +const AD_URL = 'https://ssp.dauup.com/pbjs'; + +function isBidResponseValid(bid) { + if (!bid.requestId || !bid.cpm || !bid.creativeId || !bid.ttl || !bid.currency) { + return false; + } + + switch (bid.mediaType) { + case BANNER: + return Boolean(bid.width && bid.height && bid.ad); + case VIDEO: + return Boolean(bid.vastUrl || bid.vastXml); + case NATIVE: + return Boolean(bid.native && bid.native.impressionTrackers && bid.native.impressionTrackers.length); + default: + return false; + } +} + +function getPlacementReqData(bid) { + const { params, bidId, mediaTypes } = bid; + const schain = bid.schain || {}; + const { placementId, endpointId } = params; + const bidfloor = getBidFloor(bid); + + const placement = { + bidId, + schain, + bidfloor + }; + + if (placementId) { + placement.placementId = placementId; + placement.type = 'publisher'; + } else if (endpointId) { + placement.endpointId = endpointId; + placement.type = 'network'; + } + + if (mediaTypes && mediaTypes[BANNER]) { + placement.adFormat = BANNER; + placement.sizes = mediaTypes[BANNER].sizes; + } else if (mediaTypes && mediaTypes[VIDEO]) { + placement.adFormat = VIDEO; + placement.playerSize = mediaTypes[VIDEO].playerSize; + placement.minduration = mediaTypes[VIDEO].minduration; + placement.maxduration = mediaTypes[VIDEO].maxduration; + placement.mimes = mediaTypes[VIDEO].mimes; + placement.protocols = mediaTypes[VIDEO].protocols; + placement.startdelay = mediaTypes[VIDEO].startdelay; + placement.placement = mediaTypes[VIDEO].placement; + placement.skip = mediaTypes[VIDEO].skip; + placement.skipafter = mediaTypes[VIDEO].skipafter; + placement.minbitrate = mediaTypes[VIDEO].minbitrate; + placement.maxbitrate = mediaTypes[VIDEO].maxbitrate; + placement.delivery = mediaTypes[VIDEO].delivery; + placement.playbackmethod = mediaTypes[VIDEO].playbackmethod; + placement.api = mediaTypes[VIDEO].api; + placement.linearity = mediaTypes[VIDEO].linearity; + } else if (mediaTypes && mediaTypes[NATIVE]) { + placement.native = mediaTypes[NATIVE]; + placement.adFormat = NATIVE; + } + + return placement; +} + +function getBidFloor(bid) { + if (!isFn(bid.getFloor)) { + return deepAccess(bid, 'params.bidfloor', 0); + } + + try { + const bidFloor = bid.getFloor({ + currency: 'USD', + mediaType: '*', + size: '*', + }); + return bidFloor.floor; + } catch (err) { + logError(err); + return 0; + } +} + +export const spec = { + code: BIDDER_CODE, + supportedMediaTypes: [BANNER, VIDEO, NATIVE], + + isBidRequestValid: (bid = {}) => { + const { params, bidId, mediaTypes } = bid; + let valid = Boolean(bidId && params && (params.placementId || params.endpointId)); + + if (mediaTypes && mediaTypes[BANNER]) { + valid = valid && Boolean(mediaTypes[BANNER] && mediaTypes[BANNER].sizes); + } else if (mediaTypes && mediaTypes[VIDEO]) { + valid = valid && Boolean(mediaTypes[VIDEO] && mediaTypes[VIDEO].playerSize); + } else if (mediaTypes && mediaTypes[NATIVE]) { + valid = valid && Boolean(mediaTypes[NATIVE]); + } else { + valid = false; + } + return valid; + }, + + buildRequests: (validBidRequests = [], bidderRequest = {}) => { + // convert Native ORTB definition to old-style prebid native definition + validBidRequests = convertOrtbRequestToProprietaryNative(validBidRequests); + + let deviceWidth = 0; + let deviceHeight = 0; + + let winLocation; + try { + const winTop = window.top; + deviceWidth = winTop.screen.width; + deviceHeight = winTop.screen.height; + winLocation = winTop.location; + } catch (e) { + logMessage(e); + winLocation = window.location; + } + + const refferUrl = bidderRequest.refererInfo && bidderRequest.refererInfo.page; + let refferLocation; + try { + refferLocation = refferUrl && new URL(refferUrl); + } catch (e) { + logMessage(e); + } + // TODO: does the fallback make sense here? + let location = refferLocation || winLocation; + const language = (navigator && navigator.language) ? navigator.language.split('-')[0] : ''; + const host = location.host; + const page = location.pathname; + const secure = location.protocol === 'https:' ? 1 : 0; + const placements = []; + const request = { + deviceWidth, + deviceHeight, + language, + secure, + host, + page, + placements, + coppa: config.getConfig('coppa') === true ? 1 : 0, + ccpa: bidderRequest.uspConsent || undefined, + gdpr: bidderRequest.gdprConsent || undefined, + tmax: bidderRequest.timeout + }; + + const len = validBidRequests.length; + for (let i = 0; i < len; i++) { + const bid = validBidRequests[i]; + placements.push(getPlacementReqData(bid)); + } + + return { + method: 'POST', + url: AD_URL, + data: request + }; + }, + + interpretResponse: (serverResponse) => { + let response = []; + for (let i = 0; i < serverResponse.body.length; i++) { + let resItem = serverResponse.body[i]; + if (isBidResponseValid(resItem)) { + const advertiserDomains = resItem.adomain && resItem.adomain.length ? resItem.adomain : []; + resItem.meta = { ...resItem.meta, advertiserDomains }; + + response.push(resItem); + } + } + return response; + } +}; + +registerBidder(spec); diff --git a/modules/edge226BidAdapter.md b/modules/edge226BidAdapter.md new file mode 100644 index 00000000000..b38ff67065f --- /dev/null +++ b/modules/edge226BidAdapter.md @@ -0,0 +1,79 @@ +# Overview + +``` +Module Name: Edge226 Bidder Adapter +Module Type: Edge226 Bidder Adapter +Maintainer: audit@edge226.com +``` + +# Description + +Connects to Edge226 exchange for bids. +Edge226 bid adapter supports Banner, Video (instream and outstream) and Native. + +# Test Parameters +``` + var adUnits = [ + // Will return static test banner + { + code: 'adunit1', + mediaTypes: { + banner: { + sizes: [ [300, 250], [320, 50] ], + } + }, + bids: [ + { + bidder: 'edge226', + params: { + placementId: 'testBanner', + } + } + ] + }, + { + code: 'addunit2', + mediaTypes: { + video: { + playerSize: [ [640, 480] ], + context: 'instream', + minduration: 5, + maxduration: 60, + } + }, + bids: [ + { + bidder: 'edge226', + params: { + placementId: 'testVideo', + } + } + ] + }, + { + code: 'addunit3', + mediaTypes: { + native: { + title: { + required: true + }, + body: { + required: true + }, + icon: { + required: true, + size: [64, 64] + } + } + }, + bids: [ + { + bidder: 'edge226', + params: { + placementId: 'testNative', + } + } + ] + } + ]; +``` diff --git a/modules/eplanningBidAdapter.js b/modules/eplanningBidAdapter.js index 2216ab329b0..d57804c04e6 100644 --- a/modules/eplanningBidAdapter.js +++ b/modules/eplanningBidAdapter.js @@ -1,8 +1,9 @@ -import {getWindowSelf, isEmpty, parseSizesInput, isGptPubadsDefined, isSlotMatchingAdUnitCode} from '../src/utils.js'; +import {getWindowSelf, isEmpty, parseSizesInput, isGptPubadsDefined} from '../src/utils.js'; import {getGlobal} from '../src/prebidGlobal.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import {getStorageManager} from '../src/storageManager.js'; import {BANNER, VIDEO} from '../src/mediaTypes.js'; +import {isSlotMatchingAdUnitCode} from '../libraries/gptUtils/gptUtils.js'; const BIDDER_CODE = 'eplanning'; export const storage = getStorageManager({bidderCode: BIDDER_CODE}); @@ -267,6 +268,21 @@ function cleanName(name) { return name.replace(/_|\.|-|\//g, '').replace(/\)\(|\(|\)|:/g, '_').replace(/^_+|_+$/g, ''); } +function getFloorStr(bid) { + if (typeof bid.getFloor === 'function') { + let bidFloor = bid.getFloor({ + currency: DOLLAR_CODE, + mediaType: '*', + size: '*' + }); + + if (bidFloor.floor) { + return '|' + encodeURIComponent(bidFloor.floor); + } + } + return ''; +} + function getSpaces(bidRequests, ml) { let impType = bidRequests.reduce((previousBits, bid) => (bid.mediaTypes && bid.mediaTypes[VIDEO]) ? (bid.mediaTypes[VIDEO].context == 'outstream' ? (previousBits | 2) : (previousBits | 1)) : previousBits, 0); // Only one type of auction is supported at a time @@ -286,7 +302,7 @@ function getSpaces(bidRequests, ml) { let sizeVast = firstSize ? firstSize.join('x') : DEFAULT_SIZE_VAST; name = 'video_' + sizeVast + '_' + i; es.map[name] = bid.bidId; - return name + ':' + sizeVast + ';1'; + return name + ':' + sizeVast + ';1' + getFloorStr(bid); } if (ml) { @@ -296,7 +312,7 @@ function getSpaces(bidRequests, ml) { } es.map[name] = bid.bidId; - return name + ':' + getSize(bid); + return name + ':' + getSize(bid) + getFloorStr(bid); }).join('+')).join('+'); return es; } diff --git a/modules/eskimiBidAdapter.js b/modules/eskimiBidAdapter.js index 88d8f95b859..ce01abb9e71 100644 --- a/modules/eskimiBidAdapter.js +++ b/modules/eskimiBidAdapter.js @@ -1,7 +1,12 @@ -import { ortbConverter } from '../libraries/ortbConverter/converter.js'; -import { registerBidder } from '../src/adapters/bidderFactory.js'; -import { BANNER, VIDEO } from '../src/mediaTypes.js'; +import {ortbConverter} from '../libraries/ortbConverter/converter.js'; +import {registerBidder} from '../src/adapters/bidderFactory.js'; +import {BANNER, VIDEO} from '../src/mediaTypes.js'; import * as utils from '../src/utils.js'; +import {getBidIdParameter} from '../src/utils.js'; + +/** + * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid + */ const BIDDER_CODE = 'eskimi'; // const ENDPOINT = 'https://hb.eskimi.com/bids' @@ -65,7 +70,7 @@ const CONVERTER = ortbConverter({ imp.secure = Number(window.location.protocol === 'https:'); if (!imp.bidfloor && bidRequest.params.bidFloor) { imp.bidfloor = bidRequest.params.bidFloor; - imp.bidfloorcur = utils.getBidIdParameter('bidFloorCur', bidRequest.params).toUpperCase() || 'USD' + imp.bidfloorcur = getBidIdParameter('bidFloorCur', bidRequest.params).toUpperCase() || 'USD' } if (bidRequest.mediaTypes[VIDEO]) { diff --git a/modules/euidIdSystem.js b/modules/euidIdSystem.js index c4e15b11d15..fa6113250a8 100644 --- a/modules/euidIdSystem.js +++ b/modules/euidIdSystem.js @@ -12,7 +12,14 @@ import {MODULE_TYPE_UID} from '../src/activities/modules.js'; // RE below lint exception: UID2 and EUID are separate modules, but the protocol is the same and shared code makes sense here. // eslint-disable-next-line prebid/validate-imports -import { Uid2GetId, Uid2CodeVersion } from './uid2IdSystem_shared.js'; +import { Uid2GetId, Uid2CodeVersion, extractIdentityFromParams } from './uid2IdSystem_shared.js'; + +/** + * @typedef {import('../modules/userId/index.js').Submodule} Submodule + * @typedef {import('../modules/userId/index.js').SubmoduleConfig} SubmoduleConfig + * @typedef {import('../modules/userId/index.js').ConsentData} ConsentData + * @typedef {import('../modules/userId/index.js').euidId} euidId + */ const MODULE_NAME = 'euid'; const MODULE_REVISION = Uid2CodeVersion; @@ -99,10 +106,27 @@ export const euidIdSubmodule = { internalStorage: ADVERTISING_COOKIE }; + if (FEATURES.UID2_CSTG) { + mappedConfig.cstg = { + serverPublicKey: config?.params?.serverPublicKey, + subscriptionId: config?.params?.subscriptionId, + ...extractIdentityFromParams(config?.params ?? {}) + } + } + _logInfo(`EUID configuration loaded and mapped.`, mappedConfig); const result = Uid2GetId(mappedConfig, storage, _logInfo, _logWarn); _logInfo(`EUID getId returned`, result); return result; }, + eids: { + 'euid': { + source: 'euid.eu', + atype: 3, + getValue: function(data) { + return data.id; + } + }, + }, }; function decodeImpl(value) { diff --git a/modules/euidIdSystem.md b/modules/euidIdSystem.md index e3e16bce89d..72e40b8ce7b 100644 --- a/modules/euidIdSystem.md +++ b/modules/euidIdSystem.md @@ -1,9 +1,59 @@ ## EUID User ID Submodule -EUID requires initial tokens to be generated server-side. The EUID module handles storing, providing, and optionally refreshing them. The module can operate in one of two different modes: *Client Refresh* mode or *Server Only* mode. +The EUID module handles storing, providing, and optionally refreshing tokens. While initial tokens traditionally required server-side generation, the introduction of the *Client-Side Token Generation (CSTG)* mode offers publishers the flexibility to generate EUID tokens directly from the module, eliminating this need. Publishers can choose to operate the module in one of three distinct modes: *Client Refresh* mode, *Server Only* mode and *Client-Side Token Generation* mode. *Server Only* mode was originally referred to as *legacy mode*, but it is a popular mode for new integrations where publishers prefer to handle token refresh server-side. +*Client-Side Token Generation* mode is included in EUID module by default. However, it's important to note that this mode is created and made available recently. For publishers who do not intend to use it, you have the option to instruct the build to exclude the code related to this feature: + +``` + $ gulp build --modules=uid2IdSystem --disable UID2_CSTG +``` +If you do plan to use Client-Side Token Generation (CSTG) mode, please consult the EUID Team first as they will provide required configuration values for you to use (see the Client-Side Token Generation (CSTG) mode section below for details) + +**This mode is created and made available recently. Please consult EUID Team first as they will provide required configuration values for you to use.** + +For publishers seeking a purely client-side integration without the complexities of server-side involvement, the CSTG mode is highly recommended. This mode requires the provision of a public key, subscription ID and [directly identifying information (DII)](https://unifiedid.com/docs/ref-info/glossary-uid#gl-dii) - either emails or phone numbers. In the CSTG mode, the module takes on the responsibility of encrypting the DII, generating the EUID token, and handling token refreshes when necessary. + +To configure the module to use this mode, you must: +1. Set `parmas.serverPublicKey` and `params.subscriptionId` (please reach out to the UID2 team to obtain these values) +2. Provide **ONLY ONE DII** by setting **ONLY ONE** of `params.email`/`params.phone`/`params.emailHash`/`params.phoneHash` + +Below is a table that provides guidance on when to use each directly identifying information (DII) parameter, along with information on whether normalization and hashing are required by the publisher for each parameter. + +| DII param | When to use it | Normalization required by publisher? | Hashing required by publisher? | +|------------------|-------------------------------------------------------|--------------------------------------|--------------------------------| +| params.email | When you have users' email address | No | No | +| params.phone | When you have user's phone number | Yes | No | +| params.emailHash | When you have user's hashed, normalized email address | Yes | Yes | +| params.phoneHash | When you have user's hashed, normalized phone number | Yes | Yes | + + +*Note that setting params.email will normalize email addresses, but params.phone requires phone numbers to be normalized.* + +Refer to [Normalization and Encoding](#normalization-and-encoding) for details on email address normalization, SHA-256 hashing and Base64 encoding. + +### CSTG example + +Configuration: +``` +pbjs.setConfig({ + userSync: { + userIds: [{ + name: 'euid', + params: { + serverPublicKey: '...server public key...', + subscriptionId: '...subcription id...', + email: 'user@email.com', + //phone: '+0000000', + //emailHash: '...email hash...', + //phoneHash: '...phone hash ...' + } + }] + } +}); +``` + ## Client Refresh mode This is the recommended mode for most scenarios. In this mode, the full response body from the EUID Token Generate or Token Refresh endpoint must be provided to the module. As long as the refresh token remains valid, the module will refresh the advertising token as needed. diff --git a/modules/experianRtdProvider.js b/modules/experianRtdProvider.js new file mode 100644 index 00000000000..cd415d4b32c --- /dev/null +++ b/modules/experianRtdProvider.js @@ -0,0 +1,141 @@ +import { submodule } from '../src/hook.js'; +import { getStorageManager } from '../src/storageManager.js'; +import { MODULE_TYPE_RTD } from '../src/activities/modules.js'; +import { + deepAccess, + isArray, + isPlainObject, + isStr, + mergeDeep, + safeJSONParse, + timestamp +} from '../src/utils.js'; +import { ajax } from '../src/ajax.js'; + +/** + * @typedef {import('../modules/rtdModule/index.js').RtdSubmodule} RtdSubmodule + * @typedef {import('../modules/rtdModule/index.js').SubmoduleConfig} SubmoduleConfig + * @typedef {import('../modules/rtdModule/index.js').UserConsentData} UserConsentData + */ + +export const SUBMODULE_NAME = 'experian_rtid'; +export const EXPERIAN_RTID_DATA_KEY = 'experian_rtid_data'; +export const EXPERIAN_RTID_EXPIRATION_KEY = 'experian_rtid_expiration'; +export const EXPERIAN_RTID_STALE_KEY = 'experian_rtid_stale'; +export const EXPERIAN_RTID_NO_TRACK_KEY = 'experian_rtid_no_track'; +const EXPERIAN_RTID_URL = 'https://rtid.tapad.com' +const storage = getStorageManager({ moduleType: MODULE_TYPE_RTD, moduleName: SUBMODULE_NAME }); + +export const experianRtdObj = { + /** + * @summary modify bid request data + * @param {Object} reqBidsConfigObj + * @param {function} done + * @param {SubmoduleConfig} config + * @param {UserConsentData} userConsent + */ + getBidRequestData(reqBidsConfigObj, done, config, userConsent) { + const dataEnvelope = storage.getDataFromLocalStorage(EXPERIAN_RTID_DATA_KEY, null); + const stale = storage.getDataFromLocalStorage(EXPERIAN_RTID_STALE_KEY, null); + const expired = storage.getDataFromLocalStorage(EXPERIAN_RTID_EXPIRATION_KEY, null); + const noTrack = storage.getDataFromLocalStorage(EXPERIAN_RTID_NO_TRACK_KEY, null); + const now = timestamp() + if (now > new Date(expired).getTime() || (noTrack == null && dataEnvelope == null)) { + // request data envelope and don't manipulate bids + experianRtdObj.requestDataEnvelope(config, userConsent) + done(); + return false; + } + if (now > new Date(stale).getTime()) { + // request data envelope and manipulate bids + experianRtdObj.requestDataEnvelope(config, userConsent); + } + if (noTrack != null) { + done(); + return false; + } + experianRtdObj.alterBids(reqBidsConfigObj, config); + done() + return true; + }, + + alterBids(reqBidsConfigObj, config) { + const dataEnvelope = safeJSONParse(storage.getDataFromLocalStorage(EXPERIAN_RTID_DATA_KEY, null)); + if (dataEnvelope == null) { + return; + } + deepAccess(config, 'params.bidders').forEach((bidderCode) => { + const bidderData = dataEnvelope.find(({ bidder }) => bidder === bidderCode) + if (bidderData != null) { + mergeDeep(reqBidsConfigObj.ortb2Fragments.bidder, { [bidderCode]: { experianRtidKey: bidderData.data.key, experianRtidData: bidderData.data.data } }) + } + }) + }, + requestDataEnvelope(config, userConsent) { + function storeDataEnvelopeResponse(response) { + const responseJson = safeJSONParse(response); + if (responseJson != null) { + storage.setDataInLocalStorage(EXPERIAN_RTID_STALE_KEY, responseJson.staleAt, null); + storage.setDataInLocalStorage(EXPERIAN_RTID_EXPIRATION_KEY, responseJson.expiresAt, null); + if (responseJson.status === 'no_track') { + storage.setDataInLocalStorage(EXPERIAN_RTID_NO_TRACK_KEY, 'no_track', null); + storage.removeDataFromLocalStorage(EXPERIAN_RTID_DATA_KEY, null); + } else { + storage.setDataInLocalStorage(EXPERIAN_RTID_DATA_KEY, JSON.stringify(responseJson.data), null); + storage.removeDataFromLocalStorage(EXPERIAN_RTID_NO_TRACK_KEY, null); + } + } + } + const queryString = experianRtdObj.extractConsentQueryString(config, userConsent) + const fullUrl = queryString == null ? `${EXPERIAN_RTID_URL}/acc/${deepAccess(config, 'params.accountId')}/ids` : `${EXPERIAN_RTID_URL}/acc/${deepAccess(config, 'params.accountId')}/ids${queryString}` + ajax(fullUrl, storeDataEnvelopeResponse, null, { withCredentials: true, contentType: 'application/json' }) + }, + extractConsentQueryString(config, userConsent) { + const queryObj = {}; + + if (userConsent != null) { + if (userConsent.gdpr != null) { + const { gdprApplies, consentString } = userConsent.gdpr; + mergeDeep(queryObj, {gdpr: gdprApplies, gdpr_consent: consentString}) + } + if (userConsent.uspConsent != null) { + mergeDeep(queryObj, {us_privacy: userConsent.uspConsent}) + } + } + const consentQueryString = Object.entries(queryObj).map(([key, val]) => `${key}=${val}`).join('&'); + + let idsString = ''; + if (deepAccess(config, 'params.ids') != null && isPlainObject(deepAccess(config, 'params.ids'))) { + idsString = Object.entries(deepAccess(config, 'params.ids')).map(([idType, val]) => { + if (isArray(val)) { + return val.map((singleVal) => `id.${idType}=${singleVal}`).join('&') + } else { + return `id.${idType}=${val}` + } + }).join('&') + } + + const combinedString = [consentQueryString, idsString].filter((string) => string !== '').join('&'); + return combinedString !== '' ? `?${combinedString}` : undefined; + }, + /** + * @function + * @summary init sub module + * @name RtdSubmodule#init + * @param {SubmoduleConfig} config + * @param {UserConsentData} userConsent + * @return {boolean} false to remove sub module + */ + init(config, userConsent) { + return isStr(deepAccess(config, 'params.accountId')); + } +} + +/** @type {RtdSubmodule} */ +export const experianRtdSubmodule = { + name: SUBMODULE_NAME, + getBidRequestData: experianRtdObj.getBidRequestData, + init: experianRtdObj.init +} + +submodule('realTimeData', experianRtdSubmodule); diff --git a/modules/experianRtdProvider.md b/modules/experianRtdProvider.md new file mode 100644 index 00000000000..ad46e0c3d55 --- /dev/null +++ b/modules/experianRtdProvider.md @@ -0,0 +1,52 @@ +# Experian Real-time Data Submodule + +## Overview + + Module Name: Experian Rtd Provider + Module Type: Rtd Provider + Maintainer: team-ui@tapad.com + +## Description + +The Experian RTD module adds encrypted identifier envelope to the bidding object. + +## Usage + +### Build +``` +gulp build --modules="rtdModule,experianRtdProvider,appnexusBidAdapter,..." +``` + +> Note that the global RTD module, `rtdModule`, is a prerequisite of the Experian RTD module. + +### Configuration + +Use `setConfig` to instruct Prebid.js to initialize the Experian RTD module, as specified below. + +This module is configured as part of the `realTimeData.dataProviders` + +```javascript +pbjs.setConfig({ + realTimeData: { + auctionDelay: 300, + dataProviders: [{ + name: 'experian_rtid', + waitForIt: true, + params: { + accountId: 'ZylatYg', + bidders: ['sovrn', 'pubmatic'], + ids: { maid: ['424', '2982'], hem: 'my-hem' } + } + }] + } +}) +``` + +### Parameters +| Name | Type | Description | Default | +|:-----------------|:----------------------------------------|:-----------------------------------------------------------------------------|:-----------------------| +| name | String | Real time data module name | Always 'experian_rtid' | +| waitForIt | Boolean | Set to true to maximize chance for bidder enrichment, used with auctionDelay | `false` | +| params.accountId | String | Your account id issued by Experian | | +| params.bidders | Array | List of bidders for which you would like data to be set | | +| params.ids | Record or string> | Additional identifiers to send to Experian RTID endpoint | | diff --git a/modules/fabrickIdSystem.js b/modules/fabrickIdSystem.js index dfc2985b0db..f62bafcf637 100644 --- a/modules/fabrickIdSystem.js +++ b/modules/fabrickIdSystem.js @@ -10,6 +10,13 @@ import { ajax } from '../src/ajax.js'; import { submodule } from '../src/hook.js'; import { getRefererInfo } from '../src/refererDetection.js'; +/** + * @typedef {import('../modules/userId/index.js').Submodule} Submodule + * @typedef {import('../modules/userId/index.js').SubmoduleConfig} SubmoduleConfig + * @typedef {import('../modules/userId/index.js').ConsentData} ConsentData + * @typedef {import('../modules/userId/index.js').IdResponse} IdResponse + */ + /** @type {Submodule} */ export const fabrickIdSubmodule = { /** @@ -117,6 +124,12 @@ export const fabrickIdSubmodule = { } catch (e) { logError(`fabrickIdSystem encountered an error`, e); } + }, + eids: { + 'fabrickId': { + source: 'neustar.biz', + atype: 1 + }, } }; diff --git a/modules/feedadBidAdapter.js b/modules/feedadBidAdapter.js index 7b41f0fcc03..e68a932b726 100644 --- a/modules/feedadBidAdapter.js +++ b/modules/feedadBidAdapter.js @@ -3,6 +3,15 @@ import {registerBidder} from '../src/adapters/bidderFactory.js'; import {BANNER} from '../src/mediaTypes.js'; import {ajax} from '../src/ajax.js'; +/** + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid + * @typedef {import('../src/adapters/bidderFactory.js').ServerRequest} ServerRequest + * @typedef {import('../src/adapters/bidderFactory.js').SyncOptions} SyncOptions + * @typedef {import('../src/adapters/bidderFactory.js').BidderSpec} BidderSpec + * @typedef {import('../src/adapters/bidderFactory.js').MediaTypes} MediaTypes + */ + /** * Version of the FeedAd bid adapter * @type {string} diff --git a/modules/fledgeForGpt.js b/modules/fledgeForGpt.js index f29ce7508d5..7162f4e9230 100644 --- a/modules/fledgeForGpt.js +++ b/modules/fledgeForGpt.js @@ -4,50 +4,112 @@ */ import { config } from '../src/config.js'; import { getHook } from '../src/hook.js'; -import { getGptSlotForAdUnitCode, logInfo, logWarn } from '../src/utils.js'; +import {deepSetValue, logInfo, logWarn, mergeDeep} from '../src/utils.js'; import {IMP, PBS, registerOrtbProcessor, RESPONSE} from '../src/pbjsORTB.js'; +import * as events from '../src/events.js' +import CONSTANTS from '../src/constants.json'; +import {currencyCompare} from '../libraries/currencyUtils/currency.js'; +import {maximum, minimum} from '../src/utils/reducers.js'; +import {getGptSlotForAdUnitCode} from '../libraries/gptUtils/gptUtils.js'; const MODULE = 'fledgeForGpt' +const PENDING = {}; export let isEnabled = false; config.getConfig('fledgeForGpt', config => init(config.fledgeForGpt)); /** - * Module init. - */ + * Module init. + */ export function init(cfg) { if (cfg && cfg.enabled === true) { if (!isEnabled) { getHook('addComponentAuction').before(addComponentAuctionHook); + getHook('makeBidRequests').after(markForFledge); + events.on(CONSTANTS.EVENTS.AUCTION_INIT, onAuctionInit); + events.on(CONSTANTS.EVENTS.AUCTION_END, onAuctionEnd); isEnabled = true; } logInfo(`${MODULE} enabled (browser ${isFledgeSupported() ? 'supports' : 'does NOT support'} fledge)`, cfg); } else { if (isEnabled) { getHook('addComponentAuction').getHooks({hook: addComponentAuctionHook}).remove(); + getHook('makeBidRequests').getHooks({hook: markForFledge}).remove() + events.off(CONSTANTS.EVENTS.AUCTION_INIT, onAuctionInit); + events.off(CONSTANTS.EVENTS.AUCTION_END, onAuctionEnd); isEnabled = false; } logInfo(`${MODULE} disabled`, cfg); } } -export function addComponentAuctionHook(next, adUnitCode, componentAuctionConfig) { - const seller = componentAuctionConfig.seller; +function setComponentAuction(adUnitCode, auctionConfigs) { const gptSlot = getGptSlotForAdUnitCode(adUnitCode); if (gptSlot && gptSlot.setConfig) { gptSlot.setConfig({ - componentAuction: [{ - configKey: seller, - auctionConfig: componentAuctionConfig - }] + componentAuction: auctionConfigs.map(cfg => ({ + configKey: cfg.seller, + auctionConfig: cfg + })) }); - logInfo(MODULE, `register component auction config for: ${adUnitCode} x ${seller}: ${gptSlot.getAdUnitPath()}`, componentAuctionConfig); + logInfo(MODULE, `register component auction configs for: ${adUnitCode}: ${gptSlot.getAdUnitPath()}`, auctionConfigs); } else { - logWarn(MODULE, `unable to register component auction config for: ${adUnitCode} x ${seller}.`); + logWarn(MODULE, `unable to register component auction config for ${adUnitCode}`, auctionConfigs); } +} - next(adUnitCode, componentAuctionConfig); +function onAuctionInit({auctionId}) { + PENDING[auctionId] = {}; +} + +function getSlotSignals(bidsReceived = [], bidRequests = []) { + let bidfloor, bidfloorcur; + if (bidsReceived.length > 0) { + const bestBid = bidsReceived.reduce(maximum(currencyCompare(bid => [bid.cpm, bid.currency]))); + bidfloor = bestBid.cpm; + bidfloorcur = bestBid.currency; + } else { + const floors = bidRequests.map(bid => typeof bid.getFloor === 'function' && bid.getFloor()).filter(f => f); + const minFloor = floors.length && floors.reduce(minimum(currencyCompare(floor => [floor.floor, floor.currency]))) + bidfloor = minFloor?.floor; + bidfloorcur = minFloor?.currency; + } + const cfg = {}; + if (bidfloor) { + deepSetValue(cfg, 'auctionSignals.prebid.bidfloor', bidfloor); + bidfloorcur && deepSetValue(cfg, 'auctionSignals.prebid.bidfloorcur', bidfloorcur); + } + return cfg; +} + +function onAuctionEnd({auctionId, bidsReceived, bidderRequests}) { + try { + const allReqs = bidderRequests?.flatMap(br => br.bids); + Object.entries(PENDING[auctionId]).forEach(([adUnitCode, auctionConfigs]) => { + const forThisAdUnit = (bid) => bid.adUnitCode === adUnitCode; + const slotSignals = getSlotSignals(bidsReceived?.filter(forThisAdUnit), allReqs?.filter(forThisAdUnit)); + setComponentAuction(adUnitCode, auctionConfigs.map(cfg => mergeDeep({}, slotSignals, cfg))) + }) + } finally { + delete PENDING[auctionId]; + } +} + +function setFPDSignals(auctionConfig, fpd) { + auctionConfig.auctionSignals = mergeDeep({}, {prebid: fpd}, auctionConfig.auctionSignals); +} + +export function addComponentAuctionHook(next, request, componentAuctionConfig) { + const {adUnitCode, auctionId, ortb2, ortb2Imp} = request; + if (PENDING.hasOwnProperty(auctionId)) { + setFPDSignals(componentAuctionConfig, {ortb2, ortb2Imp}); + !PENDING[auctionId].hasOwnProperty(adUnitCode) && (PENDING[auctionId][adUnitCode] = []); + PENDING[auctionId][adUnitCode].push(componentAuctionConfig); + } else { + logWarn(MODULE, `Received component auction config for auction that has closed (auction '${auctionId}', adUnit '${adUnitCode}')`, componentAuctionConfig) + } + next(request, componentAuctionConfig); } function isFledgeSupported() { @@ -56,16 +118,23 @@ function isFledgeSupported() { export function markForFledge(next, bidderRequests) { if (isFledgeSupported()) { - bidderRequests.forEach((req) => { - req.fledgeEnabled = config.runWithBidder(req.bidderCode, () => config.getConfig('fledgeEnabled')) - }) + const globalFledgeConfig = config.getConfig('fledgeForGpt'); + const bidders = globalFledgeConfig?.bidders ?? []; + bidderRequests.forEach((bidderReq) => { + const useGlobalConfig = globalFledgeConfig?.enabled && (bidders.length === 0 || bidders.includes(bidderReq.bidderCode)); + config.runWithBidder(bidderReq.bidderCode, () => { + const fledgeEnabled = config.getConfig('fledgeEnabled') ?? (useGlobalConfig ? globalFledgeConfig.enabled : undefined); + const defaultForSlots = config.getConfig('defaultForSlots') ?? (useGlobalConfig ? globalFledgeConfig?.defaultForSlots : undefined); + Object.assign(bidderReq, {fledgeEnabled}); + bidderReq.bids.forEach(bidReq => { deepSetValue(bidReq, 'ortb2Imp.ext.ae', bidReq.ortb2Imp?.ext?.ae ?? defaultForSlots) }) + }) + }); } next(bidderRequests); } -getHook('makeBidRequests').after(markForFledge); export function setImpExtAe(imp, bidRequest, context) { - if (!context.bidderRequest.fledgeEnabled) { + if (imp.ext?.ae && !context.bidderRequest.fledgeEnabled) { delete imp.ext?.ae; } } diff --git a/modules/fledgeForGpt.md b/modules/fledgeForGpt.md index 3bb86cd5946..28f44da6459 100644 --- a/modules/fledgeForGpt.md +++ b/modules/fledgeForGpt.md @@ -15,8 +15,8 @@ This is accomplished by adding the `fledgeForGpt` module to the list of modules gulp build --modules=fledgeForGpt,... ``` -Second, they must enable FLEDGE in their Prebid.js configuration. To provide a high degree of flexiblity for testing, FLEDGE -settings exist at the module level, the bidder level, and the slot level. +Second, they must enable FLEDGE in their Prebid.js configuration. +This is done through module level configuration, but to provide a high degree of flexiblity for testing, FLEDGE settings also exist at the bidder level and slot level. ### Module Configuration This module exposes the following settings: @@ -24,15 +24,20 @@ This module exposes the following settings: |Name |Type |Description |Notes | | :------------ | :------------ | :------------ |:------------ | |enabled | Boolean |Enable/disable the module |Defaults to `false` | +|bidders | Array[String] |Optional list of bidders |Defaults to all bidders | +|defaultForSlots | Number |Default value for `imp.ext.ae` in requests for specified bidders |Should be 1 | -As noted above, FLEDGE support is disabled by default. To enable it, set the `enabled` value to `true` for this module -using the `setConfig` method of Prebid.js: +As noted above, FLEDGE support is disabled by default. To enable it, set the `enabled` value to `true` for this module and configure `defaultForSlots` to be `1` (meaning _Client-side auction_). +using the `setConfig` method of Prebid.js. Optionally, a list of +bidders to apply these settings to may be provided: ```js pbjs.que.push(function() { pbjs.setConfig({ fledgeForGpt: { - enabled: true + enabled: true, + bidders: ['openx', 'rtbhouse'], + defaultForSlots: 1 } }); }); @@ -44,23 +49,25 @@ This module adds the following setting for bidders: |Name |Type |Description |Notes | | :------------ | :------------ | :------------ |:------------ | | fledgeEnabled | Boolean | Enable/disable a bidder to participate in FLEDGE | Defaults to `false` | +|defaultForSlots | Number |Default value for `imp.ext.ae` in requests for specified bidders |Should be 1| -In addition to enabling FLEDGE at the module level, individual bidders must also be enabled. This allows publishers to -selectively test with one or more bidders as they desire. To enable one or more bidders, use the `setBidderConfig` method +Individual bidders may be further included or excluded here using the `setBidderConfig` method of Prebid.js: ```js pbjs.setBidderConfig({ bidders: ["openx"], config: { - fledgeEnabled: true + fledgeEnabled: true, + defaultForSlots: 1 } }); ``` ### AdUnit Configuration -Enabling an adunit for FLEDGE eligibility is accomplished by setting an attribute of the `ortb2Imp` object for that -adunit. +All adunits can be opted-in to FLEDGE in the global config via the `defaultForSlots` parameter. +If needed, adunits can be configured individually by setting an attribute of the `ortb2Imp` object for that +adunit. This attribute will take precedence over `defaultForSlots` setting. |Name |Type |Description |Notes | | :------------ | :------------ | :------------ |:------------ | diff --git a/modules/flippBidAdapter.js b/modules/flippBidAdapter.js new file mode 100644 index 00000000000..0708c90ac0d --- /dev/null +++ b/modules/flippBidAdapter.js @@ -0,0 +1,194 @@ +import {isEmpty, parseUrl} from '../src/utils.js'; +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { BANNER } from '../src/mediaTypes.js'; +import {getStorageManager} from '../src/storageManager.js'; + +/** + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid + * @typedef {import('../src/adapters/bidderFactory.js').ServerResponse} ServerResponse + * @typedef {import('../src/adapters/bidderFactory.js').SyncOptions} SyncOptions + * @typedef {import('../src/adapters/bidderFactory.js').UserSync} UserSync + */ + +const NETWORK_ID = 10922; +const AD_TYPES = [4309, 641]; +const DTX_TYPES = [5061]; +const TARGET_NAME = 'inline'; +const BIDDER_CODE = 'flipp'; +const ENDPOINT = 'https://gateflipp.flippback.com/flyer-locator-service/client_bidding'; +const DEFAULT_TTL = 30; +const DEFAULT_CURRENCY = 'USD'; +const DEFAULT_CREATIVE_TYPE = 'NativeX'; +const VALID_CREATIVE_TYPES = ['DTX', 'NativeX']; +const FLIPP_USER_KEY = 'flipp-uid'; +const COMPACT_DEFAULT_HEIGHT = 600; + +let userKey = null; +export const storage = getStorageManager({bidderCode: BIDDER_CODE}); + +export function getUserKey(options = {}) { + if (userKey) { + return userKey; + } + + // If the partner provides the user key use it, otherwise fallback to cookies + if ('userKey' in options && options.userKey) { + if (isValidUserKey(options.userKey)) { + userKey = options.userKey; + return options.userKey; + } + } + + // Grab from Cookie + const foundUserKey = storage.cookiesAreEnabled(null) && storage.getCookie(FLIPP_USER_KEY, null); + if (foundUserKey && isValidUserKey(foundUserKey)) { + return foundUserKey; + } + + // Generate if none found + userKey = generateUUID(); + + // Set cookie + if (storage.cookiesAreEnabled()) { + storage.setCookie(FLIPP_USER_KEY, userKey); + } + + return userKey; +} + +function isValidUserKey(userKey) { + return typeof userKey === 'string' && !userKey.startsWith('#') && userKey.length > 0; +} + +const generateUUID = () => { + return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => { + const r = (Math.random() * 16) | 0; + const v = c === 'x' ? r : (r & 0x3) | 0x8; + return v.toString(16); + }); +}; + +/** + * Determines if a creativeType is valid + * + * @param {string} creativeType The Creative Type to validate. + * @return string creativeType if this is a valid Creative Type, and 'NativeX' otherwise. + */ +const validateCreativeType = (creativeType) => { + if (creativeType && VALID_CREATIVE_TYPES.includes(creativeType)) { + return creativeType; + } else { + return DEFAULT_CREATIVE_TYPE; + } +}; + +const getAdTypes = (creativeType) => { + if (creativeType === 'DTX') { + return DTX_TYPES; + } + return AD_TYPES; +} + +export const spec = { + code: BIDDER_CODE, + supportedMediaTypes: [BANNER], + /** + * Determines whether or not the given bid request is valid. + * + * @param {BidRequest} bid The bid params to validate. + * @return boolean True if this is a valid bid, and false otherwise. + */ + isBidRequestValid: function(bid) { + return !!(bid.params.siteId) && !!(bid.params.publisherNameIdentifier); + }, + /** + * Make a server request from the list of BidRequests. + * + * @param {BidRequest[]} validBidRequests[] an array of bids + * @param {BidderRequest} bidderRequest master bidRequest object + * @return ServerRequest Info describing the request to the server. + */ + buildRequests: function(validBidRequests, bidderRequest) { + const urlParams = parseUrl(bidderRequest.refererInfo.page).search; + const contentCode = urlParams['flipp-content-code']; + const userKey = getUserKey(validBidRequests[0]?.params); + const placements = validBidRequests.map((bid, index) => { + const options = bid.params.options || {}; + if (!options.hasOwnProperty('startCompact')) { + options.startCompact = true; + } + return { + divName: TARGET_NAME, + networkId: NETWORK_ID, + siteId: bid.params.siteId, + adTypes: getAdTypes(bid.params.creativeType), + count: 1, + ...(!isEmpty(bid.params.zoneIds) && {zoneIds: bid.params.zoneIds}), + properties: { + ...(!isEmpty(contentCode) && {contentCode: contentCode.slice(0, 32)}), + }, + options, + prebid: { + requestId: bid.bidId, + publisherNameIdentifier: bid.params.publisherNameIdentifier, + height: bid.mediaTypes.banner.sizes[index][0], + width: bid.mediaTypes.banner.sizes[index][1], + creativeType: validateCreativeType(bid.params.creativeType), + } + } + }); + return { + method: 'POST', + url: ENDPOINT, + data: { + placements, + url: bidderRequest.refererInfo.page, + user: { + key: userKey, + }, + }, + } + }, + /** + * Unpack the response from the server into a list of bids. + * + * @param {ServerResponse} serverResponse A successful response from the server. + * @param {BidRequest} bidRequest A bid request object + * @return {Bid[]} An array of bids which were nested inside the server. + */ + interpretResponse: function(serverResponse, bidRequest) { + if (!serverResponse?.body) return []; + const placements = bidRequest.data.placements; + const res = serverResponse.body; + if (!isEmpty(res) && !isEmpty(res.decisions) && !isEmpty(res.decisions.inline)) { + return res.decisions.inline.map(decision => { + const placement = placements.find(p => p.prebid.requestId === decision.prebid?.requestId); + const height = placement.options?.startCompact ? COMPACT_DEFAULT_HEIGHT : decision.height; + return { + bidderCode: BIDDER_CODE, + requestId: decision.prebid?.requestId, + cpm: decision.prebid?.cpm, + width: decision.width, + height, + creativeId: decision.adId, + currency: DEFAULT_CURRENCY, + netRevenue: true, + ttl: DEFAULT_TTL, + ad: decision.prebid?.creative, + } + }); + } + return []; + }, + + /** + * Register the user sync pixels which should be dropped after the auction. + * + * @param {SyncOptions} syncOptions Which user syncs are allowed? + * @param {ServerResponse[]} serverResponses List of server's responses. + * @return {UserSync[]} The user syncs which should be dropped. + */ + getUserSyncs: (syncOptions, serverResponses) => [], +} +registerBidder(spec); diff --git a/modules/flippBidAdapter.md b/modules/flippBidAdapter.md new file mode 100644 index 00000000000..e823432a60f --- /dev/null +++ b/modules/flippBidAdapter.md @@ -0,0 +1,45 @@ +# Overview + +``` +Module Name: Flipp Bid Adapter +Module Type: Bidder Adapter +Maintainer: prebid@flipp.com +``` + +# Description + +This module connects publishers to Flipp's Shopper Experience via Prebid.js. + + +# Test parameters + +```javascript +var adUnits = [ + { + code: 'flipp-scroll-ad-content', + mediaTypes: { + banner: { + sizes: [ + [300, 600] + ] + } + }, + bids: [ + { + bidder: 'flipp', + params: { + creativeType: 'NativeX', // Optional, can be one of 'NativeX' (default) or 'DTX' + publisherNameIdentifier: 'wishabi-test-publisher', // Required + siteId: 1192075, // Required + zoneIds: [260678], // Optional + userKey: ``, // Optional, but recommended for better user experience. Can be a cookie, session id or any other user identifier + options: { + startCompact: true, // Optional. Height of the experience will be reduced. Default to true + dwellExpand: true // Optional. Auto expand the experience after a certain time passes. Default to true + } + } + } + ] + } +] +``` diff --git a/modules/fluctBidAdapter.js b/modules/fluctBidAdapter.js index edb750a6b90..f6d97fa7cd8 100644 --- a/modules/fluctBidAdapter.js +++ b/modules/fluctBidAdapter.js @@ -2,22 +2,17 @@ import { _each, deepSetValue, isEmpty } from '../src/utils.js'; import { config } from '../src/config.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; +/** + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + * @typedef {import('../src/adapters/bidderFactory.js').validBidRequests} validBidRequests + */ + const BIDDER_CODE = 'fluct'; const END_POINT = 'https://hb.adingo.jp/prebid'; const VERSION = '1.2'; const NET_REVENUE = true; const TTL = 300; -/** - * See modules/userId/eids.js for supported sources - */ -const SUPPORTED_USER_ID_SOURCES = [ - 'adserver.org', - 'criteo.com', - 'intimatemerger.com', - 'liveramp.com', -]; - export const spec = { code: BIDDER_CODE, aliases: ['adingo'], @@ -36,6 +31,7 @@ export const spec = { * Make a server request from the list of BidRequests. * * @param {validBidRequests[]} - an array of bids. + * @param {bidderRequest} bidderRequest bidder request object. * @return ServerRequest Info describing the request to the server. */ buildRequests: (validBidRequests, bidderRequest) => { @@ -43,16 +39,24 @@ export const spec = { const page = bidderRequest.refererInfo.page; _each(validBidRequests, (request) => { + const impExt = request.ortb2Imp?.ext; const data = Object(); data.page = page; data.adUnitCode = request.adUnitCode; data.bidId = request.bidId; - data.transactionId = request.ortb2Imp?.ext?.tid; data.user = { - eids: (request.userIdAsEids || []).filter((eid) => SUPPORTED_USER_ID_SOURCES.indexOf(eid.source) !== -1) + data: bidderRequest.ortb2?.user?.data ?? [], + eids: [ + ...(request.userIdAsEids ?? []), + ...(bidderRequest.ortb2?.user?.ext?.eids ?? []), + ], }; + if (impExt) { + data.transactionId = impExt.tid; + data.gpid = impExt.gpid ?? impExt.data?.pbadslot ?? impExt.data?.adserver?.adslot; + } if (bidderRequest.gdprConsent) { deepSetValue(data, 'regs.gdpr', { consent: bidderRequest.gdprConsent.consentString, diff --git a/modules/freepassBidAdapter.js b/modules/freepassBidAdapter.js new file mode 100644 index 00000000000..cdcc3c6a4b0 --- /dev/null +++ b/modules/freepassBidAdapter.js @@ -0,0 +1,117 @@ +import {registerBidder} from '../src/adapters/bidderFactory.js'; +import {logMessage} from '../src/utils.js'; +import {BANNER} from '../src/mediaTypes.js'; +import {ortbConverter} from '../libraries/ortbConverter/converter.js' + +const BIDDER_SERVICE_URL = 'https://bidding-dsp.ad-m.asia/dsp/api/bid/s/s/freepass'; + +const converter = ortbConverter({ + context: { + netRevenue: true, + ttl: 30 + } +}); + +function prepareUserInfo(user, freepassId) { + let userInfo = user || {}; + let extendedUserInfo = userInfo.ext || {}; + + if (freepassId.userId) { + userInfo.id = freepassId.userId; + } + + if (freepassId.commonId) { + extendedUserInfo.fuid = freepassId.commonId; + } + userInfo.ext = extendedUserInfo; + + return userInfo; +} + +function prepareDeviceInfo(device, freepassId) { + let deviceInfo = device || {}; + let extendedDeviceInfo = deviceInfo.ext || {}; + + extendedDeviceInfo.is_accurate_ip = 0; + if (freepassId.userIp) { + deviceInfo.ip = freepassId.userIp; + extendedDeviceInfo.is_accurate_ip = 1; + } + deviceInfo.ext = extendedDeviceInfo; + + return deviceInfo; +} + +export const spec = { + code: 'freepass', + supportedMediaTypes: [BANNER], + + isBidRequestValid(bid) { + logMessage('Validating bid: ', bid); + return !(!bid.adUnitCode || !bid.params || !bid.params.publisherId); + }, + + buildRequests(validBidRequests, bidderRequest) { + if (validBidRequests.length === 0) { + logMessage('FreePass BidAdapter has no valid bid requests'); + return []; + } + + logMessage('FreePass BidAdapter is preparing bid request: ', validBidRequests); + logMessage('FreePass BidAdapter is using bidder request: ', bidderRequest); + + const data = converter.toORTB({ + bidderRequest: bidderRequest, + bidRequests: validBidRequests, + context: { mediaType: BANNER } + }); + logMessage('FreePass BidAdapter interpreted ORTB bid request as ', data); + + // Only freepassId is supported + let freepassId = (validBidRequests[0].userId && validBidRequests[0].userId.freepassId) || {}; + data.user = prepareUserInfo(data.user, freepassId); + data.device = prepareDeviceInfo(data.device, freepassId); + + // set site.page & site.publisher + data.site = data.site || {}; + data.site.publisher = data.site.publisher || {}; + // set site.publisher.id. from params.publisherId required + data.site.publisher.id = validBidRequests[0].params.publisherId; + // set site.publisher.domain from params.publisherUrl. optional + data.site.publisher.domain = validBidRequests[0].params?.publisherUrl; + + // set source + data.source = data.source || {}; + data.source.fd = 0; + data.source.tid = validBidRequests.ortb2?.source?.tid; + data.source.pchain = ''; + + // set imp.ext + validBidRequests.forEach((bidRequest, index) => { + data.imp[index].tagId = bidRequest.adUnitCode; + }); + + data.test = validBidRequests[0].test || 0; + + logMessage('FreePass BidAdapter augmented ORTB bid request user: ', data.user); + logMessage('FreePass BidAdapter augmented ORTB bid request device: ', data.device); + + return { + method: 'POST', + url: BIDDER_SERVICE_URL, + data, + options: { withCredentials: false } + }; + }, + + interpretResponse(serverResponse, bidRequest) { + logMessage('FreePass BidAdapter is interpreting server response: ', serverResponse); + logMessage('FreePass BidAdapter is using bid request: ', bidRequest); + const bids = converter.fromORTB({response: serverResponse.body, request: bidRequest.data}).bids; + logMessage('FreePass BidAdapter interpreted ORTB bids as ', bids); + + return bids; + }, +}; + +registerBidder(spec); diff --git a/modules/freepassBidAdapter.md b/modules/freepassBidAdapter.md new file mode 100644 index 00000000000..7b56a469583 --- /dev/null +++ b/modules/freepassBidAdapter.md @@ -0,0 +1,34 @@ +# Overview + +``` +Module Name: FreePass Bidder Adapter +Module Type: Bidder Adapter +Maintainer: fp-hbidding@freebit.net +``` + +# Description + +Connects to FreePass service for bids. Only BANNER is currently supported. + +This BidAdapter requires the FreePass IdSystem to be configured. Please contact FreePass for proper setup. + +# Test Parameters +```javascript + let adUnits = [ + { + code: 'ad-banner-1', // ad slot HTML element ID + mediaTypes: { + banner: { + sizes: [[1024, 1024]] + } + }, + bids: [{ + bidder: 'freepass', + params: { + publisherId: '12345' + } + }] + } + ]; +``` + diff --git a/modules/freepassIdSystem.js b/modules/freepassIdSystem.js index d52c537e800..419aa9ec414 100644 --- a/modules/freepassIdSystem.js +++ b/modules/freepassIdSystem.js @@ -1,8 +1,12 @@ import { submodule } from '../src/hook.js'; -import {generateUUID, logMessage} from '../src/utils.js'; +import { logMessage } from '../src/utils.js'; +import { getCoreStorageManager } from '../src/storageManager.js'; const MODULE_NAME = 'freepassId'; +export const FREEPASS_COOKIE_KEY = '_f_UF8cCRlr'; +export const storage = getCoreStorageManager(MODULE_NAME); + export const freepassIdSubmodule = { name: MODULE_NAME, decode: function (value, config) { @@ -15,7 +19,12 @@ export const freepassIdSubmodule = { logMessage('Getting FreePass ID using config: ' + JSON.stringify(config)); const freepassData = config.params !== undefined ? (config.params.freepassData || {}) : {} - let idObject = {userId: generateUUID()}; + const idObject = {}; + + const userId = storage.getCookie(FREEPASS_COOKIE_KEY); + if (userId !== null) { + idObject.userId = userId; + } if (freepassData.commonId !== undefined) { idObject.commonId = config.params.freepassData.commonId; @@ -29,8 +38,8 @@ export const freepassIdSubmodule = { }, extendId: function (config, consent, cachedIdObject) { - let freepassData = config.params.freepassData; - let hasFreepassData = freepassData !== undefined; + const freepassData = config.params.freepassData; + const hasFreepassData = freepassData !== undefined; if (!hasFreepassData) { logMessage('No Freepass Data. CachedIdObject will not be extended: ' + JSON.stringify(cachedIdObject)); return { @@ -38,12 +47,7 @@ export const freepassIdSubmodule = { }; } - if (freepassData.commonId === cachedIdObject.commonId && freepassData.userIp === cachedIdObject.userIp) { - logMessage('FreePass ID is already up-to-date: ' + JSON.stringify(cachedIdObject)); - return { - id: cachedIdObject - }; - } + const currentCookieId = storage.getCookie(FREEPASS_COOKIE_KEY); logMessage('Extending FreePass ID object: ' + JSON.stringify(cachedIdObject)); logMessage('Extending FreePass ID using config: ' + JSON.stringify(config)); @@ -52,8 +56,8 @@ export const freepassIdSubmodule = { id: { commonId: freepassData.commonId, userIp: freepassData.userIp, - userId: cachedIdObject.userId, - }, + userId: currentCookieId + } }; } }; diff --git a/modules/freewheel-sspBidAdapter.js b/modules/freewheel-sspBidAdapter.js index cd4785cdc78..c4653181fd0 100644 --- a/modules/freewheel-sspBidAdapter.js +++ b/modules/freewheel-sspBidAdapter.js @@ -3,7 +3,13 @@ import { BANNER, VIDEO } from '../src/mediaTypes.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; import { config } from '../src/config.js'; +/** + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid + */ + const BIDDER_CODE = 'freewheel-ssp'; +const GVL_ID = 285; const PROTOCOL = getProtocol(); const FREEWHEEL_ADSSETUP = PROTOCOL + '://ads.stickyadstv.com/www/delivery/swfIndex.php'; @@ -182,8 +188,8 @@ function getCampaignId(xmlNode) { } /** -* returns the top most accessible window -*/ + * returns the top most accessible window + */ function getTopMostWindow() { var res = window; @@ -314,24 +320,25 @@ var getOutstreamScript = function(bid) { export const spec = { code: BIDDER_CODE, + gvlid: GVL_ID, supportedMediaTypes: [BANNER, VIDEO], aliases: ['stickyadstv', 'freewheelssp'], // aliases for freewheel-ssp /** - * Determines whether or not the given bid request is valid. - * - * @param {object} bid The bid to validate. - * @return boolean True if this is a valid bid, and false otherwise. - */ + * Determines whether or not the given bid request is valid. + * + * @param {object} bid The bid to validate. + * @return boolean True if this is a valid bid, and false otherwise. + */ isBidRequestValid: function(bid) { return !!(bid.params.zoneId); }, /** - * Make a server request from the list of BidRequests. - * - * @param {BidRequest[]} bidRequests A non-empty list of bid requests which should be sent to the Server. - * @return ServerRequest Info describing the request to the server. - */ + * Make a server request from the list of BidRequests. + * + * @param {BidRequest[]} bidRequests A non-empty list of bid requests which should be sent to the Server. + * @return ServerRequest Info describing the request to the server. + */ buildRequests: function(bidRequests, bidderRequest) { // var currency = config.getConfig(currency); @@ -382,6 +389,15 @@ export const spec = { requestParams.gpp_sid = bidderRequest.ortb2.regs.gpp_sid; } + // Add content object + if (bidderRequest && bidderRequest.ortb2 && bidderRequest.ortb2.site && bidderRequest.ortb2.site.content && typeof bidderRequest.ortb2.site.content === 'object') { + try { + requestParams._fw_prebid_content = JSON.stringify(bidderRequest.ortb2.site.content); + } catch (error) { + logWarn('PREBID - ' + BIDDER_CODE + ': Unable to stringify the content object: ' + error); + } + } + // Add schain object var schain = currentBidRequest.schain; if (schain) { @@ -463,12 +479,12 @@ export const spec = { }, /** - * Unpack the response from the server into a list of bids. - * - * @param {*} serverResponse A successful response from the server. - * @param {object} request: the built request object containing the initial bidRequest. - * @return {Bid[]} An array of bids which were nested inside the server. - */ + * Unpack the response from the server into a list of bids. + * + * @param {*} serverResponse A successful response from the server. + * @param {object} request: the built request object containing the initial bidRequest. + * @return {Bid[]} An array of bids which were nested inside the server. + */ interpretResponse: function(serverResponse, request) { var bidrequest = request.bidRequest; var playerSize = []; @@ -532,10 +548,11 @@ export const spec = { }; if (bidrequest.mediaTypes.video) { - bidResponse.vastXml = serverResponse; bidResponse.mediaType = 'video'; } + bidResponse.vastXml = serverResponse; + bidResponse.ad = formatAdHTML(bidrequest, playerSize); bidResponses.push(bidResponse); } diff --git a/modules/ftrackIdSystem.js b/modules/ftrackIdSystem.js index 5f09a315b34..1794c3f76f4 100644 --- a/modules/ftrackIdSystem.js +++ b/modules/ftrackIdSystem.js @@ -12,6 +12,13 @@ import {uspDataHandler} from '../src/adapterManager.js'; import {loadExternalScript} from '../src/adloader.js'; import {MODULE_TYPE_UID} from '../src/activities/modules.js'; +/** + * @typedef {import('../modules/userId/index.js').Submodule} Submodule + * @typedef {import('../modules/userId/index.js').SubmoduleConfig} SubmoduleConfig + * @typedef {import('../modules/userId/index.js').ConsentData} ConsentData + * @typedef {import('../modules/userId/index.js').IdResponse} IdResponse + */ + const MODULE_NAME = 'ftrackId'; const LOG_PREFIX = 'FTRACK - '; const LOCAL_STORAGE_EXP_DAYS = 30; @@ -221,6 +228,22 @@ export const ftrackIdSubmodule = { if (usPrivacyVersion == 1 && usPrivacyOptOutSale === 'Y') consentValue = false; return consentValue; + }, + eids: { + 'ftrackId': { + source: 'flashtalking.com', + atype: 1, + getValue: function(data) { + let value = ''; + if (data && data.ext && data.ext.DeviceID) { + value = data.ext.DeviceID; + } + return value; + }, + getUidExt: function(data) { + return data && data.ext; + } + }, } }; diff --git a/modules/gammaBidAdapter.js b/modules/gammaBidAdapter.js index 279eb78812e..40abdd81930 100644 --- a/modules/gammaBidAdapter.js +++ b/modules/gammaBidAdapter.js @@ -1,5 +1,10 @@ import { registerBidder } from '../src/adapters/bidderFactory.js'; +/** + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid + */ + const ENDPOINT = 'https://hb.gammaplatform.com'; const ENDPOINT_USERSYNC = 'https://cm-supply-web.gammaplatform.com'; const BIDDER_CODE = 'gamma'; diff --git a/modules/gdprEnforcement.js b/modules/gdprEnforcement.js index 4bac4b0cf74..5b73ec19e08 100644 --- a/modules/gdprEnforcement.js +++ b/modules/gdprEnforcement.js @@ -5,10 +5,9 @@ import {deepAccess, logError, logWarn} from '../src/utils.js'; import {config} from '../src/config.js'; import adapterManager, {gdprDataHandler} from '../src/adapterManager.js'; -import {find} from '../src/polyfill.js'; import * as events from '../src/events.js'; import CONSTANTS from '../src/constants.json'; -import {GDPR_GVLIDS, VENDORLESS_GVLID} from '../src/consentHandler.js'; +import {GDPR_GVLIDS, VENDORLESS_GVLID, FIRST_PARTY_GVLID} from '../src/consentHandler.js'; import { MODULE_TYPE_ANALYTICS, MODULE_TYPE_BIDDER, @@ -24,44 +23,65 @@ import { import {registerActivityControl} from '../src/activities/rules.js'; import { ACTIVITY_ACCESS_DEVICE, - ACTIVITY_ENRICH_EIDS, + ACTIVITY_ENRICH_EIDS, ACTIVITY_ENRICH_UFPD, ACTIVITY_FETCH_BIDS, ACTIVITY_REPORT_ANALYTICS, - ACTIVITY_SYNC_USER + ACTIVITY_SYNC_USER, ACTIVITY_TRANSMIT_EIDS, ACTIVITY_TRANSMIT_PRECISE_GEO, ACTIVITY_TRANSMIT_UFPD } from '../src/activities/activities.js'; export const STRICT_STORAGE_ENFORCEMENT = 'strictStorageEnforcement'; -const TCF2 = { - 'purpose1': {id: 1, name: 'storage'}, - 'purpose2': {id: 2, name: 'basicAds'}, - 'purpose7': {id: 7, name: 'measurement'} +export const ACTIVE_RULES = { + purpose: {}, + feature: {} }; -/* - These rules would be used if `consentManagement.gdpr.rules` is undefined by the publisher. -*/ -const DEFAULT_RULES = [{ - purpose: 'storage', - enforcePurpose: true, - enforceVendor: true, - vendorExceptions: [] -}, { - purpose: 'basicAds', - enforcePurpose: true, - enforceVendor: true, - vendorExceptions: [] -}]; - -export let purpose1Rule; -export let purpose2Rule; -export let purpose7Rule; - -export let enforcementRules; +const CONSENT_PATHS = { + purpose: 'purpose.consents', + feature: 'specialFeatureOptins' +}; + +const CONFIGURABLE_RULES = { + storage: { + type: 'purpose', + default: { + purpose: 'storage', + enforcePurpose: true, + enforceVendor: true, + vendorExceptions: [] + }, + id: 1, + }, + basicAds: { + type: 'purpose', + id: 2, + default: { + purpose: 'basicAds', + enforcePurpose: true, + enforceVendor: true, + vendorExceptions: [] + } + }, + personalizedAds: { + type: 'purpose', + id: 4, + }, + measurement: { + type: 'purpose', + id: 7, + }, + transmitPreciseGeo: { + type: 'feature', + id: 1, + }, +}; const storageBlocked = new Set(); const biddersBlocked = new Set(); const analyticsBlocked = new Set(); +const ufpdBlocked = new Set(); +const eidsBlocked = new Set(); +const geoBlocked = new Set(); let hooksAdded = false; let strictStorageEnforcement = false; @@ -76,6 +96,9 @@ const GVLID_LOOKUP_PRIORITY = [ const RULE_NAME = 'TCF2'; const RULE_HANDLES = []; +// in JS we do not have access to the GVL; assume that everyone declares legitimate interest for basic ads +const LI_PURPOSES = [2]; + /** * Retrieve a module's GVL ID. */ @@ -88,7 +111,7 @@ export function getGvlid(moduleType, moduleName, fallbackFn) { if (gvlMapping && gvlMapping[moduleName]) { return gvlMapping[moduleName]; } else if (moduleType === MODULE_TYPE_PREBID) { - return VENDORLESS_GVLID; + return moduleName === 'cdep' ? FIRST_PARTY_GVLID : VENDORLESS_GVLID; } else { let {gvlid, modules} = GDPR_GVLIDS.get(moduleName); if (gvlid == null && Object.keys(modules).length > 0) { @@ -140,6 +163,17 @@ export function shouldEnforce(consentData, purpose, name) { return consentData && consentData.gdprApplies; } +function getConsent(consentData, type, id, gvlId) { + let purpose = !!deepAccess(consentData, `vendorData.${CONSENT_PATHS[type]}.${id}`); + let vendor = !!deepAccess(consentData, `vendorData.vendor.consents.${gvlId}`); + + if (type === 'purpose' && LI_PURPOSES.includes(id)) { + purpose ||= !!deepAccess(consentData, `vendorData.purpose.legitimateInterests.${id}`); + vendor ||= !!deepAccess(consentData, `vendorData.vendor.legitimateInterests.${gvlId}`); + } + return {purpose, vendor}; +} + /** * This function takes in a rule and consentData and validates against the consentData provided. Depending on what it returns, * the caller may decide to suppress a TCF-sensitive activity. @@ -150,55 +184,32 @@ export function shouldEnforce(consentData, purpose, name) { * @returns {boolean} */ export function validateRules(rule, consentData, currentModule, gvlId) { - const purposeId = TCF2[Object.keys(TCF2).filter(purposeName => TCF2[purposeName].name === rule.purpose)[0]].id; + const ruleOptions = CONFIGURABLE_RULES[rule.purpose]; // return 'true' if vendor present in 'vendorExceptions' if ((rule.vendorExceptions || []).includes(currentModule)) { return true; } - const vendorConsentRequred = !((gvlId === VENDORLESS_GVLID || (rule.softVendorExceptions || []).includes(currentModule))); - - // get data from the consent string - const purposeConsent = deepAccess(consentData, `vendorData.purpose.consents.${purposeId}`); - const vendorConsent = vendorConsentRequred ? deepAccess(consentData, `vendorData.vendor.consents.${gvlId}`) : true; - const liTransparency = deepAccess(consentData, `vendorData.purpose.legitimateInterests.${purposeId}`); - - /* - Since vendor exceptions have already been handled, the purpose as a whole is allowed if it's not being enforced - or the user has consented. Similar with vendors. - */ - const purposeAllowed = rule.enforcePurpose === false || purposeConsent === true; - const vendorAllowed = rule.enforceVendor === false || vendorConsent === true; - - /* - Few if any vendors should be declaring Legitimate Interest for Device Access (Purpose 1), but some are claiming - LI for Basic Ads (Purpose 2). Prebid.js can't check to see who's declaring what legal basis, so if LI has been - established for Purpose 2, allow the auction to take place and let the server sort out the legal basis calculation. - */ - if (purposeId === 2) { - return (purposeAllowed && vendorAllowed) || (liTransparency === true); + const vendorConsentRequred = rule.enforceVendor && !((gvlId === VENDORLESS_GVLID || (rule.softVendorExceptions || []).includes(currentModule))); + const {purpose, vendor} = getConsent(consentData, ruleOptions.type, ruleOptions.id, gvlId); + + let validation = (!rule.enforcePurpose || purpose) && (!vendorConsentRequred || vendor); + + if (gvlId === FIRST_PARTY_GVLID) { + validation = (!rule.enforcePurpose || !!deepAccess(consentData, `vendorData.publisher.consents.${ruleOptions.id}`)); } - return purposeAllowed && vendorAllowed; + return validation; } -/** - * all activity rules follow the same structure: - * if GDPR is in scope, check configuration for a particular purpose, and if that enables enforcement, - * check against consent data for that purpose and vendor - * - * @param purposeNo TCF purpose number to check for this activity - * @param getEnforcementRule getter for gdprEnforcement rule definition to use - * @param blocked optional set to use for collecting denied vendors - * @param gvlidFallback optional factory function for a gvlid falllback function - */ -function gdprRule(purposeNo, getEnforcementRule, blocked = null, gvlidFallback = () => null) { +function gdprRule(purposeNo, checkConsent, blocked = null, gvlidFallback = () => null) { return function (params) { const consentData = gdprDataHandler.getConsentData(); const modName = params[ACTIVITY_PARAM_COMPONENT_NAME]; + if (shouldEnforce(consentData, purposeNo, modName)) { const gvlid = getGvlid(params[ACTIVITY_PARAM_COMPONENT_TYPE], modName, gvlidFallback(params)); - let allow = !!validateRules(getEnforcementRule(), consentData, modName, gvlid); + let allow = !!checkConsent(consentData, modName, gvlid); if (!allow) { blocked && blocked.add(modName); return {allow}; @@ -207,30 +218,62 @@ function gdprRule(purposeNo, getEnforcementRule, blocked = null, gvlidFallback = }; } -export const accessDeviceRule = ((rule) => { - return function (params) { - // for vendorless (core) storage, do not enforce rules unless strictStorageEnforcement is set - if (params[ACTIVITY_PARAM_COMPONENT_TYPE] === MODULE_TYPE_PREBID && !strictStorageEnforcement) return; - return rule(params); - }; -})(gdprRule(1, () => purpose1Rule, storageBlocked)); - -export const syncUserRule = gdprRule(1, () => purpose1Rule, storageBlocked); -export const enrichEidsRule = gdprRule(1, () => purpose1Rule, storageBlocked); +function singlePurposeGdprRule(purposeNo, blocked = null, gvlidFallback = () => null) { + return gdprRule(purposeNo, (cd, modName, gvlid) => !!validateRules(ACTIVE_RULES.purpose[purposeNo], cd, modName, gvlid), blocked, gvlidFallback); +} -export const fetchBidsRule = ((rule) => { +function exceptPrebidModules(ruleFn) { return function (params) { - if (params[ACTIVITY_PARAM_COMPONENT_TYPE] !== MODULE_TYPE_BIDDER) { + if (params[ACTIVITY_PARAM_COMPONENT_TYPE] === MODULE_TYPE_PREBID) { // TODO: this special case is for the PBS adapter (componentType is 'prebid') // we should check for generic purpose 2 consent & vendor consent based on the PBS vendor's GVL ID; // that is, however, a breaking change and skipped for now return; } + return ruleFn(params); + }; +} + +export const accessDeviceRule = ((rule) => { + return function (params) { + // for vendorless (core) storage, do not enforce rules unless strictStorageEnforcement is set + if (params[ACTIVITY_PARAM_COMPONENT_TYPE] === MODULE_TYPE_PREBID && !strictStorageEnforcement) return; return rule(params); }; -})(gdprRule(2, () => purpose2Rule, biddersBlocked)); +})(singlePurposeGdprRule(1, storageBlocked)); + +export const syncUserRule = singlePurposeGdprRule(1, storageBlocked); +export const enrichEidsRule = singlePurposeGdprRule(1, storageBlocked); +export const fetchBidsRule = exceptPrebidModules(singlePurposeGdprRule(2, biddersBlocked)); +export const reportAnalyticsRule = singlePurposeGdprRule(7, analyticsBlocked, (params) => getGvlidFromAnalyticsAdapter(params[ACTIVITY_PARAM_COMPONENT_NAME], params[ACTIVITY_PARAM_ANL_CONFIG])); +export const ufpdRule = singlePurposeGdprRule(4, ufpdBlocked); + +export const transmitEidsRule = exceptPrebidModules((() => { + // Transmit EID special case: + // by default, legal basis or vendor exceptions for any purpose between 2 and 10 + // (but disregarding enforcePurpose and enforceVendor config) is enough to allow EIDs through + function check2to10Consent(consentData, modName, gvlId) { + for (let pno = 2; pno <= 10; pno++) { + if (ACTIVE_RULES.purpose[pno]?.vendorExceptions?.includes(modName)) { + return true; + } + const {purpose, vendor} = getConsent(consentData, 'purpose', pno, gvlId); + if (purpose && (vendor || ACTIVE_RULES.purpose[pno]?.softVendorExceptions?.includes(modName))) { + return true; + } + } + return false; + } + + const defaultBehavior = gdprRule('2-10', check2to10Consent, eidsBlocked); + const p4Behavior = singlePurposeGdprRule(4, eidsBlocked); + return function () { + const fn = ACTIVE_RULES.purpose[4]?.eidsRequireP4Consent ? p4Behavior : defaultBehavior; + return fn.apply(this, arguments); + }; +})()); -export const reportAnalyticsRule = gdprRule(7, () => purpose7Rule, analyticsBlocked, (params) => getGvlidFromAnalyticsAdapter(params[ACTIVITY_PARAM_COMPONENT_NAME], params[ACTIVITY_PARAM_ANL_CONFIG])); +export const transmitPreciseGeoRule = gdprRule('Special Feature 1', (cd, modName, gvlId) => validateRules(ACTIVE_RULES.feature[1], cd, modName, gvlId), geoBlocked); /** * Compiles the TCF2.0 enforcement results into an object, which is emitted as an event payload to "tcf2Enforcement" event. @@ -243,67 +286,57 @@ function emitTCF2FinalResults() { const tcf2FinalResults = { storageBlocked: formatSet(storageBlocked), biddersBlocked: formatSet(biddersBlocked), - analyticsBlocked: formatSet(analyticsBlocked) + analyticsBlocked: formatSet(analyticsBlocked), + ufpdBlocked: formatSet(ufpdBlocked), + eidsBlocked: formatSet(eidsBlocked), + geoBlocked: formatSet(geoBlocked) }; events.emit(CONSTANTS.EVENTS.TCF2_ENFORCEMENT, tcf2FinalResults); - [storageBlocked, biddersBlocked, analyticsBlocked].forEach(el => el.clear()); + [storageBlocked, biddersBlocked, analyticsBlocked, ufpdBlocked, eidsBlocked, geoBlocked].forEach(el => el.clear()); } events.on(CONSTANTS.EVENTS.AUCTION_END, emitTCF2FinalResults); -/* - Set of callback functions used to detect presence of a TCF rule, passed as the second argument to find(). -*/ -const hasPurpose1 = (rule) => { - return rule.purpose === TCF2.purpose1.name; -}; -const hasPurpose2 = (rule) => { - return rule.purpose === TCF2.purpose2.name; -}; -const hasPurpose7 = (rule) => { - return rule.purpose === TCF2.purpose7.name; -}; - /** * A configuration function that initializes some module variables, as well as adds hooks * @param {Object} config - GDPR enforcement config object */ export function setEnforcementConfig(config) { - const rules = deepAccess(config, 'gdpr.rules'); + let rules = deepAccess(config, 'gdpr.rules'); if (!rules) { logWarn('TCF2: enforcing P1 and P2 by default'); - enforcementRules = DEFAULT_RULES; - } else { - enforcementRules = rules; } + rules = Object.fromEntries((rules || []).map(r => [r.purpose, r])); strictStorageEnforcement = !!deepAccess(config, STRICT_STORAGE_ENFORCEMENT); - purpose1Rule = find(enforcementRules, hasPurpose1); - purpose2Rule = find(enforcementRules, hasPurpose2); - purpose7Rule = find(enforcementRules, hasPurpose7); - - if (!purpose1Rule) { - purpose1Rule = DEFAULT_RULES[0]; - } - - if (!purpose2Rule) { - purpose2Rule = DEFAULT_RULES[1]; - } + Object.entries(CONFIGURABLE_RULES).forEach(([name, opts]) => { + ACTIVE_RULES[opts.type][opts.id] = rules[name] ?? opts.default; + }); if (!hooksAdded) { - if (purpose1Rule) { + if (ACTIVE_RULES.purpose[1] != null) { hooksAdded = true; RULE_HANDLES.push(registerActivityControl(ACTIVITY_ACCESS_DEVICE, RULE_NAME, accessDeviceRule)); RULE_HANDLES.push(registerActivityControl(ACTIVITY_SYNC_USER, RULE_NAME, syncUserRule)); RULE_HANDLES.push(registerActivityControl(ACTIVITY_ENRICH_EIDS, RULE_NAME, enrichEidsRule)); } - if (purpose2Rule) { + if (ACTIVE_RULES.purpose[2] != null) { RULE_HANDLES.push(registerActivityControl(ACTIVITY_FETCH_BIDS, RULE_NAME, fetchBidsRule)); } - if (purpose7Rule) { + if (ACTIVE_RULES.purpose[4] != null) { + RULE_HANDLES.push( + registerActivityControl(ACTIVITY_TRANSMIT_UFPD, RULE_NAME, ufpdRule), + registerActivityControl(ACTIVITY_ENRICH_UFPD, RULE_NAME, ufpdRule) + ); + } + if (ACTIVE_RULES.purpose[7] != null) { RULE_HANDLES.push(registerActivityControl(ACTIVITY_REPORT_ANALYTICS, RULE_NAME, reportAnalyticsRule)); } + if (ACTIVE_RULES.feature[1] != null) { + RULE_HANDLES.push(registerActivityControl(ACTIVITY_TRANSMIT_PRECISE_GEO, RULE_NAME, transmitPreciseGeoRule)); + } + RULE_HANDLES.push(registerActivityControl(ACTIVITY_TRANSMIT_EIDS, RULE_NAME, transmitEidsRule)); } } diff --git a/modules/genericAnalyticsAdapter.js b/modules/genericAnalyticsAdapter.js index b52cb7e5464..7f721863912 100644 --- a/modules/genericAnalyticsAdapter.js +++ b/modules/genericAnalyticsAdapter.js @@ -1,6 +1,6 @@ import AnalyticsAdapter from '../libraries/analyticsAdapter/AnalyticsAdapter.js'; import {prefixLog, isPlainObject} from '../src/utils.js'; -import * as CONSTANTS from '../src/constants.json'; +import {has as hasEvent} from '../src/events.js'; import adapterManager from '../src/adapterManager.js'; import {ajaxBuilder} from '../src/ajax.js'; @@ -48,12 +48,12 @@ export function GenericAnalytics() { return false; } for (const [event, handler] of Object.entries(options.events)) { - if (!CONSTANTS.EVENTS.hasOwnProperty(event)) { + if (!hasEvent(event)) { logWarn(`options.events.${event} does not match any known Prebid event`); - if (typeof handler !== 'function') { - logError(`options.events.${event} must be a function`); - return false; - } + } + if (typeof handler !== 'function') { + logError(`options.events.${event} must be a function`); + return false; } } } diff --git a/modules/geoedgeRtdProvider.js b/modules/geoedgeRtdProvider.js index 6f910632fbc..a2ed71a898c 100644 --- a/modules/geoedgeRtdProvider.js +++ b/modules/geoedgeRtdProvider.js @@ -17,9 +17,16 @@ import { submodule } from '../src/hook.js'; import { ajax } from '../src/ajax.js'; -import { generateUUID, insertElement, isEmpty, logError } from '../src/utils.js'; +import { generateUUID, createInvisibleIframe, insertElement, isEmpty, logError } from '../src/utils.js'; import * as events from '../src/events.js'; import CONSTANTS from '../src/constants.json'; +import { loadExternalScript } from '../src/adloader.js'; +import { auctionManager } from '../src/auctionManager.js'; +import { getRefererInfo } from '../src/refererDetection.js'; + +/** + * @typedef {import('../modules/rtdModule/index.js').RtdSubmodule} RtdSubmodule + */ /** @type {string} */ const SUBMODULE_NAME = 'geoedge'; @@ -33,9 +40,13 @@ const PV_ID = generateUUID(); /** @type {string} */ const HOST_NAME = 'https://rumcdn.geoedge.be'; /** @type {string} */ -const FILE_NAME = 'grumi.js'; +const FILE_NAME_CLIENT = 'grumi.js'; +/** @type {string} */ +const FILE_NAME_INPAGE = 'grumi-ip.js'; +/** @type {function} */ +export let getClientUrl = (key) => `${HOST_NAME}/${key}/${FILE_NAME_CLIENT}`; /** @type {function} */ -export let getClientUrl = (key) => `${HOST_NAME}/${key}/${FILE_NAME}`; +export let getInPageUrl = (key) => `${HOST_NAME}/${key}/${FILE_NAME_INPAGE}`; /** @type {string} */ export let wrapper /** @type {boolean} */; @@ -63,17 +74,38 @@ export function setWrapper(responseText) { wrapper = responseText; } +export function getInitialParams(key) { + let refererInfo = getRefererInfo(); + let params = { + wver: 'pbjs', + wtype: 'pbjs-module', + key, + meta: { + topUrl: refererInfo.page + }, + site: refererInfo.domain, + pimp: PV_ID, + fsRan: true, + frameApi: true + }; + return params; +} + +export function markAsLoaded() { + preloaded = true; +} + /** * preloads the client - * @param {string} key + * @param {string} key */ export function preloadClient(key) { - let link = document.createElement('link'); - link.rel = 'preload'; - link.as = 'script'; - link.href = getClientUrl(key); - link.onload = () => { preloaded = true }; - insertElement(link); + let iframe = createInvisibleIframe(); + iframe.id = 'grumiFrame'; + insertElement(iframe); + iframe.contentWindow.grumi = getInitialParams(key); + let url = getClientUrl(key); + loadExternalScript(url, SUBMODULE_NAME, markAsLoaded, iframe.contentDocument); } /** @@ -97,7 +129,7 @@ export function wrapHtml(wrapper, html) { * @param {string} key * @return {Object} */ -function getMacros(bid, key) { +export function getMacros(bid, key) { return { '${key}': key, '%%ADUNIT%%': bid.adUnitCode, @@ -110,7 +142,9 @@ function getMacros(bid, key) { '%_hbadomains': bid.meta && bid.meta.advertiserDomains, '%%PATTERN:hb_pb%%': bid.pbHg, '%%SITE%%': location.hostname, - '%_pimp%': PV_ID + '%_pimp%': PV_ID, + '%_hbCpm!': bid.cpm, + '%_hbCurrency!': bid.currency }; } @@ -177,7 +211,8 @@ function isSupportedBidder(bidder, paramsBidders) { function shouldWrap(bid, params) { let supportedBidder = isSupportedBidder(bid.bidderCode, params.bidders); let donePreload = params.wap ? preloaded : true; - return wrapperReady && supportedBidder && donePreload; + let isGPT = params.gpt; + return wrapperReady && supportedBidder && donePreload && !isGPT; } function conditionallyWrap(bidResponse, config, userConsent) { @@ -187,31 +222,55 @@ function conditionallyWrap(bidResponse, config, userConsent) { } } +function isBillingMessage(data, params) { + return data.key === params.key && data.impression; +} + /** - * Fire billable events for applicable bids + * Fire billable events when our client sends a message + * Messages will be sent only when: + * a. applicable bids are wrapped + * b. our code laoded and executed sucesfully */ function fireBillableEventsForApplicableBids(params) { - events.on(CONSTANTS.EVENTS.BID_WON, function (winningBid) { - if (shouldWrap(winningBid, params)) { + window.addEventListener('message', function (message) { + let data = message.data; + if (isBillingMessage(data, params)) { + let winningBid = auctionManager.findBidByAdId(data.adId); events.emit(CONSTANTS.EVENTS.BILLABLE_EVENT, { vendor: SUBMODULE_NAME, - billingId: generateUUID(), - type: 'impression', - transactionId: winningBid.transactionId, - auctionId: winningBid.auctionId, - bidId: winningBid.requestId + billingId: data.impressionId, + type: winningBid ? 'impression' : data.type, + transactionId: winningBid?.transactionId || data.transactionId, + auctionId: winningBid?.auctionId || data.auctionId, + bidId: winningBid?.requestId || data.requestId }); } }); } +/** + * Loads Geoedge in page script that monitors all ad slots created by GPT + * @param {Object} params + */ +function setupInPage(params) { + window.grumi = params; + window.grumi.fromPrebid = true; + loadExternalScript(getInPageUrl(params.key), SUBMODULE_NAME); +} + function init(config, userConsent) { let params = config.params; if (!params || !params.key) { logError('missing key for geoedge RTD module provider'); return false; } - preloadClient(params.key); + if (params.gpt) { + setupInPage(params); + } else { + fetchWrapper(setWrapper); + preloadClient(params.key); + } fireBillableEventsForApplicableBids(params); return true; } @@ -219,17 +278,12 @@ function init(config, userConsent) { /** @type {RtdSubmodule} */ export const geoedgeSubmodule = { /** - * used to link submodule with realTimeData - * @type {string} - */ + * used to link submodule with realTimeData + * @type {string} + */ name: SUBMODULE_NAME, init, onBidResponseEvent: conditionallyWrap }; -export function beforeInit() { - fetchWrapper(setWrapper); - submodule('realTimeData', geoedgeSubmodule); -} - -beforeInit(); +submodule('realTimeData', geoedgeSubmodule); diff --git a/modules/geoedgeRtdProvider.md b/modules/geoedgeRtdProvider.md index 5414606612c..cdf913b8893 100644 --- a/modules/geoedgeRtdProvider.md +++ b/modules/geoedgeRtdProvider.md @@ -5,7 +5,7 @@ Module Type: Rtd Provider Maintainer: guy.books@geoedge.com The Geoedge Realtime module lets publishers block bad ads such as automatic redirects, malware, offensive creatives and landing pages. -To use this module, you'll need to work with [Geoedge](https://www.geoedge.com/publishers-real-time-protection/) to get an account and cutomer key. +To use this module, you'll need to work with [Geoedge](https://www.geoedge.com/publishers-real-time-protection/) to get an account and customer key. ## Integration @@ -49,6 +49,7 @@ Parameters details: |params.key | String | Customer key |Required, contact Geoedge to get your key | |params.bidders | Object | Bidders to monitor |Optional, list of bidder to include / exclude from monitoring. Omitting this will monitor bids from all bidders. | |params.wap |Boolean |Wrap after preload |Optional, defaults to `false`. Set to `true` if you want to monitor only after the module has preloaded the monitoring client. | +|params.gpt |Boolean |Wrap all GPT ad slots |Optional, defaults to `false`. Set to `true` if you want to monitor all Google Publisher Tag ad slots, regaedless if the winning bid comes from Prebid or Google Ad Manager (Direct, Adx, Adesnse, Open Bidding, etc). | ## Example diff --git a/modules/geolocationRtdProvider.js b/modules/geolocationRtdProvider.js new file mode 100644 index 00000000000..6bfed7ee934 --- /dev/null +++ b/modules/geolocationRtdProvider.js @@ -0,0 +1,65 @@ +import {submodule} from '../src/hook.js'; +import {isFn, logError, deepAccess, deepSetValue, logInfo, logWarn, timestamp} from '../src/utils.js'; +import { ACTIVITY_TRANSMIT_PRECISE_GEO } from '../src/activities/activities.js'; +import { MODULE_TYPE_RTD } from '../src/activities/modules.js'; +import { isActivityAllowed } from '../src/activities/rules.js'; +import { activityParams } from '../src/activities/activityParams.js'; +import {VENDORLESS_GVLID} from '../src/consentHandler.js'; + +let permissionsAvailable = true; +let geolocation; +function getGeolocationData(requestBidsObject, onDone, providerConfig, userConsent) { + let done = false; + if (!permissionsAvailable) { + logWarn('permission for geolocation receiving was denied'); + return complete() + }; + if (!isActivityAllowed(ACTIVITY_TRANSMIT_PRECISE_GEO, activityParams(MODULE_TYPE_RTD, 'geolocation'))) { + logWarn('permission for geolocation receiving was denied by CMP'); + return complete() + }; + const requestPermission = deepAccess(providerConfig, 'params.requestPermission') === true; + navigator.permissions.query({ + name: 'geolocation', + }).then(permission => { + if (permission.state !== 'granted' && !requestPermission) return complete(); + navigator.geolocation.getCurrentPosition(geo => { + geolocation = geo; + complete(); + }); + }); + function complete() { + if (done) return; + done = true; + if (geolocation) { + deepSetValue(requestBidsObject, 'ortb2Fragments.global.device.geo', { + lat: geolocation.coords.latitude, + lon: geolocation.coords.longitude, + lastfix: Math.round((timestamp() - geolocation.timestamp) / 1000), + type: 1 + }); + logInfo('geolocation was successfully received ', requestBidsObject.ortb2Fragments.global.device.geo) + } + onDone(); + } +} +function init(moduleConfig) { + geolocation = void 0; + if (!isFn(navigator?.permissions?.query) || !isFn(navigator?.geolocation?.getCurrentPosition || !navigator?.permissions?.query)) { + logError('geolocation is not defined'); + permissionsAvailable = false; + } else { + permissionsAvailable = true; + } + return permissionsAvailable; +} +export const geolocationSubmodule = { + name: 'geolocation', + gvlid: VENDORLESS_GVLID, + getBidRequestData: getGeolocationData, + init: init, +}; +function registerSubModule() { + submodule('realTimeData', geolocationSubmodule); +} +registerSubModule(); diff --git a/modules/getintentBidAdapter.js b/modules/getintentBidAdapter.js index 25322d81f9b..7353a1c1f67 100644 --- a/modules/getintentBidAdapter.js +++ b/modules/getintentBidAdapter.js @@ -1,6 +1,11 @@ -import { getBidIdParameter, isFn, isInteger } from '../src/utils.js'; +import {getBidIdParameter, isFn, isInteger} from '../src/utils.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; +/** + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid + */ + const BIDDER_CODE = 'getintent'; const IS_NET_REVENUE = true; const BID_HOST = 'px.adhigh.net'; @@ -38,7 +43,7 @@ export const spec = { * * @param {BidRequest} bid The bid to validate. * @return {boolean} True if this is a valid bid, and false otherwise. - * */ + */ isBidRequestValid: function(bid) { return !!(bid && bid.params && bid.params.pid && bid.params.tid); }, @@ -108,7 +113,7 @@ function buildUrl(bid) { * * @param {BidRequest} bidRequest. * @return {object} GI bid request. - * */ + */ function buildGiBidRequest(bidRequest) { let giBidRequest = { bid_id: bidRequest.bidId, @@ -191,7 +196,7 @@ function addOptional(params, request, props) { /** * @param {String} s The string representing a size (e.g. "300x250"). * @return {Number[]} An array with two elements: [width, height] (e.g.: [300, 250]). - * */ + */ function parseSize(s) { return s.split('x').map(Number); } @@ -200,7 +205,7 @@ function parseSize(s) { * @param {Array} sizes An array of sizes/numbers to be joined into single string. * May be an array (e.g. [300, 250]) or array of arrays (e.g. [[300, 250], [640, 480]]. * @return {String} The string with sizes, e.g. array of sizes [[50, 50], [80, 80]] becomes "50x50,80x80" string. - * */ + */ function produceSize (sizes) { function sizeToStr(s) { if (Array.isArray(s) && s.length === 2 && isInteger(s[0]) && isInteger(s[1])) { diff --git a/modules/gjirafaBidAdapter.js b/modules/gjirafaBidAdapter.js index 91ed5c9b3fb..9259010ac78 100644 --- a/modules/gjirafaBidAdapter.js +++ b/modules/gjirafaBidAdapter.js @@ -2,6 +2,13 @@ import { registerBidder } from '../src/adapters/bidderFactory.js'; import { getStorageManager } from '../src/storageManager.js'; import { BANNER, VIDEO } from '../src/mediaTypes.js'; +/** + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid + * @typedef {import('../src/adapters/bidderFactory.js').ServerResponse} ServerResponse + * @typedef {import('../src/adapters/bidderFactory.js').validBidRequests} validBidRequests + */ + const BIDDER_CODE = 'gjirafa'; const ENDPOINT_URL = 'https://central.gjirafa.com/bid'; const DIMENSION_SEPARATOR = 'x'; @@ -116,11 +123,11 @@ export const spec = { }; /** -* Generate size param for bid request using sizes array -* -* @param {Array} sizes Possible sizes for the ad unit. -* @return {string} Processed sizes param to be used for the bid request. -*/ + * Generate size param for bid request using sizes array + * + * @param {Array} sizes Possible sizes for the ad unit. + * @return {string} Processed sizes param to be used for the bid request. + */ function generateSizeParam(sizes) { return sizes.map(size => size.join(DIMENSION_SEPARATOR)).join(SIZE_SEPARATOR); } diff --git a/modules/gmosspBidAdapter.js b/modules/gmosspBidAdapter.js index 8c90d0cccfe..c4b8cd819e0 100644 --- a/modules/gmosspBidAdapter.js +++ b/modules/gmosspBidAdapter.js @@ -1,17 +1,25 @@ import { createTrackPixelHtml, deepAccess, - deepSetValue, - getBidIdParameter, + deepSetValue, getBidIdParameter, getDNT, getWindowTop, isEmpty, - logError, - tryAppendQueryString + logError } from '../src/utils.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import {config} from '../src/config.js'; import {BANNER} from '../src/mediaTypes.js'; +import {tryAppendQueryString} from '../libraries/urlUtils/urlUtils.js'; + +/** + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid + * @typedef {import('../src/adapters/bidderFactory.js').ServerResponse} ServerResponse + * @typedef {import('../src/adapters/bidderFactory.js').SyncOptions} SyncOptions + * @typedef {import('../src/adapters/bidderFactory.js').UserSync} UserSync + * @typedef {import('../src/adapters/bidderFactory.js').validBidRequests} validBidRequests + */ const BIDDER_CODE = 'gmossp'; const ENDPOINT = 'https://sp.gmossp-sp.jp/hb/prebid/query.ad'; diff --git a/modules/gnetBidAdapter.js b/modules/gnetBidAdapter.js index 38e96c183b9..4718438b9bb 100644 --- a/modules/gnetBidAdapter.js +++ b/modules/gnetBidAdapter.js @@ -4,6 +4,12 @@ import { BANNER } from '../src/mediaTypes.js'; import { getStorageManager } from '../src/storageManager.js'; import {ajax} from '../src/ajax.js'; +/** + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid + * @typedef {import('../src/adapters/bidderFactory.js').validBidRequests} validBidRequests + */ + const BIDDER_CODE = 'gnet'; const ENDPOINT = 'https://service.gnetrtb.com/api'; const storage = getStorageManager({bidderCode: BIDDER_CODE}); diff --git a/modules/goldbachBidAdapter.js b/modules/goldbachBidAdapter.js index 4768931950c..ff394e69e54 100644 --- a/modules/goldbachBidAdapter.js +++ b/modules/goldbachBidAdapter.js @@ -1,15 +1,9 @@ import {Renderer} from '../src/Renderer.js'; import { - chunk, - convertCamelToUnderscore, - convertTypes, createTrackPixelHtml, deepAccess, deepClone, - fill, getBidRequest, - getMaxValueFromArray, - getMinValueFromArray, getParameterByName, isArray, isArrayOfNums, @@ -29,9 +23,17 @@ import {auctionManager} from '../src/auctionManager.js'; import {find, includes} from '../src/polyfill.js'; import {INSTREAM, OUTSTREAM} from '../src/video.js'; import {hasPurpose1Consent} from '../src/utils/gpdr.js'; -import { convertOrtbRequestToProprietaryNative } from '../src/native.js'; -import { APPNEXUS_CATEGORY_MAPPING } from '../libraries/categoryTranslationMapping/index.js'; -import {getANKeywordParam, transformBidderParamKeywords} from '../libraries/appnexusKeywords/anKeywords.js'; +import {convertOrtbRequestToProprietaryNative} from '../src/native.js'; +import {APPNEXUS_CATEGORY_MAPPING} from '../libraries/categoryTranslationMapping/index.js'; +import {getANKeywordParam, transformBidderParamKeywords} from '../libraries/appnexusUtils/anKeywords.js'; +import {convertCamelToUnderscore, fill} from '../libraries/appnexusUtils/anUtils.js'; +import {convertTypes} from '../libraries/transformParamsUtils/convertTypes.js'; +import {chunk} from '../libraries/chunk/chunk.js'; + +/** + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid + */ const BIDDER_CODE = 'goldbach'; const URL = 'https://ib.adnxs.com/ut/v3/prebid'; @@ -971,7 +973,7 @@ function createAdPodRequest(tags, adPodBid) { const { durationRangeSec, requireExactDuration } = adPodBid.mediaTypes.video; const numberOfPlacements = getAdPodPlacementNumber(adPodBid.mediaTypes.video); - const maxDuration = getMaxValueFromArray(durationRangeSec); + const maxDuration = Math.max(...durationRangeSec); const tagToDuplicate = tags.filter(tag => tag.uuid === adPodBid.bidId); let request = fill(...tagToDuplicate, numberOfPlacements); @@ -997,7 +999,7 @@ function createAdPodRequest(tags, adPodBid) { function getAdPodPlacementNumber(videoParams) { const { adPodDurationSec, durationRangeSec, requireExactDuration } = videoParams; - const minAllowedDuration = getMinValueFromArray(durationRangeSec); + const minAllowedDuration = Math.min(...durationRangeSec); const numberOfPlacements = Math.floor(adPodDurationSec / minAllowedDuration); return requireExactDuration diff --git a/modules/goldfishAdsRtdProvider.js b/modules/goldfishAdsRtdProvider.js new file mode 100755 index 00000000000..c595e361968 --- /dev/null +++ b/modules/goldfishAdsRtdProvider.js @@ -0,0 +1,198 @@ +import { ajax } from '../src/ajax.js'; +import { submodule } from '../src/hook.js'; +import { getStorageManager } from '../src/storageManager.js'; +import { deepAccess } from '../src/utils.js'; + +/** + * @typedef {import('../modules/rtdModule/index.js').RtdSubmodule} RtdSubmodule + */ + +export const MODULE_NAME = 'goldfishAdsRtd'; +export const MODULE_TYPE = 'realTimeData'; +export const ENDPOINT_URL = 'https://prebid.goldfishads.com/iab-segments'; +export const DATA_STORAGE_KEY = 'goldfishads_data'; +export const DATA_STORAGE_TTL = 1800 * 1000// TTL in seconds + +export const ADAPTER_VERSION = '1.0'; + +export const storage = getStorageManager({ + gvlid: null, + moduleName: MODULE_NAME, + moduleType: MODULE_TYPE, +}); + +/** + * + * @param {{response: string[]} } response + * @returns + */ +export const manageCallbackResponse = (response) => { + try { + const foo = JSON.parse(response.response); + if (!Array.isArray(foo)) throw new Error('Invalid response'); + const enrichedResponse = { + ext: { + segtax: 4 + }, + segment: foo.map((segment) => { return { id: segment } }), + }; + const output = { + name: 'goldfishads.com', + ...enrichedResponse, + }; + return output; + } catch (e) { + throw e; + }; +}; + +/** + * @param {string} key + * @returns { Promise<{name: 'goldfishads.com', ext: { segtag: 4 }, segment: string[]}> } + */ + +const getTargetingDataFromApi = (key) => { + return new Promise((resolve, reject) => { + const requestOptions = { + customHeaders: { + 'Accept': 'application/json' + } + } + const callbacks = { + success(responseText, response) { + try { + const output = manageCallbackResponse(response); + resolve(output); + } catch (e) { + reject(e); + } + }, + error(error) { + reject(error); + } + }; + ajax(`${ENDPOINT_URL}?key=${key}`, callbacks, null, requestOptions) + }) +}; + +/** + * @returns {{ + * name: 'golfishads.com', + * ext: { segtax: 4}, + * segment: string[] + * } | null } + */ +export const getStorageData = () => { + const now = new Date(); + const data = storage.getDataFromLocalStorage(DATA_STORAGE_KEY); + if (data === null) return null; + try { + const foo = JSON.parse(data); + if (now.getTime() > foo.expiry) return null; + return foo.targeting; + } catch (e) { + return null; + } +}; + +/** + * @param { { key: string } } payload + * @returns {Promise<{ + * name: string, + * ext: { segtax: 4}, + * segment: string[] + * }> | null + * } + */ + +const getTargetingData = (payload) => new Promise((resolve) => { + const targeting = getStorageData(); + if (targeting === null) { + getTargetingDataFromApi(payload.key) + .then((response) => { + const now = new Date() + const data = { + targeting: response, + expiry: now.getTime() + DATA_STORAGE_TTL, + }; + storage.setDataInLocalStorage(DATA_STORAGE_KEY, JSON.stringify(data)); + resolve(response); + }) + .catch((e) => { + resolve(null); + }); + } else { + resolve(targeting); + } +}) + +/** + * + * @param {*} config + * @param {*} userConsent + * @returns {boolean} + */ + +const init = (config, userConsent) => { + if (!config.params || !config.params.key) return false; + // return { type: (typeof config.params.key === 'string') }; + if (!(typeof config.params.key === 'string')) return false; + return true; +}; + +/** + * + * @param {{ + * name: string, + * ext: { segtax: 4}, + * segment: {id: string}[] + * } | null } userData + * @param {*} reqBidsConfigObj + * @returns + */ +export const updateUserData = (userData, reqBidsConfigObj) => { + if (userData === null) return; + const bidders = ['appnexus', 'rubicon', 'nexx360']; + for (let i = 0; i < bidders.length; i++) { + const bidderCode = bidders[i]; + const originalConfig = deepAccess(reqBidsConfigObj, `ortb2Fragments.bidder[${bidderCode}].user.data`) || []; + const userConfig = [ + ...originalConfig, + userData, + ]; + reqBidsConfigObj.ortb2Fragments = reqBidsConfigObj.ortb2Fragments || {}; + reqBidsConfigObj.ortb2Fragments.bidder = reqBidsConfigObj.ortb2Fragments.bidder || {}; + reqBidsConfigObj.ortb2Fragments.bidder[bidderCode] = reqBidsConfigObj.ortb2Fragments.bidder[bidderCode] || {}; + reqBidsConfigObj.ortb2Fragments.bidder[bidderCode].user = reqBidsConfigObj.ortb2Fragments.bidder[bidderCode].user = {}; + reqBidsConfigObj.ortb2Fragments.bidder[bidderCode].user.data = reqBidsConfigObj.ortb2Fragments.bidder[bidderCode].user.data || userConfig; + } + return reqBidsConfigObj; +} + +/** + * + * @param {*} reqBidsConfigObj + * @param {*} callback + * @param {*} moduleConfig + * @param {*} userConsent + * @returns {void} + */ +const getBidRequestData = (reqBidsConfigObj, callback, moduleConfig, userConsent) => { + const payload = { + key: moduleConfig.params.key, + }; + getTargetingData(payload) + .then((userData) => { + updateUserData(userData, reqBidsConfigObj); + callback(); + }); +}; + +/** @type {RtdSubmodule} */ +export const goldfishAdsSubModule = { + name: MODULE_NAME, + init, + getBidRequestData, +}; + +submodule(MODULE_TYPE, goldfishAdsSubModule); diff --git a/modules/goldfishAdsRtdProvider.md b/modules/goldfishAdsRtdProvider.md new file mode 100755 index 00000000000..4625c9a7988 --- /dev/null +++ b/modules/goldfishAdsRtdProvider.md @@ -0,0 +1,48 @@ +# Goldfish Ads Real-time Data Submodule + +## Overview + + Module Name: Goldfish Ads Rtd Provider + Module Type: Rtd Provider + Maintainer: keith@goldfishads.com + +## Description + +This RTD module provides access to the Goldfish Ads Geograph, which leverages geographic and temporal data on a privcay-first platform. This module works without using cookies, PII, emails, or device IDs across all website traffic, including unauthenticated users, and adds audience data into bid requests to increase scale and yields. + +## Usage + +### Build +``` +gulp build --modules="rtdModule,goldfishAdsRtdProvider,appnexusBidAdapter,..." +``` + +> Note that the global RTD module, `rtdModule`, is a prerequisite of the Goldfish Ads RTD module. + +### Configuration + +Use `setConfig` to instruct Prebid.js to initialize the Goldfish Ads RTD module, as specified below. + +This module is configured as part of the `realTimeData.dataProviders` + +```javascript +pbjs.setConfig({ + realTimeData: { + auctionDelay: 300, + dataProviders: [{ + name: 'goldfishAds', + waitForIt: true, + params: { + key: 'testkey' + } + }] + } +}) +``` + +### Parameters +| Name | Type | Description | Default | +|:-----------------|:----------------------------------------|:-----------------------------------------------------------------------------|:-----------------------| +| name | String | Real time data module name | Always 'goldfishAds' | +| waitForIt | Boolean | Set to true to maximize chance for bidder enrichment, used with auctionDelay | `false` | +| params.key | String | Your key id issued by Goldfish Ads | | diff --git a/modules/gothamadsBidAdapter.js b/modules/gothamadsBidAdapter.js index 9f44a54460f..ab59c6febec 100644 --- a/modules/gothamadsBidAdapter.js +++ b/modules/gothamadsBidAdapter.js @@ -4,6 +4,11 @@ import { BANNER, NATIVE, VIDEO } from '../src/mediaTypes.js'; import { config } from '../src/config.js'; import { convertOrtbRequestToProprietaryNative } from '../src/native.js'; +/** + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid + */ + const BIDDER_CODE = 'gothamads'; const ACCOUNTID_MACROS = '[account_id]'; const URL_ENDPOINT = `https://us-e-node1.gothamads.com/bid?pass=${ACCOUNTID_MACROS}&integration=prebidjs`; diff --git a/modules/gppControl_usnat.js b/modules/gppControl_usnat.js new file mode 100644 index 00000000000..b38fc1a9d29 --- /dev/null +++ b/modules/gppControl_usnat.js @@ -0,0 +1,11 @@ +import {config} from '../src/config.js'; +import {setupRules} from '../libraries/mspa/activityControls.js'; + +let setupDone = false; + +config.getConfig('consentManagement', (cfg) => { + if (cfg?.consentManagement?.gpp != null && !setupDone) { + setupRules('usnat', [7]); + setupDone = true; + } +}) diff --git a/modules/gppControl_usstates.js b/modules/gppControl_usstates.js new file mode 100644 index 00000000000..bc2b434e085 --- /dev/null +++ b/modules/gppControl_usstates.js @@ -0,0 +1,176 @@ +import {config} from '../src/config.js'; +import {setupRules} from '../libraries/mspa/activityControls.js'; +import {deepSetValue, prefixLog} from '../src/utils.js'; + +const FIELDS = { + Version: 0, + Gpc: 0, + SharingNotice: 0, + SaleOptOutNotice: 0, + SharingOptOutNotice: 0, + TargetedAdvertisingOptOutNotice: 0, + SensitiveDataProcessingOptOutNotice: 0, + SensitiveDataLimitUseNotice: 0, + SaleOptOut: 0, + SharingOptOut: 0, + TargetedAdvertisingOptOut: 0, + SensitiveDataProcessing: 12, + KnownChildSensitiveDataConsents: 2, + PersonalDataConsents: 0, + MspaCoveredTransaction: 0, + MspaOptOutOptionMode: 0, + MspaServiceProviderMode: 0, +}; + +/** + * Generate a normalization function for converting US state strings to the usnat format. + * + * Scalar fields are copied over if they exist in the input (state) data, or set to null otherwise. + * List fields are also copied, but forced to the "correct" length (by truncating or padding with nulls); + * additionally, elements within them can be moved around using the `move` argument. + * + * @param {Array[String]} nullify? list of fields to force to null + * @param {{}} move? Map from list field name to an index remapping for elements within that field (using 1 as the first index). + * For example, {SensitiveDataProcessing: {1: 2, 2: [1, 3]}} means "rearrange SensitiveDataProcessing by moving + * the first element to the second position, and the second element to both the first and third position." + * @param {({}, {}) => void} fn? an optional function to run once all the processing described above is complete; + * it's passed two arguments, the original (state) data, and its normalized (usnat) version. + * @param fields + * @returns {function({}): {}} + */ +export function normalizer({nullify = [], move = {}, fn}, fields = FIELDS) { + move = Object.fromEntries(Object.entries(move).map(([k, map]) => [k, + Object.fromEntries(Object.entries(map) + .map(([k, v]) => [k, Array.isArray(v) ? v : [v]]) + .map(([k, v]) => [--k, v.map(el => --el)]) + )]) + ); + return function (cd) { + const norm = Object.fromEntries(Object.entries(fields) + .map(([field, len]) => { + let val = null; + if (len > 0) { + val = Array(len).fill(null); + if (Array.isArray(cd[field])) { + const remap = move[field] || {}; + const done = []; + cd[field].forEach((el, i) => { + const [dest, moved] = remap.hasOwnProperty(i) ? [remap[i], true] : [[i], false]; + dest.forEach(d => { + if (d < len && !done.includes(d)) { + val[d] = el; + moved && done.push(d); + } + }); + }); + } + } else if (cd[field] != null) { + val = Array.isArray(cd[field]) ? null : cd[field]; + } + return [field, val]; + })); + nullify.forEach(path => deepSetValue(norm, path, null)); + fn && fn(cd, norm); + return norm; + }; +} + +function scalarMinorsAreChildren(original, normalized) { + normalized.KnownChildSensitiveDataConsents = original.KnownChildSensitiveDataConsents === 0 ? [0, 0] : [1, 1]; +} + +export const NORMALIZATIONS = { + // normalization rules - convert state consent into usnat consent + // https://docs.prebid.org/features/mspa-usnat.html + 7: (consent) => consent, + 8: normalizer({ + move: { + SensitiveDataProcessing: { + 1: 9, + 2: 10, + 3: 8, + 4: [1, 2], + 5: 12, + 8: 3, + 9: 4, + } + }, + fn(original, normalized) { + if (original.KnownChildSensitiveDataConsents.some(el => el !== 0)) { + normalized.KnownChildSensitiveDataConsents = [1, 1]; + } + } + }), + 9: normalizer({fn: scalarMinorsAreChildren}), + 10: normalizer({fn: scalarMinorsAreChildren}), + 11: normalizer({ + move: { + SensitiveDataProcessing: { + 3: 4, + 4: 5, + 5: 3, + } + }, + fn: scalarMinorsAreChildren + }), + 12: normalizer({ + fn(original, normalized) { + const cc = original.KnownChildSensitiveDataConsents; + let repl; + if (!cc.some(el => el !== 0)) { + repl = [0, 0]; + } else if (cc[1] === 2 && cc[2] === 2) { + repl = [2, 1]; + } else { + repl = [1, 1]; + } + normalized.KnownChildSensitiveDataConsents = repl; + } + }) +}; + +export const DEFAULT_SID_MAPPING = { + 8: 'usca', + 9: 'usva', + 10: 'usco', + 11: 'usut', + 12: 'usct' +}; + +export const getSections = (() => { + const allSIDs = Object.keys(DEFAULT_SID_MAPPING).map(Number); + return function ({sections = {}, sids = allSIDs} = {}) { + return sids.map(sid => { + const logger = prefixLog(`Cannot set up MSPA controls for SID ${sid}:`); + const ov = sections[sid] || {}; + const normalizeAs = ov.normalizeAs || sid; + if (!NORMALIZATIONS.hasOwnProperty(normalizeAs)) { + logger.logError(`no normalization rules are known for SID ${normalizeAs}`) + return; + } + const api = ov.name || DEFAULT_SID_MAPPING[sid]; + if (typeof api !== 'string') { + logger.logError(`cannot determine GPP section name`) + return; + } + return [ + api, + [sid], + NORMALIZATIONS[normalizeAs] + ] + }).filter(el => el != null); + } +})(); + +const handles = []; + +config.getConfig('consentManagement', (cfg) => { + const gppConf = cfg.consentManagement?.gpp; + if (gppConf) { + while (handles.length) { + handles.pop()(); + } + getSections(gppConf?.mspa || {}) + .forEach(([api, sids, normalize]) => handles.push(setupRules(api, sids, normalize))); + } +}); diff --git a/modules/gptPreAuction.js b/modules/gptPreAuction.js index 71884235b38..bf5b4a55dbb 100644 --- a/modules/gptPreAuction.js +++ b/modules/gptPreAuction.js @@ -1,4 +1,11 @@ -import {deepAccess, isAdUnitCodeMatchingSlot, isGptPubadsDefined, logInfo, pick} from '../src/utils.js'; +import { + deepAccess, + isAdUnitCodeMatchingSlot, + isGptPubadsDefined, + logInfo, + pick, + deepSetValue +} from '../src/utils.js'; import {config} from '../src/config.js'; import {getHook} from '../src/hook.js'; import {find} from '../src/polyfill.js'; @@ -15,7 +22,8 @@ export const appendGptSlots = adUnits => { } const adUnitMap = adUnits.reduce((acc, adUnit) => { - acc[adUnit.code] = adUnit; + acc[adUnit.code] = acc[adUnit.code] || []; + acc[adUnit.code].push(adUnit); return acc; }, {}); @@ -25,15 +33,13 @@ export const appendGptSlots = adUnits => { : isAdUnitCodeMatchingSlot(slot)); if (matchingAdUnitCode) { - const adUnit = adUnitMap[matchingAdUnitCode]; - adUnit.ortb2Imp = adUnit.ortb2Imp || {}; - adUnit.ortb2Imp.ext = adUnit.ortb2Imp.ext || {}; - adUnit.ortb2Imp.ext.data = adUnit.ortb2Imp.ext.data || {}; - - const context = adUnit.ortb2Imp.ext.data; - context.adserver = context.adserver || {}; - context.adserver.name = 'gam'; - context.adserver.adslot = sanitizeSlotPath(slot.getAdUnitPath()); + const adserver = { + name: 'gam', + adslot: sanitizeSlotPath(slot.getAdUnitPath()) + }; + adUnitMap[matchingAdUnitCode].forEach((adUnit) => { + deepSetValue(adUnit, 'ortb2Imp.ext.data.adserver', Object.assign({}, adUnit.ortb2Imp?.ext?.data?.adserver, adserver)); + }); } }); }; diff --git a/modules/gravitoIdSystem.js b/modules/gravitoIdSystem.js index aa25ea7db2c..cc02c6a103e 100644 --- a/modules/gravitoIdSystem.js +++ b/modules/gravitoIdSystem.js @@ -16,16 +16,16 @@ export const cookieKey = 'gravitompId'; export const gravitoIdSystemSubmodule = { /** - * used to link submodule with config - * @type {string} - */ + * used to link submodule with config + * @type {string} + */ name: MODULE_NAME, /** - * performs action to obtain id - * @function - * @returns { {id: {gravitompId: string}} | undefined } - */ + * performs action to obtain id + * @function + * @returns { {id: {gravitompId: string}} | undefined } + */ getId: function() { const newId = storage.getCookie(cookieKey); if (!newId) { @@ -38,11 +38,11 @@ export const gravitoIdSystemSubmodule = { }, /** - * decode the stored id value for passing to bid requests - * @function - * @param { {gravitompId: string} } value - * @returns { {gravitompId: {string} } | undefined } - */ + * decode the stored id value for passing to bid requests + * @function + * @param { {gravitompId: string} } value + * @returns { {gravitompId: {string} } | undefined } + */ decode: function(value) { if (value && typeof value === 'object') { var result = {}; @@ -53,6 +53,12 @@ export const gravitoIdSystemSubmodule = { } return undefined; }, + eids: { + 'gravitompId': { + source: 'gravito.net', + atype: 1 + }, + } } submodule('userId', gravitoIdSystemSubmodule); diff --git a/modules/greenbidsAnalyticsAdapter.js b/modules/greenbidsAnalyticsAdapter.js index edc0c9c6c5c..5d1f35f24ff 100644 --- a/modules/greenbidsAnalyticsAdapter.js +++ b/modules/greenbidsAnalyticsAdapter.js @@ -2,18 +2,20 @@ 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'; +import {deepClone, generateUUID, logError, logInfo, logWarn} from '../src/utils.js'; const analyticsType = 'endpoint'; -export const ANALYTICS_VERSION = '1.0.0'; +export const ANALYTICS_VERSION = '2.0.0'; const ANALYTICS_SERVER = 'https://a.greenbids.ai'; const { EVENTS: { + AUCTION_INIT, AUCTION_END, BID_TIMEOUT, + BILLABLE_EVENT, } } = CONSTANTS; @@ -25,28 +27,53 @@ export const BIDDER_STATUS = { const analyticsOptions = {}; -export const parseBidderCode = function (bid) { - let bidderCode = bid.bidderCode || bid.bidder; - return bidderCode.toLowerCase(); -}; +export const isSampled = function(greenbidsId, samplingRate) { + if (samplingRate < 0 || samplingRate > 1) { + logWarn('Sampling rate must be between 0 and 1'); + return true; + } + const hashInt = parseInt(greenbidsId.slice(-4), 16); + + return hashInt < samplingRate * (0xFFFF + 1); +} export const greenbidsAnalyticsAdapter = Object.assign(adapter({ANALYTICS_SERVER, analyticsType}), { cachedAuctions: {}, initConfig(config) { + analyticsOptions.options = deepClone(config.options); /** * Required option: pbuid * @type {boolean} */ - analyticsOptions.options = deepClone(config.options); - if (typeof config.options.pbuid !== 'string' || config.options.pbuid.length < 1) { + if (typeof analyticsOptions.options.pbuid !== 'string' || analyticsOptions.options.pbuid.length < 1) { logError('"options.pbuid" is required.'); return false; } + /** + * Deprecate use of integerated 'sampling' config + * replace by greenbidsSampling + */ + if (typeof analyticsOptions.options.sampling === 'number') { + logWarn('"options.sampling" is deprecated, please use "greenbidsSampling" instead.'); + analyticsOptions.options.greenbidsSampling = analyticsOptions.options.sampling; + // Set sampling to null to prevent prebid analytics integrated sampling to happen + analyticsOptions.options.sampling = null; + } + + /** + * Discourage unsampled analytics + */ + if (typeof analyticsOptions.options.greenbidsSampling !== 'number' || analyticsOptions.options.greenbidsSampling >= 1) { + logWarn('"options.greenbidsSampling" is not set or >=1, using this analytics module unsampled is discouraged.'); + analyticsOptions.options.greenbidsSampling = 1; + } + analyticsOptions.pbuid = config.options.pbuid analyticsOptions.server = ANALYTICS_SERVER; + return true; }, sendEventMessage(endPoint, data) { @@ -57,13 +84,16 @@ export const greenbidsAnalyticsAdapter = Object.assign(adapter({ANALYTICS_SERVER }); }, createCommonMessage(auctionId) { + const cachedAuction = this.getCachedAuction(auctionId); return { version: ANALYTICS_VERSION, auctionId: auctionId, referrer: window.location.href, - sampling: analyticsOptions.options.sampling, + sampling: analyticsOptions.options.greenbidsSampling, prebid: '$prebid.version$', + greenbidsId: cachedAuction.greenbidsId, pbuid: analyticsOptions.pbuid, + billingId: cachedAuction.billingId, adUnits: [], }; }, @@ -96,22 +126,24 @@ export const greenbidsAnalyticsAdapter = Object.assign(adapter({ANALYTICS_SERVER } } }, - createBidMessage(auctionEndArgs, timeoutBids) { - logInfo(auctionEndArgs) + createBidMessage(auctionEndArgs) { const {auctionId, timestamp, auctionEnd, adUnits, bidsReceived, noBids} = auctionEndArgs; + const cachedAuction = this.getCachedAuction(auctionId); const message = this.createCommonMessage(auctionId); + const timeoutBids = cachedAuction.timeoutBids || []; message.auctionElapsed = (auctionEnd - timestamp); adUnits.forEach((adUnit) => { - const adUnitCode = adUnit.code.toLowerCase(); + const adUnitCode = adUnit.code?.toLowerCase() || 'unknown_adunit_code'; 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} + ...(adUnit.mediaTypes?.banner !== undefined) && {banner: adUnit.mediaTypes.banner}, + ...(adUnit.mediaTypes?.video !== undefined) && {video: adUnit.mediaTypes.video}, + ...(adUnit.mediaTypes?.native !== undefined) && {native: adUnit.mediaTypes.native} }, + ortb2Imp: adUnit.ortb2Imp || {}, bidders: [], }); }); @@ -129,13 +161,26 @@ export const greenbidsAnalyticsAdapter = Object.assign(adapter({ANALYTICS_SERVER getCachedAuction(auctionId) { this.cachedAuctions[auctionId] = this.cachedAuctions[auctionId] || { timeoutBids: [], + greenbidsId: null, + billingId: null, + isSampled: true, }; return this.cachedAuctions[auctionId]; }, + handleAuctionInit(auctionInitArgs) { + const cachedAuction = this.getCachedAuction(auctionInitArgs.auctionId); + try { + cachedAuction.greenbidsId = auctionInitArgs.adUnits[0].ortb2Imp.ext.greenbids.greenbidsId; + } catch (e) { + logInfo("Couldn't find Greenbids RTD info, assuming analytics only"); + cachedAuction.greenbidsId = generateUUID(); + } + cachedAuction.isSampled = isSampled(cachedAuction.greenbidsId, analyticsOptions.options.greenbidsSampling); + }, handleAuctionEnd(auctionEndArgs) { const cachedAuction = this.getCachedAuction(auctionEndArgs.auctionId); this.sendEventMessage('/', - this.createBidMessage(auctionEndArgs, cachedAuction.timeoutBids) + this.createBidMessage(auctionEndArgs, cachedAuction) ); }, handleBidTimeout(timeoutBids) { @@ -144,14 +189,34 @@ export const greenbidsAnalyticsAdapter = Object.assign(adapter({ANALYTICS_SERVER cachedAuction.timeoutBids.push(bid); }); }, + handleBillable(billableArgs) { + const cachedAuction = this.getCachedAuction(billableArgs.auctionId); + /* Filter Greenbids Billable Events only */ + if (billableArgs.vendor === 'greenbidsRtdProvider') { + cachedAuction.billingId = billableArgs.billingId || 'unknown_billing_id'; + } + }, track({eventType, args}) { - switch (eventType) { - case BID_TIMEOUT: - this.handleBidTimeout(args); - break; - case AUCTION_END: - this.handleAuctionEnd(args); - break; + try { + if (eventType === AUCTION_INIT) { + this.handleAuctionInit(args); + } + + if (this.getCachedAuction(args?.auctionId)?.isSampled ?? true) { + switch (eventType) { + case BID_TIMEOUT: + this.handleBidTimeout(args); + break; + case AUCTION_END: + this.handleAuctionEnd(args); + break; + case BILLABLE_EVENT: + this.handleBillable(args); + break; + } + } + } catch (e) { + logWarn('There was an error handling event ' + eventType); } }, getAnalyticsOptions() { diff --git a/modules/greenbidsAnalyticsAdapter.md b/modules/greenbidsAnalyticsAdapter.md index 46e3af2c5e2..1be2c1741ed 100644 --- a/modules/greenbidsAnalyticsAdapter.md +++ b/modules/greenbidsAnalyticsAdapter.md @@ -1,23 +1,24 @@ -# Overview +#### Registration -``` -Module Name: Greenbids Analytics Adapter -Module Type: Analytics Adapter -Maintainer: jb@greenbids.ai -``` +The Greenbids Analytics adapter requires setup and approval from the +Greenbids team. Please reach out to our team for more information [greenbids.ai](https://greenbids.ai). -# Description +#### Analytics Options -Analytics adapter for Greenbids +{: .table .table-bordered .table-striped } +| Name | Scope | Description | Example | Type | +|-------------|---------|--------------------|-----------------------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------|------------------| +| pbuid | required | The Greenbids Publisher ID | greenbids-publisher-1 | string | +| greenbidsSampling | optional | sampling factor [0-1] (a value of 0.1 will filter 90% of the traffic) | 1.0 | float | -# Test Parameters +### Example Configuration -``` -{ - provider: 'greenbids', - options: { - pbuid: "PBUID_FROM_GREENBIDS" - sampling: 1.0 - } -} -``` +```javascript + pbjs.enableAnalytics({ + provider: 'greenbids', + options: { + pbuid: "greenbids-publisher-1" // please contact Greenbids to get a pbuid for yourself + greenbidsSampling: 1.0 + } + }); +``` \ No newline at end of file diff --git a/modules/greenbidsRtdProvider.js b/modules/greenbidsRtdProvider.js index ef12326cf18..7fcd163a7c2 100644 --- a/modules/greenbidsRtdProvider.js +++ b/modules/greenbidsRtdProvider.js @@ -1,12 +1,13 @@ -import { logError } from '../src/utils.js'; +import { logError, deepClone, generateUUID, deepSetValue, deepAccess } from '../src/utils.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'; const MODULE_NAME = 'greenbidsRtdProvider'; -const MODULE_VERSION = '1.0.0'; -const ENDPOINT = 'https://europe-west1-greenbids-357713.cloudfunctions.net/partner-selection'; +const MODULE_VERSION = '2.0.0'; +const ENDPOINT = 'https://t.greenbids.ai'; -const auctionInfo = {}; const rtdOptions = {}; function init(moduleConfig) { @@ -16,22 +17,33 @@ function init(moduleConfig) { return false; } else { rtdOptions.pbuid = params?.pbuid; - rtdOptions.targetTPR = params?.targetTPR || 0.99; rtdOptions.timeout = params?.timeout || 200; return true; } } function onAuctionInitEvent(auctionDetails) { - auctionInfo.auctionId = auctionDetails.auctionId; + /* Emitting one billing event per auction */ + let defaultId = 'default_id'; + let greenbidsId = deepAccess(auctionDetails.adUnits[0], 'ortb2Imp.ext.greenbids.greenbidsId', defaultId); + /* greenbids was successfully called so we emit the event */ + if (greenbidsId !== defaultId) { + events.emit(CONSTANTS.EVENTS.BILLABLE_EVENT, { + type: 'auction', + billingId: generateUUID(), + auctionId: auctionDetails.auctionId, + vendor: MODULE_NAME + }); + } } function getBidRequestData(reqBidsConfigObj, callback, config, userConsent) { - let promise = createPromise(reqBidsConfigObj); + let greenbidsId = generateUUID(); + let promise = createPromise(reqBidsConfigObj, greenbidsId); promise.then(callback); } -function createPromise(reqBidsConfigObj) { +function createPromise(reqBidsConfigObj, greenbidsId) { return new Promise((resolve) => { const timeoutId = setTimeout(() => { resolve(reqBidsConfigObj); @@ -40,7 +52,7 @@ function createPromise(reqBidsConfigObj) { ENDPOINT, { success: (response) => { - processSuccessResponse(response, timeoutId, reqBidsConfigObj); + processSuccessResponse(response, timeoutId, reqBidsConfigObj, greenbidsId); resolve(reqBidsConfigObj); }, error: () => { @@ -48,24 +60,35 @@ function createPromise(reqBidsConfigObj) { resolve(reqBidsConfigObj); }, }, - createPayload(reqBidsConfigObj), - { contentType: 'application/json' } + createPayload(reqBidsConfigObj, greenbidsId), + { + contentType: 'application/json', + customHeaders: { + 'Greenbids-Pbuid': rtdOptions.pbuid + } + } ); }); } -function processSuccessResponse(response, timeoutId, reqBidsConfigObj) { +function processSuccessResponse(response, timeoutId, reqBidsConfigObj, greenbidsId) { clearTimeout(timeoutId); const responseAdUnits = JSON.parse(response); - - updateAdUnitsBasedOnResponse(reqBidsConfigObj.adUnits, responseAdUnits); + updateAdUnitsBasedOnResponse(reqBidsConfigObj.adUnits, responseAdUnits, greenbidsId); } -function updateAdUnitsBasedOnResponse(adUnits, responseAdUnits) { +function updateAdUnitsBasedOnResponse(adUnits, responseAdUnits, greenbidsId) { adUnits.forEach((adUnit) => { const matchingAdUnit = findMatchingAdUnit(responseAdUnits, adUnit.code); if (matchingAdUnit) { - removeFalseBidders(adUnit, matchingAdUnit); + deepSetValue(adUnit, 'ortb2Imp.ext.greenbids', { + greenbidsId: greenbidsId, + keptInAuction: matchingAdUnit.bidders, + isExploration: matchingAdUnit.isExploration + }); + if (!matchingAdUnit.isExploration) { + removeFalseBidders(adUnit, matchingAdUnit); + } } }); } @@ -85,14 +108,24 @@ function getFalseBidders(bidders) { .map(([bidder]) => bidder); } -function createPayload(reqBidsConfigObj) { +function stripAdUnits(adUnits) { + const stripedAdUnits = deepClone(adUnits); + return stripedAdUnits.map(adUnit => { + adUnit.bids = adUnit.bids.map(bid => { + return { bidder: bid.bidder }; + }); + return adUnit; + }); +} + +function createPayload(reqBidsConfigObj, greenbidsId) { return JSON.stringify({ - auctionId: auctionInfo.auctionId, version: MODULE_VERSION, + ...rtdOptions, referrer: window.location.href, prebid: '$prebid.version$', - rtdOptions: rtdOptions, - adUnits: reqBidsConfigObj.adUnits, + greenbidsId: greenbidsId, + adUnits: stripAdUnits(reqBidsConfigObj.adUnits), }); } @@ -105,6 +138,7 @@ export const greenbidsSubmodule = { findMatchingAdUnit: findMatchingAdUnit, removeFalseBidders: removeFalseBidders, getFalseBidders: getFalseBidders, + stripAdUnits: stripAdUnits, }; submodule('realTimeData', greenbidsSubmodule); diff --git a/modules/greenbidsRtdProvider.md b/modules/greenbidsRtdProvider.md index 85b8f5a7859..ab8105a4537 100644 --- a/modules/greenbidsRtdProvider.md +++ b/modules/greenbidsRtdProvider.md @@ -2,6 +2,7 @@ ``` Module Name: Greenbids RTD Provider +Module Version: 2.0.0 Module Type: RTD Provider Maintainer: jb@greenbids.ai ``` @@ -21,7 +22,6 @@ This module is configured as part of the `realTimeData.dataProviders` object. | `waitForIt ` | required (mandatory true value) | Tells prebid auction to wait for the result of this module | `'true'` | `boolean` | | `params` | required | | | `Object` | | `params.pbuid` | required | The client site id provided by Greenbids. | `'TEST_FROM_GREENBIDS'` | `string` | -| `params.targetTPR` | optional (default 0.95) | Target True positive rate for the throttling model | `0.99` | `[0-1]` | | `params.timeout` | optional (default 200) | Maximum amount of milliseconds allowed for module to finish working (has to be <= to the realTimeData.auctionDelay property) | `200` | `number` | #### Example diff --git a/modules/gridBidAdapter.js b/modules/gridBidAdapter.js index 9c63e871356..7147864a2e6 100644 --- a/modules/gridBidAdapter.js +++ b/modules/gridBidAdapter.js @@ -9,14 +9,22 @@ import { isNumber, isStr } from '../src/utils.js'; +import { ajax } from '../src/ajax.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; import { Renderer } from '../src/Renderer.js'; import { VIDEO, BANNER } from '../src/mediaTypes.js'; import { config } from '../src/config.js'; import { getStorageManager } from '../src/storageManager.js'; +/** + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid + * @typedef {import('../src/adapters/bidderFactory.js').ServerRequest} ServerRequest + */ + const BIDDER_CODE = 'grid'; const ENDPOINT_URL = 'https://grid.bidswitch.net/hbjson'; +const USP_DELETE_DATA_HANDLER = 'https://media.grid.bidswitch.net/uspapi_delete_c2s' const SYNC_URL = 'https://x.bidswitch.net/sync?ssp=themediagrid'; const TIME_TO_LIVE = 360; @@ -26,10 +34,9 @@ const RENDERER_URL = 'https://acdn.adnxs.com/video/outstream/ANOutstreamVideo.js export const storage = getStorageManager({ bidderCode: BIDDER_CODE }); const LOG_ERROR_MESS = { - noAuid: 'Bid from response has no auid parameter - ', + noAdid: 'Bid from response has no adid parameter - ', noAdm: 'Bid from response has no adm parameter - ', noBid: 'Array of bid objects is empty', - noPlacementCode: 'Can\'t find in requested bids the bid with auid - ', emptyUids: 'Uids should be not empty', emptySeatbid: 'Seatbid array from response has empty item', emptyResponse: 'Response is empty', @@ -90,7 +97,7 @@ export const spec = { let {bidderRequestId, gdprConsent, uspConsent, timeout, refererInfo, gppConsent} = bidderRequest || {}; const referer = refererInfo ? encodeURIComponent(refererInfo.page) : ''; - const tmax = timeout; + const tmax = parseInt(timeout) || null; const imp = []; const bidsMap = {}; const requests = []; @@ -132,20 +139,13 @@ export const spec = { }; if (ortb2Imp) { if (ortb2Imp.instl) { - impObj.instl = ortb2Imp.instl; + impObj.instl = parseInt(ortb2Imp.instl) || null; } if (ortb2Imp.ext) { + impObj.ext.gpid = ortb2Imp.ext.gpid?.toString() || ortb2Imp.ext.data?.pbadslot?.toString() || ortb2Imp.ext.data?.adserver?.adslot?.toString(); if (ortb2Imp.ext.data) { impObj.ext.data = ortb2Imp.ext.data; - if (impObj.ext.data.adserver && impObj.ext.data.adserver.adslot) { - impObj.ext.gpid = impObj.ext.data.adserver.adslot.toString(); - } else if (ortb2Imp.ext.data.pbadslot) { - impObj.ext.gpid = ortb2Imp.ext.data.pbadslot.toString(); - } - } - if (ortb2Imp.ext.gpid) { - impObj.ext.gpid = ortb2Imp.ext.gpid.toString(); } } } @@ -463,6 +463,15 @@ export const spec = { url: syncUrl + params }; } + }, + + ajaxCall: function(url, cb, data, options) { + options.browsingTopics = false; + return ajax(url, cb, data, options); + }, + + onDataDeletionRequest: function(data) { + spec.ajaxCall(USP_DELETE_DATA_HANDLER, null, null, {method: 'GET'}); } }; @@ -474,7 +483,7 @@ export const spec = { */ function _getFloor (mediaTypes, bid) { const curMediaType = mediaTypes.video ? 'video' : 'banner'; - let floor = bid.params.bidFloor || bid.params.floorcpm || 0; + let floor = parseFloat(bid.params.bidFloor || bid.params.floorcpm || 0) || null; if (typeof bid.getFloor === 'function') { const floorInfo = bid.getFloor({ @@ -507,7 +516,7 @@ function _getBidFromResponse(respItem) { function _addBidResponse(serverBid, bidRequest, bidResponses, RendererConst, bidderCode) { if (!serverBid) return; let errorMessage; - if (!serverBid.auid) errorMessage = LOG_ERROR_MESS.noAuid + JSON.stringify(serverBid); + if (!serverBid.adid) errorMessage = LOG_ERROR_MESS.noAdid + JSON.stringify(serverBid); if (!errorMessage && !serverBid.adm && !serverBid.nurl) errorMessage = LOG_ERROR_MESS.noAdm + JSON.stringify(serverBid); else { const bidObject = bidRequest.bidsMap ? bidRequest.bidsMap[serverBid.impid] : bidRequest.bidObject; @@ -518,7 +527,7 @@ function _addBidResponse(serverBid, bidRequest, bidResponses, RendererConst, bid cpm: serverBid.price, width: serverBid.w, height: serverBid.h, - creativeId: serverBid.auid, // bid.bidId + creativeId: serverBid.adid, currency: 'USD', netRevenue: true, ttl: TIME_TO_LIVE, @@ -584,8 +593,8 @@ function createVideoRequest(videoParams, mediaType, bidSizes) { if (!videoData.w || !videoData.h) return; - const minDur = mind || durationRangeSec[0] || videoData.minduration; - const maxDur = maxd || durationRangeSec[1] || videoData.maxduration; + const minDur = mind || durationRangeSec[0] || parseInt(videoData.minduration) || null; + const maxDur = maxd || durationRangeSec[1] || parseInt(videoData.maxduration) || null; if (minDur) { videoData.minduration = minDur; diff --git a/modules/growadvertisingBidAdapter.js b/modules/growadvertisingBidAdapter.js index b9b256dbaff..f6f7867f0fe 100644 --- a/modules/growadvertisingBidAdapter.js +++ b/modules/growadvertisingBidAdapter.js @@ -1,6 +1,6 @@ 'use strict'; -import { getBidIdParameter, deepAccess, _each, triggerPixel } from '../src/utils.js'; +import {deepAccess, _each, triggerPixel, getBidIdParameter} from '../src/utils.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import {BANNER, NATIVE} from '../src/mediaTypes.js'; import { convertOrtbRequestToProprietaryNative } from '../src/native.js'; diff --git a/modules/growthCodeAnalyticsAdapter.js b/modules/growthCodeAnalyticsAdapter.js index a2ab4ddbfac..5c7cc254f1d 100644 --- a/modules/growthCodeAnalyticsAdapter.js +++ b/modules/growthCodeAnalyticsAdapter.js @@ -13,7 +13,7 @@ import {MODULE_TYPE_ANALYTICS} from '../src/activities/modules.js'; const MODULE_NAME = 'growthCodeAnalytics'; const DEFAULT_PID = 'INVALID_PID' -const ENDPOINT_URL = 'https://p2.gcprivacy.com/v1/pb/analytics' +const ENDPOINT_URL = 'https://analytics.gcprivacy.com/v3/pb/analytics' export const storage = getStorageManager({moduleType: MODULE_TYPE_ANALYTICS, moduleName: MODULE_NAME}); @@ -30,8 +30,8 @@ let bidRequestTimeout = 0; let analyticsType = 'endpoint'; let growthCodeAnalyticsAdapter = Object.assign(adapter({url: url, analyticsType}), { - track({eventType, eventData}) { - eventData = eventData ? JSON.parse(JSON.stringify(eventData)) : {}; + track({eventType, args}) { + let eventData = args ? JSON.parse(JSON.stringify(args)) : {}; let data = {}; if (!trackEvents.includes(eventType)) return; switch (eventType) { @@ -98,6 +98,11 @@ let growthCodeAnalyticsAdapter = Object.assign(adapter({url: url, analyticsType} break; } + case CONSTANTS.EVENTS.NO_BID: { + data = eventData + break; + } + default: return; } @@ -133,12 +138,15 @@ growthCodeAnalyticsAdapter.enableAnalytics = function(conf = {}) { function logToServer() { if (pid === DEFAULT_PID) return; - if (eventQueue.length > 1) { + if (eventQueue.length >= 1) { + // Get the correct GCID + let gcid = localStorage.getItem('gcid') + let data = { session: sessionId, pid: pid, + gcid: gcid, timestamp: Date.now(), - timezoneoffset: new Date().getTimezoneOffset(), url: getRefererInfo().page, referer: document.referrer, events: eventQueue @@ -162,7 +170,7 @@ function sendEvent(event) { eventQueue.push(event); logInfo(MODULE_NAME + 'Analytics Event: ' + event); - if (event.eventType === CONSTANTS.EVENTS.AUCTION_END) { + if ((event.eventType === CONSTANTS.EVENTS.AUCTION_END) || (event.eventType === CONSTANTS.EVENTS.BID_WON)) { logToServer(); } } diff --git a/modules/growthCodeAnalyticsAdapter.md b/modules/growthCodeAnalyticsAdapter.md index e45cb2e9c62..6625d492ee6 100644 --- a/modules/growthCodeAnalyticsAdapter.md +++ b/modules/growthCodeAnalyticsAdapter.md @@ -21,13 +21,7 @@ pbjs.enableAnalytics({ pid: '', trackEvents: [ 'auctionEnd', - 'bidAdjustment', - 'bidTimeout', - 'bidRequested', - 'bidResponse', - 'noBid', - 'bidWon', - 'bidderDone'] + 'bidWon'] } }); ``` diff --git a/modules/growthCodeIdSystem.js b/modules/growthCodeIdSystem.js index fae022f1a56..539a20a7302 100644 --- a/modules/growthCodeIdSystem.js +++ b/modules/growthCodeIdSystem.js @@ -5,11 +5,18 @@ * @requires module:modules/userId */ -import {logError, logInfo, tryAppendQueryString} from '../src/utils.js'; +import {logError, logInfo, pick} from '../src/utils.js'; import {ajax} from '../src/ajax.js'; import { submodule } from '../src/hook.js' import {getStorageManager} from '../src/storageManager.js'; import {MODULE_TYPE_UID} from '../src/activities/modules.js'; +import {tryAppendQueryString} from '../libraries/urlUtils/urlUtils.js'; + +/** + * @typedef {import('../modules/userId/index.js').Submodule} Submodule + * @typedef {import('../modules/userId/index.js').SubmoduleConfig} SubmoduleConfig + * @typedef {import('../modules/userId/index.js').IdResponse} IdResponse + */ const MODULE_NAME = 'growthCodeId'; const GC_DATA_KEY = '_gc_data'; @@ -166,6 +173,25 @@ export const growthCodeIdSubmodule = { } }; return { callback: resp }; + }, + eids: { + 'growthCodeId': { + getValue: function(data) { + return data.gc_id + }, + source: 'growthcode.io', + atype: 1, + getUidExt: function(data) { + const extendedData = pick(data, [ + 'h1', + 'h2', + 'h3', + ]); + if (Object.keys(extendedData).length) { + return extendedData; + } + } + }, } }; diff --git a/modules/growthCodeRtdProvider.js b/modules/growthCodeRtdProvider.js index 370ace9a203..b12b25a0951 100644 --- a/modules/growthCodeRtdProvider.js +++ b/modules/growthCodeRtdProvider.js @@ -5,10 +5,11 @@ import { submodule } from '../src/hook.js' import { getStorageManager } from '../src/storageManager.js'; import { - logMessage, logError, tryAppendQueryString, mergeDeep + logMessage, logError, mergeDeep } from '../src/utils.js'; import * as ajax from '../src/ajax.js'; import { MODULE_TYPE_RTD } from '../src/activities/modules.js'; +import {tryAppendQueryString} from '../libraries/urlUtils/urlUtils.js'; const MODULE_NAME = 'growthCodeRtd'; const LOG_PREFIX = 'GrowthCodeRtd: '; @@ -59,7 +60,11 @@ function init(config, userConsent) { items = tryParse(storage.getDataFromLocalStorage(RTD_CACHE_KEY, null)); - return callServer(configParams, items, expiresAt, userConsent); + if (configParams.pid === undefined) { + return true; // Die gracefully + } else { + return callServer(configParams, items, expiresAt, userConsent); + } } function callServer(configParams, items, expiresAt, userConsent) { // Expire Cache @@ -76,8 +81,8 @@ function callServer(configParams, items, expiresAt, userConsent) { url = tryAppendQueryString(url, 'pid', configParams.pid); url = tryAppendQueryString(url, 'u', window.location.href); url = tryAppendQueryString(url, 'gcid', gcid); - if ((userConsent !== null) && (userConsent.gdpr !== null) && (userConsent.gdpr.consentData.getTCData.tcString)) { - url = tryAppendQueryString(url, 'tcf', userConsent.gdpr.consentData.getTCData.tcString) + if ((userConsent !== null) && (userConsent.gdpr !== null) && (userConsent.gdpr.consentString)) { + url = tryAppendQueryString(url, 'tcf', userConsent.gdpr.consentString) } ajax.ajaxBuilder()(url, { diff --git a/modules/gumgumBidAdapter.js b/modules/gumgumBidAdapter.js index d050af4ac8f..15af94494a3 100644 --- a/modules/gumgumBidAdapter.js +++ b/modules/gumgumBidAdapter.js @@ -6,6 +6,15 @@ import {getStorageManager} from '../src/storageManager.js'; import {includes} from '../src/polyfill.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; +/** + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid + * @typedef {import('../src/adapters/bidderFactory.js').ServerResponse} ServerResponse + * @typedef {import('../src/adapters/bidderFactory.js').SyncOptions} SyncOptions + * @typedef {import('../src/adapters/bidderFactory.js').UserSync} UserSync + * @typedef {import('../src/adapters/bidderFactory.js').validBidRequests} validBidRequests + */ + const BIDDER_CODE = 'gumgum'; const storage = getStorageManager({bidderCode: BIDDER_CODE}); const ALIAS_BIDDER_CODE = ['gg']; @@ -14,6 +23,7 @@ const JCSI = { t: 0, rq: 8, pbv: '$prebid.version$' } const SUPPORTED_MEDIA_TYPES = [BANNER, VIDEO]; const TIME_TO_LIVE = 60; const DELAY_REQUEST_TIME = 1800000; // setting to 30 mins +const pubProvidedIdSources = ['dac.co.jp', 'audigent.com', 'id5-sync.com', 'liveramp.com', 'intentiq.com', 'liveintent.com', 'crwdcntrl.net', 'quantcast.com', 'adserver.org', 'yahoo.com'] let invalidRequestIds = {}; let pageViewId = null; @@ -175,6 +185,7 @@ function _getVidParams(attributes) { linearity: li, startdelay: sd, placement: pt, + plcmt, protocols = [], playerSize = [] } = attributes; @@ -186,7 +197,7 @@ function _getVidParams(attributes) { pr = protocols.join(','); } - return { + const result = { mind, maxd, li, @@ -196,6 +207,11 @@ function _getVidParams(attributes) { viw, vih }; + // Add vplcmt property to the result object if plcmt is available + if (plcmt !== undefined && plcmt !== null) { + result.vplcmt = plcmt; + } + return result; } /** @@ -310,7 +326,23 @@ function buildRequests(validBidRequests, bidderRequest) { // ADTS-174 Removed unnecessary checks to fix failing test data.lt = lt; data.to = to; - + function jsoStringifynWithMaxLength(data, maxLength) { + let jsonString = JSON.stringify(data); + if (jsonString.length <= maxLength) { + return jsonString; + } else { + const truncatedData = data.slice(0, Math.floor(data.length * (maxLength / jsonString.length))); + jsonString = JSON.stringify(truncatedData); + return jsonString; + } + } + // Send filtered pubProvidedId's + if (userId && userId.pubProvidedId) { + let filteredData = userId.pubProvidedId.filter(item => pubProvidedIdSources.includes(item.source)); + let maxLength = 1800; // replace this with your desired maximum length + let truncatedJsonString = jsoStringifynWithMaxLength(filteredData, maxLength); + data.pubProvidedId = truncatedJsonString + } // ADJS-1286 Read id5 id linktype field if (userId && userId.id5id && userId.id5id.uid && userId.id5id.ext) { data.id5Id = userId.id5id.uid || null @@ -322,9 +354,6 @@ function buildRequests(validBidRequests, bidderRequest) { // ADTS-134 Retrieve ID envelopes for (const eid in eids) data[eid] = eids[eid]; - // ADJS-1024 & ADSS-1297 & ADTS-175 - gpid && (data.gpid = gpid); - if (mediaTypes.banner) { sizes = mediaTypes.banner.sizes; } else if (mediaTypes.video) { @@ -332,6 +361,9 @@ function buildRequests(validBidRequests, bidderRequest) { data = _getVidParams(mediaTypes.video); } + // ADJS-1024 & ADSS-1297 & ADTS-175 + gpid && (data.gpid = gpid); + if (pageViewId) { data.pv = pageViewId; } @@ -383,15 +415,11 @@ function buildRequests(validBidRequests, bidderRequest) { data.uspConsent = uspConsent; } if (gppConsent) { - data.gppConsent = { - gppString: bidderRequest.gppConsent.gppString, - gpp_sid: bidderRequest.gppConsent.applicableSections - } + data.gppString = bidderRequest.gppConsent.gppString ? bidderRequest.gppConsent.gppString : '' + data.gppSid = Array.isArray(bidderRequest.gppConsent.applicableSections) ? bidderRequest.gppConsent.applicableSections.join(',') : '' } else if (!gppConsent && bidderRequest?.ortb2?.regs?.gpp) { - data.gppConsent = { - gppString: bidderRequest.ortb2.regs.gpp, - gpp_sid: bidderRequest.ortb2.regs.gpp_sid - }; + data.gppString = bidderRequest.ortb2.regs.gpp + data.gppSid = Array.isArray(bidderRequest.ortb2.regs.gpp_sid) ? bidderRequest.ortb2.regs.gpp_sid.join(',') : '' } if (coppa) { data.coppa = coppa; diff --git a/modules/hadronIdSystem.js b/modules/hadronIdSystem.js index 596bf9611e6..01b0283d3d1 100644 --- a/modules/hadronIdSystem.js +++ b/modules/hadronIdSystem.js @@ -9,8 +9,17 @@ import {ajax} from '../src/ajax.js'; import {getStorageManager} from '../src/storageManager.js'; import {submodule} from '../src/hook.js'; import {isFn, isStr, isPlainObject, logError, logInfo} from '../src/utils.js'; +import { config } from '../src/config.js'; import {MODULE_TYPE_UID} from '../src/activities/modules.js'; +import { gdprDataHandler, uspDataHandler, gppDataHandler } from '../src/adapterManager.js'; +/** + * @typedef {import('../modules/userId/index.js').Submodule} Submodule + * @typedef {import('../modules/userId/index.js').SubmoduleConfig} SubmoduleConfig + * @typedef {import('../modules/userId/index.js').IdResponse} IdResponse + */ + +const LOG_PREFIX = '[hadronIdSystem]'; const HADRONID_LOCAL_NAME = 'auHadronId'; const MODULE_NAME = 'hadronId'; const AU_GVLID = 561; @@ -42,6 +51,8 @@ const urlAddParams = (url, params) => { return url + (url.indexOf('?') > -1 ? '&' : '?') + params } +const isDebug = config.getConfig('debug') || false; + /** @type {Submodule} */ export const hadronIdSubmodule = { /** @@ -88,7 +99,7 @@ export const hadronIdSubmodule = { } catch (error) { logError(error); } - logInfo(`Response from backend is ${responseObj}`); + logInfo(LOG_PREFIX, `Response from backend is ${response}`, responseObj); hadronId = responseObj['hadronId']; storage.setDataInLocalStorage(HADRONID_LOCAL_NAME, hadronId); responseObj = {id: {hadronId}}; @@ -100,16 +111,43 @@ export const hadronIdSubmodule = { callback(); } }; - logInfo('HadronId not found in storage, calling backend...'); - const url = urlAddParams( + let 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` + `partner_id=${partnerId}&_it=prebid&t=1&src=id` // src=id => the backend was called from getId ); + if (isDebug) { + url += '&debug=1' + } + const gdprConsent = gdprDataHandler.getConsentData() + if (gdprConsent) { + url += `${gdprConsent.consentString ? '&gdprString=' + encodeURIComponent(gdprConsent.consentString) : ''}`; + url += `&gdpr=${gdprConsent.gdprApplies === true ? 1 : 0}`; + } + + const usPrivacyString = uspDataHandler.getConsentData(); + if (usPrivacyString) { + url += `&us_privacy=${encodeURIComponent(usPrivacyString)}`; + } + + const gppConsent = gppDataHandler.getConsentData(); + if (gppConsent) { + url += `${gppConsent.gppString ? '&gpp=' + encodeURIComponent(gppConsent.gppString) : ''}`; + url += `${gppConsent.applicableSections ? '&gpp_sid=' + encodeURIComponent(gppConsent.applicableSections) : ''}`; + } + + logInfo(LOG_PREFIX, `hadronId not found in storage, calling home (${url})`); + ajax(url, callbacks, undefined, {method: 'GET'}); }; return {callback: resp}; + }, + eids: { + 'hadronId': { + source: 'audigent.com', + atype: 1 + }, } }; diff --git a/modules/hadronIdSystem.md b/modules/hadronIdSystem.md index 7521cca06ac..212030cbcd9 100644 --- a/modules/hadronIdSystem.md +++ b/modules/hadronIdSystem.md @@ -35,5 +35,4 @@ The below parameters apply only to the HadronID User ID Module integration. | value | Optional | Object | Used only if the page has a separate mechanism for storing the Hadron ID. The value is an object containing the values to be sent to the adapters. In this scenario, no URL is called and nothing is added to local storage | `{"hadronId": "eb33b0cb-8d35-4722-b9c0-1a31d4064888"}` | | params | Optional | Object | Used to store params for the id system | | params.partnerId | Required | Number | This is the Audigent Partner ID obtained from Audigent. | `1234` | -| params.url | Optional | String | Set an alternate GET url for HadronId with this parameter | -| params.urlArg | Optional | Object | Optional url parameter for params.url | + | diff --git a/modules/hadronRtdProvider.js b/modules/hadronRtdProvider.js index 6fb982815c1..5c604709b4b 100644 --- a/modules/hadronRtdProvider.js +++ b/modules/hadronRtdProvider.js @@ -14,6 +14,10 @@ import {isFn, isStr, isArray, deepEqual, isPlainObject, logError, logInfo} from import {loadExternalScript} from '../src/adloader.js'; import {MODULE_TYPE_RTD} from '../src/activities/modules.js'; +/** + * @typedef {import('../modules/rtdModule/index.js').RtdSubmodule} RtdSubmodule + */ + const LOG_PREFIX = 'User ID - HadronRtdProvider submodule: '; const MODULE_NAME = 'realTimeData'; const SUBMODULE_NAME = 'hadron'; diff --git a/modules/holidBidAdapter.js b/modules/holidBidAdapter.js index f41829bd123..fbcbb9492c7 100644 --- a/modules/holidBidAdapter.js +++ b/modules/holidBidAdapter.js @@ -1,4 +1,10 @@ -import {deepAccess, getBidIdParameter, isStr, logMessage, triggerPixel, } from '../src/utils.js'; +import { + deepAccess, + deepSetValue, getBidIdParameter, + isStr, + logMessage, + triggerPixel, +} from '../src/utils.js'; import * as events from '../src/events.js'; import CONSTANTS from '../src/constants.json'; import {BANNER} from '../src/mediaTypes.js'; @@ -10,6 +16,7 @@ const GVLID = 1177 const ENDPOINT = 'https://helloworld.holid.io/openrtb2/auction' const COOKIE_SYNC_ENDPOINT = 'https://null.holid.io/sync.html' const TIME_TO_LIVE = 300 +const TMAX = 500 let wurlMap = {} events.on(CONSTANTS.EVENTS.BID_WON, bidWonHandler) @@ -23,12 +30,19 @@ export const spec = { return !!bid.params.adUnitID }, - buildRequests: function (validBidRequests, _bidderRequest) { + buildRequests: function (validBidRequests, bidderRequest) { return validBidRequests.map((bid) => { const requestData = { ...bid.ortb2, - id: _bidderRequest.bidderRequestId, + source: {schain: bid.schain}, + id: bidderRequest.bidderRequestId, imp: [getImp(bid)], + tmax: TMAX, + ...buildStoredRequest(bid) + } + + if (bid.userIdAsEids) { + deepSetValue(requestData, 'user.ext.eids', bid.userIdAsEids) } return { @@ -50,8 +64,6 @@ export const spec = { serverResponse.body.seatbid.map((response) => { response.bid.map((bid) => { const requestId = bidRequest.bidId - // TODO: fix auctionId leak: https://github.com/prebid/Prebid.js/issues/9781 - const auctionId = bidRequest.auctionId const wurl = deepAccess(bid, 'ext.prebid.events.win') const bidResponse = { requestId, @@ -65,7 +77,7 @@ export const spec = { ttl: TIME_TO_LIVE, } - addWurl({ auctionId, requestId, wurl }) + addWurl(requestId, wurl) bidResponses.push(bidResponse) }) @@ -75,11 +87,15 @@ export const spec = { }, getUserSyncs(optionsType, serverResponse, gdprConsent, uspConsent) { + const syncs = [{ + type: 'image', + url: 'https://track.adform.net/Serving/TrackPoint/?pm=2992097&lid=132720821' + }] + if (!serverResponse || serverResponse.length === 0) { - return [] + return syncs } - const syncs = [] const bidders = getBidders(serverResponse) if (optionsType.iframeEnabled && bidders) { @@ -100,24 +116,14 @@ export const spec = { type: 'iframe', url: COOKIE_SYNC_ENDPOINT + strQueryParams + '&type=iframe', }) - - return syncs } - return [] + return syncs }, } function getImp(bid) { - const imp = { - ext: { - prebid: { - storedrequest: { - id: getBidIdParameter('adUnitID', bid.params), - }, - }, - }, - } + const imp = buildStoredRequest(bid) const sizes = bid.sizes && !Array.isArray(bid.sizes[0]) ? [bid.sizes] : bid.sizes @@ -132,6 +138,18 @@ function getImp(bid) { return imp } +function buildStoredRequest(bid) { + return { + ext: { + prebid: { + storedrequest: { + id: getBidIdParameter('adUnitID', bid.params), + }, + }, + }, + } +} + function getBidders(serverResponse) { const bidders = serverResponse .map((res) => Object.keys(res.body.ext.responsetimemillis || [])) @@ -142,28 +160,28 @@ function getBidders(serverResponse) { } } -function addWurl(auctionId, adId, wurl) { - if ([auctionId, adId].every(isStr)) { - wurlMap[`${auctionId}${adId}`] = wurl +function addWurl(requestId, wurl) { + if (isStr(requestId)) { + wurlMap[requestId] = wurl } } -function removeWurl(auctionId, adId) { - delete wurlMap[`${auctionId}${adId}`] +function removeWurl(requestId) { + delete wurlMap[requestId] } -function getWurl(auctionId, adId) { - if ([auctionId, adId].every(isStr)) { - return wurlMap[`${auctionId}${adId}`] +function getWurl(requestId) { + if (isStr(requestId)) { + return wurlMap[requestId] } } function bidWonHandler(bid) { - const wurl = getWurl(bid.auctionId, bid.adId) + const wurl = getWurl(bid.requestId) if (wurl) { logMessage(`Invoking image pixel for wurl on BID_WIN: "${wurl}"`) triggerPixel(wurl) - removeWurl(bid.auctionId, bid.adId) + removeWurl(bid.requestId) } } diff --git a/modules/hybridBidAdapter.js b/modules/hybridBidAdapter.js index f746e69cbba..00e109516e5 100644 --- a/modules/hybridBidAdapter.js +++ b/modules/hybridBidAdapter.js @@ -5,6 +5,13 @@ import {BANNER, VIDEO} from '../src/mediaTypes.js'; import {Renderer} from '../src/Renderer.js'; import {find} from '../src/polyfill.js'; +/** + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid + * @typedef {import('../src/adapters/bidderFactory.js').ServerResponse} ServerResponse + * @typedef {import('../src/adapters/bidderFactory.js').validBidRequests} validBidRequests + */ + const BIDDER_CODE = 'hybrid'; const DSP_ENDPOINT = 'https://hbe198.hybrid.ai/prebidhb'; const TRAFFIC_TYPE_WEB = 1; diff --git a/modules/iasRtdProvider.js b/modules/iasRtdProvider.js index 994be7c0804..b9de7ef4e46 100644 --- a/modules/iasRtdProvider.js +++ b/modules/iasRtdProvider.js @@ -1,7 +1,9 @@ -import { submodule } from '../src/hook.js'; +import {submodule} from '../src/hook.js'; import * as utils from '../src/utils.js'; -import { ajax } from '../src/ajax.js'; -import { getGlobal } from '../src/prebidGlobal.js'; +import {ajax} from '../src/ajax.js'; +import {getGlobal} from '../src/prebidGlobal.js'; +import {getAdUnitSizes} from '../libraries/sizeUtils/sizeUtils.js'; +import {getGptSlotInfoForAdUnitCode} from '../libraries/gptUtils/gptUtils.js'; /** @type {string} */ const MODULE_NAME = 'realTimeData'; @@ -76,7 +78,7 @@ function getAdUnitPath(adSlot, bidRequest, adUnitPath) { if (!utils.isEmpty(adSlot)) { p = adSlot.gptSlot; } else { - if (!utils.isEmpty(adUnitPath) && utils.hasOwn(adUnitPath, bidRequest.code)) { + if (!utils.isEmpty(adUnitPath) && adUnitPath.hasOwnProperty(bidRequest.code)) { if (utils.isStr(adUnitPath[bidRequest.code]) && !utils.isEmpty(adUnitPath[bidRequest.code])) { p = adUnitPath[bidRequest.code]; } @@ -86,13 +88,13 @@ function getAdUnitPath(adSlot, bidRequest, adUnitPath) { } function stringifySlot(bidRequest, adUnitPath) { - const sizes = utils.getAdUnitSizes(bidRequest); + const sizes = getAdUnitSizes(bidRequest); const id = bidRequest.code; const ss = stringifySlotSizes(sizes); - const adSlot = utils.getGptSlotInfoForAdUnitCode(bidRequest.code); + const adSlot = getGptSlotInfoForAdUnitCode(bidRequest.code); const p = getAdUnitPath(adSlot, bidRequest, adUnitPath); const slot = { id, ss, p }; - const keyValues = utils.getKeys(slot).map(function (key) { + const keyValues = Object.keys(slot).map(function (key) { return [key, slot[key]].join(':'); }); return '{' + keyValues.join(',') + '}'; diff --git a/modules/id5IdSystem.js b/modules/id5IdSystem.js index bb8a21264d0..b10f3c5d2cf 100644 --- a/modules/id5IdSystem.js +++ b/modules/id5IdSystem.js @@ -15,12 +15,21 @@ import { logWarn, safeJSONParse } from '../src/utils.js'; -import {ajax} from '../src/ajax.js'; +import {fetch} from '../src/ajax.js'; import {submodule} from '../src/hook.js'; import {getRefererInfo} from '../src/refererDetection.js'; import {getStorageManager} from '../src/storageManager.js'; -import {uspDataHandler} from '../src/adapterManager.js'; +import {uspDataHandler, gppDataHandler} from '../src/adapterManager.js'; import {MODULE_TYPE_UID} from '../src/activities/modules.js'; +import { GreedyPromise } from '../src/utils/promise.js'; +import { loadExternalScript } from '../src/adloader.js'; + +/** + * @typedef {import('../modules/userId/index.js').Submodule} Submodule + * @typedef {import('../modules/userId/index.js').SubmoduleConfig} SubmoduleConfig + * @typedef {import('../modules/userId/index.js').ConsentData} ConsentData + * @typedef {import('../modules/userId/index.js').IdResponse} IdResponse + */ const MODULE_NAME = 'id5Id'; const GVLID = 131; @@ -37,6 +46,70 @@ const LEGACY_COOKIE_NAMES = ['pbjs-id5id', 'id5id.1st', 'id5id']; export const storage = getStorageManager({moduleType: MODULE_TYPE_UID, moduleName: MODULE_NAME}); +/** + * @typedef {Object} IdResponse + * @property {string} [universal_uid] - The encrypted ID5 ID to pass to bidders + * @property {Object} [ext] - The extensions object to pass to bidders + * @property {Object} [ab_testing] - A/B testing configuration +*/ + +/** + * @typedef {Object} FetchCallConfig + * @property {string} [url] - The URL for the fetch endpoint + * @property {Object} [overrides] - Overrides to apply to fetch parameters +*/ + +/** + * @typedef {Object} ExtensionsCallConfig + * @property {string} [url] - The URL for the extensions endpoint + * @property {string} [method] - Overrides the HTTP method to use to make the call + * @property {Object} [body] - Specifies a body to pass to the extensions endpoint +*/ + +/** + * @typedef {Object} DynamicConfig + * @property {FetchCallConfig} [fetchCall] - The fetch call configuration + * @property {ExtensionsCallConfig} [extensionsCall] - The extensions call configuration +*/ + +/** + * @typedef {Object} ABTestingConfig + * @property {boolean} enabled - Tells whether A/B testing is enabled for this instance + * @property {number} controlGroupPct - A/B testing probability + */ + +/** + * @typedef {Object} Multiplexing + * @property {boolean} [disabled] - Disable multiplexing (instance will work in single mode) + */ + +/** + * @typedef {Object} Diagnostics + * @property {boolean} [publishingDisabled] - Disable diagnostics publishing + * @property {number} [publishAfterLoadInMsec] - Delay in ms after script load after which collected diagnostics are published + * @property {boolean} [publishBeforeWindowUnload] - When true, diagnostics publishing is triggered on Window 'beforeunload' event + * @property {number} [publishingSampleRatio] - Diagnostics publishing sample ratio + */ + +/** + * @typedef {Object} Segment + * @property {string} [destination] - GVL ID or ID5-XX Partner ID. Mandatory + * @property {Array} [ids] - The segment IDs to push. Must contain at least one segment ID. + */ + +/** + * @typedef {Object} Id5PrebidConfig + * @property {number} partner - The ID5 partner ID + * @property {string} pd - The ID5 partner data string + * @property {ABTestingConfig} abTesting - The A/B testing configuration + * @property {boolean} disableExtensions - Disabled extensions call + * @property {string} [externalModuleUrl] - URL for the id5 prebid external module + * @property {Multiplexing} [multiplexing] - Multiplexing options. Only supported when loading the external module. + * @property {Diagnostics} [diagnostics] - Diagnostics options. Supported only in multiplexing + * @property {Array} [segments] - A list of segments to push to partners. Supported only in multiplexing. + * @property {boolean} [disableUaHints] - When true, look up of high entropy values through user agent hints is disabled. + */ + /** @type {Submodule} */ export const id5IdSubmodule = { /** @@ -108,7 +181,7 @@ export const id5IdSubmodule = { * @returns {IdResponse|undefined} */ getId(submoduleConfig, consentData, cacheIdObj) { - if (!hasRequiredConfig(submoduleConfig)) { + if (!validateConfig(submoduleConfig)) { return undefined; } @@ -118,7 +191,8 @@ export const id5IdSubmodule = { } const resp = function (cbFunction) { - new IdFetchFlow(submoduleConfig, consentData, cacheIdObj, uspDataHandler.getConsentData()).execute() + const fetchFlow = new IdFetchFlow(submoduleConfig, consentData, cacheIdObj, uspDataHandler.getConsentData(), gppDataHandler.getConsentData()); + fetchFlow.execute() .then(response => { cbFunction(response) }) @@ -142,106 +216,133 @@ export const id5IdSubmodule = { * @return {(IdResponse|function(callback:function))} A response object that contains id and/or callback. */ extendId(config, consentData, cacheIdObj) { - hasRequiredConfig(config); - if (!hasWriteConsentToLocalStorage(consentData)) { logInfo(LOG_PREFIX + 'No consent given for ID5 local storage writing, skipping nb increment.') return cacheIdObj; } - const partnerId = (config && config.params && config.params.partner) || 0; + const partnerId = validateConfig(config) ? config.params.partner : 0; incrementNb(partnerId); logInfo(LOG_PREFIX + 'using cached ID', cacheIdObj); return cacheIdObj; - } + }, + eids: { + 'id5id': { + getValue: function(data) { + return data.uid + }, + source: 'id5-sync.com', + atype: 1, + getUidExt: function(data) { + if (data.ext) { + return data.ext; + } + } + }, + }, }; -class IdFetchFlow { - constructor(submoduleConfig, gdprConsentData, cacheIdObj, usPrivacyData) { +export class IdFetchFlow { + constructor(submoduleConfig, gdprConsentData, cacheIdObj, usPrivacyData, gppData) { this.submoduleConfig = submoduleConfig this.gdprConsentData = gdprConsentData this.cacheIdObj = cacheIdObj this.usPrivacyData = usPrivacyData + this.gppData = gppData } - execute() { - return this.#callForConfig(this.submoduleConfig) - .then(fetchFlowConfig => { - return this.#callForExtensions(fetchFlowConfig.extensionsCall) - .then(extensionsData => { - return this.#callId5Fetch(fetchFlowConfig.fetchCall, extensionsData) - }) - }) - .then(fetchCallResponse => { - try { - resetNb(this.submoduleConfig.params.partner); - if (fetchCallResponse.privacy) { - storeInLocalStorage(ID5_PRIVACY_STORAGE_NAME, JSON.stringify(fetchCallResponse.privacy), NB_EXP_DAYS); - } - } catch (error) { - logError(LOG_PREFIX + error); - } - return fetchCallResponse; - }) + /** + * Calls the ID5 Servers to fetch an ID5 ID + * @returns {Promise} The result of calling the server side + */ + async execute() { + const configCallPromise = this.#callForConfig(); + if (this.#isExternalModule()) { + try { + return await this.#externalModuleFlow(configCallPromise); + } catch (error) { + logError(LOG_PREFIX + 'Error while performing ID5 external module flow. Continuing with regular flow.', error); + return this.#regularFlow(configCallPromise); + } + } else { + return this.#regularFlow(configCallPromise); + } + } + + #isExternalModule() { + return typeof this.submoduleConfig.params.externalModuleUrl === 'string'; + } + + // eslint-disable-next-line no-dupe-class-members + async #externalModuleFlow(configCallPromise) { + await loadExternalModule(this.submoduleConfig.params.externalModuleUrl); + const fetchFlowConfig = await configCallPromise; + + return this.#getExternalIntegration().fetchId5Id(fetchFlowConfig, this.submoduleConfig.params, getRefererInfo(), this.gdprConsentData, this.usPrivacyData, this.gppData); + } + + // eslint-disable-next-line no-dupe-class-members + #getExternalIntegration() { + return window.id5Prebid && window.id5Prebid.integration; } - #ajaxPromise(url, data, options) { - return new Promise((resolve, reject) => { - ajax(url, - { - success: function (res) { - resolve(res) - }, - error: function (err) { - reject(err) - } - }, data, options) - }) + // eslint-disable-next-line no-dupe-class-members + async #regularFlow(configCallPromise) { + const fetchFlowConfig = await configCallPromise; + const extensionsData = await this.#callForExtensions(fetchFlowConfig.extensionsCall); + const fetchCallResponse = await this.#callId5Fetch(fetchFlowConfig.fetchCall, extensionsData); + return this.#processFetchCallResponse(fetchCallResponse); } // eslint-disable-next-line no-dupe-class-members - #callForConfig(submoduleConfig) { - let url = submoduleConfig.params.configUrl || ID5_API_CONFIG_URL; // override for debug/test purposes only - return this.#ajaxPromise(url, JSON.stringify(submoduleConfig), {method: 'POST'}) - .then(response => { - let responseObj = JSON.parse(response); - logInfo(LOG_PREFIX + 'config response received from the server', responseObj); - return responseObj; - }); + async #callForConfig() { + let url = this.submoduleConfig.params.configUrl || ID5_API_CONFIG_URL; // override for debug/test purposes only + const response = await fetch(url, { + method: 'POST', + body: JSON.stringify(this.submoduleConfig) + }); + if (!response.ok) { + throw new Error('Error while calling config endpoint: ', response); + } + const dynamicConfig = await response.json(); + logInfo(LOG_PREFIX + 'config response received from the server', dynamicConfig); + return dynamicConfig; } // eslint-disable-next-line no-dupe-class-members - #callForExtensions(extensionsCallConfig) { + async #callForExtensions(extensionsCallConfig) { if (extensionsCallConfig === undefined) { - return Promise.resolve(undefined) + return undefined; } - let extensionsUrl = extensionsCallConfig.url - let method = extensionsCallConfig.method || 'GET' - let data = method === 'GET' ? undefined : JSON.stringify(extensionsCallConfig.body || {}) - return this.#ajaxPromise(extensionsUrl, data, {'method': method}) - .then(response => { - let responseObj = JSON.parse(response); - logInfo(LOG_PREFIX + 'extensions response received from the server', responseObj); - return responseObj; - }) + const extensionsUrl = extensionsCallConfig.url; + const method = extensionsCallConfig.method || 'GET'; + const body = method === 'GET' ? undefined : JSON.stringify(extensionsCallConfig.body || {}); + const response = await fetch(extensionsUrl, { method, body }); + if (!response.ok) { + throw new Error('Error while calling extensions endpoint: ', response); + } + const extensions = await response.json(); + logInfo(LOG_PREFIX + 'extensions response received from the server', extensions); + return extensions; } // eslint-disable-next-line no-dupe-class-members - #callId5Fetch(fetchCallConfig, extensionsData) { - let url = fetchCallConfig.url; - let additionalData = fetchCallConfig.overrides || {}; - let data = { + async #callId5Fetch(fetchCallConfig, extensionsData) { + const fetchUrl = fetchCallConfig.url; + const additionalData = fetchCallConfig.overrides || {}; + const body = JSON.stringify({ ...this.#createFetchRequestData(), ...additionalData, extensions: extensionsData - }; - return this.#ajaxPromise(url, JSON.stringify(data), {method: 'POST', withCredentials: true}) - .then(response => { - let responseObj = JSON.parse(response); - logInfo(LOG_PREFIX + 'fetch response received from the server', responseObj); - return responseObj; - }); + }); + const response = await fetch(fetchUrl, { method: 'POST', body, credentials: 'include' }); + if (!response.ok) { + throw new Error('Error while calling fetch endpoint: ', response); + } + const fetchResponse = await response.json(); + logInfo(LOG_PREFIX + 'fetch response received from the server', fetchResponse); + return fetchResponse; } // eslint-disable-next-line no-dupe-class-members @@ -250,7 +351,7 @@ class IdFetchFlow { const hasGdpr = (this.gdprConsentData && typeof this.gdprConsentData.gdprApplies === 'boolean' && this.gdprConsentData.gdprApplies) ? 1 : 0; const referer = getRefererInfo(); const signature = (this.cacheIdObj && this.cacheIdObj.signature) ? this.cacheIdObj.signature : getLegacyCookieSignature(); - const nbPage = incrementNb(params.partner); + const nbPage = incrementAndResetNb(params.partner); const data = { 'partner': params.partner, 'gdpr': hasGdpr, @@ -273,6 +374,11 @@ class IdFetchFlow { if (this.usPrivacyData !== undefined && !isEmpty(this.usPrivacyData) && !isEmptyStr(this.usPrivacyData)) { data.us_privacy = this.usPrivacyData; } + if (this.gppData) { + data.gpp_string = this.gppData.gppString; + data.gpp_sid = this.gppData.applicableSections; + } + if (signature !== undefined && !isEmptyStr(signature)) { data.s = signature; } @@ -291,11 +397,52 @@ class IdFetchFlow { } return data; } + + // eslint-disable-next-line no-dupe-class-members + #processFetchCallResponse(fetchCallResponse) { + try { + if (fetchCallResponse.privacy) { + storeInLocalStorage(ID5_PRIVACY_STORAGE_NAME, JSON.stringify(fetchCallResponse.privacy), NB_EXP_DAYS); + } + } catch (error) { + logError(LOG_PREFIX + 'Error while writing privacy info into local storage.', error); + } + return fetchCallResponse; + } } -function hasRequiredConfig(config) { - if (!config || !config.params || !config.params.partner || typeof config.params.partner !== 'number') { - logError(LOG_PREFIX + 'partner required to be defined as a number'); +async function loadExternalModule(url) { + return new GreedyPromise((resolve, reject) => { + if (window.id5Prebid) { + // Already loaded + resolve(); + } else { + try { + loadExternalScript(url, 'id5', resolve); + } catch (error) { + reject(error); + } + } + }); +} + +function validateConfig(config) { + if (!config || !config.params || !config.params.partner) { + logError(LOG_PREFIX + 'partner required to be defined'); + return false; + } + + const partner = config.params.partner + if (typeof partner === 'string' || partner instanceof String) { + let parsedPartnerId = parseInt(partner); + if (isNaN(parsedPartnerId) || parsedPartnerId < 0) { + logError(LOG_PREFIX + 'partner required to be a number or a String parsable to a positive integer'); + return false; + } else { + config.params.partner = parsedPartnerId; + } + } else if (typeof partner !== 'number') { + logError(LOG_PREFIX + 'partner required to be a number or a String parsable to a positive integer'); return false; } @@ -339,8 +486,10 @@ function incrementNb(partnerId) { return nb; } -function resetNb(partnerId) { +function incrementAndResetNb(partnerId) { + const result = incrementNb(partnerId); storeNbInCache(partnerId, 0); + return result; } function getLegacyCookieSignature() { @@ -379,7 +528,7 @@ export function getFromLocalStorage(key) { * by default it's not required * @param {string} key * @param {any} value - * @param {integer} expDays + * @param {number} expDays */ export function storeInLocalStorage(key, value, expDays) { storage.setDataInLocalStorage(`${key}_exp`, expDaysStr(expDays)); diff --git a/modules/id5IdSystem.md b/modules/id5IdSystem.md index cf90290b1d8..592c69056fa 100644 --- a/modules/id5IdSystem.md +++ b/modules/id5IdSystem.md @@ -1,6 +1,6 @@ -# ID5 Universal ID +# ID5 ID -The ID5 ID is a shared, neutral identifier that publishers and ad tech platforms can use to recognise users even in environments where 3rd party cookies are not available. The ID5 ID is designed to respect users' privacy choices and publishers’ preferences throughout the advertising value chain. For more information about the ID5 ID and detailed integration docs, please visit [our documentation](https://support.id5.io/portal/en/kb/articles/prebid-js-user-id-module). +The ID5 ID is a shared, neutral identifier that publishers and ad tech platforms can use to recognise users even in environments where 3rd party cookies are not available. The ID5 ID is designed to respect users' privacy choices and publishers’ preferences throughout the advertising value chain. For more information about the ID5 ID and detailed integration docs, please visit [our documentation](https://wiki.id5.io/en/identitycloud/retrieve-id5-ids/prebid-user-id-module/id5-prebid-user-id-module). ## ID5 ID Registration @@ -25,11 +25,12 @@ pbjs.setConfig({ name: 'id5Id', params: { partner: 173, // change to the Partner Number you received from ID5 + externalModuleUrl: "https://cdn.id5-sync.com/api/1.0/id5PrebidModule.js" // optional but recommended pd: 'MT1iNTBjY...', // optional, see table below for a link to how to generate this abTesting: { // optional enabled: true, // false by default controlGroupPct: 0.1 // valid values are 0.0 - 1.0 (inclusive) - }, + }, disableExtensions: false // optional }, storage: { @@ -49,7 +50,8 @@ pbjs.setConfig({ | name | Required | String | The name of this module: `"id5Id"` | `"id5Id"` | | params | Required | Object | Details for the ID5 ID. | | | params.partner | Required | Number | This is the ID5 Partner Number obtained from registering with ID5. | `173` | -| params.pd | Optional | String | Partner-supplied data used for linking ID5 IDs across domains. See [our documentation](https://support.id5.io/portal/en/kb/articles/passing-partner-data-to-id5) for details on generating the string. Omit the parameter or leave as an empty string if no data to supply | `"MT1iNTBjY..."` | +| params.externalModuleUrl | Optional | String | The URL for the id5-prebid external module. It is recommended to use the latest version at the URL in the example. Source code available [here](https://github.com/id5io/id5-api.js/blob/master/src/id5PrebidModule.js). | https://cdn.id5-sync.com/api/1.0/id5PrebidModule.js +| params.pd | Optional | String | Partner-supplied data used for linking ID5 IDs across domains. See [our documentation](https://wiki.id5.io/en/identitycloud/retrieve-id5-ids/passing-partner-data-to-id5) for details on generating the string. Omit the parameter or leave as an empty string if no data to supply | `"MT1iNTBjY..."` | | params.provider | Optional | String | An identifier provided by ID5 to technology partners who manage Prebid setups on behalf of publishers. Reach out to [ID5](mailto:prebid@id5.io) if you have questions about this parameter | `pubmatic-identity-hub` | | params.abTesting | Optional | Object | Allows publishers to easily run an A/B Test. If enabled and the user is in the Control Group, the ID5 ID will NOT be exposed to bid adapters for that request | Disabled by default | | params.abTesting.enabled | Optional | Boolean | Set this to `true` to turn on this feature | `true` or `false` | @@ -68,3 +70,6 @@ pbjs.setConfig({ Publishers may want to test the value of the ID5 ID with their downstream partners. While there are various ways to do this, A/B testing is a standard approach. Instead of publishers manually enabling or disabling the ID5 User ID Module based on their control group settings (which leads to fewer calls to ID5, reducing our ability to recognize the user), we have baked this in to our module directly. To turn on A/B Testing, simply edit the configuration (see above table) to enable it and set what percentage of users you would like to set for the control group. The control group is the set of user where an ID5 ID will not be exposed in to bid adapters or in the various user id functions available on the `pbjs` global. An additional value of `ext.abTestingControlGroup` will be set to `true` or `false` that can be used to inform reporting systems that the user was in the control group or not. It's important to note that the control group is user based, and not request based. In other words, from one page view to another, a user will always be in or out of the control group. + +### A Note on Using Multiple Wrappers +If you or your monetization partners are deploying multiple Prebid wrappers on your websites, you should make sure you add the ID5 ID User ID module to *every* wrapper. Only the bidders configured in the Prebid wrapper where the ID5 ID User ID module is installed and configured will be able to pick up the ID5 ID. Bidders from other Prebid instances will not be able to pick up the ID5 ID. diff --git a/modules/idWardRtdProvider.js b/modules/idWardRtdProvider.js index 29dda216fdc..dd08a132b2d 100644 --- a/modules/idWardRtdProvider.js +++ b/modules/idWardRtdProvider.js @@ -9,6 +9,9 @@ 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'; +/** + * @typedef {import('../modules/rtdModule/index.js').RtdSubmodule} RtdSubmodule + */ const MODULE_NAME = 'realTimeData'; const SUBMODULE_NAME = 'idWard'; @@ -28,9 +31,9 @@ function addRealTimeData(ortb2, rtd) { } /** - * Try parsing stringified array of segment IDs. - * @param {String} data - */ + * Try parsing stringified array of segment IDs. + * @param {String} data + */ function tryParse(data) { try { return JSON.parse(data); @@ -41,12 +44,12 @@ function tryParse(data) { } /** - * Real-time data retrieval from ID Ward - * @param {Object} reqBidsConfigObj - * @param {function} onDone - * @param {Object} rtdConfig - * @param {Object} userConsent - */ + * Real-time data retrieval from ID Ward + * @param {Object} reqBidsConfigObj + * @param {function} onDone + * @param {Object} rtdConfig + * @param {Object} userConsent + */ export function getRealTimeData(reqBidsConfigObj, onDone, rtdConfig, userConsent) { if (rtdConfig && isPlainObject(rtdConfig.params)) { const jsonData = storage.getDataFromLocalStorage(rtdConfig.params.cohortStorageKey) @@ -85,11 +88,11 @@ export function getRealTimeData(reqBidsConfigObj, onDone, rtdConfig, userConsent } /** - * Module init - * @param {Object} provider - * @param {Object} userConsent - * @return {boolean} - */ + * Module init + * @param {Object} provider + * @param {Object} userConsent + * @return {boolean} + */ function init(provider, userConsent) { return true; } diff --git a/modules/identityLinkIdSystem.js b/modules/identityLinkIdSystem.js index ab10288f38f..82aa2303e1c 100644 --- a/modules/identityLinkIdSystem.js +++ b/modules/identityLinkIdSystem.js @@ -10,11 +10,21 @@ import { ajax } from '../src/ajax.js'; import { submodule } from '../src/hook.js'; import {getStorageManager} from '../src/storageManager.js'; import {MODULE_TYPE_UID} from '../src/activities/modules.js'; +import { gppDataHandler } from '../src/adapterManager.js'; + +/** + * @typedef {import('../modules/userId/index.js').Submodule} Submodule + * @typedef {import('../modules/userId/index.js').SubmoduleConfig} SubmoduleConfig + * @typedef {import('../modules/userId/index.js').ConsentData} ConsentData + * @typedef {import('../modules/userId/index.js').IdResponse} IdResponse + */ const MODULE_NAME = 'identityLink'; export const storage = getStorageManager({moduleType: MODULE_TYPE_UID, moduleName: MODULE_NAME}); +const liverampEnvelopeName = '_lr_env'; + /** @type {Submodule} */ export const identityLinkSubmodule = { /** @@ -51,18 +61,21 @@ export const identityLinkSubmodule = { } const hasGdpr = (consentData && typeof consentData.gdprApplies === 'boolean' && consentData.gdprApplies) ? 1 : 0; const gdprConsentString = hasGdpr ? consentData.consentString : ''; - const tcfPolicyV2 = utils.deepAccess(consentData, 'vendorData.tcfPolicyVersion') === 2; // use protocol relative urls for http or https if (hasGdpr && (!gdprConsentString || gdprConsentString === '')) { utils.logInfo('identityLink: Consent string is required to call envelope API.'); return; } - const url = `https://api.rlcdn.com/api/identity/envelope?pid=${configParams.pid}${hasGdpr ? (tcfPolicyV2 ? '&ct=4&cv=' : '&ct=1&cv=') + gdprConsentString : ''}`; + const gppData = gppDataHandler.getConsentData(); + const gppString = gppData && gppData.gppString ? gppData.gppString : false; + const gppSectionId = gppData && gppData.gppString && gppData.applicableSections.length > 0 && gppData.applicableSections[0] !== -1 ? gppData.applicableSections[0] : false; + const hasGpp = gppString && gppSectionId; + const url = `https://api.rlcdn.com/api/identity/envelope?pid=${configParams.pid}${hasGdpr ? '&ct=4&cv=' + gdprConsentString : ''}${hasGpp ? '&gpp=' + gppString + '&gpp_sid=' + gppSectionId : ''}`; let resp; resp = function (callback) { // Check ats during callback so it has a chance to initialise. // If ats library is available, use it to retrieve envelope. If not use standard third party endpoint - if (window.ats) { + if (window.ats && window.ats.retrieveEnvelope) { utils.logInfo('identityLink: ATS exists!'); window.ats.retrieveEnvelope(function (envelope) { if (envelope) { @@ -74,11 +87,24 @@ export const identityLinkSubmodule = { } }); } else { - getEnvelope(url, callback, configParams); + // try to get envelope directly from storage if ats lib is not present on a page + let envelope = getEnvelopeFromStorage(); + if (envelope) { + utils.logInfo('identityLink: LiveRamp envelope successfully retrieved from storage!'); + callback(JSON.parse(envelope).envelope); + } else { + getEnvelope(url, callback, configParams); + } } }; return { callback: resp }; + }, + eids: { + 'idl_env': { + source: 'liveramp.com', + atype: 3 + }, } }; // return envelope from third party endpoint @@ -121,4 +147,9 @@ function setEnvelopeSource(src) { storage.setCookie('_lr_env_src_ats', src, now.toUTCString()); } +export function getEnvelopeFromStorage() { + let rawEnvelope = storage.getCookie(liverampEnvelopeName) || storage.getDataFromLocalStorage(liverampEnvelopeName); + return rawEnvelope ? window.atob(rawEnvelope) : undefined; +} + submodule('userId', identityLinkSubmodule); diff --git a/modules/idxIdSystem.js b/modules/idxIdSystem.js index 3c00bbde34c..db545eecd8c 100644 --- a/modules/idxIdSystem.js +++ b/modules/idxIdSystem.js @@ -9,6 +9,11 @@ import { submodule } from '../src/hook.js'; import {getStorageManager} from '../src/storageManager.js'; import {MODULE_TYPE_UID} from '../src/activities/modules.js'; +/** + * @typedef {import('../modules/userId/index.js').Submodule} Submodule + * @typedef {import('../modules/userId/index.js').SubmoduleConfig} SubmoduleConfig + */ + const IDX_MODULE_NAME = 'idx'; const IDX_COOKIE_NAME = '_idx'; export const storage = getStorageManager({moduleType: MODULE_TYPE_UID, moduleName: IDX_MODULE_NAME}); @@ -57,6 +62,12 @@ export const idxIdSubmodule = { } } return undefined; + }, + eids: { + 'idx': { + source: 'idx.lat', + atype: 1 + }, } }; submodule('userId', idxIdSubmodule); diff --git a/modules/illuminBidAdapter.js b/modules/illuminBidAdapter.js new file mode 100644 index 00000000000..36ff7bea0a3 --- /dev/null +++ b/modules/illuminBidAdapter.js @@ -0,0 +1,336 @@ +import {_each, deepAccess, parseSizesInput, parseUrl, uniques, isFn} from '../src/utils.js'; +import {registerBidder} from '../src/adapters/bidderFactory.js'; +import {BANNER, VIDEO} from '../src/mediaTypes.js'; +import {getStorageManager} from '../src/storageManager.js'; +import {config} from '../src/config.js'; + +const DEFAULT_SUB_DOMAIN = 'exchange'; +const BIDDER_CODE = 'illumin'; +const BIDDER_VERSION = '1.0.0'; +const CURRENCY = 'USD'; +const TTL_SECONDS = 60 * 5; +const UNIQUE_DEAL_ID_EXPIRY = 1000 * 60 * 15; +const storage = getStorageManager({bidderCode: BIDDER_CODE}); + +function getTopWindowQueryParams() { + try { + const parsedUrl = parseUrl(window.top.document.URL, {decodeSearchAsString: true}); + return parsedUrl.search; + } catch (e) { + return ''; + } +} + +export function createDomain(subDomain = DEFAULT_SUB_DOMAIN) { + return `https://${subDomain}.illumin.com`; +} + +export function extractCID(params) { + return params.cId; +} + +export function extractPID(params) { + return params.pId; +} + +export function extractSubDomain(params) { + return params.subDomain; +} + +function isBidRequestValid(bid) { + const params = bid.params || {}; + return !!(extractCID(params) && extractPID(params)); +} + +function buildRequest(bid, topWindowUrl, sizes, bidderRequest, bidderTimeout) { + const { + params, + bidId, + userId, + adUnitCode, + schain, + mediaTypes, + ortb2Imp, + bidderRequestId, + bidRequestsCount, + bidderRequestsCount, + bidderWinsCount + } = bid; + let {bidFloor, ext} = params; + const hashUrl = hashCode(topWindowUrl); + const uniqueDealId = getUniqueDealId(hashUrl); + const cId = extractCID(params); + const pId = extractPID(params); + const subDomain = extractSubDomain(params); + + const gpid = deepAccess(bid, 'ortb2Imp.ext.gpid', deepAccess(bid, 'ortb2Imp.ext.data.pbadslot', '')); + + if (isFn(bid.getFloor)) { + const floorInfo = bid.getFloor({ + currency: 'USD', + mediaType: '*', + size: '*' + }); + + if (floorInfo.currency === 'USD') { + bidFloor = floorInfo.floor; + } + } + + let data = { + url: encodeURIComponent(topWindowUrl), + uqs: getTopWindowQueryParams(), + cb: Date.now(), + bidFloor: bidFloor, + bidId: bidId, + referrer: bidderRequest.refererInfo.ref, + adUnitCode: adUnitCode, + publisherId: pId, + sizes: sizes, + uniqueDealId: uniqueDealId, + bidderVersion: BIDDER_VERSION, + prebidVersion: '$prebid.version$', + res: `${screen.width}x${screen.height}`, + schain: schain, + mediaTypes: mediaTypes, + gpid: gpid, + transactionId: ortb2Imp?.ext?.tid, + bidderRequestId: bidderRequestId, + bidRequestsCount: bidRequestsCount, + bidderRequestsCount: bidderRequestsCount, + bidderWinsCount: bidderWinsCount, + bidderTimeout: bidderTimeout + }; + + appendUserIdsToRequestPayload(data, userId); + + const sua = deepAccess(bidderRequest, 'ortb2.device.sua'); + + if (sua) { + data.sua = sua; + } + + if (bidderRequest.gdprConsent) { + if (bidderRequest.gdprConsent.consentString) { + data.gdprConsent = bidderRequest.gdprConsent.consentString; + } + if (bidderRequest.gdprConsent.gdprApplies !== undefined) { + data.gdpr = bidderRequest.gdprConsent.gdprApplies ? 1 : 0; + } + } + if (bidderRequest.uspConsent) { + data.usPrivacy = bidderRequest.uspConsent; + } + + if (bidderRequest.gppConsent) { + data.gppString = bidderRequest.gppConsent.gppString; + data.gppSid = bidderRequest.gppConsent.applicableSections; + } else if (bidderRequest.ortb2?.regs?.gpp) { + data.gppString = bidderRequest.ortb2.regs.gpp; + data.gppSid = bidderRequest.ortb2.regs.gpp_sid; + } + + const dto = { + method: 'POST', + url: `${createDomain(subDomain)}/prebid/multi/${cId}`, + data: data + }; + + _each(ext, (value, key) => { + dto.data['ext.' + key] = value; + }); + + return dto; +} + +function appendUserIdsToRequestPayload(payloadRef, userIds) { + let key; + _each(userIds, (userId, idSystemProviderName) => { + key = `uid.${idSystemProviderName}`; + + switch (idSystemProviderName) { + case 'digitrustid': + payloadRef[key] = deepAccess(userId, 'data.id'); + break; + case 'lipb': + payloadRef[key] = userId.lipbid; + break; + case 'parrableId': + payloadRef[key] = userId.eid; + break; + case 'id5id': + payloadRef[key] = userId.uid; + break; + default: + payloadRef[key] = userId; + } + }); +} + +function buildRequests(validBidRequests, bidderRequest) { + const topWindowUrl = bidderRequest.refererInfo.page || bidderRequest.refererInfo.topmostLocation; + const bidderTimeout = config.getConfig('bidderTimeout'); + const requests = []; + validBidRequests.forEach(validBidRequest => { + const sizes = parseSizesInput(validBidRequest.sizes); + const request = buildRequest(validBidRequest, topWindowUrl, sizes, bidderRequest, bidderTimeout); + requests.push(request); + }); + return requests; +} + +function interpretResponse(serverResponse, request) { + if (!serverResponse || !serverResponse.body) { + return []; + } + const {bidId} = request.data; + const {results} = serverResponse.body; + + let output = []; + + try { + results.forEach(result => { + const { + creativeId, + ad, + price, + exp, + width, + height, + currency, + metaData, + advertiserDomains, + mediaType = BANNER + } = result; + if (!ad || !price) { + return; + } + + const response = { + requestId: bidId, + cpm: price, + width: width, + height: height, + creativeId: creativeId, + currency: currency || CURRENCY, + netRevenue: true, + ttl: exp || TTL_SECONDS, + }; + + if (metaData) { + Object.assign(response, { + meta: metaData + }) + } else { + Object.assign(response, { + meta: { + advertiserDomains: advertiserDomains || [] + } + }) + } + + if (mediaType === BANNER) { + Object.assign(response, { + ad: ad, + }); + } else { + Object.assign(response, { + vastXml: ad, + mediaType: VIDEO + }); + } + output.push(response); + }); + return output; + } catch (e) { + return []; + } +} + +function getUserSyncs(syncOptions, responses, gdprConsent = {}, uspConsent = '') { + let syncs = []; + const {iframeEnabled, pixelEnabled} = syncOptions; + const {gdprApplies, consentString = ''} = gdprConsent; + + const cidArr = responses.filter(resp => deepAccess(resp, 'body.cid')).map(resp => resp.body.cid).filter(uniques); + const params = `?cid=${encodeURIComponent(cidArr.join(','))}&gdpr=${gdprApplies ? 1 : 0}&gdpr_consent=${encodeURIComponent(consentString || '')}&us_privacy=${encodeURIComponent(uspConsent || '')}` + if (iframeEnabled) { + syncs.push({ + type: 'iframe', + url: `https://sync.illumin.com/api/sync/iframe/${params}` + }); + } + if (pixelEnabled) { + syncs.push({ + type: 'image', + url: `https://sync.illumin.com/api/sync/image/${params}` + }); + } + return syncs; +} + +export function hashCode(s, prefix = '_') { + const l = s.length; + let h = 0 + let i = 0; + if (l > 0) { + while (i < l) { + h = (h << 5) - h + s.charCodeAt(i++) | 0; + } + } + return prefix + h; +} + +export function getUniqueDealId(key, expiry = UNIQUE_DEAL_ID_EXPIRY) { + const storageKey = `u_${key}`; + const now = Date.now(); + const data = getStorageItem(storageKey); + let uniqueId; + + if (!data || !data.value || now - data.created > expiry) { + uniqueId = `${key}_${now.toString()}`; + setStorageItem(storageKey, uniqueId); + } else { + uniqueId = data.value; + } + + return uniqueId; +} + +export function getStorageItem(key) { + try { + return tryParseJSON(storage.getDataFromLocalStorage(key)); + } catch (e) { + } + + return null; +} + +export function setStorageItem(key, value, timestamp) { + try { + const created = timestamp || Date.now(); + const data = JSON.stringify({value, created}); + storage.setDataInLocalStorage(key, data); + } catch (e) { + } +} + +export function tryParseJSON(value) { + try { + return JSON.parse(value); + } catch (e) { + return value; + } +} + +export const spec = { + code: BIDDER_CODE, + version: BIDDER_VERSION, + supportedMediaTypes: [BANNER, VIDEO], + isBidRequestValid, + buildRequests, + interpretResponse, + getUserSyncs +}; + +registerBidder(spec); diff --git a/modules/illuminBidAdapter.md b/modules/illuminBidAdapter.md new file mode 100644 index 00000000000..8ca656230e4 --- /dev/null +++ b/modules/illuminBidAdapter.md @@ -0,0 +1,35 @@ +# Overview + +**Module Name:** Illumin Bid Adapter + +**Module Type:** Bidder Adapter + +**Maintainer:** integrations@illumin.com + +# Description + +Module that connects to Illumin's demand sources. + +# Test Parameters +```js +var adUnits = [ + { + code: 'test-ad', + sizes: [[300, 250]], + bids: [ + { + bidder: 'illumin', + params: { + cId: '562524b21b1c1f08117fc7f9', + pId: '59ac17c192832d0011283fe3', + bidFloor: 0.0001, + ext: { + param1: 'loremipsum', + param2: 'dolorsitamet' + } + } + } + ] + } +]; +``` diff --git a/modules/imRtdProvider.js b/modules/imRtdProvider.js index 26d49c49f8c..78681c2beda 100644 --- a/modules/imRtdProvider.js +++ b/modules/imRtdProvider.js @@ -20,6 +20,10 @@ import { import {submodule} from '../src/hook.js'; import {MODULE_TYPE_RTD} from '../src/activities/modules.js'; +/** + * @typedef {import('../modules/rtdModule/index.js').RtdSubmodule} RtdSubmodule + */ + export const imUidLocalName = '__im_uid'; export const imVidCookieName = '_im_vid'; export const imRtdLocalName = '__im_sids'; @@ -50,8 +54,8 @@ function getSegments(segments, moduleConfig) { } /** -* @param {string} bidderName -*/ + * @param {string} bidderName + */ export function getBidderFunction(bidderName) { const biddersFunction = { pubmatic: function (bid, data, moduleConfig) { diff --git a/modules/imdsBidAdapter.js b/modules/imdsBidAdapter.js index 545a0bd1ac3..4cad1d614c5 100644 --- a/modules/imdsBidAdapter.js +++ b/modules/imdsBidAdapter.js @@ -1,14 +1,16 @@ 'use strict'; -import {deepAccess, deepSetValue, getAdUnitSizes, isFn, isPlainObject, logWarn} from '../src/utils.js'; +import {deepAccess, deepSetValue, isFn, isPlainObject, logWarn, mergeDeep} from '../src/utils.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import {BANNER, VIDEO} from '../src/mediaTypes.js'; import {includes} from '../src/polyfill.js'; import {config} from '../src/config.js'; +import {getAdUnitSizes} from '../libraries/sizeUtils/sizeUtils.js'; const BID_SCHEME = 'https://'; const BID_DOMAIN = 'technoratimedia.com'; -const USER_SYNC_HOST = 'https://ad-cdn.technoratimedia.com'; +const USER_SYNC_IFRAME_URL = 'https://ad-cdn.technoratimedia.com/html/usersync.html'; +const USER_SYNC_PIXEL_URL = 'https://sync.technoratimedia.com/services'; const VIDEO_PARAMS = [ 'minduration', 'maxduration', 'startdelay', 'placement', 'linearity', 'mimes', 'protocols', 'api' ]; const BLOCKED_AD_SIZES = [ '1x1', @@ -38,11 +40,11 @@ export const spec = { return; } const refererInfo = bidderRequest.refererInfo; - const openRtbBidRequest = { + // start with some defaults, overridden by anything set in ortb2, if provided. + const openRtbBidRequest = mergeDeep({ id: bidderRequest.bidderRequestId, site: { - // TODO: does the fallback make sense here? - domain: refererInfo.domain || location.hostname, + domain: refererInfo.domain, page: refererInfo.page, ref: refererInfo.ref }, @@ -50,7 +52,7 @@ export const spec = { ua: navigator.userAgent }, imp: [] - }; + }, bidderRequest.ortb2 || {}); const tmax = bidderRequest.timeout; if (tmax) { @@ -101,9 +103,17 @@ export const spec = { } }); - // CCPA - if (bidderRequest.uspConsent) { - deepSetValue(openRtbBidRequest, 'regs.ext.us_privacy', bidderRequest.uspConsent); + // Move us_privacy from regs.ext to regs if there isn't already a us_privacy in regs + if (openRtbBidRequest.regs?.ext?.us_privacy && !openRtbBidRequest.regs?.us_privacy) { + deepSetValue(openRtbBidRequest, 'regs.us_privacy', openRtbBidRequest.regs.ext.us_privacy); + } + + // Remove regs.ext.us_privacy + if (openRtbBidRequest.regs?.ext?.us_privacy) { + delete openRtbBidRequest.regs.ext.us_privacy; + if (Object.keys(openRtbBidRequest.regs.ext).length < 1) { + delete openRtbBidRequest.regs.ext; + } } // User ID @@ -117,7 +127,7 @@ export const spec = { if (openRtbBidRequest.imp.length && seatId) { return { method: 'POST', - url: `${BID_SCHEME}${seatId}.${BID_DOMAIN}/openrtb/bids/${seatId}?src=$$REPO_AND_VERSION$$`, + url: `${BID_SCHEME}${seatId}.${BID_DOMAIN}/openrtb/bids/${seatId}?src=pbjs%2F$prebid.version$`, data: openRtbBidRequest, options: { contentType: 'application/json', @@ -214,7 +224,6 @@ export const spec = { }; if (!serverResponse.body || typeof serverResponse.body != 'object') { - logWarn('IMDS: server returned empty/non-json response: ' + JSON.stringify(serverResponse.body)); return; } const {id, seatbid: seatbids} = serverResponse.body; @@ -294,16 +303,31 @@ export const spec = { } return bids; }, - getUserSyncs: function (syncOptions, serverResponses) { + getUserSyncs: function(syncOptions, serverResponses, gdprConsent, uspConsent, gppConsent) { const syncs = []; + const queryParams = ['src=pbjs%2F$prebid.version$']; + if (gdprConsent) { + queryParams.push(`gdpr=${Number(gdprConsent.gdprApplies && 1)}&consent=${encodeURIComponent(gdprConsent.consentString || '')}`); + } + if (uspConsent) { + queryParams.push('us_privacy=' + encodeURIComponent(uspConsent)); + } + if (gppConsent) { + queryParams.push('gpp=' + encodeURIComponent(gppConsent.gppString || '') + '&gppsid=' + encodeURIComponent((gppConsent.applicableSections || []).join(','))); + } + if (syncOptions.iframeEnabled) { syncs.push({ type: 'iframe', - url: `${USER_SYNC_HOST}/html/usersync.html?src=$$REPO_AND_VERSION$$` + url: `${USER_SYNC_IFRAME_URL}?${queryParams.join('&')}` + }); + } else if (syncOptions.pixelEnabled) { + syncs.push({ + type: 'image', + url: `${USER_SYNC_PIXEL_URL}?srv=cs&${queryParams.join('&')}` }); - } else { - logWarn('IMDS: Please enable iframe based user sync.'); } + return syncs; } }; diff --git a/modules/imdsBidAdapter.md b/modules/imdsBidAdapter.md index 15fb407e7ef..2a50868d726 100644 --- a/modules/imdsBidAdapter.md +++ b/modules/imdsBidAdapter.md @@ -11,11 +11,11 @@ Maintainer: eng-demand@imds.tv The iMedia Digital Services adapter requires setup and approval from iMedia Digital Services. Please reach out to your account manager for more information. -### DFP Video Creative -To use video, setup a `VAST redirect` creative within Google AdManager (DFP) with the following VAST tag URL: +### Google Ad Manager Video Creative +To use video, setup a `VAST redirect` creative within Google Ad Manager with the following VAST tag URL: -``` -https://track.technoratimedia.com/openrtb/tags?ID=%%PATTERN:hb_cache_id_synacorm%%&AUCTION_PRICE=%%PATTERN:hb_pb_synacormedia%% +```text +https://track.technoratimedia.com/openrtb/tags?ID=%%PATTERN:hb_uuid_imds%%&AUCTION_PRICE=%%PATTERN:hb_pb_imds%% ``` # Test Parameters diff --git a/modules/impactifyBidAdapter.js b/modules/impactifyBidAdapter.js index f2bf9aaddcb..ea446bd150d 100644 --- a/modules/impactifyBidAdapter.js +++ b/modules/impactifyBidAdapter.js @@ -1,7 +1,18 @@ -import {deepAccess, deepSetValue, generateUUID} from '../src/utils.js'; -import {registerBidder} from '../src/adapters/bidderFactory.js'; -import {config} from '../src/config.js'; -import {ajax} from '../src/ajax.js'; +'use strict'; + +import { deepAccess, deepSetValue, generateUUID } from '../src/utils.js'; +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { config } from '../src/config.js'; +import { ajax } from '../src/ajax.js'; +import { getStorageManager } from '../src/storageManager.js'; + +/** + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid + * @typedef {import('../src/adapters/bidderFactory.js').ServerResponse} ServerResponse + * @typedef {import('../src/adapters/bidderFactory.js').SyncOptions} SyncOptions + * @typedef {import('../src/adapters/bidderFactory.js').UserSync} UserSync + */ const BIDDER_CODE = 'impactify'; const BIDDER_ALIAS = ['imp']; @@ -10,58 +21,125 @@ const DEFAULT_VIDEO_WIDTH = 640; const DEFAULT_VIDEO_HEIGHT = 360; const ORIGIN = 'https://sonic.impactify.media'; const LOGGER_URI = 'https://logger.impactify.media'; -const AUCTIONURI = '/bidder'; -const COOKIESYNCURI = '/static/cookie_sync.html'; -const GVLID = 606; -const GETCONFIG = config.getConfig; - -const getDeviceType = () => { - // OpenRTB Device type - if ((/ipad|android 3.0|xoom|sch-i800|playbook|tablet|kindle/i.test(navigator.userAgent.toLowerCase()))) { - return 5; - } - if ((/iphone|ipod|android|blackberry|opera|mini|windows\sce|palm|smartphone|iemobile/i.test(navigator.userAgent.toLowerCase()))) { - return 4; - } - return 2; -}; +const AUCTION_URI = '/bidder'; +const COOKIE_SYNC_URI = '/static/cookie_sync.html'; +const GVL_ID = 606; +const GET_CONFIG = config.getConfig; +export const STORAGE = getStorageManager({ gvlid: GVL_ID, bidderCode: BIDDER_CODE }); +export const STORAGE_KEY = '_im_str' + +/** + * Helpers object + * @type {{getExtParamsFromBid(*): {impactify: {appId}}, createOrtbImpVideoObj(*): {context: string, playerSize: [number,number], id: string, mimes: [string]}, getDeviceType(): (number), createOrtbImpBannerObj(*, *): {format: [], id: string}}} + */ +const helpers = { + getExtParamsFromBid(bid) { + let ext = { + impactify: { + appId: bid.params.appId + }, + }; -const getFloor = (bid) => { - const floorInfo = bid.getFloor({ - currency: DEFAULT_CURRENCY, - mediaType: '*', - size: '*' - }); - if (typeof floorInfo === 'object' && floorInfo.currency === DEFAULT_CURRENCY && !isNaN(parseFloat(floorInfo.floor))) { - return parseFloat(floorInfo.floor); + if (typeof bid.params.format == 'string') { + ext.impactify.format = bid.params.format; + } + + if (typeof bid.params.style == 'string') { + ext.impactify.style = bid.params.style; + } + + if (typeof bid.params.container == 'string') { + ext.impactify.container = bid.params.container; + } + + if (typeof bid.params.size == 'string') { + ext.impactify.size = bid.params.size; + } + + return ext; + }, + + getDeviceType() { + // OpenRTB Device type + if ((/ipad|android 3.0|xoom|sch-i800|playbook|tablet|kindle/i.test(navigator.userAgent.toLowerCase()))) { + return 5; + } + if ((/iphone|ipod|android|blackberry|opera|mini|windows\sce|palm|smartphone|iemobile/i.test(navigator.userAgent.toLowerCase()))) { + return 4; + } + return 2; + }, + + createOrtbImpBannerObj(bid, size) { + let sizes = size.split('x'); + + return { + id: 'banner-' + bid.bidId, + format: [{ + w: parseInt(sizes[0]), + h: parseInt(sizes[1]) + }] + } + }, + + createOrtbImpVideoObj(bid) { + return { + id: 'video-' + bid.bidId, + playerSize: [DEFAULT_VIDEO_WIDTH, DEFAULT_VIDEO_HEIGHT], + context: 'outstream', + mimes: ['video/mp4'], + } + }, + + getFloor(bid) { + const floorInfo = bid.getFloor({ + currency: DEFAULT_CURRENCY, + mediaType: '*', + size: '*' + }); + if (typeof floorInfo === 'object' && floorInfo.currency === DEFAULT_CURRENCY && !isNaN(parseFloat(floorInfo.floor))) { + return parseFloat(floorInfo.floor); + } + return null; + }, + + getImStrFromLocalStorage() { + return STORAGE.localStorageIsEnabled(false) ? STORAGE.getDataFromLocalStorage(STORAGE_KEY, false) : ''; } - return null; + } -const createOpenRtbRequest = (validBidRequests, bidderRequest) => { +/** + * Create an OpenRTB formated object from prebid payload + * @param validBidRequests + * @param bidderRequest + * @returns {{cur: string[], validBidRequests, id, source: {tid}, imp: *[]}} + */ +function createOpenRtbRequest(validBidRequests, bidderRequest) { // Create request and set imp bids inside let request = { id: bidderRequest.bidderRequestId, validBidRequests, cur: [DEFAULT_CURRENCY], imp: [], - source: {tid: bidderRequest.ortb2?.source?.tid} + source: { tid: bidderRequest.ortb2?.source?.tid } }; // Get the url parameters const queryString = window.location.search; const urlParams = new URLSearchParams(queryString); const checkPrebid = urlParams.get('_checkPrebid'); - // Force impactify debugging parameter + + // Force impactify debugging parameter if present if (checkPrebid != null) { request.test = Number(checkPrebid); } - // Set Schain in request + // Set SChain in request let schain = deepAccess(validBidRequests, '0.schain'); if (schain) request.source.ext = { schain: schain }; - // Set eids + // Set Eids let eids = deepAccess(validBidRequests, '0.userIdAsEids'); if (eids && eids.length) { deepSetValue(request, 'user.ext.eids', eids); @@ -73,13 +151,13 @@ const createOpenRtbRequest = (validBidRequests, bidderRequest) => { request.device = { w: window.innerWidth, h: window.innerHeight, - devicetype: getDeviceType(), + devicetype: helpers.getDeviceType(), ua: navigator.userAgent, js: 1, dnt: (navigator.doNotTrack == 'yes' || navigator.doNotTrack == '1' || navigator.msDoNotTrack == '1') ? 1 : 0, language: ((navigator.language || navigator.userLanguage || '').split('-'))[0] || 'en', }; - request.site = {page: bidderRequest.refererInfo.page}; + request.site = { page: bidderRequest.refererInfo.page }; // Handle privacy settings for GDPR/CCPA/COPPA let gdprApplies = 0; @@ -91,9 +169,10 @@ const createOpenRtbRequest = (validBidRequests, bidderRequest) => { if (bidderRequest.uspConsent) { deepSetValue(request, 'regs.ext.us_privacy', bidderRequest.uspConsent); + this.syncStore.uspConsent = bidderRequest.uspConsent; } - if (GETCONFIG('coppa') == true) deepSetValue(request, 'regs.coppa', 1); + if (GET_CONFIG('coppa') == true) deepSetValue(request, 'regs.coppa', 1); if (bidderRequest.uspConsent) { deepSetValue(request, 'regs.ext.us_privacy', bidderRequest.uspConsent); @@ -104,42 +183,47 @@ const createOpenRtbRequest = (validBidRequests, bidderRequest) => { // Create imps with bids validBidRequests.forEach((bid) => { + let bannerObj = deepAccess(bid.mediaTypes, `banner`); + let imp = { id: bid.bidId, bidfloor: bid.params.bidfloor ? bid.params.bidfloor : 0, - ext: { - impactify: { - appId: bid.params.appId, - format: bid.params.format, - style: bid.params.style - }, - }, - video: { - playerSize: [DEFAULT_VIDEO_WIDTH, DEFAULT_VIDEO_HEIGHT], - context: 'outstream', - mimes: ['video/mp4'], - }, + ext: helpers.getExtParamsFromBid(bid) }; - if (bid.params.container) { - imp.ext.impactify.container = bid.params.container; + + if (bannerObj && typeof imp.ext.impactify.size == 'string') { + imp.banner = { + ...helpers.createOrtbImpBannerObj(bid, imp.ext.impactify.size) + } + } else { + imp.video = { + ...helpers.createOrtbImpVideoObj(bid) + } } + if (typeof bid.getFloor === 'function') { - const floor = getFloor(bid); + const floor = helpers.getFloor(bid); if (floor) { imp.bidfloor = floor; } } + request.imp.push(imp); }); return request; -}; +} +/** + * Export BidderSpec type object and register it to Prebid + * @type {{supportedMediaTypes: string[], interpretResponse: ((function(ServerResponse, *): Bid[])|*), code: string, aliases: string[], getUserSyncs: ((function(SyncOptions, ServerResponse[], *, *): UserSync[])|*), buildRequests: (function(*, *): {method: string, data: string, url}), onTimeout: (function(*): boolean), gvlid: number, isBidRequestValid: ((function(BidRequest): (boolean))|*), onBidWon: (function(*): boolean)}} + */ export const spec = { code: BIDDER_CODE, - gvlid: GVLID, + gvlid: GVL_ID, supportedMediaTypes: ['video', 'banner'], aliases: BIDDER_ALIAS, + storageAllowed: true, /** * Determines whether or not the given bid request is valid. @@ -148,13 +232,16 @@ export const spec = { * @return boolean True if this is a valid bid, and false otherwise. */ isBidRequestValid: function (bid) { - if (!bid.params.appId || typeof bid.params.appId != 'string' || !bid.params.format || typeof bid.params.format != 'string' || !bid.params.style || typeof bid.params.style != 'string') { + if (typeof bid.params.appId != 'string' || !bid.params.appId) { return false; } - if (bid.params.format != 'screen' && bid.params.format != 'display') { + if (typeof bid.params.format != 'string' || typeof bid.params.style != 'string' || !bid.params.format || !bid.params.style) { return false; } - if (bid.params.style != 'inline' && bid.params.style != 'impact' && bid.params.style != 'static') { + if (bid.params.format !== 'screen' && bid.params.format !== 'display') { + return false; + } + if (bid.params.style !== 'inline' && bid.params.style !== 'impact' && bid.params.style !== 'static') { return false; } @@ -171,11 +258,20 @@ export const spec = { buildRequests: function (validBidRequests, bidderRequest) { // Create a clean openRTB request let request = createOpenRtbRequest(validBidRequests, bidderRequest); + const imStr = helpers.getImStrFromLocalStorage(); + const options = {} + + if (imStr) { + options.customHeaders = { + 'x-impact': imStr + }; + } return { method: 'POST', - url: ORIGIN + AUCTIONURI, + url: ORIGIN + AUCTION_URI, data: JSON.stringify(request), + options }; }, @@ -265,16 +361,16 @@ export const spec = { return [{ type: 'iframe', - url: ORIGIN + COOKIESYNCURI + params + url: ORIGIN + COOKIE_SYNC_URI + params }]; }, /** * Register bidder specific code, which will execute if a bid from this bidder won the auction * @param {Bid} The bid that won the auction - */ - onBidWon: function(bid) { - ajax(`${LOGGER_URI}/log/bidder/won`, null, JSON.stringify(bid), { + */ + onBidWon: function (bid) { + ajax(`${LOGGER_URI}/prebid/won`, null, JSON.stringify(bid), { method: 'POST', contentType: 'application/json' }); @@ -285,9 +381,9 @@ export const spec = { /** * Register bidder specific code, which will execute if bidder timed out after an auction * @param {data} Containing timeout specific data - */ - onTimeout: function(data) { - ajax(`${LOGGER_URI}/log/bidder/timeout`, null, JSON.stringify(data[0]), { + */ + onTimeout: function (data) { + ajax(`${LOGGER_URI}/prebid/timeout`, null, JSON.stringify(data[0]), { method: 'POST', contentType: 'application/json' }); diff --git a/modules/impactifyBidAdapter.md b/modules/impactifyBidAdapter.md index 3de9a8cfb84..de3373395dc 100644 --- a/modules/impactifyBidAdapter.md +++ b/modules/impactifyBidAdapter.md @@ -10,14 +10,22 @@ Maintainer: thomas.destefano@impactify.io Module that connects to the Impactify solution. The impactify bidder need 3 parameters: - - appId : This is your unique publisher identifier - - format : This is the ad format needed, can be : screen or display - - style : This is the ad style needed, can be : inline, impact or static +- appId : This is your unique publisher identifier +- format : This is the ad format needed, can be : screen or display (Only for video media type) +- style : This is the ad style needed, can be : inline, impact or static (Only for video media type) + +Note : Impactify adapter need storage access to work properly (Do not forget to set storageAllowed to true). # Test Parameters ``` - var adUnits = [{ - code: 'your-slot-div-id', // This is your slot div id + pbjs.bidderSettings = { + impactify: { + storageAllowed: true // Mandatory + } + }; + + var adUnitsVideo = [{ + code: 'your-slot-div-id-video', // This is your slot div id mediaTypes: { video: { context: 'outstream' @@ -32,4 +40,24 @@ The impactify bidder need 3 parameters: } }] }]; + + var adUnitsBanner = [{ + code: 'your-slot-div-id-banner', // This is your slot div id + mediaTypes: { + banner: { + sizes: [ + [728, 90] + ] + } + }, + bids: [{ + bidder: 'impactify', + params: { + appId: 'example.com', + format: 'display', + size: '728x90', + style: 'static' + } + }] + }]; ``` diff --git a/modules/improvedigitalBidAdapter.js b/modules/improvedigitalBidAdapter.js index b56cc56a186..3a258dfa327 100644 --- a/modules/improvedigitalBidAdapter.js +++ b/modules/improvedigitalBidAdapter.js @@ -7,6 +7,14 @@ import {hasPurpose1Consent} from '../src/utils/gpdr.js'; import {ortbConverter} from '../libraries/ortbConverter/converter.js'; import {loadExternalScript} from '../src/adloader.js'; +/** + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid + * @typedef {import('../src/adapters/bidderFactory.js').ServerResponse} ServerResponse + * @typedef {import('../src/adapters/bidderFactory.js').SyncOptions} SyncOptions + * @typedef {import('../src/adapters/bidderFactory.js').UserSync} UserSync + */ + const BIDDER_CODE = 'improvedigital'; const CREATIVE_TTL = 300; @@ -182,6 +190,10 @@ export const CONVERTER = ortbConverter({ })(); const bidResponse = buildBidResponse(bid, context); const idExt = deepAccess(bid, `ext.${BIDDER_CODE}`, {}); + // Programmatic guaranteed flag + if (idExt.pg === 1) { + bidResponse.adserverTargeting = { hb_deal_type_improve: 'pg' }; + } Object.assign(bidResponse, { dealId: (typeof idExt.buying_type === 'string' && idExt.buying_type !== 'rtb') ? idExt.line_item_id : undefined, netRevenue: idExt.is_net || false, diff --git a/modules/imuIdSystem.js b/modules/imuIdSystem.js index 898f32b27b0..1242ca183ea 100644 --- a/modules/imuIdSystem.js +++ b/modules/imuIdSystem.js @@ -11,6 +11,11 @@ import { submodule } from '../src/hook.js'; import {getStorageManager} from '../src/storageManager.js'; import {MODULE_TYPE_UID} from '../src/activities/modules.js'; +/** + * @typedef {import('../modules/userId/index.js').Submodule} Submodule + * @typedef {import('../modules/userId/index.js').SubmoduleConfig} SubmoduleConfig + */ + const MODULE_NAME = 'imuid'; export const storage = getStorageManager({moduleType: MODULE_TYPE_UID, moduleName: MODULE_NAME}); @@ -160,6 +165,16 @@ export const imuIdSubmodule = { imppid: localData.ppid } }; + }, + eids: { + 'imppid': { + source: 'ppid.intimatemerger.com', + atype: 1 + }, + 'imuid': { + source: 'intimatemerger.com', + atype: 1 + }, } }; diff --git a/modules/incrxBidAdapter.js b/modules/incrxBidAdapter.js index d054309ee40..9b939aff11b 100644 --- a/modules/incrxBidAdapter.js +++ b/modules/incrxBidAdapter.js @@ -2,6 +2,12 @@ import { parseSizesInput, isEmpty } from '../src/utils.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; import { BANNER } from '../src/mediaTypes.js' +/** + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid + * @typedef {import('../src/adapters/bidderFactory.js').ServerResponse} ServerResponse + */ + const BIDDER_CODE = 'incrementx'; const ENDPOINT_URL = 'https://hb.incrementxserv.com/vzhbidder/bid'; const DEFAULT_CURRENCY = 'USD'; @@ -12,22 +18,22 @@ export const spec = { 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. - */ + * 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. - * - * @param validBidRequests - * @param bidderRequest - * @return Array Info describing the request to the server. - */ + * Make a server request from the list of BidRequests. + * + * @param validBidRequests + * @param bidderRequest + * @return Array Info describing the request to the server. + */ buildRequests: function (validBidRequests, bidderRequest) { return validBidRequests.map(bidRequest => { const sizes = parseSizesInput(bidRequest.params.size || bidRequest.sizes); @@ -53,11 +59,11 @@ export const spec = { }, /** - * 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. - */ + * 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) { const response = serverResponse.body; const bids = []; diff --git a/modules/insticatorBidAdapter.js b/modules/insticatorBidAdapter.js index c770ac69dbe..3ea95ef8443 100644 --- a/modules/insticatorBidAdapter.js +++ b/modules/insticatorBidAdapter.js @@ -1,7 +1,7 @@ import {config} from '../src/config.js'; import {BANNER, VIDEO} from '../src/mediaTypes.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; -import {deepAccess, generateUUID, logError, isArray} from '../src/utils.js'; +import {deepAccess, generateUUID, logError, isArray, isInteger, isArrayOfNums} from '../src/utils.js'; import {getStorageManager} from '../src/storageManager.js'; import {find} from '../src/polyfill.js'; @@ -12,6 +12,35 @@ const USER_ID_COOKIE_EXP = 2592000000; // 30 days const BID_TTL = 300; // 5 minutes const GVLID = 910; +const isSubarray = (arr, target) => { + if (!isArrayOfNums(arr) || arr.length === 0) { + return false; + } + const targetSet = new Set(target); + return arr.every(el => targetSet.has(el)); +}; + +export const OPTIONAL_VIDEO_PARAMS = { + 'minduration': (value) => isInteger(value), + 'maxduration': (value) => isInteger(value), + 'protocols': (value) => isSubarray(value, [2, 3, 5, 6, 7, 8]), // protocols values supported by Inticator, according to the OpenRTB spec + 'startdelay': (value) => isInteger(value), + 'linearity': (value) => isInteger(value) && [1].includes(value), + 'skip': (value) => isInteger(value) && [1, 0].includes(value), + 'skipmin': (value) => isInteger(value), + 'skipafter': (value) => isInteger(value), + 'sequence': (value) => isInteger(value), + 'battr': (value) => isSubarray(value, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17]), + 'maxextended': (value) => isInteger(value), + 'minbitrate': (value) => isInteger(value), + 'maxbitrate': (value) => isInteger(value), + 'playbackmethod': (value) => isSubarray(value, [1, 2, 3, 4]), + 'playbackend': (value) => isInteger(value) && [1, 2, 3].includes(value), + 'delivery': (value) => isSubarray(value, [1, 2, 3]), + 'pos': (value) => isInteger(value) && [0, 1, 2, 3, 4, 5, 6, 7].includes(value), + 'api': (value) => isSubarray(value, [1, 2, 3, 4, 5, 6, 7]), +}; + export const storage = getStorageManager({bidderCode: BIDDER_CODE}); config.setDefaults({ @@ -68,17 +97,50 @@ function buildBanner(bidRequest) { } function buildVideo(bidRequest) { - const w = deepAccess(bidRequest, 'mediaTypes.video.w'); - const h = deepAccess(bidRequest, 'mediaTypes.video.h'); + let w = deepAccess(bidRequest, 'mediaTypes.video.w'); + let h = deepAccess(bidRequest, 'mediaTypes.video.h'); const mimes = deepAccess(bidRequest, 'mediaTypes.video.mimes'); const placement = deepAccess(bidRequest, 'mediaTypes.video.placement') || 3; + const plcmt = deepAccess(bidRequest, 'mediaTypes.video.plcmt') || undefined; + const playerSize = deepAccess(bidRequest, 'mediaTypes.video.playerSize'); + + if (!w && playerSize) { + if (Array.isArray(playerSize[0])) { + w = parseInt(playerSize[0][0], 10); + } else if (typeof playerSize[0] === 'number' && !isNaN(playerSize[0])) { + w = parseInt(playerSize[0], 10); + } + } + if (!h && playerSize) { + if (Array.isArray(playerSize[0])) { + h = parseInt(playerSize[0][1], 10); + } else if (typeof playerSize[1] === 'number' && !isNaN(playerSize[1])) { + h = parseInt(playerSize[1], 10); + } + } - return { + const bidRequestVideo = deepAccess(bidRequest, 'mediaTypes.video'); + const videoBidderParams = deepAccess(bidRequest, 'params.video', {}); + let optionalParams = {}; + for (const param in OPTIONAL_VIDEO_PARAMS) { + if (bidRequestVideo[param]) { + optionalParams[param] = bidRequestVideo[param]; + } + } + + let videoObj = { placement, mimes, w, h, + ...optionalParams, + ...videoBidderParams // bidder specific overrides for video + } + + if (plcmt) { + videoObj['plcmt'] = plcmt; } + return videoObj } function buildImpression(bidRequest) { @@ -106,8 +168,10 @@ function buildImpression(bidRequest) { return imp; } -function buildDevice() { - const deviceConfig = config.getConfig('device'); +function buildDevice(bidRequest) { + const ortb2Data = bidRequest?.ortb2 || {}; + const deviceConfig = ortb2Data?.device || {} + const device = { w: window.innerWidth, h: window.innerHeight, @@ -184,7 +248,7 @@ function buildRequest(validBidRequests, bidderRequest) { page: bidderRequest.refererInfo.page, ref: bidderRequest.refererInfo.ref, }, - device: buildDevice(), + device: buildDevice(bidderRequest), regs: buildRegs(bidderRequest), user: buildUser(validBidRequests[0]), imp: validBidRequests.map((bidRequest) => buildImpression(bidRequest)), @@ -233,7 +297,11 @@ function buildBid(bid, bidderRequest) { meta.advertiserDomains = bid.adomain } - return { + let mediaType = 'banner'; + if (bid.adm && bid.adm.includes(' 0 ? {meta} : {}) }; + + if (mediaType === 'video') { + bidResponse.vastXml = bid.adm; + } + + // Inticator bid adaptor only returns `vastXml` for video bids. No VastUrl or videoCache. + if (!bidResponse.vastUrl && bidResponse.vastXml) { + bidResponse.vastUrl = 'data:text/xml;charset=utf-8;base64,' + window.btoa(bidResponse.vastXml.replace(/\\"/g, '"')); + } + + return bidResponse; } function buildBidSet(seatbid, bidderRequest) { @@ -307,15 +386,38 @@ function validateBanner(bid) { } function validateVideo(bid) { - const video = deepAccess(bid, 'mediaTypes.video'); - - if (video === undefined) { - return true; + const videoParams = deepAccess(bid, 'mediaTypes.video', {}); + const videoBidderParams = deepAccess(bid, 'params.video', {}); + let video = { + ...videoParams, + ...videoBidderParams // bidder specific overrides for video + } + // Check if the merged video object is empty + if (Object.keys(video).length === 0 || video === undefined) { + logError('insticator: video object is empty'); + return false; // or handle the case where video parameters are undefined or empty } + let w = deepAccess(bid, 'mediaTypes.video.w'); + let h = deepAccess(bid, 'mediaTypes.video.h'); + const playerSize = deepAccess(bid, 'mediaTypes.video.playerSize'); + if (!w && playerSize) { + if (Array.isArray(playerSize[0])) { + w = parseInt(playerSize[0][0], 10); + } else if (typeof playerSize[0] === 'number' && !isNaN(playerSize[0])) { + w = parseInt(playerSize[0], 10); + } + } + if (!h && playerSize) { + if (Array.isArray(playerSize[0])) { + h = parseInt(playerSize[0][1], 10); + } else if (typeof playerSize[1] === 'number' && !isNaN(playerSize[1])) { + h = parseInt(playerSize[1], 10); + } + } const videoSize = [ - deepAccess(bid, 'mediaTypes.video.w'), - deepAccess(bid, 'mediaTypes.video.h'), + w, + h, ]; if ( @@ -339,6 +441,27 @@ function validateVideo(bid) { return false; } + const plcmt = deepAccess(bid, 'mediaTypes.video.plcmt'); + + if (typeof plcmt !== 'undefined' && typeof plcmt !== 'number') { + logError('insticator: video plcmt is not a number'); + return false; + } + + for (const param in OPTIONAL_VIDEO_PARAMS) { + if (video[param]) { + if (!OPTIONAL_VIDEO_PARAMS[param](video[param])) { + logError(`insticator: video ${param} is invalid or not supported by insticator`); + return false + } + } + } + + if (video.minduration && video.maxduration && video.minduration > video.maxduration) { + logError('insticator: video minduration is greater than maxduration'); + return false; + } + return true; } @@ -380,7 +503,6 @@ export const spec = { interpretResponse: function (serverResponse, request) { const bidderRequest = request.bidderRequest; const body = serverResponse.body; - if (!body || body.id !== bidderRequest.bidderRequestId) { logError('insticator: response id does not match bidderRequestId'); return []; diff --git a/modules/instreamTracking.js b/modules/instreamTracking.js index ff8305c7fed..ece556d0fd2 100644 --- a/modules/instreamTracking.js +++ b/modules/instreamTracking.js @@ -5,6 +5,12 @@ import { INSTREAM } from '../src/video.js'; import * as events from '../src/events.js'; import CONSTANTS from '../src/constants.json' +/** + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid + * @typedef {import('../src/adapters/bidderFactory.js').AdUnit} AdUnit + */ + const {CACHE_ID, UUID} = CONSTANTS.TARGETING_KEYS; const {BID_WON, AUCTION_END} = CONSTANTS.EVENTS; const {RENDERED} = CONSTANTS.BID_STATUS; diff --git a/modules/integr8BidAdapter.js b/modules/integr8BidAdapter.js index a85e9b0a55c..949483ea7bf 100644 --- a/modules/integr8BidAdapter.js +++ b/modules/integr8BidAdapter.js @@ -3,13 +3,20 @@ import { registerBidder } from '../src/adapters/bidderFactory.js'; import { getStorageManager } from '../src/storageManager.js'; import { BANNER, VIDEO } from '../src/mediaTypes.js'; +/** + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid + * @typedef {import('../src/adapters/bidderFactory.js').ServerResponse} ServerResponse + * @typedef {import('../src/adapters/bidderFactory.js').validBidRequests} validBidRequests + */ + const BIDDER_CODE = 'integr8'; -const ENDPOINT_URL = 'https://integr8.central.gjirafa.tech/bid'; +const DEFAULT_ENDPOINT_URL = 'https://central.sea.integr8.digital/bid'; const DIMENSION_SEPARATOR = 'x'; const SIZE_SEPARATOR = ';'; -const BISKO_ID = 'biskoId'; +const BISKO_ID = 'integr8Id'; const STORAGE_ID = 'bisko-sid'; -const SEGMENTS = 'biskoSegments'; +const SEGMENTS = 'integr8Segments'; const storage = getStorageManager({bidderCode: BIDDER_CODE}); export const spec = { @@ -31,6 +38,7 @@ export const spec = { * @return ServerRequest Info describing the request to the server. */ buildRequests: function (validBidRequests, bidderRequest) { + let deliveryUrl = ''; const storageId = storage.localStorageIsEnabled() ? storage.getDataFromLocalStorage(STORAGE_ID) || '' : ''; const biskoId = storage.localStorageIsEnabled() ? storage.getDataFromLocalStorage(BISKO_ID) || '' : ''; const segments = storage.localStorageIsEnabled() ? JSON.parse(storage.getDataFromLocalStorage(SEGMENTS)) || [] : []; @@ -55,6 +63,9 @@ export const spec = { if (!pageViewGuid) { pageViewGuid = bidRequest.params.pageViewGuid || ''; } if (!contents.length && bidRequest.params.contents && bidRequest.params.contents.length) { contents = bidRequest.params.contents; } if (!Object.keys(data).length && bidRequest.params.data && Object.keys(bidRequest.params.data).length) { data = bidRequest.params.data; } + if (!deliveryUrl && bidRequest.params && typeof bidRequest.params.deliveryUrl === 'string') { + deliveryUrl = bidRequest.params.deliveryUrl; + } return { sizes: generateSizeParam(bidRequest.sizes), @@ -67,6 +78,10 @@ export const spec = { }; }); + if (!deliveryUrl) { + deliveryUrl = DEFAULT_ENDPOINT_URL; + } + let body = { propertyId: propertyId, pageViewGuid: pageViewGuid, @@ -82,7 +97,7 @@ export const spec = { return [{ method: 'POST', - url: ENDPOINT_URL, + url: deliveryUrl, data: body }]; }, @@ -120,11 +135,11 @@ export const spec = { }; /** -* Generate size param for bid request using sizes array -* -* @param {Array} sizes Possible sizes for the ad unit. -* @return {string} Processed sizes param to be used for the bid request. -*/ + * Generate size param for bid request using sizes array + * + * @param {Array} sizes Possible sizes for the ad unit. + * @return {string} Processed sizes param to be used for the bid request. + */ function generateSizeParam(sizes) { return sizes.map(size => size.join(DIMENSION_SEPARATOR)).join(SIZE_SEPARATOR); } diff --git a/modules/integr8BidAdapter.md b/modules/integr8BidAdapter.md index eadab7acdb3..da52a2164c6 100644 --- a/modules/integr8BidAdapter.md +++ b/modules/integr8BidAdapter.md @@ -3,7 +3,7 @@ Module Name: Integr8 Bidder Adapter Module Type: Bidder Adapter -Maintainer: arditb@gjirafa.com +Maintainer: myhedin@gjirafa.com # Description Integr8 Bidder Adapter for Prebid.js. @@ -23,8 +23,9 @@ var adUnits = [ bids: [{ bidder: 'integr8', params: { - propertyId: '105109', //Required - placementId: '846835', //Required + propertyId: '105135', //Required + placementId: '846837', //Required, + deliveryUrl: 'https://central.sea.integr8.digital/bid', //Optional data: { //Optional catalogs: [{ catalogId: "699229", @@ -48,8 +49,9 @@ var adUnits = [ bids: [{ bidder: 'integr8', params: { - propertyId: '105109', //Required - placementId: '846830', //Required + propertyId: '105135', //Required + placementId: '846835', //Required, + deliveryUrl: 'https://central.sea.integr8.digital/bid', //Optional data: { //Optional catalogs: [{ catalogId: "699229", diff --git a/modules/intentIqIdSystem.js b/modules/intentIqIdSystem.js index 4a6977973f3..109ea5c39c6 100644 --- a/modules/intentIqIdSystem.js +++ b/modules/intentIqIdSystem.js @@ -11,6 +11,12 @@ import { submodule } from '../src/hook.js' import { getStorageManager } from '../src/storageManager.js'; import { MODULE_TYPE_UID } from '../src/activities/modules.js'; +/** + * @typedef {import('../modules/userId/index.js').Submodule} Submodule + * @typedef {import('../modules/userId/index.js').SubmoduleConfig} SubmoduleConfig + * @typedef {import('../modules/userId/index.js').IdResponse} IdResponse + */ + const PCID_EXPIRY = 365; const MODULE_NAME = 'intentIqId'; @@ -59,7 +65,7 @@ export function readData(key) { * @param key * @param {string} value IntentIQ ID value to sintentIqIdSystem_spec.jstore */ -function storeData(key, value) { +function storeData(key, value, cookieStorageEnabled = false) { try { logInfo(MODULE_NAME + ': storing data: key=' + key + ' value=' + value); @@ -68,7 +74,7 @@ function storeData(key, value) { storage.setDataInLocalStorage(key, value); } const expiresStr = (new Date(Date.now() + (PCID_EXPIRY * (60 * 60 * 24 * 1000)))).toUTCString(); - if (storage.cookiesAreEnabled()) { + if (storage.cookiesAreEnabled() && cookieStorageEnabled) { storage.setCookie(key, value, expiresStr, 'LAX'); } } @@ -119,6 +125,7 @@ export const intentIqIdSubmodule = { logError('User ID - intentIqId submodule requires a valid partner to be defined'); return; } + const cookieStorageEnabled = typeof configParams.enableCookieStorage === 'boolean' ? configParams.enableCookieStorage : false if (!FIRST_PARTY_DATA_KEY.includes(configParams.partner)) { FIRST_PARTY_DATA_KEY += '_' + configParams.partner; } let rrttStrtTime = 0; @@ -127,7 +134,7 @@ export const intentIqIdSubmodule = { if (!firstPartyData || !firstPartyData.pcid || firstPartyData.pcidDate) { const firstPartyId = generateGUID(); firstPartyData = { 'pcid': firstPartyId, 'pcidDate': Date.now() }; - storeData(FIRST_PARTY_KEY, JSON.stringify(firstPartyData)); + storeData(FIRST_PARTY_KEY, JSON.stringify(firstPartyData), cookieStorageEnabled); } let partnerData = tryParse(readData(FIRST_PARTY_DATA_KEY)); @@ -172,8 +179,8 @@ export const intentIqIdSubmodule = { } if (shouldUpdateLs === true) { partnerData.date = Date.now(); - storeData(FIRST_PARTY_KEY, JSON.stringify(firstPartyData)); - storeData(FIRST_PARTY_DATA_KEY, JSON.stringify(partnerData)); + storeData(FIRST_PARTY_KEY, JSON.stringify(firstPartyData), cookieStorageEnabled); + storeData(FIRST_PARTY_DATA_KEY, JSON.stringify(partnerData), cookieStorageEnabled); } callback(respJson.data); } else { @@ -192,6 +199,12 @@ export const intentIqIdSubmodule = { } }; return { callback: resp }; + }, + eids: { + 'intentIqId': { + source: 'intentiq.com', + atype: 1 + }, } }; diff --git a/modules/invamiaBidAdapter.js b/modules/invamiaBidAdapter.js index 2d36fb77e16..96af163ca4f 100644 --- a/modules/invamiaBidAdapter.js +++ b/modules/invamiaBidAdapter.js @@ -1,6 +1,11 @@ import {registerBidder} from '../src/adapters/bidderFactory.js'; import {BANNER} from '../src/mediaTypes.js'; +/** + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + * @typedef {import('../src/adapters/bidderFactory.js').ServerResponse} ServerResponse + */ + const BIDDER_CODE = 'invamia'; const ENDPOINT_URL = 'https://ad.invamia.com/delivery/impress'; diff --git a/modules/invibesBidAdapter.js b/modules/invibesBidAdapter.js index b2444043c22..2c37c0edad9 100644 --- a/modules/invibesBidAdapter.js +++ b/modules/invibesBidAdapter.js @@ -1,7 +1,12 @@ -import { logInfo } from '../src/utils.js'; +import {logInfo} from '../src/utils.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import {getStorageManager} from '../src/storageManager.js'; +/** + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid + */ + const CONSTANTS = { BIDDER_CODE: 'invibes', BID_ENDPOINT: '.videostep.com/Bid/VideoAdContent', @@ -9,7 +14,7 @@ const CONSTANTS = { SYNC_ENDPOINT: 'https://k.r66net.com/GetUserSync', TIME_TO_LIVE: 300, DEFAULT_CURRENCY: 'EUR', - PREBID_VERSION: 9, + PREBID_VERSION: 11, METHOD: 'GET', INVIBES_VENDOR_ID: 436, USERID_PROVIDERS: ['pubcid', 'pubProvidedId', 'uid2', 'zeotapIdPlus', 'id5id'], @@ -49,14 +54,36 @@ registerBidder(spec); // some state info is required: cookie info, unique user visit id const topWin = getTopMostWindow(); let invibes = topWin.invibes = topWin.invibes || {}; -invibes.purposes = invibes.purposes || [false, false, false, false, false, false, false, false, false, false]; -invibes.legitimateInterests = invibes.legitimateInterests || [false, false, false, false, false, false, false, false, false, false]; +invibes.purposes = invibes.purposes || [false, false, false, false, false, false, false, false, false, false, false]; +invibes.legitimateInterests = invibes.legitimateInterests || [false, false, false, false, false, false, false, false, false, false, false]; invibes.placementBids = invibes.placementBids || []; invibes.pushedCids = invibes.pushedCids || {}; let preventPageViewEvent = false; +let isInfiniteScrollPage = false; +let isPlacementRefresh = false; let _customUserSync; let _disableUserSyncs; +function updateInfiniteScrollFlag() { + const { scrollHeight } = document.documentElement; + + if (invibes.originalURL === undefined) { + invibes.originalURL = window.location.href; + return; + } + + if (invibes.originalScrollHeight === undefined) { + invibes.originalScrollHeight = scrollHeight; + return; + } + + const currentURL = window.location.href; + + if (scrollHeight > invibes.originalScrollHeight && invibes.originalURL !== currentURL) { + isInfiniteScrollPage = true; + } +} + function isBidRequestValid(bid) { if (typeof bid.params !== 'object') { return false; @@ -87,10 +114,24 @@ function buildRequest(bidRequests, bidderRequest) { const _placementIds = []; const _adUnitCodes = []; let _customEndpoint, _userId, _domainId; - let _ivAuctionStart = bidderRequest.auctionStart || Date.now(); + let _ivAuctionStart = Date.now(); + window.invibes = window.invibes || {}; + window.invibes.placementIds = window.invibes.placementIds || []; + + if (isInfiniteScrollPage == false) { + updateInfiniteScrollFlag(); + } bidRequests.forEach(function (bidRequest) { bidRequest.startTime = new Date().getTime(); + + if (window.invibes.placementIds.includes(bidRequest.params.placementId)) { + isPlacementRefresh = true; + } + + window.invibes.placementIds.push(bidRequest.params.placementId); + + _placementIds.push(bidRequest.params.placementId); _placementIds.push(bidRequest.params.placementId); _adUnitCodes.push(bidRequest.adUnitCode); _domainId = _domainId || bidRequest.params.domainId; @@ -116,7 +157,7 @@ function buildRequest(bidRequests, bidderRequest) { bidParamsJson.userId = userIdModel; } let data = { - location: getDocumentLocation(topWin), + location: getDocumentLocation(bidderRequest), videoAdHtmlId: generateRandomId(), showFallback: currentQueryStringParams['advs'] === '0', ivbsCampIdsLocal: readFromLocalStorage('IvbsCampIdsLocal'), @@ -138,6 +179,8 @@ function buildRequest(bidRequests, bidderRequest) { tc: invibes.gdpr_consent, isLocalStorageEnabled: storage.hasLocalStorage(), preventPageViewEvent: preventPageViewEvent, + isPlacementRefresh: isPlacementRefresh, + isInfiniteScrollPage: isInfiniteScrollPage, }; let lid = readFromLocalStorage('ivbsdid'); @@ -368,11 +411,13 @@ function addMeta(bidModelMeta) { } function generateRandomId() { - return (Math.round(Math.random() * 1e12)).toString(36).substring(0, 10); + return '10000000100040008000100000000000'.replace(/[018]/g, c => + (c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16) + ); } -function getDocumentLocation(topWin) { - return topWin.location.href.substring(0, 300).split(/[?#]/)[0]; +function getDocumentLocation(bidderRequest) { + return bidderRequest.refererInfo.page.substring(0, 300); } function getUserIds(bidUserId) { @@ -568,7 +613,7 @@ function readGdprConsent(gdprConsent) { } let legitimateInterests = getLegitimateInterests(gdprConsent.vendorData); - tryCopyValueToArray(legitimateInterests, invibes.legitimateInterests, 10); + tryCopyValueToArray(legitimateInterests, invibes.legitimateInterests, purposesLength); let invibesVendorId = CONSTANTS.INVIBES_VENDOR_ID.toString(10); let vendorConsents = getVendorConsents(gdprConsent.vendorData); @@ -621,6 +666,10 @@ function tryCopyValueToArray(value, target, length) { function getPurposeConsentsCounter(vendorData) { if (vendorData.purpose && vendorData.purpose.consents) { + if (vendorData.tcfPolicyVersion >= 4) { + return 11; + } + return 10; } diff --git a/modules/ipromBidAdapter.js b/modules/ipromBidAdapter.js index eaf20ad3ad3..1188af471a7 100644 --- a/modules/ipromBidAdapter.js +++ b/modules/ipromBidAdapter.js @@ -3,13 +3,15 @@ import { registerBidder } from '../src/adapters/bidderFactory.js'; const BIDDER_CODE = 'iprom'; const ENDPOINT_URL = 'https://core.iprom.net/programmatic'; -const VERSION = 'v1.0.2'; +const VERSION = 'v1.0.3'; const DEFAULT_CURRENCY = 'EUR'; const DEFAULT_NETREVENUE = true; const DEFAULT_TTL = 360; +const IAB_GVL_ID = 811; export const spec = { code: BIDDER_CODE, + gvlid: IAB_GVL_ID, isBidRequestValid: function ({ bidder, params = {} } = {}) { // id parameter checks if (!params.id) { diff --git a/modules/iqmBidAdapter.js b/modules/iqmBidAdapter.js index c3808afd225..c94a88748a7 100644 --- a/modules/iqmBidAdapter.js +++ b/modules/iqmBidAdapter.js @@ -3,6 +3,10 @@ import {registerBidder} from '../src/adapters/bidderFactory.js'; import {BANNER, VIDEO} from '../src/mediaTypes.js'; import {INSTREAM} from '../src/video.js'; +/** + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + */ + const BIDDER_CODE = 'iqm'; const VERSION = 'v.1.0.0'; const VIDEO_ORTB_PARAMS = [ diff --git a/modules/iqxBidAdapter.js b/modules/iqxBidAdapter.js new file mode 100644 index 00000000000..1bef158c4a2 --- /dev/null +++ b/modules/iqxBidAdapter.js @@ -0,0 +1,207 @@ +import {config} from '../src/config.js'; +import {BANNER, VIDEO} from '../src/mediaTypes.js'; +import {registerBidder} from '../src/adapters/bidderFactory.js'; +import {parseSizesInput, isFn, deepAccess, getBidIdParameter, logError, isArray} from '../src/utils.js'; +import {getAdUnitSizes} from '../libraries/sizeUtils/sizeUtils.js'; + +const CUR = 'USD'; +const BIDDER_CODE = 'iqx'; +const ENDPOINT = 'https://pbjs.iqzonertb.live'; + +/** + * Determines whether or not the given bid request is valid. + * + * @param {BidRequest} bid The bid params to validate. + * @return boolean True if this is a valid bid, and false otherwise. + */ +function isBidRequestValid(req) { + if (req && typeof req.params !== 'object') { + logError('Params is not defined or is incorrect in the bidder settings'); + return false; + } + + if (!getBidIdParameter('env', req.params) || !getBidIdParameter('pid', req.params)) { + logError('Env or pid is not present in bidder params'); + return false; + } + + if (deepAccess(req, 'mediaTypes.video') && !isArray(deepAccess(req, 'mediaTypes.video.playerSize'))) { + logError('mediaTypes.video.playerSize is required for video'); + return false; + } + + return true; +} + +/** + * Make a server request from the list of BidRequests. + * + * @param {validBidRequest?pbjs_debug=trues[]} - an array of bids + * @return ServerRequest Info describing the request to the server. + */ +function buildRequests(validBidRequests, bidderRequest) { + const {refererInfo = {}, gdprConsent = {}, uspConsent} = bidderRequest; + const requests = validBidRequests.map(req => { + const request = {}; + request.bidId = req.bidId; + request.banner = deepAccess(req, 'mediaTypes.banner'); + request.auctionId = req.ortb2?.source?.tid; + request.transactionId = req.ortb2Imp?.ext?.tid; + request.sizes = parseSizesInput(getAdUnitSizes(req)); + request.schain = req.schain; + request.location = { + page: refererInfo.page, + location: refererInfo.location, + domain: refererInfo.domain, + whost: window.location.host, + ref: refererInfo.ref, + isAmp: refererInfo.isAmp + }; + request.device = { + ua: navigator.userAgent, + lang: navigator.language + }; + request.env = { + env: req.params.env, + pid: req.params.pid + }; + request.ortb2 = req.ortb2; + request.ortb2Imp = req.ortb2Imp; + request.tz = new Date().getTimezoneOffset(); + request.ext = req.params.ext; + request.bc = req.bidRequestsCount; + request.floor = getBidFloor(req); + + if (req.userIdAsEids && req.userIdAsEids.length !== 0) { + request.userEids = req.userIdAsEids; + } else { + request.userEids = []; + } + if (gdprConsent.gdprApplies) { + request.gdprApplies = Number(gdprConsent.gdprApplies); + request.consentString = gdprConsent.consentString; + } else { + request.gdprApplies = 0; + request.consentString = ''; + } + if (uspConsent) { + request.usPrivacy = uspConsent; + } else { + request.usPrivacy = ''; + } + if (config.getConfig('coppa')) { + request.coppa = 1; + } else { + request.coppa = 0; + } + + const video = deepAccess(req, 'mediaTypes.video'); + if (video) { + request.sizes = parseSizesInput(deepAccess(req, 'mediaTypes.video.playerSize')); + request.video = video; + } + + return request; + }); + + return { + method: 'POST', + url: ENDPOINT + '/bid', + data: JSON.stringify(requests), + withCredentials: true, + bidderRequest, + options: { + contentType: 'application/json', + } + }; +} + +/** + * Unpack the response from the server into a list of bids. + * + * @param {ServerResponse} serverResponse A successful response from the server. + * @return {Bid[]} An array of bids which were nested inside the server. + */ +function interpretResponse(serverResponse, {bidderRequest}) { + const response = []; + if (!isArray(deepAccess(serverResponse, 'body.data'))) { + return response; + } + + serverResponse.body.data.forEach(serverBid => { + const bid = { + requestId: bidderRequest.bidId, + dealId: bidderRequest.dealId || null, + ...serverBid + }; + response.push(bid); + }); + + return response; +} + +/** + * Register the user sync pixels which should be dropped after the auction. + * + * @param {SyncOptions} syncOptions Which user syncs are allowed? + * @param {ServerResponse[]} serverResponses List of server's responses. + * @return {UserSync[]} The user syncs which should be dropped. + */ +function getUserSyncs(syncOptions, serverResponses, gdprConsent = {}, uspConsent = '') { + const syncs = []; + const pixels = deepAccess(serverResponses, '0.body.data.0.ext.pixels'); + + if ((syncOptions.iframeEnabled || syncOptions.pixelEnabled) && isArray(pixels) && pixels.length !== 0) { + const gdprFlag = `&gdpr=${gdprConsent.gdprApplies ? 1 : 0}`; + const gdprString = `&gdpr_consent=${encodeURIComponent((gdprConsent.consentString || ''))}`; + const usPrivacy = `us_privacy=${encodeURIComponent(uspConsent)}`; + + pixels.forEach(pixel => { + const [type, url] = pixel; + const sync = {type, url: `${url}&${usPrivacy}${gdprFlag}${gdprString}`}; + if (type === 'iframe' && syncOptions.iframeEnabled) { + syncs.push(sync) + } else if (type === 'image' && syncOptions.pixelEnabled) { + syncs.push(sync) + } + }); + } + + return syncs; +} + +/** + * Get valid floor value from getFloor fuction. + * + * @param {Object} bid Current bid request. + * @return {null|Number} Returns floor value when bid.getFloor is function and returns valid floor object with USD currency, otherwise returns null. + */ +export function getBidFloor(bid) { + if (!isFn(bid.getFloor)) { + return null; + } + + let floor = bid.getFloor({ + currency: CUR, + mediaType: '*', + size: '*' + }); + + if (typeof floor === 'object' && !isNaN(floor.floor) && floor.currency === CUR) { + return floor.floor; + } + + return null; +} + +export const spec = { + code: BIDDER_CODE, + aliases: ['iqx'], + supportedMediaTypes: [BANNER, VIDEO], + isBidRequestValid, + buildRequests, + interpretResponse, + getUserSyncs +} + +registerBidder(spec); diff --git a/modules/iqxBidAdapter.md b/modules/iqxBidAdapter.md new file mode 100644 index 00000000000..c48864c4306 --- /dev/null +++ b/modules/iqxBidAdapter.md @@ -0,0 +1,54 @@ +# Overview + +``` +Module Name: IQX Bidder Adapter +Module Type: IQX Bidder Adapter +Maintainer: it@iqzone.com +``` + +# Description + +Module that connects to iqx.com demand sources + +# Test Parameters +``` +var adUnits = [ + { + code: 'test-banner', + mediaTypes: { + banner: { + sizes: [[300, 250]], + } + }, + bids: [ + { + bidder: 'iqx', + params: { + env: 'iqx', + pid: '40', + ext: {} + } + } + ] + }, + { + code: 'test-video', + sizes: [ [ 640, 480 ] ], + mediaTypes: { + video: { + playerSize: [640, 480], + context: 'instream', + skipppable: true + } + }, + bids: [{ + bidder: 'iqx', + params: { + env: 'iqx', + pid: '40', + ext: {} + } + }] + } +]; +``` diff --git a/modules/ivsBidAdapter.js b/modules/ivsBidAdapter.js index 47685fbbe46..3deebf9bff3 100644 --- a/modules/ivsBidAdapter.js +++ b/modules/ivsBidAdapter.js @@ -1,9 +1,16 @@ import { ortbConverter } from '../libraries/ortbConverter/converter.js'; -import { deepAccess, deepSetValue, getBidIdParameter, logError } from '../src/utils.js'; +import {deepAccess, deepSetValue, getBidIdParameter, logError} from '../src/utils.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; import { VIDEO } from '../src/mediaTypes.js'; import { INSTREAM } from '../src/video.js'; +/** + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid + * @typedef {import('../src/adapters/bidderFactory.js').ServerResponse} ServerResponse + * @typedef {import('../src/adapters/bidderFactory.js').validBidRequests} validBidRequests + */ + const BIDDER_CODE = 'ivs'; const ENDPOINT_URL = 'https://a.ivstracker.net/prod/openrtb/2.5'; diff --git a/modules/ixBidAdapter.js b/modules/ixBidAdapter.js index 000131d9e03..4cc6807b670 100644 --- a/modules/ixBidAdapter.js +++ b/modules/ixBidAdapter.js @@ -1,10 +1,8 @@ import { contains, - convertTypes, deepAccess, deepClone, deepSetValue, - getGptSlotInfoForAdUnitCode, inIframe, isArray, isEmpty, @@ -24,6 +22,8 @@ import { find } from '../src/polyfill.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; import { INSTREAM, OUTSTREAM } from '../src/video.js'; import { Renderer } from '../src/Renderer.js'; +import {getGptSlotInfoForAdUnitCode} from '../libraries/gptUtils/gptUtils.js'; +import {convertTypes} from '../libraries/transformParamsUtils/convertTypes.js'; const BIDDER_CODE = 'ix'; const ALIAS_BIDDER_CODE = 'roundel'; @@ -76,7 +76,12 @@ const SOURCE_RTI_MAPPING = { 'audigent.com': '', // Hadron ID from Audigent, hadronId 'pubcid.org': '', // SharedID, pubcid 'utiq.com': '', // Utiq - 'intimatemerger.com': '' + 'criteo.com': '', // Criteo + 'euid.eu': '', // EUID + 'intimatemerger.com': '', + '33across.com': '', + 'liveintent.indexexchange.com': '', + 'google.com': '' }; const PROVIDERS = [ 'britepoolid', @@ -87,7 +92,8 @@ const PROVIDERS = [ 'connectid', 'tapadId', 'quantcastId', - 'pubProvidedId' + 'pubProvidedId', + 'pairId' ]; const REQUIRED_VIDEO_PARAMS = ['mimes', 'minduration', 'maxduration']; // note: protocol/protocols is also reqd const VIDEO_PARAMS_ALLOW_LIST = [ @@ -104,7 +110,9 @@ let hasRegisteredHandler = false; export const storage = getStorageManager({ bidderCode: BIDDER_CODE }); export const FEATURE_TOGGLES = { // Update with list of CFTs to be requested from Exchange - REQUESTED_FEATURE_TOGGLES: [], + REQUESTED_FEATURE_TOGGLES: [ + 'pbjs_enable_multiformat' + ], featureToggles: {}, isFeatureEnabled: function (ft) { @@ -164,6 +172,7 @@ const MEDIA_TYPES = { function bidToBannerImp(bid) { const imp = bidToImp(bid, BANNER); imp.banner = {}; + imp.adunitCode = bid.adUnitCode; const impSize = deepAccess(bid, 'params.size'); if (impSize) { imp.banner.w = impSize[0]; @@ -177,13 +186,48 @@ function bidToBannerImp(bid) { return imp; } +/** + * Sets imp.displaymanager + * + * @param {object} imp + * @param {object} bid + */ +function setDisplayManager(imp, bid) { + if (deepAccess(bid, 'mediaTypes.video.context') === OUTSTREAM) { + let renderer = deepAccess(bid, 'mediaTypes.video.renderer'); + if (!renderer) { + renderer = deepAccess(bid, 'renderer'); + } + + if (deepAccess(bid, 'schain', false)) { + imp.displaymanager = 'pbjs_wrapper'; + } else if (renderer && typeof (renderer) === 'object') { + if (renderer.url !== undefined) { + let domain = ''; + try { + domain = new URL(renderer.url).hostname + } catch { + return; + } + if (domain.includes('js-sec.indexww')) { + imp.displaymanager = 'ix'; + } else { + imp.displaymanager = renderer.url; + } + } + } else { + imp.displaymanager = 'ix'; + } + } +} + /** * Transform valid bid request config object to video impression object that will be sent to ad server. * * @param {object} bid A valid bid request config object. * @return {object} A impression object that will be sent to ad server. */ -function bidToVideoImp(bid) { +export function bidToVideoImp(bid) { const imp = bidToImp(bid, VIDEO); const videoAdUnitRef = deepAccess(bid, 'mediaTypes.video'); const videoParamRef = deepAccess(bid, 'params.video'); @@ -194,10 +238,15 @@ function bidToVideoImp(bid) { imp.video = videoParamRef ? deepClone(bid.params.video) : {}; // populate imp level transactionId - imp.ext.tid = deepAccess(bid, 'ortb2Imp.ext.tid'); + let tid = deepAccess(bid, 'ortb2Imp.ext.tid'); + if (tid) { + deepSetValue(imp, 'ext.tid', tid); + } + + setDisplayManager(imp, bid); // AdUnit-Specific First Party Data - addAdUnitFPD(imp, bid) + addAdUnitFPD(imp, bid); // copy all video properties to imp object for (const adUnitProperty in videoAdUnitRef) { @@ -266,7 +315,7 @@ function verifyVideoPlcmt(imp) { * @param {object} bid A valid bid request config object. * @return {object} A impression object that will be sent to ad server. */ -function bidToNativeImp(bid) { +export function bidToNativeImp(bid) { const imp = bidToImp(bid, NATIVE); const request = bid.nativeOrtbRequest @@ -282,7 +331,10 @@ function bidToNativeImp(bid) { }; // populate imp level transactionId - imp.ext.tid = deepAccess(bid, 'ortb2Imp.ext.tid'); + let tid = deepAccess(bid, 'ortb2Imp.ext.tid'); + if (tid) { + deepSetValue(imp, 'ext.tid', tid); + } // AdUnit-Specific First Party Data addAdUnitFPD(imp, bid) @@ -302,27 +354,30 @@ function bidToImp(bid, mediaType) { imp.id = bid.bidId; - imp.ext = {}; - + if (isExchangeIdConfigured() && deepAccess(bid, `params.externalId`)) { + deepSetValue(imp, 'ext.externalID', bid.params.externalId); + } if (deepAccess(bid, `params.${mediaType}.siteId`) && !isNaN(Number(bid.params[mediaType].siteId))) { switch (mediaType) { case BANNER: - imp.ext.siteID = bid.params.banner.siteId.toString(); + deepSetValue(imp, 'ext.siteID', bid.params.banner.siteId.toString()); break; case VIDEO: - imp.ext.siteID = bid.params.video.siteId.toString(); + deepSetValue(imp, 'ext.siteID', bid.params.video.siteId.toString()); break; case NATIVE: - imp.ext.siteID = bid.params.native.siteId.toString(); + deepSetValue(imp, 'ext.siteID', bid.params.native.siteId.toString()); break; } } else { - imp.ext.siteID = bid.params.siteId.toString(); + if (bid.params.siteId) { + deepSetValue(imp, 'ext.siteID', bid.params.siteId.toString()); + } } // populate imp level sid if (bid.params.hasOwnProperty('id') && (typeof bid.params.id === 'string' || typeof bid.params.id === 'number')) { - imp.ext.sid = String(bid.params.id); + deepSetValue(imp, 'ext.sid', String(bid.params.id)); } return imp; @@ -364,14 +419,30 @@ function _applyFloor(bid, imp, mediaType) { } // Prioritize module floor over bidder.param floor + let setFloor = false; if (moduleFloor) { imp.bidfloor = moduleFloor.floor; imp.bidfloorcur = moduleFloor.currency; - imp.ext.fl = FLOOR_SOURCE.PBJS; + deepSetValue(imp, 'ext.fl', FLOOR_SOURCE.PBJS); + setFloor = true; } else if (adapterFloor) { imp.bidfloor = adapterFloor.floor; imp.bidfloorcur = adapterFloor.currency; - imp.ext.fl = FLOOR_SOURCE.IX; + deepSetValue(imp, 'ext.fl', FLOOR_SOURCE.IX); + setFloor = true; + } + + if (setFloor) { + if (mediaType == BANNER) { + deepSetValue(imp, 'banner.ext.bidfloor', imp.bidfloor); + deepSetValue(imp, 'banner.ext.fl', imp.ext.fl); + } else if (mediaType == VIDEO) { + deepSetValue(imp, 'video.ext.bidfloor', imp.bidfloor); + deepSetValue(imp, 'video.ext.fl', imp.ext.fl); + } else { + deepSetValue(imp, 'native.ext.bidfloor', imp.bidfloor); + deepSetValue(imp, 'native.ext.fl', imp.ext.fl); + } } } @@ -464,7 +535,7 @@ function isValidSize(size) { * Determines whether or not the given size object is an element of the size * array. * - * @param {array} sizeArray The size array. + * @param {Array} sizeArray The size array. * @param {object} size The size object. * @return {boolean} True if the size object is an element of the size array, and false * otherwise. @@ -512,7 +583,6 @@ function checkVideoParams(mediaTypeVideoRef, paramsVideoRef) { if (!(protocolMediaType || protocolsMediaType || protocolVideoRef || protocolsVideoRef)) { errorList.push('IX Bid Adapter: protocol/protcols is not included in either the adunit or params level'); } - return errorList; } @@ -520,7 +590,7 @@ function checkVideoParams(mediaTypeVideoRef, paramsVideoRef) { * Get One size from Size Array * [[250,350]] -> [250, 350] * [250, 350] -> [250, 350] - * @param {array} sizes array of sizes + * @param {Array} sizes array of sizes */ function getFirstSize(sizes = []) { if (isValidSize(sizes)) { @@ -537,7 +607,7 @@ function getFirstSize(sizes = []) { * * @param {number} bidFloor The bidFloor parameter inside bid request config. * @param {number} bidFloorCur The bidFloorCur parameter inside bid request config. - * @return {bool} True if this is a valid bidFloor parameters format, and false + * @return {boolean} True if this is a valid bidFloor parameters format, and false * otherwise. */ function isValidBidFloorParams(bidFloor, bidFloorCur) { @@ -560,7 +630,7 @@ function nativeMediaTypeValid(bid) { * Get bid request object with the associated id. * * @param {*} id Id of the impression. - * @param {array} impressions List of impressions sent in the request. + * @param {Array} impressions List of impressions sent in the request. * @return {object} The impression with the associated id. */ function getBidRequest(id, impressions, validBidRequests) { @@ -578,7 +648,7 @@ function getBidRequest(id, impressions, validBidRequests) { /** * From the userIdAsEids array, filter for the ones our adserver can use, and modify them * for our purposes, e.g. add rtiPartner - * @param {array} allEids userIdAsEids passed in by prebid + * @param {Array} allEids userIdAsEids passed in by prebid * @return {object} contains toSend (eids to send to the adserver) and seenSources (used to filter * identity info from IX Library) */ @@ -606,11 +676,11 @@ function getEidInfo(allEids) { /** * Builds a request object to be sent to the ad server based on bid requests. * - * @param {array} validBidRequests A list of valid bid request config objects. + * @param {Array} validBidRequests A list of valid bid request config objects. * @param {object} bidderRequest An object containing other info like gdprConsent. * @param {object} impressions An object containing a list of impression objects describing the bids for each transaction - * @param {array} version Endpoint version denoting banner, video or native. - * @return {array} List of objects describing the request to the server. + * @param {Array} version Endpoint version denoting banner, video or native. + * @return {Array} List of objects describing the request to the server. * */ function buildRequest(validBidRequests, bidderRequest, impressions, version) { @@ -638,7 +708,8 @@ function buildRequest(validBidRequests, bidderRequest, impressions, version) { r = addRequestedFeatureToggles(r, FEATURE_TOGGLES.REQUESTED_FEATURE_TOGGLES) // getting ixdiags for adunits of the video, outstream & multi format (MF) style - let ixdiag = buildIXDiag(validBidRequests); + const fledgeEnabled = deepAccess(bidderRequest, 'fledgeEnabled') + let ixdiag = buildIXDiag(validBidRequests, fledgeEnabled); for (var key in ixdiag) { r.ext.ixdiag[key] = ixdiag[key]; } @@ -648,8 +719,10 @@ function buildRequest(validBidRequests, bidderRequest, impressions, version) { r = applyRegulations(r, bidderRequest); let payload = {}; - siteID = validBidRequests[0].params.siteId; - payload.s = siteID; + if (validBidRequests[0].params.siteId) { + siteID = validBidRequests[0].params.siteId; + payload.s = siteID; + } const impKeys = Object.keys(impressions); let isFpdAdded = false; @@ -680,10 +753,24 @@ function buildRequest(validBidRequests, bidderRequest, impressions, version) { const isLastAdUnit = adUnitIndex === impKeys.length - 1; + r = addDeviceInfo(r); + r = deduplicateImpExtFields(r); + r = removeSiteIDs(r); + if (isLastAdUnit) { + let exchangeUrl = `${baseUrl}?`; + + if (siteID !== 0) { + exchangeUrl += `s=${siteID}`; + } + + if (isExchangeIdConfigured()) { + exchangeUrl += siteID !== 0 ? '&' : ''; + exchangeUrl += `p=${config.getConfig('exchangeId')}`; + } requests.push({ method: 'POST', - url: baseUrl + '?s=' + siteID, + url: exchangeUrl, data: deepClone(r), option: { contentType: 'text/plain', @@ -702,8 +789,8 @@ function buildRequest(validBidRequests, bidderRequest, impressions, version) { /** * 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 + * @param {Array} userEids userEids info retrieved from prebid + * @param {Array} eidInfo eidInfo info from prebid */ function addRTI(userEids, eidInfo) { let identityInfo = window.headertag.getIdentityInfo(); @@ -722,7 +809,7 @@ function addRTI(userEids, eidInfo) { /** * createRequest creates the base request object - * @param {array} validBidRequests A list of valid bid request config objects. + * @param {Array} validBidRequests A list of valid bid request config objects. * @return {object} Object describing the request to the server. */ function createRequest(validBidRequests) { @@ -762,9 +849,9 @@ function addRequestedFeatureToggles(r, requestedFeatureToggles) { * * @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. + * @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) { @@ -871,10 +958,10 @@ function applyRegulations(r, bidderRequest) { /** * addImpressions adds impressions to request object * - * @param {array} impressions List of impressions to be added to the request. - * @param {array} impKeys List of impression keys. + * @param {Array} impressions List of impressions to be added to the request. + * @param {Array} impKeys List of impression keys. * @param {object} r Reuqest object. - * @param {int} adUnitIndex Index of the current add unit + * @param {number} adUnitIndex Index of the current add unit * @return {object} Reqyest object with added impressions describing the request to the server. */ function addImpressions(impressions, impKeys, r, adUnitIndex) { @@ -889,63 +976,135 @@ function addImpressions(impressions, impKeys, r, adUnitIndex) { const gpid = impressions[impKeys[adUnitIndex]].gpid; const dfpAdUnitCode = impressions[impKeys[adUnitIndex]].dfp_ad_unit_code; const tid = impressions[impKeys[adUnitIndex]].tid; - const sid = impressions[impKeys[adUnitIndex]].sid - - if (impressionObjects.length && BANNER in impressionObjects[0]) { - const { id, banner: { topframe } } = impressionObjects[0]; - const _bannerImpression = { - id, - banner: { - topframe, - format: impressionObjects.map(({ banner: { w, h }, ext }) => ({ w, h, ext })) - }, - }; + const sid = impressions[impKeys[adUnitIndex]].sid; + const auctionEnvironment = impressions[impKeys[adUnitIndex]].ae; + const bannerImpressions = impressionObjects.filter(impression => BANNER in impression); + const otherImpressions = impressionObjects.filter(impression => !(BANNER in impression)); + + if (bannerImpressions.length > 0) { + const bannerImpsKeyed = bannerImpressions.reduce((acc, bannerImp) => { + if (!acc[bannerImp.adunitCode]) { + acc[bannerImp.adunitCode] = [] + } + acc[bannerImp.adunitCode].push(bannerImp); + return acc; + }, {}); + for (const impId in bannerImpsKeyed) { + const bannerImps = bannerImpsKeyed[impId]; + const { id, banner: { topframe } } = bannerImps[0]; + let externalID = deepAccess(bannerImps[0], 'ext.externalID'); + const _bannerImpression = { + id, + banner: { + topframe, + format: bannerImps.map(({ banner: { w, h }, ext }) => ({ w, h, ext })) + }, + }; - for (let i = 0; i < _bannerImpression.banner.format.length; i++) { - // We add sid 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; + for (let i = 0; i < _bannerImpression.banner.format.length; i++) { + // We add sid and externalID in imp.ext therefore, remove from banner.format[].ext + if (_bannerImpression.banner.format[i].ext != null) { + if (_bannerImpression.banner.format[i].ext.sid != null) { + delete _bannerImpression.banner.format[i].ext.sid; + } + if (_bannerImpression.banner.format[i].ext.externalID != null) { + delete _bannerImpression.banner.format[i].ext.externalID; + } + } + + // add floor per size + if ('bidfloor' in bannerImps[i]) { + _bannerImpression.banner.format[i].ext.bidfloor = bannerImps[i].bidfloor; + } + + if (JSON.stringify(_bannerImpression.banner.format[i].ext) === '{}') { + delete _bannerImpression.banner.format[i].ext; + } } - // add floor per size - if ('bidfloor' in impressionObjects[i]) { - _bannerImpression.banner.format[i].ext.bidfloor = impressionObjects[i].bidfloor; + const position = impressions[impKeys[adUnitIndex]].pos; + if (isInteger(position)) { + _bannerImpression.banner.pos = position; } - } - const position = impressions[impKeys[adUnitIndex]].pos; - if (isInteger(position)) { - _bannerImpression.banner.pos = position; - } + if (dfpAdUnitCode || gpid || tid || sid || auctionEnvironment || externalID) { + _bannerImpression.ext = {}; - if (dfpAdUnitCode || gpid || tid || sid) { - _bannerImpression.ext = {}; - _bannerImpression.ext.dfp_ad_unit_code = dfpAdUnitCode; - _bannerImpression.ext.gpid = gpid; - _bannerImpression.ext.tid = tid; - _bannerImpression.ext.sid = sid; - } + _bannerImpression.ext.dfp_ad_unit_code = dfpAdUnitCode; + _bannerImpression.ext.gpid = gpid; + _bannerImpression.ext.tid = tid; + _bannerImpression.ext.sid = sid; + _bannerImpression.ext.externalID = externalID; - if ('bidfloor' in impressionObjects[0]) { - _bannerImpression.bidfloor = impressionObjects[0].bidfloor; - } + // enable fledge auction + if (auctionEnvironment == 1) { + _bannerImpression.ext.ae = 1; + } + } - if ('bidfloorcur' in impressionObjects[0]) { - _bannerImpression.bidfloorcur = impressionObjects[0].bidfloorcur; - } + if ('bidfloor' in bannerImps[0]) { + _bannerImpression.bidfloor = bannerImps[0].bidfloor; + } - const adUnitFPD = impressions[impKeys[adUnitIndex]].adUnitFPD - if (adUnitFPD) { - _bannerImpression.ext.data = adUnitFPD; - } + if ('bidfloorcur' in bannerImps[0]) { + _bannerImpression.bidfloorcur = bannerImps[0].bidfloorcur; + } - r.imp.push(_bannerImpression); - } else { - // set imp.ext.gpid to resolved gpid for each imp - impressionObjects.forEach(imp => deepSetValue(imp, 'ext.gpid', gpid)); - r.imp.push(...impressionObjects); + const adUnitFPD = impressions[impKeys[adUnitIndex]].adUnitFPD + if (adUnitFPD) { + deepSetValue(_bannerImpression, 'ext.data', adUnitFPD); + } + r.imp.push(_bannerImpression); + } + } + if (otherImpressions.length > 0) { + // Creates multiformat imp if they have the same ID + // if not same ID, just add the imp to the imp array + // Removes imp.ext.bidfloor + // Sets imp.ext.siteID to one of the other [video/native].ext.siteid if imp.ext.siteID doesnt exist + otherImpressions.forEach(imp => { + if (gpid) { + deepSetValue(imp, 'ext.gpid', gpid); + } + if (r.imp.length > 0) { + let matchFound = false; + r.imp.forEach((rImp, index) => { + if (imp.id === rImp.id && VIDEO in imp) { + rImp.video = imp.video; + rImp.video.ext = Object.assign({}, imp.video.ext, imp.ext); + if (deepAccess(rImp, 'video.ext.bidfloor', false) && deepAccess(rImp, 'bidfloor', false)) { + if (rImp.video.ext.bidfloor < rImp.bidfloor) { + rImp.bidfloor = rImp.video.ext.bidfloor; + } + } + if (!deepAccess(rImp, 'ext.siteID', false) && deepAccess(imp, 'video.ext.siteID')) { + deepSetValue(rImp, 'ext.siteID', imp.video.ext.siteID); + deepSetValue(r, 'ext.ixdiag.usid', true); + } + matchFound = true; + } else if (imp.id === rImp.id && NATIVE in imp) { + rImp.native = imp.native; + rImp.native.ext = Object.assign({}, imp.native.ext, imp.ext); + if (deepAccess(rImp, 'native.ext.bidfloor', false) && deepAccess(rImp, 'bidfloor', false)) { + if (rImp.native.ext.bidfloor < rImp.bidfloor) { + rImp.bidfloor = rImp.native.ext.bidfloor; + } + } + if (!deepAccess(rImp, 'ext.siteID', false) && deepAccess(imp, 'native.ext.siteID', false)) { + deepSetValue(rImp, 'ext.siteID', imp.native.ext.siteID); + deepSetValue(r, 'ext.ixdiag.usid', true); + } + matchFound = true; + } + }); + if (!matchFound) { + r.imp.push(imp); + } + } else { + r.imp.push(imp); + } + }); } - return r; } @@ -954,7 +1113,7 @@ This function retrieves the page URL and appends first party data query paramete to it without adding duplicate query parameters. Returns original referer URL if no IX FPD exists. @param {Object} bidderRequest - The bidder request object containing information about the bid and the page. @returns {string} - The modified page URL with first party data query parameters appended. -*/ + */ function getIxFirstPartyDataPageUrl (bidderRequest) { // Parse additional runtime configs. const bidderCode = (bidderRequest && bidderRequest.bidderCode) || 'ix'; @@ -984,7 +1143,7 @@ This function appends the provided query parameters to the given URL without add @param {string} url - The base URL to which query parameters will be appended. @param {Object} params - An object containing key-value pairs of query parameters to append. @returns {string} - The modified URL with the provided query parameters appended. -*/ + */ function appendIXQueryParams(bidderRequest, url, params) { let urlObj; try { @@ -1069,10 +1228,10 @@ function addAdUnitFPD(imp, bid) { /** * addIdentifiersInfo adds indentifier info to ixDaig. * - * @param {array} impressions List of impressions to be added to the request. + * @param {Array} impressions List of impressions to be added to the request. * @param {object} r Reuqest object. - * @param {array} impKeys List of impression keys. - * @param {int} adUnitIndex Index of the current add unit + * @param {Array} impKeys List of impression keys. + * @param {number} 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. @@ -1095,7 +1254,7 @@ function addIdentifiersInfo(impressions, r, impKeys, adUnitIndex, payload, baseU /** * Return an object of user IDs stored by Prebid User ID module * - * @returns {array} ID providers that are present in userIds + * @returns {Array} ID providers that are present in userIds */ function _getUserIds(bidRequest) { const userIds = bidRequest.userId || {}; @@ -1106,15 +1265,16 @@ function _getUserIds(bidRequest) { /** * Calculates IX diagnostics values and packages them into an object * - * @param {array} validBidRequests The valid bid requests from prebid + * @param {Array} validBidRequests - The valid bid requests from prebid + * @param {boolean} fledgeEnabled - Flag indicating if protected audience (fledge) is enabled * @return {Object} IX diag values for ad units */ -function buildIXDiag(validBidRequests) { +function buildIXDiag(validBidRequests, fledgeEnabled) { var adUnitMap = validBidRequests .map(bidRequest => bidRequest.adUnitCode) .filter((value, index, arr) => arr.indexOf(value) === index); - var ixdiag = { + let ixdiag = { mfu: 0, bu: 0, iu: 0, @@ -1125,12 +1285,13 @@ function buildIXDiag(validBidRequests) { version: '$prebid.version$', userIds: _getUserIds(validBidRequests[0]), url: window.location.href.split('?')[0], - vpd: defaultVideoPlacement + vpd: defaultVideoPlacement, + ae: fledgeEnabled }; // create ad unit map and collect the required diag properties - for (let i = 0; i < adUnitMap.length; i++) { - var bid = validBidRequests.filter(bidRequest => bidRequest.adUnitCode === adUnitMap[i])[0]; + for (let adUnit of adUnitMap) { + let bid = validBidRequests.filter(bidRequest => bidRequest.adUnitCode === adUnit)[0]; if (deepAccess(bid, 'mediaTypes')) { if (Object.keys(bid.mediaTypes).length > 1) { @@ -1166,8 +1327,8 @@ function buildIXDiag(validBidRequests) { /** * - * @param {array} bannerSizeList list of banner sizes - * @param {array} bannerSize the size to be removed + * @param {Array} bannerSizeList list of banner sizes + * @param {Array} bannerSize the size to be removed * @return {boolean} true if successfully removed, false if not found */ @@ -1236,7 +1397,7 @@ function createVideoImps(validBidRequest, videoImps) { * @param {object} missingBannerSizes reference to missing banner config sizes * @param {object} bannerImps reference to created banner impressions */ -function createBannerImps(validBidRequest, missingBannerSizes, bannerImps) { +function createBannerImps(validBidRequest, missingBannerSizes, bannerImps, bidderRequest) { let imp = bidToBannerImp(validBidRequest); const bannerSizeDefined = includesSize(deepAccess(validBidRequest, 'mediaTypes.banner.sizes'), deepAccess(validBidRequest, 'params.size')); @@ -1252,6 +1413,21 @@ function createBannerImps(validBidRequest, missingBannerSizes, bannerImps) { bannerImps[validBidRequest.adUnitCode].tagId = deepAccess(validBidRequest, 'params.tagId'); bannerImps[validBidRequest.adUnitCode].pos = deepAccess(validBidRequest, 'mediaTypes.banner.pos'); + // Add Fledge flag if enabled + const fledgeEnabled = deepAccess(bidderRequest, 'fledgeEnabled') + if (fledgeEnabled) { + const auctionEnvironment = deepAccess(validBidRequest, 'ortb2Imp.ext.ae') + if (auctionEnvironment) { + if (isInteger(auctionEnvironment)) { + bannerImps[validBidRequest.adUnitCode].ae = auctionEnvironment; + } else { + logWarn('error setting auction environment flag - must be an integer') + } + } else if (deepAccess(bidderRequest, 'defaultForSlots') == 1) { + bannerImps[validBidRequest.adUnitCode].ae = 1 + } + } + // AdUnit-Specific First Party Data const adUnitFPD = deepAccess(validBidRequest, 'ortb2Imp.ext.data'); if (adUnitFPD) { @@ -1311,7 +1487,7 @@ function updateMissingSizes(validBidRequest, missingBannerSizes, imp) { /** * @param {object} bid ValidBidRequest object, used to adjust floor * @param {object} imp Impression object to be modified - * @param {array} newSize The new size to be applied + * @param {Array} newSize The new size to be applied * @return {object} newImp Updated impression object */ function createMissingBannerImp(bid, imp, newSize) { @@ -1487,6 +1663,17 @@ function isIndexRendererPreferred(bid) { return !isValid || renderer.backupOnly; } +function isExchangeIdConfigured() { + let exchangeId = config.getConfig('exchangeId'); + if (typeof exchangeId === 'number' && isFinite(exchangeId)) { + return true; + } + if (typeof exchangeId === 'string' && exchangeId.trim() !== '' && isFinite(Number(exchangeId))) { + return true; + } + return false; +} + export const spec = { code: BIDDER_CODE, @@ -1544,14 +1731,21 @@ export const spec = { } } - if (typeof bid.params.siteId !== 'string' && typeof bid.params.siteId !== 'number') { - logError('IX Bid Adapter: siteId must be string or number type.', { bidder: BIDDER_CODE, code: ERROR_CODES.SITE_ID_INVALID_VALUE }); + if (!isExchangeIdConfigured() && bid.params.siteId == undefined) { + logError('IX Bid Adapter: Invalid configuration - either siteId or exchangeId must be configured.'); return false; } - if (typeof bid.params.siteId !== 'string' && isNaN(Number(bid.params.siteId))) { - logError('IX Bid Adapter: siteId must valid value', { bidder: BIDDER_CODE, code: ERROR_CODES.SITE_ID_INVALID_VALUE }); - return false; + if (bid.params.siteId !== undefined) { + if (typeof bid.params.siteId !== 'string' && typeof bid.params.siteId !== 'number') { + logError('IX Bid Adapter: siteId must be string or number type.', { bidder: BIDDER_CODE, code: ERROR_CODES.SITE_ID_INVALID_VALUE }); + return false; + } + + if (typeof bid.params.siteId !== 'string' && isNaN(Number(bid.params.siteId))) { + logError('IX Bid Adapter: siteId must valid value', { bidder: BIDDER_CODE, code: ERROR_CODES.SITE_ID_INVALID_VALUE }); + return false; + } } if (hasBidFloor || hasBidFloorCur) { @@ -1584,10 +1778,15 @@ export const spec = { return nativeMediaTypeValid(bid); }, + // For testing only - resets the siteID to 0 so that it can be set again + resetSiteID: function () { + siteID = 0; + }, + /** * Make a server request from the list of BidRequests. * - * @param {array} validBidRequests A list of valid bid request config objects. + * @param {Array} validBidRequests A list of valid bid request config objects. * @param {object} bidderRequest A object contains bids and other info like gdprConsent. * @return {object} Info describing the request to the server. */ @@ -1606,7 +1805,7 @@ export const spec = { for (const type in adUnitMediaTypes) { switch (adUnitMediaTypes[type]) { case BANNER: - createBannerImps(validBidRequest, missingBannerSizes, bannerImps); + createBannerImps(validBidRequest, missingBannerSizes, bannerImps, bidderRequest); break; case VIDEO: createVideoImps(validBidRequest, videoImps) @@ -1642,17 +1841,31 @@ export const spec = { } } - // Step 4: Build banner, video & native requests + // Step 3: Build banner, video & native requests + let allImps = []; if (Object.keys(bannerImps).length > 0) { - reqs.push(...buildRequest(validBidRequests, bidderRequest, bannerImps, BANNER_ENDPOINT_VERSION)); + allImps.push(bannerImps); } if (Object.keys(videoImps).length > 0) { - reqs.push(...buildRequest(validBidRequests, bidderRequest, videoImps, VIDEO_ENDPOINT_VERSION)); + allImps.push(videoImps); } if (Object.keys(nativeImps).length > 0) { - reqs.push(...buildRequest(validBidRequests, bidderRequest, nativeImps)); + allImps.push(nativeImps); } + if (FEATURE_TOGGLES.isFeatureEnabled('pbjs_enable_multiformat')) { + reqs.push(...buildRequest(validBidRequests, bidderRequest, combineImps(allImps))); + } else { + if (Object.keys(bannerImps).length > 0) { + reqs.push(...buildRequest(validBidRequests, bidderRequest, bannerImps, BANNER_ENDPOINT_VERSION)); + } + if (Object.keys(videoImps).length > 0) { + reqs.push(...buildRequest(validBidRequests, bidderRequest, videoImps, VIDEO_ENDPOINT_VERSION)); + } + if (Object.keys(nativeImps).length > 0) { + reqs.push(...buildRequest(validBidRequests, bidderRequest, nativeImps)); + } + } return reqs; }, @@ -1661,19 +1874,24 @@ export const spec = { * * @param {object} serverResponse A successful response from the server. * @param {object} bidderRequest The bid request sent to the server. - * @return {array} An array of bids which were nested inside the server. + * @return {Array} An array of bids which were nested inside the server. */ interpretResponse: function (serverResponse, bidderRequest) { const bids = []; let bid = null; - if (!serverResponse.hasOwnProperty('body') || !serverResponse.body.hasOwnProperty('seatbid')) { - FEATURE_TOGGLES.setFeatureToggles(serverResponse); + // Extract the FLEDGE auction configuration list from the response + let fledgeAuctionConfigs = deepAccess(serverResponse, 'body.ext.protectedAudienceAuctionConfigs') || []; + + FEATURE_TOGGLES.setFeatureToggles(serverResponse); + + if (!serverResponse.hasOwnProperty('body')) { return bids; } const responseBody = serverResponse.body; - const seatbid = responseBody.seatbid; + const seatbid = responseBody.seatbid || []; + for (let i = 0; i < seatbid.length; i++) { if (!seatbid[i].hasOwnProperty('bid')) { continue; @@ -1709,8 +1927,28 @@ export const spec = { } } - FEATURE_TOGGLES.setFeatureToggles(serverResponse); - return bids; + if (Array.isArray(fledgeAuctionConfigs) && fledgeAuctionConfigs.length > 0) { + // Validate and filter fledgeAuctionConfigs + fledgeAuctionConfigs = fledgeAuctionConfigs.filter(config => { + if (!isValidAuctionConfig(config)) { + logWarn('Malformed auction config detected:', config); + return false; + } + return true; + }); + + try { + return { + bids, + fledgeAuctionConfigs, + }; + } catch (error) { + logWarn('Error attaching AuctionConfigs', error); + return bids; + } + } else { + return bids; + } }, /** @@ -1728,8 +1966,8 @@ export const spec = { /** * Determine which user syncs should occur * @param {object} syncOptions - * @param {array} serverResponses - * @returns {array} User sync pixels + * @param {Array} serverResponses + * @returns {Array} User sync pixels */ getUserSyncs: function (syncOptions, serverResponses) { const syncs = []; @@ -1770,11 +2008,11 @@ export const spec = { }; /** - * Build img user sync url - * @param {int} syncsPerBidder number of syncs Per Bidder - * @param {int} index index to pass - * @returns {string} img user sync url - */ + * Build img user sync url + * @param {number} syncsPerBidder number of syncs Per Bidder + * @param {number} index index to pass + * @returns {string} img user sync url + */ function buildImgSyncUrl(syncsPerBidder, index) { let consentString = ''; let gdprApplies = '0'; @@ -1784,8 +2022,176 @@ function buildImgSyncUrl(syncsPerBidder, index) { if (gdprConsent && gdprConsent.hasOwnProperty('consentString')) { consentString = gdprConsent.consentString || ''; } + let siteIdParam = siteID !== 0 ? '&site_id=' + siteID.toString() : ''; + + return IMG_USER_SYNC_URL + siteIdParam + '&p=' + syncsPerBidder.toString() + '&i=' + index.toString() + '&gdpr=' + gdprApplies + '&gdpr_consent=' + consentString + '&us_privacy=' + (usPrivacy || ''); +} + +/** + * Combines all imps into a single object + * @param {Array} imps array of imps + * @returns object + */ +export function combineImps(imps) { + const result = {} + imps.forEach((imp) => { + Object.keys(imp).forEach((key) => { + if (Object.keys(result).includes(key)) { + if (result[key].hasOwnProperty('ixImps') && imp[key].hasOwnProperty('ixImps')) { + result[key].ixImps = [...result[key].ixImps, ...imp[key].ixImps]; + } else if (result[key].hasOwnProperty('missingImps') && imp[key].hasOwnProperty('missingImps')) { + result[key].missingImps = [...result[key].missingImps, ...imp[key].missingImps]; + } else if (imp[key].hasOwnProperty('ixImps')) { + result[key].ixImps = imp[key].ixImps; + } else if (imp[key].hasOwnProperty('missingImps')) { + result[key].missingImps = imp[key].missingImps + } + } else { + result[key] = imp[key]; + } + }); + }); + return result; +} + +/** + * Deduplicates ext fields. For example if imp.ext.tid exists, removes imp.banner.ext.tid + * + * @param {object} r request object + * @returns object + */ +export function deduplicateImpExtFields(r) { + r.imp.forEach((imp, index) => { + const impExt = imp.ext; + if (impExt == undefined) { + return r; + } + if (getFormatCount(imp) < 2) { + return; + } + Object.keys(impExt).forEach((key) => { + if (BANNER in imp) { + const bannerExt = imp.banner.ext; + if (bannerExt !== undefined && bannerExt[key] !== undefined && bannerExt[key] == impExt[key]) { + delete r.imp[index].banner.ext[key]; + } + if (imp.banner.format !== undefined) { + for (let i = 0; i < imp.banner.format.length; i++) { + if (imp.banner.format[i].ext != undefined && imp.banner.format[i].ext[key] != undefined && imp.banner.format[i].ext[key] == impExt[key]) { + delete r.imp[index].banner.format[i].ext[key]; + } + } + } + } + if (VIDEO in imp) { + const videoExt = imp.video.ext; + if (videoExt !== undefined && videoExt[key] !== undefined && videoExt[key] == impExt[key]) { + delete r.imp[index].video.ext[key]; + } + } - return IMG_USER_SYNC_URL + '&site_id=' + siteID.toString() + '&p=' + syncsPerBidder.toString() + '&i=' + index.toString() + '&gdpr=' + gdprApplies + '&gdpr_consent=' + consentString + '&us_privacy=' + (usPrivacy || ''); + if (NATIVE in imp) { + const nativeExt = imp.native.ext; + if (nativeExt !== undefined && nativeExt[key] !== undefined && nativeExt[key] == impExt[key]) { + delete r.imp[index].native.ext[key]; + } + } + }); + }); + return r; +} + +/** + * Removes ext.siteids in multiformat scenario + * Site id will be set only at imp.ext.siteId + * + * @param {object} r request object + * @returns object + */ +export function removeSiteIDs(r) { + r.imp.forEach((imp, index) => { + const impExt = imp.ext; + if (impExt == undefined) { + return r; + } + if (getFormatCount(imp) < 2) { + return; + } + + if (BANNER in imp) { + const bannerExt = imp.banner.ext; + if (bannerExt !== undefined && bannerExt.siteID !== undefined) { + delete r.imp[index].banner.ext.siteID; + } + if (imp.banner.format !== undefined) { + for (let i = 0; i < imp.banner.format.length; i++) { + if (imp.banner.format[i].ext !== undefined && imp.banner.format[i].ext.siteID !== undefined) { + deepSetValue(r.imp[index], 'ext.siteID', imp.banner.format[i].ext.siteID); + deepSetValue(r, 'ext.ixdiag.usid', true); + delete r.imp[index].banner.format[i].ext.siteID; + } + } + } + } + + if (VIDEO in imp) { + const videoExt = imp.video.ext; + if (videoExt !== undefined && videoExt.siteID !== undefined) { + delete r.imp[index].video.ext.siteID; + } + } + + if (NATIVE in imp) { + const nativeExt = imp.native.ext; + if (nativeExt !== undefined && nativeExt.siteID !== undefined) { + delete r.imp[index].native.ext.siteID; + } + } + }); + return r; +} + +/** + * Gets count of banner/video/native formats in imp + * @param {object} imp + * @returns int + */ +function getFormatCount(imp) { + let formatCount = 0; + if (imp.banner !== undefined) { + formatCount += 1; + } + if (imp.video !== undefined) { + formatCount += 1; + } + if (imp.native !== undefined) { + formatCount += 1; + } + return formatCount; +} + +/** + * Checks if auction config is valid + * @param {object} config + * @returns bool + */ +function isValidAuctionConfig(config) { + return typeof config === 'object' && config !== null; +} + +/** + * Adds device.w / device.h info + * @param {object} r + * @returns object + */ +export function addDeviceInfo(r) { + if (r.device == undefined) { + r.device = {}; + } + r.device.h = window.screen.height; + r.device.w = window.screen.width; + + return r; } registerBidder(spec); diff --git a/modules/ixBidAdapter.md b/modules/ixBidAdapter.md index 638cb11c5ab..0705c5932cf 100644 --- a/modules/ixBidAdapter.md +++ b/modules/ixBidAdapter.md @@ -469,6 +469,11 @@ pbjs.setConfig({ The timeout value must be a positive whole number in milliseconds. +Protected Audience API (FLEDGE) +=========================== + +In order to enable receiving [Protected Audience API](https://developer.chrome.com/en/docs/privacy-sandbox/fledge/) traffic, follow Prebid's documentation on [fledgeForGpt](https://docs.prebid.org/dev-docs/modules/fledgeForGpt.html) module to build and enable Fledge. + Additional Information ====================== diff --git a/modules/jixieBidAdapter.js b/modules/jixieBidAdapter.js index b587011c748..75268e9d168 100644 --- a/modules/jixieBidAdapter.js +++ b/modules/jixieBidAdapter.js @@ -1,4 +1,4 @@ -import {deepAccess, getDNT, isArray, logWarn} from '../src/utils.js'; +import {deepAccess, getDNT, isArray, logWarn, isFn, isPlainObject, logError, logInfo} from '../src/utils.js'; import {config} from '../src/config.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import {getStorageManager} from '../src/storageManager.js'; @@ -7,13 +7,36 @@ import {ajax} from '../src/ajax.js'; import {getRefererInfo} from '../src/refererDetection.js'; import {Renderer} from '../src/Renderer.js'; +const ADAPTER_VERSION = '2.1.0'; +const PREBID_VERSION = '$prebid.version$'; + const BIDDER_CODE = 'jixie'; export const storage = getStorageManager({bidderCode: BIDDER_CODE}); -const EVENTS_URL = 'https://hbtra.jixie.io/sync/hb?'; const JX_OUTSTREAM_RENDERER_URL = 'https://scripts.jixie.media/jxhbrenderer.1.1.min.js'; const REQUESTS_URL = 'https://hb.jixie.io/v2/hbpost'; const sidTTLMins_ = 30; +/** + * Get bid floor from Price Floors Module + * + * @param {Object} bid + * @returns {float||null} + */ +function getBidFloor(bid) { + if (!isFn(bid.getFloor)) { + return null; + } + let floor = bid.getFloor({ + currency: 'USD', + mediaType: '*', + size: '*' + }); + if (isPlainObject(floor) && !isNaN(floor.floor) && floor.currency === 'USD') { + return floor.floor; + } + return null; +} + /** * Own miscellaneous support functions: */ @@ -38,12 +61,24 @@ function setIds_(clientId, sessionId) { } catch (error) {} } -function fetchIds_() { +/** + * fetch some ids from cookie, LS. + * @returns + */ +const defaultGenIds_ = [ + { id: '_jxtoko' }, + { id: '_jxifo' }, + { id: '_jxtdid' }, + { id: '_jxcomp' } +]; + +function fetchIds_(cfg) { let ret = { client_id_c: '', client_id_ls: '', session_id_c: '', - session_id_ls: '' + session_id_ls: '', + jxeids: {} }; try { let tmp = storage.getCookie('_jxx'); @@ -55,8 +90,12 @@ function fetchIds_() { if (tmp) ret.client_id_ls = tmp; tmp = storage.getDataFromLocalStorage('_jxxs'); if (tmp) ret.session_id_ls = tmp; - tmp = storage.getCookie('_jxtoko'); - if (tmp) ret.jxtoko_id = tmp; + + let arr = cfg.genids ? cfg.genids : defaultGenIds_; + arr.forEach(function(o) { + tmp = storage.getCookie(o.ck ? o.ck : o.id); + if (tmp) ret.jxeids[o.id] = tmp; + }); } catch (error) {} return ret; } @@ -73,14 +112,6 @@ function getDevice_() { return device; } -function pingTracking_(endpointOverride, qpobj) { - internal.ajax((endpointOverride || EVENTS_URL), null, qpobj, { - withCredentials: true, - method: 'GET', - crossOrigin: true - }); -} - function jxOutstreamRender_(bidAd) { bidAd.renderer.push(() => { window.JixieOutstreamVideo.init({ @@ -132,17 +163,6 @@ function getMiscDims_() { return ret; } -/* function addUserId(eids, id, source, rti) { - if (id) { - if (rti) { - eids.push({ source, id, rti_partner: rti }); - } else { - eids.push({ source, id }); - } - } - return eids; -} */ - // easier for replacement in the unit test export const internal = { getDevice: getDevice_, @@ -153,7 +173,6 @@ export const internal = { export const spec = { code: BIDDER_CODE, - EVENTS_URL: EVENTS_URL, supportedMediaTypes: [BANNER, VIDEO], isBidRequestValid: function(bid) { if (bid.bidder !== BIDDER_CODE || typeof bid.params === 'undefined') { @@ -170,39 +189,38 @@ export const spec = { let bids = []; validBidRequests.forEach(function(one) { - bids.push({ + let gpid = deepAccess(one, 'ortb2Imp.ext.gpid', deepAccess(one, 'ortb2Imp.ext.data.pbadslot', '')); + let tmp = { bidId: one.bidId, adUnitCode: one.adUnitCode, mediaTypes: (one.mediaTypes === 'undefined' ? {} : one.mediaTypes), sizes: (one.sizes === 'undefined' ? [] : one.sizes), params: one.params, - }); + gpid: gpid + }; + let bidFloor = getBidFloor(one); + if (bidFloor) { + tmp.bidFloor = bidFloor; + } + bids.push(tmp); }); + let jxCfg = config.getConfig('jixie') || {}; - let jixieCfgBlob = config.getConfig('jixie'); - if (!jixieCfgBlob) { - jixieCfgBlob = {}; - } - - let ids = fetchIds_(); + let ids = fetchIds_(jxCfg); let eids = []; let miscDims = internal.getMiscDims(); let schain = deepAccess(validBidRequests[0], 'schain'); - let eids1 = validBidRequests[0].userIdAsEids + let eids1 = validBidRequests[0].userIdAsEids; // all available user ids are sent to our backend in the standard array layout: if (eids1 && eids1.length) { eids = eids1; } // we want to send this blob of info to our backend: - let pg = config.getConfig('priceGranularity'); - if (!pg) { - pg = {}; - } - let transformedParams = Object.assign({}, { // TODO: fix auctionId leak: https://github.com/prebid/Prebid.js/issues/9781 - auctionid: bidderRequest.auctionId, + auctionid: bidderRequest.auctionId || '', + aid: jxCfg.aid || '', timeout: bidderRequest.timeout, currency: currency, timestamp: (new Date()).getTime(), @@ -213,8 +231,10 @@ export const spec = { bids: bids, eids: eids, schain: schain, - pricegranularity: pg, - cfg: jixieCfgBlob + pricegranularity: (config.getConfig('priceGranularity') || {}), + ver: ADAPTER_VERSION, + pbjsver: PREBID_VERSION, + cfg: jxCfg }, ids); return Object.assign({}, { method: 'POST', @@ -225,48 +245,20 @@ export const spec = { }, onTimeout: function(timeoutData) { - let jxCfgBlob = config.getConfig('jixie'); - if (jxCfgBlob && jxCfgBlob.onTimeout == 'off') { - return; - } - let url = null;// default - if (jxCfgBlob && jxCfgBlob.onTimeoutUrl && typeof jxCfgBlob.onTimeoutUrl == 'string') { - url = jxCfgBlob.onTimeoutUrl; - } - let miscDims = internal.getMiscDims(); - pingTracking_(url, // no overriding ping URL . just use default - { - action: 'hbtimeout', - device: miscDims.device, - pageurl: encodeURIComponent(miscDims.pageurl), - domain: encodeURIComponent(miscDims.domain), - auctionid: deepAccess(timeoutData, '0.auctionId'), - timeout: deepAccess(timeoutData, '0.timeout'), - count: timeoutData.length - }); + logError('jixie adapter timed out for the auction.', timeoutData); }, onBidWon: function(bid) { - if (bid.notrack) { - return; - } if (bid.trackingUrl) { - pingTracking_(bid.trackingUrl, {}); - } else { - let miscDims = internal.getMiscDims(); - pingTracking_((bid.trackingUrlBase ? bid.trackingUrlBase : null), { - action: 'hbbidwon', - device: miscDims.device, - pageurl: encodeURIComponent(miscDims.pageurl), - domain: encodeURIComponent(miscDims.domain), - cid: bid.cid, - cpid: bid.cpid, - jxbidid: bid.jxBidId, - auctionid: bid.auctionId, - cpm: bid.cpm, - requestid: bid.requestId + internal.ajax(bid.trackingUrl, null, {}, { + withCredentials: true, + method: 'GET', + crossOrigin: true }); } + logInfo( + `jixie adapter won the auction. Bid id: ${bid.bidId}, Ad Unit Id: ${bid.adUnitId}` + ); }, interpretResponse: function(response, bidRequest) { @@ -274,7 +266,6 @@ export const spec = { const bidResponses = []; response.body.bids.forEach(function(oneBid) { let bnd = {}; - Object.assign(bnd, oneBid); if (oneBid.osplayer) { bnd.adResponse = { diff --git a/modules/justIdSystem.js b/modules/justIdSystem.js index 15b1c90da4e..a5698023020 100644 --- a/modules/justIdSystem.js +++ b/modules/justIdSystem.js @@ -10,6 +10,13 @@ import { submodule } from '../src/hook.js' import { loadExternalScript } from '../src/adloader.js' import {includes} from '../src/polyfill.js'; +/** + * @typedef {import('../modules/userId/index.js').Submodule} Submodule + * @typedef {import('../modules/userId/index.js').SubmoduleConfig} SubmoduleConfig + * @typedef {import('../modules/userId/index.js').ConsentData} ConsentData + * @typedef {import('../modules/userId/index.js').IdResponse} IdResponse + */ + const MODULE_NAME = 'justId'; const EXTERNAL_SCRIPT_MODULE_CODE = 'justtag'; const LOG_PREFIX = 'User ID - JustId submodule: '; @@ -91,6 +98,12 @@ export const justIdSubmodule = { } } }; + }, + eids: { + 'justId': { + source: 'justtag.com', + atype: 1 + }, } }; diff --git a/modules/jwplayerRtdProvider.js b/modules/jwplayerRtdProvider.js index b79843dccfd..573ff391dae 100644 --- a/modules/jwplayerRtdProvider.js +++ b/modules/jwplayerRtdProvider.js @@ -16,6 +16,11 @@ import {deepAccess, logError} from '../src/utils.js'; import {find} from '../src/polyfill.js'; import {getGlobal} from '../src/prebidGlobal.js'; +/** + * @typedef {import('../modules/rtdModule/index.js').RtdSubmodule} RtdSubmodule + * @typedef {import('../modules/rtdModule/index.js').adUnit} adUnit + */ + const SUBMODULE_NAME = 'jwplayer'; const JWPLAYER_DOMAIN = SUBMODULE_NAME + '.com'; const segCache = {}; @@ -26,16 +31,16 @@ let resumeBidRequest; /** @type {RtdSubmodule} */ export const jwplayerSubmodule = { /** - * used to link submodule with realTimeData - * @type {string} - */ + * used to link submodule with realTimeData + * @type {string} + */ name: SUBMODULE_NAME, /** - * add targeting data to bids and signal completion to realTimeData module - * @function - * @param {Obj} bidReqConfig - * @param {function} onDone - */ + * add targeting data to bids and signal completion to realTimeData module + * @function + * @param {Obj} bidReqConfig + * @param {function} onDone + */ getBidRequestData: enrichBidRequest, init }; diff --git a/modules/kargoBidAdapter.js b/modules/kargoBidAdapter.js index 5b315ef5791..9d8c7bc06a1 100644 --- a/modules/kargoBidAdapter.js +++ b/modules/kargoBidAdapter.js @@ -24,6 +24,7 @@ const CURRENCY = Object.freeze({ }); const REQUEST_KEYS = Object.freeze({ + USER_DATA: 'ortb2.user.data', SOCIAL_CANVAS: 'params.socialCanvas', SUA: 'ortb2.device.sua', TDID_ADAPTER: 'userId.tdid', @@ -97,11 +98,26 @@ function buildRequests(validBidRequests, bidderRequest) { user: getUserIds(tdidAdapter, bidderRequest.uspConsent, bidderRequest.gdprConsent, firstBidRequest.userIdAsEids, bidderRequest.gppConsent), }); + if (firstBidRequest.ortb2 != null) { + krakenParams.site = { + cat: firstBidRequest.ortb2.site.cat + } + } + + // Add schain + if (firstBidRequest.schain && firstBidRequest.schain.nodes) { + krakenParams.schain = firstBidRequest.schain + } + + // Add user data object if available + krakenParams.user.data = deepAccess(firstBidRequest, REQUEST_KEYS.USER_DATA) || []; + const reqCount = getRequestCount() if (reqCount != null) { krakenParams.requestCount = reqCount; } + // Add currency if not USD if (currency != null && currency != CURRENCY.US_DOLLAR) { krakenParams.cur = currency; } @@ -459,8 +475,8 @@ function getImpression(bid) { imp.bidderWinCount = bid.bidderWinsCount; } - const gpid = getGPID(bid) - if (gpid != null && gpid != '') { + const gpid = deepAccess(bid, 'ortb2Imp.ext.gpid') || deepAccess(bid, 'ortb2Imp.ext.data.pbadslot'); + if (gpid) { imp.fpd = { gpid: gpid } @@ -483,29 +499,6 @@ function getImpression(bid) { return imp } -function getGPID(bid) { - if (bid.ortb2Imp != null) { - if (bid.ortb2Imp.gpid != null && bid.ortb2Imp.gpid != '') { - return bid.ortb2Imp.gpid; - } - - if (bid.ortb2Imp.ext != null && bid.ortb2Imp.ext.data != null) { - if (bid.ortb2Imp.ext.data.pbAdSlot != null && bid.ortb2Imp.ext.data.pbAdSlot != '') { - return bid.ortb2Imp.ext.data.pbAdSlot; - } - - if (bid.ortb2Imp.ext.data.adServer != null && bid.ortb2Imp.ext.data.adServer.adSlot != null && bid.ortb2Imp.ext.data.adServer.adSlot != '') { - return bid.ortb2Imp.ext.data.adServer.adSlot; - } - } - } - - if (bid.adUnitCode != null && bid.adUnitCode != '') { - return bid.adUnitCode; - } - return ''; -} - export const spec = { gvlid: BIDDER.GVLID, code: BIDDER.CODE, diff --git a/modules/kinessoIdSystem.js b/modules/kinessoIdSystem.js index 632f3a669aa..35b8dcc182d 100644 --- a/modules/kinessoIdSystem.js +++ b/modules/kinessoIdSystem.js @@ -10,6 +10,12 @@ import {ajax} from '../src/ajax.js'; import {submodule} from '../src/hook.js'; import {coppaDataHandler, uspDataHandler} from '../src/adapterManager.js'; +/** + * @typedef {import('../modules/userId/index.js').Submodule} Submodule + * @typedef {import('../modules/userId/index.js').SubmoduleConfig} SubmoduleConfig + * @typedef {import('../modules/userId/index.js').ConsentData} ConsentData + */ + const MODULE_NAME = 'kpuid'; const ID_SVC = 'https://id.knsso.com/id'; // These values should NEVER change. If @@ -233,8 +239,13 @@ export const kinessoIdSubmodule = { const payloadString = JSON.stringify(kinessoIdPayload); ajax(kinessoSyncUrl(accountId, consentData), syncId(knnsoId), payloadString, {method: 'POST', withCredentials: true}); return {'id': knnsoId}; - } - + }, + eids: { + 'kpuid': { + source: 'kpuid.com', + atype: 3 + }, + }, }; // Register submodule for userId diff --git a/modules/kueezBidAdapter.js b/modules/kueezBidAdapter.js index 0a868661310..5a5536e0c1a 100644 --- a/modules/kueezBidAdapter.js +++ b/modules/kueezBidAdapter.js @@ -1,4 +1,16 @@ -import { logWarn, logInfo, isArray, isFn, deepAccess, isEmpty, contains, timestamp, getBidIdParameter, triggerPixel, isInteger } from '../src/utils.js'; +import { + logWarn, + logInfo, + isArray, + isFn, + deepAccess, + isEmpty, + contains, + timestamp, + triggerPixel, + isInteger, + getBidIdParameter +} from '../src/utils.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; import { BANNER, VIDEO } from '../src/mediaTypes.js'; import { config } from '../src/config.js'; diff --git a/modules/kueezRtbBidAdapter.js b/modules/kueezRtbBidAdapter.js index bb0534d6372..9a336b16136 100644 --- a/modules/kueezRtbBidAdapter.js +++ b/modules/kueezRtbBidAdapter.js @@ -251,13 +251,20 @@ function interpretResponse(serverResponse, request) { } } -function getUserSyncs(syncOptions, responses, gdprConsent = {}, uspConsent = '') { +function getUserSyncs(syncOptions, responses, gdprConsent = {}, uspConsent = '', gppConsent = {}) { let syncs = []; const {iframeEnabled, pixelEnabled} = syncOptions; const {gdprApplies, consentString = ''} = gdprConsent; + const {gppString, applicableSections} = gppConsent; const cidArr = responses.filter(resp => deepAccess(resp, 'body.cid')).map(resp => resp.body.cid).filter(uniques); - const params = `?cid=${encodeURIComponent(cidArr.join(','))}&gdpr=${gdprApplies ? 1 : 0}&gdpr_consent=${encodeURIComponent(consentString || '')}&us_privacy=${encodeURIComponent(uspConsent || '')}` + let params = `?cid=${encodeURIComponent(cidArr.join(','))}&gdpr=${gdprApplies ? 1 : 0}&gdpr_consent=${encodeURIComponent(consentString || '')}&us_privacy=${encodeURIComponent(uspConsent || '')}` + + if (gppString && applicableSections?.length) { + params += '&gpp=' + encodeURIComponent(gppString); + params += '&gpp_sid=' + encodeURIComponent(applicableSections.join(',')); + } + if (iframeEnabled) { syncs.push({ type: 'iframe', diff --git a/modules/kulturemediaBidAdapter.js b/modules/kulturemediaBidAdapter.js deleted file mode 100644 index fb3f6e4e231..00000000000 --- a/modules/kulturemediaBidAdapter.js +++ /dev/null @@ -1,472 +0,0 @@ -import { - deepAccess, - deepSetValue, - isArray, - isFn, - isNumber, - isPlainObject, - isStr, - logError, - logInfo, - logMessage -} from '../src/utils.js'; -import {registerBidder} from '../src/adapters/bidderFactory.js'; -import {BANNER, VIDEO} from '../src/mediaTypes.js'; - -const BIDDER_CODE = 'kulturemedia'; -const DEFAULT_BID_TTL = 300; -const DEFAULT_CURRENCY = 'USD'; -const DEFAULT_NET_REVENUE = true; -const DEFAULT_NETWORK_ID = 1; -const OPENRTB_VIDEO_PARAMS = [ - 'mimes', - 'minduration', - 'maxduration', - 'placement', - 'protocols', - 'startdelay', - 'skip', - 'skipafter', - 'minbitrate', - 'maxbitrate', - 'delivery', - 'playbackmethod', - 'api', - 'linearity' -]; - -export const spec = { - code: BIDDER_CODE, - VERSION: '1.0.0', - supportedMediaTypes: [BANNER, VIDEO], - ENDPOINT: 'https://ads.kulture.media/pbjs', - - /** - * Determines whether or not the given bid request is valid. - * - * @param {BidRequest} bidRequest The bid params to validate. - * @return boolean True if this is a valid bid, and false otherwise. - */ - isBidRequestValid: function (bid) { - return ( - _validateParams(bid) && - _validateBanner(bid) && - _validateVideo(bid) - ); - }, - - /** - * Make a server request from the list of BidRequests. - * - * @param {BidRequest[]} validBidRequests A non-empty list of bid requests which should be sent to the Server. - * @param {BidderRequest} bidderRequest bidder request object. - * @return ServerRequest Info describing the request to the server. - */ - buildRequests: function (validBidRequests, bidderRequest) { - if (!validBidRequests || !bidderRequest) { - return; - } - - // We need to refactor this to support mixed content when there are both - // banner and video bid requests - let openrtbRequest; - if (hasBannerMediaType(validBidRequests[0])) { - openrtbRequest = buildBannerRequestData(validBidRequests, bidderRequest); - } else if (hasVideoMediaType(validBidRequests[0])) { - openrtbRequest = buildVideoRequestData(validBidRequests[0], bidderRequest); - } - - // adding schain object - if (validBidRequests[0].schain) { - deepSetValue(openrtbRequest, 'source.ext.schain', validBidRequests[0].schain); - } - - // Attaching GDPR Consent Params - if (bidderRequest.gdprConsent) { - deepSetValue(openrtbRequest, 'user.ext.consent', bidderRequest.gdprConsent.consentString); - deepSetValue(openrtbRequest, 'regs.ext.gdpr', (bidderRequest.gdprConsent.gdprApplies ? 1 : 0)); - } - - // CCPA - if (bidderRequest.uspConsent) { - deepSetValue(openrtbRequest, 'regs.ext.us_privacy', bidderRequest.uspConsent); - } - - // EIDS - const eids = deepAccess(validBidRequests[0], 'userIdAsEids'); - if (Array.isArray(eids) && eids.length > 0) { - deepSetValue(openrtbRequest, 'user.ext.eids', eids); - } - - let publisherId = validBidRequests[0].params.publisherId; - let placementId = validBidRequests[0].params.placementId; - const networkId = validBidRequests[0].params.networkId || DEFAULT_NETWORK_ID; - - if (validBidRequests[0].params.e2etest) { - logMessage('E2E test mode enabled'); - publisherId = 'e2etest' - } - let baseEndpoint = spec.ENDPOINT + '?pid=' + publisherId; - - if (placementId) { - baseEndpoint += '&placementId=' + placementId - } - if (networkId) { - baseEndpoint += '&nId=' + networkId - } - - const payloadString = JSON.stringify(openrtbRequest); - return { - method: 'POST', - url: baseEndpoint, - data: payloadString, - }; - }, - - interpretResponse: function (serverResponse) { - const bidResponses = []; - const response = (serverResponse || {}).body; - // response is always one seat (exchange) with (optional) bids for each impression - if (response && response.seatbid && response.seatbid.length === 1 && response.seatbid[0].bid && response.seatbid[0].bid.length) { - response.seatbid[0].bid.forEach(bid => { - if (bid.adm && bid.price) { - bidResponses.push(_createBidResponse(bid)); - } - }) - } else { - logInfo('kulturemedia.interpretResponse :: no valid responses to interpret'); - } - return bidResponses; - }, - - getUserSyncs: function (syncOptions, serverResponses) { - logInfo('kulturemedia.getUserSyncs', 'syncOptions', syncOptions, 'serverResponses', serverResponses); - let syncs = []; - - if (!syncOptions.iframeEnabled && !syncOptions.pixelEnabled) { - return syncs; - } - - serverResponses.forEach(resp => { - const userSync = deepAccess(resp, 'body.ext.usersync'); - if (userSync) { - let syncDetails = []; - Object.keys(userSync).forEach(key => { - const value = userSync[key]; - if (value.syncs && value.syncs.length) { - syncDetails = syncDetails.concat(value.syncs); - } - }); - syncDetails.forEach(syncDetails => { - syncs.push({ - type: syncDetails.type === 'iframe' ? 'iframe' : 'image', - url: syncDetails.url - }); - }); - - if (!syncOptions.iframeEnabled) { - syncs = syncs.filter(s => s.type !== 'iframe') - } - if (!syncOptions.pixelEnabled) { - syncs = syncs.filter(s => s.type !== 'image') - } - } - }); - logInfo('kulturemedia.getUserSyncs result=%o', syncs); - return syncs; - }, - -}; - -/* ======================================= - * Util Functions - *======================================= */ - -/** - * @param {BidRequest} bidRequest bid request - */ -function hasBannerMediaType(bidRequest) { - return !!deepAccess(bidRequest, 'mediaTypes.banner'); -} - -/** - * @param {BidRequest} bidRequest bid request - */ -function hasVideoMediaType(bidRequest) { - return !!deepAccess(bidRequest, 'mediaTypes.video'); -} - -function _validateParams(bidRequest) { - if (!bidRequest.params) { - return false; - } - - if (bidRequest.params.e2etest) { - return true; - } - - if (!bidRequest.params.publisherId) { - logError('Validation failed: publisherId not declared'); - return false; - } - - if (!bidRequest.params.placementId) { - logError('Validation failed: placementId not declared'); - return false; - } - - const mediaTypesExists = hasVideoMediaType(bidRequest) || hasBannerMediaType(bidRequest); - if (!mediaTypesExists) { - return false; - } - - return true; -} - -/** - * Validates banner bid request. If it is not banner media type returns true. - * @param {object} bid, bid to validate - * @return boolean, true if valid, otherwise false - */ -function _validateBanner(bidRequest) { - // If there's no banner no need to validate - if (!hasBannerMediaType(bidRequest)) { - return true; - } - const banner = deepAccess(bidRequest, 'mediaTypes.banner'); - if (!Array.isArray(banner.sizes)) { - return false; - } - - return true; -} - -/** - * Validates video bid request. If it is not video media type returns true. - * @param {object} bid, bid to validate - * @return boolean, true if valid, otherwise false - */ -function _validateVideo(bidRequest) { - // If there's no video no need to validate - if (!hasVideoMediaType(bidRequest)) { - return true; - } - - const videoPlacement = deepAccess(bidRequest, 'mediaTypes.video', {}); - const videoBidderParams = deepAccess(bidRequest, 'params.video', {}); - const params = deepAccess(bidRequest, 'params', {}); - - if (params && params.e2etest) { - return true; - } - - const videoParams = { - ...videoPlacement, - ...videoBidderParams // Bidder Specific overrides - }; - - if (!Array.isArray(videoParams.mimes) || videoParams.mimes.length === 0) { - logError('Validation failed: mimes are invalid'); - return false; - } - - if (!Array.isArray(videoParams.protocols) || videoParams.protocols.length === 0) { - logError('Validation failed: protocols are invalid'); - return false; - } - - if (!videoParams.context) { - logError('Validation failed: context id not declared'); - return false; - } - - if (videoParams.context !== 'instream') { - logError('Validation failed: only context instream is supported '); - return false; - } - - if (typeof videoParams.playerSize === 'undefined' || !Array.isArray(videoParams.playerSize) || !Array.isArray(videoParams.playerSize[0])) { - logError('Validation failed: player size not declared or is not in format [[w,h]]'); - return false; - } - - return true; -} - -/** - * Prepares video request data. - * - * @param bidRequest - * @param bidderRequest - * @returns openrtbRequest - */ -function buildVideoRequestData(bidRequest, bidderRequest) { - const {params} = bidRequest; - - const videoAdUnit = deepAccess(bidRequest, 'mediaTypes.video', {}); - const videoBidderParams = deepAccess(bidRequest, 'params.video', {}); - - const videoParams = { - ...videoAdUnit, - ...videoBidderParams // Bidder Specific overrides - }; - - if (bidRequest.params && bidRequest.params.e2etest) { - videoParams.playerSize = [[640, 480]] - videoParams.conext = 'instream' - } - - const video = { - w: parseInt(videoParams.playerSize[0][0], 10), - h: parseInt(videoParams.playerSize[0][1], 10), - } - - // Obtain all ORTB params related video from Ad Unit - OPENRTB_VIDEO_PARAMS.forEach((param) => { - if (videoParams.hasOwnProperty(param)) { - video[param] = videoParams[param]; - } - }); - - // Placement Inference Rules: - // - If no placement is defined then default to 1 (In Stream) - video.placement = video.placement || 2; - - // - If product is instream (for instream context) then override placement to 1 - if (params.context === 'instream') { - video.startdelay = video.startdelay || 0; - video.placement = 1; - } - - // bid floor - const bidFloorRequest = { - currency: bidRequest.params.cur || 'USD', - mediaType: 'video', - size: '*' - }; - let floorData = bidRequest.params - if (isFn(bidRequest.getFloor)) { - floorData = bidRequest.getFloor(bidFloorRequest); - } else { - if (params.bidfloor) { - floorData = {floor: params.bidfloor, currency: params.currency || 'USD'}; - } - } - - const openrtbRequest = { - id: bidRequest.bidId, - imp: [ - { - id: '1', - video: video, - secure: isSecure() ? 1 : 0, - bidfloor: floorData.floor, - bidfloorcur: floorData.currency - } - ], - site: { - domain: bidderRequest.refererInfo.domain, - page: bidderRequest.refererInfo.page, - ref: bidderRequest.refererInfo.ref, - }, - ext: { - hb: 1, - prebidver: '$prebid.version$', - adapterver: spec.VERSION, - }, - }; - - // content - if (videoParams.content && isPlainObject(videoParams.content)) { - openrtbRequest.site.content = {}; - const contentStringKeys = ['id', 'title', 'series', 'season', 'genre', 'contentrating', 'language', 'url']; - const contentNumberkeys = ['episode', 'prodq', 'context', 'livestream', 'len']; - const contentArrayKeys = ['cat']; - const contentObjectKeys = ['ext']; - for (const contentKey in videoBidderParams.content) { - if ( - (contentStringKeys.indexOf(contentKey) > -1 && isStr(videoParams.content[contentKey])) || - (contentNumberkeys.indexOf(contentKey) > -1 && isNumber(videoParams.content[contentKey])) || - (contentObjectKeys.indexOf(contentKey) > -1 && isPlainObject(videoParams.content[contentKey])) || - (contentArrayKeys.indexOf(contentKey) > -1 && isArray(videoParams.content[contentKey]) && - videoParams.content[contentKey].every(catStr => isStr(catStr)))) { - openrtbRequest.site.content[contentKey] = videoParams.content[contentKey]; - } else { - logMessage('KultureMedia bid adapter validation error: ', contentKey, ' is either not supported is OpenRTB V2.5 or value is undefined'); - } - } - } - - return openrtbRequest; -} - -/** - * Prepares video request data. - * - * @param bidRequest - * @param bidderRequest - * @returns openrtbRequest - */ -function buildBannerRequestData(bidRequests, bidderRequest) { - const impr = bidRequests.map(bidRequest => ({ - id: bidRequest.bidId, - banner: { - format: bidRequest.mediaTypes.banner.sizes.map(sizeArr => ({ - w: sizeArr[0], - h: sizeArr[1] - })) - }, - ext: { - exchange: { - placementId: bidRequest.params.placementId - } - } - })); - - const openrtbRequest = { - id: bidderRequest.bidderRequestId, - imp: impr, - site: { - domain: bidderRequest.refererInfo?.domain, - page: bidderRequest.refererInfo?.page, - ref: bidderRequest.refererInfo?.ref, - }, - ext: {} - }; - return openrtbRequest; -} - -function _createBidResponse(bid) { - const isADomainPresent = - bid.adomain && bid.adomain.length; - const bidResponse = { - requestId: bid.impid, - cpm: bid.price, - width: bid.w, - height: bid.h, - ad: bid.adm, - ttl: typeof bid.exp === 'number' ? bid.exp : DEFAULT_BID_TTL, - creativeId: bid.crid, - netRevenue: DEFAULT_NET_REVENUE, - currency: DEFAULT_CURRENCY, - mediaType: deepAccess(bid, 'ext.prebid.type', BANNER) - } - - if (isADomainPresent) { - bidResponse.meta = { - advertiserDomains: bid.adomain - }; - } - - if (bidResponse.mediaType === VIDEO) { - bidResponse.vastXml = bid.adm; - } - - return bidResponse; -} - -function isSecure() { - return document.location.protocol === 'https:'; -} - -registerBidder(spec); diff --git a/modules/lemmaDigitalBidAdapter.js b/modules/lemmaDigitalBidAdapter.js index 9fa3081a47e..dde7c25d9b9 100644 --- a/modules/lemmaDigitalBidAdapter.js +++ b/modules/lemmaDigitalBidAdapter.js @@ -3,6 +3,15 @@ import { config } from '../src/config.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; import { BANNER, VIDEO } from '../src/mediaTypes.js'; +/** + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid + * @typedef {import('../src/adapters/bidderFactory.js').ServerResponse} ServerResponse + * @typedef {import('../src/adapters/bidderFactory.js').SyncOptions} SyncOptions + * @typedef {import('../src/adapters/bidderFactory.js').UserSync} UserSync + * @typedef {import('../src/adapters/bidderFactory.js').validBidRequests} validBidRequests + */ + var BIDDER_CODE = 'lemmadigital'; var LOG_WARN_PREFIX = 'LEMMADIGITAL: '; var ENDPOINT = 'https://bid.lemmadigital.com/lemma/servad'; @@ -26,7 +35,7 @@ export var spec = { * * @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'); @@ -51,11 +60,11 @@ export var spec = { }, /** - * Make a server request from the list of BidRequests. - * - * @param {validBidRequests[]} - an array of bids - * @return ServerRequest Info describing the request to the server. - **/ + * 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; @@ -79,11 +88,11 @@ export var spec = { }, /** - * 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. - **/ + * 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); }, @@ -93,7 +102,7 @@ export var spec = { * @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) { @@ -115,7 +124,7 @@ export var spec = { /** * parse object - **/ + */ _parseJSON: function (rawPayload) { try { if (rawPayload) { @@ -155,7 +164,7 @@ export var spec = { /** * create IAB standard OpenRTB bid request - **/ + */ _createoRTBRequest: (bidRequests, conf) => { var oRTBObject = {}; try { @@ -202,7 +211,7 @@ export var spec = { /** * create impression array objects - **/ + */ _getImpressionArray: (request) => { var impArray = []; var map = request.map(bid => spec._getImpressionObject(bid)); @@ -218,7 +227,7 @@ export var spec = { /** * create impression (single) object - **/ + */ _getImpressionObject: (bid) => { var impression = {}; var bObj; @@ -277,8 +286,8 @@ export var spec = { }, /** - * set bid floor - **/ + * set bid floor + */ _setFloor: (impObj, bid) => { let bidFloor = -1; // get lowest floor from floorModule @@ -304,8 +313,8 @@ export var spec = { }, /** - * parse Open RTB response - **/ + * parse Open RTB response + */ _parseRTBResponse: (request, response) => { var bidResponses = []; try { @@ -358,8 +367,8 @@ export var spec = { }, /** - * get bid request api end point url - **/ + * get bid request api end point url + */ _endPointURL: (request) => { var params = request && request[0].params ? request[0].params : null; if (params) { @@ -371,8 +380,8 @@ export var spec = { }, /** - * get domain name from url - **/ + * get domain name from url + */ _getDomain: (url) => { var a = document.createElement('a'); a.setAttribute('href', url); @@ -380,8 +389,8 @@ export var spec = { }, /** - * create the site object - **/ + * create the site object + */ _getSiteObject: (request, conf) => { var params = request && request.params ? request.params : null; if (params) { @@ -406,8 +415,8 @@ export var spec = { }, /** - * create the app object - **/ + * create the app object + */ _getAppObject: (request) => { var params = request && request.params ? request.params : null; if (params) { @@ -432,8 +441,8 @@ export var spec = { }, /** - * create the device object - **/ + * create the device object + */ _getDeviceObject: (request) => { var params = request && request.params ? request.params : null; if (params) { @@ -481,8 +490,8 @@ export var spec = { }, /** - * get request ad sizes - **/ + * get request ad sizes + */ _getSizes: (request) => { if (request && request.sizes && utils.isArray(request.sizes[0]) && request.sizes[0].length > 0) { return request.sizes[0]; @@ -491,8 +500,8 @@ export var spec = { }, /** - * create the banner object - **/ + * create the banner object + */ _getBannerRequest: (bid) => { var bObj; var adFormat = []; @@ -531,8 +540,8 @@ export var spec = { }, /** - * create the video object - **/ + * create the video object + */ _getVideoRequest: (bid) => { var vObj; if (utils.deepAccess(bid, 'mediaTypes.video')) { @@ -554,8 +563,8 @@ export var spec = { }, /** - * check media type - **/ + * check media type + */ _checkMediaType: (adm, newBid) => { // Create a regex here to check the strings var videoRegex = new RegExp(/VAST.*version/); diff --git a/modules/lifestreetBidAdapter.js b/modules/lifestreetBidAdapter.js index 6a8b783ce21..5b5eb639fcf 100644 --- a/modules/lifestreetBidAdapter.js +++ b/modules/lifestreetBidAdapter.js @@ -2,6 +2,10 @@ import { isInteger } from '../src/utils.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; import { BANNER, VIDEO } from '../src/mediaTypes.js'; +/** + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + */ + const BIDDER_CODE = 'lifestreet'; const ADAPTER_VERSION = '$prebid.version$'; diff --git a/modules/limelightDigitalBidAdapter.js b/modules/limelightDigitalBidAdapter.js index 0eb9e900160..acc76014abe 100644 --- a/modules/limelightDigitalBidAdapter.js +++ b/modules/limelightDigitalBidAdapter.js @@ -3,6 +3,12 @@ import { registerBidder } from '../src/adapters/bidderFactory.js'; import { BANNER, VIDEO } from '../src/mediaTypes.js'; import { ajax } from '../src/ajax.js'; +/** + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid + * @typedef {import('../src/adapters/bidderFactory.js').ServerResponse} ServerResponse + */ + const BIDDER_CODE = 'limelightDigital'; /** diff --git a/modules/liveIntentIdSystem.js b/modules/liveIntentIdSystem.js index 3157a4c155e..ad71a584e42 100644 --- a/modules/liveIntentIdSystem.js +++ b/modules/liveIntentIdSystem.js @@ -8,12 +8,21 @@ import { triggerPixel, logError } from '../src/utils.js'; import { ajaxBuilder } from '../src/ajax.js'; import { submodule } from '../src/hook.js'; import { LiveConnect } from 'live-connect-js'; // eslint-disable-line prebid/validate-imports -import { gdprDataHandler, uspDataHandler } from '../src/adapterManager.js'; +import { gdprDataHandler, uspDataHandler, gppDataHandler } from '../src/adapterManager.js'; import {getStorageManager} from '../src/storageManager.js'; import {MODULE_TYPE_UID} from '../src/activities/modules.js'; +import {UID2_EIDS} from '../libraries/uid2Eids/uid2Eids.js'; +/** + * @typedef {import('../modules/userId/index.js').Submodule} Submodule + * @typedef {import('../modules/userId/index.js').SubmoduleConfig} SubmoduleConfig + * @typedef {import('../modules/userId/index.js').IdResponse} IdResponse + */ + +const DEFAULT_AJAX_TIMEOUT = 5000 const EVENTS_TOPIC = 'pre_lips' const MODULE_NAME = 'liveIntentId'; +const LI_PROVIDER_DOMAIN = 'liveintent.com'; export const storage = getStorageManager({moduleType: MODULE_TYPE_UID, moduleName: MODULE_NAME}); const defaultRequestedAttributes = {'nonId': true} const calls = { @@ -64,6 +73,7 @@ function parseLiveIntentCollectorConfig(collectConfig) { collectConfig.fpiStorageStrategy && (config.storageStrategy = collectConfig.fpiStorageStrategy); collectConfig.fpiExpirationDays && (config.expirationDays = collectConfig.fpiExpirationDays); collectConfig.collectorUrl && (config.collectorUrl = collectConfig.collectorUrl); + config.ajaxTimeout = collectConfig.ajaxTimeout || DEFAULT_AJAX_TIMEOUT; return config; } @@ -98,9 +108,8 @@ function initializeLiveConnect(configParams) { if (configParams.url) { identityResolutionConfig.url = configParams.url } - if (configParams.ajaxTimeout) { - identityResolutionConfig.ajaxTimeout = configParams.ajaxTimeout; - } + + identityResolutionConfig.ajaxTimeout = configParams.ajaxTimeout || DEFAULT_AJAX_TIMEOUT; const liveConnectConfig = parseLiveIntentCollectorConfig(configParams.liCollectConfig); @@ -112,6 +121,7 @@ function initializeLiveConnect(configParams) { } liveConnectConfig.wrapperName = 'prebid'; + liveConnectConfig.trackerVersion = '$prebid.version$'; liveConnectConfig.identityResolutionConfig = identityResolutionConfig; liveConnectConfig.identifiersToResolve = configParams.identifiersToResolve || []; liveConnectConfig.fireEventDelay = configParams.fireEventDelay; @@ -124,7 +134,11 @@ function initializeLiveConnect(configParams) { liveConnectConfig.gdprApplies = gdprConsent.gdprApplies; liveConnectConfig.gdprConsent = gdprConsent.consentString; } - + const gppConsent = gppDataHandler.getConsentData(); + if (gppConsent) { + liveConnectConfig.gppString = gppConsent.gppString; + liveConnectConfig.gppApplicableSections = gppConsent.applicableSections; + } // The second param is the storage object, LS & Cookie manipulation uses PBJS // The third param is the ajax and pixel object, the ajax and pixel use PBJS liveConnect = liveIntentIdSubmodule.getInitializer()(liveConnectConfig, storage, calls); @@ -149,7 +163,7 @@ function tryFireEvent() { /** @type {Submodule} */ export const liveIntentIdSubmodule = { - moduleMode: process.env.LiveConnectMode, + moduleMode: '$$LIVE_INTENT_MODULE_MODE$$', /** * used to link submodule with config * @type {string} @@ -189,15 +203,35 @@ export const liveIntentIdSubmodule = { // As adapters are applied in lexicographical order, we will always // be overwritten by the 'proper' uid2 module if it is present. if (value.uid2) { - result.uid2 = { 'id': value.uid2 } + result.uid2 = { 'id': value.uid2, ext: { provider: LI_PROVIDER_DOMAIN } } } if (value.bidswitch) { - result.bidswitch = { 'id': value.bidswitch } + result.bidswitch = { 'id': value.bidswitch, ext: { provider: LI_PROVIDER_DOMAIN } } } if (value.medianet) { - result.medianet = { 'id': value.medianet } + result.medianet = { 'id': value.medianet, ext: { provider: LI_PROVIDER_DOMAIN } } + } + + if (value.magnite) { + result.magnite = { 'id': value.magnite, ext: { provider: LI_PROVIDER_DOMAIN } } + } + + if (value.index) { + result.index = { 'id': value.index, ext: { provider: LI_PROVIDER_DOMAIN } } + } + + if (value.openx) { + result.openx = { 'id': value.openx, ext: { provider: LI_PROVIDER_DOMAIN } } + } + + if (value.pubmatic) { + result.pubmatic = { 'id': value.pubmatic, ext: { provider: LI_PROVIDER_DOMAIN } } + } + + if (value.sovrn) { + result.sovrn = { 'id': value.sovrn, ext: { provider: LI_PROVIDER_DOMAIN } } } return result @@ -237,6 +271,107 @@ export const liveIntentIdSubmodule = { } return { callback: result }; + }, + eids: { + ...UID2_EIDS, + 'lipb': { + getValue: function(data) { + return data.lipbid; + }, + source: 'liveintent.com', + atype: 3, + getEidExt: function(data) { + if (Array.isArray(data.segments) && data.segments.length) { + return { + segments: data.segments + }; + } + } + }, + 'bidswitch': { + source: 'bidswitch.net', + atype: 3, + getValue: function(data) { + return data.id; + }, + getUidExt: function(data) { + if (data.ext) { + return data.ext; + } + } + }, + 'medianet': { + source: 'media.net', + atype: 3, + getValue: function(data) { + return data.id; + }, + getUidExt: function(data) { + if (data.ext) { + return data.ext; + } + } + }, + 'magnite': { + source: 'rubiconproject.com', + atype: 3, + getValue: function(data) { + return data.id; + }, + getUidExt: function(data) { + if (data.ext) { + return data.ext; + } + } + }, + 'index': { + source: 'liveintent.indexexchange.com', + atype: 3, + getValue: function(data) { + return data.id; + }, + getUidExt: function(data) { + if (data.ext) { + return data.ext; + } + } + }, + 'openx': { + source: 'openx.net', + atype: 3, + getValue: function(data) { + return data.id; + }, + getUidExt: function(data) { + if (data.ext) { + return data.ext; + } + } + }, + 'pubmatic': { + source: 'pubmatic.com', + atype: 3, + getValue: function(data) { + return data.id; + }, + getUidExt: function(data) { + if (data.ext) { + return data.ext; + } + } + }, + 'sovrn': { + source: 'liveintent.sovrn.com', + atype: 3, + getValue: function(data) { + return data.id; + }, + getUidExt: function(data) { + if (data.ext) { + return data.ext; + } + } + } } }; diff --git a/modules/livewrappedAnalyticsAdapter.js b/modules/livewrappedAnalyticsAdapter.js index fe69220e123..f3ee81cae7a 100644 --- a/modules/livewrappedAnalyticsAdapter.js +++ b/modules/livewrappedAnalyticsAdapter.js @@ -168,7 +168,7 @@ livewrappedAnalyticsAdapter.sendEvents = function() { requests: sentRequests.sentRequests, responses: getResponses(sentRequests.gdpr, sentRequests.auctionIds), wins: getWins(sentRequests.gdpr, sentRequests.auctionIds), - timeouts: getTimeouts(sentRequests.auctionIds), + timeouts: getTimeouts(sentRequests.gdpr, sentRequests.auctionIds), bidAdUnits: getbidAdUnits(), rf: getAdRenderFailed(sentRequests.auctionIds), rcv: getAdblockerRecovered() @@ -237,27 +237,9 @@ function getResponses(gdpr, auctionIds) { if (bid.readyToSend && !(bid.sendStatus & RESPONSESENT) && !bid.timeout) { bid.sendStatus |= RESPONSESENT; - responses.push({ - timeStamp: auction.timeStamp, - adUnit: bid.adUnit, - adUnitId: bid.adUnitId, - bidder: bid.bidder, - width: bid.width, - height: bid.height, - cpm: bid.cpm, - orgCpm: bid.originalCpm, - ttr: bid.ttr, - IsBid: bid.isBid, - mediaType: bid.mediaType, - gdpr: gdprPos, - floor: bid.lwFloor ? bid.lwFloor : (bid.floorData ? bid.floorData.floorValue : undefined), - floorCur: bid.floorData ? bid.floorData.floorCurrency : undefined, - auctionId: auctionIdPos, - auc: bid.auc, - buc: bid.buc, - lw: bid.lw, - meta: bid.meta - }); + let response = getResponseObject(auction, bid, gdprPos, auctionIdPos); + + responses.push(response); } }); }); @@ -337,27 +319,45 @@ function getAuctionIdPos(auctionIds, auctionId) { return auctionIdPos; } -function getTimeouts(auctionIds) { +function getResponseObject(auction, bid, gdprPos, auctionIdPos) { + return { + timeStamp: auction.timeStamp, + adUnit: bid.adUnit, + adUnitId: bid.adUnitId, + bidder: bid.bidder, + width: bid.width, + height: bid.height, + cpm: bid.cpm, + orgCpm: bid.originalCpm, + ttr: bid.ttr, + IsBid: bid.isBid, + mediaType: bid.mediaType, + gdpr: gdprPos, + floor: bid.lwFloor ? bid.lwFloor : (bid.floorData ? bid.floorData.floorValue : undefined), + floorCur: bid.floorData ? bid.floorData.floorCurrency : undefined, + auctionId: auctionIdPos, + auc: bid.auc, + buc: bid.buc, + lw: bid.lw, + meta: bid.meta + }; +} + +function getTimeouts(gdpr, auctionIds) { var timeouts = []; Object.keys(cache.auctions).forEach(auctionId => { let auctionIdPos = getAuctionIdPos(auctionIds, auctionId); Object.keys(cache.auctions[auctionId].bids).forEach(bidId => { let auction = cache.auctions[auctionId]; + let gdprPos = getGdprPos(gdpr, auction); let bid = auction.bids[bidId]; if (!(bid.sendStatus & TIMEOUTSENT) && bid.timeout) { bid.sendStatus |= TIMEOUTSENT; - timeouts.push({ - bidder: bid.bidder, - adUnit: bid.adUnit, - adUnitId: bid.adUnitId, - timeStamp: auction.timeStamp, - auctionId: auctionIdPos, - auc: bid.auc, - buc: bid.buc, - lw: bid.lw - }); + let timeout = getResponseObject(auction, bid, gdprPos, auctionIdPos); + + timeouts.push(timeout); } }); }); diff --git a/modules/livewrappedBidAdapter.js b/modules/livewrappedBidAdapter.js index 82affe40e03..cf1e690d862 100644 --- a/modules/livewrappedBidAdapter.js +++ b/modules/livewrappedBidAdapter.js @@ -6,6 +6,11 @@ import {BANNER, NATIVE, VIDEO} from '../src/mediaTypes.js'; import {getStorageManager} from '../src/storageManager.js'; import { convertOrtbRequestToProprietaryNative } from '../src/native.js'; +/** + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid + */ + const BIDDER_CODE = 'livewrapped'; export const storage = getStorageManager({bidderCode: BIDDER_CODE}); export const URL = 'https://lwadm.com/ad'; diff --git a/modules/lm_kiviadsBidAdapter.js b/modules/lm_kiviadsBidAdapter.js new file mode 100644 index 00000000000..7c3085047c4 --- /dev/null +++ b/modules/lm_kiviadsBidAdapter.js @@ -0,0 +1,215 @@ +import {config} from '../src/config.js'; +import {BANNER, VIDEO} from '../src/mediaTypes.js'; +import {registerBidder} from '../src/adapters/bidderFactory.js'; +import {parseSizesInput, isFn, deepAccess, getBidIdParameter, logError, isArray} from '../src/utils.js'; +import {getAdUnitSizes} from '../libraries/sizeUtils/sizeUtils.js'; + +/** + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid + * @typedef {import('../src/adapters/bidderFactory.js').ServerResponse} ServerResponse + * @typedef {import('../src/adapters/bidderFactory.js').SyncOptions} SyncOptions + * @typedef {import('../src/adapters/bidderFactory.js').UserSync} UserSync + */ + +const CUR = 'USD'; +const BIDDER_CODE = 'lm_kiviads'; +const ENDPOINT = 'https://pbjs.kiviads.live'; + +/** + * Determines whether or not the given bid request is valid. + * + * @param {BidRequest} bid The bid params to validate. + * @return boolean True if this is a valid bid, and false otherwise. + */ +function isBidRequestValid(req) { + if (req && typeof req.params !== 'object') { + logError('Params is not defined or is incorrect in the bidder settings'); + return false; + } + + if (!getBidIdParameter('env', req.params) || !getBidIdParameter('pid', req.params)) { + logError('Env or pid is not present in bidder params'); + return false; + } + + if (deepAccess(req, 'mediaTypes.video') && !isArray(deepAccess(req, 'mediaTypes.video.playerSize'))) { + logError('mediaTypes.video.playerSize is required for video'); + return false; + } + + return true; +} + +/** + * Make a server request from the list of BidRequests. + * + * @param {validBidRequest?pbjs_debug=trues[]} - an array of bids + * @return ServerRequest Info describing the request to the server. + */ +function buildRequests(validBidRequests, bidderRequest) { + const {refererInfo = {}, gdprConsent = {}, uspConsent} = bidderRequest; + const requests = validBidRequests.map(req => { + const request = {}; + request.bidId = req.bidId; + request.banner = deepAccess(req, 'mediaTypes.banner'); + request.auctionId = req.ortb2?.source?.tid; + request.transactionId = req.ortb2Imp?.ext?.tid; + request.sizes = parseSizesInput(getAdUnitSizes(req)); + request.schain = req.schain; + request.location = { + page: refererInfo.page, + location: refererInfo.location, + domain: refererInfo.domain, + whost: window.location.host, + ref: refererInfo.ref, + isAmp: refererInfo.isAmp + }; + request.device = { + ua: navigator.userAgent, + lang: navigator.language + }; + request.env = { + env: req.params.env, + pid: req.params.pid + }; + request.ortb2 = req.ortb2; + request.ortb2Imp = req.ortb2Imp; + request.tz = new Date().getTimezoneOffset(); + request.ext = req.params.ext; + request.bc = req.bidRequestsCount; + request.floor = getBidFloor(req); + + if (req.userIdAsEids && req.userIdAsEids.length !== 0) { + request.userEids = req.userIdAsEids; + } else { + request.userEids = []; + } + if (gdprConsent.gdprApplies) { + request.gdprApplies = Number(gdprConsent.gdprApplies); + request.consentString = gdprConsent.consentString; + } else { + request.gdprApplies = 0; + request.consentString = ''; + } + if (uspConsent) { + request.usPrivacy = uspConsent; + } else { + request.usPrivacy = ''; + } + if (config.getConfig('coppa')) { + request.coppa = 1; + } else { + request.coppa = 0; + } + + const video = deepAccess(req, 'mediaTypes.video'); + if (video) { + request.sizes = parseSizesInput(deepAccess(req, 'mediaTypes.video.playerSize')); + request.video = video; + } + + return request; + }); + + return { + method: 'POST', + url: ENDPOINT + '/bid', + data: JSON.stringify(requests), + withCredentials: true, + bidderRequest, + options: { + contentType: 'application/json', + } + }; +} + +/** + * Unpack the response from the server into a list of bids. + * + * @param {ServerResponse} serverResponse A successful response from the server. + * @return {Bid[]} An array of bids which were nested inside the server. + */ +function interpretResponse(serverResponse, {bidderRequest}) { + const response = []; + if (!isArray(deepAccess(serverResponse, 'body.data'))) { + return response; + } + + serverResponse.body.data.forEach(serverBid => { + const bid = { + requestId: bidderRequest.bidId, + dealId: bidderRequest.dealId || null, + ...serverBid + }; + response.push(bid); + }); + + return response; +} + +/** + * Register the user sync pixels which should be dropped after the auction. + * + * @param {SyncOptions} syncOptions Which user syncs are allowed? + * @param {ServerResponse[]} serverResponses List of server's responses. + * @return {UserSync[]} The user syncs which should be dropped. + */ +function getUserSyncs(syncOptions, serverResponses, gdprConsent = {}, uspConsent = '') { + const syncs = []; + const pixels = deepAccess(serverResponses, '0.body.data.0.ext.pixels'); + + if ((syncOptions.iframeEnabled || syncOptions.pixelEnabled) && isArray(pixels) && pixels.length !== 0) { + const gdprFlag = `&gdpr=${gdprConsent.gdprApplies ? 1 : 0}`; + const gdprString = `&gdpr_consent=${encodeURIComponent((gdprConsent.consentString || ''))}`; + const usPrivacy = `us_privacy=${encodeURIComponent(uspConsent)}`; + + pixels.forEach(pixel => { + const [type, url] = pixel; + const sync = {type, url: `${url}&${usPrivacy}${gdprFlag}${gdprString}`}; + if (type === 'iframe' && syncOptions.iframeEnabled) { + syncs.push(sync) + } else if (type === 'image' && syncOptions.pixelEnabled) { + syncs.push(sync) + } + }); + } + + return syncs; +} + +/** + * Get valid floor value from getFloor fuction. + * + * @param {Object} bid Current bid request. + * @return {null|Number} Returns floor value when bid.getFloor is function and returns valid floor object with USD currency, otherwise returns null. + */ +export function getBidFloor(bid) { + if (!isFn(bid.getFloor)) { + return null; + } + + let floor = bid.getFloor({ + currency: CUR, + mediaType: '*', + size: '*' + }); + + if (typeof floor === 'object' && !isNaN(floor.floor) && floor.currency === CUR) { + return floor.floor; + } + + return null; +} + +export const spec = { + code: BIDDER_CODE, + aliases: ['kivi'], + supportedMediaTypes: [BANNER, VIDEO], + isBidRequestValid, + buildRequests, + interpretResponse, + getUserSyncs +} + +registerBidder(spec); diff --git a/modules/lm_kiviadsBidAdapter.md b/modules/lm_kiviadsBidAdapter.md new file mode 100644 index 00000000000..fc1b05d1ef7 --- /dev/null +++ b/modules/lm_kiviadsBidAdapter.md @@ -0,0 +1,54 @@ +# Overview + +``` +Module Name: lm_kiviads Bidder Adapter +Module Type: lm_kiviads Bidder Adapter +Maintainer: pavlo@xe.works +``` + +# Description + +Module that connects to kiviads.com demand sources + +# Test Parameters +``` +var adUnits = [ + { + code: 'test-banner', + mediaTypes: { + banner: { + sizes: [[300, 250]], + } + }, + bids: [ + { + bidder: 'lm_kiviads', + params: { + env: 'lm_kiviads', + pid: '40', + ext: {} + } + } + ] + }, + { + code: 'test-video', + sizes: [ [ 640, 480 ] ], + mediaTypes: { + video: { + playerSize: [640, 480], + context: 'instream', + skipppable: true + } + }, + bids: [{ + bidder: 'lm_kiviads', + params: { + env: 'lm_kiviads', + pid: '40', + ext: {} + } + }] + } +]; +``` diff --git a/modules/lockerdomeBidAdapter.js b/modules/lockerdomeBidAdapter.js index 5c38753c1e2..5038eadce30 100644 --- a/modules/lockerdomeBidAdapter.js +++ b/modules/lockerdomeBidAdapter.js @@ -1,6 +1,6 @@ -import { getBidIdParameter } from '../src/utils.js'; import {BANNER} from '../src/mediaTypes.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; +import {getBidIdParameter} from '../src/utils.js'; export const spec = { code: 'lockerdome', diff --git a/modules/lotamePanoramaIdSystem.js b/modules/lotamePanoramaIdSystem.js index 02b01b8bd9d..64d631c2469 100644 --- a/modules/lotamePanoramaIdSystem.js +++ b/modules/lotamePanoramaIdSystem.js @@ -20,6 +20,13 @@ import {getStorageManager} from '../src/storageManager.js'; import { uspDataHandler } from '../src/adapterManager.js'; import {MODULE_TYPE_UID} from '../src/activities/modules.js'; +/** + * @typedef {import('../modules/userId/index.js').Submodule} Submodule + * @typedef {import('../modules/userId/index.js').SubmoduleConfig} SubmoduleConfig + * @typedef {import('../modules/userId/index.js').ConsentData} ConsentData + * @typedef {import('../modules/userId/index.js').IdResponse} IdResponse + */ + const KEY_ID = 'panoramaId'; const KEY_EXPIRY = `${KEY_ID}_expiry`; const KEY_PROFILE = '_cc_id'; @@ -368,6 +375,12 @@ export const lotamePanoramaIdSubmodule = { return { callback: resolveIdFunction }; }, + eids: { + lotamePanoramaId: { + source: 'crwdcntrl.net', + atype: 1, + }, + }, }; submodule('userId', lotamePanoramaIdSubmodule); diff --git a/modules/madvertiseBidAdapter.js b/modules/madvertiseBidAdapter.js index 457ff2409b8..3b031623aef 100644 --- a/modules/madvertiseBidAdapter.js +++ b/modules/madvertiseBidAdapter.js @@ -2,6 +2,11 @@ import { parseSizesInput, _each } from '../src/utils.js'; import {config} from '../src/config.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; +/** + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid + */ + // use protocol relative urls for http or https const MADVERTISE_ENDPOINT = 'https://mobile.mng-ads.com/'; diff --git a/modules/magniteAnalyticsAdapter.js b/modules/magniteAnalyticsAdapter.js index b501e3bef5a..b9665b93494 100644 --- a/modules/magniteAnalyticsAdapter.js +++ b/modules/magniteAnalyticsAdapter.js @@ -142,6 +142,10 @@ const sendEvent = payload => { ...getTopLevelDetails(), ...payload } + if (window.pbjs?.rp?.eventDispatcher) { + const analyticsEvent = new CustomEvent('beforeSendingMagniteAnalytics', { detail: event }); + window.pbjs.rp.eventDispatcher.dispatchEvent(analyticsEvent); + } ajax( endpoint, null, diff --git a/modules/malltvBidAdapter.js b/modules/malltvBidAdapter.js index 5ac50936ed6..67c8a4aec07 100644 --- a/modules/malltvBidAdapter.js +++ b/modules/malltvBidAdapter.js @@ -2,6 +2,13 @@ import { registerBidder } from '../src/adapters/bidderFactory.js'; import { getStorageManager } from '../src/storageManager.js'; import { BANNER, VIDEO } from '../src/mediaTypes.js'; +/** + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid + * @typedef {import('../src/adapters/bidderFactory.js').ServerResponse} ServerResponse + * @typedef {import('../src/adapters/bidderFactory.js').validBidRequests} validBidRequests + */ + const BIDDER_CODE = 'malltv'; const ENDPOINT_URL = 'https://central.mall.tv/bid'; const DIMENSION_SEPARATOR = 'x'; @@ -124,11 +131,11 @@ export const spec = { }; /** -* Generate size param for bid request using sizes array -* -* @param {Array} sizes Possible sizes for the ad unit. -* @return {string} Processed sizes param to be used for the bid request. -*/ + * Generate size param for bid request using sizes array + * + * @param {Array} sizes Possible sizes for the ad unit. + * @return {string} Processed sizes param to be used for the bid request. + */ function generateSizeParam(sizes) { return sizes.map(size => size.join(DIMENSION_SEPARATOR)).join(SIZE_SEPARATOR); } diff --git a/modules/mediabramaBidAdapter.js b/modules/mediabramaBidAdapter.js new file mode 100644 index 00000000000..caf6854fe03 --- /dev/null +++ b/modules/mediabramaBidAdapter.js @@ -0,0 +1,155 @@ +import { + isFn, + isStr, + deepAccess, + getWindowTop, + triggerPixel +} from '../src/utils.js'; +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { BANNER } from '../src/mediaTypes.js'; +import { config } from '../src/config.js'; + +const BIDDER_CODE = 'mediabrama'; +const AD_URL = 'https://prebid.mediabrama.com/pbjs'; +const SYNC_URL = 'https://prebid.mediabrama.com/sync'; + +function isBidResponseValid(bid) { + if (!bid.requestId || !bid.cpm || !bid.creativeId || + !bid.ttl || !bid.currency || !bid.meta) { + return false; + } + + switch (bid.mediaType) { + case BANNER: + return Boolean(bid.width && bid.height && bid.ad); + default: + return false; + } +} + +function getBidFloor(bid) { + if (!isFn(bid.getFloor)) { + return deepAccess(bid, 'params.bidFloor', 0); + } + + try { + const bidFloor = bid.getFloor({ + currency: 'USD', + mediaType: '*', + size: '*', + }); + return bidFloor.floor; + } catch (_) { + return 0 + } +} + +export const spec = { + code: BIDDER_CODE, + supportedMediaTypes: [BANNER], + + isBidRequestValid: (bid) => { + return Boolean(bid.bidId && bid.params && bid.params.placementId); + }, + + buildRequests: (validBidRequests = [], bidderRequest) => { + const winTop = getWindowTop(); + const location = winTop.location; + const placements = []; + + const request = { + deviceWidth: winTop.screen.width, + deviceHeight: winTop.screen.height, + language: (navigator && navigator.language) ? navigator.language.split('-')[0] : '', + host: location.host, + page: location.pathname, + placements: placements + }; + + if (bidderRequest) { + if (bidderRequest.uspConsent) { + request.ccpa = bidderRequest.uspConsent; + } + if (bidderRequest.gdprConsent) { + request.gdpr = bidderRequest.gdprConsent; + } + } + + const len = validBidRequests.length; + for (let i = 0; i < len; i++) { + const bid = validBidRequests[i]; + const placement = { + placementId: bid.params.placementId, + bidId: bid.bidId, + schain: bid.schain || {}, + bidfloor: getBidFloor(bid) + }; + + if (typeof bid.userId !== 'undefined') { + placement.userId = bid.userId; + } + + const mediaType = bid.mediaTypes; + + if (mediaType && mediaType[BANNER] && mediaType[BANNER].sizes) { + placement.sizes = mediaType[BANNER].sizes; + placement.adFormat = BANNER; + } + + placements.push(placement); + } + + return { + method: 'POST', + url: AD_URL, + data: request + }; + }, + + interpretResponse: (serverResponse) => { + let response = []; + for (let i = 0; i < serverResponse.body.length; i++) { + let resItem = serverResponse.body[i]; + if (isBidResponseValid(resItem)) { + const advertiserDomains = resItem.adomain && resItem.adomain.length ? resItem.adomain : []; + resItem.meta = { ...resItem.meta, advertiserDomains }; + + response.push(resItem); + } + } + return response; + }, + + getUserSyncs: (syncOptions, serverResponses, gdprConsent, uspConsent) => { + let syncType = syncOptions.iframeEnabled ? 'iframe' : 'image'; + let syncUrl = SYNC_URL + `/${syncType}?pbjs=1`; + if (gdprConsent && gdprConsent.consentString) { + if (typeof gdprConsent.gdprApplies === 'boolean') { + syncUrl += `&gdpr=${Number(gdprConsent.gdprApplies)}&gdpr_consent=${gdprConsent.consentString}`; + } else { + syncUrl += `&gdpr=0&gdpr_consent=${gdprConsent.consentString}`; + } + } + if (uspConsent && uspConsent.consentString) { + syncUrl += `&ccpa_consent=${uspConsent.consentString}`; + } + + const coppa = config.getConfig('coppa') ? 1 : 0; + syncUrl += `&coppa=${coppa}`; + + return [{ + type: syncType, + url: syncUrl + }]; + }, + + onBidWon: (bid) => { + const cpm = deepAccess(bid, 'adserverTargeting.hb_pb') || ''; + if (isStr(bid.nurl) && bid.nurl !== '') { + bid.nurl = bid.nurl.replace(/\${AUCTION_PRICE}/, cpm); + triggerPixel(bid.nurl); + } + } +}; + +registerBidder(spec); diff --git a/modules/mediabramaBidAdapter.md b/modules/mediabramaBidAdapter.md new file mode 100644 index 00000000000..fde0a399852 --- /dev/null +++ b/modules/mediabramaBidAdapter.md @@ -0,0 +1,33 @@ +# Overview + +``` +Module Name: MediaBrama Bidder Adapter +Module Type: MediaBrama Bidder Adapter +Maintainer: support@mediabrama.com +``` + +# Description + +Module that connects to mediabrama demand sources + +# Test Parameters +``` + var adUnits = [ + { + code: 'div-prebid', + mediaTypes:{ + banner: { + sizes: [[300, 250]], + } + }, + bids:[ + { + bidder: 'mediabrama', + params: { + placementId: '24428' //test, please replace after test + } + } + ] + }, + ]; +``` diff --git a/modules/mediafilterRtdProvider.js b/modules/mediafilterRtdProvider.js new file mode 100644 index 00000000000..8a082ad4d59 --- /dev/null +++ b/modules/mediafilterRtdProvider.js @@ -0,0 +1,94 @@ +/** + * This module adds the Media Filter real-time ad monitoring and protection module. + * + * The {@link module:modules/realTimeData} module is required + * + * For more information, visit {@link https://www.themediatrust.com The Media Trust}. + * + * @author Mirnes Cajlakovic + * @module modules/mediafilterRtdProvider + * @requires module:modules/realTimeData + */ + +import { submodule } from '../src/hook.js'; +import { logError, generateUUID } from '../src/utils.js'; +import { loadExternalScript } from '../src/adloader.js'; +import * as events from '../src/events.js'; +import CONSTANTS from '../src/constants.json'; + +/** The event type for Media Filter. */ +export const MEDIAFILTER_EVENT_TYPE = 'com.mediatrust.pbjs.'; +/** The base URL for Media Filter scripts. */ +export const MEDIAFILTER_BASE_URL = 'https://scripts.webcontentassessor.com/scripts/'; + +export const MediaFilter = { + /** + * Registers the Media Filter as a submodule of real-time data. + */ + register: function() { + submodule('realTimeData', { + 'name': 'mediafilter', + 'init': this.generateInitHandler() + }); + }, + + /** + * Sets up the Media Filter by initializing event listeners and loading the external script. + * @param {object} configuration - The configuration object. + */ + setup: function(configuration) { + this.setupEventListener(configuration.configurationHash); + this.setupScript(configuration.configurationHash); + }, + + /** + * Sets up an event listener for Media Filter messages. + * @param {string} configurationHash - The configuration hash. + */ + setupEventListener: function(configurationHash) { + window.addEventListener('message', this.generateEventHandler(configurationHash)); + }, + + /** + * Loads the Media Filter script based on the provided configuration hash. + * @param {string} configurationHash - The configuration hash. + */ + setupScript: function(configurationHash) { + loadExternalScript(MEDIAFILTER_BASE_URL.concat(configurationHash), 'mediafilter', () => {}); + }, + + /** + * Generates an event handler for Media Filter messages. + * @param {string} configurationHash - The configuration hash. + * @returns {function} The generated event handler. + */ + generateEventHandler: function(configurationHash) { + return (windowEvent) => { + if (windowEvent.data.type === MEDIAFILTER_EVENT_TYPE.concat('.', configurationHash)) { + events.emit(CONSTANTS.EVENTS.BILLABLE_EVENT, { + 'billingId': generateUUID(), + 'configurationHash': configurationHash, + 'type': 'impression', + 'vendor': 'mediafilter', + }); + } + }; + }, + + /** + * Generates an initialization handler for Media Filter. + * @returns {function} The generated init handler. + */ + generateInitHandler: function() { + return (configuration) => { + try { + this.setup(configuration); + } catch (error) { + logError(`Error in initialization: ${error.message}`); + } + }; + } +}; + +// Register the module +MediaFilter.register(); diff --git a/modules/mediafilterRtdProvider.md b/modules/mediafilterRtdProvider.md new file mode 100644 index 00000000000..469479f8d0b --- /dev/null +++ b/modules/mediafilterRtdProvider.md @@ -0,0 +1,37 @@ +## Overview + +**Module:** The Media Filter +**Type: **Real Time Data Module + +As malvertising, scams, and controversial and offensive ad content proliferate across the digital media ecosystem, publishers need advanced controls to both shield audiences from malware attacks and ensure quality site experience. With the market’s fastest and most comprehensive real-time ad quality tool, The Media Trust empowers publisher Ad/Revenue Operations teams to block a wide range of malware, high-risk ad platforms, heavy ads, ads with sensitive or objectionable content, and custom lists (e.g., competitors). Customizable replacement code calls for a new ad to ensure impressions are still monetized. + +[![IMAGE ALT TEXT](http://img.youtube.com/vi/VBHRiirge7s/0.jpg)](http://www.youtube.com/watch?v=VBHRiirge7s "Publishers' Ultimate Avenger: Media Filter") + +To start using this module, please contact [The Media Trust](https://mediatrust.com/how-we-help/media-filter/ "The Media Trust") to get a script and configuration hash for module configuration. + +## Integration + +1. Build Prebid bundle with The Media Filter module included. + +``` +gulp build --modules=mediafilterRtdProvider +``` + +2. Inlcude the bundled script in your application. + +## Configuration + +Add configuration entry to `realTimeData.dataProviders` for The Media Filter module. + +``` +pbjs.setConfig({ + realTimeData: { + dataProviders: [{ + name: 'mediafilter', + params: { + configurationHash: '', + } + }] + } +}); +``` diff --git a/modules/mediaforceBidAdapter.js b/modules/mediaforceBidAdapter.js index 3d33bbf8c12..9f899974721 100644 --- a/modules/mediaforceBidAdapter.js +++ b/modules/mediaforceBidAdapter.js @@ -3,6 +3,12 @@ import {registerBidder} from '../src/adapters/bidderFactory.js'; import {BANNER, NATIVE} from '../src/mediaTypes.js'; import { convertOrtbRequestToProprietaryNative } from '../src/native.js'; +/** + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid + * @typedef {import('../src/adapters/bidderFactory.js').ServerResponse} ServerResponse + */ + const BIDDER_CODE = 'mediaforce'; const ENDPOINT_URL = 'https://rtb.mfadsrvr.com/header_bid'; const TEST_ENDPOINT_URL = 'https://rtb.mfadsrvr.com/header_bid?debug_key=abcdefghijklmnop'; diff --git a/modules/mediafuseBidAdapter.js b/modules/mediafuseBidAdapter.js index 98179c49e0d..5e7221583a8 100644 --- a/modules/mediafuseBidAdapter.js +++ b/modules/mediafuseBidAdapter.js @@ -1,14 +1,8 @@ import { - chunk, - convertCamelToUnderscore, - convertTypes, createTrackPixelHtml, deepAccess, deepClone, - fill, getBidRequest, - getMaxValueFromArray, - getMinValueFromArray, getParameterByName, isArray, isArrayOfNums, @@ -38,7 +32,15 @@ import { getANKewyordParamFromMaps, getANKeywordParam, transformBidderParamKeywords -} from '../libraries/appnexusKeywords/anKeywords.js'; +} from '../libraries/appnexusUtils/anKeywords.js'; +import {convertCamelToUnderscore, fill} from '../libraries/appnexusUtils/anUtils.js'; +import {convertTypes} from '../libraries/transformParamsUtils/convertTypes.js'; +import {chunk} from '../libraries/chunk/chunk.js'; + +/** + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid + */ const BIDDER_CODE = 'mediafuse'; const URL = 'https://ib.adnxs.com/ut/v3/prebid'; @@ -959,7 +961,7 @@ function createAdPodRequest(tags, adPodBid) { const { durationRangeSec, requireExactDuration } = adPodBid.mediaTypes.video; const numberOfPlacements = getAdPodPlacementNumber(adPodBid.mediaTypes.video); - const maxDuration = getMaxValueFromArray(durationRangeSec); + const maxDuration = Math.max(...durationRangeSec); const tagToDuplicate = tags.filter(tag => tag.uuid === adPodBid.bidId); let request = fill(...tagToDuplicate, numberOfPlacements); @@ -985,7 +987,7 @@ function createAdPodRequest(tags, adPodBid) { function getAdPodPlacementNumber(videoParams) { const { adPodDurationSec, durationRangeSec, requireExactDuration } = videoParams; - const minAllowedDuration = getMinValueFromArray(durationRangeSec); + const minAllowedDuration = Math.min(...durationRangeSec); const numberOfPlacements = Math.floor(adPodDurationSec / minAllowedDuration); return requireExactDuration diff --git a/modules/mediagoBidAdapter.js b/modules/mediagoBidAdapter.js index 055fef3bb39..8f687d30ff3 100644 --- a/modules/mediagoBidAdapter.js +++ b/modules/mediagoBidAdapter.js @@ -8,39 +8,115 @@ import { registerBidder } from '../src/adapters/bidderFactory.js'; // import { config } from '../src/config.js'; // import { isPubcidEnabled } from './pubCommonId.js'; +/** + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid + * @typedef {import('../src/adapters/bidderFactory.js').ServerResponse} ServerResponse + * @typedef {import('../src/adapters/bidderFactory.js').mediaType} mediaType + */ + const BIDDER_CODE = 'mediago'; // const PROTOCOL = window.document.location.protocol; -const ENDPOINT_URL = - // ((PROTOCOL === 'https:') ? 'https' : 'http') + - 'https://rtb-us.mediago.io/api/bid?tn='; +const ENDPOINT_URL = 'https://gbid.mediago.io/api/bid?tn='; +// const COOKY_SYNC_URL = 'https://gtrace.mediago.io/ju/cs/eplist'; +const COOKY_SYNC_IFRAME_URL = 'https://cdn.mediago.io/js/cookieSync.html'; +export const THIRD_PARTY_COOKIE_ORIGIN = 'https://cdn.mediago.io'; + const TIME_TO_LIVE = 500; +const GVLID = 1020; // const ENDPOINT_URL = '/api/bid?tn='; -const storage = getStorageManager({bidderCode: BIDDER_CODE}); +export const storage = getStorageManager({bidderCode: BIDDER_CODE}); let globals = {}; let itemMaps = {}; /* ----- mguid:start ------ */ -const COOKIE_KEY_MGUID = '__mguid_'; +export const COOKIE_KEY_MGUID = '__mguid_'; +const COOKIE_KEY_PMGUID = '__pmguid_'; +const COOKIE_RETENTION_TIME = 365 * 24 * 60 * 60 * 1000; // 1 year +let reqTimes = 0; +/** + * get page title + * @returns {string} + */ + +export function getPageTitle(win = window) { + try { + const ogTitle = win.top.document.querySelector('meta[property="og:title"]') + return win.top.document.title || (ogTitle && ogTitle.content) || ''; + } catch (e) { + const ogTitle = document.querySelector('meta[property="og:title"]') + return document.title || (ogTitle && ogTitle.content) || ''; + } +} /** - * čŽˇå–į”¨æˆˇid - * @return {string} + * get page description + * + * @returns {string} + */ +export function getPageDescription(win = window) { + let element; + + try { + element = win.top.document.querySelector('meta[name="description"]') || + win.top.document.querySelector('meta[property="og:description"]') + } catch (e) { + element = document.querySelector('meta[name="description"]') || + document.querySelector('meta[property="og:description"]') + } + + return (element && element.content) || ''; +} + +/** + * get page keywords + * @returns {string} */ -const getUserID = () => { - const i = storage.getCookie(COOKIE_KEY_MGUID); +export function getPageKeywords(win = window) { + let element; - if (i === null) { - const uuid = utils.generateUUID(); - storage.setCookie(COOKIE_KEY_MGUID, uuid); - return uuid; + try { + element = win.top.document.querySelector('meta[name="keywords"]'); + } catch (e) { + element = document.querySelector('meta[name="keywords"]'); } - return i; + + return (element && element.content) || ''; +} + +/** + * get connection downlink + * @returns {number} + */ +export function getConnectionDownLink(win = window) { + const nav = win.navigator || {}; + return nav && nav.connection && nav.connection.downlink >= 0 ? nav.connection.downlink.toString() : undefined; +} + +/** + * get pmg uid + * čŽˇå–åšļį”Ÿæˆį”¨æˆˇįš„id + * + * @return {string} + */ +export const getPmgUID = () => { + if (!storage.cookiesAreEnabled()) return; + + let pmgUid = storage.getCookie(COOKIE_KEY_PMGUID); + if (!pmgUid) { + pmgUid = utils.generateUUID(); + try { + storage.setCookie(COOKIE_KEY_PMGUID, pmgUid, getCurrentTimeToUTCString()); + } catch (e) {} + } + return pmgUid; }; -/* ----- mguid:end ------ */ +/* ----- pmguid:end ------ */ /** * čŽˇå–ä¸€ä¸Ēå¯ščąĄįš„某ä¸Ēå€ŧīŧŒåĻ‚æžœæ˛Ąæœ‰åˆ™čŋ”回įŠē字įŦĻ串 + * * @param {Object} obj å¯ščąĄ * @param {...string} keys 锎名 * @return {any} @@ -72,7 +148,7 @@ function isMobileAndTablet() { '.+mobile|avantgo|bada/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)', '|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone', '|p(ixi|re)/|plucker|pocket|psp|series(4|6)0|symbian|treo|up.(browser|link)|vodafone|wap', - '|windows ce|xda|xiino|android|ipad|playbook|silk', + '|windows ce|xda|xiino|android|ipad|playbook|silk' ].join(''), 'i' ); @@ -96,7 +172,7 @@ function isMobileAndTablet() { '|tim-|t-mo|to(pl|sh)|ts(70|m-|m3|m5)|tx-9|up(.b|g1|si)|utst|', 'v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|-v)', '|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas-', - '|your|zeto|zte-', + '|your|zeto|zte-' ].join(''), 'i' ); @@ -138,7 +214,7 @@ function getBidFloor(bid) { const bidFloor = bid.getFloor({ currency: 'USD', mediaType: '*', - size: '*', + size: '*' }); return bidFloor.floor; } catch (_) { @@ -156,11 +232,7 @@ function transformSizes(requestSizes) { let sizes = []; let sizeObj = {}; - if ( - utils.isArray(requestSizes) && - requestSizes.length === 2 && - !utils.isArray(requestSizes[0]) - ) { + if (utils.isArray(requestSizes) && requestSizes.length === 2 && !utils.isArray(requestSizes[0])) { sizeObj.width = parseInt(requestSizes[0], 10); sizeObj.height = parseInt(requestSizes[1], 10); sizes.push(sizeObj); @@ -187,7 +259,7 @@ const mediagoAdSize = [ { w: 160, h: 600 }, { w: 320, h: 180 }, { w: 320, h: 100 }, - { w: 336, h: 280 }, + { w: 336, h: 280 } ]; /** @@ -207,24 +279,32 @@ function getItems(validBidRequests, bidderRequest) { // įĄŽčŽ¤å°ē寸是åĻįŦĻ合我äģŦčĻæą‚ for (let size of sizes) { - matchSize = mediagoAdSize.find( - (item) => size.width === item.w && size.height === item.h - ); + matchSize = mediagoAdSize.find(item => size.width === item.w && size.height === item.h); if (matchSize) { break; } } if (!matchSize) { - matchSize = sizes[0] - ? { h: sizes[0].height || 0, w: sizes[0].width || 0 } - : { h: 0, w: 0 }; + matchSize = sizes[0] ? { h: sizes[0].height || 0, w: sizes[0].width || 0 } : { h: 0, w: 0 }; } const bidFloor = getBidFloor(req); - // const gpid = - // utils.deepAccess(req, 'ortb2Imp.ext.gpid') || - // utils.deepAccess(req, 'ortb2Imp.ext.data.pbadslot') || - // utils.deepAccess(req, 'params.placementId', 0); + const gpid = + utils.deepAccess(req, 'ortb2Imp.ext.gpid') || + utils.deepAccess(req, 'ortb2Imp.ext.data.pbadslot') || + utils.deepAccess(req, 'params.placementId', 0); + + const gdprConsent = {}; + if (bidderRequest && bidderRequest.gdprConsent) { + gdprConsent.consent = bidderRequest.gdprConsent.consentString; + gdprConsent.gdpr = bidderRequest.gdprConsent.gdprApplies ? 1 : 0; + // if (bidderRequest.gdprConsent.addtlConsent && bidderRequest.gdprConsent.addtlConsent.indexOf('~') !== -1) { + // let ac = bidderRequest.gdprConsent.addtlConsent; + // // pull only the ids from the string (after the ~) and convert them to an array of ints + // let acStr = ac.substring(ac.indexOf('~') + 1); + // gdpr_consent.addtl_consent = acStr.split('.').map(id => parseInt(id, 10)); + // } + } // if (mediaTypes.native) {} // banneråšŋ告įąģ型 @@ -237,15 +317,21 @@ function getItems(validBidRequests, bidderRequest) { h: matchSize.h, w: matchSize.w, pos: 1, - format: sizes, + format: sizes }, ext: { - // gpid: gpid, // 加å…Ĩ后无æŗ•čŋ”回åšŋ告 + adUnitCode: req.adUnitCode, + referrer: getReferrer(req, bidderRequest), + ortb2Imp: utils.deepAccess(req, 'ortb2Imp'), // äŧ å…ĨåŽŒæ•´å¯ščąĄīŧŒåˆ†æžæ—Ĩåŋ—数捎 + gpid: gpid, // 加å…Ĩ后无æŗ•čŋ”回åšŋ告 + adslot: utils.deepAccess(req, 'ortb2Imp.ext.data.adserver.adslot', '', ''), + ...gdprConsent // gdpr }, + tagid: req.params && req.params.tagid }; itemMaps[id] = { req, - ret, + ret }; } @@ -254,6 +340,31 @@ function getItems(validBidRequests, bidderRequest) { return items; } +/** + * @param {BidRequest} bidRequest + * @param bidderRequest + * @returns {string} + */ +function getReferrer(bidRequest = {}, bidderRequest = {}) { + let pageUrl; + if (bidRequest.params && bidRequest.params.referrer) { + pageUrl = bidRequest.params.referrer; + } else { + pageUrl = utils.deepAccess(bidderRequest, 'refererInfo.page'); + } + return pageUrl; +} + +/** + * get current time to UTC string + * @returns utc string + */ +export function getCurrentTimeToUTCString() { + const date = new Date(); + date.setTime(date.getTime() + COOKIE_RETENTION_TIME); + return date.toUTCString(); +} + /** * čŽˇå–rtbč¯ˇæą‚å‚æ•° * @@ -266,7 +377,14 @@ 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; + + const bidsUserIdAsEids = validBidRequests[0].userIdAsEids; + const bidsUserid = validBidRequests[0].userId; + const eids = bidsUserIdAsEids || bidsUserid; + const ppuid = bidsUserid && bidsUserid.pubProvidedId; + const content = utils.deepAccess(bidderRequest, 'ortb2.site.content'); + const cat = utils.deepAccess(bidderRequest, 'ortb2.site.cat'); + reqTimes += 1; let isMobile = isMobileAndTablet() ? 1 : 0; // input test status by Publisher. more frequently for test true req @@ -274,14 +392,17 @@ function getParam(validBidRequests, bidderRequest) { let auctionId = getProperty(bidderRequest, 'auctionId'); let items = getItems(validBidRequests, bidderRequest); - const domain = - utils.deepAccess(bidderRequest, 'refererInfo.domain') || document.domain; + const domain = utils.deepAccess(bidderRequest, 'refererInfo.domain') || document.domain; const location = utils.deepAccess(bidderRequest, 'refererInfo.location'); const page = utils.deepAccess(bidderRequest, 'refererInfo.page'); const referer = utils.deepAccess(bidderRequest, 'refererInfo.ref'); const timeout = bidderRequest.timeout || 2000; const firstPartyData = bidderRequest.ortb2; + const topWindow = window.top; + const title = getPageTitle(); + const desc = getPageDescription(); + const keywords = getPageKeywords(); if (items && items.length) { let c = { @@ -299,14 +420,32 @@ function getParam(validBidRequests, bidderRequest) { // 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, + language: /en/.test(navigator.language) ? 'en' : navigator.language }, ext: { eids, + bidsUserIdAsEids, + bidsUserid, + ppuid, firstPartyData, + content, + cat, + reqTimes, + pmguid: getPmgUID(), + page: { + title: title ? title.slice(0, 100) : undefined, + desc: desc ? desc.slice(0, 300) : undefined, + keywords: keywords ? keywords.slice(0, 100) : undefined, + hLen: topWindow.history?.length || undefined, + }, + device: { + nbw: getConnectionDownLink(), + hc: topWindow.navigator?.hardwareConcurrency || undefined, + dm: topWindow.navigator?.deviceMemory || undefined, + } }, user: { - buyeruid: getUserID(), + buyeruid: storage.getCookie(COOKIE_KEY_MGUID) || undefined, id: sharedid || pubcid, }, eids, @@ -320,11 +459,11 @@ function getParam(validBidRequests, bidderRequest) { publisher: { // todo id: domain, - name: domain, - }, + name: domain + } }, imp: items, - tmax: timeout, + tmax: timeout }; return c; } else { @@ -334,6 +473,7 @@ function getParam(validBidRequests, bidderRequest) { export const spec = { code: BIDDER_CODE, + gvlid: GVLID, // aliases: ['ex'], // short code /** * Determines whether or not the given bid request is valid. @@ -371,7 +511,6 @@ export const spec = { /** * 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. */ @@ -396,7 +535,7 @@ export const spec = { ttl: TIME_TO_LIVE, // referrer: REFERER, ad: getProperty(bid, 'adm'), - nurl: getProperty(bid, 'nurl'), + nurl: getProperty(bid, 'nurl') // adserverTargeting: { // granularityMultiplier: 0.1, // priceGranularity: 'pbHg', @@ -413,6 +552,45 @@ export const spec = { return bidResponses; }, + getUserSyncs: function (syncOptions, serverResponse, gdprConsent, uspConsent, gppConsent) { + const origin = encodeURIComponent(location.origin || `https://${location.host}`); + let syncParamUrl = `dm=${origin}`; + + if (gdprConsent && gdprConsent.consentString) { + if (typeof gdprConsent.gdprApplies === 'boolean') { + syncParamUrl += `&gdpr=${Number(gdprConsent.gdprApplies)}&gdpr_consent=${gdprConsent.consentString}`; + } else { + syncParamUrl += `&gdpr=0&gdpr_consent=${gdprConsent.consentString}`; + } + } + if (uspConsent && uspConsent.consentString) { + syncParamUrl += `&ccpa_consent=${uspConsent.consentString}`; + } + + if (syncOptions.iframeEnabled) { + window.addEventListener('message', function handler(event) { + if (!event.data || event.origin != THIRD_PARTY_COOKIE_ORIGIN) { + return; + } + + this.removeEventListener('message', handler); + + event.stopImmediatePropagation(); + + const response = event.data; + if (!response.optout && response.mguid) { + storage.setCookie(COOKIE_KEY_MGUID, response.mguid, getCurrentTimeToUTCString()); + } + }, true); + return [ + { + type: 'iframe', + url: `${COOKY_SYNC_IFRAME_URL}?${syncParamUrl}` + } + ]; + } + }, + /** * Register bidder specific code, which will execute if bidder timed out after an auction * @param {data} Containing timeout specific data @@ -432,7 +610,7 @@ export const spec = { if (bid['nurl']) { utils.triggerPixel(bid['nurl']); } - }, + } /** * Register bidder specific code, which will execute when the adserver targeting has been set for a bid from this bidder diff --git a/modules/mediakeysBidAdapter.js b/modules/mediakeysBidAdapter.js index 7af43a3c549..f4967fed170 100644 --- a/modules/mediakeysBidAdapter.js +++ b/modules/mediakeysBidAdapter.js @@ -119,7 +119,7 @@ function getOS() { * * @param {*} bid a Prebid.js bid (request) object * @param {string} mediaType the mediaType or the wildcard '*' - * @param {string|array} size the size array or the wildcard '*' + * @param {string|Array} size the size array or the wildcard '*' * @returns {number|boolean} */ function getFloor(bid, mediaType, size = '*') { diff --git a/modules/medianetBidAdapter.js b/modules/medianetBidAdapter.js index b9e00f45df9..6a8a35dbfd4 100644 --- a/modules/medianetBidAdapter.js +++ b/modules/medianetBidAdapter.js @@ -1,7 +1,6 @@ import { buildUrl, deepAccess, - getGptSlotInfoForAdUnitCode, getWindowTop, isArray, isEmpty, @@ -18,9 +17,18 @@ import {getRefererInfo} from '../src/refererDetection.js'; import {Renderer} from '../src/Renderer.js'; import { convertOrtbRequestToProprietaryNative } from '../src/native.js'; import {getGlobal} from '../src/prebidGlobal.js'; +import {getGptSlotInfoForAdUnitCode} from '../libraries/gptUtils/gptUtils.js'; + +/** + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid + * @typedef {import('../src/adapters/bidderFactory.js').TimedOutBid} TimedOutBid + */ const BIDDER_CODE = 'medianet'; +const TRUSTEDSTACK_CODE = 'trustedstack'; const BID_URL = 'https://prebid.media.net/rtb/prebid'; +const TRUSTEDSTACK_URL = 'https://prebid.trustedstack.com/rtb/trustedstack'; const PLAYER_URL = 'https://prebid.media.net/video/bundle.js'; const SLOT_VISIBILITY = { NOT_DETERMINED: 0, @@ -49,7 +57,7 @@ mnData.urlData = { }; const aliases = [ - { code: 'aax', gvlid: 720 }, + { code: TRUSTEDSTACK_CODE }, ]; getGlobal().medianetGlobals = getGlobal().medianetGlobals || {}; @@ -323,8 +331,9 @@ function normalizeCoordinates(coordinates) { } } -function getBidderURL(cid) { - return BID_URL + '?cid=' + encodeURIComponent(cid); +function getBidderURL(bidderCode, cid) { + const url = (bidderCode === TRUSTEDSTACK_CODE) ? TRUSTEDSTACK_URL : BID_URL; + return url + '?cid=' + encodeURIComponent(cid); } function generatePayload(bidRequests, bidderRequests) { @@ -463,7 +472,7 @@ export const spec = { let payload = generatePayload(bidRequests, bidderRequests); return { method: 'POST', - url: getBidderURL(payload.ext.customer_id), + url: getBidderURL(bidderRequests.bidderCode, payload.ext.customer_id), data: JSON.stringify(payload) }; }, diff --git a/modules/mediasniperBidAdapter.js b/modules/mediasniperBidAdapter.js index 378a804487a..5cf0ceaba18 100644 --- a/modules/mediasniperBidAdapter.js +++ b/modules/mediasniperBidAdapter.js @@ -1,8 +1,7 @@ import { deepAccess, deepClone, - deepSetValue, - getBidIdParameter, + deepSetValue, getBidIdParameter, inIframe, isArray, isEmpty, @@ -242,7 +241,7 @@ function createImp(bid) { * * @param {*} bid a Prebid.js bid (request) object * @param {string} mediaType the mediaType or the wildcard '*' - * @param {string|array} size the size array or the wildcard '*' + * @param {string|Array} size the size array or the wildcard '*' * @returns {number|boolean} */ function getFloor(bid, mediaType, size = '*') { diff --git a/modules/mediasquareBidAdapter.js b/modules/mediasquareBidAdapter.js index 720063253de..550c715e741 100644 --- a/modules/mediasquareBidAdapter.js +++ b/modules/mediasquareBidAdapter.js @@ -6,6 +6,15 @@ import { convertOrtbRequestToProprietaryNative } from '../src/native.js'; import {Renderer} from '../src/Renderer.js'; import { getRefererInfo } from '../src/refererDetection.js'; +/** + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid + * @typedef {import('../src/adapters/bidderFactory.js').ServerResponse} ServerResponse + * @typedef {import('../src/adapters/bidderFactory.js').SyncOptions} SyncOptions + * @typedef {import('../src/adapters/bidderFactory.js').UserSync} UserSync + * @typedef {import('../src/adapters/bidderFactory.js').validBidRequests} validBidRequests + */ + const BIDDER_CODE = 'mediasquare'; const BIDDER_URL_PROD = 'https://pbs-front.mediasquare.fr/' const BIDDER_URL_TEST = 'https://bidder-test.mediasquare.fr/' @@ -20,20 +29,20 @@ export const spec = { aliases: ['msq'], // short code supportedMediaTypes: [BANNER, NATIVE, 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. - */ + * 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.owner && bid.params.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. - */ + * 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) { // convert Native ORTB definition to old-style prebid native definition validBidRequests = convertOrtbRequestToProprietaryNative(validBidRequests); @@ -96,11 +105,11 @@ export const spec = { }; }, /** - * 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. - */ + * 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 serverBody = serverResponse.body; // const headerValue = serverResponse.headers.get('some-response-header'); @@ -120,23 +129,17 @@ export const spec = { netRevenue: value['net_revenue'], ttl: value['ttl'], ad: value['ad'], - mediasquare: { - 'bidder': value['bidder'], - 'code': value['code'] - }, + mediasquare: {}, meta: { 'advertiserDomains': value['adomain'] } }; - if ('context' in value) { - bidResponse['mediasquare']['context'] = value['context']; - } - if ('match' in value) { - bidResponse['mediasquare']['match'] = value['match']; - } - if ('hasConsent' in value) { - bidResponse['mediasquare']['hasConsent'] = value['hasConsent']; - } + let paramsToSearchFor = ['bidder', 'code', 'match', 'hasConsent', 'context', 'increment', 'ova']; + paramsToSearchFor.forEach(param => { + if (param in value) { + bidResponse['mediasquare'][param] = value[param]; + } + }); if ('native' in value) { bidResponse['native'] = value['native']; bidResponse['mediaType'] = 'native'; @@ -154,12 +157,12 @@ export const spec = { }, /** - * 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. + */ getUserSyncs: function(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') { @@ -170,9 +173,9 @@ export const spec = { }, /** - * Register bidder specific code, which will execute if a bid from this bidder won the auction - * @param {Bid} The bid that won the auction - */ + * Register bidder specific code, which will execute if a bid from this bidder won the auction + * @param {Bid} The bid that won the auction + */ onBidWon: function(bid) { // fires a pixel to confirm a winning bid if (bid.hasOwnProperty('mediaType') && bid.mediaType == 'video') { @@ -180,21 +183,26 @@ export const spec = { } let params = { pbjs: '$prebid.version$', referer: encodeURIComponent(getRefererInfo().page || getRefererInfo().topmostLocation) }; let endpoint = document.location.search.match(/msq_test=true/) ? BIDDER_URL_TEST : BIDDER_URL_PROD; - let paramsToSearchFor = ['bidder', 'code', 'match', 'hasConsent', 'context']; + let paramsToSearchFor = ['bidder', 'code', 'match', 'hasConsent', 'context', 'increment', 'ova']; if (bid.hasOwnProperty('mediasquare')) { - for (let i = 0; i < paramsToSearchFor.length; i++) { - if (bid['mediasquare'].hasOwnProperty(paramsToSearchFor[i])) { - params[paramsToSearchFor[i]] = bid['mediasquare'][paramsToSearchFor[i]]; + paramsToSearchFor.forEach(param => { + if (bid['mediasquare'].hasOwnProperty(param)) { + params[param] = bid['mediasquare'][param]; + if (typeof params[param] == 'number') { + params[param] = params[param].toString(); + } } - } + }); }; paramsToSearchFor = ['cpm', 'size', 'mediaType', 'currency', 'creativeId', 'adUnitCode', 'timeToRespond', 'requestId', 'auctionId', 'originalCpm', 'originalCurrency']; - for (let i = 0; i < paramsToSearchFor.length; i++) { - if (bid.hasOwnProperty(paramsToSearchFor[i])) { - params[paramsToSearchFor[i]] = bid[paramsToSearchFor[i]]; - if (typeof params[paramsToSearchFor[i]] == 'number') { params[paramsToSearchFor[i]] = params[paramsToSearchFor[i]].toString() } + paramsToSearchFor.forEach(param => { + if (bid.hasOwnProperty(param)) { + params[param] = bid[param]; + if (typeof params[param] == 'number') { + params[param] = params[param].toString(); + } } - } + }); ajax(endpoint + BIDDER_ENDPOINT_WINNING, null, JSON.stringify(params), {method: 'POST', withCredentials: true}); return true; } diff --git a/modules/merkleIdSystem.js b/modules/merkleIdSystem.js index c522d588970..3f3a90c3c49 100644 --- a/modules/merkleIdSystem.js +++ b/modules/merkleIdSystem.js @@ -11,6 +11,13 @@ import {submodule} from '../src/hook.js' import {getStorageManager} from '../src/storageManager.js'; import {MODULE_TYPE_UID} from '../src/activities/modules.js'; +/** + * @typedef {import('../modules/userId/index.js').Submodule} Submodule + * @typedef {import('../modules/userId/index.js').SubmoduleConfig} SubmoduleConfig + * @typedef {import('../modules/userId/index.js').ConsentData} ConsentData + * @typedef {import('../modules/userId/index.js').IdResponse} IdResponse + */ + const MODULE_NAME = 'merkleId'; const ID_URL = 'https://prebid.sv.rkdms.com/identity/'; const DEFAULT_REFRESH = 7 * 3600; @@ -87,17 +94,17 @@ function generateId(configParams, configStorage) { /** @type {Submodule} */ export const merkleIdSubmodule = { /** - * used to link submodule with config - * @type {string} - */ + * used to link submodule with config + * @type {string} + */ name: MODULE_NAME, /** - * decode the stored id value for passing to bid requests - * @function - * @param {string} value - * @returns {{eids:arrayofields}} - */ + * decode the stored id value for passing to bid requests + * @function + * @param {string} value + * @returns {{eids:arrayofields}} + */ decode(value) { // Legacy support for a single id const id = (value && value.pam_id && typeof value.pam_id.id === 'string') ? value.pam_id : undefined; @@ -115,12 +122,12 @@ export const merkleIdSubmodule = { }, /** - * performs action to obtain id and return a value in the callback's response argument - * @function - * @param {SubmoduleConfig} [config] - * @param {ConsentData} [consentData] - * @returns {IdResponse|undefined} - */ + * performs action to obtain id and return a value in the callback's response argument + * @function + * @param {SubmoduleConfig} [config] + * @param {ConsentData} [consentData] + * @returns {IdResponse|undefined} + */ getId(config, consentData) { logInfo('User ID - merkleId generating id'); @@ -196,6 +203,30 @@ export const merkleIdSubmodule = { logInfo('User ID - merkleId not refreshed'); return {id: storedId}; + }, + eids: { + 'merkleId': { + atype: 3, + getSource: function(data) { + if (data?.ext?.ssp) { + return `${data.ext.ssp}.merkleinc.com` + } + return 'merkleinc.com' + }, + getValue: function(data) { + return data.id; + }, + getUidExt: function(data) { + if (data.keyID) { + return { + keyID: data.keyID + } + } + if (data.ext) { + return data.ext; + } + } + }, } }; diff --git a/modules/mgidBidAdapter.js b/modules/mgidBidAdapter.js index 8e889261e52..fb3990e97f1 100644 --- a/modules/mgidBidAdapter.js +++ b/modules/mgidBidAdapter.js @@ -9,11 +9,10 @@ import { isEmpty, triggerPixel, logWarn, - getBidIdParameter, isFn, isNumber, isBoolean, - isInteger, deepSetValue, + isInteger, deepSetValue, getBidIdParameter, } from '../src/utils.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import {BANNER, NATIVE} from '../src/mediaTypes.js'; @@ -22,6 +21,12 @@ import { getStorageManager } from '../src/storageManager.js'; import { convertOrtbRequestToProprietaryNative } from '../src/native.js'; import {USERSYNC_DEFAULT_CONFIG} from '../src/userSync.js'; +/** + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid + * @typedef {import('../src/adapters/bidderFactory.js').ServerResponse} ServerResponse + */ + const GVLID = 358; const DEFAULT_CUR = 'USD'; const BIDDER_CODE = 'mgid'; diff --git a/modules/mgidRtdProvider.js b/modules/mgidRtdProvider.js index fd2c0bbe6fd..059be4e9103 100644 --- a/modules/mgidRtdProvider.js +++ b/modules/mgidRtdProvider.js @@ -5,6 +5,10 @@ import {getStorageManager} from '../src/storageManager.js'; import {getRefererInfo} from '../src/refererDetection.js'; import {MODULE_TYPE_RTD} from '../src/activities/modules.js'; +/** + * @typedef {import('../modules/rtdModule/index.js').RtdSubmodule} RtdSubmodule + */ + const MODULE_NAME = 'realTimeData'; const SUBMODULE_NAME = 'mgid'; const MGID_RTD_API_URL = 'https://servicer.mgid.com/sda'; diff --git a/modules/mgidXBidAdapter.js b/modules/mgidXBidAdapter.js index 5789f0d8b95..7e084977cf1 100644 --- a/modules/mgidXBidAdapter.js +++ b/modules/mgidXBidAdapter.js @@ -166,10 +166,15 @@ export const spec = { placements, coppa: config.getConfig('coppa') === true ? 1 : 0, ccpa: bidderRequest.uspConsent || undefined, - gdpr: bidderRequest.gdprConsent || undefined, tmax: config.getConfig('bidderTimeout') }; + if (bidderRequest.gdprConsent) { + request.gdpr = { + consentString: bidderRequest.gdprConsent.consentString + }; + } + const len = validBidRequests.length; for (let i = 0; i < len; i++) { const bid = validBidRequests[i]; diff --git a/modules/minutemediaBidAdapter.js b/modules/minutemediaBidAdapter.js index 85c1386957d..a4158e2abfa 100644 --- a/modules/minutemediaBidAdapter.js +++ b/modules/minutemediaBidAdapter.js @@ -1,4 +1,16 @@ -import { logWarn, logInfo, isArray, isFn, deepAccess, isEmpty, contains, timestamp, getBidIdParameter, triggerPixel, isInteger } from '../src/utils.js'; +import { + logWarn, + logInfo, + isArray, + isFn, + deepAccess, + isEmpty, + contains, + timestamp, + triggerPixel, + isInteger, + getBidIdParameter +} from '../src/utils.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import {BANNER, VIDEO} from '../src/mediaTypes.js'; import {config} from '../src/config.js'; @@ -7,7 +19,7 @@ const SUPPORTED_AD_TYPES = [BANNER, VIDEO]; const BIDDER_CODE = 'minutemedia'; const ADAPTER_VERSION = '6.0.0'; const TTL = 360; -const CURRENCY = 'USD'; +const DEFAULT_CURRENCY = 'USD'; const SELLER_ENDPOINT = 'https://hb.minutemedia-prebid.com/'; const MODES = { PRODUCTION: 'hb-mm-multi', @@ -60,7 +72,7 @@ export const spec = { const bidResponse = { requestId: adUnit.requestId, cpm: adUnit.cpm, - currency: adUnit.currency || CURRENCY, + currency: adUnit.currency || DEFAULT_CURRENCY, width: adUnit.width, height: adUnit.height, ttl: adUnit.ttl || TTL, @@ -129,16 +141,16 @@ registerBidder(spec); * @param bid {bid} * @returns {Number} */ -function getFloor(bid, mediaType) { +function getFloor(bid, mediaType, currency) { if (!isFn(bid.getFloor)) { return 0; } let floorResult = bid.getFloor({ - currency: CURRENCY, + currency: currency, mediaType: mediaType, size: '*' }); - return floorResult.currency === CURRENCY && floorResult.floor ? floorResult.floor : 0; + return floorResult.currency === currency && floorResult.floor ? floorResult.floor : 0; } /** @@ -274,6 +286,7 @@ function generateBidParameters(bid, bidderRequest) { const {params} = bid; const mediaType = isBanner(bid) ? BANNER : VIDEO; const sizesArray = getSizesArray(bid, mediaType); + const currency = params.currency || config.getConfig('currency.adServerCurrency') || DEFAULT_CURRENCY; // fix floor price in case of NAN if (isNaN(params.floorPrice)) { @@ -284,12 +297,13 @@ function generateBidParameters(bid, bidderRequest) { mediaType, adUnitCode: getBidIdParameter('adUnitCode', bid), sizes: sizesArray, - floorPrice: Math.max(getFloor(bid, mediaType), params.floorPrice), + currency: currency, + floorPrice: Math.max(getFloor(bid, mediaType, currency), params.floorPrice), bidId: getBidIdParameter('bidId', bid), loop: getBidIdParameter('bidderRequestsCount', bid), bidderRequestId: getBidIdParameter('bidderRequestId', bid), transactionId: bid.ortb2Imp?.ext?.tid || '', - coppa: 0 + coppa: 0, }; const pos = deepAccess(bid, `mediaTypes.${mediaType}.pos`); @@ -307,6 +321,15 @@ function generateBidParameters(bid, bidderRequest) { bidObject.placementId = placementId; } + const mimes = deepAccess(bid, `mediaTypes.${mediaType}.mimes`); + if (mimes) { + bidObject.mimes = mimes; + } + const api = deepAccess(bid, `mediaTypes.${mediaType}.api`); + if (api) { + bidObject.api = api; + } + const sua = deepAccess(bid, `ortb2.device.sua`); if (sua) { bidObject.sua = sua; @@ -358,6 +381,11 @@ function generateBidParameters(bid, bidderRequest) { bidObject.linearity = linearity; } + const protocols = deepAccess(bid, `mediaTypes.video.protocols`); + if (protocols) { + bidObject.protocols = protocols; + } + const plcmt = deepAccess(bid, `mediaTypes.video.plcmt`); if (plcmt) { bidObject.plcmt = plcmt; @@ -398,7 +426,8 @@ function generateGeneralParams(generalObject, bidderRequest) { dnt: (navigator.doNotTrack == 'yes' || navigator.doNotTrack == '1' || navigator.msDoNotTrack == '1') ? 1 : 0, device_type: getDeviceType(navigator.userAgent), ua: navigator.userAgent, - session_id: getBidIdParameter('bidderRequestId', generalObject), + is_wrapper: !!generalBidParams.isWrapper, + session_id: generalBidParams.sessionId || getBidIdParameter('bidderRequestId', generalObject), tmax: timeout } @@ -441,7 +470,7 @@ function generateGeneralParams(generalObject, bidderRequest) { if (bidderRequest && bidderRequest.refererInfo) { generalParams.referrer = deepAccess(bidderRequest, 'refererInfo.ref'); - generalParams.page_url = deepAccess(bidderRequest, 'refererInfo.page') || window.location.href + generalParams.page_url = deepAccess(bidderRequest, 'refererInfo.page') || deepAccess(window, 'location.href'); } return generalParams diff --git a/modules/minutemediaBidAdapter.md b/modules/minutemediaBidAdapter.md index 66b54adaf0e..fdfdf1b32bf 100644 --- a/modules/minutemediaBidAdapter.md +++ b/modules/minutemediaBidAdapter.md @@ -24,6 +24,7 @@ The adapter supports Video(instream) & Banner. | `floorPrice` | optional | Number | Minimum price in USD. Misuse of this parameter can impact revenue | 2.00 | `placementId` | optional | String | A unique placement identifier | "12345678" | `testMode` | optional | Boolean | This activates the test mode | false +| `currency` | optional | String | 3 letters currency | "EUR" # Test Parameters ```javascript diff --git a/modules/minutemediaplusBidAdapter.js b/modules/minutemediaplusBidAdapter.js index 33f3634e17f..146d437b1fa 100644 --- a/modules/minutemediaplusBidAdapter.js +++ b/modules/minutemediaplusBidAdapter.js @@ -252,13 +252,20 @@ function interpretResponse(serverResponse, request) { } } -function getUserSyncs(syncOptions, responses, gdprConsent = {}, uspConsent = '') { +function getUserSyncs(syncOptions, responses, gdprConsent = {}, uspConsent = '', gppConsent = {}) { let syncs = []; const {iframeEnabled, pixelEnabled} = syncOptions; const {gdprApplies, consentString = ''} = gdprConsent; + const {gppString, applicableSections} = gppConsent; const cidArr = responses.filter(resp => deepAccess(resp, 'body.cid')).map(resp => resp.body.cid).filter(uniques); - const params = `?cid=${encodeURIComponent(cidArr.join(','))}&gdpr=${gdprApplies ? 1 : 0}&gdpr_consent=${encodeURIComponent(consentString || '')}&us_privacy=${encodeURIComponent(uspConsent || '')}` + let params = `?cid=${encodeURIComponent(cidArr.join(','))}&gdpr=${gdprApplies ? 1 : 0}&gdpr_consent=${encodeURIComponent(consentString || '')}&us_privacy=${encodeURIComponent(uspConsent || '')}` + + if (gppString && applicableSections?.length) { + params += '&gpp=' + encodeURIComponent(gppString); + params += '&gpp_sid=' + encodeURIComponent(applicableSections.join(',')); + } + if (iframeEnabled) { syncs.push({ type: 'iframe', diff --git a/modules/missenaBidAdapter.js b/modules/missenaBidAdapter.js index 33fa6857e85..527b6704146 100644 --- a/modules/missenaBidAdapter.js +++ b/modules/missenaBidAdapter.js @@ -1,12 +1,46 @@ -import { buildUrl, formatQS, logInfo, triggerPixel } from '../src/utils.js'; +import { + buildUrl, + formatQS, + isFn, + logInfo, + safeJSONParse, + triggerPixel, +} from '../src/utils.js'; +import { config } from '../src/config.js'; import { BANNER } from '../src/mediaTypes.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { getStorageManager } from '../src/storageManager.js'; + +/** + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid + * @typedef {import('../src/adapters/bidderFactory.js').ServerResponse} ServerResponse + * @typedef {import('../src/adapters/bidderFactory.js').validBidRequests} validBidRequests + */ 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 storage = getStorageManager({ bidderCode: BIDDER_CODE }); + +/* Get Floor price information */ +function getFloor(bidRequest) { + if (!isFn(bidRequest.getFloor)) { + return {}; + } + + const bidFloors = bidRequest.getFloor({ + currency: 'USD', + mediaType: BANNER, + }); + + if (!isNaN(bidFloors.floor)) { + return bidFloors; + } +} + export const spec = { aliases: ['msna'], code: BIDDER_CODE, @@ -30,6 +64,18 @@ export const spec = { * @return ServerRequest Info describing the request to the server. */ buildRequests: function (validBidRequests, bidderRequest) { + const capKey = `missena.missena.capper.remove-bubble.${validBidRequests[0]?.params.apiKey}`; + const capping = safeJSONParse(storage.getDataFromLocalStorage(capKey)); + const referer = bidderRequest?.refererInfo?.topmostLocation; + if ( + typeof capping?.expiry === 'number' && + new Date().getTime() < capping?.expiry && + (!capping?.referer || capping?.referer == referer) + ) { + logInfo('Missena - Capped'); + return []; + } + return validBidRequests.map((bidRequest) => { const payload = { adunit: bidRequest.adUnitCode, @@ -60,7 +106,17 @@ export const spec = { if (bidRequest.params.isInternal) { payload.is_internal = bidRequest.params.isInternal; } + if (bidRequest.ortb2?.device?.ext?.cdep) { + payload.cdep = bidRequest.ortb2?.device?.ext?.cdep; + } payload.userEids = bidRequest.userIdAsEids || []; + payload.version = '$prebid.version$'; + + const bidFloor = getFloor(bidRequest); + payload.floor = bidFloor?.floor; + payload.floor_currency = bidFloor?.currency; + payload.currency = config.getConfig('currency.adServerCurrency') || 'EUR'; + return { method: 'POST', url: baseUrl + '?' + formatQS({ t: bidRequest.params.apiKey }), @@ -89,7 +145,7 @@ export const spec = { syncOptions, serverResponses, gdprConsent, - uspConsent + uspConsent, ) { if (!syncOptions.iframeEnabled) { return []; @@ -128,8 +184,13 @@ export const spec = { protocol: 'https', hostname, pathname: '/v1/bidsuccess', - search: { t: bid.params[0].apiKey, provider: bid.meta?.networkName, cpm: bid.cpm, currency: bid.currency }, - }) + search: { + t: bid.params[0].apiKey, + provider: bid.meta?.networkName, + cpm: bid.originalCpm, + currency: bid.originalCurrency, + }, + }), ); logInfo('Missena - Bid won', bid); }, diff --git a/modules/mobfoxpbBidAdapter.js b/modules/mobfoxpbBidAdapter.js index 9ff50e2531f..35e9b03c031 100644 --- a/modules/mobfoxpbBidAdapter.js +++ b/modules/mobfoxpbBidAdapter.js @@ -71,6 +71,15 @@ export const spec = { if (bidderRequest.gdprConsent) { request.gdpr = bidderRequest.gdprConsent; } + + // Add GPP consent + if (bidderRequest.gppConsent) { + request.gpp = bidderRequest.gppConsent.gppString; + request.gpp_sid = bidderRequest.gppConsent.applicableSections; + } else if (bidderRequest.ortb2?.regs?.gpp) { + request.gpp = bidderRequest.ortb2.regs.gpp; + request.gpp_sid = bidderRequest.ortb2.regs.gpp_sid; + } } const len = validBidRequests.length; diff --git a/modules/multibid/index.js b/modules/multibid/index.js index df77a157bee..27b88d47cf7 100644 --- a/modules/multibid/index.js +++ b/modules/multibid/index.js @@ -45,10 +45,10 @@ config.getConfig(MODULE_NAME, conf => { }); /** - * @summary validates multibid configuration entries - * @param {Object[]} multibid - example [{bidder: 'bidderA', maxbids: 2, prefix: 'bidA'}, {bidder: 'bidderB', maxbids: 2}] - * @return {Boolean} -*/ + * @summary validates multibid configuration entries + * @param {Object[]} multibid - example [{bidder: 'bidderA', maxbids: 2, prefix: 'bidA'}, {bidder: 'bidderB', maxbids: 2}] + * @return {Boolean} + */ export function validateMultibid(conf) { let check = true; let duplicate = conf.filter(entry => { @@ -77,10 +77,10 @@ export function validateMultibid(conf) { } /** - * @summary addBidderRequests before hook - * @param {Function} fn reference to original function (used by hook logic) - * @param {Object[]} array containing copy of each bidderRequest object -*/ + * @summary addBidderRequests before hook + * @param {Function} fn reference to original function (used by hook logic) + * @param {Object[]} array containing copy of each bidderRequest object + */ export function adjustBidderRequestsHook(fn, bidderRequests) { bidderRequests.map(bidRequest => { // Loop through bidderRequests and check if bidderCode exists in multiconfig @@ -95,11 +95,11 @@ export function adjustBidderRequestsHook(fn, bidderRequests) { } /** - * @summary addBidResponse before hook - * @param {Function} fn reference to original function (used by hook logic) - * @param {String} ad unit code for bid - * @param {Object} bid object -*/ + * @summary addBidResponse before hook + * @param {Function} fn reference to original function (used by hook logic) + * @param {String} ad unit code for bid + * @param {Object} bid object + */ export const addBidResponseHook = timedBidResponseHook('multibid', function addBidResponseHook(fn, adUnitCode, bid, reject) { let floor = deepAccess(bid, 'floorData.floorValue'); @@ -146,9 +146,9 @@ export const addBidResponseHook = timedBidResponseHook('multibid', function addB }); /** -* A descending sort function that will sort the list of objects based on the following: -* - bids without dynamic aliases are sorted before bids with dynamic aliases -*/ + * A descending sort function that will sort the list of objects based on the following: + * - bids without dynamic aliases are sorted before bids with dynamic aliases + */ export function sortByMultibid(a, b) { if (a.bidder !== a.bidderCode && b.bidder === b.bidderCode) { return 1; @@ -162,13 +162,13 @@ export function sortByMultibid(a, b) { } /** - * @summary getHighestCpmBidsFromBidPool before hook - * @param {Function} fn reference to original function (used by hook logic) - * @param {Object[]} array of objects containing all bids from bid pool - * @param {Function} function to reduce to only highest cpm value for each bidderCode - * @param {Number} adUnit bidder targeting limit, default set to 0 - * @param {Boolean} default set to false, this hook modifies targeting and sets to true -*/ + * @summary getHighestCpmBidsFromBidPool before hook + * @param {Function} fn reference to original function (used by hook logic) + * @param {Object[]} array of objects containing all bids from bid pool + * @param {Function} function to reduce to only highest cpm value for each bidderCode + * @param {Number} adUnit bidder targeting limit, default set to 0 + * @param {Boolean} default set to false, this hook modifies targeting and sets to true + */ export function targetBidPoolHook(fn, bidsReceived, highestCpmCallback, adUnitBidLimit = 0, hasModified = false) { if (!config.getConfig('multibid')) resetMultiConfig(); if (hasMultibid) { @@ -216,18 +216,18 @@ export function targetBidPoolHook(fn, bidsReceived, highestCpmCallback, adUnitBi } /** -* Resets globally stored multibid configuration -*/ + * Resets globally stored multibid configuration + */ export const resetMultiConfig = () => { hasMultibid = false; multiConfig = {}; }; /** -* Resets globally stored multibid ad unit bids -*/ + * Resets globally stored multibid ad unit bids + */ export const resetMultibidUnits = () => multibidUnits = {}; /** -* Set up hooks on init -*/ + * Set up hooks on init + */ function init() { // TODO: does this reset logic make sense - what about simultaneous auctions? events.on(CONSTANTS.EVENTS.AUCTION_INIT, resetMultibidUnits); diff --git a/modules/mwOpenLinkIdSystem.js b/modules/mwOpenLinkIdSystem.js index 9b1035cbf18..c06f61ff82f 100644 --- a/modules/mwOpenLinkIdSystem.js +++ b/modules/mwOpenLinkIdSystem.js @@ -11,6 +11,11 @@ import { submodule } from '../src/hook.js'; import {getStorageManager} from '../src/storageManager.js'; import {MODULE_TYPE_UID} from '../src/activities/modules.js'; +/** + * @typedef {import('../modules/userId/index.js').Submodule} Submodule + * @typedef {import('../modules/userId/index.js').SubmoduleParams} SubmoduleParams + */ + const openLinkID = { name: 'mwol', cookie_expiration: (86400 * 1000 * 365 * 1) // 1 year @@ -112,31 +117,37 @@ export { writeCookie }; /** @type {Submodule} */ export const mwOpenLinkIdSubModule = { /** - * used to link submodule with config - * @type {string} - */ + * used to link submodule with config + * @type {string} + */ name: 'mwOpenLinkId', /** - * decode the stored id value for passing to bid requests - * @function - * @param {MwOlId} mwOlId - * @return {(Object|undefined} - */ + * decode the stored id value for passing to bid requests + * @function + * @param {MwOlId} mwOlId + * @return {(Object|undefined} + */ decode(mwOlId) { const id = mwOlId && isPlainObject(mwOlId) ? mwOlId.eid : undefined; return id ? { 'mwOpenLinkId': id } : undefined; }, /** - * performs action to obtain id and return a value in the callback's response argument - * @function - * @param {SubmoduleParams} [submoduleParams] - * @returns {id:MwOlId | undefined} - */ + * performs action to obtain id and return a value in the callback's response argument + * @function + * @param {SubmoduleParams} [submoduleParams] + * @returns {id:MwOlId | undefined} + */ getId(submoduleConfig) { const submoduleConfigParams = (submoduleConfig && submoduleConfig.params) || {}; if (!isValidConfig(submoduleConfigParams)) return undefined; return setID(submoduleConfigParams); + }, + eids: { + 'mwOpenLinkId': { + source: 'mediawallahscript.com', + atype: 1 + }, } }; diff --git a/modules/mygaruIdSystem.js b/modules/mygaruIdSystem.js new file mode 100644 index 00000000000..9133480477b --- /dev/null +++ b/modules/mygaruIdSystem.js @@ -0,0 +1,104 @@ +/** + * This module adds MyGaru Real Time User Sync to the User ID module + * The {@link module:modules/userId} module is required + * @module modules/mygaruIdSystem + * @requires module:modules/userId + */ + +import { ajax } from '../src/ajax.js'; +import { submodule } from '../src/hook.js'; + +/** + * @typedef {import('../modules/userId/index.js').Submodule} Submodule + * @typedef {import('../modules/userId/index.js').SubmoduleConfig} SubmoduleConfig + * @typedef {import('../modules/userId/index.js').ConsentData} ConsentData + */ + +const bidderCode = 'mygaruId'; +const syncUrl = 'https://ident.mygaru.com/v2/id'; + +export function buildUrl(opts) { + const queryPairs = []; + for (let key in opts) { + if (opts[key] !== undefined) { + queryPairs.push(`${key}=${encodeURIComponent(opts[key])}`); + } + } + return `${syncUrl}?${queryPairs.join('&')}`; +} + +function requestRemoteIdAsync(url) { + return new Promise((resolve) => { + ajax( + url, + { + success: response => { + try { + const jsonResponse = JSON.parse(response); + const { iuid } = jsonResponse; + resolve(iuid); + } catch (e) { + resolve(); + } + }, + error: () => { + resolve(); + }, + }, + undefined, + { + method: 'GET', + contentType: 'application/json' + } + ); + }); +} + +/** @type {Submodule} */ +export const mygaruIdSubmodule = { + /** + * used to link submodule with config + * @type {string} + */ + name: bidderCode, + /** + * decode the stored id value for passing to bid requests + * @function + * @returns {{id: string} | null} + */ + decode(id) { + return id; + }, + /** + * get the MyGaru Id from local storages and initiate a new user sync + * @function + * @param {SubmoduleConfig} [config] + * @param {ConsentData} [consentData] + * @returns {{id: string | undefined}} + */ + getId(config, consentData) { + const gdprApplies = consentData && typeof consentData.gdprApplies === 'boolean' && consentData.gdprApplies ? 1 : 0; + const gdprConsentString = gdprApplies ? consentData.consentString : undefined; + const url = buildUrl({ + gdprApplies, + gdprConsentString + }); + + return { + url, + callback: function (done) { + return requestRemoteIdAsync(url).then((id) => { + done({ mygaruId: id }); + }) + } + } + }, + eids: { + 'mygaruId': { + source: 'mygaru.com', + atype: 1 + }, + } +}; + +submodule('userId', mygaruIdSubmodule); diff --git a/modules/mygaruIdSystem.md b/modules/mygaruIdSystem.md new file mode 100644 index 00000000000..92724f99469 --- /dev/null +++ b/modules/mygaruIdSystem.md @@ -0,0 +1,24 @@ +## Mygaru User ID Submodule + +MyGaru provides single use tokens as a UserId for SSPs and DSP that consume telecom DMP data. + +## Building Prebid with Mygaru ID Support + +First, make sure to add submodule to your Prebid.js package with: + +``` +gulp build --modules=userId,mygaruIdSystem +``` +Params configuration is not required. +Also mygaru is async, in order to get ids for initial ad auctions you need to add auctionDelay param to userSync config. + +```javascript +pbjs.setConfig({ + userSync: { + auctionDelay: 100, + userIds: [{ + name: 'mygaruId', + }] + } +}); +``` diff --git a/modules/nativoBidAdapter.js b/modules/nativoBidAdapter.js index c62a74e6d6c..69a270247cd 100644 --- a/modules/nativoBidAdapter.js +++ b/modules/nativoBidAdapter.js @@ -2,7 +2,20 @@ import { deepAccess, isEmpty } from '../src/utils.js' import { registerBidder } from '../src/adapters/bidderFactory.js' import { BANNER } from '../src/mediaTypes.js' import { getGlobal } from '../src/prebidGlobal.js' -// import { config } from 'src/config' +import { ortbConverter } from '../libraries/ortbConverter/converter.js' + +const converter = ortbConverter({ + context: { + // `netRevenue` and `ttl` are required properties of bid responses - provide a default for them + netRevenue: true, // or false if your adapter should set bidResponse.netRevenue = false + ttl: 30 // default bidResponse.ttl (when not specified in ORTB response.seatbid[].bid[].exp) + }, + imp(buildImp, bidRequest, context) { + const imp = buildImp(bidRequest, context); + imp.tagid = bidRequest.adUnitCode + return imp; + } +}); const BIDDER_CODE = 'nativo' const BIDDER_ENDPOINT = 'https://exchange.postrelease.com/prebid' @@ -136,6 +149,10 @@ export const spec = { * @return ServerRequest Info describing the request to the server. */ buildRequests: function (validBidRequests, bidderRequest) { + // Get OpenRTB Data + const openRTBData = converter.toORTB({bidRequests: validBidRequests, bidderRequest}) + const openRTBDataString = JSON.stringify(openRTBData) + const requestData = new RequestData() requestData.addBidRequestDataSource(new UserEIDs()) @@ -271,8 +288,9 @@ export const spec = { const requestUrl = buildRequestUrl(BIDDER_ENDPOINT, qsParamStrings) let serverRequest = { - method: 'GET', - url: requestUrl + method: 'POST', + url: requestUrl, + data: openRTBDataString, } return serverRequest diff --git a/modules/naveggIdSystem.js b/modules/naveggIdSystem.js index 878ae7fadb2..42c6b113566 100644 --- a/modules/naveggIdSystem.js +++ b/modules/naveggIdSystem.js @@ -10,6 +10,11 @@ import { ajax } from '../src/ajax.js'; import {getStorageManager} from '../src/storageManager.js'; import {MODULE_TYPE_UID} from '../src/activities/modules.js'; +/** + * @typedef {import('../modules/userId/index.js').Submodule} Submodule + * @typedef {import('../modules/userId/index.js').SubmoduleConfig} SubmoduleConfig + */ + const MODULE_NAME = 'naveggId'; const OLD_NAVEGG_ID = 'nid'; const NAVEGG_ID = 'nvggid'; @@ -74,16 +79,16 @@ function readnavIDFromCookie() { /** @type {Submodule} */ export const naveggIdSubmodule = { /** - * used to link submodule with config - * @type {string} - */ + * used to link submodule with config + * @type {string} + */ name: MODULE_NAME, /** - * decode the stored id value for passing to bid requests - * @function - * @param { Object | string | undefined } value - * @return { Object | string | undefined } - */ + * decode the stored id value for passing to bid requests + * @function + * @param { Object | string | undefined } value + * @return { Object | string | undefined } + */ decode(value) { const naveggIdVal = value ? isStr(value) ? value : isPlainObject(value) ? value.id : undefined : undefined; return naveggIdVal ? { @@ -91,11 +96,11 @@ export const naveggIdSubmodule = { } : undefined; }, /** - * performs action to obtain id and return a value in the callback's response argument - * @function - * @param {SubmoduleConfig} config - * @return {{id: string | undefined } | undefined} - */ + * performs action to obtain id and return a value in the callback's response argument + * @function + * @param {SubmoduleConfig} config + * @return {{id: string | undefined } | undefined} + */ getId() { const naveggIdString = readnaveggIdFromLocalStorage() || readnaveggIDFromCookie() || getNaveggIdFromApi() || readoldnaveggIDFromCookie() || readnvgIDFromCookie() || readnavIDFromCookie(); @@ -107,6 +112,12 @@ export const naveggIdSubmodule = { } } return undefined; + }, + eids: { + 'naveggId': { + source: 'navegg.com', + atype: 1 + }, } }; submodule('userId', naveggIdSubmodule); diff --git a/modules/netIdSystem.js b/modules/netIdSystem.js index 90c8735c993..4765f892a97 100644 --- a/modules/netIdSystem.js +++ b/modules/netIdSystem.js @@ -7,6 +7,13 @@ import {submodule} from '../src/hook.js'; +/** + * @typedef {import('../modules/userId/index.js').Submodule} Submodule + * @typedef {import('../modules/userId/index.js').SubmoduleConfig} SubmoduleConfig + * @typedef {import('../modules/userId/index.js').ConsentData} ConsentData + * @typedef {import('../modules/userId/index.js').IdResponse} IdResponse + */ + /** @type {Submodule} */ export const netIdSubmodule = { /** @@ -34,6 +41,12 @@ export const netIdSubmodule = { getId(config) { /* currently not possible */ return {}; + }, + eids: { + 'netId': { + source: 'netid.de', + atype: 1 + }, } }; diff --git a/modules/neuwoRtdProvider.js b/modules/neuwoRtdProvider.js index 00a3c59b4a6..7c594e2a1c3 100644 --- a/modules/neuwoRtdProvider.js +++ b/modules/neuwoRtdProvider.js @@ -10,14 +10,14 @@ const SEGTAX_IAB = 6 // IAB - Content Taxonomy version 2 const RESPONSE_IAB_TIER_1 = 'marketing_categories.iab_tier_1' const RESPONSE_IAB_TIER_2 = 'marketing_categories.iab_tier_2' -function init(config = {}, userConsent) { - config.params = config.params || {} +function init(config, userConsent) { + // config.params = config.params || {} // ignore module if publicToken is missing (module setup failure) - if (!config.params.publicToken) { + if (!config || !config.params || !config.params.publicToken) { logError('publicToken missing', 'NeuwoRTDModule', 'config.params.publicToken') return false; } - if (!config.params.apiUrl) { + if (!config || !config.params || !config.params.apiUrl) { logError('apiUrl missing', 'NeuwoRTDModule', 'config.params.apiUrl') return false; } @@ -25,14 +25,14 @@ function init(config = {}, userConsent) { } export function getBidRequestData(reqBidsConfigObj, callback, config, userConsent) { - config.params = config.params || {}; + const confParams = config.params || {}; logInfo('NeuwoRTDModule', 'starting getBidRequestData') - const wrappedArgUrl = encodeURIComponent(config.params.argUrl || getRefererInfo().page); + const wrappedArgUrl = encodeURIComponent(confParams.argUrl || getRefererInfo().page); /* adjust for pages api.url?prefix=test (to add params with '&') as well as api.url (to add params with '?') */ - const joiner = config.params.apiUrl.indexOf('?') < 0 ? '?' : '&' - const url = config.params.apiUrl + joiner + [ - 'token=' + config.params.publicToken, + const joiner = confParams.apiUrl.indexOf('?') < 0 ? '?' : '&' + const url = confParams.apiUrl + joiner + [ + 'token=' + confParams.publicToken, 'url=' + wrappedArgUrl ].join('&') const billingId = generateUUID(); @@ -71,7 +71,7 @@ export function addFragment(base, path, addition) { /** * 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 {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 diff --git a/modules/neuwoRtdProvider.md b/modules/neuwoRtdProvider.md index 2adead66d4e..fb52734d451 100644 --- a/modules/neuwoRtdProvider.md +++ b/modules/neuwoRtdProvider.md @@ -33,6 +33,8 @@ pbjs.setConfig({realTimeData: { dataProviders: [ neuwoDataProvider ]}}) # Testing +`gulp test --modules=rtdModule,neuwoRtdProvider` + ## Add development tools if necessary - Install node for npm diff --git a/modules/newspassid.md b/modules/newspassid.md new file mode 100644 index 00000000000..6fa709e5ba6 --- /dev/null +++ b/modules/newspassid.md @@ -0,0 +1,76 @@ +--- +Module Name: NewspassId Bidder Adapter +Module Type: Bidder Adapter +Maintainer: techsupport@newspassid.com +layout: bidder +title: Newspass ID +description: LMC Newspass ID Prebid JS Bidder Adapter +biddercode: newspassid +gdpr_supported: false +gvl_id: none +usp_supported: true +coppa_supported: false +schain_supported: true +dchain_supported: false +userIds: criteo, id5Id, tdid, identityLink, liveIntentId, parrableId, pubCommonId, lotamePanoramaId, sharedId, fabrickId +media_types: banner +safeframes_ok: true +deals_supported: true +floors_supported: false +fpd_supported: false +pbjs: true +pbs: false +prebid_member: false +multiformat_supported: will-bid-on-any +--- + +### Description + +LMC Newspass ID Prebid JS Bidder Adapter that connects to the NewspassId demand source(s). + +The Newspass bid adapter supports Banner mediaTypes ONLY. +This is intended for USA audiences only, and does not support GDPR + + +### Bid Params + +{: .table .table-bordered .table-striped } + +| Name | Scope | Description | Example | Type | +|-----------|----------|---------------------------|------------|----------| +| `siteId` | required | The site ID. | `"NPID0000001"` | `string` | +| `publisherId` | required | The publisher ID. | `"4204204201"` | `string` | +| `placementId` | required | The placement ID. | `"0420420421"` | `string` | +| `customData` | optional | publisher key-values used for targeting | `[{"settings":{},"targeting":{"key1": "value1", "key2": "value2"}}], ` | `array` | + +### Test Parameters + + +A test ad unit that will consistently return test creatives: + +``` + +//Banner adUnit + +adUnits = [{ + code: 'id-of-your-banner-div', + mediaTypes: { + banner: { + sizes: [[300, 250], [300,600]] + } + }, + bids: [{ + bidder: 'newspassid', + params: { + publisherId: 'NEWSPASS0001', /* an ID to identify the publisher account - required */ + siteId: '4204204201', /* An ID used to identify a site within a publisher account - required */ + placementId: '8000000015', /* an ID used to identify the piece of inventory - required - for appnexus test use 13144370. */ + customData: [{"settings": {}, "targeting": {"key": "value", "key2": ["value1", "value2"]}}],/* optional array with 'targeting' placeholder for passing publisher specific key-values for targeting. */ + } + }] + }]; +``` + +### Note: + +Please contact us at techsupport@newspassid.com for any assistance testing your implementation before going live into production. diff --git a/modules/newspassidBidAdapter.js b/modules/newspassidBidAdapter.js index b440edc2beb..2a4b2da186b 100644 --- a/modules/newspassidBidAdapter.js +++ b/modules/newspassidBidAdapter.js @@ -1,15 +1,24 @@ -import {contains, deepAccess, deepSetValue, isArray, logError, logInfo, logWarn, parseUrl} from '../src/utils.js'; -import {registerBidder} from '../src/adapters/bidderFactory.js'; -import {BANNER, NATIVE} from '../src/mediaTypes.js'; +import { + logInfo, + logError, + deepAccess, + logWarn, + deepSetValue, + isArray, + contains, + parseUrl, + generateUUID +} from '../src/utils.js'; +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { BANNER, NATIVE } from '../src/mediaTypes.js'; import {config} from '../src/config.js'; import {getPriceBucketString} from '../src/cpmBucketManager.js'; import {getRefererInfo} from '../src/refererDetection.js'; - const BIDDER_CODE = 'newspassid'; const ORIGIN = 'https://bidder.newspassid.com' // applies only to auction & cookie const AUCTIONURI = '/openrtb2/auction'; const NEWSPASSCOOKIESYNC = '/static/load-cookie.html'; -const NEWSPASSVERSION = '1.0.1'; +const NEWSPASSVERSION = '1.1.4'; export const spec = { version: NEWSPASSVERSION, code: BIDDER_CODE, @@ -155,7 +164,7 @@ export const spec = { let placementId = placementIdOverrideFromGetParam || this.getPlacementId(npBidRequest); // prefer to use a valid override param, else the bidRequest placement Id obj.id = npBidRequest.bidId; // this causes an error if we change it to something else, even if you update the bidRequest object: "WARNING: Bidder newspass made bid for unknown request ID: mb7953.859498327448. Ignoring." obj.tagid = placementId; - let parsed = parseUrl(getRefererInfo().page); + let parsed = parseUrl(this.getRefererInfo().page); obj.secure = parsed.protocol === 'https' ? 1 : 0; let arrBannerSizes = []; if (!npBidRequest.hasOwnProperty('mediaTypes')) { @@ -189,7 +198,6 @@ export const spec = { deepSetValue(obj, 'ext.prebid', {'storedrequest': {'id': placementId}}); obj.ext['newspassid'] = {}; obj.ext['newspassid'].adUnitCode = npBidRequest.adUnitCode; // eg. 'mpu' - obj.ext['newspassid'].transactionId = npBidRequest.transactionId; // this is the transactionId PER adUnit, common across bidders for this unit if (npBidRequest.params.hasOwnProperty('customData')) { obj.ext['newspassid'].customData = npBidRequest.params.customData; } @@ -216,6 +224,10 @@ export const spec = { if (!schain && deepAccess(npBidRequest, 'schain')) { schain = npBidRequest.schain; } + let gpid = deepAccess(npBidRequest, 'ortb2Imp.ext.gpid'); + if (gpid) { + deepSetValue(obj, 'ext.gpid', gpid); + } return obj; }); let extObj = {}; @@ -240,7 +252,7 @@ export const spec = { let userExtEids = deepAccess(validBidRequests, '0.userIdAsEids', []); // generate the UserIDs in the correct format for UserId module npRequest.site = { 'publisher': {'id': htmlParams.publisherId}, - 'page': getRefererInfo().page, + 'page': this.getRefererInfo().page, 'id': htmlParams.siteId }; npRequest.test = config.getConfig('debug') ? 1 : 0; @@ -259,12 +271,9 @@ export const spec = { } if (singleRequest) { logInfo('buildRequests starting to generate response for a single request'); - // TODO: fix auctionId & transactionId leak: https://github.com/prebid/Prebid.js/issues/9781 - npRequest.id = bidderRequest.auctionId; // Unique ID of the bid request, provided by the exchange. - npRequest.auctionId = bidderRequest.auctionId; // not sure if this should be here? + npRequest.id = generateUUID(); // Unique ID of the bid request, provided by the exchange. (REQUIRED) npRequest.imp = tosendtags; npRequest.ext = extObj; - deepSetValue(npRequest, 'source.tid', bidderRequest.auctionId);// RTB 2.5 : tid is Transaction ID that must be common across all participants in this bid request (e.g., potentially multiple exchanges). deepSetValue(npRequest, 'user.ext.eids', userExtEids); var ret = { method: 'POST', @@ -280,12 +289,9 @@ export const spec = { let arrRet = tosendtags.map(imp => { logInfo('buildRequests starting to generate non-single response, working on imp : ', imp); let npRequestSingle = Object.assign({}, npRequest); - imp.ext['newspassid'].pageAuctionId = bidderRequest['auctionId']; // make a note in the ext object of what the original auctionId was, in the bidderRequest object - npRequestSingle.id = imp.ext['newspassid'].transactionId; // Unique ID of the bid request, provided by the exchange. - npRequestSingle.auctionId = imp.ext['newspassid'].transactionId; // not sure if this should be here? + npRequestSingle.id = generateUUID(); npRequestSingle.imp = [imp]; npRequestSingle.ext = extObj; - deepSetValue(npRequestSingle, 'source.tid', bidderRequest.auctionId);// RTB 2.5 : tid is Transaction ID that must be common across all participants in this bid request (e.g., potentially multiple exchanges). deepSetValue(npRequestSingle, 'user.ext.eids', userExtEids); logInfo('buildRequests RequestSingle (for non-single) = ', npRequestSingle); return { @@ -305,6 +311,7 @@ export const spec = { logInfo(`interpretResponse time: ${startTime}. buildRequests done -> interpretResponse start was ${startTime - this.propertyBag.buildRequestsEnd}ms`); logInfo(`serverResponse, request`, JSON.parse(JSON.stringify(serverResponse)), JSON.parse(JSON.stringify(request))); serverResponse = serverResponse.body || {}; + let aucId = serverResponse.id; // this will be correct for single requests and non-single if (!serverResponse.hasOwnProperty('seatbid')) { return []; } @@ -351,7 +358,7 @@ export const spec = { logInfo(`newspassid.enhancedAdserverTargeting is set to false, no per-bid keys will be sent to adserver.`); } let {seat: winningSeat, bid: winningBid} = this.getWinnerForRequestBid(thisBid.bidId, serverResponse.seatbid); - adserverTargeting['np_auc_id'] = String(request.bidderRequest.auctionId); + adserverTargeting['np_auc_id'] = String(aucId); adserverTargeting['np_winner'] = String(winningSeat); adserverTargeting['np_bid'] = 'true'; if (enhancedAdserverTargeting) { @@ -490,10 +497,29 @@ export const spec = { return null; }, getGetParametersAsObject() { - let parsed = parseUrl(getRefererInfo().page); + let parsed = parseUrl(this.getRefererInfo().location); // was getRefererInfo().page but this is not backwards compatible logInfo('getGetParametersAsObject found:', parsed.search); return parsed.search; }, + getRefererInfo() { + if (getRefererInfo().hasOwnProperty('location')) { + logInfo('FOUND location on getRefererInfo OK (prebid >= 7); will use getRefererInfo for location & page'); + return getRefererInfo(); + } else { + logInfo('DID NOT FIND location on getRefererInfo (prebid < 7); will use legacy code that ALWAYS worked reliably to get location & page ;-)'); + try { + return { + page: top.location.href, + location: top.location.href + }; + } catch (e) { + return { + page: window.location.href, + location: window.location.href + }; + } + } + }, blockTheRequest() { let npRequest = config.getConfig('newspassid.np_request'); if (typeof npRequest == 'boolean' && !npRequest) { diff --git a/modules/nextMillenniumBidAdapter.js b/modules/nextMillenniumBidAdapter.js index cb660ad9fd6..0af2129de61 100644 --- a/modules/nextMillenniumBidAdapter.js +++ b/modules/nextMillenniumBidAdapter.js @@ -2,6 +2,7 @@ import { _each, createTrackPixelHtml, deepAccess, + deepSetValue, getBidIdParameter, getDefinedParams, getWindowTop, @@ -13,6 +14,7 @@ import { triggerPixel, } from '../src/utils.js'; +import {getGlobal} from '../src/prebidGlobal.js'; import CONSTANTS from '../src/constants.json'; import {BANNER, VIDEO} from '../src/mediaTypes.js'; import {config} from '../src/config.js'; @@ -21,17 +23,34 @@ import * as events from '../src/events.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import {getRefererInfo} from '../src/refererDetection.js'; +const NM_VERSION = '3.1.0'; +const GVLID = 1060; 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 SYNC_ENDPOINT = 'https://cookies.nextmillmedia.com/sync?gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&gpp={{.GPP}}&gpp_sid={{.GPPSID}}&type={{.TYPE_PIXEL}}'; const REPORT_ENDPOINT = 'https://report2.hb.brainlyads.com/statistics/metric'; const TIME_TO_LIVE = 360; +const DEFAULT_CURRENCY = 'USD'; + const VIDEO_PARAMS = [ - 'api', 'linearity', 'maxduration', 'mimes', 'minduration', 'placement', - 'playbackmethod', 'protocols', 'startdelay' + 'api', + 'linearity', + 'maxduration', + 'mimes', + 'minduration', + 'placement', + 'playbackmethod', + 'protocols', + 'startdelay', +]; + +const ALLOWED_ORTB2_PARAMETERS = [ + 'site.pagecat', + 'site.content.cat', + 'site.content.language', + 'device.sua', ]; -const GVLID = 1060; const sendingDataStatistic = initSendingDataStatistic(); events.on(CONSTANTS.EVENTS.AUCTION_INIT, auctionInitHandler); @@ -62,85 +81,39 @@ export const spec = { const id = getPlacementId(bid); const auctionId = bid.auctionId; const bidId = bid.bidId; - let sizes = bid.sizes; - if (sizes && !Array.isArray(sizes[0])) sizes = [sizes]; const site = getSiteObj(); const device = getDeviceObj(); + const {cur, mediaTypes} = getCurrency(bid); const postBody = { - 'id': bidderRequest?.bidderRequestId, - 'ext': { - 'prebid': { - 'storedrequest': { - 'id': id - } + id: bidderRequest?.bidderRequestId, + cur, + ext: { + prebid: { + storedrequest: { + id, + }, }, - 'nextMillennium': { - 'refresh_count': window.nmmRefreshCounts[bid.adUnitCode]++, - 'elOffsets': getBoundingClient(bid), - 'scrollTop': window.pageYOffset || document.documentElement.scrollTop - } + nextMillennium: { + nm_version: NM_VERSION, + pbjs_version: getGlobal()?.version || undefined, + refresh_count: window.nmmRefreshCounts[bid.adUnitCode]++, + elOffsets: getBoundingClient(bid), + scrollTop: window.pageYOffset || document.documentElement.scrollTop, + }, }, device, site, - imp: [] + imp: [], }; - const imp = { - id: bid.adUnitCode, - ext: { - prebid: { - storedrequest: {id} - } - } - }; - - if (deepAccess(bid, 'mediaTypes.banner')) { - imp.banner = { - format: (sizes || []).map(s => { return {w: s[0], h: s[1]} }) - }; - }; - - const video = deepAccess(bid, 'mediaTypes.video'); - if (video) { - imp.video = getDefinedParams(video, VIDEO_PARAMS); - if (video.playerSize) { - imp.video = Object.assign( - imp.video, parseGPTSingleSizeArrayToRtbSize(video.playerSize[0]) || {} - ); - } else if (video.w && video.h) { - imp.video.w = video.w; - imp.video.h = video.h; - }; - }; - - postBody.imp.push(imp); - - const gdprConsent = bidderRequest && bidderRequest.gdprConsent; - const uspConsent = bidderRequest && bidderRequest.uspConsent; - - if (gdprConsent || uspConsent) { - postBody.regs = { ext: {} }; - - if (uspConsent) { - postBody.regs.ext.us_privacy = uspConsent; - }; - - if (gdprConsent) { - if (typeof gdprConsent.gdprApplies !== 'undefined') { - postBody.regs.ext.gdpr = gdprConsent.gdprApplies ? 1 : 0; - }; - - if (typeof gdprConsent.consentString !== 'undefined') { - postBody.user = { - ext: { consent: gdprConsent.consentString } - }; - }; - }; - }; + postBody.imp.push(getImp(bid, id, mediaTypes)); + setConsentStrings(postBody, bidderRequest); + setOrtb2Parameters(postBody, bidderRequest?.ortb2); + setEids(postBody, bid); const urlParameters = parseUrl(getWindowTop().location.href).search; const isTest = urlParameters['pbs'] && urlParameters['pbs'] === 'test'; @@ -152,7 +125,7 @@ export const spec = { data: JSON.stringify(postBody), options: { contentType: 'text/plain', - withCredentials: true + withCredentials: true, }, bidId, @@ -174,6 +147,7 @@ export const spec = { const params = bidRequest.params; const auctionId = bidRequest.auctionId; const wurl = deepAccess(bid, 'ext.prebid.events.win'); + // TODO: fix auctionId leak: https://github.com/prebid/Prebid.js/issues/9781 addWurl({auctionId, requestId, wurl}); @@ -186,12 +160,12 @@ export const spec = { width: bid.w, height: bid.h, creativeId: bid.adid, - currency: response.cur, + currency: response.cur || DEFAULT_CURRENCY, netRevenue: true, ttl: TIME_TO_LIVE, meta: { - advertiserDomains: bid.adomain || [] - } + advertiserDomains: bid.adomain || [], + }, }; if (vastUrl || vastXml) { @@ -211,38 +185,29 @@ export const spec = { return bidResponses; }, - getUserSyncs: function (syncOptions, responses, gdprConsent, uspConsent) { + getUserSyncs: function (syncOptions, responses, gdprConsent, uspConsent, gppConsent) { + if (!syncOptions.iframeEnabled && !syncOptions.pixelEnabled) return []; + const pixels = []; + const getSetPixelFunc = type => url => { pixels.push({type, url: replaceUsersyncMacros(url, gdprConsent, uspConsent, gppConsent, type)}) }; + const getSetPixelsFunc = type => response => { deepAccess(response, `body.ext.sync.${type}`, []).forEach(getSetPixelFunc(type)) }; + + const setPixel = (type, url) => { (getSetPixelFunc(type))(url) }; + const setPixelImages = getSetPixelsFunc('image'); + const setPixelIframes = getSetPixelsFunc('iframe'); 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) { - deepAccess(response, 'body.ext.sync.iframe', []).forEach(iframeUrl => { - pixels.push({ - type: 'iframe', - url: replaceUsersyncMacros(iframeUrl, gdprConsent, uspConsent) - }); - }) - } + if (syncOptions.pixelEnabled) setPixelImages(response); + if (syncOptions.iframeEnabled) setPixelIframes(response); }) } 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'}); + if (syncOptions.pixelEnabled) setPixel('image', SYNC_ENDPOINT); + if (syncOptions.iframeEnabled) setPixel('iframe', SYNC_ENDPOINT); } + return pixels; }, @@ -282,27 +247,131 @@ export const spec = { }, }; -function replaceUsersyncMacros(url, gdprConsent, uspConsent) { - const { consentString, gdprApplies } = gdprConsent || {}; +export function getImp(bid, id, mediaTypes) { + const {banner, video} = mediaTypes; + const imp = { + id: bid.adUnitCode, + ext: { + prebid: { + storedrequest: { + id, + }, + }, + }, + }; - if (gdprApplies) { - const gdpr = Number(gdprApplies); - url = url.replace('{{.GDPR}}', gdpr); + if (banner) { + if (banner.bidfloorcur) imp.bidfloorcur = banner.bidfloorcur; + if (banner.bidfloor) imp.bidfloor = banner.bidfloor; - if (gdpr == 1 && consentString && consentString.length > 0) { - url = url.replace('{{.GDPRConsent}}', consentString); - } - } else { - url = url.replace('{{.GDPR}}', 0); - url = url.replace('{{.GDPRConsent}}', ''); - } + imp.banner = { + format: (banner.data?.sizes || []).map(s => { return {w: s[0], h: s[1]} }), + }; + }; + + if (video) { + if (video.bidfloorcur) imp.bidfloorcur = video.bidfloorcur; + if (video.bidfloor) imp.bidfloor = video.bidfloor; - if (uspConsent) { - url = url.replace('{{.USPrivacy}}', uspConsent); + imp.video = getDefinedParams(video, VIDEO_PARAMS); + if (video.data.playerSize) { + imp.video = Object.assign(imp.video, parseGPTSingleSizeArrayToRtbSize(video.data.playerSize) || {}); + } else if (video.w && video.h) { + imp.video.w = video.w; + imp.video.h = video.h; + }; + }; + + return imp; +}; + +export function setConsentStrings(postBody = {}, bidderRequest) { + const gdprConsent = bidderRequest?.gdprConsent; + const uspConsent = bidderRequest?.uspConsent; + let gppConsent = bidderRequest?.gppConsent?.gppString && bidderRequest?.gppConsent; + if (!gppConsent && bidderRequest?.ortb2?.regs?.gpp) gppConsent = bidderRequest?.ortb2?.regs; + + if (gdprConsent || uspConsent || gppConsent) { + postBody.regs = { ext: {} }; + + if (uspConsent) { + postBody.regs.ext.us_privacy = uspConsent; + }; + + if (gppConsent) { + postBody.regs.gpp = gppConsent?.gppString || gppConsent?.gpp; + postBody.regs.gpp_sid = bidderRequest.gppConsent?.applicableSections || gppConsent?.gpp_sid; + }; + + if (gdprConsent) { + if (typeof gdprConsent.gdprApplies !== 'undefined') { + postBody.regs.ext.gdpr = gdprConsent.gdprApplies ? 1 : 0; + }; + + if (typeof gdprConsent.consentString !== 'undefined') { + postBody.user = { + ext: { consent: gdprConsent.consentString }, + }; + }; + }; + }; +}; + +export function setOrtb2Parameters(postBody, ortb2 = {}) { + for (let parameter of ALLOWED_ORTB2_PARAMETERS) { + const value = deepAccess(ortb2, parameter); + if (value) deepSetValue(postBody, parameter, value); } +} + +export function setEids(postBody, bid) { + if (!isArray(bid.userIdAsEids) || !bid.userIdAsEids.length) return; + + deepSetValue(postBody, 'user.eids', bid.userIdAsEids); +} + +export function replaceUsersyncMacros(url, gdprConsent = {}, uspConsent = '', gppConsent = {}, type = '') { + const { consentString = '', gdprApplies = false } = gdprConsent; + const gdpr = Number(gdprApplies); + url = url + .replace('{{.GDPR}}', gdpr) + .replace('{{.GDPRConsent}}', consentString) + .replace('{{.USPrivacy}}', uspConsent) + .replace('{{.GPP}}', gppConsent.gppString || '') + .replace('{{.GPPSID}}', (gppConsent.applicableSections || []).join(',')) + .replace('{{.TYPE_PIXEL}}', type); return url; -}; +} + +function getCurrency(bid = {}) { + const currency = config?.getConfig('currency')?.adServerCurrency || DEFAULT_CURRENCY; + const cur = []; + const types = ['banner', 'video']; + const mediaTypes = {}; + for (const mediaType of types) { + const mediaTypeData = deepAccess(bid, `mediaTypes.${mediaType}`); + if (mediaTypeData) { + mediaTypes[mediaType] = {data: mediaTypeData}; + } else { + continue; + }; + + if (typeof bid.getFloor === 'function') { + let floorInfo = bid.getFloor({currency, mediaType, size: '*'}); + mediaTypes[mediaType].bidfloorcur = floorInfo.currency; + mediaTypes[mediaType].bidfloor = floorInfo.floor; + } else { + mediaTypes[mediaType].bidfloorcur = currency; + }; + + if (cur.includes(mediaTypes[mediaType].bidfloorcur)) cur.push(mediaTypes[mediaType].bidfloorcur); + }; + + if (!cur.length) cur.push(DEFAULT_CURRENCY); + + return {cur, mediaTypes}; +} function getAdEl(bid) { // best way I could think of to get El, is by matching adUnitCode to google slots... @@ -369,7 +438,7 @@ function getAd(bid) { } else if (bid.nurl) { adUrl = bid.nurl; }; - } + }; return {ad, adUrl, vastXml, vastUrl}; } @@ -377,10 +446,21 @@ function getAd(bid) { function getSiteObj() { const refInfo = (getRefererInfo && getRefererInfo()) || {}; + let language = navigator.language; + let content; + if (language) { + // get ISO-639-1-alpha-2 (2 character language) + language = language.split('-')[0]; + content = { + language, + }; + }; + return { page: refInfo.page, ref: refInfo.ref, - domain: refInfo.domain + domain: refInfo.domain, + content, }; } @@ -388,6 +468,19 @@ function getDeviceObj() { return { w: window.innerWidth || window.document.documentElement.clientWidth || window.document.body.clientWidth || 0, h: window.innerHeight || window.document.documentElement.clientHeight || window.document.body.clientHeight || 0, + ua: window.navigator.userAgent || undefined, + sua: getSua(), + }; +} + +function getSua() { + let {brands, mobile, platform} = (window?.navigator?.userAgentData || {}); + if (!(brands && platform)) return undefined; + + return { + brands, + mobile: Number(!!mobile), + platform: (platform && {brand: platform}) || undefined, }; } diff --git a/modules/nextrollBidAdapter.js b/modules/nextrollBidAdapter.js index 0dd4b334f6e..8a41efe4dcc 100644 --- a/modules/nextrollBidAdapter.js +++ b/modules/nextrollBidAdapter.js @@ -1,6 +1,5 @@ import { - deepAccess, - getBidIdParameter, + deepAccess, getBidIdParameter, isArray, isFn, isNumber, @@ -15,6 +14,12 @@ import { convertOrtbRequestToProprietaryNative } from '../src/native.js'; import {find} from '../src/polyfill.js'; +/** + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid + * @typedef {import('../src/adapters/bidderFactory.js').ServerResponse} ServerResponse + * @typedef {import('../src/adapters/bidderFactory.js').validBidRequests} validBidRequests + */ const BIDDER_CODE = 'nextroll'; const BIDDER_ENDPOINT = 'https://d.adroll.com/bid/prebid/'; const ADAPTER_VERSION = 5; diff --git a/modules/nexx360BidAdapter.js b/modules/nexx360BidAdapter.js index 671cc800980..baadaa272e6 100644 --- a/modules/nexx360BidAdapter.js +++ b/modules/nexx360BidAdapter.js @@ -8,12 +8,21 @@ import {getGlobal} from '../src/prebidGlobal.js'; import {ortbConverter} from '../libraries/ortbConverter/converter.js' import { INSTREAM, OUTSTREAM } from '../src/video.js'; +/** + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid + * @typedef {import('../src/adapters/bidderFactory.js').ServerResponse} ServerResponse + * @typedef {import('../src/adapters/bidderFactory.js').SyncOptions} SyncOptions + * @typedef {import('../src/adapters/bidderFactory.js').UserSync} UserSync + * @typedef {import('../src/adapters/bidderFactory.js').validBidRequests} validBidRequests + */ + const OUTSTREAM_RENDERER_URL = 'https://acdn.adnxs.com/video/outstream/ANOutstreamVideo.js'; const BIDDER_CODE = 'nexx360'; const REQUEST_URL = 'https://fast.nexx360.io/booster'; const PAGE_VIEW_ID = generateUUID(); -const BIDDER_VERSION = '2.0'; +const BIDDER_VERSION = '3.0'; const GVLID = 965; const NEXXID_KEY = 'nexx360_storage'; @@ -190,9 +199,15 @@ function interpretResponse(serverResponse) { demandSource: bid.ext.ssp, }, }; - if (allowAlternateBidderCodes) response.bidderCode = `n360-${bid.ext.ssp}`; + if (allowAlternateBidderCodes) response.bidderCode = `n360_${bid.ext.ssp}`; - if (bid.ext.mediaType === BANNER) response.adUrl = bid.ext.adUrl; + if (bid.ext.mediaType === BANNER) { + if (bid.adm) { + response.ad = bid.adm; + } else { + response.adUrl = bid.ext.adUrl; + } + } if ([INSTREAM, OUTSTREAM].includes(bid.ext.mediaType)) response.vastXml = bid.ext.vastXml; if (bid.ext.mediaType === OUTSTREAM) { diff --git a/modules/nobidAnalyticsAdapter.js b/modules/nobidAnalyticsAdapter.js new file mode 100644 index 00000000000..3a7912c37e1 --- /dev/null +++ b/modules/nobidAnalyticsAdapter.js @@ -0,0 +1,242 @@ +import {deepClone, logError, getParameterByName} from '../src/utils.js'; +import {ajax} from '../src/ajax.js'; +import {getStorageManager} from '../src/storageManager.js'; +import adapter from '../libraries/analyticsAdapter/AnalyticsAdapter.js'; +import CONSTANTS from '../src/constants.json'; +import adapterManager from '../src/adapterManager.js'; +import {MODULE_TYPE_ANALYTICS} from '../src/activities/modules.js'; + +const VERSION = '1.1.0'; +const MODULE_NAME = 'nobidAnalyticsAdapter'; +const ANALYTICS_OPT_FLUSH_TIMEOUT_SECONDS = 5 * 1000; +const RETENTION_SECONDS = 1 * 24 * 3600; +const TEST_ALLOCATION_PERCENTAGE = 5; // dont block 5% of the time; +window.nobidAnalyticsVersion = VERSION; +const analyticsType = 'endpoint'; +const url = 'localhost:8383/event'; +const GVLID = 816; +const storage = getStorageManager({gvlid: GVLID, moduleName: MODULE_NAME, moduleType: MODULE_TYPE_ANALYTICS}); +const { + EVENTS: { + AUCTION_INIT, + BID_REQUESTED, + BID_TIMEOUT, + BID_RESPONSE, + BID_WON, + AUCTION_END, + AD_RENDER_SUCCEEDED + } +} = CONSTANTS; +function log (msg) { + // eslint-disable-next-line no-console + console.log(`%cNoBid Analytics ${VERSION}`, 'padding: 2px 8px 2px 8px; background-color:#f50057; color: white', msg); +} +function isJson (str) { + return str && str.startsWith('{') && str.endsWith('}'); +} +function isExpired (data, retentionSeconds) { + retentionSeconds = retentionSeconds || RETENTION_SECONDS; + if (data.ts + retentionSeconds * 1000 < Date.now()) return true; + return false; +} +function sendEvent (event, eventType) { + function resolveEndpoint() { + var ret = 'https://carbon-nv.servenobids.com/admin/status'; + var env = (typeof getParameterByName === 'function') && (getParameterByName('nobid-env')); + env = window.location.href.indexOf('nobid-env=dev') > 0 ? 'dev' : env; + if (!env) ret = 'https://carbon-nv.servenobids.com'; + else if (env == 'dev') ret = 'https://localhost:8383'; + return ret; + } + if (!nobidAnalytics.initOptions || !nobidAnalytics.initOptions.siteId || !event) return; + if (nobidAnalytics.isAnalyticsDisabled()) { + log('NoBid Analytics is Disabled'); + return; + } + try { + const endpoint = `${resolveEndpoint()}/event/${eventType}?pubid=${nobidAnalytics.initOptions.siteId}`; + ajax(endpoint, + function (response) { + try { + nobidAnalytics.processServerResponse(response); + } catch (e) { + logError(e); + } + }, + JSON.stringify(event), + { + contentType: 'application/json', + method: 'POST' + } + ); + } catch (err) { + log(`Sending event error ${err}`); + } +} +function cleanupObjectAttributes (obj, attributes) { + if (!obj) return; + if (Array.isArray(obj)) { + obj.forEach(item => { + Object.keys(item).forEach(attr => { if (!attributes.includes(attr)) delete item[attr] }); + }); + } else Object.keys(obj).forEach(attr => { if (!attributes.includes(attr)) delete obj[attr] }); +} +function sendBidWonEvent (event, eventType) { + const data = deepClone(event); + cleanupObjectAttributes(data, ['bidderCode', 'size', 'statusMessage', 'adId', 'requestId', 'mediaType', 'adUnitCode', 'cpm', 'timeToRespond']); + if (nobidAnalytics.topLocation) data.topLocation = nobidAnalytics.topLocation; + sendEvent(data, eventType); +} +function sendAuctionEndEvent (event, eventType) { + if (event?.bidderRequests?.length > 0 && event?.bidderRequests[0]?.refererInfo?.topmostLocation) { + nobidAnalytics.topLocation = event.bidderRequests[0].refererInfo.topmostLocation; + } + const data = deepClone(event); + + cleanupObjectAttributes(data, ['timestamp', 'timeout', 'auctionId', 'bidderRequests', 'bidsReceived']); + if (data) cleanupObjectAttributes(data.bidderRequests, ['bidderCode', 'bidderRequestId', 'bids', 'refererInfo']); + if (data) cleanupObjectAttributes(data.bidsReceived, ['bidderCode', 'width', 'height', 'adUnitCode', 'statusMessage', 'requestId', 'mediaType', 'cpm']); + if (data) cleanupObjectAttributes(data.noBids, ['bidder', 'sizes', 'bidId']); + if (data.bidderRequests) cleanupObjectAttributes(data.bidderRequests.bids, ['mediaTypes', 'adUnitCode', 'sizes', 'bidId']); + if (data.bidderRequests) cleanupObjectAttributes(data.bidderRequests.refererInfo, ['topmostLocation']); + sendEvent(data, eventType); +} +function auctionInit (event) { + if (event?.bidderRequests?.length > 0 && event?.bidderRequests[0]?.refererInfo?.topmostLocation) { + nobidAnalytics.topLocation = event.bidderRequests[0].refererInfo.topmostLocation; + } +} +let nobidAnalytics = Object.assign(adapter({url, analyticsType}), { + track({ eventType, args }) { + switch (eventType) { + case AUCTION_INIT: + auctionInit(args); + break; + case BID_REQUESTED: + break; + case BID_RESPONSE: + break; + case BID_WON: + sendBidWonEvent(args, eventType); + break; + case BID_TIMEOUT: + break; + case AUCTION_END: + sendAuctionEndEvent(args, eventType); + break; + case AD_RENDER_SUCCEEDED: + break; + default: + break; + } + } +}); + +nobidAnalytics = { + ...nobidAnalytics, + originEnableAnalytics: nobidAnalytics.enableAnalytics, // save the base class function + enableAnalytics: function (config) { // override enableAnalytics so we can get access to the config passed in from the page + if (!config.options.siteId) { + logError('NoBid Analytics - siteId parameter is not defined. Analytics won\'t work'); + return; + } + this.initOptions = config.options; + this.originEnableAnalytics(config); // call the base class function + }, + retentionSeconds: RETENTION_SECONDS, + isExpired (data) { + return isExpired(data, this.retentionSeconds); + }, + isAnalyticsDisabled () { + let stored = storage.getDataFromLocalStorage(this.ANALYTICS_DATA_NAME); + if (!isJson(stored)) return false; + stored = JSON.parse(stored); + if (this.isExpired(stored)) return false; + return stored.disabled; + }, + processServerResponse (response) { + if (!isJson(response)) return; + const resp = JSON.parse(response); + storage.setDataInLocalStorage(this.ANALYTICS_DATA_NAME, JSON.stringify({ ...resp, ts: Date.now() })); + }, + ANALYTICS_DATA_NAME: 'analytics.nobid.io', + ANALYTICS_OPT_NAME: 'analytics.nobid.io.optData' +} + +adapterManager.registerAnalyticsAdapter({ + adapter: nobidAnalytics, + code: 'nobidAnalytics', + gvlid: GVLID +}); +nobidAnalytics.originalAdUnits = {}; +window.nobidCarbonizer = { + getStoredLocalData: function () { + const a = storage.getDataFromLocalStorage(nobidAnalytics.ANALYTICS_DATA_NAME); + const b = storage.getDataFromLocalStorage(nobidAnalytics.ANALYTICS_OPT_NAME); + const ret = {}; + if (a) ret[nobidAnalytics.ANALYTICS_DATA_NAME] = a; + if (b) ret[nobidAnalytics.ANALYTICS_OPT_NAME] = b + return ret; + }, + isActive: function () { + let stored = storage.getDataFromLocalStorage(nobidAnalytics.ANALYTICS_DATA_NAME); + if (!isJson(stored)) return false; + stored = JSON.parse(stored); + if (isExpired(stored, nobidAnalytics.retentionSeconds)) return false; + return stored.carbonizer_active || false; + }, + carbonizeAdunits: function (adunits, skipTestGroup) { + function processBlockedBidders (blockedBidders) { + function sendOptimizerData() { + let optData = storage.getDataFromLocalStorage(nobidAnalytics.ANALYTICS_OPT_NAME); + storage.removeDataFromLocalStorage(nobidAnalytics.ANALYTICS_OPT_NAME); + if (isJson(optData)) { + optData = JSON.parse(optData); + if (Object.getOwnPropertyNames(optData).length > 0) { + const event = { o_bidders: optData }; + if (nobidAnalytics.topLocation) event.topLocation = nobidAnalytics.topLocation; + sendEvent(event, 'optData'); + } + } + } + if (blockedBidders && blockedBidders.length > 0) { + let optData = storage.getDataFromLocalStorage(nobidAnalytics.ANALYTICS_OPT_NAME); + optData = isJson(optData) ? JSON.parse(optData) : {}; + const bidders = blockedBidders.map(rec => rec.bidder); + if (bidders && bidders.length > 0) { + bidders.forEach(bidder => { + if (!optData[bidder]) optData[bidder] = 1; + else optData[bidder] += 1; + }); + storage.setDataInLocalStorage(nobidAnalytics.ANALYTICS_OPT_NAME, JSON.stringify(optData)); + if (window.nobidAnalyticsOptTimer) return; + window.nobidAnalyticsOptTimer = setInterval(sendOptimizerData, ANALYTICS_OPT_FLUSH_TIMEOUT_SECONDS); + } + } + } + function carbonizeAdunit (adunit) { + let stored = storage.getDataFromLocalStorage(nobidAnalytics.ANALYTICS_DATA_NAME); + if (!isJson(stored)) return; + stored = JSON.parse(stored); + if (isExpired(stored, nobidAnalytics.retentionSeconds)) return; + const carbonizerBidders = stored.bidders || []; + let originalAdUnit = null; + if (nobidAnalytics.originalAdUnits && nobidAnalytics.originalAdUnits[adunit.code]) originalAdUnit = nobidAnalytics.originalAdUnits[adunit.code]; + const allowedBidders = originalAdUnit.bids.filter(rec => carbonizerBidders.includes(rec.bidder)); + const blockedBidders = originalAdUnit.bids.filter(rec => !carbonizerBidders.includes(rec.bidder)); + processBlockedBidders(blockedBidders); + adunit.bids = allowedBidders; + } + for (const adunit of adunits) { + if (!nobidAnalytics.originalAdUnits[adunit.code]) nobidAnalytics.originalAdUnits[adunit.code] = JSON.parse(JSON.stringify(adunit)); + }; + if (this.isActive()) { + // 5% of the time do not block; + if (!skipTestGroup && Math.floor(Math.random() * 101) <= TEST_ALLOCATION_PERCENTAGE) return; + for (const adunit of adunits) { + carbonizeAdunit(adunit); + }; + } + } +}; +export default nobidAnalytics; diff --git a/modules/nobidAnalyticsAdapter.md b/modules/nobidAnalyticsAdapter.md new file mode 100644 index 00000000000..92b9bdbb3cb --- /dev/null +++ b/modules/nobidAnalyticsAdapter.md @@ -0,0 +1,38 @@ +# Overview +Module Name: NoBid Analytics Adapter + +Module Type: Analytics Adapter + +Maintainer: [nobid.io](https://nobid.io) + + +# NoBid Analytics Registration + +The NoBid Analytics Adapter is free to use during our Beta period, but requires a simple registration with NoBid. Please visit [www.nobid.io](https://www.nobid.io/contact-1/) to sign up and request your NoBid Site ID to get started. If you're already using the NoBid Prebid Adapter, you may use your existing Site ID with the NoBid Analytics Adapter. + +The NoBid privacy policy is at [nobid.io/privacy-policy](https://www.nobid.io/privacy-policy/). + +## NoBid Analytics Configuration + +First, make sure to add the NoBid Analytics submodule to your Prebid.js package with: + +``` +gulp build --modules=...,nobidAnalyticsAdapter... +``` + +The following configuration parameters are available: + +```javascript +pbjs.enableAnalytics({ + provider: 'nobidAnalytics', + options: { + siteId: 123 // change to the Site ID you received from NoBid + } +}); +``` + +{: .table .table-bordered .table-striped } +| Parameter | Scope | Type | Description | Example | +| --- | --- | --- | --- | --- | +| provider | Required | String | The name of this module: `nobidAnalytics` | `nobidAnalytics` | +| options.siteId | Required | Number | This is the NoBid Site ID Number obtained from registering with NoBid. | `1234` | diff --git a/modules/nobidBidAdapter.js b/modules/nobidBidAdapter.js index 7bf1c4b80db..28fb38e14e5 100644 --- a/modules/nobidBidAdapter.js +++ b/modules/nobidBidAdapter.js @@ -3,7 +3,16 @@ import { config } from '../src/config.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; import { BANNER, VIDEO } from '../src/mediaTypes.js'; import { getStorageManager } from '../src/storageManager.js'; -import {hasPurpose1Consent} from '../src/utils/gpdr.js'; +import { hasPurpose1Consent } from '../src/utils/gpdr.js'; + +/** + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid + * @typedef {import('../src/adapters/bidderFactory.js').ServerResponse} ServerResponse + * @typedef {import('../src/adapters/bidderFactory.js').SyncOptions} SyncOptions + * @typedef {import('../src/adapters/bidderFactory.js').UserSync} UserSync + * @typedef {import('../src/adapters/bidderFactory.js').validBidRequests} validBidRequests + */ const GVLID = 816; const BIDDER_CODE = 'nobid'; @@ -63,6 +72,19 @@ function nobidBuildRequests(bids, bidderRequest) { } return uspConsent; } + var gppConsent = function(bidderRequest) { + let gppConsent = null; + if (bidderRequest?.gppConsent?.gppString && bidderRequest?.gppConsent?.applicableSections) { + gppConsent = {}; + gppConsent.gpp = bidderRequest.gppConsent.gppString; + gppConsent.gpp_sid = Array.isArray(bidderRequest.gppConsent.applicableSections) ? bidderRequest.gppConsent.applicableSections : []; + } else if (bidderRequest?.ortb2?.regs?.gpp && bidderRequest?.ortb2.regs?.gpp_sid) { + gppConsent = {}; + gppConsent.gpp = bidderRequest.ortb2.regs.gpp; + gppConsent.gpp_sid = Array.isArray(bidderRequest.ortb2.regs.gpp_sid) ? bidderRequest.ortb2.regs.gpp_sid : []; + } + return gppConsent; + } var schain = function(bids) { if (bids && bids.length > 0) { return bids[0].schain @@ -145,6 +167,9 @@ function nobidBuildRequests(bids, bidderRequest) { if (cop) state['coppa'] = cop; const eids = getEIDs(deepAccess(bids, '0.userIdAsEids')); if (eids && eids.length > 0) state['eids'] = eids; + const gpp = gppConsent(bidderRequest); + if (gpp?.gpp) state['gpp'] = gpp.gpp; + if (gpp?.gpp_sid) state['gpp_sid'] = gpp.gpp_sid; if (bidderRequest && bidderRequest.ortb2) state['ortb2'] = bidderRequest.ortb2; return state; }; @@ -230,9 +255,9 @@ function nobidBuildRequests(bids, bidderRequest) { siteId = (typeof bid.params['siteId'] != 'undefined' && bid.params['siteId']) ? bid.params['siteId'] : siteId; var placementId = bid.params['placementId']; - var adType = 'banner'; + let adType = 'banner'; const videoMediaType = deepAccess(bid, 'mediaTypes.video'); - const context = deepAccess(bid, 'mediaTypes.video.context'); + const context = deepAccess(bid, 'mediaTypes.video.context') || ''; if (bid.mediaType === VIDEO || (videoMediaType && (context === 'instream' || context === 'outstream'))) { adType = 'video'; } @@ -246,7 +271,8 @@ function nobidBuildRequests(bids, bidderRequest) { placementId: placementId, ad_type: adType, params: bid.params, - floor: floor + floor: floor, + ctx: context }, adunits); } @@ -350,25 +376,26 @@ export const spec = { ], 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. - */ + * 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) { log('isBidRequestValid', bid); return !!bid.params.siteId; }, /** - * Make a server request from the list of BidRequests. - * - * @param {validBidRequests[]} - an array of bids - * @return ServerRequest Info describing the request to the server. - */ + * 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) { function resolveEndpoint() { var ret = 'https://ads.servenobid.com/'; var env = (typeof getParameterByName === 'function') && (getParameterByName('nobid-env')); + env = window.location.href.indexOf('nobid-env=dev') > 0 ? 'dev' : env; if (!env) ret = 'https://ads.servenobid.com/'; else if (env == 'beta') ret = 'https://beta.servenobid.com/'; else if (env == 'dev') ret = '//localhost:8282/'; @@ -403,11 +430,11 @@ export const spec = { }; }, /** - * 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. - */ + * 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) { log('interpretResponse -> serverResponse', serverResponse); log('interpretResponse -> bidRequest', bidRequest); @@ -415,13 +442,13 @@ export const spec = { }, /** - * 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, usPrivacy) { + * 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, usPrivacy, gppConsent) { if (syncOptions.iframeEnabled) { let params = ''; if (gdprConsent && typeof gdprConsent.consentString === 'string') { @@ -437,6 +464,12 @@ export const spec = { else params += '?'; params += 'usp_consent=' + usPrivacy; } + if (gppConsent?.gppString && gppConsent?.applicableSections?.length) { + if (params.length > 0) params += '&'; + else params += '?'; + params += 'gpp=' + encodeURIComponent(gppConsent.gppString); + params += 'gpp_sid=' + encodeURIComponent(gppConsent.applicableSections.join(',')); + } return [{ type: 'iframe', url: 'https://public.servenobid.com/sync.html' + params @@ -459,9 +492,9 @@ export const spec = { }, /** - * Register bidder specific code, which will execute if bidder timed out after an auction - * @param {data} Containing timeout specific data - */ + * Register bidder specific code, which will execute if bidder timed out after an auction + * @param {data} Containing timeout specific data + */ onTimeout: function(data) { window.nobid.timeoutTotal++; log('Timeout total: ' + window.nobid.timeoutTotal, data); diff --git a/modules/nobidBidAdapter.md b/modules/nobidBidAdapter.md index 4449ad5c88b..e2f1c75e782 100644 --- a/modules/nobidBidAdapter.md +++ b/modules/nobidBidAdapter.md @@ -7,6 +7,7 @@ hide: true media_types: banner, video gdpr_supported: true usp_supported: true +gpp_supported: true --- ### Bid Params diff --git a/modules/novatiqIdSystem.js b/modules/novatiqIdSystem.js index 7a801a945ae..b6eab776df2 100644 --- a/modules/novatiqIdSystem.js +++ b/modules/novatiqIdSystem.js @@ -11,15 +11,20 @@ import { submodule } from '../src/hook.js'; import {getStorageManager} from '../src/storageManager.js'; import {MODULE_TYPE_UID} from '../src/activities/modules.js'; +/** + * @typedef {import('../modules/userId/index.js').Submodule} Submodule + * @typedef {import('../modules/userId/index.js').SubmoduleConfig} SubmoduleConfig + */ + const MODULE_NAME = 'novatiq'; /** @type {Submodule} */ export const novatiqIdSubmodule = { /** - * used to link submodule with config - * @type {string} - */ + * used to link submodule with config + * @type {string} + */ name: MODULE_NAME, /** * used to specify vendor id @@ -28,10 +33,10 @@ export const novatiqIdSubmodule = { gvlid: 1119, /** - * decode the stored id value for passing to bid requests - * @function - * @returns {novatiq: {snowflake: string}} - */ + * decode the stored id value for passing to bid requests + * @function + * @returns {novatiq: {snowflake: string}} + */ decode(novatiqId, config) { let responseObj = { novatiq: { @@ -52,11 +57,11 @@ export const novatiqIdSubmodule = { }, /** - * performs action to obtain id and return a value in the callback's response argument - * @function - * @param {SubmoduleConfig} config - * @returns {id: string} - */ + * performs action to obtain id and return a value in the callback's response argument + * @function + * @param {SubmoduleConfig} config + * @returns {id: string} + */ getId(config) { const configParams = config.params || {}; const urlParams = this.getUrlParams(configParams); @@ -259,6 +264,18 @@ export const novatiqIdSubmodule = { srcId = configParams.sourceid; } return srcId; + }, + eids: { + 'novatiq': { + getValue: function(data) { + if (data.snowflake.id === undefined) { + return data.snowflake; + } + + return data.snowflake.id; + }, + source: 'novatiq.com', + }, } }; submodule('userId', novatiqIdSubmodule); diff --git a/modules/oguryBidAdapter.js b/modules/oguryBidAdapter.js index 718ae44c83c..9937391f6e7 100644 --- a/modules/oguryBidAdapter.js +++ b/modules/oguryBidAdapter.js @@ -1,9 +1,10 @@ 'use strict'; import {BANNER} from '../src/mediaTypes.js'; -import {getAdUnitSizes, getWindowSelf, getWindowTop, isFn, logWarn} from '../src/utils.js'; +import {getWindowSelf, getWindowTop, isFn, logWarn} from '../src/utils.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import {ajax} from '../src/ajax.js'; +import {getAdUnitSizes} from '../libraries/sizeUtils/sizeUtils.js'; const BIDDER_CODE = 'ogury'; const GVLID = 31; @@ -11,7 +12,7 @@ const DEFAULT_TIMEOUT = 1000; const BID_HOST = 'https://mweb-hb.presage.io/api/header-bidding-request'; const TIMEOUT_MONITORING_HOST = 'https://ms-ads-monitoring-events.presage.io'; const MS_COOKIE_SYNC_DOMAIN = 'https://ms-cookie-sync.presage.io'; -const ADAPTER_VERSION = '1.4.1'; +const ADAPTER_VERSION = '1.6.0'; function getClientWidth() { const documentElementClientWidth = window.top.document.documentElement.clientWidth @@ -46,22 +47,35 @@ function isBidRequestValid(bid) { } function getUserSyncs(syncOptions, serverResponses, gdprConsent, uspConsent) { - if (!syncOptions.pixelEnabled) return []; + const consent = (gdprConsent && gdprConsent.consentString) || ''; - return [ - { - type: 'image', - url: `${MS_COOKIE_SYNC_DOMAIN}/v1/init-sync/bid-switch?iab_string=${(gdprConsent && gdprConsent.consentString) || ''}&source=prebid` - }, - { - type: 'image', - url: `${MS_COOKIE_SYNC_DOMAIN}/ttd/init-sync?iab_string=${(gdprConsent && gdprConsent.consentString) || ''}&source=prebid` - }, - { - type: 'image', - url: `${MS_COOKIE_SYNC_DOMAIN}/xandr/init-sync?iab_string=${(gdprConsent && gdprConsent.consentString) || ''}&source=prebid` - } - ] + if (syncOptions.iframeEnabled) { + return [ + { + type: 'iframe', + url: `${MS_COOKIE_SYNC_DOMAIN}/user-sync.html?gdpr_consent=${consent}&source=prebid` + } + ]; + } + + if (syncOptions.pixelEnabled) { + return [ + { + type: 'image', + url: `${MS_COOKIE_SYNC_DOMAIN}/v1/init-sync/bid-switch?iab_string=${consent}&source=prebid` + }, + { + type: 'image', + url: `${MS_COOKIE_SYNC_DOMAIN}/ttd/init-sync?iab_string=${consent}&source=prebid` + }, + { + type: 'image', + url: `${MS_COOKIE_SYNC_DOMAIN}/xandr/init-sync?iab_string=${consent}&source=prebid` + } + ]; + } + + return []; } function buildRequests(validBidRequests, bidderRequest) { @@ -108,6 +122,13 @@ function buildRequests(validBidRequests, bidderRequest) { openRtbBidRequestBanner.site.id = bidRequest.params.assetKey; const floor = getFloor(bidRequest); + if (bidRequest.userId) { + openRtbBidRequestBanner.user.ext.uids = bidRequest.userId + } + if (bidRequest.userIdAsEids) { + openRtbBidRequestBanner.user.ext.eids = bidRequest.userIdAsEids + } + openRtbBidRequestBanner.imp.push({ id: bidRequest.bidId, tagid: bidRequest.params.adUnitId, diff --git a/modules/oneKeyIdSystem.js b/modules/oneKeyIdSystem.js index 8d8d594b533..8765a72a1af 100644 --- a/modules/oneKeyIdSystem.js +++ b/modules/oneKeyIdSystem.js @@ -8,6 +8,13 @@ import {submodule} from '../src/hook.js'; import { logError, logMessage } from '../src/utils.js'; +/** + * @typedef {import('../modules/userId/index.js').Submodule} Submodule + * @typedef {import('../modules/userId/index.js').SubmoduleConfig} SubmoduleConfig + * @typedef {import('../modules/userId/index.js').ConsentData} ConsentData + * @typedef {import('../modules/userId/index.js').IdResponse} IdResponse + */ + // Pre-init OneKey if it has not load yet. window.OneKey = window.OneKey || {}; window.OneKey.queue = window.OneKey.queue || []; @@ -47,31 +54,57 @@ const getIdsAndPreferences = (callback) => { /** @type {Submodule} */ export const oneKeyIdSubmodule = { /** - * used to link submodule with config - * @type {string} - */ + * used to link submodule with config + * @type {string} + */ name: 'oneKeyData', /** - * decode the stored data value for passing to bid requests - * @function decode - * @param {(Object|string)} value - * @returns {(Object|undefined)} - */ + * decode the stored data value for passing to bid requests + * @function decode + * @param {(Object|string)} value + * @returns {(Object|undefined)} + */ decode(data) { return { oneKeyData: data }; }, /** - * performs action to obtain id and return a value in the callback's response argument - * @function - * @param {SubmoduleConfig} [config] - * @param {ConsentData} [consentData] - * @param {(Object|undefined)} cacheIdObj - * @returns {IdResponse|undefined} - */ + * performs action to obtain id and return a value in the callback's response argument + * @function + * @param {SubmoduleConfig} [config] + * @param {ConsentData} [consentData] + * @param {(Object|undefined)} cacheIdObj + * @returns {IdResponse|undefined} + */ getId(config) { return { callback: getIdsAndPreferences }; + }, + eids: { + 'oneKeyData': { + getValue: function(data) { + if (data && Array.isArray(data.identifiers) && data.identifiers[0]) { + return data.identifiers[0].value; + } + }, + source: 'paf', + atype: 1, + getEidExt: function(data) { + if (data && data.preferences) { + return {preferences: data.preferences}; + } + }, + getUidExt: function(data) { + if (data && Array.isArray(data.identifiers) && data.identifiers[0]) { + const id = data.identifiers[0]; + return { + version: id.version, + type: id.type, + source: id.source + }; + } + } + } } }; diff --git a/modules/oneKeyRtdProvider.js b/modules/oneKeyRtdProvider.js index 27511017676..19915609820 100644 --- a/modules/oneKeyRtdProvider.js +++ b/modules/oneKeyRtdProvider.js @@ -3,6 +3,10 @@ import { submodule } from '../src/hook.js'; import { mergeDeep, logError, logMessage, deepSetValue, generateUUID } from '../src/utils.js'; import { getGlobal } from '../src/prebidGlobal.js'; +/** + * @typedef {import('../modules/rtdModule/index.js').RtdSubmodule} RtdSubmodule + */ + const SUBMODULE_NAME = 'oneKey'; const prefixLog = 'OneKey.RTD-module' diff --git a/modules/onetagBidAdapter.js b/modules/onetagBidAdapter.js index 724a53a3095..eb9fa2eb536 100644 --- a/modules/onetagBidAdapter.js +++ b/modules/onetagBidAdapter.js @@ -8,6 +8,11 @@ import { getStorageManager } from '../src/storageManager.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; import { deepClone, logError, deepAccess } from '../src/utils.js'; +/** + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + * @typedef {import('../src/adapters/bidderFactory.js').validBidRequests} validBidRequests + */ + const ENDPOINT = 'https://onetag-sys.com/prebid-request'; const USER_SYNC_ENDPOINT = 'https://onetag-sys.com/usync/'; const BIDDER_CODE = 'onetag'; @@ -70,6 +75,9 @@ function buildRequests(validBidRequests, bidderRequest) { if (bidderRequest && bidderRequest.uspConsent) { payload.usPrivacy = bidderRequest.uspConsent; } + if (bidderRequest && bidderRequest.ortb2) { + payload.ortb2 = bidderRequest.ortb2; + } if (validBidRequests && validBidRequests.length !== 0 && validBidRequests[0].userIdAsEids) { payload.userId = validBidRequests[0].userIdAsEids; } @@ -84,6 +92,7 @@ function buildRequests(validBidRequests, bidderRequest) { const connection = navigator.connection || navigator.webkitConnection; payload.networkConnectionType = (connection && connection.type) ? connection.type : null; payload.networkEffectiveConnectionType = (connection && connection.effectiveType) ? connection.effectiveType : null; + payload.fledgeEnabled = Boolean(bidderRequest && bidderRequest.fledgeEnabled) return { method: 'POST', url: ENDPOINT, @@ -98,10 +107,10 @@ function interpretResponse(serverResponse, bidderRequest) { if (!body || (body.nobid && body.nobid === true)) { return bids; } - if (!body.bids || !Array.isArray(body.bids) || body.bids.length === 0) { + if (!body.fledgeAuctionConfigs && (!body.bids || !Array.isArray(body.bids) || body.bids.length === 0)) { return bids; } - body.bids.forEach(bid => { + Array.isArray(body.bids) && body.bids.forEach(bid => { const responseBid = { requestId: bid.requestId, cpm: bid.cpm, @@ -138,7 +147,16 @@ function interpretResponse(serverResponse, bidderRequest) { } bids.push(responseBid); }); - return bids; + + if (body.fledgeAuctionConfigs && Array.isArray(body.fledgeAuctionConfigs)) { + const fledgeAuctionConfigs = body.fledgeAuctionConfigs + return { + bids, + fledgeAuctionConfigs, + } + } else { + return bids; + } } function createRenderer(bid, rendererOptions = {}) { @@ -264,9 +282,8 @@ function setGeneralInfo(bidRequest) { this['adUnitCode'] = bidRequest.adUnitCode; this['bidId'] = bidRequest.bidId; this['bidderRequestId'] = bidRequest.bidderRequestId; - // TODO: fix auctionId leak: https://github.com/prebid/Prebid.js/issues/9781 - this['auctionId'] = bidRequest.auctionId; - this['transactionId'] = bidRequest.ortb2Imp?.ext?.tid; + this['auctionId'] = deepAccess(bidRequest, 'ortb2.source.tid'); + this['transactionId'] = deepAccess(bidRequest, 'ortb2Imp.ext.tid'); this['gpid'] = deepAccess(bidRequest, 'ortb2Imp.ext.gpid') || deepAccess(bidRequest, 'ortb2Imp.ext.data.pbadslot'); this['pubId'] = params.pubId; this['ext'] = params.ext; diff --git a/modules/onomagicBidAdapter.js b/modules/onomagicBidAdapter.js index edab625e541..78f00153a8b 100644 --- a/modules/onomagicBidAdapter.js +++ b/modules/onomagicBidAdapter.js @@ -1,7 +1,6 @@ import { _each, - createTrackPixelHtml, - getBidIdParameter, + createTrackPixelHtml, getBidIdParameter, getUniqueIdentifierStr, getWindowSelf, getWindowTop, diff --git a/modules/open8BidAdapter.js b/modules/open8BidAdapter.js index 5fa1dd0a143..49523926c0e 100644 --- a/modules/open8BidAdapter.js +++ b/modules/open8BidAdapter.js @@ -1,8 +1,9 @@ import { Renderer } from '../src/Renderer.js'; import {ajax} from '../src/ajax.js'; -import { createTrackPixelHtml, getBidIdParameter, logError, logWarn, tryAppendQueryString } from '../src/utils.js'; +import {createTrackPixelHtml, getBidIdParameter, logError, logWarn} from '../src/utils.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; import { VIDEO, BANNER } from '../src/mediaTypes.js'; +import {tryAppendQueryString} from '../libraries/urlUtils/urlUtils.js'; const BIDDER_CODE = 'open8'; const URL = 'https://as.vt.open8.com/v1/control/prebid'; diff --git a/modules/openwebBidAdapter.js b/modules/openwebBidAdapter.js index 296bfc682f1..547447039da 100644 --- a/modules/openwebBidAdapter.js +++ b/modules/openwebBidAdapter.js @@ -1,8 +1,9 @@ -import {convertTypes, deepAccess, flatten, isArray, isNumber, parseSizesInput} from '../src/utils.js'; +import {deepAccess, flatten, isArray, isNumber, parseSizesInput} from '../src/utils.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import {ADPOD, BANNER, VIDEO} from '../src/mediaTypes.js'; import {config} from '../src/config.js'; import {find} from '../src/polyfill.js'; +import {convertTypes} from '../libraries/transformParamsUtils/convertTypes.js'; const ENDPOINT = 'https://ghb.spotim.market/v2/auction'; const BIDDER_CODE = 'openweb'; diff --git a/modules/openxBidAdapter.js b/modules/openxBidAdapter.js index 03423a028b4..0f8bee213f7 100644 --- a/modules/openxBidAdapter.js +++ b/modules/openxBidAdapter.js @@ -4,6 +4,7 @@ import * as utils from '../src/utils.js'; import {mergeDeep} from '../src/utils.js'; import {BANNER, VIDEO} from '../src/mediaTypes.js'; import {ortbConverter} from '../libraries/ortbConverter/converter.js'; +import {convertTypes} from '../libraries/transformParamsUtils/convertTypes.js'; const bidderConfig = 'hb_pb_ortb'; const bidderVersion = '2.0'; @@ -12,6 +13,7 @@ export const SYNC_URL = 'https://u.openx.net/w/1.0/pd'; export const DEFAULT_PH = '2d1251ae-7f3a-47cf-bd2a-2f288854a0ba'; export const spec = { code: 'openx', + gvlid: 69, supportedMediaTypes: [BANNER, VIDEO], isBidRequestValid, buildRequests, @@ -48,7 +50,8 @@ const converter = ortbConverter({ mergeDeep(req, { at: 1, ext: { - bc: `${bidderConfig}_${bidderVersion}` + bc: `${bidderConfig}_${bidderVersion}`, + pv: '$prebid.version$' } }) const bid = context.bidRequests[0]; @@ -149,7 +152,7 @@ const converter = ortbConverter({ }); function transformBidParams(params, isOpenRtb) { - return utils.convertTypes({ + return convertTypes({ 'unit': 'string', 'customFloor': 'number' }, params); diff --git a/modules/operaadsBidAdapter.js b/modules/operaadsBidAdapter.js index e721fb85fd7..957192d1bec 100644 --- a/modules/operaadsBidAdapter.js +++ b/modules/operaadsBidAdapter.js @@ -18,6 +18,13 @@ import {Renderer} from '../src/Renderer.js'; import {OUTSTREAM} from '../src/video.js'; import {convertOrtbRequestToProprietaryNative} from '../src/native.js'; +/** + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid + * @typedef {import('../src/adapters/bidderFactory.js').ServerResponse} ServerResponse + * @typedef {import('../src/adapters/bidderFactory.js').SyncOptions} SyncOptions + * @typedef {import('../src/adapters/bidderFactory.js').UserSync} UserSync + */ const BIDDER_CODE = 'operaads'; const ENDPOINT = 'https://s.adx.opera.com/ortb/v2/'; @@ -70,7 +77,7 @@ const NATIVE_DEFAULTS = { export const spec = { code: BIDDER_CODE, - + gvlid: 1135, // short code aliases: ['opera'], @@ -232,13 +239,6 @@ function buildOpenRtbBidRequest(bidRequest, bidderRequest) { test: config.getConfig('debug') ? 1 : 0, imp: createImp(bidRequest), device: getDevice(), - site: { - id: String(deepAccess(bidRequest, 'params.publisherId')), - // TODO: does the fallback make sense here? - domain: bidderRequest?.refererInfo?.domain || window.location.host, - page: bidderRequest?.refererInfo?.page, - ref: bidderRequest?.refererInfo?.ref || '', - }, at: 1, bcat: getBcat(bidRequest), cur: [DEFAULT_CURRENCY], @@ -250,6 +250,7 @@ function buildOpenRtbBidRequest(bidRequest, bidderRequest) { buyeruid: getUserId(bidRequest) } } + fulfillInventoryInfo(payload, bidRequest, bidderRequest); const gdprConsent = deepAccess(bidderRequest, 'gdprConsent'); if (!!gdprConsent && gdprConsent.gdprApplies) { @@ -680,6 +681,11 @@ function mapNativeImage(image, type) { * @returns {String} userId */ function getUserId(bidRequest) { + let operaId = deepAccess(bidRequest, 'userId.operaId'); + if (operaId) { + return operaId; + } + let sharedId = deepAccess(bidRequest, 'userId.sharedid.id'); if (sharedId) { return sharedId; @@ -759,6 +765,38 @@ function getDevice() { return device; } +/** + * Fulfill inventory info + * + * @param payload + * @param bidRequest + * @param bidderRequest + */ +function fulfillInventoryInfo(payload, bidRequest, bidderRequest) { + let info = deepAccess(bidRequest, 'params.site'); + // 1.If the inventory info for site specified, use the site object provided in params. + let key = 'site'; + if (!isPlainObject(info)) { + info = deepAccess(bidRequest, 'params.app'); + if (isPlainObject(info)) { + // 2.If the inventory info for app specified, use the app object provided in params. + key = 'app'; + } else { + // 3.Otherwise, we use site by default. + info = {}; + } + } + // Fulfill key parameters. + info.id = String(deepAccess(bidRequest, 'params.publisherId')); + info.domain = info.domain || bidderRequest?.refererInfo?.domain || window.location.host; + if (key === 'site') { + info.ref = info.ref || bidderRequest?.refererInfo?.ref || ''; + info.page = info.page || bidderRequest?.refererInfo?.page; + } + + payload[key] = info; +} + /** * Get browser language * diff --git a/modules/operaadsBidAdapter.md b/modules/operaadsBidAdapter.md index 709c67a04a7..6f13eebd7d5 100644 --- a/modules/operaadsBidAdapter.md +++ b/modules/operaadsBidAdapter.md @@ -14,41 +14,43 @@ Module that connects to OperaAds's demand sources ## Bid Parameters -| Name | Scope | Type | Description | Example -| ---- | ----- | ---- | ----------- | ------- -| `placementId` | required | String | The Placement Id provided by Opera Ads. | `s5340077725248` -| `endpointId` | required | String | The Endpoint Id provided by Opera Ads. | `ep3425464070464` -| `publisherId` | required | String | The Publisher Id provided by Opera Ads. | `pub3054952966336` -| `bcat` | optional | String or String[] | The bcat value. | `IAB9-31` +| Name | Scope | Type | Description | Example | +|---------------|----------|--------------------|-----------------------------------------|-------------------------------------------------| +| `placementId` | required | String | The Placement Id provided by Opera Ads. | `s5340077725248` | +| `endpointId` | required | String | The Endpoint Id provided by Opera Ads. | `ep3425464070464` | +| `publisherId` | required | String | The Publisher Id provided by Opera Ads. | `pub3054952966336` | +| `bcat` | optional | String or String[] | The bcat value. | `IAB9-31` | +| `site` | optional | Object | The site information. | `{"name": "my_site", "domain": "www.test.com"}` | +| `app` | optional | Object | The app information. | `{"name": "my_app", "ver": "1.1.0"}` | ### Bid Video Parameters Set these parameters to `bid.mediaTypes.video`. -| Name | Scope | Type | Description | Example -| ---- | ----- | ---- | ----------- | ------- -| `context` | optional | String | `instream` or `outstream`. | `instream` -| `mimes` | optional | String[] | Content MIME types supported. | `['video/mp4']` -| `playerSize` | optional | Number[] or Number[][] | Video player size in device independent pixels | `[[640, 480]]` -| `protocols` | optional | Number[] | Array of supported video protocls. | `[1, 2, 3, 4, 5, 6, 7, 8]` -| `startdelay` | optional | Number | Indicates the start delay in seconds for pre-roll, mid-roll, or post-roll ad placements. | `0` -| `skip` | optional | Number | Indicates if the player will allow the video to be skipped, where 0 = no, 1 = yes. | `1` -| `playbackmethod` | optional | Number[] | Playback methods that may be in use. | `[2]` -| `delivery` | optional | Number[] | Supported delivery methods. | `[1]` -| `api` | optional | Number[] | List of supported API frameworks for this impression. | `[1, 2, 5]` +| Name | Scope | Type | Description | Example | +|------------------|----------|------------------------|------------------------------------------------------------------------------------------|----------------------------| +| `context` | optional | String | `instream` or `outstream`. | `instream` | +| `mimes` | optional | String[] | Content MIME types supported. | `['video/mp4']` | +| `playerSize` | optional | Number[] or Number[][] | Video player size in device independent pixels | `[[640, 480]]` | +| `protocols` | optional | Number[] | Array of supported video protocls. | `[1, 2, 3, 4, 5, 6, 7, 8]` | +| `startdelay` | optional | Number | Indicates the start delay in seconds for pre-roll, mid-roll, or post-roll ad placements. | `0` | +| `skip` | optional | Number | Indicates if the player will allow the video to be skipped, where 0 = no, 1 = yes. | `1` | +| `playbackmethod` | optional | Number[] | Playback methods that may be in use. | `[2]` | +| `delivery` | optional | Number[] | Supported delivery methods. | `[1]` | +| `api` | optional | Number[] | List of supported API frameworks for this impression. | `[1, 2, 5]` | ### Bid Native Parameters Set these parameters to `bid.nativeParams` or `bid.mediaTypes.native`. -| Name | Scope | Type | Description | Example -| ---- | ----- | ---- | ----------- | ------- -| `title` | optional | Object | Config for native asset title. | `{required: true, len: 25}` -| `image` | optional | Object | Config for native asset image. | `{required: true, sizes: [[300, 250]], aspect_ratios: [{min_width: 300, min_height: 250, ratio_width: 1, ratio_height: 1}]}` -| `icon` | optional | Object | Config for native asset icon. | `{required: true, sizes: [[60, 60]], aspect_ratios: [{min_width: 60, min_height: 60, ratio_width: 1, ratio_height: 1}]}}` -| `sponsoredBy` | optional | Object | Config for native asset sponsoredBy. | `{required: true, len: 20}` -| `body` | optional | Object | Config for native asset body. | `{required: true, len: 200}` -| `cta` | optional | Object | Config for native asset cta. | `{required: true, len: 20}` +| Name | Scope | Type | Description | Example | +|---------------|----------|--------|--------------------------------------|------------------------------------------------------------------------------------------------------------------------------| +| `title` | optional | Object | Config for native asset title. | `{required: true, len: 25}` | +| `image` | optional | Object | Config for native asset image. | `{required: true, sizes: [[300, 250]], aspect_ratios: [{min_width: 300, min_height: 250, ratio_width: 1, ratio_height: 1}]}` | +| `icon` | optional | Object | Config for native asset icon. | `{required: true, sizes: [[60, 60]], aspect_ratios: [{min_width: 60, min_height: 60, ratio_width: 1, ratio_height: 1}]}}` | +| `sponsoredBy` | optional | Object | Config for native asset sponsoredBy. | `{required: true, len: 20}` | +| `body` | optional | Object | Config for native asset body. | `{required: true, len: 200}` | +| `cta` | optional | Object | Config for native asset cta. | `{required: true, len: 20}` | ## Example @@ -127,7 +129,9 @@ var adUnits = [{ params: { placementId: 's5340077725248', endpointId: 'ep3425464070464', - publisherId: 'pub3054952966336' + publisherId: 'pub3054952966336', + // You might want to specify some application information here if the bid requests are from an application instead of a browser. + app: { 'name': 'my_app', 'bundle': 'test_bundle', 'store_url': 'www.some-store.com', 'ver': '1.1.0' } } }] }]; @@ -135,18 +139,18 @@ var adUnits = [{ ### User Ids -Opera Ads Bid Adapter uses `sharedId`, `pubcid` or `tdid`, please config at least one. +Opera Ads Bid Adapter uses `operaId`, please refer to [`Opera ID System`](./operaadsIdSystem.md). ```javascript pbjs.setConfig({ ..., userSync: { userIds: [{ - name: 'sharedId', + name: 'operaId', storage: { - name: '_sharedID', // name of the 1st party cookie - type: 'cookie', - expires: 30 + name: 'operaId', + type: 'html5', + expires: 14 } }] } diff --git a/modules/operaadsIdSystem.js b/modules/operaadsIdSystem.js new file mode 100644 index 00000000000..7cf5e2ce5e1 --- /dev/null +++ b/modules/operaadsIdSystem.js @@ -0,0 +1,111 @@ +/** + * This module adds operaId to the User ID module + * The {@link module:modules/userId} module is required + * @module modules/operaadsIdSystem + * @requires module:modules/userId + */ +import * as ajax from '../src/ajax.js'; +import { submodule } from '../src/hook.js'; +import { logMessage, logError } from '../src/utils.js'; + +/** + * @typedef {import('../modules/userId/index.js').SubmoduleConfig} SubmoduleConfig + * @typedef {import('../modules/userId/index.js').IdResponse} IdResponse + */ + +const MODULE_NAME = 'operaId'; +const ID_KEY = MODULE_NAME; +const version = '1.0'; +const SYNC_URL = 'https://t.adx.opera.com/identity/'; +const AJAX_TIMEOUT = 300; +const AJAX_OPTIONS = {method: 'GET', withCredentials: true, contentType: 'application/json'}; + +function constructUrl(pairs) { + const queries = []; + for (let key in pairs) { + queries.push(`${key}=${encodeURIComponent(pairs[key])}`); + } + return `${SYNC_URL}?${queries.join('&')}`; +} + +function asyncRequest(url, cb) { + ajax.ajaxBuilder(AJAX_TIMEOUT)( + url, + { + success: response => { + try { + const jsonResponse = JSON.parse(response); + const { uid: operaId } = jsonResponse; + cb(operaId); + return; + } catch (e) { + logError(`${MODULE_NAME}: invalid response`, response); + } + cb(); + }, + error: (err) => { + logError(`${MODULE_NAME}: ID error response`, err); + cb(); + } + }, + null, + AJAX_OPTIONS + ); +} + +export const operaIdSubmodule = { + /** + * used to link submodule with config + * @type {string} + */ + name: MODULE_NAME, + + /** + * @type {string} + */ + version, + + /** + * decode the stored id value for passing to bid requests + * @function + * @param {string} id + * @returns {{'operaId': string}} + */ + decode: (id) => + id != null && id.length > 0 + ? { [ID_KEY]: id } + : undefined, + + /** + * performs action to obtain id and return a value in the callback's response argument + * @function + * @param {SubmoduleConfig} [config] + * @returns {IdResponse|undefined} + */ + getId(config, consentData) { + logMessage(`${MODULE_NAME}: start synchronizing opera uid`); + const params = (config && config.params) || {}; + if (typeof params.pid !== 'string' || params.pid.length == 0) { + logError(`${MODULE_NAME}: submodule requires a publisher ID to be defined`); + return; + } + + const { pid, syncUrl = SYNC_URL } = params; + const url = constructUrl(syncUrl, { publisherId: pid }); + + return { + callback: (cb) => { + asyncRequest(url, cb); + } + } + }, + + eids: { + 'operaId': { + source: 't.adx.opera.com', + atype: 1 + }, + } +}; + +submodule('userId', operaIdSubmodule); diff --git a/modules/operaadsIdSystem.md b/modules/operaadsIdSystem.md new file mode 100644 index 00000000000..288fb960b96 --- /dev/null +++ b/modules/operaadsIdSystem.md @@ -0,0 +1,52 @@ +# Opera ID System + +For help adding this module, please contact [adtech-prebid-group@opera.com](adtech-prebid-group@opera.com). + +### Prebid Configuration + +You should configure this module under your `userSync.userIds[]` configuration: + +```javascript +pbjs.setConfig({ + userSync: { + userIds: [ + { + name: "operaId", + storage: { + name: "operaId", + type: "html5", + expires: 14 + }, + params: { + pid: "your-pulisher-ID-here" + } + } + ] + } +}) +``` +
+ +| Param under `userSync.userIds[]` | Scope | Type | Description | Example | +| -------------------------------- | -------- | ------ | ----------------------------- | ----------------------------------------- | +| name | Required | string | ID for the operaId module | `"operaId"` | +| storage | Optional | Object | Settings for operaId storage | See [storage settings](#storage-settings) | +| params | Required | Object | Parameters for opreaId module | See [params](#params) | +
+ +### Params + +| Param under `params` | Scope | Type | Description | Example | +| -------------------- | -------- | ------ | ------------------------------ | --------------- | +| pid | Required | string | Publisher ID assigned by Opera | `"pub12345678"` | +
+ +### Storage Settings + +The following settings are suggested for the `storage` property in the `userSync.userIds[]` object: + +| Param under `storage` | Type | Description | Example | +| --------------------- | ------------- | -------------------------------------------------------------------------------- | ----------- | +| name | String | Where the ID will be stored | `"operaId"` | +| type | String | For best performance, this should be `"html5"` | `"html5"` | +| expires | Number <= 30 | number of days until the stored ID expires. **Must be less than or equal to 30** | `14` | \ No newline at end of file diff --git a/modules/optidigitalBidAdapter.js b/modules/optidigitalBidAdapter.js index a0fa641a424..27b858c84fe 100755 --- a/modules/optidigitalBidAdapter.js +++ b/modules/optidigitalBidAdapter.js @@ -1,23 +1,34 @@ -import { registerBidder } from '../src/adapters/bidderFactory.js'; -import { BANNER } from '../src/mediaTypes.js'; -import { deepAccess, parseSizesInput, getAdUnitSizes } from '../src/utils.js'; +import {registerBidder} from '../src/adapters/bidderFactory.js'; +import {BANNER} from '../src/mediaTypes.js'; +import {deepAccess, parseSizesInput} from '../src/utils.js'; +import {getAdUnitSizes} from '../libraries/sizeUtils/sizeUtils.js'; + +/** + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid + * @typedef {import('../src/adapters/bidderFactory.js').ServerResponse} ServerResponse + * @typedef {import('../src/adapters/bidderFactory.js').SyncOptions} SyncOptions + * @typedef {import('../src/adapters/bidderFactory.js').UserSync} UserSync + * @typedef {import('../src/adapters/bidderFactory.js').validBidRequests} validBidRequests + */ const BIDDER_CODE = 'optidigital'; const GVL_ID = 915; const ENDPOINT_URL = 'https://pbs.optidigital.com/bidder'; const USER_SYNC_URL_IFRAME = 'https://scripts.opti-digital.com/js/presync.html?endpoint=optidigital'; let CUR = 'USD'; +let isSynced = false; export const spec = { code: BIDDER_CODE, 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. - */ + * 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) { @@ -27,11 +38,11 @@ export const spec = { 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. - */ + * 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 []; @@ -46,8 +57,6 @@ export const spec = { referrer: (bidderRequest.refererInfo && bidderRequest.refererInfo.page) ? bidderRequest.refererInfo.page : '', hb_version: '$prebid.version$', deviceWidth: document.documentElement.clientWidth, - // TODO: fix auctionId leak: https://github.com/prebid/Prebid.js/issues/9781 - auctionId: deepAccess(validBidRequests[0], 'auctionId'), bidderRequestId: deepAccess(validBidRequests[0], 'bidderRequestId'), publisherId: deepAccess(validBidRequests[0], 'params.publisherId'), imp: validBidRequests.map(bidRequest => buildImp(bidRequest, ortb2)), @@ -56,6 +65,10 @@ export const spec = { bapp: deepAccess(validBidRequests[0], 'params.bapp') || [] } + if (validBidRequests[0].auctionId) { + payload.auctionId = validBidRequests[0].auctionId; + } + if (validBidRequests[0].params.pageTemplate && validBidRequests[0].params.pageTemplate !== '') { payload.pageTemplate = validBidRequests[0].params.pageTemplate; } @@ -87,6 +100,12 @@ export const spec = { payload.uspConsent = bidderRequest.uspConsent; } + if (_getEids(validBidRequests[0])) { + payload.user = { + eids: _getEids(validBidRequests[0]) + } + } + const payloadObject = JSON.stringify(payload); return { method: 'POST', @@ -95,11 +114,11 @@ export const spec = { }; }, /** - * 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. - */ + * 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; @@ -128,29 +147,31 @@ export const spec = { }, /** - * 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. + */ getUserSyncs: function(syncOptions, serverResponses, gdprConsent, uspConsent) { let syncurl = ''; + if (!isSynced) { + // Attaching GDPR Consent Params in UserSync url + if (gdprConsent) { + syncurl += '&gdpr=' + (gdprConsent.gdprApplies ? 1 : 0); + syncurl += '&gdpr_consent=' + encodeURIComponent(gdprConsent.consentString || ''); + } + if (uspConsent && uspConsent.consentString) { + syncurl += `&ccpa_consent=${uspConsent.consentString}`; + } - // Attaching GDPR Consent Params in UserSync url - if (gdprConsent) { - syncurl += '&gdpr=' + (gdprConsent.gdprApplies ? 1 : 0); - syncurl += '&gdpr_consent=' + encodeURIComponent(gdprConsent.consentString || ''); - } - if (uspConsent && uspConsent.consentString) { - syncurl += `&ccpa_consent=${uspConsent.consentString}`; - } - - if (syncOptions.iframeEnabled) { - return [{ - type: 'iframe', - url: USER_SYNC_URL_IFRAME + syncurl - }]; + if (syncOptions.iframeEnabled) { + isSynced = true; + return [{ + type: 'iframe', + url: USER_SYNC_URL_IFRAME + syncurl + }]; + } } }, }; @@ -218,4 +239,14 @@ function _getFloor (bid, sizes, currency) { return floor !== null ? floor : bid.params.floor; } +function _getEids(bidRequest) { + if (deepAccess(bidRequest, 'userIdAsEids')) { + return bidRequest.userIdAsEids; + } +} + +export function resetSync() { + isSynced = false; +} + registerBidder(spec); diff --git a/modules/optidigitalBidAdapter.md b/modules/optidigitalBidAdapter.md index 466dfb3bef2..327e7a27c75 100755 --- a/modules/optidigitalBidAdapter.md +++ b/modules/optidigitalBidAdapter.md @@ -37,10 +37,13 @@ Bidder Adapter for Prebid.js. ``` pbjs.setConfig({ - userSync: { - iframeEnabled: true, - syncEnabled: true, - syncDelay: 3000 - } +  userSync: { +    filterSettings: { +      iframe: { +        bidders: '*', // '*' represents all bidders +        filter: 'include' +      } +    } +  } }); ``` diff --git a/modules/optimeraRtdProvider.js b/modules/optimeraRtdProvider.js index dfe8f1bfcf2..bd564e3a260 100644 --- a/modules/optimeraRtdProvider.js +++ b/modules/optimeraRtdProvider.js @@ -16,12 +16,17 @@ * @property {string} clientID * @property {string} optimeraKeyName * @property {string} device + * @property {string} apiVersion */ import { logInfo, logError } from '../src/utils.js'; import { submodule } from '../src/hook.js'; import { ajaxBuilder } from '../src/ajax.js'; +/** + * @typedef {import('../modules/rtdModule/index.js').RtdSubmodule} RtdSubmodule + */ + /** @type {ModuleParams} */ let _moduleParams = {}; @@ -29,7 +34,8 @@ let _moduleParams = {}; * Default Optimera Key Name * This can default to hb_deal_optimera for publishers * who used the previous Optimera Bidder Adapter. - * @type {string} */ + * @type {string} + */ export let optimeraKeyName = 'hb_deal_optimera'; /** @@ -38,7 +44,10 @@ export let optimeraKeyName = 'hb_deal_optimera'; * the targeting values. * @type {string} */ -export const scoresBaseURL = 'https://dyv1bugovvq1g.cloudfront.net/'; +export const scoresBaseURL = { + v0: 'https://dyv1bugovvq1g.cloudfront.net/', + v1: 'https://v1.oapi26b.com/', +}; /** * Optimera Score File URL. @@ -58,6 +67,12 @@ export let clientID; */ export let device = 'default'; +/** + * Optional apiVersion parameter. + * @type {string} + */ +export let apiVersion = 'v0'; + /** * Targeting object for all ad positions. * @type {string} @@ -127,6 +142,7 @@ export function onAuctionInit(auctionDetails, config, userConsent) { /** * Initialize the Module. + * moduleConfig.params.apiVersion can be either v0 or v1. */ export function init(moduleConfig) { _moduleParams = moduleConfig.params; @@ -138,6 +154,9 @@ export function init(moduleConfig) { if (_moduleParams.device) { device = _moduleParams.device; } + if (_moduleParams.apiVersion) { + apiVersion = (_moduleParams.apiVersion.includes('v1', 'v0')) ? _moduleParams.apiVersion : 'v0'; + } setScoresURL(); scoreFileRequest(); return true; @@ -162,7 +181,15 @@ export function init(moduleConfig) { export function setScoresURL() { const optimeraHost = window.location.host; const optimeraPathName = window.location.pathname; - const newScoresURL = `${scoresBaseURL}${clientID}/${optimeraHost}${optimeraPathName}.js`; + const baseUrl = scoresBaseURL[apiVersion] ? scoresBaseURL[apiVersion] : scoresBaseURL.v0; + let newScoresURL; + + if (apiVersion === 'v1') { + newScoresURL = `${baseUrl}api/products/scores?c=${clientID}&h=${optimeraHost}&p=${optimeraPathName}&s=${device}`; + } else { + newScoresURL = `${baseUrl}${clientID}/${optimeraHost}${optimeraPathName}.js`; + } + if (scoresURL !== newScoresURL) { scoresURL = newScoresURL; fetchScoreFile = true; diff --git a/modules/optimeraRtdProvider.md b/modules/optimeraRtdProvider.md index 610dec537e0..8b66deb5ad5 100644 --- a/modules/optimeraRtdProvider.md +++ b/modules/optimeraRtdProvider.md @@ -1,6 +1,6 @@ # Overview ``` -Module Name: Optimera Real Time Date Module +Module Name: Optimera Real Time Data Module Module Type: RTD Module Maintainer: mcallari@optimera.nyc ``` @@ -26,7 +26,8 @@ Configuration example for using RTD module with `optimera` provider params: { clientID: '9999', optimeraKeyName: 'optimera', - device: 'de' + device: 'de', + apiVersion: 'v0', } } ] @@ -42,3 +43,4 @@ Contact Optimera to get assistance with the params. | clientID | string | required | Optimera Client ID | | optimeraKeyName | string | optional | GAM key name for Optimera. If migrating from the Optimera bidder adapter this will default to hb_deal_optimera and can be ommitted from the configuration. | | device | string | optional | Device type code for mobile, tablet, or desktop. Either mo, tb, de | +| apiVersion | string | optional | Optimera API Versions. Either v0, or v1. ** Note: v1 wll need to be enabled specifically for your account, otherwise use v0. \ No newline at end of file diff --git a/modules/optimonAnalyticsAdapter.js b/modules/optimonAnalyticsAdapter.js index 82bc18f605d..68baf007563 100644 --- a/modules/optimonAnalyticsAdapter.js +++ b/modules/optimonAnalyticsAdapter.js @@ -1,12 +1,12 @@ /** -* -********************************************************* -* -* Optimon.io Prebid Analytics Adapter -* -********************************************************* -* -*/ + * + ********************************************************* + * + * Optimon.io Prebid Analytics Adapter + * + ********************************************************* + * + */ import adapter from '../libraries/analyticsAdapter/AnalyticsAdapter.js'; import adapterManager from '../src/adapterManager.js'; diff --git a/modules/orbidderBidAdapter.js b/modules/orbidderBidAdapter.js index f135ebb2bd1..0f912384db7 100644 --- a/modules/orbidderBidAdapter.js +++ b/modules/orbidderBidAdapter.js @@ -1,11 +1,15 @@ import { isFn, isPlainObject } from '../src/utils.js'; -import {registerBidder} from '../src/adapters/bidderFactory.js'; +import { registerBidder } from '../src/adapters/bidderFactory.js'; import { getStorageManager } from '../src/storageManager.js'; import { BANNER, NATIVE } from '../src/mediaTypes.js'; import { convertOrtbRequestToProprietaryNative } from '../src/native.js'; -import {getGlobal} from '../src/prebidGlobal.js'; +import { getGlobal } from '../src/prebidGlobal.js'; -const storageManager = getStorageManager({bidderCode: 'orbidder'}); +/** + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid + */ +const storageManager = getStorageManager({ bidderCode: 'orbidder' }); /** * Determines whether or not the given bid response is valid. @@ -69,7 +73,7 @@ export const spec = { return !!(bid.sizes && bid.bidId && bid.params && (bid.params.accountId && (typeof bid.params.accountId === 'string')) && (bid.params.placementId && (typeof bid.params.placementId === 'string')) && - ((typeof bid.params.profile === 'undefined') || (typeof bid.params.profile === 'object'))); + ((typeof bid.params.keyValues === 'undefined') || (typeof bid.params.keyValues === 'object'))); }, /** @@ -99,15 +103,7 @@ export const spec = { data: { v: getGlobal().version, pageUrl: referer, - bidId: bidRequest.bidId, - auctionId: bidRequest.auctionId, - // TODO: fix auctionId leak: https://github.com/prebid/Prebid.js/issues/9781 - transactionId: bidRequest.ortb2Imp?.ext?.tid, - adUnitCode: bidRequest.adUnitCode, - bidRequestCount: bidRequest.bidRequestCount, - params: bidRequest.params, - sizes: bidRequest.sizes, - mediaTypes: bidRequest.mediaTypes + ...bidRequest // get all data provided by bid request } }; diff --git a/modules/orbitsoftBidAdapter.js b/modules/orbitsoftBidAdapter.js index 4c3f2e38c58..f55c7ff9917 100644 --- a/modules/orbitsoftBidAdapter.js +++ b/modules/orbitsoftBidAdapter.js @@ -1,5 +1,6 @@ import * as utils from '../src/utils.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; +import {getBidIdParameter} from '../src/utils.js'; const BIDDER_CODE = 'orbitsoft'; let styleParamsMap = { @@ -45,10 +46,10 @@ export const spec = { for (let i = 0; i < validBidRequests.length; i++) { bidRequest = validBidRequests[i]; let bidRequestParams = bidRequest.params; - let placementId = utils.getBidIdParameter('placementId', bidRequestParams); - let requestUrl = utils.getBidIdParameter('requestUrl', bidRequestParams); - let referrer = utils.getBidIdParameter('ref', bidRequestParams); - let location = utils.getBidIdParameter('loc', bidRequestParams); + let placementId = getBidIdParameter('placementId', bidRequestParams); + let requestUrl = getBidIdParameter('requestUrl', bidRequestParams); + let referrer = getBidIdParameter('ref', bidRequestParams); + let location = getBidIdParameter('loc', bidRequestParams); // Append location & referrer if (location === '') { location = utils.getWindowLocation(); @@ -58,7 +59,7 @@ export const spec = { } // Styles params - let stylesParams = utils.getBidIdParameter('style', bidRequestParams); + let stylesParams = getBidIdParameter('style', bidRequestParams); let stylesParamsArray = {}; for (let currentValue in stylesParams) { if (stylesParams.hasOwnProperty(currentValue)) { @@ -74,7 +75,7 @@ export const spec = { } } // Custom params - let customParams = utils.getBidIdParameter('customParams', bidRequestParams); + let customParams = getBidIdParameter('customParams', bidRequestParams); let customParamsArray = {}; for (let customField in customParams) { if (customParams.hasOwnProperty(customField)) { diff --git a/modules/otmBidAdapter.js b/modules/otmBidAdapter.js index 6125cee6593..7d4049e3054 100644 --- a/modules/otmBidAdapter.js +++ b/modules/otmBidAdapter.js @@ -2,14 +2,13 @@ import { registerBidder } from '../src/adapters/bidderFactory.js'; import { logInfo, logError, - getBidIdParameter, _each, getValue, isFn, isPlainObject, isArray, isStr, - isNumber, + isNumber, getBidIdParameter, } from '../src/utils.js'; import { BANNER } from '../src/mediaTypes.js'; diff --git a/modules/outbrainBidAdapter.js b/modules/outbrainBidAdapter.js index 0637d680912..6015ff37e08 100644 --- a/modules/outbrainBidAdapter.js +++ b/modules/outbrainBidAdapter.js @@ -3,6 +3,7 @@ import {registerBidder} from '../src/adapters/bidderFactory.js'; import {BANNER, NATIVE, VIDEO} from '../src/mediaTypes.js'; +import { getStorageManager } from '../src/storageManager.js'; import {OUTSTREAM} from '../src/video.js'; import {_map, deepAccess, deepSetValue, isArray, logWarn, replaceAuctionPrice} from '../src/utils.js'; import {ajax} from '../src/ajax.js'; @@ -23,6 +24,9 @@ const NATIVE_PARAMS = { cta: { id: 1, type: 12, name: 'data' } }; const OUTSTREAM_RENDERER_URL = 'https://acdn.adnxs.com/video/outstream/ANOutstreamVideo.js'; +const OB_USER_TOKEN_KEY = 'OB-USER-TOKEN'; + +export const storage = getStorageManager({bidderCode: BIDDER_CODE}); export const spec = { code: BIDDER_CODE, @@ -130,6 +134,11 @@ export const spec = { request.test = 1; } + const obUserToken = storage.getDataFromLocalStorage(OB_USER_TOKEN_KEY) + if (obUserToken) { + deepSetValue(request, 'user.ext.obusertoken', obUserToken) + } + if (deepAccess(bidderRequest, 'gdprConsent.gdprApplies')) { deepSetValue(request, 'user.ext.consent', bidderRequest.gdprConsent.consentString) deepSetValue(request, 'regs.ext.gdpr', bidderRequest.gdprConsent.gdprApplies & 1) @@ -140,6 +149,13 @@ export const spec = { if (config.getConfig('coppa') === true) { deepSetValue(request, 'regs.coppa', config.getConfig('coppa') & 1) } + if (bidderRequest.gppConsent) { + deepSetValue(request, 'regs.ext.gpp', bidderRequest.gppConsent.gppString) + deepSetValue(request, 'regs.ext.gpp_sid', bidderRequest.gppConsent.applicableSections) + } else if (deepAccess(bidderRequest, 'ortb2.regs.gpp')) { + deepSetValue(request, 'regs.ext.gpp', bidderRequest.ortb2.regs.gpp) + deepSetValue(request, 'regs.ext.gpp_sid', bidderRequest.ortb2.regs.gpp_sid) + } if (eids) { deepSetValue(request, 'user.ext.eids', eids); @@ -203,7 +219,7 @@ export const spec = { } }).filter(Boolean); }, - getUserSyncs: (syncOptions, responses, gdprConsent, uspConsent) => { + getUserSyncs: (syncOptions, responses, gdprConsent, uspConsent, gppConsent) => { const syncs = []; let syncUrl = config.getConfig('outbrain.usersyncUrl'); @@ -216,6 +232,10 @@ export const spec = { if (uspConsent) { query.push('us_privacy=' + encodeURIComponent(uspConsent)); } + if (gppConsent) { + query.push('gpp=' + encodeURIComponent(gppConsent.gppString)); + query.push('gpp_sid=' + encodeURIComponent(gppConsent.applicableSections.join(','))); + } syncs.push({ type: 'image', diff --git a/modules/oxxionAnalyticsAdapter.js b/modules/oxxionAnalyticsAdapter.js index c4a51bfb7fa..25732d440ff 100644 --- a/modules/oxxionAnalyticsAdapter.js +++ b/modules/oxxionAnalyticsAdapter.js @@ -21,8 +21,9 @@ let saveEvents = {} let allEvents = {} let auctionEnd = {} let initOptions = {} +let mode = {}; let endpoint = 'https://default' -let requestsAttributes = ['adUnitCode', 'auctionId', 'bidder', 'bidderCode', 'bidId', 'cpm', 'creativeId', 'currency', 'width', 'height', 'mediaType', 'netRevenue', 'originalCpm', 'originalCurrency', 'requestId', 'size', 'source', 'status', 'timeToRespond', 'transactionId', 'ttl', 'sizes', 'mediaTypes', 'src', 'params', 'userId', 'labelAny', 'bids', 'adId']; +let requestsAttributes = ['adUnitCode', 'auctionId', 'bidder', 'bidderCode', 'bidId', 'cpm', 'creativeId', 'currency', 'width', 'height', 'mediaType', 'netRevenue', 'originalCpm', 'originalCurrency', 'requestId', 'size', 'source', 'status', 'timeToRespond', 'transactionId', 'ttl', 'sizes', 'mediaTypes', 'src', 'params', 'userId', 'labelAny', 'bids', 'adId', 'ova']; function getAdapterNameForAlias(aliasName) { return adapterManager.aliasRegistry[aliasName] || aliasName; @@ -41,16 +42,27 @@ function filterAttributes(arg, removead) { } if (typeof arg['gdprConsent'] != 'undefined') { response['gdprConsent'] = {}; - if (typeof arg['gdprConsent']['consentString'] != 'undefined') { response['gdprConsent']['consentString'] = arg['gdprConsent']['consentString']; } + if (typeof arg['gdprConsent']['consentString'] != 'undefined') { + response['gdprConsent']['consentString'] = arg['gdprConsent']['consentString']; + } } - if (typeof arg['meta'] == 'object' && typeof arg['meta']['advertiserDomains'] != 'undefined') { - response['meta'] = {'advertiserDomains': arg['meta']['advertiserDomains']}; + if (typeof arg['meta'] == 'object') { + response['meta'] = {}; + if (typeof arg['meta']['advertiserDomains'] != 'undefined') { + response['meta']['advertiserDomains'] = arg['meta']['advertiserDomains']; + } + if (typeof arg['meta']['demandSource'] == 'string') { + response['meta']['demandSource'] = arg['meta']['demandSource']; + } } requestsAttributes.forEach((attr) => { if (typeof arg[attr] != 'undefined') { response[attr] = arg[attr]; } }); - if (typeof response['creativeId'] == 'number') { response['creativeId'] = response['creativeId'].toString(); } + if (typeof response['creativeId'] == 'number') { + response['creativeId'] = response['creativeId'].toString(); + } } + response['oxxionMode'] = mode; return response; } @@ -77,7 +89,8 @@ function cleanAuctionEnd(args) { } function cleanCreatives(args) { - return filterAttributes(args, false); + let stringArgs = JSON.parse(dereferenceWithoutRenderer(args)); + return filterAttributes(stringArgs, false); } function enhanceMediaType(arg) { @@ -93,7 +106,7 @@ function enhanceMediaType(arg) { function addBidResponse(args) { let eventType = BID_RESPONSE; - let argsCleaned = cleanCreatives(JSON.parse(JSON.stringify(args))); ; + let argsCleaned = cleanCreatives(args); ; if (allEvents[eventType] == undefined) { allEvents[eventType] = [] } allEvents[eventType].push(argsCleaned); } @@ -110,7 +123,9 @@ function addTimeout(args) { if (saveEvents[eventType] == undefined) { saveEvents[eventType] = [] } saveEvents[eventType].push(args); let argsCleaned = []; - let argsDereferenced = JSON.parse(JSON.stringify(args)); + let argsDereferenced = {}; + let stringArgs = JSON.parse(dereferenceWithoutRenderer(args)); + argsDereferenced = stringArgs; argsDereferenced.forEach((attr) => { argsCleaned.push(filterAttributes(JSON.parse(JSON.stringify(attr)), false)); }); @@ -118,17 +133,42 @@ function addTimeout(args) { auctionEnd[eventType].push(argsCleaned); } +export const dereferenceWithoutRenderer = function(args) { + if (args.renderer) { + let tmp = args.renderer; + delete args.renderer; + let stringified = JSON.stringify(args); + args['renderer'] = tmp; + return stringified; + } + if (args.bidsReceived) { + let tmp = {} + for (let key in args.bidsReceived) { + if (args.bidsReceived[key].renderer) { + tmp[key] = args.bidsReceived[key].renderer; + delete args.bidsReceived[key].renderer; + } + } + let stringified = JSON.stringify(args); + for (let key in tmp) { + args.bidsReceived[key].renderer = tmp[key]; + } + return stringified; + } + return JSON.stringify(args); +} + function addAuctionEnd(args) { let eventType = AUCTION_END; if (saveEvents[eventType] == undefined) { saveEvents[eventType] = [] } saveEvents[eventType].push(args); - let argsCleaned = cleanAuctionEnd(JSON.parse(JSON.stringify(args))); + let argsCleaned = cleanAuctionEnd(JSON.parse(dereferenceWithoutRenderer(args))); if (auctionEnd[eventType] == undefined) { auctionEnd[eventType] = [] } auctionEnd[eventType].push(argsCleaned); } function handleBidWon(args) { - args = enhanceMediaType(filterAttributes(JSON.parse(JSON.stringify(args)), true)); + args = enhanceMediaType(filterAttributes(JSON.parse(dereferenceWithoutRenderer(args)), true)); let increment = args['cpm']; if (typeof saveEvents['auctionEnd'] == 'object') { saveEvents['auctionEnd'].forEach((auction) => { @@ -143,6 +183,15 @@ function handleBidWon(args) { } }); } + if (auction['auctionId'] == args['auctionId'] && typeof auction['bidderRequests'] == 'object') { + auction['bidderRequests'].forEach((req) => { + req.bids.forEach((bid) => { + if (bid['bidId'] == args['requestId'] && bid['transactionId'] == args['transactionId']) { + args['ova'] = bid['ova']; + } + }); + }); + } }); } args['cpmIncrement'] = increment; @@ -192,7 +241,8 @@ let oxxionAnalytics = Object.assign(adapter({url, analyticsType}), { addTimeout(args); break; } - }}); + } +}); // save the base class function oxxionAnalytics.originEnableAnalytics = oxxionAnalytics.enableAnalytics; @@ -201,7 +251,12 @@ oxxionAnalytics.originEnableAnalytics = oxxionAnalytics.enableAnalytics; oxxionAnalytics.enableAnalytics = function (config) { oxxionAnalytics.originEnableAnalytics(config); // call the base class function initOptions = config.options; - if (initOptions.domain) { endpoint = 'https://' + initOptions.domain; } + if (initOptions.domain) { + endpoint = 'https://' + initOptions.domain; + } + if (window.OXXION_MODE) { + mode = window.OXXION_MODE; + } }; adapterManager.registerAnalyticsAdapter({ diff --git a/modules/oxxionRtdProvider.js b/modules/oxxionRtdProvider.js index 4b5a7547088..a0476d8ca0f 100644 --- a/modules/oxxionRtdProvider.js +++ b/modules/oxxionRtdProvider.js @@ -1,97 +1,155 @@ import { submodule } from '../src/hook.js' -import { deepAccess, logInfo } from '../src/utils.js' +import { logInfo, logError } from '../src/utils.js' +import { ajax } from '../src/ajax.js'; +import adapterManager from '../src/adapterManager.js'; + +/** + * @typedef {import('../modules/rtdModule/index.js').RtdSubmodule} RtdSubmodule + */ -const oxxionRtdSearchFor = [ 'adUnitCode', 'auctionId', 'bidder', 'bidderCode', 'bidId', 'cpm', 'creativeId', 'currency', 'width', 'height', 'mediaType', 'netRevenue', 'originalCpm', 'originalCurrency', 'requestId', 'size', 'source', 'status', 'timeToRespond', 'transactionId', 'ttl', 'sizes', 'mediaTypes', 'src', 'userId', 'labelAny', 'adId' ]; const LOG_PREFIX = 'oxxionRtdProvider submodule: '; -const allAdUnits = []; +const bidderAliasRegistry = adapterManager.aliasRegistry || {}; /** @type {RtdSubmodule} */ export const oxxionSubmodule = { name: 'oxxionRtd', init: init, getBidRequestData: getAdUnits, - onBidResponseEvent: insertVideoTracking, + getRequestsList: getRequestsList, + getFilteredAdUnitsOnBidRates: getFilteredAdUnitsOnBidRates, }; function init(config, userConsent) { - if (!config.params || !config.params.domain || !config.params.contexts || !Array.isArray(config.params.contexts) || config.params.contexts.length == 0) { - return false - } - return true; + if (!config.params || !config.params.domain) { return false } + if (typeof config.params.threshold != 'undefined' && typeof config.params.samplingRate == 'number') { return true } + return false; } function getAdUnits(reqBidsConfigObj, callback, config, userConsent) { - const reqAdUnits = reqBidsConfigObj.adUnits; - if (Array.isArray(reqAdUnits)) { - reqAdUnits.forEach(adunit => { - if (config.params.contexts.includes(deepAccess(adunit, 'mediaTypes.video.context'))) { - allAdUnits.push(adunit); + const moduleStarted = new Date(); + logInfo(LOG_PREFIX + 'started with ', config); + if (typeof config.params.threshold != 'undefined' && typeof config.params.samplingRate == 'number') { + let filteredBids; + const requests = getRequestsList(reqBidsConfigObj); + const gdpr = userConsent && userConsent.gdpr ? userConsent.gdpr.consentString : null; + const payload = { + gdpr, + requests + }; + const endpoint = 'https://' + config.params.domain + '.oxxion.io/analytics/bid_rate_interests'; + getPromisifiedAjax(endpoint, JSON.stringify(payload), { + method: 'POST', + withCredentials: true + }).then(bidsRateInterests => { + if (bidsRateInterests.length) { + [reqBidsConfigObj.adUnits, filteredBids] = getFilteredAdUnitsOnBidRates(bidsRateInterests, reqBidsConfigObj.adUnits, config.params, true); } - }); - } -} - -function insertVideoTracking(bidResponse, config, userConsent) { - // this should only be do for video bids - if (bidResponse.mediaType === 'video') { - let maxCpm = 0; - const trackingUrl = getImpUrl(config, bidResponse, maxCpm); - if (!trackingUrl) { - return; - } - // Vast Impression URL - if (bidResponse.vastUrl) { - bidResponse.vastImpUrl = bidResponse.vastImpUrl - ? trackingUrl + '&url=' + encodeURI(bidResponse.vastImpUrl) - : trackingUrl; - logInfo(LOG_PREFIX + 'insert into vastImpUrl for adId ' + bidResponse.adId); - } - // Vast XML document - if (bidResponse.vastXml !== undefined) { - const doc = new DOMParser().parseFromString(bidResponse.vastXml, 'text/xml'); - const wrappers = doc.querySelectorAll('VAST Ad Wrapper, VAST Ad InLine'); - let hasAltered = false; - if (wrappers.length) { - wrappers.forEach(wrapper => { - const impression = doc.createElement('Impression'); - impression.appendChild(doc.createCDATASection(trackingUrl)); - wrapper.appendChild(impression) + if (filteredBids.length > 0) { + getPromisifiedAjax('https://' + config.params.domain + '.oxxion.io/analytics/request_rejecteds', JSON.stringify({'bids': filteredBids, 'gdpr': gdpr}), { + method: 'POST', + withCredentials: true }); - bidResponse.vastXml = new XMLSerializer().serializeToString(doc); - hasAltered = true; } - if (hasAltered) { - logInfo(LOG_PREFIX + 'insert into vastXml for adId ' + bidResponse.adId); + if (typeof callback == 'function') { callback(); } + const timeToRun = new Date() - moduleStarted; + logInfo(LOG_PREFIX + ' time to run: ' + timeToRun); + if (getRandomNumber(50) == 1) { + ajax('https://' + config.params.domain + '.oxxion.io/ova/time', null, JSON.stringify({'duration': timeToRun, 'auctionId': reqBidsConfigObj.auctionId}), {method: 'POST', withCredentials: true}); } - } + }).catch(error => logError(LOG_PREFIX, 'bidInterestError', error)); } } -function getImpUrl(config, data, maxCpm) { - const adUnitCode = data.adUnitCode; - const adUnits = allAdUnits.find(adunit => adunit.code === adUnitCode && - 'mediaTypes' in adunit && - 'video' in adunit.mediaTypes && - typeof adunit.mediaTypes.video.context === 'string'); - const context = adUnits !== undefined - ? adUnits.mediaTypes.video.context - : 'unknown'; - if (!config.params.contexts.includes(context)) { - return false; - } - let trackingImpUrl = 'https://' + config.params.domain + '.oxxion.io/analytics/vast_imp?'; - trackingImpUrl += oxxionRtdSearchFor.reduce((acc, param) => { - switch (typeof data[param]) { - case 'string': - case 'number': - acc += param + '=' + data[param] + '&' - break; +function getPromisifiedAjax (url, data = {}, options = {}) { + return new Promise((resolve, reject) => { + const callbacks = { + success(responseText, { response }) { + resolve(JSON.parse(response)); + }, + error(error) { + reject(error); + } + }; + ajax(url, callbacks, data, options); + }) +} + +function getFilteredAdUnitsOnBidRates (bidsRateInterests, adUnits, params, useSampling) { + const { threshold, samplingRate } = params; + const sampling = getRandomNumber(100) < samplingRate && useSampling; + const filteredBids = []; + // Separate bidsRateInterests in two groups against threshold & samplingRate + const { interestingBidsRates, uninterestingBidsRates, sampledBidsRates } = bidsRateInterests.reduce((acc, interestingBid) => { + const isBidRateUpper = typeof threshold == 'number' ? interestingBid.rate === true || interestingBid.rate > threshold : interestingBid.suggestion; + const isBidInteresting = isBidRateUpper || sampling; + const key = isBidInteresting ? 'interestingBidsRates' : 'uninterestingBidsRates'; + acc[key].push(interestingBid); + if (!isBidRateUpper && sampling) { + acc['sampledBidsRates'].push(interestingBid); } return acc; - }, ''); - const cpmIncrement = 0.0; - return trackingImpUrl + 'cpmIncrement=' + cpmIncrement + '&context=' + context; + }, { + interestingBidsRates: [], + uninterestingBidsRates: [], // Do something with later + sampledBidsRates: [] + }); + logInfo(LOG_PREFIX, 'getFilteredAdUnitsOnBidRates()', interestingBidsRates, uninterestingBidsRates); + // Filter bids and adUnits against interesting bids rates + const newAdUnits = adUnits.filter(({ bids = [] }, adUnitIndex) => { + adUnits[adUnitIndex].bids = bids.filter((bid, bidIndex) => { + if (!params.bidders || params.bidders.includes(bid.bidder)) { + const index = interestingBidsRates.findIndex(({ id }) => id === bid._id); + if (index == -1) { + let tmpBid = bid; + tmpBid['code'] = adUnits[adUnitIndex].code; + tmpBid['mediaTypes'] = adUnits[adUnitIndex].mediaTypes; + tmpBid['originalBidder'] = bidderAliasRegistry[bid.bidder] || bid.bidder; + if (tmpBid.floorData) { + delete tmpBid.floorData; + } + filteredBids.push(tmpBid); + adUnits[adUnitIndex].bids[bidIndex]['ova'] = 'filtered'; + } else { + if (sampledBidsRates.findIndex(({ id }) => id === bid._id) == -1) { + adUnits[adUnitIndex].bids[bidIndex]['ova'] = 'cleared'; + } else { + adUnits[adUnitIndex].bids[bidIndex]['ova'] = 'sampled'; + logInfo(LOG_PREFIX + ' sampled ! '); + } + } + delete bid._id; + return index !== -1; + } else { + adUnits[adUnitIndex].bids[bidIndex]['ova'] = 'protected'; + return true; + } + }); + return !!adUnits[adUnitIndex].bids.length; + }); + return [newAdUnits, filteredBids]; +} + +function getRandomNumber (max = 10) { + return Math.round(Math.random() * max); +} + +function getRequestsList(reqBidsConfigObj) { + let count = 0; + return reqBidsConfigObj.adUnits.flatMap(({ + bids = [], + mediaTypes = {}, + code = '' + }) => bids.reduce((acc, { bidder = '', params = {} }, index) => { + const id = count++; + bids[index]._id = id; + return acc.concat({ + id, + adUnit: code, + bidder, + mediaTypes, + }); + }, [])); } submodule('realTimeData', oxxionSubmodule); diff --git a/modules/oxxionRtdProvider.md b/modules/oxxionRtdProvider.md index 061acdbe11a..bfdbfae1fa9 100644 --- a/modules/oxxionRtdProvider.md +++ b/modules/oxxionRtdProvider.md @@ -7,12 +7,12 @@ Maintainer: tech@oxxion.io # Oxxion Real-Time-Data submodule Oxxion helps you to understand how your prebid stack performs. -This Rtd module is to use in order to improve video events tracking. +This Rtd module purpose is to filter bidders requested. # Integration Make sure to have the following modules listed while building prebid : `rtdModule,oxxionRtdProvider` -`rtbModule` is required to activate real-time-data submodules. +`rtdModule` is required to activate real-time-data submodules. For example : ``` gulp build --modules=schain,priceFloors,currency,consentManagement,appnexusBidAdapter,rubiconBidAdapter,rtdModule,oxxionRtdProvider @@ -23,14 +23,15 @@ Then add the oxxion Rtd module to your prebid configuration : pbjs.setConfig( ... realTimeData: { - auctionDelay: 200, + auctionDelay: 300, dataProviders: [ { name: "oxxionRtd", waitForIt: true, params: { domain: "test.endpoint", - contexts: ["instream"], + threshold: false, + samplingRate: 10, } } ] @@ -39,10 +40,17 @@ pbjs.setConfig( ) ``` -# setConfig Parameters +# setConfig Parameters General | Name | Type | Description | |:---------------------------------|:---------|:------------------------------------------------------------------------------------------------------------| | domain | String | This string identifies yourself in Oxxion's systems and is provided to you by your Oxxion representative. | -| contexts | Array | Array defining which video contexts to add tracking events into. Values can be instream and/or outstream. | + +# setConfig Parameters for bidder filtering + +| Name | Type | Description | +|:---------------------------------|:-----------|:------------------------------------------------------------------------------------------------------------| +| threshold | Float/Bool | False or minimum expected bid rate to call a bidder (ex: 1.0 for 1% bid rate). | +| samplingRate | Integer | Percentage of request not meeting the criterias to run anyway in order to check for any change. | +| bidders | Array | Optional: If set, filtering will only be applied to bidders listed. diff --git a/modules/ozoneBidAdapter.js b/modules/ozoneBidAdapter.js index 1b725ce3a05..0d921f57cda 100644 --- a/modules/ozoneBidAdapter.js +++ b/modules/ozoneBidAdapter.js @@ -7,7 +7,8 @@ import { isArray, contains, mergeDeep, - parseUrl + parseUrl, + generateUUID } from '../src/utils.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; import { BANNER, NATIVE, VIDEO } from '../src/mediaTypes.js'; @@ -16,15 +17,15 @@ import {getPriceBucketString} from '../src/cpmBucketManager.js'; import { Renderer } from '../src/Renderer.js'; import {getRefererInfo} from '../src/refererDetection.js'; const BIDDER_CODE = 'ozone'; -const ORIGIN = 'https://elb.the-ozone-project.com'; // applies only to auction & cookie +const ORIGIN = 'https://elb.the-ozone-project.com' // applies only to auction & cookie const AUCTIONURI = '/openrtb2/auction'; const OZONECOOKIESYNC = '/static/load-cookie.html'; const OZONE_RENDERER_URL = 'https://prebid.the-ozone-project.com/ozone-renderer.js'; const ORIGIN_DEV = 'https://test.ozpr.net'; -const OZONEVERSION = '2.8.0'; +const OZONEVERSION = '2.9.1'; export const spec = { gvlid: 524, - aliases: [{code: 'lmc', gvlid: 524}], + aliases: [{code: 'lmc', gvlid: 524}, {code: 'venatus', gvlid: 524}], version: OZONEVERSION, code: BIDDER_CODE, supportedMediaTypes: [VIDEO, BANNER], @@ -36,7 +37,8 @@ export const spec = { 'keyPrefix': 'oz', 'auctionUrl': ORIGIN + AUCTIONURI, 'cookieSyncUrl': ORIGIN + OZONECOOKIESYNC, - 'rendererUrl': OZONE_RENDERER_URL + 'rendererUrl': OZONE_RENDERER_URL, + 'batchRequests': false /* you can change this to true OR override it in the config: config.ozone.batchRequests */ }, loadWhitelabelData(bid) { if (this.propertyBag.whitelabel) { return; } @@ -73,6 +75,12 @@ export const spec = { this.propertyBag.whitelabel.auctionUrl = bidderConfig.endpointOverride.auctionUrl; } } + if (bidderConfig.hasOwnProperty('batchRequests')) { + this.propertyBag.whitelabel.batchRequests = bidderConfig.batchRequests; + } + if (arr.hasOwnProperty('batchRequests')) { + this.propertyBag.whitelabel.batchRequests = true; + } try { if (arr.hasOwnProperty('auction') && arr.auction === 'dev') { logInfo('GET: auction=dev'); @@ -94,11 +102,15 @@ export const spec = { getRendererUrl() { return this.propertyBag.whitelabel.rendererUrl; }, + isBatchRequests() { + logInfo('isBatchRequests going to return ', this.propertyBag.whitelabel.batchRequests); + return this.propertyBag.whitelabel.batchRequests; + }, isBidRequestValid(bid) { this.loadWhitelabelData(bid); logInfo('isBidRequestValid : ', config.getConfig(), bid); let adUnitCode = bid.adUnitCode; // adunit[n].code - let err1 = 'VALIDATION FAILED : missing {param} : siteId, placementId and publisherId are REQUIRED'; + let err1 = 'VALIDATION FAILED : missing {param} : siteId, placementId and publisherId are REQUIRED' if (!(bid.params.hasOwnProperty('placementId'))) { logError(err1.replace('{param}', 'placementId'), adUnitCode); return false; @@ -199,7 +211,7 @@ export const spec = { let placementId = placementIdOverrideFromGetParam || this.getPlacementId(ozoneBidRequest); // prefer to use a valid override param, else the bidRequest placement Id obj.id = ozoneBidRequest.bidId; // this causes an error if we change it to something else, even if you update the bidRequest object: "WARNING: Bidder ozone made bid for unknown request ID: mb7953.859498327448. Ignoring." obj.tagid = placementId; - let parsed = parseUrl(getRefererInfo().page); + let parsed = parseUrl(this.getRefererInfo().page); obj.secure = parsed.protocol === 'https' ? 1 : 0; let arrBannerSizes = []; if (!ozoneBidRequest.hasOwnProperty('mediaTypes')) { @@ -262,9 +274,7 @@ export const spec = { obj.placementId = placementId; deepSetValue(obj, 'ext.prebid', {'storedrequest': {'id': placementId}}); obj.ext[whitelabelBidder] = {}; - // TODO: fix auctionId/transactionID leak: https://github.com/prebid/Prebid.js/issues/9781 obj.ext[whitelabelBidder].adUnitCode = ozoneBidRequest.adUnitCode; // eg. 'mpu' - obj.ext[whitelabelBidder].transactionId = ozoneBidRequest.transactionId; // this is the transactionId PER adUnit, common across bidders for this unit if (ozoneBidRequest.params.hasOwnProperty('customData')) { obj.ext[whitelabelBidder].customData = ozoneBidRequest.params.customData; } @@ -291,6 +301,10 @@ export const spec = { if (!schain && deepAccess(ozoneBidRequest, 'schain')) { schain = ozoneBidRequest.schain; } + let gpid = deepAccess(ozoneBidRequest, 'ortb2Imp.ext.gpid'); + if (gpid) { + deepSetValue(obj, 'ext.gpid', gpid); + } return obj; }); let extObj = {}; @@ -326,7 +340,7 @@ export const spec = { let userExtEids = deepAccess(validBidRequests, '0.userIdAsEids', []); // generate the UserIDs in the correct format for UserId module ozoneRequest.site = { 'publisher': {'id': htmlParams.publisherId}, - 'page': getRefererInfo().page, + 'page': this.getRefererInfo().page, 'id': htmlParams.siteId }; ozoneRequest.test = config.getConfig('debug') ? 1 : 0; @@ -355,13 +369,33 @@ export const spec = { if (config.getConfig('coppa') === true) { deepSetValue(ozoneRequest, 'regs.coppa', 1); } + let ozUuid = generateUUID(); + if (this.isBatchRequests()) { + logInfo('going to batch the requests'); + let arrRet = []; // return an array of objects containing data describing max 10 bids + for (let i = 0; i < tosendtags.length; i += 10) { + ozoneRequest.id = ozUuid; // Unique ID of the bid request, provided by the exchange. (REQUIRED) + ozoneRequest.imp = tosendtags.slice(i, i + 10); + ozoneRequest.ext = extObj; + deepSetValue(ozoneRequest, 'user.ext.eids', userExtEids); + if (ozoneRequest.imp.length > 0) { + arrRet.push({ + method: 'POST', + url: this.getAuctionUrl(), + data: JSON.stringify(ozoneRequest), + bidderRequest: bidderRequest + }); + } + } + logInfo('batch request going to return : ', arrRet); + return arrRet; + } + logInfo('requests will not be batched.'); if (singleRequest) { logInfo('buildRequests starting to generate response for a single request'); - ozoneRequest.id = bidderRequest.auctionId; // Unique ID of the bid request, provided by the exchange. - ozoneRequest.auctionId = bidderRequest.auctionId; // not sure if this should be here? + ozoneRequest.id = ozUuid; // Unique ID of the bid request, provided by the exchange. (REQUIRED) ozoneRequest.imp = tosendtags; ozoneRequest.ext = extObj; - deepSetValue(ozoneRequest, 'source.tid', bidderRequest.auctionId);// RTB 2.5 : tid is Transaction ID that must be common across all participants in this bid request (e.g., potentially multiple exchanges). deepSetValue(ozoneRequest, 'user.ext.eids', userExtEids); var ret = { method: 'POST', @@ -377,12 +411,9 @@ export const spec = { let arrRet = tosendtags.map(imp => { logInfo('buildRequests starting to generate non-single response, working on imp : ', imp); let ozoneRequestSingle = Object.assign({}, ozoneRequest); - imp.ext[whitelabelBidder].pageAuctionId = bidderRequest['auctionId']; // make a note in the ext object of what the original auctionId was, in the bidderRequest object - ozoneRequestSingle.id = imp.ext[whitelabelBidder].transactionId; // Unique ID of the bid request, provided by the exchange. - ozoneRequestSingle.auctionId = imp.ext[whitelabelBidder].transactionId; // not sure if this should be here? + ozoneRequestSingle.id = generateUUID(); // Unique ID of the bid request, provided by the exchange. (REQUIRED) ozoneRequestSingle.imp = [imp]; ozoneRequestSingle.ext = extObj; - deepSetValue(ozoneRequestSingle, 'source.tid', bidderRequest.auctionId);// RTB 2.5 : tid is Transaction ID that must be common across all participants in this bid request (e.g., potentially multiple exchanges). deepSetValue(ozoneRequestSingle, 'user.ext.eids', userExtEids); logInfo('buildRequests RequestSingle (for non-single) = ', ozoneRequestSingle); return { @@ -424,6 +455,7 @@ export const spec = { logInfo(`interpretResponse time: ${startTime} . Time between buildRequests done and interpretResponse start was ${startTime - this.propertyBag.buildRequestsEnd}ms`); logInfo(`serverResponse, request`, JSON.parse(JSON.stringify(serverResponse)), JSON.parse(JSON.stringify(request))); serverResponse = serverResponse.body || {}; + let aucId = serverResponse.id; // this will be correct for single requests and non-single if (!serverResponse.hasOwnProperty('seatbid')) { return []; } @@ -518,7 +550,7 @@ export const spec = { } } let {seat: winningSeat, bid: winningBid} = ozoneGetWinnerForRequestBid(thisBid.bidId, serverResponse.seatbid); - adserverTargeting[whitelabelPrefix + '_auc_id'] = String(request.bidderRequest.auctionId); + adserverTargeting[whitelabelPrefix + '_auc_id'] = String(aucId); // was request.bidderRequest.auctionId adserverTargeting[whitelabelPrefix + '_winner'] = String(winningSeat); adserverTargeting[whitelabelPrefix + '_bid'] = 'true'; adserverTargeting[whitelabelPrefix + '_cache_id'] = deepAccess(thisBid, 'ext.prebid.targeting.hb_cache_id', 'no-id'); @@ -686,10 +718,29 @@ export const spec = { return null; }, getGetParametersAsObject() { - let parsed = parseUrl(getRefererInfo().page); + let parsed = parseUrl(this.getRefererInfo().location); logInfo('getGetParametersAsObject found:', parsed.search); return parsed.search; }, + getRefererInfo() { + if (getRefererInfo().hasOwnProperty('location')) { + logInfo('FOUND location on getRefererInfo OK (prebid >= 7); will use getRefererInfo for location & page'); + return getRefererInfo(); + } else { + logInfo('DID NOT FIND location on getRefererInfo (prebid < 7); will use legacy code that ALWAYS worked reliably to get location & page ;-)'); + try { + return { + page: top.location.href, + location: top.location.href + }; + } catch (e) { + return { + page: window.location.href, + location: window.location.href + }; + } + } + }, blockTheRequest() { let ozRequest = this.getWhitelabelConfigItem('ozone.oz_request'); if (typeof ozRequest == 'boolean' && !ozRequest) { diff --git a/modules/ozoneBidAdapter.md b/modules/ozoneBidAdapter.md index 6f4cf752f22..9787e069283 100644 --- a/modules/ozoneBidAdapter.md +++ b/modules/ozoneBidAdapter.md @@ -73,6 +73,8 @@ adUnits = [{ }]; ``` + +``` //Instream Video adUnit adUnits = [{ @@ -108,4 +110,4 @@ adUnits = [{ } }] }; -``` +``` \ No newline at end of file diff --git a/modules/pairIdSystem.js b/modules/pairIdSystem.js index f90e25fdd0f..dbff4c6a402 100644 --- a/modules/pairIdSystem.js +++ b/modules/pairIdSystem.js @@ -10,6 +10,10 @@ import {getStorageManager} from '../src/storageManager.js' import { logInfo } from '../src/utils.js'; import {MODULE_TYPE_UID} from '../src/activities/modules.js'; +/** + * @typedef {import('../modules/userId/index.js').Submodule} Submodule + */ + const MODULE_NAME = 'pairId'; const PAIR_ID_KEY = 'pairId'; const DEFAULT_LIVERAMP_PAIR_ID_KEY = '_lr_pairId'; @@ -27,29 +31,29 @@ function pairIdFromCookie(key) { /** @type {Submodule} */ export const pairIdSubmodule = { /** - * used to link submodule with config - * @type {string} - */ + * used to link submodule with config + * @type {string} + */ name: MODULE_NAME, /** - * used to specify vendor id - * @type {number} - */ + * used to specify vendor id + * @type {number} + */ gvlid: 755, /** - * decode the stored id value for passing to bid requests - * @function - * @param { string | undefined } value - * @returns {{pairId:string} | undefined } - */ + * decode the stored id value for passing to bid requests + * @function + * @param { string | undefined } value + * @returns {{pairId:string} | undefined } + */ decode(value) { return value && Array.isArray(value) ? {'pairId': value} : undefined }, /** - * performs action to obtain id and return a value in the callback's response argument - * @function - * @returns {id: string | undefined } - */ + * performs action to obtain id and return a value in the callback's response argument + * @function + * @returns {id: string | undefined } + */ getId(config) { const pairIdsString = pairIdFromLocalStorage(PAIR_ID_KEY) || pairIdFromCookie(PAIR_ID_KEY) let ids = [] @@ -78,6 +82,12 @@ export const pairIdSubmodule = { return undefined; } return {'id': ids}; + }, + eids: { + 'pairId': { + source: 'google.com', + atype: 571187 + }, } }; diff --git a/modules/pangleBidAdapter.js b/modules/pangleBidAdapter.js new file mode 100644 index 00000000000..f4a52168743 --- /dev/null +++ b/modules/pangleBidAdapter.js @@ -0,0 +1,178 @@ +// ver V1.0.4 +import { BANNER, VIDEO } from '../src/mediaTypes.js'; +import { ortbConverter } from '../libraries/ortbConverter/converter.js' +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { deepSetValue, generateUUID, timestamp, deepAccess } from '../src/utils.js'; +import { getStorageManager } from '../src/storageManager.js'; +import { MODULE_TYPE_RTD } from '../src/activities/modules.js'; + +import { Renderer } from '../src/Renderer.js'; + +const BIDDER_CODE = 'pangle'; +const ENDPOINT = 'https://pangle.pangleglobal.com/api/ad/union/web_js/common/get_ads'; + +const OUTSTREAM_RENDERER_URL = 'https://sf16-static.i18n-pglstatp.com/obj/ad-pattern-sg/pangle/web/ads/video.js'; + +const DEFAULT_BID_TTL = 30; +const DEFAULT_CURRENCY = 'USD'; +const DEFAULT_NET_REVENUE = true; +const PANGLE_COOKIE = '_pangle_id'; +const COOKIE_EXP = 86400 * 1000 * 365 * 1; // 1 year +const MEDIA_TYPES = { + Banner: 1, + Video: 2 +}; + +export const storage = getStorageManager({ moduleType: MODULE_TYPE_RTD, moduleName: BIDDER_CODE }) + +export function isValidUuid(uuid) { + return /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test( + uuid + ); +} + +function getPangleCookieId() { + let sid = storage.cookiesAreEnabled() && storage.getCookie(PANGLE_COOKIE); + + if (!sid || !isValidUuid(sid)) { + sid = generateUUID(); + setPangleCookieId(sid); + } + + return sid; +} + +function setPangleCookieId(sid) { + if (storage.cookiesAreEnabled()) { + const expires = new Date(timestamp() + COOKIE_EXP).toGMTString(); + + storage.setCookie(PANGLE_COOKIE, sid, expires); + } +} + +function createRequest(bidRequests, bidderRequest, mediaType) { + const data = converter.toORTB({ + bidRequests, + bidderRequest, + context: { mediaType }, + }); + const devicetype = spec.getDeviceType(navigator.userAgent); + deepSetValue(data, 'device.devicetype', devicetype); + if (bidderRequest.userId && typeof bidderRequest.userId === 'object') { + const pangleId = getPangleCookieId(); + // add pangle cookie + const _eids = data.user?.ext?.eids ?? []; + deepSetValue(data, 'user.ext.eids', [ + ..._eids, + { + source: document.location.host, + uids: [ + { + id: pangleId, + atype: 1, + }, + ], + }, + ]); + } + bidRequests.forEach((item, idx) => { + deepSetValue(data.imp[idx], 'ext.networkids', item.params); + deepSetValue(data.imp[idx], 'banner.api', [5]); + deepSetValue(data, 'test', item.params.test ?? 0) + }); + return { + method: 'POST', + url: ENDPOINT, + data, + options: { contentType: 'application/json', withCredentials: true } + } +} + +function isVideoBid(bid) { + return !!deepAccess(bid, 'mediaTypes.video'); +} + +function isBannerBid(bid) { + return !!deepAccess(bid, 'mediaTypes.banner'); +} + +function renderOutstream(bid) { + bid.renderer.push(() => { + window.outstreamPlayer({ bid, codeId: bid.adUnitCode }); + }); +} + +const converter = ortbConverter({ + context: { + netRevenue: DEFAULT_NET_REVENUE, + ttl: DEFAULT_BID_TTL, + currency: DEFAULT_CURRENCY, + }, + bidResponse(buildBidResponse, bid, context) { + const { bidRequest } = context; + let bidResponse; + if (bid.mtype === MEDIA_TYPES.Video) { + context.mediaType = VIDEO; + bidResponse = buildBidResponse(bid, context); + if (bidRequest.mediaTypes.video?.context === 'outstream') { + const renderer = Renderer.install({id: bid.bidId, url: OUTSTREAM_RENDERER_URL, adUnitCode: bid.adUnitCode}); + renderer.setRender(renderOutstream); + bidResponse.renderer = renderer; + } + } + if (bid.mtype === MEDIA_TYPES.Banner) { + context.mediaType = BANNER; + bidResponse = buildBidResponse(bid, context); + } + return bidResponse; + }, +}); + +export const spec = { + code: BIDDER_CODE, + supportedMediaTypes: [BANNER, VIDEO], + + getDeviceType: function (ua) { + if ( + /ipad|android 3.0|xoom|sch-i800|playbook|tablet|kindle/i.test( + ua.toLowerCase() + ) + ) { + return 5; // 'tablet' + } + if ( + /iphone|ipod|android|blackberry|opera|mini|windows\sce|palm|smartphone|iemobile/i.test( + ua.toLowerCase() + ) + ) { + return 4; // 'mobile' + } + return 2; // 'desktop' + }, + + isBidRequestValid: function (bid) { + return Boolean(bid.params.token); + }, + + buildRequests(bidRequests, bidderRequest) { + const videoBids = bidRequests.filter((bid) => isVideoBid(bid)); + const bannerBids = bidRequests.filter((bid) => isBannerBid(bid)); + let requests = bannerBids.length + ? [createRequest(bannerBids, bidderRequest, BANNER)] + : []; + videoBids.forEach((bid) => { + requests.push(createRequest([bid], bidderRequest, VIDEO)); + }); + return requests; + }, + + interpretResponse(response, request) { + const bids = converter.fromORTB({ + response: response.body, + request: request.data, + }).bids; + return bids; + }, +}; + +registerBidder(spec); diff --git a/modules/pangleBidAdapter.md b/modules/pangleBidAdapter.md new file mode 100644 index 00000000000..8fc628dcc89 --- /dev/null +++ b/modules/pangleBidAdapter.md @@ -0,0 +1,32 @@ +# Overview + +Module Name: pangle Bidder Adapter +Module Type: Bidder Adapter +Maintainer: + +# Description + +An adapter to get a bid from pangle DSP. + +# Test Parameters + +```javascript +var adUnits = [{ + // banner + code: 'test1', + mediaTypes: { + banner: { + sizes: [[300, 250]] + } + }, + + bids: [{ + bidder: 'pangle', + params: { + token: 'aass', + appid: 612, + placementid: 123, + } + }] +}]; +``` diff --git a/modules/parrableIdSystem.js b/modules/parrableIdSystem.js index 01ffec3c249..5651bdf0434 100644 --- a/modules/parrableIdSystem.js +++ b/modules/parrableIdSystem.js @@ -7,7 +7,17 @@ // ci trigger: 1 -import {contains, deepClone, inIframe, isEmpty, isPlainObject, logError, logWarn, timestamp} from '../src/utils.js'; +import { + contains, + deepClone, + inIframe, + isEmpty, + isPlainObject, + logError, + logWarn, + pick, + timestamp +} from '../src/utils.js'; import {find} from '../src/polyfill.js'; import {ajax} from '../src/ajax.js'; import {submodule} from '../src/hook.js'; @@ -16,6 +26,12 @@ import {uspDataHandler} from '../src/adapterManager.js'; import {getStorageManager} from '../src/storageManager.js'; import {MODULE_TYPE_UID} from '../src/activities/modules.js'; +/** + * @typedef {import('../modules/userId/index.js').Submodule} Submodule + * @typedef {import('../modules/userId/index.js').SubmoduleConfig} SubmoduleConfig + * @typedef {import('../modules/userId/index.js').ConsentData} ConsentData + */ + const PARRABLE_URL = 'https://h.parrable.com/prebid'; const PARRABLE_COOKIE_NAME = '_parrable_id'; const PARRABLE_GVLID = 928; @@ -368,7 +384,33 @@ export const parrableIdSubmodule = { getId(config, gdprConsentData, currentStoredId) { const configParams = (config && config.params) || {}; return fetchId(configParams, gdprConsentData); - } + }, + eids: { + 'parrableId': { + source: 'parrable.com', + atype: 1, + getValue: function(parrableId) { + if (parrableId.eid) { + return parrableId.eid; + } + if (parrableId.ccpaOptout) { + // If the EID was suppressed due to a non consenting ccpa optout then + // we still wish to provide this as a reason to the adapters + return ''; + } + return null; + }, + getUidExt: function(parrableId) { + const extendedData = pick(parrableId, [ + 'ibaOptout', + 'ccpaOptout' + ]); + if (Object.keys(extendedData).length) { + return extendedData; + } + } + }, + }, }; submodule('userId', parrableIdSubmodule); diff --git a/modules/permutiveRtdProvider.js b/modules/permutiveRtdProvider.js index 697d7721205..5a63990f84f 100644 --- a/modules/permutiveRtdProvider.js +++ b/modules/permutiveRtdProvider.js @@ -12,6 +12,10 @@ import {deepAccess, deepSetValue, isFn, logError, mergeDeep, isPlainObject, safe import {includes} from '../src/polyfill.js'; import {MODULE_TYPE_RTD} from '../src/activities/modules.js'; +/** + * @typedef {import('../modules/rtdModule/index.js').RtdSubmodule} RtdSubmodule + */ + const MODULE_NAME = 'permutive' const logger = prefixLog('[PermutiveRTD]') diff --git a/modules/pgamsspBidAdapter.js b/modules/pgamsspBidAdapter.js new file mode 100644 index 00000000000..b38131c1c18 --- /dev/null +++ b/modules/pgamsspBidAdapter.js @@ -0,0 +1,229 @@ +import { isFn, deepAccess, logMessage, logError } from '../src/utils.js'; +import { convertOrtbRequestToProprietaryNative } from '../src/native.js'; + +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { BANNER, NATIVE, VIDEO } from '../src/mediaTypes.js'; +import { config } from '../src/config.js'; + +const BIDDER_CODE = 'pgamssp'; +const AD_URL = 'https://us-east.pgammedia.com/pbjs'; +const SYNC_URL = 'https://cs.pgammedia.com'; + +function isBidResponseValid(bid) { + if (!bid.requestId || !bid.cpm || !bid.creativeId || !bid.ttl || !bid.currency) { + return false; + } + + switch (bid.mediaType) { + case BANNER: + return Boolean(bid.width && bid.height && bid.ad); + case VIDEO: + return Boolean(bid.vastUrl || bid.vastXml); + case NATIVE: + return Boolean(bid.native && bid.native.impressionTrackers && bid.native.impressionTrackers.length); + default: + return false; + } +} + +function getPlacementReqData(bid) { + const { params, bidId, mediaTypes } = bid; + const schain = bid.schain || {}; + const { placementId, endpointId } = params; + const bidfloor = getBidFloor(bid); + + const placement = { + bidId, + schain, + bidfloor, + eids: [] + }; + + if (bid.userId) { + getUserId(placement.eids, bid.userId.uid2 && bid.userId.uid2.id, 'uidapi.com'); + } + + 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; + } +} +function getUserId(eids, id, source, uidExt) { + if (id) { + var uid = { id }; + if (uidExt) { + uid.ext = uidExt; + } + eids.push({ + source, + uids: [ uid ] + }); + } +} + +export const spec = { + code: BIDDER_CODE, + supportedMediaTypes: [BANNER, VIDEO, NATIVE], + + isBidRequestValid: (bid = {}) => { + const { params, bidId, mediaTypes } = bid; + let valid = Boolean(bidId && params && (params.placementId || params.endpointId)); + + if (mediaTypes && mediaTypes[BANNER]) { + valid = valid && Boolean(mediaTypes[BANNER] && mediaTypes[BANNER].sizes); + } else if (mediaTypes && mediaTypes[VIDEO]) { + valid = valid && Boolean(mediaTypes[VIDEO] && mediaTypes[VIDEO].playerSize); + } else if (mediaTypes && mediaTypes[NATIVE]) { + valid = valid && Boolean(mediaTypes[NATIVE]); + } else { + valid = false; + } + return valid; + }, + + buildRequests: (validBidRequests = [], bidderRequest = {}) => { + // convert Native ORTB definition to old-style prebid native definition + validBidRequests = convertOrtbRequestToProprietaryNative(validBidRequests); + + let deviceWidth = 0; + let deviceHeight = 0; + + let winLocation; + try { + const winTop = window.top; + deviceWidth = winTop.screen.width; + deviceHeight = winTop.screen.height; + winLocation = winTop.location; + } catch (e) { + logMessage(e); + winLocation = window.location; + } + + const refferUrl = bidderRequest.refererInfo && bidderRequest.refererInfo.page; + let refferLocation; + try { + refferLocation = refferUrl && new URL(refferUrl); + } catch (e) { + logMessage(e); + } + // TODO: does the fallback make sense here? + let location = refferLocation || winLocation; + const language = (navigator && navigator.language) ? navigator.language.split('-')[0] : ''; + const host = location.host; + const page = location.pathname; + const secure = location.protocol === 'https:' ? 1 : 0; + const placements = []; + const request = { + deviceWidth, + deviceHeight, + language, + secure, + host, + page, + placements, + coppa: config.getConfig('coppa') === true ? 1 : 0, + ccpa: bidderRequest.uspConsent || undefined, + gdpr: bidderRequest.gdprConsent || undefined, + tmax: bidderRequest.timeout + }; + + const len = validBidRequests.length; + for (let i = 0; i < len; i++) { + const bid = validBidRequests[i]; + placements.push(getPlacementReqData(bid)); + } + + return { + method: 'POST', + url: AD_URL, + data: request + }; + }, + + interpretResponse: (serverResponse) => { + let response = []; + for (let i = 0; i < serverResponse.body.length; i++) { + let resItem = serverResponse.body[i]; + if (isBidResponseValid(resItem)) { + const advertiserDomains = resItem.adomain && resItem.adomain.length ? resItem.adomain : []; + resItem.meta = { ...resItem.meta, advertiserDomains }; + + response.push(resItem); + } + } + return response; + }, + + getUserSyncs: (syncOptions, serverResponses, gdprConsent, uspConsent) => { + let syncType = syncOptions.iframeEnabled ? 'iframe' : 'image'; + let syncUrl = SYNC_URL + `/${syncType}?pbjs=1`; + if (gdprConsent && gdprConsent.consentString) { + if (typeof gdprConsent.gdprApplies === 'boolean') { + syncUrl += `&gdpr=${Number(gdprConsent.gdprApplies)}&gdpr_consent=${gdprConsent.consentString}`; + } else { + syncUrl += `&gdpr=0&gdpr_consent=${gdprConsent.consentString}`; + } + } + if (uspConsent && uspConsent.consentString) { + syncUrl += `&ccpa_consent=${uspConsent.consentString}`; + } + + const coppa = config.getConfig('coppa') ? 1 : 0; + syncUrl += `&coppa=${coppa}`; + + return [{ + type: syncType, + url: syncUrl + }]; + } +}; + +registerBidder(spec); diff --git a/modules/pgamsspBidAdapter.md b/modules/pgamsspBidAdapter.md new file mode 100644 index 00000000000..c162ec33053 --- /dev/null +++ b/modules/pgamsspBidAdapter.md @@ -0,0 +1,79 @@ +# Overview + +``` +Module Name: PGAMSSP Bidder Adapter +Module Type: PGAMSSP Bidder Adapter +Maintainer: info@pgammedia.com +``` + +# Description + +Connects to PGAMSSP exchange for bids. +PGAMSSP bid adapter supports Banner, Video (instream and outstream) and Native. + +# Test Parameters +``` + var adUnits = [ + // Will return static test banner + { + code: 'adunit1', + mediaTypes: { + banner: { + sizes: [ [300, 250], [320, 50] ], + } + }, + bids: [ + { + bidder: 'pgamssp', + params: { + placementId: 'testBanner', + } + } + ] + }, + { + code: 'addunit2', + mediaTypes: { + video: { + playerSize: [ [640, 480] ], + context: 'instream', + minduration: 5, + maxduration: 60, + } + }, + bids: [ + { + bidder: 'pgamssp', + params: { + placementId: 'testVideo', + } + } + ] + }, + { + code: 'addunit3', + mediaTypes: { + native: { + title: { + required: true + }, + body: { + required: true + }, + icon: { + required: true, + size: [64, 64] + } + } + }, + bids: [ + { + bidder: 'pgamssp', + params: { + placementId: 'testNative', + } + } + ] + } + ]; +``` \ No newline at end of file diff --git a/modules/pilotxBidAdapter.js b/modules/pilotxBidAdapter.js index 335c461e3d9..0fb39e19076 100644 --- a/modules/pilotxBidAdapter.js +++ b/modules/pilotxBidAdapter.js @@ -1,4 +1,12 @@ import { registerBidder } from '../src/adapters/bidderFactory.js'; + +/** + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid + * @typedef {import('../src/adapters/bidderFactory.js').ServerResponse} ServerResponse + * @typedef {import('../src/adapters/bidderFactory.js').validBidRequests} validBidRequests + */ + const BIDDER_CODE = 'pilotx'; const ENDPOINT_URL = '//adn.pilotx.tv/hb' export const spec = { @@ -6,11 +14,11 @@ export const spec = { supportedMediaTypes: ['banner', 'video'], aliases: ['pilotx'], // short code /** - * Determines whether or not the given bid request is valid. - * - * @param {BidRequest} bid The bid params to validate. - * @return boolean True if this is a valid bid, and false otherwise. - */ + * 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 sizesCheck = !!bid.sizes let paramSizesCheck = !!bid.params.sizes @@ -35,11 +43,11 @@ export const spec = { return !!(bid.params.placementId); }, /** - * Make a server request from the list of BidRequests. - * - * @param {validBidRequests[]} - an array of bids - * @return ServerRequest Info describing the request to the server. - */ + * 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) { let payloadItems = {}; validBidRequests.forEach(bidRequest => { @@ -84,11 +92,11 @@ export const spec = { }; }, /** - * 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. - */ + * 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 serverBody = serverResponse.body; const bidResponses = []; diff --git a/modules/pixfutureBidAdapter.js b/modules/pixfutureBidAdapter.js index 608ba20aa5f..1c3f9b8da1a 100644 --- a/modules/pixfutureBidAdapter.js +++ b/modules/pixfutureBidAdapter.js @@ -3,10 +3,11 @@ import {getStorageManager} from '../src/storageManager.js'; import {BANNER} from '../src/mediaTypes.js'; import {config} from '../src/config.js'; import {find, includes} from '../src/polyfill.js'; -import {convertCamelToUnderscore, deepAccess, isArray, isFn, isNumber, isPlainObject} from '../src/utils.js'; +import {deepAccess, isArray, isFn, isNumber, isPlainObject} from '../src/utils.js'; import {auctionManager} from '../src/auctionManager.js'; import {getGlobal} from '../src/prebidGlobal.js'; -import {getANKeywordParam} from '../libraries/appnexusKeywords/anKeywords.js'; +import {getANKeywordParam} from '../libraries/appnexusUtils/anKeywords.js'; +import {convertCamelToUnderscore} from '../libraries/appnexusUtils/anUtils.js'; const SOURCE = 'pbjs'; const storageManager = getStorageManager({bidderCode: 'pixfuture'}); diff --git a/modules/prebidServerBidAdapter/config.js b/modules/prebidServerBidAdapter/config.js index f6b8ac9f86a..87274504f64 100644 --- a/modules/prebidServerBidAdapter/config.js +++ b/modules/prebidServerBidAdapter/config.js @@ -1,11 +1,11 @@ // accountId and bidders params are not included here, should be configured by end-user export const S2S_VENDORS = { - 'appnexus': { + 'appnexuspsp': { adapter: 'prebidServer', enabled: true, endpoint: { - p1Consent: 'https://prebid.adnxs.com/pbs/v1/openrtb2/auction', - noP1Consent: 'https://prebid.adnxs-simple.com/pbs/v1/openrtb2/auction' + p1Consent: 'https://ib.adnxs.com/openrtb2/prebid', + noP1Consent: 'https://ib.adnxs-simple.com/openrtb2/prebid' }, syncEndpoint: { p1Consent: 'https://prebid.adnxs.com/pbs/v1/cookie_sync', @@ -13,15 +13,6 @@ export const S2S_VENDORS = { }, timeout: 1000 }, - 'appnexuspsp': { - adapter: 'prebidServer', - enabled: true, - endpoint: { - p1Consent: 'https://ib.adnxs.com/openrtb2/prebid', - noP1Consent: 'https://ib.adnxs-simple.com/openrtb2/prebid' - }, - timeout: 1000 - }, 'rubicon': { adapter: 'prebidServer', enabled: true, @@ -47,5 +38,14 @@ export const S2S_VENDORS = { noP1Consent: 'https://prebid.openx.net/cookie_sync' }, timeout: 1000 + }, + 'openwrap': { + adapter: 'prebidServer', + enabled: true, + endpoint: { + p1Consent: 'https://ow.pubmatic.com/openrtb2/auction?source=pbjs', + noP1Consent: 'https://ow.pubmatic.com/openrtb2/auction?source=pbjs' + }, + timeout: 500 } } diff --git a/modules/prebidServerBidAdapter/index.js b/modules/prebidServerBidAdapter/index.js index 3cc38923d57..6e4aec8ad92 100644 --- a/modules/prebidServerBidAdapter/index.js +++ b/modules/prebidServerBidAdapter/index.js @@ -1,6 +1,6 @@ import Adapter from '../../src/adapter.js'; import { - bind, + deepAccess, deepClone, flatten, generateUUID, @@ -15,10 +15,9 @@ import { logWarn, triggerPixel, uniques, - deepAccess, } from '../../src/utils.js'; import CONSTANTS from '../../src/constants.json'; -import adapterManager from '../../src/adapterManager.js'; +import adapterManager, {s2sActivityParams} from '../../src/adapterManager.js'; import {config} from '../../src/config.js'; import {addComponentAuction, isValid} from '../../src/adapters/bidderFactory.js'; import * as events from '../../src/events.js'; @@ -29,6 +28,8 @@ import {hook} from '../../src/hook.js'; import {hasPurpose1Consent} from '../../src/utils/gpdr.js'; import {buildPBSRequest, interpretPBSResponse} from './ortbConverter.js'; import {useMetrics} from '../../src/utils/perfMetrics.js'; +import {isActivityAllowed} from '../../src/activities/rules.js'; +import {ACTIVITY_TRANSMIT_UFPD} from '../../src/activities/activities.js'; const getConfig = config.getConfig; @@ -206,7 +207,7 @@ getConfig('s2sConfig', ({s2sConfig}) => setS2sConfig(s2sConfig)); /** * resets the _synced variable back to false, primiarily used for testing purposes -*/ + */ export function resetSyncedStatus() { _syncCount = 0; } @@ -295,7 +296,7 @@ function doAllSyncs(bidders, s2sConfig) { // if PBS reports this bidder doesn't have an ID, then call the sync and recurse to the next sync entry if (thisSync.no_cookie) { - doPreBidderSync(thisSync.usersync.type, thisSync.usersync.url, thisSync.bidder, bind.call(doAllSyncs, null, bidders, s2sConfig), s2sConfig); + doPreBidderSync(thisSync.usersync.type, thisSync.usersync.url, thisSync.bidder, doAllSyncs.bind(null, bidders, s2sConfig), s2sConfig); } else { // bidder already has an ID, so just recurse to the next sync entry doAllSyncs(bidders, s2sConfig); @@ -354,8 +355,7 @@ function doClientSideSyncs(bidders, gdprConsent, uspConsent, gppConsent) { if (clientAdapter && clientAdapter.registerSyncs) { config.runWithBidder( bidder, - bind.call( - clientAdapter.registerSyncs, + clientAdapter.registerSyncs.bind( clientAdapter, [], gdprConsent, @@ -478,10 +478,14 @@ export function PrebidServer() { adapterMetrics }); } - done(); + done(false); doClientSideSyncs(requestedBidders, gdprConsent, uspConsent, gppConsent); }, - onError: done, + onError(msg, error) { + logError(`Prebid server call failed: '${msg}'`, error); + bidRequests.forEach(bidderRequest => events.emit(CONSTANTS.EVENTS.BIDDER_ERROR, {error, bidderRequest})); + done(error.timedOut); + }, onBid: function ({adUnit, bid}) { const metrics = bid.metrics = s2sBidRequest.metrics.fork().renameWith(); metrics.checkpoint('addBidResponse'); @@ -499,8 +503,8 @@ export function PrebidServer() { } } }, - onFledge: ({adUnitCode, config}) => { - addComponentAuction(adUnitCode, config); + onFledge: (params) => { + addComponentAuction({auctionId: bidRequests[0].auctionId, ...params}, params.config); } }) } @@ -571,7 +575,11 @@ export const processPBSRequest = hook('sync', function (s2sBidRequest, bidReques } }, requestJson, - {contentType: 'text/plain', withCredentials: true} + { + contentType: 'text/plain', + withCredentials: true, + browsingTopics: isActivityAllowed(ACTIVITY_TRANSMIT_UFPD, s2sActivityParams(s2sBidRequest.s2sConfig)) + } ); } else { logError('PBS request not made. Check endpoints.'); @@ -585,7 +593,7 @@ function shouldEmitNonbids(s2sConfig, response) { /** * Global setter that sets eids permissions for bidders * This setter is to be used by userId module when included - * @param {array} newEidPermissions + * @param {Array} newEidPermissions */ function setEidPermissions(newEidPermissions) { eidPermissions = newEidPermissions; diff --git a/modules/prebidServerBidAdapter/ortbConverter.js b/modules/prebidServerBidAdapter/ortbConverter.js index 9d29b66cfc8..7aeb4302280 100644 --- a/modules/prebidServerBidAdapter/ortbConverter.js +++ b/modules/prebidServerBidAdapter/ortbConverter.js @@ -17,13 +17,14 @@ import {pbsExtensions} from '../../libraries/pbsExtensions/pbsExtensions.js'; import {setImpBidParams} from '../../libraries/pbsExtensions/processors/params.js'; import {SUPPORTED_MEDIA_TYPES} from '../../libraries/pbsExtensions/processors/mediaType.js'; import {IMP, REQUEST, RESPONSE} from '../../src/pbjsORTB.js'; -import {beConvertCurrency} from '../../src/utils/currency.js'; import {redactor} from '../../src/activities/redactor.js'; import {s2sActivityParams} from '../../src/adapterManager.js'; import {activityParams} from '../../src/activities/activityParams.js'; import {MODULE_TYPE_BIDDER} from '../../src/activities/modules.js'; import {isActivityAllowed} from '../../src/activities/rules.js'; import {ACTIVITY_TRANSMIT_TID} from '../../src/activities/activities.js'; +import {currencyCompare} from '../../libraries/currencyUtils/currency.js'; +import {minimum} from '../../src/utils/reducers.js'; const DEFAULT_S2S_TTL = 60; const DEFAULT_S2S_CURRENCY = 'USD'; @@ -141,6 +142,7 @@ const PBS_CONVERTER = ortbConverter({ bidfloor(orig, imp, proxyBidRequest, context) { // for bid floors, we pass each bidRequest associated with this imp through normal bidfloor processing, // and aggregate all of them into a single, minimum floor to put in the request + const getMin = minimum(currencyCompare(floor => [floor.bidfloor, floor.bidfloorcur])); let min; for (const req of context.actualBidRequests.values()) { const floor = {}; @@ -149,14 +151,8 @@ const PBS_CONVERTER = ortbConverter({ if (floor.bidfloorcur == null || floor.bidfloor == null) { min = null; break; - } else if (min == null) { - min = floor; - } else { - const value = beConvertCurrency(floor.bidfloor, floor.bidfloorcur, min.bidfloorcur); - if (value != null && value < min.bidfloor) { - min = floor; - } } + min = min == null ? floor : getMin(min, floor); } if (min != null) { Object.assign(imp, min); @@ -168,6 +164,10 @@ const PBS_CONVERTER = ortbConverter({ // FPD is handled different for PBS - the base request will only contain global FPD; // bidder-specific values are set in ext.prebid.bidderconfig + if (context.transmitTids) { + deepSetValue(ortbRequest, 'source.tid', proxyBidderRequest.auctionId); + } + mergeDeep(ortbRequest, context.s2sBidRequest.ortb2Fragments?.global); // also merge in s2sConfig.extPrebid @@ -240,7 +240,16 @@ const PBS_CONVERTER = ortbConverter({ }, fledgeAuctionConfigs(orig, response, ortbResponse, context) { const configs = Object.values(context.impContext) - .flatMap((impCtx) => (impCtx.fledgeConfigs || []).map(cfg => ({adUnitCode: impCtx.adUnit.code, config: cfg.config}))); + .flatMap((impCtx) => (impCtx.fledgeConfigs || []).map(cfg => { + const bidderReq = impCtx.actualBidderRequests.find(br => br.bidderCode === cfg.bidder); + const bidReq = impCtx.actualBidRequests.get(cfg.bidder); + return { + adUnitCode: impCtx.adUnit.code, + ortb2: bidderReq?.ortb2, + ortb2Imp: bidReq?.ortb2Imp, + config: cfg.config + }; + })); if (configs.length > 0) { response.fledgeAuctionConfigs = configs; } diff --git a/modules/precisoBidAdapter.js b/modules/precisoBidAdapter.js new file mode 100644 index 00000000000..9125f6f3911 --- /dev/null +++ b/modules/precisoBidAdapter.js @@ -0,0 +1,212 @@ +import { logMessage, isFn, deepAccess, logInfo } from '../src/utils.js'; +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { BANNER, NATIVE, VIDEO } from '../src/mediaTypes.js'; +import { config } from '../src/config.js'; +import { convertOrtbRequestToProprietaryNative } from '../src/native.js'; + +const BIDDER_CODE = 'preciso'; +const AD_URL = 'https://ssp-bidder.mndtrk.com/bid_request/openrtb'; +const URL_SYNC = 'https://ck.2trk.info/rtb/user/usersync.aspx?'; +const SUPPORTED_MEDIA_TYPES = [BANNER, NATIVE, VIDEO]; +const GVLID = 874; +let userId = 'NA'; + +export const spec = { + code: BIDDER_CODE, + supportedMediaTypes: SUPPORTED_MEDIA_TYPES, + gvlid: GVLID, + + isBidRequestValid: (bid) => { + return Boolean(bid.bidId && bid.params && !isNaN(bid.params.publisherId) && bid.params.host == 'prebid'); + }, + + buildRequests: (validBidRequests = [], bidderRequest) => { + // convert Native ORTB definition to old-style prebid native definition + validBidRequests = convertOrtbRequestToProprietaryNative(validBidRequests); + // userId = validBidRequests[0].userId.pubcid; + let winTop = window; + let location; + var offset = new Date().getTimezoneOffset(); + logInfo('timezone ' + offset); + var city = Intl.DateTimeFormat().resolvedOptions().timeZone; + logInfo('location test' + city) + + const countryCode = getCountryCodeByTimezone(city); + logInfo(`The country code for ${city} is ${countryCode}`); + + // TODO: this odd try-catch block was copied in several adapters; it doesn't seem to be correct for cross-origin + try { + location = new URL(bidderRequest.refererInfo.page) + winTop = window.top; + } catch (e) { + location = winTop.location; + logMessage(e); + }; + + let request = { + id: validBidRequests[0].bidderRequestId, + + imp: validBidRequests.map(request => { + const { bidId, sizes, mediaType, ortb2 } = request + const item = { + id: bidId, + region: request.params.region, + traffic: mediaType, + bidFloor: getBidFloor(request), + ortb2: ortb2 + + } + + if (request.mediaTypes.banner) { + item.banner = { + format: (request.mediaTypes.banner.sizes || sizes).map(size => { + return { w: size[0], h: size[1] } + }), + } + } + + if (request.schain) { + item.schain = request.schain; + } + + if (request.floorData) { + item.bidFloor = request.floorData.floorMin; + } + return item + }), + auctionId: validBidRequests[0].auctionId, + 'deviceWidth': winTop.screen.width, + 'deviceHeight': winTop.screen.height, + 'language': (navigator && navigator.language) ? navigator.language : '', + geo: navigator.geolocation.getCurrentPosition(position => { + const { latitude, longitude } = position.coords; + return { + latitude: latitude, + longitude: longitude + } + // Show a map centered at latitude / longitude. + }) || { utcoffset: new Date().getTimezoneOffset() }, + city: city, + 'host': location.host, + 'page': location.pathname, + 'coppa': config.getConfig('coppa') === true ? 1 : 0 + // userId: validBidRequests[0].userId + }; + + request.language.indexOf('-') != -1 && (request.language = request.language.split('-')[0]) + if (bidderRequest) { + if (bidderRequest.uspConsent) { + request.ccpa = bidderRequest.uspConsent; + } + if (bidderRequest.gdprConsent) { + request.gdpr = bidderRequest.gdprConsent + } + if (bidderRequest.gppConsent) { + request.gpp = bidderRequest.gppConsent; + } + } + + return { + method: 'POST', + url: AD_URL, + data: request, + + }; + }, + + interpretResponse: function (serverResponse) { + const response = serverResponse.body + + const bids = [] + + response.seatbid.forEach(seat => { + seat.bid.forEach(bid => { + bids.push({ + requestId: bid.impid, + cpm: bid.price, + width: bid.w, + height: bid.h, + creativeId: bid.crid, + ad: bid.adm, + currency: 'USD', + netRevenue: true, + ttl: 300, + meta: { + advertiserDomains: bid.adomain || [], + }, + }) + }) + }) + + return bids + }, + + getUserSyncs: (syncOptions, serverResponses = [], gdprConsent = {}, uspConsent = '', gppConsent = '') => { + let syncs = []; + let { gdprApplies, consentString = '' } = gdprConsent; + + if (serverResponses.length > 0) { + logInfo('preciso bidadapter getusersync serverResponses:' + serverResponses.toString); + } + if (syncOptions.iframeEnabled) { + syncs.push({ + type: 'iframe', + url: `${URL_SYNC}id=${userId}&gdpr=${gdprApplies ? 1 : 0}&gdpr_consent=${consentString}&us_privacy=${uspConsent}&t=4` + }); + } else { + syncs.push({ + type: 'image', + url: `${URL_SYNC}&gdpr=${gdprApplies ? 1 : 0}&gdpr_consent=${consentString}&us_privacy=${uspConsent}&t=2` + }); + } + + return syncs + } + +}; + +function getCountryCodeByTimezone(city) { + try { + const now = new Date(); + const options = { + timeZone: city, + timeZoneName: 'long', + }; + const [timeZoneName] = new Intl.DateTimeFormat('en-US', options) + .formatToParts(now) + .filter((part) => part.type === 'timeZoneName'); + + if (timeZoneName) { + // Extract the country code from the timezone name + const parts = timeZoneName.value.split('-'); + if (parts.length >= 2) { + return parts[1]; + } + } + } catch (error) { + // Handle errors, such as an invalid timezone city + logInfo(error); + } + + // Handle the case where the city is not found or an error occurred + return 'Unknown'; +} + +function getBidFloor(bid) { + if (!isFn(bid.getFloor)) { + return deepAccess(bid, 'params.bidFloor', 0); + } + + try { + const bidFloor = bid.getFloor({ + currency: 'USD', + mediaType: '*', + size: '*', + }); + return bidFloor.floor; + } catch (_) { + return 0 + } +} + +registerBidder(spec); diff --git a/modules/precisoBidAdapter.md b/modules/precisoBidAdapter.md new file mode 100644 index 00000000000..b1fb0d062da --- /dev/null +++ b/modules/precisoBidAdapter.md @@ -0,0 +1,84 @@ +# Overview + +``` +Module Name: Preciso Bidder Adapter +Module Type: Bidder Adapter +Maintainer: tech@preciso.net +``` + +# Description + +Module that connects to preciso' demand sources + +# Parameters + +| Name | Scope | Description | Example | +| :------------ | :------- | :------------------------ | :------------------- | +| `region` | required (for prebid.js) | region | "prebid-eu" | +| `publisherId` | required (for prebid-server) | partner ID | "1901" | +| `traffic` | optional (for prebid.js) | Configures the mediaType that should be used. Values can be banner, native or video | "banner" | + +# Test Parameters +``` + var adUnits = [ + // Will return static native ad. Assets are stored through user UI for each placement separetly + { + code: 'placementId_0', + mediaTypes: { + native: {} + }, + bids: [ + { + bidder: 'preciso', + params: { + host: 'prebid', + publisherId: '0', + region: 'prebid-eu', + traffic: 'native' + } + } + ] + }, + // Will return static test banner + { + code: 'placementId_0', + mediaTypes: { + banner: { + sizes: [[300, 250]], + } + }, + bids: [ + { + bidder: 'preciso', + params: { + host: 'prebid', + publisherId: '0', + region: 'prebid-eu', + traffic: 'banner' + } + } + ] + }, + // Will return test vast xml. All video params are stored under placement in publishers UI + { + code: 'placementId_0', + mediaTypes: { + video: { + playerSize: [640, 480], + context: 'instream' + } + }, + bids: [ + { + bidder: 'preciso', + params: { + host: 'prebid', + publisherId: '0', + region: 'prebid-eu', + traffic: 'video' + } + } + ] + } + ]; +``` \ No newline at end of file diff --git a/modules/priceFloors.js b/modules/priceFloors.js index 37167fff691..9c3869c480a 100644 --- a/modules/priceFloors.js +++ b/modules/priceFloors.js @@ -4,7 +4,6 @@ import { deepClone, deepSetValue, generateUUID, - getGptSlotInfoForAdUnitCode, getParameterByName, isNumber, logError, @@ -13,7 +12,8 @@ import { mergeDeep, parseGPTSingleSizeArray, parseUrl, - pick + pick, + deepEqual } from '../src/utils.js'; import {getGlobal} from '../src/prebidGlobal.js'; import {config} from '../src/config.js'; @@ -27,8 +27,9 @@ import {bidderSettings} from '../src/bidderSettings.js'; import {auctionManager} from '../src/auctionManager.js'; import {IMP, PBS, registerOrtbProcessor, REQUEST} from '../src/pbjsORTB.js'; import {timedAuctionHook, timedBidResponseHook} from '../src/utils/perfMetrics.js'; -import {beConvertCurrency} from '../src/utils/currency.js'; import {adjustCpm} from '../src/utils/cpm.js'; +import {getGptSlotInfoForAdUnitCode} from '../libraries/gptUtils/gptUtils.js'; +import {convertCurrency} from '../libraries/currencyUtils/currency.js'; /** * @summary This Module is intended to provide users with the ability to dynamically set and enforce price floors on a per auction basis. @@ -40,19 +41,22 @@ const MODULE_NAME = 'Price Floors'; */ const ajax = ajaxBuilder(10000); +// eslint-disable-next-line symbol-description +const SYN_FIELD = Symbol(); + /** * @summary Allowed fields for rules to have */ -export let allowedFields = ['gptSlot', 'adUnitCode', 'size', 'domain', 'mediaType']; +export let allowedFields = [SYN_FIELD, 'gptSlot', 'adUnitCode', 'size', 'domain', 'mediaType']; /** * @summary This is a flag to indicate if a AJAX call is processing for a floors request -*/ + */ let fetching = false; /** * @summary so we only register for our hooks once -*/ + */ let addedFloorsHook = false; /** @@ -104,6 +108,7 @@ function getAdUnitCode(request, response, {index = auctionManager.index} = {}) { * @summary floor field types with their matching functions to resolve the actual matched value */ export let fieldMatchingFunctions = { + [SYN_FIELD]: () => '*', 'size': (bidRequest, bidResponse) => parseGPTSingleSizeArray(bidResponse.size) || '*', 'mediaType': (bidRequest, bidResponse) => bidResponse.mediaType || 'banner', 'gptSlot': (bidRequest, bidResponse) => getGptSlotFromAdUnit((bidRequest || bidResponse).transactionId) || getGptSlotInfoForAdUnitCode(getAdUnitCode(bidRequest, bidResponse)).gptSlot, @@ -117,6 +122,7 @@ export let fieldMatchingFunctions = { * Returns array of Tuple [exact match, catch all] for each field in rules file */ function enumeratePossibleFieldValues(floorFields, bidObject, responseObject) { + if (!floorFields.length) return []; // generate combination of all exact matches and catch all for each field type return floorFields.reduce((accum, field) => { let exactMatch = fieldMatchingFunctions[field](bidObject, responseObject) || '*'; @@ -132,7 +138,9 @@ function enumeratePossibleFieldValues(floorFields, bidObject, responseObject) { */ export function getFirstMatchingFloor(floorData, bidObject, responseObject = {}) { let fieldValues = enumeratePossibleFieldValues(deepAccess(floorData, 'schema.fields') || [], bidObject, responseObject); - if (!fieldValues.length) return { matchingFloor: floorData.default }; + if (!fieldValues.length) { + return {matchingFloor: undefined} + } // look to see if a request for this context was made already let matchingInput = fieldValues.map(field => field[0]).join('-'); @@ -146,9 +154,9 @@ export function getFirstMatchingFloor(floorData, bidObject, responseObject = {}) let matchingData = { floorMin: floorData.floorMin || 0, - floorRuleValue: isNaN(floorData.values[matchingRule]) ? floorData.default : floorData.values[matchingRule], + floorRuleValue: floorData.values[matchingRule], matchingData: allPossibleMatches[0], // the first possible match is an "exact" so contains all data relevant for anlaytics adapters - matchingRule + matchingRule: matchingRule === floorData.meta?.defaultRule ? undefined : matchingRule }; // use adUnit floorMin as priority! const floorMin = deepAccess(bidObject, 'ortb2Imp.ext.prebid.floors.floorMin'); @@ -300,14 +308,20 @@ function normalizeRulesForAuction(floorData, adUnitCode) { * Only called if no set config or fetch level data has returned */ export function getFloorDataFromAdUnits(adUnits) { + const schemaAu = adUnits.find(au => au.floors?.schema != null); return adUnits.reduce((accum, adUnit) => { - if (isFloorsDataValid(adUnit.floors)) { + if (adUnit.floors?.schema != null && !deepEqual(adUnit.floors.schema, schemaAu?.floors?.schema)) { + logError(`${MODULE_NAME}: adUnit '${adUnit.code}' declares a different schema from one previously declared by adUnit '${schemaAu.code}'. Floor config for '${adUnit.code}' will be ignored.`) + return accum; + } + const floors = Object.assign({}, schemaAu?.floors, {values: undefined}, adUnit.floors) + if (isFloorsDataValid(floors)) { // if values already exist we want to not overwrite them if (!accum.values) { - accum = getFloorsDataForAuction(adUnit.floors, adUnit.code); + accum = getFloorsDataForAuction(floors, adUnit.code); accum.location = 'adUnit'; } else { - let newRules = getFloorsDataForAuction(adUnit.floors, adUnit.code).values; + let newRules = getFloorsDataForAuction(floors, adUnit.code).values; // copy over the new rules into our values object Object.assign(accum.values, newRules); } @@ -318,13 +332,29 @@ export function getFloorDataFromAdUnits(adUnits) { }, {}); } +function getNoFloorSignalBidersArray(floorData) { + const { data, enforcement } = floorData + // The data.noFloorSignalBidders higher priority then the enforcment + if (data?.noFloorSignalBidders?.length > 0) { + return data.noFloorSignalBidders + } else if (enforcement?.noFloorSignalBidders?.length > 0) { + return enforcement.noFloorSignalBidders + } + return [] +} + /** * @summary This function takes the adUnits for the auction and update them accordingly as well as returns the rules hashmap for the auction */ export function updateAdUnitsForAuction(adUnits, floorData, auctionId) { + const noFloorSignalBiddersArray = getNoFloorSignalBidersArray(floorData) + adUnits.forEach((adUnit) => { adUnit.bids.forEach(bid => { - if (floorData.skipped) { + // check if the bidder is in the no signal list + const isNoFloorSignaled = noFloorSignalBiddersArray.some(bidderName => bidderName === bid.bidder) + if (floorData.skipped || isNoFloorSignaled) { + isNoFloorSignaled && logInfo(`noFloorSignal to ${bid.bidder}`) delete bid.getFloor; } else { bid.getFloor = getFloor; @@ -332,8 +362,9 @@ export function updateAdUnitsForAuction(adUnits, floorData, auctionId) { // information for bid and analytics adapters bid.auctionId = auctionId; bid.floorData = { + noFloorSignaled: isNoFloorSignaled, skipped: floorData.skipped, - skipRate: floorData.skipRate, + skipRate: deepAccess(floorData, 'data.skipRate') ?? floorData.skipRate, floorMin: floorData.floorMin, modelVersion: deepAccess(floorData, 'data.modelVersion'), modelWeight: deepAccess(floorData, 'data.modelWeight'), @@ -382,7 +413,7 @@ export function createFloorsDataForAuction(adUnits, auctionId) { resolvedFloorsData.skipped = true; } else { // determine the skip rate now - const auctionSkipRate = getParameterByName('pbjs_skipRate') || resolvedFloorsData.skipRate; + const auctionSkipRate = getParameterByName('pbjs_skipRate') || (deepAccess(resolvedFloorsData, 'data.skipRate') ?? resolvedFloorsData.skipRate); const isSkipped = Math.random() * 100 < parseFloat(auctionSkipRate); resolvedFloorsData.skipped = isSkipped; } @@ -417,7 +448,7 @@ function validateSchemaFields(fields) { if (Array.isArray(fields) && fields.length > 0 && fields.every(field => allowedFields.indexOf(field) !== -1)) { return true; } - logError(`${MODULE_NAME}: Fields recieved do not match allowed fields`); + logError(`${MODULE_NAME}: Fields received do not match allowed fields`); return false; } @@ -443,7 +474,26 @@ function validateRules(floorsData, numFields, delimiter) { return Object.keys(floorsData.values).length > 0; } +export function normalizeDefault(model) { + if (isNumber(model.default)) { + let defaultRule = '*'; + const numFields = (model.schema?.fields || []).length; + if (!numFields) { + deepSetValue(model, 'schema.fields', [SYN_FIELD]); + } else { + defaultRule = Array(numFields).fill('*').join(model.schema?.delimiter || '|'); + } + model.values = model.values || {}; + if (model.values[defaultRule] == null) { + model.values[defaultRule] = model.default; + model.meta = {defaultRule}; + } + } + return model; +} + function modelIsValid(model) { + model = normalizeDefault(model); // schema.fields has only allowed attributes if (!validateSchemaFields(deepAccess(model, 'schema.fields'))) { return false; @@ -583,7 +633,7 @@ function handleFetchError(status) { } /** - * This function handles sending and recieving the AJAX call for a floors fetch + * This function handles sending and receiving the AJAX call for a floors fetch * @param {object} floorsConfig the floors config coming from setConfig */ export function generateAndHandleFetch(floorEndpoint) { @@ -630,7 +680,8 @@ export function handleSetFloorsConfig(config) { 'enforceJS', enforceJS => enforceJS !== false, // defaults to true 'enforcePBS', enforcePBS => enforcePBS === true, // defaults to false 'floorDeals', floorDeals => floorDeals === true, // defaults to false - 'bidAdjustment', bidAdjustment => bidAdjustment !== false, // defaults to true + 'bidAdjustment', bidAdjustment => bidAdjustment !== false, // defaults to true, + 'noFloorSignalBidders', noFloorSignalBidders => noFloorSignalBidders || [] ]), 'additionalSchemaFields', additionalSchemaFields => typeof additionalSchemaFields === 'object' && Object.keys(additionalSchemaFields).length > 0 ? addFieldOverrides(additionalSchemaFields) : undefined, 'data', data => (data && parseFloorData(data, 'setConfig')) || undefined @@ -715,7 +766,7 @@ export const addBidResponseHook = timedBidResponseHook('priceFloors', function a let floorInfo = getFirstMatchingFloor(floorData.data, matchingBidRequest, {...bid, size: [bid.width, bid.height]}); if (!floorInfo.matchingFloor) { - logWarn(`${MODULE_NAME}: unable to determine a matching price floor for bidResponse`, bid); + if (floorInfo.matchingFloor !== 0) logWarn(`${MODULE_NAME}: unable to determine a matching price floor for bidResponse`, bid); return fn.call(this, adUnitCode, bid, reject); } @@ -794,8 +845,8 @@ export function setImpExtPrebidFloors(imp, bidRequest, context) { if (floorMinCur == null) { floorMinCur = imp.bidfloorcur } const ortb2ImpFloorCur = imp.ext?.prebid?.floors?.floorMinCur || imp.ext?.prebid?.floorMinCur || floorMinCur; const ortb2ImpFloorMin = imp.ext?.prebid?.floors?.floorMin || imp.ext?.prebid?.floorMin; - const convertedFloorMinValue = beConvertCurrency(imp.bidfloor, imp.bidfloorcur, floorMinCur); - const convertedOrtb2ImpFloorMinValue = ortb2ImpFloorMin && ortb2ImpFloorCur ? beConvertCurrency(ortb2ImpFloorMin, ortb2ImpFloorCur, floorMinCur) : false; + const convertedFloorMinValue = convertCurrency(imp.bidfloor, imp.bidfloorcur, floorMinCur); + const convertedOrtb2ImpFloorMinValue = ortb2ImpFloorMin && ortb2ImpFloorCur ? convertCurrency(ortb2ImpFloorMin, ortb2ImpFloorCur, floorMinCur) : false; const lowestImpFloorMin = convertedOrtb2ImpFloorMinValue && convertedOrtb2ImpFloorMinValue < convertedFloorMinValue ? convertedOrtb2ImpFloorMinValue diff --git a/modules/prismaBidAdapter.js b/modules/prismaBidAdapter.js index 7c9108f60b1..4b4b5677cb3 100644 --- a/modules/prismaBidAdapter.js +++ b/modules/prismaBidAdapter.js @@ -2,7 +2,16 @@ import {ajax} from '../src/ajax.js'; import {config} from '../src/config.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import {BANNER, VIDEO} from '../src/mediaTypes.js'; -import {getANKeywordParam} from '../libraries/appnexusKeywords/anKeywords.js'; +import {getANKeywordParam} from '../libraries/appnexusUtils/anKeywords.js'; + +/** + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid + * @typedef {import('../src/adapters/bidderFactory.js').ServerResponse} ServerResponse + * @typedef {import('../src/adapters/bidderFactory.js').SyncOptions} SyncOptions + * @typedef {import('../src/adapters/bidderFactory.js').UserSync} UserSync + * @typedef {import('../src/adapters/bidderFactory.js').validBidRequests} validBidRequests + */ const BIDDER_CODE = 'prisma'; const BIDDER_URL = 'https://prisma.nexx360.io/prebid'; @@ -44,20 +53,20 @@ export const spec = { aliases: ['prismadirect'], // short 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. - */ + * 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.account && bid.params.tagId); }, /** - * Make a server request from the list of BidRequests. - * - * @param {validBidRequests[]} - an array of bids - * @return ServerRequest Info describing the request to the server. - */ + * 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) { const adUnits = []; const test = config.getConfig('debug') ? 1 : 0; @@ -109,11 +118,11 @@ export const spec = { }; }, /** - * 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. - */ + * 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 serverBody = serverResponse.body; const bidResponses = []; @@ -163,12 +172,12 @@ export const spec = { }, /** - * 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. + */ getUserSyncs: function(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') { @@ -179,9 +188,9 @@ export const spec = { }, /** - * Register bidder specific code, which will execute if a bid from this bidder won the auction - * @param {Bid} The bid that won the auction - */ + * Register bidder specific code, which will execute if a bid from this bidder won the auction + * @param {Bid} The bid that won the auction + */ onBidWon: function(bid) { // fires a pixel to confirm a winning bid const params = { type: 'prebid', mediatype: 'banner' }; diff --git a/modules/programmaticaBidAdapter.js b/modules/programmaticaBidAdapter.js new file mode 100644 index 00000000000..7d52e305189 --- /dev/null +++ b/modules/programmaticaBidAdapter.js @@ -0,0 +1,153 @@ +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { BANNER, VIDEO } from '../src/mediaTypes.js'; +import { hasPurpose1Consent } from '../src/utils/gpdr.js'; +import { deepAccess, parseSizesInput, isArray } from '../src/utils.js'; + +const BIDDER_CODE = 'programmatica'; +const DEFAULT_ENDPOINT = 'asr.programmatica.com'; +const SYNC_ENDPOINT = 'sync.programmatica.com'; +const ADOMAIN = 'programmatica.com'; +const TIME_TO_LIVE = 360; + +export const spec = { + code: BIDDER_CODE, + + isBidRequestValid: function(bid) { + let valid = bid.params.siteId && bid.params.placementId; + + return !!valid; + }, + + buildRequests: function(validBidRequests, bidderRequest) { + let requests = []; + for (const bid of validBidRequests) { + let endpoint = bid.params.endpoint || DEFAULT_ENDPOINT; + + requests.push({ + method: 'GET', + url: `https://${endpoint}/get`, + data: { + site_id: bid.params.siteId, + placement_id: bid.params.placementId, + prebid: true, + }, + bidRequest: bid, + }); + } + + return requests; + }, + + interpretResponse: function(serverResponse, request) { + if (!serverResponse?.body?.content?.data) { + return []; + } + + const bidResponses = []; + const body = serverResponse.body; + + let mediaType = BANNER; + let ad, vastXml; + let width; + let height; + + let sizes = getSize(body.size); + if (isArray(sizes)) { + [width, height] = sizes; + } + + if (body.type.format != '') { + // banner + ad = body.content.data; + if (body.content.imps?.length) { + for (const imp of body.content.imps) { + ad += ``; + } + } + } else { + // video + vastXml = body.content.data; + mediaType = VIDEO; + + if (!width || !height) { + const pSize = deepAccess(request.bidRequest, 'mediaTypes.video.playerSize'); + const reqSize = getSize(pSize); + if (isArray(reqSize)) { + [width, height] = reqSize; + } + } + } + + const bidResponse = { + requestId: request.bidRequest.bidId, + cpm: body.cpm, + currency: body.currency || 'USD', + width: parseInt(width), + height: parseInt(height), + creativeId: body.id, + netRevenue: true, + ttl: TIME_TO_LIVE, + ad: ad, + mediaType: mediaType, + vastXml: vastXml, + meta: { + advertiserDomains: [ADOMAIN], + } + }; + + if ((mediaType === VIDEO && request.bidRequest.mediaTypes?.video) || (mediaType === BANNER && request.bidRequest.mediaTypes?.banner)) { + bidResponses.push(bidResponse); + } + + return bidResponses; + }, + + getUserSyncs: function(syncOptions, serverResponses, gdprConsent, uspConsent) { + const syncs = [] + + if (!hasPurpose1Consent(gdprConsent)) { + return syncs; + } + + let params = `usp=${uspConsent ?? ''}&consent=${gdprConsent?.consentString ?? ''}`; + if (typeof gdprConsent?.gdprApplies === 'boolean') { + params += `&gdpr=${Number(gdprConsent.gdprApplies)}`; + } + + if (syncOptions.iframeEnabled) { + syncs.push({ + type: 'iframe', + url: `//${SYNC_ENDPOINT}/match/sp.ifr?${params}` + }); + } + + if (syncOptions.pixelEnabled) { + syncs.push({ + type: 'image', + url: `//${SYNC_ENDPOINT}/match/sp?${params}` + }); + } + + return syncs; + }, + + onTimeout: function(timeoutData) {}, + onBidWon: function(bid) {}, + onSetTargeting: function(bid) {}, + onBidderError: function() {}, + supportedMediaTypes: [ BANNER, VIDEO ] +} + +registerBidder(spec); + +function getSize(paramSizes) { + const parsedSizes = parseSizesInput(paramSizes); + const sizes = parsedSizes.map(size => { + const [width, height] = size.split('x'); + const w = parseInt(width, 10); + const h = parseInt(height, 10); + return [w, h]; + }); + + return sizes[0] || null; +} diff --git a/modules/programmaticaBidAdapter.md b/modules/programmaticaBidAdapter.md new file mode 100644 index 00000000000..5982edf143e --- /dev/null +++ b/modules/programmaticaBidAdapter.md @@ -0,0 +1,46 @@ +# Overview + +``` +Module Name: Programmatica Bid Adapter +Module Type: Bidder Adapter +Maintainer: tech@programmatica.com +``` + +# Description +Connects to Programmatica server for bids. +Module supports banner and video mediaType. + +# Test Parameters + +``` + var adUnits = [{ + code: '/test/div', + mediaTypes: { + banner: { + sizes: [[300, 250]] + } + }, + bids: [{ + bidder: 'programmatica', + params: { + siteId: 'cga9l34ipgja79esubrg', + placementId: 'cgim20sipgj0vj1cb510' + } + }] + }, + { + code: '/test/div', + mediaTypes: { + video: { + playerSize: [[640, 360]] + } + }, + bids: [{ + bidder: 'programmatica', + params: { + siteId: 'cga9l34ipgja79esubrg', + placementId: 'cioghpcipgj8r721e9ag' + } + }] + },]; +``` diff --git a/modules/pubCircleBidAdapter.js b/modules/pubCircleBidAdapter.js new file mode 100644 index 00000000000..54224fd0403 --- /dev/null +++ b/modules/pubCircleBidAdapter.js @@ -0,0 +1,231 @@ +import { isFn, deepAccess, logMessage, logError } from '../src/utils.js'; +import { convertOrtbRequestToProprietaryNative } from '../src/native.js'; + +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { BANNER, NATIVE, VIDEO } from '../src/mediaTypes.js'; +import { config } from '../src/config.js'; + +const BIDDER_CODE = 'pubcircle'; +const AD_URL = 'https://ml.pubcircle.ai/pbjs'; +const SYNC_URL = 'https://cs.pubcircle.ai'; + +function isBidResponseValid(bid) { + if (!bid.requestId || !bid.cpm || !bid.creativeId || !bid.ttl || !bid.currency) { + return false; + } + + switch (bid.mediaType) { + case BANNER: + return Boolean(bid.width && bid.height && bid.ad); + case VIDEO: + return Boolean(bid.vastUrl || bid.vastXml); + case NATIVE: + return Boolean(bid.native && bid.native.impressionTrackers && bid.native.impressionTrackers.length); + default: + return false; + } +} + +function getPlacementReqData(bid) { + const { params, bidId, mediaTypes } = bid; + const schain = bid.schain || {}; + const { placementId } = params; + const bidfloor = getBidFloor(bid); + + const placement = { + bidId, + schain, + bidfloor + }; + + placement.placementId = placementId; + placement.type = 'publisher'; + + if (bid.userId) { + getUserId(placement.eids, bid.userId.uid2 && bid.userId.uid2.id, 'uidapi.com'); + getUserId(placement.eids, bid.userId.lotamePanoramaId, 'lotame.com'); + getUserId(placement.eids, bid.userId.idx, 'idx.lat'); + getUserId(placement.eids, bid.userId.idl_env, 'liveramp.com'); + } + + if (mediaTypes && mediaTypes[BANNER]) { + placement.adFormat = BANNER; + placement.sizes = mediaTypes[BANNER].sizes; + } else if (mediaTypes && mediaTypes[VIDEO]) { + placement.adFormat = VIDEO; + placement.playerSize = mediaTypes[VIDEO].playerSize; + placement.minduration = mediaTypes[VIDEO].minduration; + placement.maxduration = mediaTypes[VIDEO].maxduration; + placement.mimes = mediaTypes[VIDEO].mimes; + placement.protocols = mediaTypes[VIDEO].protocols; + placement.startdelay = mediaTypes[VIDEO].startdelay; + placement.placement = mediaTypes[VIDEO].placement; + placement.skip = mediaTypes[VIDEO].skip; + placement.skipafter = mediaTypes[VIDEO].skipafter; + placement.minbitrate = mediaTypes[VIDEO].minbitrate; + placement.maxbitrate = mediaTypes[VIDEO].maxbitrate; + placement.delivery = mediaTypes[VIDEO].delivery; + placement.playbackmethod = mediaTypes[VIDEO].playbackmethod; + placement.api = mediaTypes[VIDEO].api; + placement.linearity = mediaTypes[VIDEO].linearity; + } else if (mediaTypes && mediaTypes[NATIVE]) { + placement.native = mediaTypes[NATIVE]; + placement.adFormat = NATIVE; + } + + return placement; +} + +function getBidFloor(bid) { + if (!isFn(bid.getFloor)) { + return deepAccess(bid, 'params.bidfloor', 0); + } + + try { + const bidFloor = bid.getFloor({ + currency: 'USD', + mediaType: '*', + size: '*', + }); + return bidFloor.floor; + } catch (err) { + logError(err); + return 0; + } +} + +function getUserId(eids, id, source, uidExt) { + if (id) { + var uid = { id }; + if (uidExt) { + uid.ext = uidExt; + } + eids.push({ + source, + uids: [ uid ] + }); + } +} + +export const spec = { + code: BIDDER_CODE, + supportedMediaTypes: [BANNER, VIDEO, NATIVE], + + isBidRequestValid: (bid = {}) => { + const { params, bidId, mediaTypes } = bid; + let valid = Boolean(bidId && params && params.placementId); + + if (mediaTypes && mediaTypes[BANNER]) { + valid = valid && Boolean(mediaTypes[BANNER] && mediaTypes[BANNER].sizes); + } else if (mediaTypes && mediaTypes[VIDEO]) { + valid = valid && Boolean(mediaTypes[VIDEO] && mediaTypes[VIDEO].playerSize); + } else if (mediaTypes && mediaTypes[NATIVE]) { + valid = valid && Boolean(mediaTypes[NATIVE]); + } else { + valid = false; + } + return valid; + }, + + buildRequests: (validBidRequests = [], bidderRequest = {}) => { + // convert Native ORTB definition to old-style prebid native definition + validBidRequests = convertOrtbRequestToProprietaryNative(validBidRequests); + + let deviceWidth = 0; + let deviceHeight = 0; + + let winLocation; + try { + const winTop = window.top; + deviceWidth = winTop.screen.width; + deviceHeight = winTop.screen.height; + winLocation = winTop.location; + } catch (e) { + logMessage(e); + winLocation = window.location; + } + + const refferUrl = bidderRequest.refererInfo && bidderRequest.refererInfo.page; + let refferLocation; + try { + refferLocation = refferUrl && new URL(refferUrl); + } catch (e) { + logMessage(e); + } + // TODO: does the fallback make sense here? + let location = refferLocation || winLocation; + const language = (navigator && navigator.language) ? navigator.language.split('-')[0] : ''; + const host = location.host; + const page = location.pathname; + const secure = location.protocol === 'https:' ? 1 : 0; + const placements = []; + const request = { + deviceWidth, + deviceHeight, + language, + secure, + host, + page, + placements, + coppa: config.getConfig('coppa') === true ? 1 : 0, + ccpa: bidderRequest.uspConsent || undefined, + gdpr: bidderRequest.gdprConsent || undefined, + tmax: bidderRequest.timeout + }; + + const len = validBidRequests.length; + for (let i = 0; i < len; i++) { + const bid = validBidRequests[i]; + placements.push(getPlacementReqData(bid)); + } + + return { + method: 'POST', + url: AD_URL, + data: request + }; + }, + + interpretResponse: (serverResponse) => { + let response = []; + for (let i = 0; i < serverResponse.body.length; i++) { + let resItem = serverResponse.body[i]; + if (isBidResponseValid(resItem)) { + const advertiserDomains = resItem.adomain && resItem.adomain.length ? resItem.adomain : []; + resItem.meta = { ...resItem.meta, advertiserDomains }; + + response.push(resItem); + } + } + return response; + }, + + getUserSyncs: (syncOptions, serverResponses, gdprConsent, uspConsent) => { + let syncType = syncOptions.iframeEnabled ? 'iframe' : 'image'; + let syncUrl = SYNC_URL + `/${syncType}?pbjs=1`; + if (gdprConsent && gdprConsent.consentString) { + if (typeof gdprConsent.gdprApplies === 'boolean') { + syncUrl += `&gdpr=${Number(gdprConsent.gdprApplies)}&gdpr_consent=${gdprConsent.consentString}`; + } else { + syncUrl += `&gdpr=0&gdpr_consent=${gdprConsent.consentString}`; + } + } + if (uspConsent && uspConsent.consentString) { + syncUrl += `&ccpa_consent=${uspConsent.consentString}`; + } + + const coppa = config.getConfig('coppa') ? 1 : 0; + syncUrl += `&coppa=${coppa}`; + + return [{ + type: syncType, + url: syncUrl + }]; + }, + + onBidViewable: function (bid) { + // to do : we need to implement js tag to fire pixel with viewability counter + } +}; + +registerBidder(spec); diff --git a/modules/pubCircleBidAdapter.md b/modules/pubCircleBidAdapter.md new file mode 100644 index 00000000000..4fc114bf20c --- /dev/null +++ b/modules/pubCircleBidAdapter.md @@ -0,0 +1,79 @@ +# Overview + +``` +Module Name: PubCirlce Bidder Adapter +Module Type: PubCirlce Bidder Adapter +Maintainer: system@smartyads.com +``` + +# Description + +Connects to PubCirlce exchange for bids. +PubCirlce bid adapter supports Banner, Video (instream and outstream) and Native. + +# Test Parameters +``` + var adUnits = [ + // Will return static test banner + { + code: 'addunit1', + mediaTypes: { + banner: { + sizes: [ [300, 250], [320, 50] ], + } + }, + bids: [ + { + bidder: 'pubcircle', + params: { + placementId: 'testBanner', + } + } + ] + }, + { + code: 'addunit2', + mediaTypes: { + video: { + playerSize: [ [640, 480] ], + context: 'instream', + minduration: 5, + maxduration: 60, + } + }, + bids: [ + { + bidder: 'pubcircle', + params: { + placementId: 'testVideo', + } + } + ] + }, + { + code: 'addunit3', + mediaTypes: { + native: { + title: { + required: true + }, + body: { + required: true + }, + icon: { + required: true, + size: [64, 64] + } + } + }, + bids: [ + { + bidder: 'pubcircle', + params: { + placementId: 'testNative', + } + } + ] + } + ]; +``` diff --git a/modules/pubProvidedIdSystem.js b/modules/pubProvidedIdSystem.js index baffd997443..d23d992e495 100644 --- a/modules/pubProvidedIdSystem.js +++ b/modules/pubProvidedIdSystem.js @@ -9,6 +9,11 @@ import {submodule} from '../src/hook.js'; import { logInfo, isArray } from '../src/utils.js'; import {VENDORLESS_GVLID} from '../src/consentHandler.js'; +/** + * @typedef {import('../modules/userId/index.js').Submodule} Submodule + * @typedef {import('../modules/userId/index.js').SubmoduleConfig} SubmoduleConfig + */ + const MODULE_NAME = 'pubProvidedId'; /** @type {Submodule} */ @@ -25,7 +30,7 @@ export const pubProvidedIdSubmodule = { * decode the stored id value for passing to bid request * @function * @param {string} value - * @returns {{pubProvidedId: array}} or undefined if value doesn't exists + * @returns {{pubProvidedId: Array}} or undefined if value doesn't exists */ decode(value) { const res = value ? {pubProvidedId: value} : undefined; @@ -37,7 +42,7 @@ export const pubProvidedIdSubmodule = { * performs action to obtain id and return a value. * @function * @param {SubmoduleConfig} [config] - * @returns {{id: array}} + * @returns {{id: Array}} */ getId(config) { const configParams = (config && config.params) || {}; diff --git a/modules/publinkIdSystem.js b/modules/publinkIdSystem.js index 6a5504b5ba0..e8eb90cd02a 100644 --- a/modules/publinkIdSystem.js +++ b/modules/publinkIdSystem.js @@ -12,10 +12,19 @@ import { parseUrl, buildUrl, logError } from '../src/utils.js'; import {uspDataHandler} from '../src/adapterManager.js'; import {MODULE_TYPE_UID} from '../src/activities/modules.js'; +/** + * @typedef {import('../modules/userId/index.js').Submodule} Submodule + * @typedef {import('../modules/userId/index.js').SubmoduleConfig} SubmoduleConfig + * @typedef {import('../modules/userId/index.js').ConsentData} ConsentData + * @typedef {import('../modules/userId/index.js').IdResponse} IdResponse + */ + const MODULE_NAME = 'publinkId'; const GVLID = 24; const PUBLINK_COOKIE = '_publink'; const PUBLINK_S2S_COOKIE = '_publink_srv'; +const PUBLINK_REQUEST_PATH = '/cvx/client/sync/publink'; +const PUBLINK_REFRESH_PATH = '/cvx/client/sync/publink/refresh'; export const storage = getStorageManager({moduleType: MODULE_TYPE_UID, moduleName: MODULE_NAME}); @@ -23,10 +32,9 @@ function isHex(s) { return /^[A-F0-9]+$/i.test(s); } -function publinkIdUrl(params, consentData) { - let url = parseUrl('https://proc.ad.cpe.dotomi.com/cvx/client/sync/publink'); +function publinkIdUrl(params, consentData, storedId) { + let url = parseUrl('https://proc.ad.cpe.dotomi.com' + PUBLINK_REFRESH_PATH); url.search = { - deh: params.e, mpn: 'Prebid.js', mpv: '$prebid.version$', }; @@ -36,9 +44,21 @@ function publinkIdUrl(params, consentData) { url.search.gdpr_consent = consentData.consentString; } - if (params.site_id) { url.search.sid = params.site_id; } + if (params) { + if (params.e) { + // if there's an email parameter call the request path + url.search.deh = params.e; + url.pathname = PUBLINK_REQUEST_PATH; + } - if (params.api_key) { url.search.apikey = params.api_key; } + if (params.site_id) { url.search.sid = params.site_id; } + + if (params.api_key) { url.search.apikey = params.api_key; } + } + + if (storedId) { + url.search.publink = storedId; + } const usPrivacyString = uspDataHandler.getConsentData(); if (usPrivacyString && typeof usPrivacyString === 'string') { @@ -48,7 +68,7 @@ function publinkIdUrl(params, consentData) { return buildUrl(url); } -function makeCallback(config = {}, consentData) { +function makeCallback(config = {}, consentData, storedId) { return function(prebidCallback) { const options = {method: 'GET', withCredentials: true}; let handleResponse = function(responseText, xhr) { @@ -59,15 +79,12 @@ function makeCallback(config = {}, consentData) { } } }; - - if (config.params && config.params.e) { - if (isHex(config.params.e)) { - ajax(publinkIdUrl(config.params, consentData), handleResponse, undefined, options); - } else { - logError('params.e must be a hex string'); - } + if ((config.params && config.params.e && isHex(config.params.e)) || storedId) { + ajax(publinkIdUrl(config.params, consentData, storedId), handleResponse, undefined, options); + } else if (config.params.e) { + logError('params.e must be a hex string'); } - }; + } } function getlocalValue() { @@ -137,9 +154,13 @@ export const publinkIdSubmodule = { if (localValue) { return {id: localValue}; } - if (!storedId) { - return {callback: makeCallback(config, consentData)}; - } - } + return {callback: makeCallback(config, consentData, storedId)}; + }, + eids: { + 'publinkId': { + source: 'epsilon.com', + atype: 3 + }, + }, }; submodule('userId', publinkIdSubmodule); diff --git a/modules/pubmaticAnalyticsAdapter.js b/modules/pubmaticAnalyticsAdapter.js index acae93c57be..ad2a06ea86d 100755 --- a/modules/pubmaticAnalyticsAdapter.js +++ b/modules/pubmaticAnalyticsAdapter.js @@ -1,13 +1,15 @@ -import { _each, pick, logWarn, isStr, isArray, logError, getGptSlotInfoForAdUnitCode } from '../src/utils.js'; +import {_each, isArray, isStr, logError, logWarn, pick, generateUUID} from '../src/utils.js'; import adapter from '../libraries/analyticsAdapter/AnalyticsAdapter.js'; import adapterManager from '../src/adapterManager.js'; import CONSTANTS from '../src/constants.json'; -import { ajax } from '../src/ajax.js'; -import { config } from '../src/config.js'; -import { getGlobal } from '../src/prebidGlobal.js'; +import {ajax} from '../src/ajax.js'; +import {config} from '../src/config.js'; +import {getGlobal} from '../src/prebidGlobal.js'; +import {getGptSlotInfoForAdUnitCode} from '../libraries/gptUtils/gptUtils.js'; /// /////////// CONSTANTS ////////////// const ADAPTER_CODE = 'pubmatic'; +const VENDOR_OPENWRAP = 'openwrap'; const SEND_TIMEOUT = 2000; const END_POINT_HOST = 'https://t.pubmatic.com/'; const END_POINT_BID_LOGGER = END_POINT_HOST + 'wl?'; @@ -22,6 +24,7 @@ const ERROR = 'error'; const REQUEST_ERROR = 'request-error'; const TIMEOUT_ERROR = 'timeout-error'; const EMPTY_STRING = ''; +const OPEN_AUCTION_DEAL_ID = '-1'; const MEDIA_TYPE_BANNER = 'banner'; const CURRENCY_USD = 'USD'; const BID_PRECISION = 2; @@ -91,7 +94,7 @@ function copyRequiredBidDetails(bid) { 'bidderCode', 'adapterCode', 'bidId', - 'status', () => NO_BID, // default a bid to NO_BID until response is recieved or bid is timed out + 'status', () => NO_BID, // default a bid to NO_BID until response is received or bid is timed out 'finalSource as source', 'params', 'floorData', @@ -256,12 +259,27 @@ function isS2SBidder(bidder) { return (s2sBidders.indexOf(bidder) > -1) ? 1 : 0 } +function isOWPubmaticBid(adapterName) { + let s2sConf = config.getConfig('s2sConfig'); + let s2sConfArray = isArray(s2sConf) ? s2sConf : [s2sConf]; + return s2sConfArray.some(conf => { + if (adapterName === ADAPTER_CODE && conf.defaultVendor === VENDOR_OPENWRAP && + conf.bidders.indexOf(ADAPTER_CODE) > -1) { + return true; + } + }) +} + function gatherPartnerBidsForAdUnitForLogger(adUnit, adUnitId, highestBid) { highestBid = (highestBid && highestBid.length > 0) ? highestBid[0] : null; return Object.keys(adUnit.bids).reduce(function(partnerBids, bidId) { adUnit.bids[bidId].forEach(function(bid) { + let adapterName = getAdapterNameForAlias(bid.adapterCode || bid.bidder); + if (isOWPubmaticBid(adapterName) && isS2SBidder(bid.bidder)) { + return; + } partnerBids.push({ - 'pn': getAdapterNameForAlias(bid.adapterCode || bid.bidder), + 'pn': adapterName, 'bc': bid.bidderCode || bid.bidder, 'bidid': bid.bidId || bidId, 'db': bid.bidResponse ? 0 : 1, @@ -270,9 +288,10 @@ function gatherPartnerBidsForAdUnitForLogger(adUnit, adUnitId, highestBid) { 'psz': bid.bidResponse ? (bid.bidResponse.dimensions.width + 'x' + bid.bidResponse.dimensions.height) : '0x0', 'eg': bid.bidResponse ? bid.bidResponse.bidGrossCpmUSD : 0, 'en': bid.bidResponse ? bid.bidResponse.bidPriceUSD : 0, - 'di': bid.bidResponse ? (bid.bidResponse.dealId || EMPTY_STRING) : EMPTY_STRING, + 'di': bid.bidResponse ? (bid.bidResponse.dealId || OPEN_AUCTION_DEAL_ID) : OPEN_AUCTION_DEAL_ID, 'dc': bid.bidResponse ? (bid.bidResponse.dealChannel || EMPTY_STRING) : EMPTY_STRING, - 'l1': bid.bidResponse ? bid.clientLatencyTimeMs : 0, + 'l1': bid.bidResponse ? bid.partnerTimeToRespond : 0, + 'ol1': bid.bidResponse ? bid.clientLatencyTimeMs : 0, 'l2': 0, 'adv': bid.bidResponse ? getAdDomain(bid.bidResponse) || undefined : undefined, 'ss': isS2SBidder(bid.bidder), @@ -283,7 +302,7 @@ function gatherPartnerBidsForAdUnitForLogger(adUnit, adUnitId, highestBid) { 'ocpm': bid.bidResponse ? (bid.bidResponse.originalCpm || 0) : 0, 'ocry': bid.bidResponse ? (bid.bidResponse.originalCurrency || CURRENCY_USD) : CURRENCY_USD, 'piid': bid.bidResponse ? (bid.bidResponse.partnerImpId || EMPTY_STRING) : EMPTY_STRING, - 'frv': (s2sBidders.indexOf(bid.bidder) > -1) ? undefined : (bid.bidResponse ? (bid.bidResponse.floorData ? bid.bidResponse.floorData.floorRuleValue : undefined) : undefined), + 'frv': bid.bidResponse ? bid.bidResponse.floorData?.floorRuleValue : undefined, 'md': bid.bidResponse ? getMetadata(bid.bidResponse.meta) : undefined }); }); @@ -319,11 +338,24 @@ function getTgId() { return 0; } +function getFloorFetchStatus(floorData) { + if (!floorData?.floorRequestData) { + return false; + } + const { location, fetchStatus } = floorData?.floorRequestData; + const isDataValid = location !== CONSTANTS.FLOOR_VALUES.NO_DATA; + const isFetchSuccessful = location === CONSTANTS.FLOOR_VALUES.FETCH && fetchStatus === CONSTANTS.FLOOR_VALUES.SUCCESS; + const isAdUnitOrSetConfig = location === CONSTANTS.FLOOR_VALUES.AD_UNIT || location === CONSTANTS.FLOOR_VALUES.SET_CONFIG; + return isDataValid && (isAdUnitOrSetConfig || isFetchSuccessful); +} + function executeBidsLoggerCall(e, highestCpmBids) { let auctionId = e.auctionId; let referrer = config.getConfig('pageUrl') || cache.auctions[auctionId].referer || ''; let auctionCache = cache.auctions[auctionId]; - let floorData = auctionCache.floorData; + let wiid = auctionCache?.wiid || auctionId; + let floorData = auctionCache?.floorData; + let floorFetchStatus = getFloorFetchStatus(auctionCache?.floorData); let outputObj = { s: [] }; let pixelURL = END_POINT_BID_LOGGER; @@ -337,7 +369,7 @@ function executeBidsLoggerCall(e, highestCpmBids) { pixelURL += 'pubid=' + publisherId; outputObj['pubid'] = '' + publisherId; - outputObj['iid'] = '' + auctionId; + outputObj['iid'] = '' + wiid; outputObj['to'] = '' + auctionCache.timeout; outputObj['purl'] = referrer; outputObj['orig'] = getDomainFromUrl(referrer); @@ -346,8 +378,9 @@ function executeBidsLoggerCall(e, highestCpmBids) { outputObj['pdvid'] = '' + profileVersionId; outputObj['dvc'] = {'plt': getDevicePlatform()}; outputObj['tgid'] = getTgId(); + outputObj['pbv'] = getGlobal()?.version || '-1'; - if (floorData) { + if (floorData && floorFetchStatus) { outputObj['fmv'] = floorData.floorRequestData ? floorData.floorRequestData.modelVersion || undefined : undefined; outputObj['ft'] = floorData.floorResponseData ? (floorData.floorResponseData.enforcements.enforceJS == false ? 0 : 1) : undefined; } @@ -362,8 +395,25 @@ function executeBidsLoggerCall(e, highestCpmBids) { 'mt': getAdUnitAdFormats(origAdUnit), 'sz': getSizesForAdUnit(adUnit, adUnitId), 'ps': gatherPartnerBidsForAdUnitForLogger(adUnit, adUnitId, highestCpmBids.filter(bid => bid.adUnitCode === adUnitId)), - 'fskp': floorData ? (floorData.floorRequestData ? (floorData.floorRequestData.skipped == false ? 0 : 1) : undefined) : undefined, + 'fskp': floorData && floorFetchStatus ? (floorData.floorRequestData ? (floorData.floorRequestData.skipped == false ? 0 : 1) : undefined) : undefined, + 'sid': generateUUID() }; + if (floorData?.floorRequestData) { + const { location, fetchStatus, floorProvider } = floorData?.floorRequestData; + slotObject.ffs = { + [CONSTANTS.FLOOR_VALUES.SUCCESS]: 1, + [CONSTANTS.FLOOR_VALUES.ERROR]: 2, + [CONSTANTS.FLOOR_VALUES.TIMEOUT]: 4, + undefined: 0 + }[fetchStatus]; + slotObject.fsrc = { + [CONSTANTS.FLOOR_VALUES.FETCH]: 2, + [CONSTANTS.FLOOR_VALUES.NO_DATA]: 2, + [CONSTANTS.FLOOR_VALUES.AD_UNIT]: 1, + [CONSTANTS.FLOOR_VALUES.SET_CONFIG]: 1 + }[location]; + slotObject.fp = floorProvider; + } slotsArray.push(slotObject); return slotsArray; }, []); @@ -385,16 +435,24 @@ function executeBidsLoggerCall(e, highestCpmBids) { function executeBidWonLoggerCall(auctionId, adUnitId) { const winningBidId = cache.auctions[auctionId].adUnitCodes[adUnitId].bidWon; const winningBids = cache.auctions[auctionId].adUnitCodes[adUnitId].bids[winningBidId]; - let winningBid = winningBids[0]; + if (!winningBids) { + logWarn(LOG_PRE_FIX + 'Could not find winningBids for : ', auctionId); + return; + } + let winningBid = winningBids[0]; if (winningBids.length > 1) { winningBid = winningBids.filter(bid => bid.adId === cache.auctions[auctionId].adUnitCodes[adUnitId].bidWonAdId)[0]; } const adapterName = getAdapterNameForAlias(winningBid.adapterCode || winningBid.bidder); + if (isOWPubmaticBid(adapterName) && isS2SBidder(winningBid.bidder)) { + return; + } let origAdUnit = getAdUnit(cache.auctions[auctionId].origAdUnits, adUnitId) || {}; let auctionCache = cache.auctions[auctionId]; let floorData = auctionCache.floorData; + let wiid = cache.auctions[auctionId]?.wiid || auctionId; let referrer = config.getConfig('pageUrl') || cache.auctions[auctionId].referer || ''; let adv = winningBid.bidResponse ? getAdDomain(winningBid.bidResponse) || undefined : undefined; let fskp = floorData ? (floorData.floorRequestData ? (floorData.floorRequestData.skipped == false ? 0 : 1) : undefined) : undefined; @@ -403,7 +461,7 @@ function executeBidWonLoggerCall(auctionId, adUnitId) { pixelURL += 'pubid=' + publisherId; pixelURL += '&purl=' + enc(config.getConfig('pageUrl') || cache.auctions[auctionId].referer || ''); pixelURL += '&tst=' + Math.round((new window.Date()).getTime() / 1000); - pixelURL += '&iid=' + enc(auctionId); + pixelURL += '&iid=' + enc(wiid); pixelURL += '&bidid=' + enc(winningBidId); pixelURL += '&pid=' + enc(profileId); pixelURL += '&pdvid=' + enc(profileVersionId); @@ -415,6 +473,7 @@ function executeBidWonLoggerCall(auctionId, adUnitId) { pixelURL += '&eg=' + enc(winningBid.bidResponse.bidGrossCpmUSD); pixelURL += '&kgpv=' + enc(getValueForKgpv(winningBid, adUnitId)); pixelURL += '&piid=' + enc(winningBid.bidResponse.partnerImpId || EMPTY_STRING); + pixelURL += '&di=' + enc(winningBid?.bidResponse?.dealId || OPEN_AUCTION_DEAL_ID); pixelURL += '&plt=' + enc(getDevicePlatform()); pixelURL += '&psz=' + enc((winningBid?.bidResponse?.dimensions?.width || '0') + 'x' + @@ -443,7 +502,10 @@ function executeBidWonLoggerCall(auctionId, adUnitId) { function auctionInitHandler(args) { s2sBidders = (function() { let s2sConf = config.getConfig('s2sConfig'); - return (s2sConf && isArray(s2sConf.bidders)) ? s2sConf.bidders : []; + let s2sBidders = []; + (s2sConf || []) && + isArray(s2sConf) ? s2sConf.map(conf => s2sBidders.push(...conf.bidders)) : s2sBidders.push(...s2sConf.bidders); + return s2sBidders || []; }()); let cacheEntry = pick(args, [ 'timestamp', @@ -466,6 +528,9 @@ function bidRequestedHandler(args) { dimensions: bid.sizes }; } + if (bid.bidder === 'pubmatic' && !!bid?.params?.wiid) { + cache.auctions[args.auctionId].wiid = bid.params.wiid; + } cache.auctions[args.auctionId].adUnitCodes[bid.adUnitCode].bids[bid.bidId] = [copyRequiredBidDetails(bid)]; if (bid.floorData) { cache.auctions[args.auctionId].floorData['floorRequestData'] = bid.floorData; @@ -474,6 +539,10 @@ function bidRequestedHandler(args) { } function bidResponseHandler(args) { + if (!args.requestId) { + logWarn(LOG_PRE_FIX + 'Got null requestId in bidResponseHandler'); + return; + } let bid = cache.auctions[args.auctionId].adUnitCodes[args.adUnitCode].bids[args.requestId][0]; if (!bid) { logError(LOG_PRE_FIX + 'Could not find associated bid request for bid response with requestId: ', args.requestId); @@ -492,10 +561,24 @@ function bidResponseHandler(args) { bid.adId = args.adId; bid.source = formatSource(bid.source || args.source); setBidStatus(bid, args); + const latency = args?.timeToRespond || Date.now() - cache.auctions[args.auctionId].timestamp; + const auctionTime = cache.auctions[args.auctionId].timeout; + // Check if latency is greater than auctiontime+150, then log auctiontime+150 to avoid large numbers + bid.partnerTimeToRespond = latency > (auctionTime + 150) ? (auctionTime + 150) : latency; bid.clientLatencyTimeMs = Date.now() - cache.auctions[args.auctionId].timestamp; bid.bidResponse = parseBidResponse(args); } +function bidRejectedHandler(args) { + // If bid is rejected due to floors value did not met + // make cpm as 0, status as bidRejected and forward the bid for logging + if (args.rejectionReason === CONSTANTS.REJECTION_REASON.FLOOR_NOT_MET) { + args.cpm = 0; + args.status = CONSTANTS.BID_STATUS.BID_REJECTED; + bidResponseHandler(args); + } +} + function bidderDoneHandler(args) { cache.auctions[args.auctionId].bidderDonePendingCount--; args.bids.forEach(bid => { @@ -524,7 +607,7 @@ function auctionEndHandler(args) { let highestCpmBids = getGlobal().getHighestCpmBids() || []; setTimeout(() => { executeBidsLoggerCall.call(this, args, highestCpmBids); - }, (cache.auctions[args.auctionId].bidderDonePendingCount === 0 ? 500 : SEND_TIMEOUT)); + }, (cache.auctions[args.auctionId]?.bidderDonePendingCount === 0 ? 500 : SEND_TIMEOUT)); } function bidTimeoutHandler(args) { @@ -594,6 +677,9 @@ let pubmaticAdapter = Object.assign({}, baseAdapter, { case CONSTANTS.EVENTS.BID_RESPONSE: bidResponseHandler(args); break; + case CONSTANTS.EVENTS.BID_REJECTED: + bidRejectedHandler(args) + break; case CONSTANTS.EVENTS.BIDDER_DONE: bidderDoneHandler(args); break; diff --git a/modules/pubmaticBidAdapter.js b/modules/pubmaticBidAdapter.js index eb2ad8ccb03..d25627a7b90 100644 --- a/modules/pubmaticBidAdapter.js +++ b/modules/pubmaticBidAdapter.js @@ -1,10 +1,17 @@ -import { getBidRequest, logWarn, isBoolean, isStr, isArray, inIframe, mergeDeep, deepAccess, isNumber, deepSetValue, logInfo, logError, deepClone, convertTypes, uniques, isPlainObject, isInteger } from '../src/utils.js'; +import { getBidRequest, logWarn, isBoolean, isStr, isArray, inIframe, mergeDeep, deepAccess, isNumber, deepSetValue, logInfo, logError, deepClone, uniques, isPlainObject, isInteger, generateUUID } from '../src/utils.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; import { BANNER, VIDEO, NATIVE, ADPOD } from '../src/mediaTypes.js'; import { config } from '../src/config.js'; import { Renderer } from '../src/Renderer.js'; import { bidderSettings } from '../src/bidderSettings.js'; import CONSTANTS from '../src/constants.json'; +import {convertTypes} from '../libraries/transformParamsUtils/convertTypes.js'; + +/** + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid + * @typedef {import('../src/adapters/bidderFactory.js').validBidRequests} validBidRequests + */ const BIDDER_CODE = 'pubmatic'; const LOG_WARN_PREFIX = 'PubMatic: '; @@ -733,9 +740,9 @@ function _addImpressionFPD(imp, bid) { const ortb2 = {...deepAccess(bid, 'ortb2Imp.ext.data')}; Object.keys(ortb2).forEach(prop => { /** - * Prebid AdSlot - * @type {(string|undefined)} - */ + * Prebid AdSlot + * @type {(string|undefined)} + */ if (prop === 'pbadslot') { if (typeof ortb2[prop] === 'string' && ortb2[prop]) deepSetValue(imp, 'ext.data.pbadslot', ortb2[prop]); } else if (prop === 'adserver') { @@ -757,6 +764,9 @@ function _addImpressionFPD(imp, bid) { deepSetValue(imp, `ext.data.${prop}`, ortb2[prop]); } }); + + const gpid = deepAccess(bid, 'ortb2Imp.ext.gpid'); + gpid && deepSetValue(imp, `ext.gpid`, gpid); } function _addFloorFromFloorModule(impObj, bid) { @@ -1007,11 +1017,11 @@ export const spec = { gvlid: 76, supportedMediaTypes: [BANNER, VIDEO, NATIVE], /** - * Determines whether or not the given bid request is valid. Valid bid request must have placementId and hbid - * - * @param {BidRequest} bid The bid params to validate. - * @return boolean True if this is a valid bid, and false otherwise. - */ + * Determines whether or not the given bid request is valid. Valid bid request must have placementId and hbid + * + * @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) { if (!isStr(bid.params.publisherId)) { @@ -1077,8 +1087,10 @@ export const spec = { var bid; var blockedIabCategories = []; var allowedIabCategories = []; + var wiid = generateUUID(); validBidRequests.forEach(originalBid => { + originalBid.params.wiid = originalBid.params.wiid || bidderRequest.auctionId || wiid; bid = deepClone(originalBid); bid.params.adSlot = bid.params.adSlot || ''; _parseAdSlot(bid); @@ -1150,10 +1162,7 @@ export const spec = { payload.user.gender = (conf.gender ? conf.gender.trim() : UNDEFINED); payload.user.geo = {}; // TODO: fix lat and long to only come from request object, not params - payload.user.geo.lat = _parseSlotParam('lat', 0); - payload.user.geo.lon = _parseSlotParam('lon', 0); payload.user.yob = _parseSlotParam('yob', conf.yob); - payload.device.geo = payload.user.geo; payload.site.page = conf.kadpageurl.trim() || payload.site.page.trim(); payload.site.domain = _getDomainFromURL(payload.site.page); @@ -1194,6 +1203,15 @@ export const spec = { deepSetValue(payload, 'regs.ext.us_privacy', bidderRequest.uspConsent); } + // Attaching GPP Consent Params + if (bidderRequest?.gppConsent?.gppString) { + deepSetValue(payload, 'regs.gpp', bidderRequest.gppConsent.gppString); + deepSetValue(payload, 'regs.gpp_sid', bidderRequest.gppConsent.applicableSections); + } else if (bidderRequest?.ortb2?.regs?.gpp) { + deepSetValue(payload, 'regs.gpp', bidderRequest.ortb2.regs.gpp); + deepSetValue(payload, 'regs.gpp_sid', bidderRequest.ortb2.regs.gpp_sid); + } + // coppa compliance if (config.getConfig('coppa') === true) { deepSetValue(payload, 'regs.coppa', 1); @@ -1203,22 +1221,39 @@ export const spec = { // First Party Data const commonFpd = (bidderRequest && bidderRequest.ortb2) || {}; - if (commonFpd.site) { + const { user, device, site, bcat, badv } = commonFpd; + if (site) { const { page, domain, ref } = payload.site; - mergeDeep(payload, {site: commonFpd.site}); + mergeDeep(payload, {site: site}); payload.site.page = page; payload.site.domain = domain; payload.site.ref = ref; } - if (commonFpd.user) { - mergeDeep(payload, {user: commonFpd.user}); + if (user) { + mergeDeep(payload, {user: user}); + } + if (badv) { + mergeDeep(payload, {badv: badv}); } - if (commonFpd.bcat) { - blockedIabCategories = blockedIabCategories.concat(commonFpd.bcat); + if (bcat) { + blockedIabCategories = blockedIabCategories.concat(bcat); } // check if fpd ortb2 contains device property with sua object - if (commonFpd.device?.sua) { - payload.device.sua = commonFpd.device?.sua; + if (device?.sua) { + payload.device.sua = device?.sua; + } + + if (device?.ext?.cdep) { + deepSetValue(payload, 'device.ext.cdep', device.ext.cdep); + } + + if (user?.geo && device?.geo) { + payload.device.geo = { ...payload.device.geo, ...device.geo }; + payload.user.geo = { ...payload.user.geo, ...user.geo }; + } else { + if (user?.geo || device?.geo) { + payload.user.geo = payload.device.geo = (user?.geo ? { ...payload.user.geo, ...user.geo } : { ...payload.user.geo, ...device.geo }); + } } if (commonFpd.ext?.prebid?.bidderparams?.[bidderRequest.bidderCode]?.acat) { @@ -1343,6 +1378,21 @@ export const spec = { }); }); } + let fledgeAuctionConfigs = deepAccess(response.body, 'ext.fledge_auction_configs'); + if (fledgeAuctionConfigs) { + fledgeAuctionConfigs = Object.entries(fledgeAuctionConfigs).map(([bidId, cfg]) => { + return { + bidId, + config: Object.assign({ + auctionSignals: {}, + }, cfg) + } + }); + return { + bids: bidResponses, + fledgeAuctionConfigs, + } + } } catch (error) { logError(error); } @@ -1352,7 +1402,7 @@ export const spec = { /** * Register User Sync. */ - getUserSyncs: (syncOptions, responses, gdprConsent, uspConsent) => { + getUserSyncs: (syncOptions, responses, gdprConsent, uspConsent, gppConsent) => { let syncurl = '' + publisherId; // Attaching GDPR Consent Params in UserSync url @@ -1366,6 +1416,12 @@ export const spec = { syncurl += '&us_privacy=' + encodeURIComponent(uspConsent); } + // GPP Consent + if (gppConsent?.gppString && gppConsent?.applicableSections?.length) { + syncurl += '&gpp=' + encodeURIComponent(gppConsent.gppString); + syncurl += '&gpp_sid=' + encodeURIComponent(gppConsent?.applicableSections?.join(',')); + } + // coppa compliance if (config.getConfig('coppa') === true) { syncurl += '&coppa=1'; diff --git a/modules/pubwiseBidAdapter.js b/modules/pubwiseBidAdapter.js index 6a5d866c76d..507df4a2bb0 100644 --- a/modules/pubwiseBidAdapter.js +++ b/modules/pubwiseBidAdapter.js @@ -6,6 +6,13 @@ import { config } from '../src/config.js'; import { convertOrtbRequestToProprietaryNative } from '../src/native.js'; import { OUTSTREAM, INSTREAM } from '../src/video.js'; +/** + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid + * @typedef {import('../src/adapters/bidderFactory.js').ServerResponse} ServerResponse + * @typedef {import('../src/adapters/bidderFactory.js').validBidRequests} validBidRequests + */ + const VERSION = '0.3.0'; const GVLID = 842; const NET_REVENUE = true; diff --git a/modules/pubxBidAdapter.js b/modules/pubxBidAdapter.js index ee28d549475..60e5be2a321 100644 --- a/modules/pubxBidAdapter.js +++ b/modules/pubxBidAdapter.js @@ -55,8 +55,8 @@ export const spec = { /** * Determine which user syncs should occur * @param {object} syncOptions - * @param {array} serverResponses - * @returns {array} User sync pixels + * @param {Array} serverResponses + * @returns {Array} User sync pixels */ getUserSyncs: function (syncOptions, serverResponses) { const kwTag = document.getElementsByName('keywords'); diff --git a/modules/pubxaiAnalyticsAdapter.js b/modules/pubxaiAnalyticsAdapter.js index 19a3c236942..e97e5505768 100644 --- a/modules/pubxaiAnalyticsAdapter.js +++ b/modules/pubxaiAnalyticsAdapter.js @@ -1,9 +1,10 @@ -import { deepAccess, getGptSlotInfoForAdUnitCode, parseSizesInput, getWindowLocation, buildUrl } from '../src/utils.js'; +import { deepAccess, parseSizesInput, getWindowLocation, buildUrl } from '../src/utils.js'; import { ajax } from '../src/ajax.js'; import adapter from '../libraries/analyticsAdapter/AnalyticsAdapter.js'; import adapterManager from '../src/adapterManager.js'; import CONSTANTS from '../src/constants.json'; import {getGlobal} from '../src/prebidGlobal.js'; +import {getGptSlotInfoForAdUnitCode} from '../libraries/gptUtils/gptUtils.js'; const emptyUrl = ''; const analyticsType = 'endpoint'; diff --git a/modules/pulsepointBidAdapter.js b/modules/pulsepointBidAdapter.js index 015e80d5692..516254b358b 100644 --- a/modules/pulsepointBidAdapter.js +++ b/modules/pulsepointBidAdapter.js @@ -1,21 +1,10 @@ -import { convertOrtbRequestToProprietaryNative } from '../src/native.js'; -/* eslint dot-notation:0, quote-props:0 */ -import {convertTypes, deepAccess, isArray, isFn, logError} from '../src/utils.js'; +import { ortbConverter } from '../libraries/ortbConverter/converter.js'; +import {isArray} from '../src/utils.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; -import {Renderer} from '../src/Renderer.js'; +import {convertTypes} from '../libraries/transformParamsUtils/convertTypes.js'; -const NATIVE_DEFAULTS = { - TITLE_LEN: 100, - DESCR_LEN: 200, - SPONSORED_BY_LEN: 50, - IMG_MIN: 150, - ICON_MIN: 50, -}; - -const DEFAULT_BID_TTL = 20; const DEFAULT_CURRENCY = 'USD'; -const DEFAULT_NET_REVENUE = true; -const KNOWN_PARAMS = ['cp', 'ct', 'cf', 'video', 'battr', 'bcat', 'badv', 'bidfloor']; +const KNOWN_PARAMS = ['cp', 'ct', 'cf', 'battr', 'deals']; const DEFAULT_TMAX = 500; /** @@ -41,33 +30,21 @@ export const spec = { ), buildRequests: (bidRequests, bidderRequest) => { - // convert Native ORTB definition to old-style prebid native definition - bidRequests = convertOrtbRequestToProprietaryNative(bidRequests); - - const request = { - id: bidRequests[0].bidderRequestId, - imp: bidRequests.map(slot => impression(slot)), - site: site(bidRequests, bidderRequest), - app: app(bidRequests), - device: device(), - bcat: deepAccess(bidderRequest.ortb2Imp, 'bcat') || bidRequests[0].params.bcat, - badv: bidRequests[0].params.badv, - user: user(bidRequests[0], bidderRequest), - regs: regs(bidderRequest), - source: source(bidRequests[0].schain), - tmax: bidderRequest.timeout || DEFAULT_TMAX, - }; + const data = converter.toORTB({bidRequests, bidderRequest}); return { method: 'POST', url: 'https://bid.contextweb.com/header/ortb?src=prebid', - data: request, + data, bidderRequest }; }, - interpretResponse: (response, request) => ( - bidResponseAvailable(request, response) - ), + interpretResponse: (response, request) => { + if (response.body) { + return converter.fromORTB({response: response.body, request: request.data}).bids; + } + return []; + }, getUserSyncs: syncOptions => { if (syncOptions.iframeEnabled) { @@ -82,7 +59,7 @@ export const spec = { }]; } }, - transformBidParams: function(params, isOpenRtb) { + transformBidParams: function(params) { return convertTypes({ 'cf': 'string', 'cp': 'number', @@ -91,124 +68,61 @@ export const spec = { } }; -/** - * Callback for bids, after the call to PulsePoint completes. - */ -function bidResponseAvailable(request, response) { - const idToImpMap = {}; - const idToBidMap = {}; - const idToSlotConfig = {}; - const bidResponse = response.body; - // extract the request bids and the response bids, keyed by impr-id - const ortbRequest = request.data; - ortbRequest.imp.forEach(imp => { - idToImpMap[imp.id] = imp; - }); - if (bidResponse) { - bidResponse.seatbid.forEach(seatBid => seatBid.bid.forEach(bid => { - idToBidMap[bid.impid] = bid; - })); - } - if (request.bidderRequest && request.bidderRequest.bids) { - request.bidderRequest.bids.forEach(bid => { - idToSlotConfig[bid.bidId] = bid; - }); - } - const bids = []; - Object.keys(idToImpMap).forEach(id => { - if (idToBidMap[id]) { - const bid = { - requestId: id, - cpm: idToBidMap[id].price, - creative_id: idToBidMap[id].crid, - creativeId: idToBidMap[id].crid, - adId: id, - ttl: idToBidMap[id].exp || DEFAULT_BID_TTL, - netRevenue: DEFAULT_NET_REVENUE, - currency: bidResponse.cur || DEFAULT_CURRENCY, - meta: { advertiserDomains: idToBidMap[id].adomain || [] } - }; - if (idToImpMap[id].video) { - // for outstream, a renderer is specified - if (idToSlotConfig[id] && deepAccess(idToSlotConfig[id], 'mediaTypes.video.context') === 'outstream') { - bid.renderer = outstreamRenderer(deepAccess(idToSlotConfig[id], 'renderer.options'), deepAccess(idToBidMap[id], 'ext.outstream')); +const converter = ortbConverter({ + context: { + netRevenue: true, + ttl: 300, + currency: 'USD' + }, + + imp(buildImp, bidRequest, context) { + const imp = buildImp(bidRequest, context); + // tagid + imp.tagid = bidRequest.params.ct.toString(); + // unknown params + const unknownParams = slotUnknownParams(bidRequest); + if (imp.ext || unknownParams) { + imp.ext = Object.assign({}, imp.ext, unknownParams); + } + // battr + if (bidRequest.params.battr) { + ['banner', 'video', 'audio', 'native'].forEach(k => { + if (imp[k]) { + imp[k].battr = bidRequest.params.battr; } - bid.vastXml = idToBidMap[id].adm; - bid.mediaType = 'video'; - bid.width = idToBidMap[id].w; - bid.height = idToBidMap[id].h; - } else if (idToImpMap[id].banner) { - bid.ad = idToBidMap[id].adm; - bid.width = idToBidMap[id].w || idToImpMap[id].banner.w; - bid.height = idToBidMap[id].h || idToImpMap[id].banner.h; - } else if (idToImpMap[id]['native']) { - bid['native'] = nativeResponse(idToImpMap[id], idToBidMap[id]); - bid.mediaType = 'native'; - } - bids.push(bid); + }); } - }); - return bids; -} - -/** - * Produces an OpenRTBImpression from a slot config. - */ -function impression(slot) { - var firstPartyData = slot.ortb2Imp?.ext || {}; - var ext = Object.assign({}, firstPartyData, slotUnknownParams(slot)); - return { - id: slot.bidId, - banner: banner(slot), - 'native': nativeImpression(slot), - tagid: slot.params.ct.toString(), - video: video(slot), - bidfloor: bidFloor(slot), - ext: Object.keys(ext).length > 0 ? ext : null, - }; -} - -/** - * Produces an OpenRTB Banner object for the slot given. - */ -function banner(slot) { - const sizes = parseSizes(slot); - const size = adSize(slot, sizes); - return (slot.mediaTypes && slot.mediaTypes.banner) ? { - w: size[0], - h: size[1], - battr: slot.params.battr, - format: sizes - } : null; -} + // deals + if (bidRequest.params.deals && isArray(bidRequest.params.deals)) { + imp.pmp = { + private_auction: 0, + deals: bidRequest.params.deals + }; + } + return imp; + }, -/** - * Produce openrtb format objects based on the sizes configured for the slot. - */ -function parseSizes(slot) { - const sizes = deepAccess(slot, 'mediaTypes.banner.sizes'); - if (sizes && isArray(sizes)) { - return sizes.filter(sz => isArray(sz) && sz.length === 2).map(sz => ({ - w: sz[0], - h: sz[1] - })); - } - return null; -} + request(buildRequest, imps, bidderRequest, context) { + const request = buildRequest(imps, bidderRequest, context); + // publisher id + const siteOrApp = request.site || request.app; + const pubId = context.bidRequests && context.bidRequests.length > 0 ? context.bidRequests[0].params.cp : '0'; + if (siteOrApp) { + siteOrApp.publisher = Object.assign({}, siteOrApp.publisher, { + id: pubId.toString() + }); + } + // tmax + request.tmax = request.tmax || DEFAULT_TMAX; + return request; + }, -/** - * Produces an OpenRTB Video object for the slot given - */ -function video(slot) { - if (slot.params.video) { - return Object.assign({}, - slot.params.video, // previously supported as bidder param - slot.mediaTypes && slot.mediaTypes.video ? slot.mediaTypes.video : {}, // params on mediaTypes.video - {battr: slot.params.battr} - ); - } - return null; -} + bidResponse(buildBidResponse, bid, context) { + const bidResponse = buildBidResponse(bid, context); + bidResponse.cur = bidResponse.cur || DEFAULT_CURRENCY; + return bidResponse; + }, +}); /** * Unknown params are captured and sent on ext @@ -225,278 +139,4 @@ function slotUnknownParams(slot) { return Object.keys(ext).length > 0 ? { prebid: ext } : null; } -/** - * Sets up the renderer on the bid, for outstream bid responses. - */ -function outstreamRenderer(rendererOptions, outstreamExtOptions) { - const renderer = Renderer.install({ - url: outstreamExtOptions.rendererUrl, - config: { - defaultOptions: outstreamExtOptions.config, - rendererOptions, - type: outstreamExtOptions.type - }, - loaded: false, - }); - renderer.setRender((bid) => { - bid.renderer.push(() => { - const config = bid.renderer.getConfig(); - new window.PulsePointOutstreamRenderer().render({ - adUnitCode: bid.adUnitCode, - vastXml: bid.vastXml, - type: config.type, - defaultOptions: config.defaultOptions, - rendererOptions - }); - }); - }); - return renderer; -} - -/** - * Produces an OpenRTB Native object for the slot given. - */ -function nativeImpression(slot) { - if (slot.nativeParams) { - const assets = []; - addAsset(assets, titleAsset(assets.length + 1, slot.nativeParams.title, NATIVE_DEFAULTS.TITLE_LEN)); - addAsset(assets, dataAsset(assets.length + 1, slot.nativeParams.body, 2, NATIVE_DEFAULTS.DESCR_LEN)); - addAsset(assets, dataAsset(assets.length + 1, slot.nativeParams.sponsoredBy, 1, NATIVE_DEFAULTS.SPONSORED_BY_LEN)); - addAsset(assets, imageAsset(assets.length + 1, slot.nativeParams.icon, 1, NATIVE_DEFAULTS.ICON_MIN, NATIVE_DEFAULTS.ICON_MIN)); - addAsset(assets, imageAsset(assets.length + 1, slot.nativeParams.image, 3, NATIVE_DEFAULTS.IMG_MIN, NATIVE_DEFAULTS.IMG_MIN)); - return { - request: JSON.stringify({ assets }), - ver: '1.1', - battr: slot.params.battr, - }; - } - return null; -} - -/** - * Helper method to add an asset to the assets list. - */ -function addAsset(assets, asset) { - if (asset) { - assets.push(asset); - } -} - -/** - * Produces a Native Title asset for the configuration given. - */ -function titleAsset(id, params, defaultLen) { - if (params) { - return { - id, - required: params.required ? 1 : 0, - title: { - len: params.len || defaultLen, - }, - }; - } - return null; -} - -/** - * Produces a Native Image asset for the configuration given. - */ -function imageAsset(id, params, type, defaultMinWidth, defaultMinHeight) { - return params ? { - id, - required: params.required ? 1 : 0, - img: { - type, - wmin: params.wmin || defaultMinWidth, - hmin: params.hmin || defaultMinHeight, - } - } : null; -} - -/** - * Produces a Native Data asset for the configuration given. - */ -function dataAsset(id, params, type, defaultLen) { - return params ? { - id, - required: params.required ? 1 : 0, - data: { - type, - len: params.len || defaultLen, - } - } : null; -} - -/** - * Produces an OpenRTB site object. - */ -function site(bidRequests, bidderRequest) { - const pubId = bidRequests && bidRequests.length > 0 ? bidRequests[0].params.cp : '0'; - const appParams = bidRequests[0].params.app; - if (!appParams) { - // use the first party data if available, and override only publisher/ref/page properties - var firstPartyData = bidderRequest?.ortb2?.site || {}; - return Object.assign({}, firstPartyData, { - publisher: { - id: pubId.toString(), - }, - // TODO: does the fallback make sense here? - ref: bidderRequest?.refererInfo?.ref || window.document.referrer, - page: bidderRequest?.refererInfo?.page || '' - }); - } - return null; -} - -/** - * Produces an OpenRTB App object. - */ -function app(bidderRequest) { - const pubId = bidderRequest && bidderRequest.length > 0 ? bidderRequest[0].params.cp : '0'; - const appParams = bidderRequest[0].params.app; - if (appParams) { - return { - publisher: { - id: pubId.toString(), - }, - bundle: appParams.bundle, - storeurl: appParams.storeUrl, - domain: appParams.domain, - } - } - return null; -} - -/** - * Produces an OpenRTB Device object. - */ -function device() { - return { - ua: navigator.userAgent, - language: (navigator.language || navigator.browserLanguage || navigator.userLanguage || navigator.systemLanguage), - }; -} - -/** - * Safely parses the input given. Returns null on - * parsing failure. - */ -function parse(rawResponse) { - try { - if (rawResponse) { - return JSON.parse(rawResponse); - } - } catch (ex) { - logError('pulsepointLite.safeParse', 'ERROR', ex); - } - return null; -} - -/** - * Determines the AdSize for the slot. - */ -function adSize(slot, sizes) { - if (slot.params.cf) { - const size = slot.params.cf.toUpperCase().split('X'); - const width = parseInt(slot.params.cw || size[0], 10); - const height = parseInt(slot.params.ch || size[1], 10); - return [width, height]; - } else if (sizes && sizes.length > 0) { - return [sizes[0].w, sizes[0].h]; - } - return [1, 1]; -} - -/** - * Handles the user level attributes and produces - * an openrtb User object. - */ -function user(bidRequest, bidderRequest) { - var user = bidderRequest?.ortb2?.user || { ext: {} }; - var ext = user.ext; - if (bidderRequest) { - if (bidderRequest.gdprConsent) { - ext.consent = bidderRequest.gdprConsent.consentString; - } - } - if (bidRequest) { - let eids = bidRequest.userIdAsEids; - if (eids) { - ext.eids = eids; - } - } - return user; -} - -/** - * Produces the regulations ortb object - */ -function regs(bidderRequest) { - if (bidderRequest.gdprConsent || bidderRequest.uspConsent) { - var ext = {}; - // GDPR applies attribute (actual consent value is in user object) - if (bidderRequest.gdprConsent) { - ext.gdpr = bidderRequest.gdprConsent.gdprApplies ? 1 : 0; - } - // CCPA - if (bidderRequest.uspConsent) { - ext.us_privacy = bidderRequest.uspConsent; - } - return { ext }; - } - return null; -} - -/** - * Creates source object with supply chain - */ -function source(schain) { - if (schain) { - return { - ext: { schain } - }; - } - return null; -} - -/** - * Parses the native response from the Bid given. - */ -function nativeResponse(imp, bid) { - if (imp['native']) { - const nativeAd = parse(bid.adm); - const keys = {}; - if (nativeAd && nativeAd['native'] && nativeAd['native'].assets) { - nativeAd['native'].assets.forEach(asset => { - keys.title = asset.title ? asset.title.text : keys.title; - keys.body = asset.data && asset.data.type === 2 ? asset.data.value : keys.body; - keys.sponsoredBy = asset.data && asset.data.type === 1 ? asset.data.value : keys.sponsoredBy; - keys.image = asset.img && asset.img.type === 3 ? asset.img.url : keys.image; - keys.icon = asset.img && asset.img.type === 1 ? asset.img.url : keys.icon; - }); - if (nativeAd['native'].link) { - keys.clickUrl = encodeURIComponent(nativeAd['native'].link.url); - } - keys.impressionTrackers = nativeAd['native'].imptrackers; - return keys; - } - } - return null; -} - -function bidFloor(slot) { - let floor = slot.params.bidfloor; - if (isFn(slot.getFloor)) { - const floorData = slot.getFloor({ - mediaType: slot.mediaTypes.banner ? 'banner' : slot.mediaTypes.video ? 'video' : 'Native', - size: '*', - currency: DEFAULT_CURRENCY, - }); - if (floorData && floorData.floor) { - floor = floorData.floor; - } - } - return floor; -} - registerBidder(spec); diff --git a/modules/pulsepointBidAdapter.md b/modules/pulsepointBidAdapter.md index 7f4b7e6b611..899c277f92f 100644 --- a/modules/pulsepointBidAdapter.md +++ b/modules/pulsepointBidAdapter.md @@ -2,7 +2,7 @@ **Module Name**: PulsePoint Bidder Adapter **Module Type**: Bidder Adapter -**Maintainer**: ExchangeTeam@pulsepoint.com +**Maintainer**: ExchangeTeam@pulsepoint.com # Description @@ -18,55 +18,49 @@ Please use ```pulsepoint``` as the bidder code. sizes: [[300, 250]], bids: [{ bidder: 'pulsepoint', - params: { - cf: '300X250', + params: { cp: 512379, ct: 486653 } }] },{ - code: 'native-ad-div', - sizes: [[1, 1]], - nativeParams: { - title: { required: true, len: 75 }, - image: { required: true }, - body: { len: 200 }, - sponsoredBy: { len: 20 } - }, - bids: [{ - bidder: 'pulsepoint', - params: { - cp: 512379, - ct: 505642 - } - }] - },{ - code: 'outstream-div', - mediaTypes: { - video: { - playerSize: [640, 480], - context: 'outstream', - h: 300, - w: 400, - minduration: 1, - maxduration: 210, - linearity: 1, - mimes: ["video/mp4", "video/ogg", "video/webm"], - pos: 3 - } - }, - bids: [{ - bidder: 'pulsepoint', - params: { - cp: 512379, - ct: 505642 - } - }], - renderer: { - options: { - text: "PulsePoint Outstream" - } - } + code: 'native-1-slot', + mediaTypes: { + native: { + ortb: { + assets: [{ + id: 1, + required: 1, + img: { + type: 3, + w: 150, + h: 50, + } + }, + { + id: 2, + required: 1, + title: { + len: 80 + } + }, + { + id: 3, + required: 1, + data: { + type: 1 + } + }] + } + } + }, + bids: [{ + bidder: 'pulsepoint', + params: { + cp: 512379, + ct: 694973 + } + }] },{ code: 'instream', mediaTypes: { diff --git a/modules/pxyzBidAdapter.js b/modules/pxyzBidAdapter.js index 1ab432496a3..8b9dbea339b 100644 --- a/modules/pxyzBidAdapter.js +++ b/modules/pxyzBidAdapter.js @@ -2,6 +2,11 @@ import {registerBidder} from '../src/adapters/bidderFactory.js'; import {BANNER} from '../src/mediaTypes.js'; import {isArray, logError, logInfo} from '../src/utils.js'; +/** + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid + */ + const BIDDER_CODE = 'pxyz'; const URL = 'https://ads.playground.xyz/host-config/prebid?v=2'; const DEFAULT_CURRENCY = 'USD'; diff --git a/modules/qortexRtdProvider.js b/modules/qortexRtdProvider.js new file mode 100644 index 00000000000..7aa30334756 --- /dev/null +++ b/modules/qortexRtdProvider.js @@ -0,0 +1,165 @@ +import { submodule } from '../src/hook.js'; +import { ajax } from '../src/ajax.js'; +import { logWarn, mergeDeep, logMessage, generateUUID } from '../src/utils.js'; +import { loadExternalScript } from '../src/adloader.js'; +import * as events from '../src/events.js'; +import CONSTANTS from '../src/constants.json'; + +let requestUrl; +let bidderArray; +let impressionIds; +let currentSiteContext; + +/** + * Init if module configuration is valid + * @param {Object} config Module configuration + * @returns {Boolean} + */ +function init (config) { + if (!config?.params?.groupId?.length > 0) { + logWarn('Qortex RTD module config does not contain valid groupId parameter. Config params: ' + JSON.stringify(config.params)) + return false; + } else { + initializeModuleData(config); + } + if (config?.params?.tagConfig) { + loadScriptTag(config) + } + return true; +} + +/** + * Processess prebid request and attempts to add context to ort2b fragments + * @param {Object} reqBidsConfig Bid request configuration object + * @param {Function} callback Called on completion + */ +function getBidRequestData (reqBidsConfig, callback) { + if (reqBidsConfig?.adUnits?.length > 0) { + getContext() + .then(contextData => { + setContextData(contextData) + addContextToRequests(reqBidsConfig) + callback(); + }) + .catch((e) => { + logWarn(e?.message); + callback(); + }); + } else { + logWarn('No adunits found on request bids configuration: ' + JSON.stringify(reqBidsConfig)) + callback(); + } +} + +/** + * determines whether to send a request to context api and does so if necessary + * @returns {Promise} ortb Content object + */ +export function getContext () { + if (!currentSiteContext) { + logMessage('Requesting new context data'); + return new Promise((resolve, reject) => { + const callbacks = { + success(text, data) { + const result = data.status === 200 ? JSON.parse(data.response)?.content : null; + resolve(result); + }, + error(error) { + reject(new Error(error)); + } + } + ajax(requestUrl, callbacks) + }) + } else { + logMessage('Adding Content object from existing context data'); + return new Promise(resolve => resolve(currentSiteContext)); + } +} + +/** + * Updates bidder configs with the response from Qortex context services + * @param {Object} reqBidsConfig Bid request configuration object + * @param {string[]} bidders Bidders specified in module's configuration + */ +export function addContextToRequests (reqBidsConfig) { + if (currentSiteContext === null) { + logWarn('No context data received at this time'); + } else { + const fragment = { site: {content: currentSiteContext} } + if (bidderArray?.length > 0) { + bidderArray.forEach(bidder => mergeDeep(reqBidsConfig.ortb2Fragments.bidder, {[bidder]: fragment})) + } else if (!bidderArray) { + mergeDeep(reqBidsConfig.ortb2Fragments.global, fragment); + } else { + logWarn('Config contains an empty bidders array, unable to determine which bids to enrich'); + } + } +} + +/** + * Loads Qortex header tag using data passed from module config object + * @param {Object} config module config obtained during init + */ +export function loadScriptTag(config) { + const code = 'qortex'; + const groupId = config.params.groupId; + const src = 'https://tags.qortex.ai/bootstrapper' + const attr = {'data-group-id': groupId} + const tc = config.params.tagConfig + + Object.keys(tc).forEach(p => { + attr[`data-${p.replace(/([A-Z])/g, (m) => `-${m.toLowerCase()}`)}`] = tc[p] + }) + + addEventListener('qortex-rtd', (e) => { + const billableEvent = { + vendor: code, + billingId: generateUUID(), + type: e?.detail?.type, + accountId: groupId + } + switch (e?.detail?.type) { + case 'qx-impression': + const {uid} = e.detail; + if (!uid || impressionIds.has(uid)) { + logWarn(`received invalid billable event due to ${!uid ? 'missing' : 'duplicate'} uid: qx-impression`) + return; + } else { + logMessage('received billable event: qx-impression') + impressionIds.add(uid) + billableEvent.transactionId = e.detail.uid; + events.emit(CONSTANTS.EVENTS.BILLABLE_EVENT, billableEvent); + break; + } + default: + logWarn(`received invalid billable event: ${e.detail?.type}`) + } + }) + + loadExternalScript(src, code, undefined, undefined, attr); +} + +/** + * Helper function to set initial values when they are obtained by init + * @param {Object} config module config obtained during init + */ +export function initializeModuleData(config) { + const DEFAULT_API_URL = 'https://demand.qortex.ai'; + const {apiUrl, groupId, bidders} = config.params; + requestUrl = `${apiUrl || DEFAULT_API_URL}/api/v1/analyze/${groupId}/prebid`; + bidderArray = bidders; + impressionIds = new Set(); + currentSiteContext = null; +} + +export function setContextData(value) { + currentSiteContext = value +} + +export const qortexSubmodule = { + name: 'qortex', + init, + getBidRequestData +} + +submodule('realTimeData', qortexSubmodule); diff --git a/modules/qortexRtdProvider.md b/modules/qortexRtdProvider.md new file mode 100644 index 00000000000..312696068cd --- /dev/null +++ b/modules/qortexRtdProvider.md @@ -0,0 +1,69 @@ +# Qortex Real-time Data Submodule + +## Overview + +``` +Module Name: Qortex RTD Provider +Module Type: RTD Provider +Maintainer: mannese@qortex.ai +``` + +## Description + +The Qortex RTD module appends contextual segments to the bidding object based on the content of a page using the Qortex API. + +Upon load, the Qortex context API will analyze the bidder page (video, text, image, etc.) and will return a [Content object](https://www.iab.com/wp-content/uploads/2016/03/OpenRTB-API-Specification-Version-2-5-FINAL.pdf#page=26). The module will then merge that object into the appropriate bidders' `ortb2.site.content`, which can be used by prebid adapters that use `site.content` data. + + +## Build +``` +gulp build --modules="rtdModule,qortexRtdProvider,qortexBidAdapter,..." +``` + +> `rtdModule` is a required module to use Qortex RTD module. + +## Configuration + +Please refer to [Prebid Documentation](https://docs.prebid.org/dev-docs/publisher-api-reference/setConfig.html#setConfig-realTimeData) on RTD module configuration for details on required and optional parameters of `realTimeData` + +When configuring Qortex as a data provider, refer to the template below to add the necessary information to ensure the proper connection is made. + +### RTD Module Setup + +```javascript +pbjs.setConfig({ + realTimeData: { + auctionDelay: 1000, + dataProviders: [{ + name: 'qortex', + waitForIt: true, + params: { + groupId: 'ABC123', //required + bidders: ['qortex', 'adapter2'], //optional (see below) + tagConfig: { // optional, please reach out to your account manager for configuration reccommendation + videoContainer: 'string', + htmlContainer: 'string', + attachToTop: 'string', + esm6Mod: 'string', + continuousLoad: 'string' + } + } + }] + } +}); +``` + +### Paramter Details + +#### `groupId` - Required +- The Qortex groupId linked to the publisher, this is required to make a request using this adapter + +#### `bidders` - optional +- If this parameter is included, it must be an array of the strings that match the bidder code of the prebid adapters you would like this module to impact. `ortb2.site.content` will be updated *only* for adapters in this array + +- If this parameter is omitted, the RTD module will default to updating `ortb2.site.content` on *all* bid adapters being used on the page + +#### `tagConfig` - optional +- This optional parameter is an object containing the config settings that could be usedto initialize the Qortex integration on your page. A preconfigured object for this step will be provided to you by the Qortex team. + +- If this parameter is not present, the Qortex integration can still be configured and loaded manually on your page outside of prebid. The RTD module will continue to initialize and operate as normal. \ No newline at end of file diff --git a/modules/quantcastBidAdapter.js b/modules/quantcastBidAdapter.js index 2c721a61616..1ba23302367 100644 --- a/modules/quantcastBidAdapter.js +++ b/modules/quantcastBidAdapter.js @@ -6,6 +6,11 @@ import {registerBidder} from '../src/adapters/bidderFactory.js'; import {find} from '../src/polyfill.js'; import {parseDomain} from '../src/refererDetection.js'; +/** + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid + */ + const BIDDER_CODE = 'quantcast'; const DEFAULT_BID_FLOOR = 0.0000000001; diff --git a/modules/quantcastIdSystem.js b/modules/quantcastIdSystem.js index 6a07082b61c..d980f5316e5 100644 --- a/modules/quantcastIdSystem.js +++ b/modules/quantcastIdSystem.js @@ -11,6 +11,10 @@ import { triggerPixel, logInfo } from '../src/utils.js'; import { uspDataHandler, coppaDataHandler, gdprDataHandler } from '../src/adapterManager.js'; import {MODULE_TYPE_UID} from '../src/activities/modules.js'; +/** + * @typedef {import('../modules/userId/index.js').Submodule} Submodule + */ + const QUANTCAST_FPA = '__qca'; const DEFAULT_COOKIE_EXP_DAYS = 392; // (13 months - 2 days) const DAY_MS = 86400000; @@ -214,6 +218,12 @@ export const quantcastIdSubmodule = { } return { id: fpa ? { quantcastId: fpa } : undefined }; + }, + eids: { + 'quantcastId': { + source: 'quantcast.com', + atype: 1 + }, } }; diff --git a/modules/r2b2BidAdapter.js b/modules/r2b2BidAdapter.js new file mode 100644 index 00000000000..15a65e3924c --- /dev/null +++ b/modules/r2b2BidAdapter.js @@ -0,0 +1,309 @@ +import {logWarn, logError, triggerPixel, deepSetValue, getParameterByName} from '../src/utils.js'; +import {ortbConverter} from '../libraries/ortbConverter/converter.js' +import {registerBidder} from '../src/adapters/bidderFactory.js'; +import {Renderer} from '../src/Renderer.js'; +import {BANNER, VIDEO, NATIVE} from '../src/mediaTypes.js'; +import {pbsExtensions} from '../libraries/pbsExtensions/pbsExtensions.js'; +import {bidderSettings} from '../src/bidderSettings.js'; + +const ADAPTER_VERSION = '1.0.0'; +const BIDDER_CODE = 'r2b2'; +const GVL_ID = 1235; + +const DEFAULT_CURRENCY = 'USD'; +const DEFAULT_TTL = 360; +const DEFAULT_NET_REVENUE = true; +const DEBUG_PARAM = 'pbjs_test_r2b2'; +const RENDERER_URL = 'https://delivery.r2b2.io/static/rendering.js'; + +const ENDPOINT = bidderSettings.get(BIDDER_CODE, 'endpoint') || 'hb.r2b2.cz'; +const SERVER_URL = 'https://' + ENDPOINT; +const URL_BID = SERVER_URL + '/openrtb2/bid'; +const URL_SYNC = SERVER_URL + '/cookieSync'; +const URL_EVENT = SERVER_URL + '/event'; + +const URL_EVENT_ON_BIDDER_ERROR = URL_EVENT + '/bidError'; +const URL_EVENT_ON_TIMEOUT = URL_EVENT + '/timeout'; + +const R2B2_TEST_UNIT = 'selfpromo'; + +export const internal = { + placementsToSync: [], + mappedParams: {} +} + +let r2b2Error = function(message, params) { + logError(message, params, BIDDER_CODE) +} + +function getIdParamsFromPID(pid) { + // selfpromo test creative + if (pid === R2B2_TEST_UNIT) { + return { d: 'test', g: 'test', p: 'selfpromo', m: 0, selfpromo: 1 } + } + if (!isNaN(pid)) { + return { pid: Number(pid) } + } + if (typeof pid === 'string') { + const params = pid.split('/'); + if (params.length === 3 || params.length === 4) { + const paramNames = ['d', 'g', 'p', 'm']; + return paramNames.reduce((p, paramName, index) => { + let param = params[index]; + if (paramName === 'm') { + param = ['desktop', 'classic', '0'].includes(param) ? 0 : Number(!!param) + } + p[paramName] = param; + return p + }, {}); + } + } +} + +function pickIdFromParams(params) { + if (!params) return null; + const { d, g, p, m, pid } = params; + return d ? { d, g, p, m } : { pid }; +} + +function getIdsFromBids(bids) { + return bids.reduce((ids, bid) => { + const params = internal.mappedParams[bid.bidId]; + const id = pickIdFromParams(params); + if (id) { + ids.push(id); + } + return ids + }, []); +} + +function triggerEvent(eventUrl, ids) { + if (ids && !ids.length) return; + const timeStamp = new Date().getTime(); + const symbol = (eventUrl.indexOf('?') === -1 ? '?' : '&'); + const url = eventUrl + symbol + `p=${btoa(JSON.stringify(ids))}&cb=${timeStamp}`; + triggerPixel(url) +} + +const converter = ortbConverter({ + imp(buildImp, bidRequest, context) { + const imp = buildImp(bidRequest, context); + const idParams = getIdParamsFromPID(bidRequest.params.pid); + deepSetValue(imp, 'ext.r2b2', idParams); + internal.placementsToSync.push(idParams); + internal.mappedParams[imp.id] = Object.assign({}, bidRequest.params, idParams); + return imp; + }, + request(buildRequest, imps, bidderRequest, context) { + const request = buildRequest(imps, bidderRequest, context); + deepSetValue(request, 'ext.version', ADAPTER_VERSION); + request.cur = [DEFAULT_CURRENCY]; + const test = getParameterByName(DEBUG_PARAM) === '1' ? 1 : 0; + deepSetValue(request, 'test', test); + return request; + }, + context: { + netRevenue: DEFAULT_NET_REVENUE, + ttl: DEFAULT_TTL + }, + processors: pbsExtensions +}); + +function setUpRenderer(adUnitCode, bid) { + // let renderer load once in main window, but pass the renderDocument + let renderDoc; + const config = { + documentResolver: (bid, sourceDocument, renderDocument) => { + renderDoc = renderDocument; + return sourceDocument; + } + } + let renderer = Renderer.install({ + url: RENDERER_URL, + config: config, + id: bid.requestId, + adUnitCode + }); + + renderer.setRender(function (bid, doc) { + doc = renderDoc || doc; + window.R2B2 = window.R2B2 || {}; + let main = window.R2B2; + main.HB = main.HB || {}; + main.HB.Render = main.HB.Render || {}; + main.HB.Render.queue = main.HB.Render.queue || []; + main.HB.Render.queue.push(() => { + const id = pickIdFromParams(internal.mappedParams[bid.requestId]) + main.HB.Renderer.render(id, bid, null, doc) + }) + }) + + return renderer +} + +function getExtMediaType(bidMediaType, responseBid) { + switch (bidMediaType) { + case BANNER: + return { + type: 'banner', + settings: { + chd: null, + width: responseBid.w, + height: responseBid.h, + ad: { + type: 'content', + data: responseBid.adm + } + } + }; + case NATIVE: + break; + case VIDEO: + break; + default: + break; + } +} + +function createPrebidResponseBid(requestImp, bidResponse, serverResponse, bids) { + const bidId = requestImp.id; + const adUnitCode = bids[0].adUnitCode; + const mediaType = bidResponse.ext.prebid.type; + let bidOut = { + requestId: bidId, + cpm: bidResponse.price, + creativeId: bidResponse.crid, + width: bidResponse.w, + height: bidResponse.h, + ttl: bidResponse.ttl ?? DEFAULT_TTL, + netRevenue: serverResponse.netRevenue ?? DEFAULT_NET_REVENUE, + currency: serverResponse.cur ?? DEFAULT_CURRENCY, + ad: bidResponse.adm, + mediaType: mediaType, + winUrl: bidResponse.nurl, + ext: { + cid: bidResponse.ext?.r2b2?.cid, + cdid: bidResponse.ext?.r2b2?.cdid, + mediaType: getExtMediaType(mediaType, bidResponse), + adUnit: adUnitCode, + dgpm: internal.mappedParams[bidId], + events: bidResponse.ext?.r2b2?.events + } + }; + if (bidResponse.ext?.r2b2?.useRenderer) { + bidOut.renderer = setUpRenderer(adUnitCode, bidOut); + } + return bidOut; +} + +export const spec = { + code: BIDDER_CODE, + gvlid: GVL_ID, + supportedMediaTypes: [BANNER], + + isBidRequestValid: function(bid) { + if (!bid.params || !bid.params.pid) { + logWarn('Bad params, "pid" required.'); + return false + } + const id = getIdParamsFromPID(bid.params.pid); + if (!id || !(id.pid || (id.d && id.g && id.p))) { + logWarn('Bad params, "pid" has to be either a number or a correctly assembled string.'); + return false + } + return true + }, + buildRequests: function(validBidRequests, bidderRequest) { + const data = converter.toORTB({ + bidRequests: validBidRequests, + bidderRequest + }); + return [{ + method: 'POST', + url: URL_BID, + data, + bids: bidderRequest.bids + }] + }, + + interpretResponse: function(serverResponse, request) { + // r2b2Error('error message', {params: 1}); + let prebidResponses = []; + + const response = serverResponse.body; + if (!response || !response.seatbid || !response.seatbid[0] || !response.seatbid[0].bid) { + return prebidResponses; + } + let requestImps = request.data.imp || []; + try { + response.seatbid.forEach(seat => { + let bids = seat.bid; + + for (let responseBid of bids) { + let responseImpId = responseBid.impid; + let requestCurrentImp = requestImps.find((requestImp) => requestImp.id === responseImpId); + if (!requestCurrentImp) { + r2b2Error('Cant match bid response.', {impid: Boolean(responseBid.impid)}); + continue;// Skip this iteration if there's no match + } + prebidResponses.push(createPrebidResponseBid(requestCurrentImp, responseBid, response, request.bids)); + } + }) + } catch (e) { + r2b2Error('Error while interpreting response:', {msg: e.message}); + } + return prebidResponses; + }, + getUserSyncs: function(syncOptions, serverResponses, gdprConsent, uspConsent) { + const syncs = []; + + if (!syncOptions.iframeEnabled) { + logWarn('Please enable iframe based user sync.'); + return syncs; + } + + let plString; + try { + plString = btoa(JSON.stringify(internal.placementsToSync || [])); + } catch (e) { + logWarn('User sync failed: ' + e.message); + return syncs + } + + let url = URL_SYNC + `?p=${plString}`; + + if (gdprConsent) { + url += `&gdpr=${Number(gdprConsent.gdprApplies)}&gdpr_consent=${gdprConsent.consentString}` + } + + if (uspConsent) { + url += `&us_privacy=${uspConsent}` + } + + syncs.push({ + type: 'iframe', + url: url + }) + return syncs; + }, + onBidWon: function(bid) { + const url = bid.ext?.events?.onBidWon; + if (url) { + triggerEvent(url) + } + }, + onSetTargeting: function(bid) { + const url = bid.ext?.events?.onSetTargeting; + if (url) { + triggerEvent(url) + } + }, + onTimeout: function(bids) { + triggerEvent(URL_EVENT_ON_TIMEOUT, getIdsFromBids(bids)) + }, + onBidderError: function(params) { + let { bidderRequest } = params; + triggerEvent(URL_EVENT_ON_BIDDER_ERROR, getIdsFromBids(bidderRequest.bids)) + } +} +registerBidder(spec); diff --git a/modules/r2b2BidAdapter.md b/modules/r2b2BidAdapter.md new file mode 100644 index 00000000000..43b59133215 --- /dev/null +++ b/modules/r2b2BidAdapter.md @@ -0,0 +1,37 @@ +# Overview + +``` +Module Name: R2B2 Bid Adapter +Module Type: Bidder Adapter +Maintainer: dev@r2b2.cz +``` + +## Description + +Module that integrates R2B2 demand sources. To get your bidder configuration reach out to our account team on partner@r2b2.io + + + +## Test unit + +```javascript + var adUnits = [ + { + code: 'test-r2b2', + mediaTypes: { + banner: { + sizes: [[300, 250], [300, 600]], + } + }, + bids: [{ + bidder: 'r2b2', + params: { + pid: 'selfpromo' + } + }] + } + ]; +``` +## Rendering + +Our adapter can feature a custom renderer specifically for display ads, tailored to enhance ad presentation and functionality. This is particularly beneficial for non-standard ad formats that require more complex logic. It's important to note that our rendering process operates outside of SafeFrames. For additional information, not limited to rendering aspects, please feel free to contact us at partner@r2b2.io diff --git a/modules/radsBidAdapter.js b/modules/radsBidAdapter.js index ae16bcf9d83..faa35ee51f7 100644 --- a/modules/radsBidAdapter.js +++ b/modules/radsBidAdapter.js @@ -2,6 +2,10 @@ import {deepAccess} from '../src/utils.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import {BANNER, VIDEO} from '../src/mediaTypes.js'; +/** + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + */ + const BIDDER_CODE = 'rads'; const ENDPOINT_URL = 'https://rads.recognified.net/md.request.php'; const ENDPOINT_URL_DEV = 'https://dcradn1.online-solution.biz/md.request.php'; diff --git a/modules/rasBidAdapter.js b/modules/rasBidAdapter.js index a7aceb107b9..4e93f2aa8eb 100644 --- a/modules/rasBidAdapter.js +++ b/modules/rasBidAdapter.js @@ -1,7 +1,8 @@ import { registerBidder } from '../src/adapters/bidderFactory.js'; import { BANNER } from '../src/mediaTypes.js'; -import { isEmpty, getAdUnitSizes, parseSizesInput, deepAccess } from '../src/utils.js'; +import { isEmpty, parseSizesInput, deepAccess } from '../src/utils.js'; import {getAllOrtbKeywords} from '../libraries/keywords/keywords.js'; +import {getAdUnitSizes} from '../libraries/sizeUtils/sizeUtils.js'; const BIDDER_CODE = 'ras'; const VERSION = '1.0'; @@ -129,6 +130,36 @@ const getGdprParams = (bidderRequest) => { return queryString; }; +const parseAuctionConfigs = (serverResponse, bidRequest) => { + if (isEmpty(bidRequest)) { + return null; + } + const auctionConfigs = []; + const gctx = serverResponse && serverResponse.body?.gctx; + + bidRequest.bidIds.filter(bid => bid.fledgeEnabled).forEach((bid) => { + auctionConfigs.push({ + 'bidId': bid.bidId, + 'config': { + 'seller': 'https://csr.onet.pl', + 'decisionLogicUrl': `https://csr.onet.pl/${encodeURIComponent(bid.params.network)}/v1/protected-audience-api/decision-logic.js`, + 'interestGroupBuyers': ['https://csr.onet.pl'], + 'auctionSignals': { + 'params': bid.params, + 'sizes': bid.sizes, + 'gctx': gctx + } + } + }); + }); + + if (auctionConfigs.length === 0) { + return null; + } else { + return auctionConfigs; + } +} + export const spec = { code: BIDDER_CODE, supportedMediaTypes: [BANNER], @@ -145,8 +176,16 @@ export const spec = { const slotsQuery = getSlots(bidRequests); const contextQuery = getContextParams(bidRequests, bidderRequest); const gdprQuery = getGdprParams(bidderRequest); - const bidIds = bidRequests.map((bid) => ({ slot: bid.params.slot, bidId: bid.bidId })); + const fledgeEligible = Boolean(bidderRequest && bidderRequest.fledgeEnabled); const network = bidRequests[0].params.network; + const bidIds = bidRequests.map((bid) => ({ + slot: bid.params.slot, + bidId: bid.bidId, + sizes: getAdUnitSizes(bid), + params: bid.params, + fledgeEnabled: fledgeEligible + })); + return [{ method: 'GET', url: getEndpoint(network) + contextQuery + slotsQuery + gdprQuery, @@ -156,10 +195,16 @@ export const spec = { interpretResponse: function (serverResponse, bidRequest) { const response = serverResponse.body; - if (!response || !response.ads || response.ads.length === 0) { - return []; + + const fledgeAuctionConfigs = parseAuctionConfigs(serverResponse, bidRequest); + const bids = (!response || !response.ads || response.ads.length === 0) ? [] : response.ads.map(buildBid).filter((bid) => !isEmpty(bid)); + + if (fledgeAuctionConfigs) { + // Return a tuple of bids and auctionConfigs. It is possible that bids could be null. + return {bids, fledgeAuctionConfigs}; + } else { + return bids; } - return response.ads.map(buildBid).filter((bid) => !isEmpty(bid)); } }; diff --git a/modules/reconciliationRtdProvider.js b/modules/reconciliationRtdProvider.js index 9b6a3d7aca3..5671b2021d8 100644 --- a/modules/reconciliationRtdProvider.js +++ b/modules/reconciliationRtdProvider.js @@ -21,6 +21,10 @@ import {ajaxBuilder} from '../src/ajax.js'; import {generateUUID, isGptPubadsDefined, logError, timestamp} from '../src/utils.js'; import {find} from '../src/polyfill.js'; +/** + * @typedef {import('../modules/rtdModule/index.js').RtdSubmodule} RtdSubmodule + */ + /** @type {Object} */ const MessageType = { IMPRESSION_REQUEST: 'rsdk:impression:req', diff --git a/modules/relaidoBidAdapter.js b/modules/relaidoBidAdapter.js index b2961b09eb5..751e8fa442c 100644 --- a/modules/relaidoBidAdapter.js +++ b/modules/relaidoBidAdapter.js @@ -1,4 +1,14 @@ -import { deepAccess, logWarn, getBidIdParameter, parseQueryStringParameters, triggerPixel, generateUUID, isArray, isNumber, parseSizesInput } from '../src/utils.js'; +import { + deepAccess, + logWarn, + parseQueryStringParameters, + triggerPixel, + generateUUID, + isArray, + isNumber, + parseSizesInput, + getBidIdParameter +} from '../src/utils.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; import { BANNER, VIDEO } from '../src/mediaTypes.js'; import { Renderer } from '../src/Renderer.js'; @@ -94,7 +104,8 @@ function buildRequests(validBidRequests, bidderRequest) { width: width, height: height, banner_sizes: getBannerSizes(bidRequest), - media_type: mediaType + media_type: mediaType, + userIdAsEids: bidRequest.userIdAsEids || {}, }); } @@ -107,6 +118,7 @@ function buildRequests(validBidRequests, bidderRequest) { uuid: getUuid(), pv: '$prebid.version$', imuid: imuid, + canonical_url: bidderRequest.refererInfo?.canonicalUrl || null, canonical_url_hash: getCanonicalUrlHash(bidderRequest.refererInfo), ref: bidderRequest.refererInfo.page }); @@ -132,6 +144,7 @@ function interpretResponse(serverResponse, bidRequest) { const playerUrl = res.playerUrl || bidRequest.player || body.playerUrl; let bidResponse = { requestId: res.bidId, + placementId: res.placementId, width: res.width, height: res.height, cpm: res.price, diff --git a/modules/relayBidAdapter.js b/modules/relayBidAdapter.js new file mode 100644 index 00000000000..af145a5e163 --- /dev/null +++ b/modules/relayBidAdapter.js @@ -0,0 +1,99 @@ +import { isNumber, logMessage } from '../src/utils.js'; +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { config } from '../src/config.js'; +import { BANNER, VIDEO, NATIVE } from '../src/mediaTypes.js'; +import { ortbConverter } from '../libraries/ortbConverter/converter.js' + +const BIDDER_CODE = 'relay'; +const METHOD = 'POST'; +const ENDPOINT_URL = 'https://e.relay.bid/p/openrtb2'; + +// The default impl from the prebid docs. +const CONVERTER = + ortbConverter({ + context: { + netRevenue: true, + ttl: 30 + } + }); + +function buildRequests(bidRequests, bidderRequest) { + const prebidVersion = config.getConfig('prebid_version') || 'v8.1.0'; + // Group bids by accountId param + const groupedByAccountId = bidRequests.reduce((accu, item) => { + const accountId = ((item || {}).params || {}).accountId; + if (!accu[accountId]) { accu[accountId] = []; }; + accu[accountId].push(item); + return accu; + }, {}); + // Send one overall request with all grouped bids per accountId + let reqs = []; + for (const [accountId, accountBidRequests] of Object.entries(groupedByAccountId)) { + const url = `${ENDPOINT_URL}?a=${accountId}&pb=1&pbv=${prebidVersion}`; + const data = CONVERTER.toORTB({ bidRequests: accountBidRequests, bidderRequest }) + const req = { + method: METHOD, + url, + data + }; + reqs.push(req); + } + return reqs; +}; + +function interpretResponse(response, request) { + return CONVERTER.fromORTB({ response: response.body, request: request.data }).bids; +}; + +function isBidRequestValid(bid) { + return isNumber((bid.params || {}).accountId); +}; + +function getUserSyncs(syncOptions, serverResponses, gdprConsent, uspConsent) { + let syncs = [] + for (const response of serverResponses) { + const responseSyncs = ((((response || {}).body || {}).ext || {}).user_syncs || []) + // Relay returns user_syncs in the format expected by prebid. If for any + // reason the request/response failed to properly capture the GDPR settings + // -- fallback to those identified by Prebid. + for (const sync of responseSyncs) { + const syncUrl = new URL(sync.url); + const missingGdpr = !syncUrl.searchParams.has('gdpr'); + const missingGdprConsent = !syncUrl.searchParams.has('gdpr_consent'); + if (missingGdpr) { + syncUrl.searchParams.set('gdpr', Number(gdprConsent.gdprApplies)) + sync.url = syncUrl.toString(); + } + if (missingGdprConsent) { + syncUrl.searchParams.set('gdpr_consent', gdprConsent.consentString); + sync.url = syncUrl.toString(); + } + if (syncOptions.iframeEnabled && sync.type === 'iframe') { + syncs.push(sync); + } else if (syncOptions.pixelEnabled && sync.type === 'image') { + syncs.push(sync); + } + } + } + + return syncs; +} + +export const spec = { + code: BIDDER_CODE, + isBidRequestValid, + buildRequests, + interpretResponse, + getUserSyncs, + onTimeout: function (timeoutData) { + logMessage('Timeout: ', timeoutData); + }, + onBidWon: function (bid) { + logMessage('Bid won: ', bid); + }, + onBidderError: function ({ error, bidderRequest }) { + logMessage('Error: ', error, bidderRequest); + }, + supportedMediaTypes: [BANNER, VIDEO, NATIVE] +} +registerBidder(spec); diff --git a/modules/relayBidAdapter.md b/modules/relayBidAdapter.md new file mode 100644 index 00000000000..882e04b7b13 --- /dev/null +++ b/modules/relayBidAdapter.md @@ -0,0 +1,79 @@ +# Overview + +``` +Module Name: Relay Bid Adapter +Module Type: Bid Adapter +Maintainer: relay@kevel.co +``` + +# Description + +Connects to Relay exchange API for bids. +Supports Banner, Video and Native. + +# Test Parameters + +``` +var adUnits = [ + // Banner with minimal bid configuration + { + code: 'minimal', + mediaTypes: { + banner: { + sizes: [[300, 250]] + } + }, + bids: [ + { + bidder: 'relay', + params: { + accountId: 1234 + }, + ortb2imp: { + ext: { + relay: { + bidders: { + bidderA: { + param: 1234 + } + } + } + } + } + } + ] + }, + // Minimal video + { + code: 'video-minimal', + mediaTypes: { + video: { + maxduration: 30, + api: [1, 3], + mimes: ['video/mp4'], + placement: 3, + protocols: [2,3,5,6] + } + }, + bids: [ + { + bidder: 'relay', + params: { + accountId: 1234 + }, + ortb2imp: { + ext: { + relay: { + bidders: { + bidderA: { + param: 'example' + } + } + } + } + } + } + ] + } +]; +``` diff --git a/modules/relevadRtdProvider.js b/modules/relevadRtdProvider.js index a7d0305da62..613eaa71a1f 100644 --- a/modules/relevadRtdProvider.js +++ b/modules/relevadRtdProvider.js @@ -110,6 +110,7 @@ function composeOrtb2Data(rtdData, prefix) { !isEmpty(categories.cat) && deepSetValue(addOrtb2, prefix + '.cat', categories.cat); !isEmpty(categories.pagecat) && deepSetValue(addOrtb2, prefix + '.pagecat', categories.pagecat); !isEmpty(categories.sectioncat) && deepSetValue(addOrtb2, prefix + '.sectioncat', categories.sectioncat); + !isEmpty(categories.sectioncat) && deepSetValue(addOrtb2, prefix + '.ext.data.relevad_rtd', categories.sectioncat); !isEmpty(categories.cattax) && deepSetValue(addOrtb2, prefix + '.cattax', categories.cattax); if (!isEmpty(content) && !isEmpty(content.segs) && content.segtax) { @@ -134,7 +135,8 @@ function setBidderSiteAndContent(bidderOrtbFragment, bidder, rtdData) { try { let addOrtb2 = composeOrtb2Data(rtdData, 'site'); !isEmpty(rtdData.segments) && deepSetValue(addOrtb2, 'user.ext.data.relevad_rtd', rtdData.segments); - !isEmpty(rtdData.categories?.sectioncat) && deepSetValue(addOrtb2, 'site.ext.data.relevad_rtd', rtdData.categories.sectioncat); + !isEmpty(rtdData.segments) && deepSetValue(addOrtb2, 'user.ext.data.segments', rtdData.segments); + !isEmpty(rtdData.categories) && deepSetValue(addOrtb2, 'user.ext.data.contextual_categories', rtdData.categories.pagecat); if (isEmpty(addOrtb2)) { return; } @@ -180,7 +182,7 @@ function getFiltered(data, minscore) { const cats = filterByScore(data.cats, minscore); const pcats = filterByScore(data.pcats, minscore) || cats; const scats = filterByScore(data.scats, minscore) || pcats; - const cattax = data.cattax ? data.cattax : CATTAX_IAB; + const cattax = (data.cattax || data.cattax === undefined) ? data.cattax : CATTAX_IAB; relevadData.categories = {cat: cats, pagecat: pcats, sectioncat: scats, cattax: cattax}; const contsegs = filterByScore(data.contsegs, minscore); @@ -268,15 +270,12 @@ export function addRtdData(reqBids, data, moduleConfig) { } if (wb && !isEmpty(relevadList)) { setBidderSiteAndContent(reqBids.ortb2Fragments?.bidder, bid.bidder, relevadData); + setBidderSiteAndContent(bid, 'ortb2', relevadData); deepSetValue(bid, 'params.keywords.relevad_rtd', relevadList); - deepSetValue(bid, 'params.target', [].concat(bid.params?.target ? [bid.params.target] : []).concat(relevadList.map(entry => { return 'relevad_rtd=' + entry; })).join(';')); + !(bid.params?.target || '').includes('relevad_rtd=') && deepSetValue(bid, 'params.target', [].concat(bid.params?.target ? [bid.params.target] : []).concat(relevadList.map(entry => { return 'relevad_rtd=' + entry; })).join(';')); let firstPartyData = {}; firstPartyData[bid.bidder] = { firstPartyData: { relevad_rtd: relevadList } }; config.setConfig(firstPartyData); - !isEmpty(relevadData.segments) && deepSetValue(bid, 'ortb2.user.ext.data.segments', relevadData.segments); - !isEmpty(relevadData.categories) && deepSetValue(bid, 'ortb2.user.ext.data.contextual_categories', relevadData.categories.pagecat); - !isEmpty(relevadData.categories) && deepSetValue(bid, 'ortb2.site.ext.data.relevad_rtd', relevadData.categories.pagecat); - !isEmpty(relevadData.segments) && deepSetValue(bid, 'ortb2.user.ext.data.relevad_rtd', relevadData.segments); } } } catch (e) { diff --git a/modules/relevantdigitalBidAdapter.js b/modules/relevantdigitalBidAdapter.js index ad9ee5e1e14..8d1265075f9 100644 --- a/modules/relevantdigitalBidAdapter.js +++ b/modules/relevantdigitalBidAdapter.js @@ -94,11 +94,18 @@ export const spec = { gvlid: 1100, supportedMediaTypes: [BANNER, VIDEO, NATIVE], - /** We need both params.placementId + a complete configuration (pbsHost + accountId) to continue **/ + /** We need both params.placementId + a complete configuration (pbsHost + accountId) to continue */ isBidRequestValid: (bid) => bid.params?.placementId && getBidderConfig([bid]).complete, /** Trigger impression-pixel */ - onBidWon: ({pbsWurl}) => pbsWurl && triggerPixel(pbsWurl), + onBidWon(bid) { + if (bid.pbsWurl) { + triggerPixel(bid.pbsWurl) + } + if (bid.burl) { + triggerPixel(bid.burl) + } + }, /** Build BidRequest for PBS */ buildRequests(bidRequests, bidderRequest) { @@ -193,6 +200,24 @@ export const spec = { }); return syncs; }, + + /** If server side, transform bid params if needed */ + transformBidParams(params, isOrtb, adUnit, bidRequests) { + if (!params.placementId) { + return; + } + const bid = bidRequests.flatMap(req => req.adUnitsS2SCopy || []).flatMap((adUnit) => adUnit.bids).find((bid) => bid.params?.placementId === params.placementId); + if (!bid) { + return; + } + const cfg = getBidderConfig([bid]); + FIELDS.forEach(({ name }) => { + if (cfg[name] && !params[name]) { + params[name] = cfg[name]; + } + }); + return params; + }, }; registerBidder(spec); diff --git a/modules/resetdigitalBidAdapter.js b/modules/resetdigitalBidAdapter.js index 8264e0cc9cc..2bac6c6dcba 100644 --- a/modules/resetdigitalBidAdapter.js +++ b/modules/resetdigitalBidAdapter.js @@ -1,25 +1,29 @@ import { timestamp, deepAccess, isStr, deepClone } from '../src/utils.js'; import { getOrigin } from '../libraries/getOrigin/index.js'; import { config } from '../src/config.js'; -import {registerBidder} from '../src/adapters/bidderFactory.js'; -import {BANNER} from '../src/mediaTypes.js'; +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { BANNER } from '../src/mediaTypes.js'; const BIDDER_CODE = 'resetdigital'; const CURRENCY = 'USD'; export const spec = { code: BIDDER_CODE, - supportedMediaTypes: [ 'banner', 'video' ], - isBidRequestValid: function(bid) { - return (!!(bid.params.pubId || bid.params.zoneId)); + supportedMediaTypes: ['banner', 'video'], + isBidRequestValid: function (bid) { + return !!(bid.params.pubId || bid.params.zoneId); }, - buildRequests: function(validBidRequests, bidderRequest) { - let stack = (bidderRequest.refererInfo && - bidderRequest.refererInfo.stack ? bidderRequest.refererInfo.stack - : []) - - let spb = (config.getConfig('userSync') && config.getConfig('userSync').syncsPerBidder) - ? config.getConfig('userSync').syncsPerBidder : 5 + buildRequests: function (validBidRequests, bidderRequest) { + let stack = + bidderRequest.refererInfo && bidderRequest.refererInfo.stack + ? bidderRequest.refererInfo.stack + : []; + + let spb = + config.getConfig('userSync') && + config.getConfig('userSync').syncsPerBidder + ? config.getConfig('userSync').syncsPerBidder + : 5; const payload = { start_time: timestamp(), @@ -29,19 +33,19 @@ export const spec = { iframe: !bidderRequest.refererInfo.reachedTop, // TODO: the last element in refererInfo.stack is window.location.href, that's unlikely to have been the intent here url: stack && stack.length > 0 ? [stack.length - 1] : null, - https: (window.location.protocol === 'https:'), + https: window.location.protocol === 'https:', // TODO: is 'page' the right value here? - referrer: bidderRequest.refererInfo.page + referrer: bidderRequest.refererInfo.page, }, imps: [], user_ids: validBidRequests[0].userId, - sync_limit: spb + sync_limit: spb, }; if (bidderRequest && bidderRequest.gdprConsent) { payload.gdpr = { applies: bidderRequest.gdprConsent.gdprApplies, - consent: bidderRequest.gdprConsent.consentString + consent: bidderRequest.gdprConsent.consentString, }; } @@ -50,10 +54,16 @@ export const spec = { } function getOrtb2Keywords(ortb2Obj) { - const fields = ['site.keywords', 'site.content.keywords', 'user.keywords', 'app.keywords', 'app.content.keywords']; + const fields = [ + 'site.keywords', + 'site.content.keywords', + 'user.keywords', + 'app.keywords', + 'app.content.keywords', + ]; let result = []; - fields.forEach(path => { + fields.forEach((path) => { let keyStr = deepAccess(ortb2Obj, path); if (isStr(keyStr)) result.push(keyStr); }); @@ -79,18 +89,26 @@ export const spec = { const floorInfo = req.getFloor({ currency: CURRENCY, mediaType: BANNER, - size: '*' + size: '*', }); - if (typeof floorInfo === 'object' && floorInfo.currency === CURRENCY && !isNaN(parseFloat(floorInfo.floor))) { + if ( + typeof floorInfo === 'object' && + floorInfo.currency === CURRENCY && + !isNaN(parseFloat(floorInfo.floor)) + ) { bidFloor = parseFloat(floorInfo.floor); bidFloorCur = CURRENCY; } } // get param kewords (if it exists) - let paramsKeywords = req.params.keywords ? req.params.keywords.split(',') : []; + let paramsKeywords = req.params.keywords + ? req.params.keywords.split(',') + : []; // merge all keywords - let keywords = ortb2KeywordsList.concat(paramsKeywords).concat(metaKeywords); + let keywords = ortb2KeywordsList + .concat(paramsKeywords) + .concat(metaKeywords); payload.imps.push({ pub_id: req.params.pubId, @@ -110,32 +128,32 @@ export const spec = { sizes: req.sizes, force_bid: req.params.forceBid, coppa: config.getConfig('coppa') === true ? 1 : 0, - media_types: deepAccess(req, 'mediaTypes') + media_types: deepAccess(req, 'mediaTypes'), }); } - let params = validBidRequests[0].params - let url = params.endpoint ? params.endpoint : '//ads.resetsrv.com' + let params = validBidRequests[0].params; + let url = params.endpoint ? params.endpoint : '//ads.resetsrv.com'; return { method: 'POST', url: url, data: JSON.stringify(payload), - bids: validBidRequests + bids: validBidRequests, }; }, - interpretResponse: function(serverResponse, bidRequest) { + interpretResponse: function (serverResponse, bidRequest) { const bidResponses = []; if (!serverResponse || !serverResponse.body) { - return bidResponses + return bidResponses; } let res = serverResponse.body; if (!res.bids || !res.bids.length) { - return [] + return []; } for (let x = 0; x < serverResponse.body.bids.length; x++) { - let bid = serverResponse.body.bids[x] + let bid = serverResponse.body.bids[x]; bidResponses.push({ requestId: bid.bid_id, @@ -152,47 +170,45 @@ export const spec = { netRevenue: true, currency: 'USD', meta: { - advertiserDomains: bid.adomain - } - }) + advertiserDomains: bid.adomain, + }, + }); } return bidResponses; }, - getUserSyncs: function(syncOptions, serverResponses, gdprConsent) { - const syncs = [] + getUserSyncs: function (syncOptions, serverResponses, gdprConsent) { + const syncs = []; if (!serverResponses.length || !serverResponses[0].body) { - return syncs + return syncs; } - let pixels = serverResponses[0].body.pixels + let pixels = serverResponses[0].body.pixels; if (!pixels || !pixels.length) { - return syncs + return syncs; } - let gdprParams = null + let gdprParams = ''; if (gdprConsent) { if (typeof gdprConsent.gdprApplies === 'boolean') { - gdprParams = `gdpr=${Number(gdprConsent.gdprApplies)}&gdpr_consent=${gdprConsent.consentString}` + gdprParams = `gdpr=${Number(gdprConsent.gdprApplies)}&gdpr_consent=${ + gdprConsent.consentString + }`; } else { - gdprParams = `gdpr_consent=${gdprConsent.consentString}` + gdprParams = `gdpr_consent=${gdprConsent.consentString}`; } } - for (let x = 0; x < pixels.length; x++) { - let pixel = pixels[x] - - if ((pixel.type === 'iframe' && syncOptions.iframeEnabled) || - (pixel.type === 'image' && syncOptions.pixelEnabled)) { - if (gdprParams && gdprParams.length) { - pixel = (pixel.indexOf('?') === -1 ? '?' : '&') + gdprParams - } - syncs.push(pixel) - } + if ((syncOptions.iframeEnabled || syncOptions.pixelEnabled)) { + return [ + { + type: 'iframe', + url: 'https://media.reset-digital.com/prebid/async_usersync.html?' + gdprParams.length ? gdprParams : '', + }, + ]; } - return syncs; - } + }, }; registerBidder(spec); diff --git a/modules/retailspotBidAdapter.js b/modules/retailspotBidAdapter.js index 616b638e840..557dd617274 100644 --- a/modules/retailspotBidAdapter.js +++ b/modules/retailspotBidAdapter.js @@ -2,6 +2,11 @@ import {buildUrl, deepAccess, parseSizesInput} from '../src/utils.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import {BANNER, VIDEO} from '../src/mediaTypes.js'; +/** + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid + */ + const BIDDER_CODE = 'retailspot'; const DEFAULT_SUBDOMAIN = 'ssp'; const PREPROD_SUBDOMAIN = 'ssp-preprod'; @@ -28,7 +33,7 @@ export const spec = { /** * Make a server request from the list of BidRequests. * - * @param {bidRequests} - bidRequests.bids[] is an array of AdUnits and bids + * @param {BidRequests} - bidRequests.bids[] is an array of AdUnits and bids * @return ServerRequest Info describing the request to the server. */ buildRequests: function (bidRequests, bidderRequest) { diff --git a/modules/revcontentBidAdapter.js b/modules/revcontentBidAdapter.js index 5bf7dd691e7..f1d5521f780 100644 --- a/modules/revcontentBidAdapter.js +++ b/modules/revcontentBidAdapter.js @@ -3,9 +3,10 @@ import {registerBidder} from '../src/adapters/bidderFactory.js'; import {BANNER, NATIVE} from '../src/mediaTypes.js'; -import {_map, deepAccess, getAdUnitSizes, isFn, parseGPTSingleSizeArrayToRtbSize, triggerPixel} from '../src/utils.js'; +import {_map, deepAccess, isFn, parseGPTSingleSizeArrayToRtbSize, triggerPixel} from '../src/utils.js'; import {parseDomain} from '../src/refererDetection.js'; import {convertOrtbRequestToProprietaryNative} from '../src/native.js'; +import {getAdUnitSizes} from '../libraries/sizeUtils/sizeUtils.js'; const BIDDER_CODE = 'revcontent'; const NATIVE_PARAMS = { diff --git a/modules/richaudienceBidAdapter.js b/modules/richaudienceBidAdapter.js index 1625912ddb8..36513aeda47 100755 --- a/modules/richaudienceBidAdapter.js +++ b/modules/richaudienceBidAdapter.js @@ -1,4 +1,4 @@ -import {deepAccess, isStr} from '../src/utils.js'; +import {deepAccess, isStr, triggerPixel} from '../src/utils.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import {config} from '../src/config.js'; import {BANNER, VIDEO} from '../src/mediaTypes.js'; @@ -183,6 +183,13 @@ export const spec = { } return syncs }, + + onTimeout: function (data) { + let url = raiGetTimeoutURL(data); + if (url) { + triggerPixel(url); + } + } }; registerBidder(spec); @@ -332,3 +339,15 @@ function raiGetFloor(bid, config) { return 0 } } + +function raiGetTimeoutURL(data) { + let {params, timeout} = data[0] + let url = 'https://s.richaudience.com/err/?ec=6&ev=[timeout_publisher]&pla=[placement_hash]&int=PREBID&pltfm=&node=&dm=[domain]'; + + url = url.replace('[timeout_publisher]', timeout) + url = url.replace('[placement_hash]', params[0].pid) + if (REFERER != null) { + url = url.replace('[domain]', document.location.host) + } + return url +} diff --git a/modules/riseBidAdapter.js b/modules/riseBidAdapter.js index d5c6469db12..78854129f8a 100644 --- a/modules/riseBidAdapter.js +++ b/modules/riseBidAdapter.js @@ -1,4 +1,16 @@ -import { logWarn, logInfo, isArray, isFn, deepAccess, isEmpty, contains, timestamp, getBidIdParameter, triggerPixel, isInteger } from '../src/utils.js'; +import { + logWarn, + logInfo, + isArray, + isFn, + deepAccess, + isEmpty, + contains, + timestamp, + triggerPixel, + isInteger, + getBidIdParameter +} from '../src/utils.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import {BANNER, VIDEO} from '../src/mediaTypes.js'; import {config} from '../src/config.js'; @@ -7,7 +19,8 @@ const SUPPORTED_AD_TYPES = [BANNER, VIDEO]; const BIDDER_CODE = 'rise'; const ADAPTER_VERSION = '6.0.0'; const TTL = 360; -const CURRENCY = 'USD'; +const DEFAULT_CURRENCY = 'USD'; +const DEFAULT_GVLID = 1043; const DEFAULT_SELLER_ENDPOINT = 'https://hb.yellowblue.io/'; const MODES = { PRODUCTION: 'hb-multi', @@ -20,7 +33,11 @@ const SUPPORTED_SYNC_METHODS = { export const spec = { code: BIDDER_CODE, - gvlid: 1043, + aliases: [ + { code: 'risexchange', gvlid: DEFAULT_GVLID }, + { code: 'openwebxchange', gvlid: 280 } + ], + gvlid: DEFAULT_GVLID, version: ADAPTER_VERSION, supportedMediaTypes: SUPPORTED_AD_TYPES, isBidRequestValid: function (bidRequest) { @@ -61,7 +78,7 @@ export const spec = { const bidResponse = { requestId: adUnit.requestId, cpm: adUnit.cpm, - currency: adUnit.currency || CURRENCY, + currency: adUnit.currency || DEFAULT_CURRENCY, width: adUnit.width, height: adUnit.height, ttl: adUnit.ttl || TTL, @@ -128,18 +145,20 @@ registerBidder(spec); /** * Get floor price * @param bid {bid} + * @param mediaType {string} + * @param currency {string} * @returns {Number} */ -function getFloor(bid, mediaType) { +function getFloor(bid, mediaType, currency) { if (!isFn(bid.getFloor)) { return 0; } let floorResult = bid.getFloor({ - currency: CURRENCY, + currency: currency, mediaType: mediaType, size: '*' }); - return floorResult.currency === CURRENCY && floorResult.floor ? floorResult.floor : 0; + return floorResult.currency === currency && floorResult.floor ? floorResult.floor : 0; } /** @@ -277,7 +296,7 @@ function generateBidParameters(bid, bidderRequest) { const {params} = bid; const mediaType = isBanner(bid) ? BANNER : VIDEO; const sizesArray = getSizesArray(bid, mediaType); - + const currency = params.currency || config.getConfig('currency.adServerCurrency') || DEFAULT_CURRENCY; // fix floor price in case of NAN if (isNaN(params.floorPrice)) { params.floorPrice = 0; @@ -287,12 +306,13 @@ function generateBidParameters(bid, bidderRequest) { mediaType, adUnitCode: getBidIdParameter('adUnitCode', bid), sizes: sizesArray, - floorPrice: Math.max(getFloor(bid, mediaType), params.floorPrice), + currency: currency, + floorPrice: Math.max(getFloor(bid, mediaType, currency), params.floorPrice), bidId: getBidIdParameter('bidId', bid), bidderRequestId: getBidIdParameter('bidderRequestId', bid), loop: getBidIdParameter('bidderRequestsCount', bid), transactionId: bid.ortb2Imp?.ext?.tid, - coppa: 0 + coppa: 0, }; const pos = deepAccess(bid, `mediaTypes.${mediaType}.pos`); diff --git a/modules/riseBidAdapter.md b/modules/riseBidAdapter.md index f0837cb5508..ac0ea559c88 100644 --- a/modules/riseBidAdapter.md +++ b/modules/riseBidAdapter.md @@ -26,6 +26,7 @@ The adapter supports Video(instream). | `testMode` | optional | Boolean | This activates the test mode | false | `rtbDomain` | optional | String | Sets the seller end point | "www.test.com" | `is_wrapper` | private | Boolean | Please don't use unless your account manager asked you to | false +| `currency` | optional | String | 3 letters currency | "EUR" # Test Parameters diff --git a/modules/rtbhouseBidAdapter.js b/modules/rtbhouseBidAdapter.js index 5d687552201..5f94ec3dea4 100644 --- a/modules/rtbhouseBidAdapter.js +++ b/modules/rtbhouseBidAdapter.js @@ -10,6 +10,9 @@ const BIDDER_CODE = 'rtbhouse'; const REGIONS = ['prebid-eu', 'prebid-us', 'prebid-asia']; const ENDPOINT_URL = 'creativecdn.com/bidder/prebid/bids'; const FLEDGE_ENDPOINT_URL = 'creativecdn.com/bidder/prebidfledge/bids'; +const FLEDGE_SELLER_URL = 'https://fledge-ssp.creativecdn.com'; +const FLEDGE_DECISION_LOGIC_URL = 'https://fledge-ssp.creativecdn.com/component-seller-prebid.js'; + const DEFAULT_CURRENCY_ARR = ['USD']; // NOTE - USD is the only supported currency right now; Hardcoded for bids const SUPPORTED_MEDIA_TYPES = [BANNER, NATIVE]; const TTL = 55; @@ -94,8 +97,12 @@ export const spec = { let computedEndpointUrl = ENDPOINT_URL; - const fledgeConfig = config.getConfig('fledgeConfig'); - if (bidderRequest.fledgeEnabled && fledgeConfig) { + if (bidderRequest.fledgeEnabled) { + const fledgeConfig = config.getConfig('fledgeConfig') || { + seller: FLEDGE_SELLER_URL, + decisionLogicUrl: FLEDGE_DECISION_LOGIC_URL, + sellerTimeout: 500 + }; mergeDeep(request, { ext: { fledge_config: fledgeConfig } }); computedEndpointUrl = FLEDGE_ENDPOINT_URL; } @@ -188,7 +195,7 @@ registerBidder(spec); /** * @param {object} slot Ad Unit Params by Prebid - * @returns {int} floor by imp type + * @returns {number} floor by imp type */ function applyFloor(slot) { const floors = []; @@ -343,7 +350,7 @@ function mapNative(slot) { /** * @param {object} slot Slot config by Prebid - * @returns {array} Request Assets by OpenRTB Native Ads 1.1 §4.2 + * @returns {Array} Request Assets by OpenRTB Native Ads 1.1 §4.2 */ function mapNativeAssets(slot) { const params = slot.nativeParams || deepAccess(slot, 'mediaTypes.native'); @@ -406,7 +413,7 @@ function mapNativeAssets(slot) { /** * @param {object} image Prebid native.image/icon - * @param {int} type Image or icon code + * @param {number} type Image or icon code * @returns {object} Request Image by OpenRTB Native Ads 1.1 §4.4 */ function mapNativeImage(image, type) { diff --git a/modules/rtbhouseBidAdapter.md b/modules/rtbhouseBidAdapter.md index 31e9377cb96..338ba6b4df4 100644 --- a/modules/rtbhouseBidAdapter.md +++ b/modules/rtbhouseBidAdapter.md @@ -81,11 +81,30 @@ The following steps should be taken to setup Protected Audience for RTB House: module documentation. a. Make sure to enable RTB House bidder to participate in FLEDGE. If there are any other bidders to be allowed for that, add them to the **bidders** array: -```javascript -pbjs.setBidderConfig({ - bidders: ["rtbhouse"], - config: { - fledgeEnabled: true - } -}); -``` + ```javascript + pbjs.setBidderConfig({ + bidders: ["rtbhouse"], + config: { + fledgeEnabled: true + } + }); + ``` + + b. If you as a publisher have your own [decisionLogicUrl](https://github.com/WICG/turtledove/blob/main/FLEDGE.md#21-initiating-an-on-device-auction) + you may utilize it by setting up a dedicated `fledgeConfig` object: + ```javascript + pbjs.setBidderConfig({ + bidders: ["rtbhouse"], + config: { + fledgeEnabled: true, + fledgeConfig: { + seller: 'https://seller.domain', + decisionLogicUrl: 'https://seller.domain/decisionLogicFile.js', + sellerTimeout: 100 + } + } + }); + ``` + The `decisionLogicUrl` must be in the same domain as `seller` and has to respond with `X-Allow-FLEDGE: true` http header. + + `sellerTimeout` is optional, defaults to 50 as per spec, will be clamped to 500 if greater. diff --git a/modules/rtbsapeBidAdapter.js b/modules/rtbsapeBidAdapter.js index 5b1a92b02a0..502b62c8799 100644 --- a/modules/rtbsapeBidAdapter.js +++ b/modules/rtbsapeBidAdapter.js @@ -4,6 +4,14 @@ import {BANNER, VIDEO} from '../src/mediaTypes.js'; import {OUTSTREAM} from '../src/video.js'; import {Renderer} from '../src/Renderer.js'; +/** + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid + * @typedef {import('../src/adapters/bidderFactory.js').ServerResponse} ServerResponse + * @typedef {import('../src/adapters/bidderFactory.js').SyncOptions} SyncOptions + * @typedef {import('../src/adapters/bidderFactory.js').UserSync} UserSync + */ + const BIDDER_CODE = 'rtbsape'; const ENDPOINT = 'https://ssp-rtb.sape.ru/prebid'; const RENDERER_SRC = 'https://cdn-rtb.sape.ru/js/player.js'; diff --git a/modules/rtdModule/index.js b/modules/rtdModule/index.js index 633c4f4cdc1..8968d4c795a 100644 --- a/modules/rtdModule/index.js +++ b/modules/rtdModule/index.js @@ -215,7 +215,8 @@ const setEventsListeners = (function () { [CONSTANTS.EVENTS.AUCTION_INIT]: ['onAuctionInitEvent'], [CONSTANTS.EVENTS.AUCTION_END]: ['onAuctionEndEvent', getAdUnitTargeting], [CONSTANTS.EVENTS.BID_RESPONSE]: ['onBidResponseEvent'], - [CONSTANTS.EVENTS.BID_REQUESTED]: ['onBidRequestEvent'] + [CONSTANTS.EVENTS.BID_REQUESTED]: ['onBidRequestEvent'], + [CONSTANTS.EVENTS.BID_ACCEPTED]: ['onBidAcceptedEvent'] }).forEach(([ev, [handler, preprocess]]) => { events.on(ev, (args) => { preprocess && preprocess(args); @@ -385,7 +386,7 @@ export function getAdUnitTargeting(auction) { /** * deep merge array of objects - * @param {array} arr - objects array + * @param {Array} arr - objects array * @return {Object} merged object */ export function deepMerge(arr) { diff --git a/modules/rubiconBidAdapter.js b/modules/rubiconBidAdapter.js index 5fad246f27d..daaf9a14b9f 100644 --- a/modules/rubiconBidAdapter.js +++ b/modules/rubiconBidAdapter.js @@ -7,7 +7,6 @@ import { find } from '../src/polyfill.js'; import { getGlobal } from '../src/prebidGlobal.js'; import { Renderer } from '../src/Renderer.js'; import { - convertTypes, deepAccess, deepSetValue, formatQS, @@ -18,16 +17,22 @@ import { logMessage, logWarn, mergeDeep, - parseSizesInput, _each + parseSizesInput, + pick, + _each } from '../src/utils.js'; import {getAllOrtbKeywords} from '../libraries/keywords/keywords.js'; +/** + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + */ + const DEFAULT_INTEGRATION = 'pbjs_lite'; const DEFAULT_PBS_INTEGRATION = 'pbjs'; const DEFAULT_RENDERER_URL = 'https://video-outstream.rubiconproject.com/apex-2.2.1.js'; // renderer code at https://github.com/rubicon-project/apex2 -let rubiConf = {}; +let rubiConf = config.getConfig('rubicon') || {}; // we are saving these as global to this module so that if a pub accidentally overwrites the entire // rubicon object, then we do not lose other data config.getConfig('rubicon', config => { @@ -124,6 +129,7 @@ var sizeMap = { 278: '320x500', 282: '320x400', 288: '640x380', + 484: '720x1280', 524: '1x2', 548: '500x1000', 550: '980x480', @@ -139,7 +145,9 @@ var sizeMap = { 576: '610x877', 578: '980x552', 580: '505x656', - 622: '192x160' + 622: '192x160', + 632: '1200x450', + 634: '340x450' }; _each(sizeMap, (item, key) => sizeMap[item] = key); @@ -196,10 +204,14 @@ export const converter = ortbConverter({ if (config.getConfig('s2sConfig.defaultTtl')) { imp.exp = config.getConfig('s2sConfig.defaultTtl'); }; - bidRequest.params.position === 'atf' && (imp.video.pos = 1); - bidRequest.params.position === 'btf' && (imp.video.pos = 3); + bidRequest.params.position === 'atf' && imp.video && (imp.video.pos = 1); + bidRequest.params.position === 'btf' && imp.video && (imp.video.pos = 3); delete imp.ext?.prebid?.storedrequest; + if (bidRequest.params.bidonmultiformat === true && bidRequestType.length > 1) { + deepSetValue(imp, 'ext.prebid.bidder.rubicon.formats', bidRequestType); + } + setBidFloors(bidRequest, imp); return imp; @@ -318,7 +330,7 @@ export const spec = { ) ); }); - if (config.getConfig('rubicon.singleRequest') !== true) { + if (rubiConf.singleRequest !== true) { // bids are not grouped if single request mode is not enabled requests = filteredHttpRequest.concat(bannerBidRequests.map(bidRequest => { const bidParams = spec.createSlotParams(bidRequest, bidderRequest); @@ -383,6 +395,8 @@ export const spec = { 'gdpr', 'gdpr_consent', 'us_privacy', + 'gpp', + 'gpp_sid', 'rp_schain', ].concat(Object.keys(params).filter(item => containsUId.test(item))) .concat([ @@ -399,6 +413,8 @@ export const spec = { 'x_source.tid', 'l_pb_bid_id', 'p_screen_res', + 'o_ae', + 'o_cdep', 'rp_floor', 'rp_secure', 'tk_user_key' @@ -472,6 +488,7 @@ export const spec = { 'x_source.tid': bidderRequest.ortb2?.source?.tid, 'x_imp.ext.tid': bidRequest.ortb2Imp?.ext?.tid, 'l_pb_bid_id': bidRequest.bidId, + 'o_cdep': bidRequest.ortb2?.device?.ext?.cdep, 'p_screen_res': _getScreenResolution(), 'tk_user_key': params.userId, 'p_geo.latitude': isNaN(parseFloat(latitude)) ? undefined : parseFloat(latitude).toFixed(4), @@ -495,6 +512,11 @@ export const spec = { data['rp_hard_floor'] = typeof floorInfo === 'object' && floorInfo.currency === 'USD' && !isNaN(parseInt(floorInfo.floor)) ? floorInfo.floor : undefined; } + // Send multiformat data if requested + if (params.bidonmultiformat === true && deepAccess(bidRequest, 'mediaTypes') && Object.keys(bidRequest.mediaTypes).length > 1) { + data['p_formats'] = Object.keys(bidRequest.mediaTypes).join(','); + } + // add p_pos only if specified and valid // For SRA we need to explicitly put empty semi colons so AE treats it as empty, instead of copying the latter value let posMapping = {1: 'atf', 3: 'btf'}; @@ -506,6 +528,10 @@ export const spec = { if (configUserId) { data['ppuid'] = configUserId; } + + if (bidRequest?.ortb2Imp?.ext?.ae) { + data['o_ae'] = 1; + } // loop through userIds and add to request if (bidRequest.userIdAsEids) { bidRequest.userIdAsEids.forEach(eid => { @@ -526,7 +552,9 @@ export const spec = { data['eid_id5-sync.com'] = `${eid.uids[0].id}^${eid.uids[0].atype}^${(eid.uids[0].ext && eid.uids[0].ext.linkType) || ''}`; } else { // add anything else with this generic format - data[`eid_${eid.source}`] = `${eid.uids[0].id}^${eid.uids[0].atype || ''}`; + // if rubicon drop ^ + const id = eid.source === 'rubiconproject.com' ? eid.uids[0].id : `${eid.uids[0].id}^${eid.uids[0].atype || ''}` + data[`eid_${eid.source}`] = id; } // send AE "ppuid" signal if exists, and hasn't already been sent if (!data['ppuid']) { @@ -554,6 +582,11 @@ export const spec = { data['us_privacy'] = encodeURIComponent(bidderRequest.uspConsent); } + if (bidderRequest.gppConsent?.gppString) { + data['gpp'] = bidderRequest.gppConsent.gppString; + data['gpp_sid'] = bidderRequest.gppConsent?.applicableSections?.toString(); + } + data['rp_maxbids'] = bidderRequest.bidLimit || 1; applyFPD(bidRequest, BANNER, data); @@ -598,7 +631,7 @@ export const spec = { * @param {*} responseObj * @param {BidRequest|Object.} request - if request was SRA the bidRequest argument will be a keyed BidRequest array object, * non-SRA responses return a plain BidRequest object - * @return {Bid[]} An array of bids which + * @return {{fledgeAuctionConfigs: *, bids: *}} An array of bids which */ interpretResponse: function (responseObj, request) { responseObj = responseObj.body; @@ -608,7 +641,6 @@ export const spec = { if (!responseObj || typeof responseObj !== 'object') { return []; } - // Response from PBS Java openRTB if (responseObj.seatbid) { const responseErrors = deepAccess(responseObj, 'ext.errors.rubicon'); @@ -634,7 +666,7 @@ export const spec = { return []; } - return ads.reduce((bids, ad, i) => { + let bids = ads.reduce((bids, ad, i) => { (ad.impression_id && lastImpId === ad.impression_id) ? multibid++ : lastImpId = ad.impression_id; if (ad.status !== 'ok') { @@ -695,8 +727,18 @@ export const spec = { }, []).sort((adA, adB) => { return (adB.cpm || 0.0) - (adA.cpm || 0.0); }); + + let fledgeAuctionConfigs = responseObj.component_auction_config?.map(config => { + return { config, bidId: config.bidId } + }); + + if (fledgeAuctionConfigs) { + return { bids, fledgeAuctionConfigs }; + } else { + return bids; + } }, - getUserSyncs: function (syncOptions, responses, gdprConsent, uspConsent) { + getUserSyncs: function (syncOptions, responses, gdprConsent, uspConsent, gppConsent) { if (!hasSynced && syncOptions.iframeEnabled) { // data is only assigned if params are available to pass to syncEndpoint let params = {}; @@ -714,6 +756,11 @@ export const spec = { params['us_privacy'] = encodeURIComponent(uspConsent); } + if (gppConsent?.gppString) { + params['gpp'] = gppConsent.gppString; + params['gpp_sid'] = gppConsent.applicableSections?.toString(); + } + params = Object.keys(params).length ? `?${formatQS(params)}` : ''; hasSynced = true; @@ -722,19 +769,6 @@ export const spec = { url: `https://${rubiConf.syncHost || 'eus'}.rubiconproject.com/usync.html` + params }; } - }, - /** - * Covert bid param types for S2S - * @param {Object} params bid params - * @param {Boolean} isOpenRtb boolean to check openrtb2 protocol - * @return {Object} params bid params - */ - transformBidParams: function(params, isOpenRtb) { - return convertTypes({ - 'accountId': 'number', - 'siteId': 'number', - 'zoneId': 'number' - }, params); } }; @@ -931,6 +965,33 @@ function applyFPD(bidRequest, mediaType, data) { if (data['tg_i.pbadslot']) { delete data['tg_i.dfp_ad_unit_code']; } + + // High Entropy stuff -> sua object is the ORTB standard (default to pass unless specifically disabled) + const clientHints = deepAccess(fpd, 'device.sua'); + if (clientHints && rubiConf.chEnabled !== false) { + // pick out client hints we want to send (any that are undefined or empty will NOT be sent) + pick(clientHints, [ + 'architecture', arch => data.m_ch_arch = arch, + 'bitness', bitness => data.m_ch_bitness = bitness, + 'browsers', browsers => { + if (!Array.isArray(browsers)) return; + // reduce down into ua and full version list attributes + const [ua, fullVer] = browsers.reduce((accum, browserData) => { + accum[0].push(`"${browserData?.brand}"|v="${browserData?.version?.[0]}"`); + accum[1].push(`"${browserData?.brand}"|v="${browserData?.version?.join?.('.')}"`); + return accum; + }, [[], []]); + data.m_ch_ua = ua?.join?.(','); + data.m_ch_full_ver = fullVer?.join?.(','); + }, + 'mobile', isMobile => data.m_ch_mobile = `?${isMobile}`, + 'model', model => data.m_ch_model = model, + 'platform', platform => { + data.m_ch_platform = platform?.brand; + data.m_ch_platform_ver = platform?.version?.join?.('.'); + } + ]) + } } else { if (Object.keys(impExt).length) { mergeDeep(data.imp[0].ext, impExt); @@ -1112,8 +1173,7 @@ export function hasValidVideoParams(bid) { var requiredParams = { mimes: arrayType, protocols: arrayType, - linearity: numberType, - api: arrayType + linearity: numberType } // loop through each param and verify it has the correct Object.keys(requiredParams).forEach(function(param) { @@ -1128,7 +1188,7 @@ export function hasValidVideoParams(bid) { /** * Make sure the required params are present * @param {Object} schain - * @param {Bool} + * @param {boolean} */ export function hasValidSupplyChainParams(schain) { let isValid = false; diff --git a/modules/schain.js b/modules/schain.js index 2991bb5b3d5..726679b133f 100644 --- a/modules/schain.js +++ b/modules/schain.js @@ -1,16 +1,17 @@ -import { config } from '../src/config.js'; +import {config} from '../src/config.js'; import adapterManager from '../src/adapterManager.js'; import { - isNumber, - isStr, + _each, + deepAccess, + deepClone, + deepSetValue, isArray, + isInteger, + isNumber, isPlainObject, - hasOwn, + isStr, logError, - isInteger, - _each, - logWarn, - deepAccess, deepSetValue, deepClone + logWarn } from '../src/utils.js'; import {registerOrtbProcessor, REQUEST} from '../src/pbjsORTB.js'; @@ -63,7 +64,7 @@ export function isSchainObjectValid(schainObject, returnOnError) { } // ext: Object [optional] - if (hasOwn(schainObject, 'ext')) { + if (schainObject.hasOwnProperty('ext')) { if (!isPlainObject(schainObject.ext)) { appendFailMsg(`schain.config.ext` + shouldBeAnObject); } @@ -92,28 +93,28 @@ export function isSchainObjectValid(schainObject, returnOnError) { } // rid: String [Optional] - if (hasOwn(node, 'rid')) { + if (node.hasOwnProperty('rid')) { if (!isStr(node.rid)) { appendFailMsg(`schain.config.nodes[${index}].rid` + shouldBeAString); } } // name: String [Optional] - if (hasOwn(node, 'name')) { + if (node.hasOwnProperty('name')) { if (!isStr(node.name)) { appendFailMsg(`schain.config.nodes[${index}].name` + shouldBeAString); } } // domain: String [Optional] - if (hasOwn(node, 'domain')) { + if (node.hasOwnProperty('domain')) { if (!isStr(node.domain)) { appendFailMsg(`schain.config.nodes[${index}].domain` + shouldBeAString); } } // ext: Object [Optional] - if (hasOwn(node, 'ext')) { + if (node.hasOwnProperty('ext')) { if (!isPlainObject(node.ext)) { appendFailMsg(`schain.config.nodes[${index}].ext` + shouldBeAnObject); } diff --git a/modules/seedtagBidAdapter.js b/modules/seedtagBidAdapter.js index 7ac7d048c50..6f36c8a191e 100644 --- a/modules/seedtagBidAdapter.js +++ b/modules/seedtagBidAdapter.js @@ -3,6 +3,15 @@ import { registerBidder } from '../src/adapters/bidderFactory.js'; import { VIDEO, BANNER } from '../src/mediaTypes.js'; import { config } from '../src/config.js'; +/** + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid + * @typedef {import('../src/adapters/bidderFactory.js').ServerResponse} ServerResponse + * @typedef {import('../src/adapters/bidderFactory.js').SyncOptions} SyncOptions + * @typedef {import('../src/adapters/bidderFactory.js').UserSync} UserSync + * @typedef {import('../src/adapters/bidderFactory.js').validBidRequests} validBidRequests + */ + const BIDDER_CODE = 'seedtag'; const SEEDTAG_ALIAS = 'st'; const SEEDTAG_SSP_ENDPOINT = 'https://s.seedtag.com/c/hb/bid'; @@ -107,7 +116,6 @@ function buildBidRequest(validBidRequest) { return mediaTypesMap[pbjsType]; } ); - const bidRequest = { id: validBidRequest.bidId, transactionId: validBidRequest.ortb2Imp?.ext?.tid, @@ -115,6 +123,7 @@ function buildBidRequest(validBidRequest) { supplyTypes: mediaTypes, adUnitId: params.adUnitId, adUnitCode: validBidRequest.adUnitCode, + geom: geom(validBidRequest.adUnitCode), placement: params.placement, requestCount: validBidRequest.bidderRequestsCount || 1, // FIXME : in unit test the parameter bidderRequestsCount is undefined }; @@ -198,6 +207,27 @@ function ttfb() { return ttfb >= 0 && ttfb <= performance.now() ? ttfb : 0; } +function geom(adunitCode) { + const slot = document.getElementById(adunitCode); + if (slot) { + const scrollY = window.scrollY; + const { top, left, width, height } = slot.getBoundingClientRect(); + const viewport = { + width: window.innerWidth, + height: window.innerHeight, + }; + + return { + scrollY, + top, + left, + width, + height, + viewport, + }; + } +} + export function getTimeoutUrl(data) { let queryParams = ''; if ( diff --git a/modules/sharedIdSystem.js b/modules/sharedIdSystem.js index ef56e10870b..fa8b5e3bfdb 100644 --- a/modules/sharedIdSystem.js +++ b/modules/sharedIdSystem.js @@ -13,7 +13,15 @@ import {VENDORLESS_GVLID} from '../src/consentHandler.js'; import {MODULE_TYPE_UID} from '../src/activities/modules.js'; import {domainOverrideToRootDomain} from '../libraries/domainOverrideToRootDomain/index.js'; -export const storage = getStorageManager({moduleType: MODULE_TYPE_UID, moduleName: 'pubCommonId'}); +/** + * @typedef {import('../modules/userId/index.js').Submodule} Submodule + * @typedef {import('../modules/userId/index.js').SubmoduleConfig} SubmoduleConfig + * @typedef {import('../modules/userId/index.js').SubmoduleParams} SubmoduleParams + * @typedef {import('../modules/userId/index.js').ConsentData} ConsentData + * @typedef {import('../modules/userId/index.js').IdResponse} IdResponse + */ + +export const storage = getStorageManager({moduleType: MODULE_TYPE_UID, moduleName: 'sharedId'}); const COOKIE = 'cookie'; const LOCAL_STORAGE = 'html5'; const OPTOUT_NAME = '_pubcid_optout'; @@ -174,6 +182,12 @@ export const sharedIdSystemSubmodule = { }, domainOverride: domainOverrideToRootDomain(storage, 'sharedId'), + eids: { + 'pubcid': { + source: 'pubcid.org', + atype: 1 + }, + } }; submodule('userId', sharedIdSystemSubmodule); diff --git a/modules/sharethroughAnalyticsAdapter.js b/modules/sharethroughAnalyticsAdapter.js index 6502c7e3a53..dc621e8da92 100644 --- a/modules/sharethroughAnalyticsAdapter.js +++ b/modules/sharethroughAnalyticsAdapter.js @@ -1,6 +1,6 @@ -import { tryAppendQueryString } from '../src/utils.js'; import adapter from '../libraries/analyticsAdapter/AnalyticsAdapter.js'; import adapterManager from '../src/adapterManager.js'; +import {tryAppendQueryString} from '../libraries/urlUtils/urlUtils.js'; const emptyUrl = ''; const analyticsType = 'endpoint'; diff --git a/modules/sharethroughBidAdapter.js b/modules/sharethroughBidAdapter.js index d3f4b456aeb..53cb67c4e6d 100644 --- a/modules/sharethroughBidAdapter.js +++ b/modules/sharethroughBidAdapter.js @@ -1,7 +1,7 @@ -import { deepAccess, generateUUID, inIframe } from '../src/utils.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; import { config } from '../src/config.js'; import { BANNER, VIDEO } from '../src/mediaTypes.js'; +import { deepAccess, generateUUID, inIframe } from '../src/utils.js'; const VERSION = '4.3.0'; const BIDDER_CODE = 'sharethrough'; @@ -18,14 +18,14 @@ export const sharethroughAdapterSpec = { code: BIDDER_CODE, supportedMediaTypes: [VIDEO, BANNER], gvlid: 80, - isBidRequestValid: bid => !!bid.params.pkey && bid.bidder === BIDDER_CODE, + isBidRequestValid: (bid) => !!bid.params.pkey && bid.bidder === BIDDER_CODE, buildRequests: (bidRequests, bidderRequest) => { const timeout = bidderRequest.timeout; const firstPartyData = bidderRequest.ortb2 || {}; const nonHttp = sharethroughInternal.getProtocol().indexOf('http') < 0; - const secure = nonHttp || (sharethroughInternal.getProtocol().indexOf('https') > -1); + const secure = nonHttp || sharethroughInternal.getProtocol().indexOf('https') > -1; const req = { id: generateUUID(), @@ -45,6 +45,7 @@ export const sharethroughAdapterSpec = { dnt: navigator.doNotTrack === '1' ? 1 : 0, h: window.screen.height, w: window.screen.width, + ext: {}, }, regs: { coppa: config.getConfig('coppa') === true ? 1 : 0, @@ -63,6 +64,10 @@ export const sharethroughAdapterSpec = { test: 0, }; + if (bidderRequest.ortb2?.device?.ext?.cdep) { + req.device.ext['cdep'] = bidderRequest.ortb2.device.ext.cdep; + } + req.user = nullish(firstPartyData.user, {}); if (!req.user.ext) req.user.ext = {}; req.user.ext.eids = bidRequests[0].userIdAsEids || []; @@ -79,64 +84,79 @@ export const sharethroughAdapterSpec = { req.regs.ext.us_privacy = bidderRequest.uspConsent; } - const imps = bidRequests.map(bidReq => { - const impression = { ext: {} }; - - // mergeDeep(impression, bidReq.ortb2Imp); // leaving this out for now as we may want to leave stuff out on purpose - const tid = deepAccess(bidReq, 'ortb2Imp.ext.tid'); - if (tid) impression.ext.tid = tid; - const gpid = deepAccess(bidReq, 'ortb2Imp.ext.gpid', deepAccess(bidReq, 'ortb2Imp.ext.data.pbadslot')); - if (gpid) impression.ext.gpid = gpid; - - const videoRequest = deepAccess(bidReq, 'mediaTypes.video'); + if (bidderRequest?.gppConsent?.gppString) { + req.regs.gpp = bidderRequest.gppConsent.gppString; + req.regs.gpp_sid = bidderRequest.gppConsent.applicableSections; + } else if (bidderRequest?.ortb2?.regs?.gpp) { + req.regs.ext.gpp = bidderRequest.ortb2.regs.gpp; + req.regs.ext.gpp_sid = bidderRequest.ortb2.regs.gpp_sid; + } - if (videoRequest) { - // default playerSize, only change this if we know width and height are properly defined in the request - let [w, h] = [640, 360]; - if (videoRequest.playerSize && videoRequest.playerSize[0] && videoRequest.playerSize[0][0] && videoRequest.playerSize[0][1]) { - [w, h] = videoRequest.playerSize[0]; + const imps = bidRequests + .map((bidReq) => { + const impression = { ext: {} }; + + // mergeDeep(impression, bidReq.ortb2Imp); // leaving this out for now as we may want to leave stuff out on purpose + const tid = deepAccess(bidReq, 'ortb2Imp.ext.tid'); + if (tid) impression.ext.tid = tid; + const gpid = deepAccess(bidReq, 'ortb2Imp.ext.gpid', deepAccess(bidReq, 'ortb2Imp.ext.data.pbadslot')); + if (gpid) impression.ext.gpid = gpid; + + const videoRequest = deepAccess(bidReq, 'mediaTypes.video'); + + if (videoRequest) { + // default playerSize, only change this if we know width and height are properly defined in the request + let [w, h] = [640, 360]; + if ( + videoRequest.playerSize && + videoRequest.playerSize[0] && + videoRequest.playerSize[0][0] && + videoRequest.playerSize[0][1] + ) { + [w, h] = videoRequest.playerSize[0]; + } + + impression.video = { + pos: nullish(videoRequest.pos, 0), + topframe: inIframe() ? 0 : 1, + skip: nullish(videoRequest.skip, 0), + linearity: nullish(videoRequest.linearity, 1), + minduration: nullish(videoRequest.minduration, 5), + maxduration: nullish(videoRequest.maxduration, 60), + playbackmethod: videoRequest.playbackmethod || [2], + api: getVideoApi(videoRequest), + mimes: videoRequest.mimes || ['video/mp4'], + protocols: getVideoProtocols(videoRequest), + w, + h, + startdelay: nullish(videoRequest.startdelay, 0), + skipmin: nullish(videoRequest.skipmin, 0), + skipafter: nullish(videoRequest.skipafter, 0), + placement: videoRequest.context === 'instream' ? 1 : +deepAccess(videoRequest, 'placement', 4), + }; + + if (videoRequest.delivery) impression.video.delivery = videoRequest.delivery; + if (videoRequest.companiontype) impression.video.companiontype = videoRequest.companiontype; + if (videoRequest.companionad) impression.video.companionad = videoRequest.companionad; + } else { + impression.banner = { + pos: deepAccess(bidReq, 'mediaTypes.banner.pos', 0), + topframe: inIframe() ? 0 : 1, + format: bidReq.sizes.map((size) => ({ w: +size[0], h: +size[1] })), + }; } - impression.video = { - pos: nullish(videoRequest.pos, 0), - topframe: inIframe() ? 0 : 1, - skip: nullish(videoRequest.skip, 0), - linearity: nullish(videoRequest.linearity, 1), - minduration: nullish(videoRequest.minduration, 5), - maxduration: nullish(videoRequest.maxduration, 60), - playbackmethod: videoRequest.playbackmethod || [2], - api: getVideoApi(videoRequest), - mimes: videoRequest.mimes || ['video/mp4'], - protocols: getVideoProtocols(videoRequest), - w, - h, - startdelay: nullish(videoRequest.startdelay, 0), - skipmin: nullish(videoRequest.skipmin, 0), - skipafter: nullish(videoRequest.skipafter, 0), - placement: videoRequest.context === 'instream' ? 1 : +deepAccess(videoRequest, 'placement', 4), + return { + id: bidReq.bidId, + tagid: String(bidReq.params.pkey), + secure: secure ? 1 : 0, + bidfloor: getBidRequestFloor(bidReq), + ...impression, }; + }) + .filter((imp) => !!imp); - if (videoRequest.delivery) impression.video.delivery = videoRequest.delivery; - if (videoRequest.companiontype) impression.video.companiontype = videoRequest.companiontype; - if (videoRequest.companionad) impression.video.companionad = videoRequest.companionad; - } else { - impression.banner = { - pos: deepAccess(bidReq, 'mediaTypes.banner.pos', 0), - topframe: inIframe() ? 0 : 1, - format: bidReq.sizes.map(size => ({ w: +size[0], h: +size[1] })), - }; - } - - return { - id: bidReq.bidId, - tagid: String(bidReq.params.pkey), - secure: secure ? 1 : 0, - bidfloor: getBidRequestFloor(bidReq), - ...impression, - }; - }).filter(imp => !!imp); - - return imps.map(impression => { + return imps.map((impression) => { return { method: 'POST', url: STR_ENDPOINT, @@ -149,11 +169,17 @@ export const sharethroughAdapterSpec = { }, interpretResponse: ({ body }, req) => { - if (!body || !body.seatbid || body.seatbid.length === 0 || !body.seatbid[0].bid || body.seatbid[0].bid.length === 0) { + if ( + !body || + !body.seatbid || + body.seatbid.length === 0 || + !body.seatbid[0].bid || + body.seatbid[0].bid.length === 0 + ) { return []; } - return body.seatbid[0].bid.map(bid => { + return body.seatbid[0].bid.map((bid) => { // Spec: https://docs.prebid.org/dev-docs/bidder-adaptor.html#interpreting-the-response const response = { requestId: bid.impid, @@ -196,24 +222,20 @@ export const sharethroughAdapterSpec = { }, getUserSyncs: (syncOptions, serverResponses) => { - const shouldCookieSync = syncOptions.pixelEnabled && deepAccess(serverResponses, '0.body.cookieSyncUrls') !== undefined; + const shouldCookieSync = + syncOptions.pixelEnabled && deepAccess(serverResponses, '0.body.cookieSyncUrls') !== undefined; - return shouldCookieSync - ? serverResponses[0].body.cookieSyncUrls.map(url => ({ type: 'image', url: url })) - : []; + return shouldCookieSync ? serverResponses[0].body.cookieSyncUrls.map((url) => ({ type: 'image', url: url })) : []; }, // Empty implementation for prebid core to be able to find it - onTimeout: (data) => { - }, + onTimeout: (data) => {}, // Empty implementation for prebid core to be able to find it - onBidWon: (bid) => { - }, + onBidWon: (bid) => {}, // Empty implementation for prebid core to be able to find it - onSetTargeting: (bid) => { - }, + onSetTargeting: (bid) => {}, }; function getVideoApi({ api }) { @@ -240,7 +262,7 @@ function getBidRequestFloor(bid) { const floorInfo = bid.getFloor({ currency: 'USD', mediaType: bid.mediaTypes && bid.mediaTypes.video ? 'video' : 'banner', - size: bid.sizes.map(size => ({ w: size[0], h: size[1] })), + size: bid.sizes.map((size) => ({ w: size[0], h: size[1] })), }); if (typeof floorInfo === 'object' && floorInfo.currency === 'USD' && !isNaN(parseFloat(floorInfo.floor))) { floor = parseFloat(floorInfo.floor); diff --git a/modules/shinezBidAdapter.js b/modules/shinezBidAdapter.js index 96b6d281fdc..47fca317de2 100644 --- a/modules/shinezBidAdapter.js +++ b/modules/shinezBidAdapter.js @@ -1,4 +1,16 @@ -import { logWarn, logInfo, isArray, isFn, deepAccess, isEmpty, contains, timestamp, getBidIdParameter, triggerPixel, isInteger } from '../src/utils.js'; +import { + logWarn, + logInfo, + isArray, + isFn, + deepAccess, + isEmpty, + contains, + timestamp, + triggerPixel, + isInteger, + getBidIdParameter +} from '../src/utils.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import {BANNER, VIDEO} from '../src/mediaTypes.js'; import {config} from '../src/config.js'; diff --git a/modules/shinezRtbBidAdapter.js b/modules/shinezRtbBidAdapter.js new file mode 100644 index 00000000000..d1d9f36a569 --- /dev/null +++ b/modules/shinezRtbBidAdapter.js @@ -0,0 +1,336 @@ +import {_each, deepAccess, parseSizesInput, parseUrl, uniques, isFn} from '../src/utils.js'; +import {registerBidder} from '../src/adapters/bidderFactory.js'; +import {BANNER, VIDEO} from '../src/mediaTypes.js'; +import {getStorageManager} from '../src/storageManager.js'; +import {config} from '../src/config.js'; + +const DEFAULT_SUB_DOMAIN = 'exchange'; +const BIDDER_CODE = 'shinezRtb'; +const BIDDER_VERSION = '1.0.0'; +const CURRENCY = 'USD'; +const TTL_SECONDS = 60 * 5; +const UNIQUE_DEAL_ID_EXPIRY = 1000 * 60 * 15; +const storage = getStorageManager({bidderCode: BIDDER_CODE}); + +function getTopWindowQueryParams() { + try { + const parsedUrl = parseUrl(window.top.document.URL, {decodeSearchAsString: true}); + return parsedUrl.search; + } catch (e) { + return ''; + } +} + +export function createDomain(subDomain = DEFAULT_SUB_DOMAIN) { + return `https://${subDomain}.sweetgum.io`; +} + +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, + ortb2Imp, + bidderRequestId, + bidRequestsCount, + bidderRequestsCount, + bidderWinsCount + } = bid; + let {bidFloor, ext} = params; + const hashUrl = hashCode(topWindowUrl); + const uniqueDealId = getUniqueDealId(hashUrl); + const cId = extractCID(params); + const pId = extractPID(params); + const subDomain = extractSubDomain(params); + + const gpid = deepAccess(bid, 'ortb2Imp.ext.gpid', deepAccess(bid, 'ortb2Imp.ext.data.pbadslot', '')); + + if (isFn(bid.getFloor)) { + const floorInfo = bid.getFloor({ + currency: 'USD', + mediaType: '*', + size: '*' + }); + + if (floorInfo.currency === 'USD') { + bidFloor = floorInfo.floor; + } + } + + let data = { + url: encodeURIComponent(topWindowUrl), + uqs: getTopWindowQueryParams(), + cb: Date.now(), + bidFloor: bidFloor, + bidId: bidId, + referrer: bidderRequest.refererInfo.ref, + adUnitCode: adUnitCode, + publisherId: pId, + sizes: sizes, + uniqueDealId: uniqueDealId, + bidderVersion: BIDDER_VERSION, + prebidVersion: '$prebid.version$', + res: `${screen.width}x${screen.height}`, + schain: schain, + mediaTypes: mediaTypes, + gpid: gpid, + transactionId: ortb2Imp?.ext?.tid, + bidderRequestId: bidderRequestId, + bidRequestsCount: bidRequestsCount, + bidderRequestsCount: bidderRequestsCount, + bidderWinsCount: bidderWinsCount, + bidderTimeout: bidderTimeout + }; + + appendUserIdsToRequestPayload(data, userId); + + const sua = deepAccess(bidderRequest, 'ortb2.device.sua'); + + if (sua) { + data.sua = sua; + } + + if (bidderRequest.gdprConsent) { + if (bidderRequest.gdprConsent.consentString) { + data.gdprConsent = bidderRequest.gdprConsent.consentString; + } + if (bidderRequest.gdprConsent.gdprApplies !== undefined) { + data.gdpr = bidderRequest.gdprConsent.gdprApplies ? 1 : 0; + } + } + if (bidderRequest.uspConsent) { + data.usPrivacy = bidderRequest.uspConsent; + } + + if (bidderRequest.gppConsent) { + data.gppString = bidderRequest.gppConsent.gppString; + data.gppSid = bidderRequest.gppConsent.applicableSections; + } else if (bidderRequest.ortb2?.regs?.gpp) { + data.gppString = bidderRequest.ortb2.regs.gpp; + data.gppSid = bidderRequest.ortb2.regs.gpp_sid; + } + + const dto = { + method: 'POST', + url: `${createDomain(subDomain)}/prebid/multi/${cId}`, + data: data + }; + + _each(ext, (value, key) => { + dto.data['ext.' + key] = value; + }); + + return dto; +} + +function appendUserIdsToRequestPayload(payloadRef, userIds) { + let key; + _each(userIds, (userId, idSystemProviderName) => { + key = `uid.${idSystemProviderName}`; + + switch (idSystemProviderName) { + case 'digitrustid': + payloadRef[key] = deepAccess(userId, 'data.id'); + break; + case 'lipb': + payloadRef[key] = userId.lipbid; + break; + case 'parrableId': + payloadRef[key] = userId.eid; + break; + case 'id5id': + payloadRef[key] = userId.uid; + break; + default: + payloadRef[key] = userId; + } + }); +} + +function buildRequests(validBidRequests, bidderRequest) { + const topWindowUrl = bidderRequest.refererInfo.page || bidderRequest.refererInfo.topmostLocation; + const bidderTimeout = config.getConfig('bidderTimeout'); + const requests = []; + validBidRequests.forEach(validBidRequest => { + const sizes = parseSizesInput(validBidRequest.sizes); + const request = buildRequest(validBidRequest, topWindowUrl, sizes, bidderRequest, bidderTimeout); + requests.push(request); + }); + return requests; +} + +function interpretResponse(serverResponse, request) { + if (!serverResponse || !serverResponse.body) { + return []; + } + const {bidId} = request.data; + const {results} = serverResponse.body; + + let output = []; + + try { + results.forEach(result => { + const { + creativeId, + ad, + price, + exp, + width, + height, + currency, + metaData, + advertiserDomains, + mediaType = BANNER + } = result; + if (!ad || !price) { + return; + } + + const response = { + requestId: bidId, + cpm: price, + width: width, + height: height, + creativeId: creativeId, + currency: currency || CURRENCY, + netRevenue: true, + ttl: exp || TTL_SECONDS, + }; + + if (metaData) { + Object.assign(response, { + meta: metaData + }) + } else { + Object.assign(response, { + meta: { + advertiserDomains: advertiserDomains || [] + } + }) + } + + if (mediaType === BANNER) { + Object.assign(response, { + ad: ad, + }); + } else { + Object.assign(response, { + vastXml: ad, + mediaType: VIDEO + }); + } + output.push(response); + }); + return output; + } catch (e) { + return []; + } +} + +function getUserSyncs(syncOptions, responses, gdprConsent = {}, uspConsent = '') { + let syncs = []; + const {iframeEnabled, pixelEnabled} = syncOptions; + const {gdprApplies, consentString = ''} = gdprConsent; + + const cidArr = responses.filter(resp => deepAccess(resp, 'body.cid')).map(resp => resp.body.cid).filter(uniques); + const params = `?cid=${encodeURIComponent(cidArr.join(','))}&gdpr=${gdprApplies ? 1 : 0}&gdpr_consent=${encodeURIComponent(consentString || '')}&us_privacy=${encodeURIComponent(uspConsent || '')}` + if (iframeEnabled) { + syncs.push({ + type: 'iframe', + url: `https://sync.sweetgum.io/api/sync/iframe/${params}` + }); + } + if (pixelEnabled) { + syncs.push({ + type: 'image', + url: `https://sync.sweetgum.io/api/sync/image/${params}` + }); + } + return syncs; +} + +export function hashCode(s, prefix = '_') { + const l = s.length; + let h = 0 + let i = 0; + if (l > 0) { + while (i < l) { + h = (h << 5) - h + s.charCodeAt(i++) | 0; + } + } + return prefix + h; +} + +export function getUniqueDealId(key, expiry = UNIQUE_DEAL_ID_EXPIRY) { + const storageKey = `u_${key}`; + const now = Date.now(); + const data = getStorageItem(storageKey); + let uniqueId; + + if (!data || !data.value || now - data.created > expiry) { + uniqueId = `${key}_${now.toString()}`; + setStorageItem(storageKey, uniqueId); + } else { + uniqueId = data.value; + } + + return uniqueId; +} + +export function getStorageItem(key) { + try { + return tryParseJSON(storage.getDataFromLocalStorage(key)); + } catch (e) { + } + + return null; +} + +export function setStorageItem(key, value, timestamp) { + try { + const created = timestamp || Date.now(); + const data = JSON.stringify({value, created}); + storage.setDataInLocalStorage(key, data); + } catch (e) { + } +} + +export function tryParseJSON(value) { + try { + return JSON.parse(value); + } catch (e) { + return value; + } +} + +export const spec = { + code: BIDDER_CODE, + version: BIDDER_VERSION, + supportedMediaTypes: [BANNER, VIDEO], + isBidRequestValid, + buildRequests, + interpretResponse, + getUserSyncs +}; + +registerBidder(spec); diff --git a/modules/shinezRtbBidAdapter.md b/modules/shinezRtbBidAdapter.md new file mode 100644 index 00000000000..e9190c2a9c4 --- /dev/null +++ b/modules/shinezRtbBidAdapter.md @@ -0,0 +1,35 @@ +# Overview + +**Module Name:** Shinez RTB Bid Adapter + +**Module Type:** Bidder Adapter + +**Maintainer:** tech-team@shinez.io + +# Description + +Module that connects to Shinez RTB demand sources. + +# Test Parameters +```js +var adUnits = [ + { + code: 'test-ad', + sizes: [[300, 250]], + bids: [ + { + bidder: 'shinezRtb', + params: { + cId: '562524b21b1c1f08117fc7f9', + pId: '59ac17c192832d0011283fe3', + bidFloor: 0.0001, + ext: { + param1: 'loremipsum', + param2: 'dolorsitamet' + } + } + } + ] + } +]; +``` diff --git a/modules/showheroes-bsBidAdapter.js b/modules/showheroes-bsBidAdapter.js index a241cb71a5d..bd2706a21d5 100644 --- a/modules/showheroes-bsBidAdapter.js +++ b/modules/showheroes-bsBidAdapter.js @@ -1,10 +1,9 @@ import { deepAccess, - getBidIdParameter, getWindowTop, triggerPixel, logInfo, - logError + logError, getBidIdParameter } from '../src/utils.js'; import { config } from '../src/config.js'; import { Renderer } from '../src/Renderer.js'; @@ -29,8 +28,11 @@ function getEnvURLs(isStage) { } } +const GVLID = 111; + export const spec = { code: BIDDER_CODE, + gvlid: GVLID, aliases: ['showheroesBs'], supportedMediaTypes: [VIDEO, BANNER], isBidRequestValid: function(bid) { diff --git a/modules/silverpushBidAdapter.js b/modules/silverpushBidAdapter.js index df81e144380..5403f3bd88c 100644 --- a/modules/silverpushBidAdapter.js +++ b/modules/silverpushBidAdapter.js @@ -12,7 +12,7 @@ const bidderConfig = 'sp_pb_ortb'; const bidderVersion = '1.0.0'; const DEFAULT_CURRENCY = 'USD'; -export const REQUEST_URL = 'https://apac.chocolateplatform.com/bidder/?identifier=prebidchoc'; +export const REQUEST_URL = 'https://prebid.chocolateplatform.co/bidder/?identifier=prebidchoc'; export const SP_OUTSTREAM_PLAYER_URL = 'https://xaido.sgp1.cdn.digitaloceanspaces.com/prebid/spoutstream.min.js'; const VIDEO_ORTB_PARAMS = [ @@ -110,7 +110,7 @@ export const CONVERTER = ortbConverter({ bidResponse.meta.paf.content_id = utils.deepAccess(bid, 'ext.paf.content_id'); } - bidResponse = buildVideoVastResponse(bidResponse) + bidResponse = buildVideoVastResponse(bidResponse); bidResponse = buildVideoOutstreamResponse(bidResponse, context) return bidResponse; diff --git a/modules/sizeMappingV2.js b/modules/sizeMappingV2.js index d212d98f50b..5ddb2e410cb 100644 --- a/modules/sizeMappingV2.js +++ b/modules/sizeMappingV2.js @@ -63,7 +63,7 @@ export function isUsingNewSizeMapping(adUnits) { does not recognize. @params {Array} adUnits @returns {Array} validateAdUnits - Unrecognized properties are deleted. -*/ + */ export function checkAdUnitSetupHook(adUnits) { const validateSizeConfig = function (mediaType, sizeConfig, adUnitCode) { let isValid = true; diff --git a/modules/slimcutBidAdapter.js b/modules/slimcutBidAdapter.js index 447e314958f..250c1ebb19e 100644 --- a/modules/slimcutBidAdapter.js +++ b/modules/slimcutBidAdapter.js @@ -1,10 +1,17 @@ -import { getValue, parseSizesInput, getBidIdParameter } from '../src/utils.js'; +import {getBidIdParameter, getValue, parseSizesInput} from '../src/utils.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; import { ajax } from '../src/ajax.js'; + +/** + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid + * @typedef {import('../src/adapters/bidderFactory.js').validBidRequests} validBidRequests + */ + const BIDDER_CODE = 'slimcut'; const ENDPOINT_URL = 'https://sb.freeskreen.com/pbr'; export const spec = { @@ -13,11 +20,11 @@ export const spec = { aliases: [{ code: 'scm', gvlid: 102 }], supportedMediaTypes: ['video', '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. - */ + * 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' && !isNaN(parseInt(getValue(bid.params, 'placementId'))) && parseInt(getValue(bid.params, 'placementId')) > 0) { @@ -26,11 +33,11 @@ export const spec = { 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. - */ + * 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) { const bids = validBidRequests.map(buildRequestObject); const payload = { @@ -55,11 +62,11 @@ export const spec = { }; }, /** - * Unpack the response from the server into a list of bids. - * - * @param {*} serverResponse A successful response from the server. - * @return {Bid[]} An array of bids which were nested inside the server. - */ + * Unpack the response from the server into a list of bids. + * + * @param {*} serverResponse A successful response from the server. + * @return {Bid[]} An array of bids which were nested inside the server. + */ interpretResponse: function(serverResponse, request) { const bidResponses = []; serverResponse = serverResponse.body; diff --git a/modules/smaatoBidAdapter.js b/modules/smaatoBidAdapter.js index 1b50e033074..ac0422842d5 100644 --- a/modules/smaatoBidAdapter.js +++ b/modules/smaatoBidAdapter.js @@ -1,22 +1,20 @@ -import { - chunk, - deepAccess, - deepSetValue, - fill, - getAdUnitSizes, - getDNT, - getMaxValueFromArray, - getMinValueFromArray, - isEmpty, - isNumber, - logError, - logInfo -} from '../src/utils.js'; +import {deepAccess, deepSetValue, getDNT, isEmpty, isNumber, logError, logInfo} from '../src/utils.js'; import {find} from '../src/polyfill.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import {config} from '../src/config.js'; import {ADPOD, BANNER, NATIVE, VIDEO} from '../src/mediaTypes.js'; import CONSTANTS from '../src/constants.json'; +import {getAdUnitSizes} from '../libraries/sizeUtils/sizeUtils.js'; +import {fill} from '../libraries/appnexusUtils/anUtils.js'; +import {chunk} from '../libraries/chunk/chunk.js'; + +/** + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid + * @typedef {import('../src/adapters/bidderFactory.js').ServerResponse} ServerResponse + * @typedef {import('../src/adapters/bidderFactory.js').SyncOptions} SyncOptions + * @typedef {import('../src/adapters/bidderFactory.js').UserSync} UserSync + */ const { NATIVE_IMAGE_TYPES } = CONSTANTS; const BIDDER_CODE = 'smaato'; @@ -466,7 +464,7 @@ function createAdPodImp(bidRequest, videoMediaType) { }); } else { // all maxdurations should be the same - const maxDuration = getMaxValueFromArray(durationRangeSec); + const maxDuration = Math.max(...durationRangeSec); imps.map((imp, index) => { const sequence = index + 1; imp.video.maxduration = maxDuration @@ -481,7 +479,7 @@ function createAdPodImp(bidRequest, videoMediaType) { function getAdPodNumberOfPlacements(videoMediaType) { const {adPodDurationSec, durationRangeSec, requireExactDuration} = videoMediaType - const minAllowedDuration = getMinValueFromArray(durationRangeSec) + const minAllowedDuration = Math.min(...durationRangeSec) const numberOfPlacements = Math.floor(adPodDurationSec / minAllowedDuration) return requireExactDuration diff --git a/modules/smartadserverBidAdapter.js b/modules/smartadserverBidAdapter.js index ca43c26ffd7..9146bba6514 100644 --- a/modules/smartadserverBidAdapter.js +++ b/modules/smartadserverBidAdapter.js @@ -1,8 +1,14 @@ -import { deepAccess, deepClone, logError, isFn, isPlainObject } from '../src/utils.js'; +import { deepAccess, deepClone, isArrayOfNums, isFn, isInteger, isPlainObject, logError } from '../src/utils.js'; import { BANNER, VIDEO } from '../src/mediaTypes.js'; import { config } from '../src/config.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; +/** + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid + * @typedef {import('../src/adapters/bidderFactory.js').ServerRequest} ServerRequest + */ + const BIDDER_CODE = 'smartadserver'; const GVL_ID = 45; const DEFAULT_FLOOR = 0.0; @@ -55,20 +61,53 @@ export const spec = { * Fills the payload with specific video attributes. * * @param {*} payload Payload that will be sent in the ServerRequest - * @param {*} videoMediaType Video media type. + * @param {*} videoMediaType Video media type */ fillPayloadForVideoBidRequest: function(payload, videoMediaType, videoParams) { const playerSize = videoMediaType.playerSize[0]; - payload.isVideo = videoMediaType.context === 'instream'; + const map = { + maxbitrate: 'vbrmax', + maxduration: 'vdmax', + minbitrate: 'vbrmin', + minduration: 'vdmin', + placement: 'vpt', + plcmt: 'vplcmt', + skip: 'skip' + }; + payload.mediaType = VIDEO; + payload.isVideo = videoMediaType.context === 'instream'; + payload.videoData = {}; + + for (const [key, value] of Object.entries(map)) { + payload.videoData = { + ...payload.videoData, + ...this.getValuableProperty(value, videoMediaType[key]) + }; + } + payload.videoData = { - videoProtocol: this.getProtocolForVideoBidRequest(videoMediaType, videoParams), - playerWidth: playerSize[0], - playerHeight: playerSize[1], - adBreak: this.getStartDelayForVideoBidRequest(videoMediaType, videoParams) + ...payload.videoData, + ...this.getValuableProperty('playerWidth', playerSize[0]), + ...this.getValuableProperty('playerHeight', playerSize[1]), + ...this.getValuableProperty('adBreak', this.getStartDelayForVideoBidRequest(videoMediaType, videoParams)), + ...this.getValuableProperty('videoProtocol', this.getProtocolForVideoBidRequest(videoMediaType, videoParams)), + ...(isArrayOfNums(videoMediaType.api) && videoMediaType.api.length ? { iabframeworks: videoMediaType.api.toString() } : {}), + ...(isArrayOfNums(videoMediaType.playbackmethod) && videoMediaType.playbackmethod.length ? { vpmt: videoMediaType.playbackmethod } : {}) }; }, + /** + * Gets a property object if the value not falsy + * @param {string} property + * @param {number} value + * @returns object with the property or empty + */ + getValuableProperty: function(property, value) { + return typeof property === 'string' && isInteger(value) && value + ? { [property]: value } : {}; + }, + /** * Gets the protocols from either videoParams or VideoMediaType * @param {*} videoMediaType diff --git a/modules/smartxBidAdapter.js b/modules/smartxBidAdapter.js index d91b62729bc..8394814365c 100644 --- a/modules/smartxBidAdapter.js +++ b/modules/smartxBidAdapter.js @@ -1,4 +1,17 @@ -import { logError, deepAccess, isArray, getBidIdParameter, getDNT, generateUUID, isEmpty, _each, logMessage, logWarn, isFn, isPlainObject } from '../src/utils.js'; +import { + logError, + deepAccess, + isArray, + getDNT, + generateUUID, + isEmpty, + _each, + logMessage, + logWarn, + isFn, + isPlainObject, + getBidIdParameter +} from '../src/utils.js'; import { Renderer } from '../src/Renderer.js'; @@ -8,6 +21,12 @@ import { import { VIDEO } from '../src/mediaTypes.js'; + +/** + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid + */ + const BIDDER_CODE = 'smartx'; const URL = 'https://bid.sxp.smartclip.net/bid/1000'; const GVLID = 115; diff --git a/modules/smartxBidAdapter.md b/modules/smartxBidAdapter.md index 853f06d6baf..50f78660458 100644 --- a/modules/smartxBidAdapter.md +++ b/modules/smartxBidAdapter.md @@ -3,7 +3,7 @@ ``` Module Name: smartclip Bidder Adapter Module Type: Bidder Adapter -Maintainer: adtech@smartclip.tv +Maintainer: bidding@smartclip.tv ``` # Description @@ -170,4 +170,4 @@ This adapter requires setup and approval from the smartclip team. } }], }]; -``` \ No newline at end of file +``` diff --git a/modules/smartyadsBidAdapter.js b/modules/smartyadsBidAdapter.js index 89749aed433..2409bebbc59 100644 --- a/modules/smartyadsBidAdapter.js +++ b/modules/smartyadsBidAdapter.js @@ -3,9 +3,16 @@ import {registerBidder} from '../src/adapters/bidderFactory.js'; import { BANNER, NATIVE, VIDEO } from '../src/mediaTypes.js'; import { config } from '../src/config.js'; import { convertOrtbRequestToProprietaryNative } from '../src/native.js'; +import { ajax } from '../src/ajax.js'; const BIDDER_CODE = 'smartyads'; -const AD_URL = 'https://n1.smartyads.com/?c=o&m=prebid&secret_key=prebid_js'; +const GVLID = 534; +const adUrls = { + US_EAST: 'https://n1.smartyads.com/?c=o&m=prebid&secret_key=prebid_js', + EU: 'https://n2.smartyads.com/?c=o&m=prebid&secret_key=prebid_js', + SGP: 'https://n6.smartyads.com/?c=o&m=prebid&secret_key=prebid_js' +} + const URL_SYNC = 'https://as.ck-ie.com/prebidjs?p=7c47322e527cf8bdeb7facc1bb03387a'; function isBidResponseValid(bid) { @@ -25,8 +32,28 @@ function isBidResponseValid(bid) { } } +function getAdUrlByRegion(bid) { + let adUrl; + + if (bid.params.region && adUrls[bid.params.region]) { + adUrl = adUrls[bid.params.region]; + } else { + try { + const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone; + const region = timezone.split('/')[0]; + if (region === 'Europe') adUrl = adUrls['EU']; + else adUrl = adUrls['US_EAST']; + } catch (err) { + adUrl = adUrls['US_EAST']; + } + } + + return adUrl; +} + export const spec = { code: BIDDER_CODE, + gvlid: GVLID, supportedMediaTypes: [BANNER, VIDEO, NATIVE], isBidRequestValid: (bid) => { @@ -66,11 +93,17 @@ export const spec = { if (bidderRequest.gdprConsent) { request.gdpr = bidderRequest.gdprConsent } + if (bidderRequest.gppConsent) { + request.gpp = bidderRequest.gppConsent; + } } const len = validBidRequests.length; + let adUrl; + for (let i = 0; i < len; i++) { let bid = validBidRequests[i]; + if (i === 0) adUrl = getAdUrlByRegion(bid); let traff = bid.params.traffic || BANNER placements.push({ placementId: bid.params.sourceid, @@ -83,11 +116,12 @@ export const spec = { placements.schain = bid.schain; } } + return { method: 'POST', - url: AD_URL, + url: adUrl, data: request - }; + } }, interpretResponse: (serverResponse) => { @@ -102,24 +136,46 @@ export const spec = { return response; }, - getUserSyncs: (syncOptions, serverResponses = [], gdprConsent = {}, uspConsent = '') => { + getUserSyncs: (syncOptions, serverResponses = [], gdprConsent = {}, uspConsent = '', gppConsent = '') => { let syncs = []; let { gdprApplies, consentString = '' } = gdprConsent; if (syncOptions.iframeEnabled) { syncs.push({ type: 'iframe', - url: `${URL_SYNC}&gdpr=${gdprApplies ? 1 : 0}&gdpr_consent=${consentString}&type=iframe&us_privacy=${uspConsent}` + url: `${URL_SYNC}&gdpr=${gdprApplies ? 1 : 0}&gdpr_consent=${consentString}&type=iframe&us_privacy=${uspConsent}&gpp=${gppConsent}` }); } else { syncs.push({ type: 'image', - url: `${URL_SYNC}&gdpr=${gdprApplies ? 1 : 0}&gdpr_consent=${consentString}&type=image&us_privacy=${uspConsent}` + url: `${URL_SYNC}&gdpr=${gdprApplies ? 1 : 0}&gdpr_consent=${consentString}&type=image&us_privacy=${uspConsent}&gpp=${gppConsent}` }); } return syncs - } + }, + + onBidWon: function(bid) { + if (bid.winUrl) { + ajax(bid.winUrl, () => {}, JSON.stringify(bid)); + } else { + if (bid?.postData && bid?.postData[0] && bid?.postData[0].params && bid?.postData[0].params[0].host == 'prebid') { + ajax('https://et-nd43.itdsmr.com/?c=o&m=prebid&secret_key=prebid_js&winTest=1', () => {}, JSON.stringify(bid)); + } + } + }, + + onTimeout: function(bid) { + if (bid?.postData && bid?.postData[0] && bid?.postData[0].params && bid?.postData[0].params[0].host == 'prebid') { + ajax('https://et-nd43.itdsmr.com/?c=o&m=prebid&secret_key=prebid_js&bidTimeout=1', () => {}, JSON.stringify(bid)); + } + }, + + onBidderError: function(bid) { + if (bid?.postData && bid?.postData[0] && bid?.postData[0].params && bid?.postData[0].params[0].host == 'prebid') { + ajax('https://et-nd43.itdsmr.com/?c=o&m=prebid&secret_key=prebid_js&bidderError=1', () => {}, JSON.stringify(bid)); + } + }, }; diff --git a/modules/smartyadsBidAdapter.md b/modules/smartyadsBidAdapter.md index e0d6023a794..443d5ab5978 100644 --- a/modules/smartyadsBidAdapter.md +++ b/modules/smartyadsBidAdapter.md @@ -14,10 +14,11 @@ Module that connects to SmartyAds' demand sources | Name | Scope | Description | Example | | :------------ | :------- | :------------------------ | :------------------- | -| `sourceid` | required (for prebid.js) | placement ID | "0" | -| `host` | required (for prebid-server) | const value, set to "prebid" | "prebid" | -| `accountid` | required (for prebid-server) | partner ID | "1901" | +| `sourceid` | required (for prebid.js) | Placement ID | "0" | +| `host` | required (for prebid-server) | Const value, set to "prebid" | "prebid" | +| `accountid` | required (for prebid-server) | Partner ID | "1901" | | `traffic` | optional (for prebid.js) | Configures the mediaType that should be used. Values can be banner, native or video | "banner" | +| `region` | optional (for prebid.js) | Prefix of the region to which prebid must send requests. Possible values: "US_EAST", "EU" | "US_EAST" | # Test Parameters ``` @@ -35,7 +36,9 @@ Module that connects to SmartyAds' demand sources host: 'prebid', sourceid: '0', accountid: '0', - traffic: 'native' + traffic: 'native', + region: 'US_EAST' + } } ] @@ -55,7 +58,8 @@ Module that connects to SmartyAds' demand sources host: 'prebid', sourceid: '0', accountid: '0', - traffic: 'banner' + traffic: 'banner', + region: 'US_EAST' } } ] @@ -76,7 +80,9 @@ Module that connects to SmartyAds' demand sources host: 'prebid', sourceid: '0', accountid: '0', - traffic: 'video' + traffic: 'video', + region: 'US_EAST' + } } ] diff --git a/modules/smilewantedBidAdapter.js b/modules/smilewantedBidAdapter.js index b7a25ba58df..515aae0e092 100644 --- a/modules/smilewantedBidAdapter.js +++ b/modules/smilewantedBidAdapter.js @@ -1,32 +1,65 @@ -import { isArray, logError, logWarn, isFn, isPlainObject } from '../src/utils.js'; -import { Renderer } from '../src/Renderer.js'; -import { config } from '../src/config.js'; -import { registerBidder } from '../src/adapters/bidderFactory.js'; -import { BANNER, VIDEO } from '../src/mediaTypes.js'; +import {deepAccess, deepClone, isArray, isFn, isPlainObject, logError, logWarn} from '../src/utils.js'; +import {Renderer} from '../src/Renderer.js'; +import {config} from '../src/config.js'; +import {registerBidder} from '../src/adapters/bidderFactory.js'; +import {BANNER, NATIVE, VIDEO} from '../src/mediaTypes.js'; +import {INSTREAM, OUTSTREAM} from '../src/video.js'; +import {convertOrtbRequestToProprietaryNative, toOrtbNativeRequest, toLegacyResponse} from '../src/native.js'; + +const BIDDER_CODE = 'smilewanted'; + +/** + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid + */ + +const GVL_ID = 639; export const spec = { - code: 'smilewanted', + code: BIDDER_CODE, + gvlid: GVL_ID, aliases: ['smile', 'sw'], - supportedMediaTypes: [BANNER, VIDEO], + supportedMediaTypes: [BANNER, VIDEO, NATIVE], /** * Determines whether or not the given bid request is valid. * - * @param {object} bid The bid to validate. + * @param {BidRequest} bid The bid to validate. * @return boolean True if this is a valid bid, and false otherwise. */ isBidRequestValid: function(bid) { - return !!(bid.params && bid.params.zoneId); + if (!bid.params || !bid.params.zoneId) { + return false; + } + + if (deepAccess(bid, 'mediaTypes.video')) { + const videoMediaTypesParams = deepAccess(bid, 'mediaTypes.video', {}); + const videoBidderParams = deepAccess(bid, 'params.video', {}); + + const videoParams = { + ...videoMediaTypesParams, + ...videoBidderParams + }; + + if (!videoParams.context || ![INSTREAM, OUTSTREAM].includes(videoParams.context)) { + return false; + } + } + + return true; }, /** * Make a server request from the list of BidRequests. * * @param {BidRequest[]} validBidRequests A non-empty list of valid bid requests that should be sent to the Server. + * @param {BidderRequest} bidderRequest bidder request object. * @return ServerRequest Info describing the request to the server. */ buildRequests: function(validBidRequests, bidderRequest) { + validBidRequests = convertOrtbRequestToProprietaryNative(validBidRequests); + return validBidRequests.map(bid => { - var payload = { + const payload = { zoneId: bid.params.zoneId, currencyCode: config.getConfig('currency.adServerCurrency') || 'EUR', tagId: bid.adUnitCode, @@ -37,11 +70,13 @@ export const spec = { transactionId: bid.ortb2Imp?.ext?.tid, timeout: bidderRequest?.timeout, bidId: bid.bidId, - /** positionType is undocumented + /** + positionType is undocumented It is unclear what this parameter means. If it means the same as pos in openRTB, It should read from openRTB object - or from mediaTypes.banner.pos */ + or from mediaTypes.banner.pos + */ positionType: bid.params.positionType || '', prebidVersion: '$prebid.version$' }; @@ -55,20 +90,41 @@ export const spec = { payload.bidfloor = bid.params.bidfloor; } - if (bidderRequest && bidderRequest.refererInfo) { + if (bidderRequest?.refererInfo) { payload.pageDomain = bidderRequest.refererInfo.page || ''; } - if (bidderRequest && bidderRequest.gdprConsent) { + if (bidderRequest?.gdprConsent) { 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; + payload.eids = bid?.userIdAsEids; + + const videoMediaType = deepAccess(bid, 'mediaTypes.video'); + const context = deepAccess(bid, 'mediaTypes.video.context'); + + if (bid.mediaType === 'video' || (videoMediaType && context === INSTREAM) || (videoMediaType && context === OUTSTREAM)) { + payload.context = context; + payload.videoParams = deepClone(videoMediaType); + } + + const nativeMediaType = deepAccess(bid, 'mediaTypes.native'); + + if (nativeMediaType) { + payload.context = 'native'; + payload.nativeParams = nativeMediaType; + let sizes = deepAccess(bid, 'mediaTypes.native.image.sizes', []); + + if (sizes.length > 0) { + const size = Array.isArray(sizes[0]) ? sizes[0] : sizes; + + payload.width = size[0] || payload.width; + payload.height = size[1] || payload.height; + } } - var payloadString = JSON.stringify(payload); + const payloadString = JSON.stringify(payload); return { method: 'POST', url: 'https://prebid.smilewanted.com', @@ -80,18 +136,21 @@ export const spec = { /** * Unpack the response from the server into a list of bids. * - * @param {*} serverResponse A successful response from the server. + * @param {ServerResponse} serverResponse A successful response from the server. + * @param {BidRequest} bidRequest * @return {Bid[]} An array of bids which were nested inside the server. */ interpretResponse: function(serverResponse, bidRequest) { + if (!serverResponse.body) return []; const bidResponses = []; - var response = serverResponse.body; try { + const response = serverResponse.body; + const bidRequestData = JSON.parse(bidRequest.data); if (response) { const dealId = response.dealId || ''; const bidResponse = { - requestId: JSON.parse(bidRequest.data).bidId, + requestId: bidRequestData.bidId, cpm: response.cpm, width: response.width, height: response.height, @@ -103,14 +162,21 @@ export const spec = { ad: response.ad, }; - if (response.formatTypeSw == 'video_instream' || response.formatTypeSw == 'video_outstream') { + if (response.formatTypeSw === 'video_instream' || response.formatTypeSw === 'video_outstream') { bidResponse['mediaType'] = 'video'; bidResponse['vastUrl'] = response.ad; bidResponse['ad'] = null; + + if (response.formatTypeSw === 'video_outstream') { + bidResponse['renderer'] = newRenderer(bidRequestData, response); + } } - if (response.formatTypeSw == 'video_outstream') { - bidResponse['renderer'] = newRenderer(JSON.parse(bidRequest.data), response); + if (response.formatTypeSw === 'native') { + const nativeAdResponse = JSON.parse(response.ad); + const ortbNativeRequest = toOrtbNativeRequest(bidRequestData.nativeParams); + bidResponse['mediaType'] = 'native'; + bidResponse['native'] = toLegacyResponse(nativeAdResponse, ortbNativeRequest); } if (dealId.length > 0) { @@ -118,7 +184,7 @@ export const spec = { } bidResponse.meta = {}; - if (response.meta && response.meta.advertiserDomains && isArray(response.meta.advertiserDomains)) { + if (response.meta?.advertiserDomains && isArray(response.meta.advertiserDomains)) { bidResponse.meta.advertiserDomains = response.meta.advertiserDomains; } bidResponses.push(bidResponse); @@ -126,15 +192,18 @@ export const spec = { } catch (error) { logError('Error while parsing smilewanted response', error); } + return bidResponses; }, /** - * User syncs. + * Register the user sync pixels which should be dropped after the auction. * - * @param {*} syncOptions Publisher prebid configuration. - * @param {*} serverResponses A successful response from the server. - * @return {Syncs[]} An array of syncs that should be executed. + * @param {SyncOptions} syncOptions Which user syncs are allowed? + * @param {ServerResponse[]} responses List of server's responses. + * @param {Object} gdprConsent The GDPR consent parameters + * @param {Object} uspConsent The USP consent parameters + * @return {UserSync[]} The user syncs which should be dropped. */ getUserSyncs: function(syncOptions, responses, gdprConsent, uspConsent) { let params = ''; @@ -167,7 +236,8 @@ export const spec = { /** * Create SmileWanted renderer - * @param requestId + * @param bidRequest + * @param bidResponse * @returns {*} */ function newRenderer(bidRequest, bidResponse) { diff --git a/modules/snigelBidAdapter.js b/modules/snigelBidAdapter.js index f41fb98d436..6d32d8f97a2 100644 --- a/modules/snigelBidAdapter.js +++ b/modules/snigelBidAdapter.js @@ -1,7 +1,7 @@ import {config} from '../src/config.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import {BANNER} from '../src/mediaTypes.js'; -import {deepAccess, isArray, isFn, isPlainObject} from '../src/utils.js'; +import {deepAccess, isArray, isFn, isPlainObject, inIframe, getDNT} from '../src/utils.js'; import {hasPurpose1Consent} from '../src/utils/gpdr.js'; import {getGlobal} from '../src/prebidGlobal.js'; @@ -13,6 +13,7 @@ const DEFAULT_CURRENCIES = ['USD']; const FLOOR_MATCH_ALL_SIZES = '*'; const getConfig = config.getConfig; +const refreshes = {}; export const spec = { code: BIDDER_CODE, @@ -29,12 +30,15 @@ export const spec = { method: 'POST', url: getEndpoint(), data: JSON.stringify({ - id: bidderRequest.bidderRequestId, + id: bidderRequest.auctionId, + accountId: deepAccess(bidRequests, '0.params.accountId'), + site: deepAccess(bidRequests, '0.params.site'), cur: getCurrencies(), test: getTestFlag(), - devw: window.innerWidth || document.documentElement.clientWidth || document.body.clientWidth, - devh: window.innerHeight || document.documentElement.clientHeight || document.body.clientHeight, version: getGlobal().version, + gpp: deepAccess(bidderRequest, 'gppConsent.gppString') || deepAccess(bidderRequest, 'ortb2.regs.gpp'), + gpp_sid: + deepAccess(bidderRequest, 'gppConsent.applicableSections') || deepAccess(bidderRequest, 'ortb2.regs.gpp_sid'), gdprApplies: gdprApplies, gdprConsentString: gdprApplies === true ? deepAccess(bidderRequest, 'gdprConsent.consentString') : undefined, gdprConsentProv: gdprApplies === true ? deepAccess(bidderRequest, 'gdprConsent.addtlConsent') : undefined, @@ -43,12 +47,24 @@ export const spec = { eids: deepAccess(bidRequests, '0.userIdAsEids'), schain: deepAccess(bidRequests, '0.schain'), page: getPage(bidderRequest), + topframe: inIframe() === true ? 0 : 1, + device: { + w: window.innerWidth || document.documentElement.clientWidth || document.body.clientWidth, + h: window.innerHeight || document.documentElement.clientHeight || document.body.clientHeight, + dnt: getDNT() ? 1 : 0, + language: getLanguage(), + }, placements: bidRequests.map((r) => { return { - uuid: r.bidId, + id: r.adUnitCode, + tid: r.transactionId, + gpid: deepAccess(r, 'ortb2Imp.ext.gpid'), + pbadslot: deepAccess(r, 'ortb2Imp.ext.data.pbadslot') || deepAccess(r, 'ortb2Imp.ext.gpid'), name: r.params.placement, sizes: r.sizes, floor: getPriceFloor(r, BANNER, FLOOR_MATCH_ALL_SIZES), + refresh: getRefreshInformation(r.adUnitCode), + params: r.params.additionalParams, }; }), }), @@ -56,14 +72,14 @@ export const spec = { }; }, - interpretResponse: function (serverResponse) { + interpretResponse: function (serverResponse, bidRequest) { if (!serverResponse.body || !serverResponse.body.bids) { return []; } return serverResponse.body.bids.map((bid) => { return { - requestId: bid.uuid, + requestId: mapIdToRequestId(bid.id, bidRequest), cpm: bid.price, creativeId: bid.crid, currency: serverResponse.body.cur, @@ -77,9 +93,9 @@ export const spec = { }); }, - getUserSyncs: function (syncOptions, responses, gdprConsent, uspConsent) { + getUserSyncs: function (syncOptions, responses, gdprConsent, uspConsent, gppConsent) { const syncUrl = getSyncUrl(responses || []); - if (syncUrl && syncOptions.iframeEnabled && hasSyncConsent(gdprConsent, uspConsent)) { + if (syncUrl && syncOptions.iframeEnabled && hasSyncConsent(gdprConsent, uspConsent, gppConsent)) { return [{type: 'iframe', url: getSyncEndpoint(syncUrl, gdprConsent)}]; } }, @@ -89,7 +105,7 @@ registerBidder(spec); function getPage(bidderRequest) { return ( - getConfig(`${BIDDER_CODE}.page`) || deepAccess(bidderRequest, 'refererInfo.canonicalUrl') || window.location.href + getConfig(`${BIDDER_CODE}.page`) || deepAccess(bidderRequest, 'refererInfo.page') || window.location.href ); } @@ -101,6 +117,14 @@ function getTestFlag() { return getConfig(`${BIDDER_CODE}.test`) === true; } +function getLanguage() { + return navigator && navigator.language + ? navigator.language.indexOf('-') != -1 + ? navigator.language.split('-')[0] + : navigator.language + : undefined; +} + function getCurrencies() { const currencyOverrides = getConfig(`${BIDDER_CODE}.cur`); if (currencyOverrides !== undefined && (!isArray(currencyOverrides) || currencyOverrides.length === 0)) { @@ -130,14 +154,43 @@ function getPriceFloor(bidRequest, mediaType, size) { } } -function hasSyncConsent(gdprConsent, uspConsent) { - if (gdprConsent?.gdprApplies && !hasPurpose1Consent(gdprConsent)) { - return false; - } else if (uspConsent && uspConsent[1] === 'Y' && uspConsent[2] === 'Y') { - return false; - } else { - return true; +function getRefreshInformation(adUnitCode) { + const refresh = refreshes[adUnitCode]; + if (!refresh) { + refreshes[adUnitCode] = { + count: 0, + previousTime: new Date(), + }; + return undefined; } + + const currentTime = new Date(); + const timeDifferenceSeconds = Math.floor((currentTime - refresh.previousTime) / 1000); + refresh.count += 1; + refresh.previousTime = currentTime; + return { + count: refresh.count, + time: timeDifferenceSeconds, + }; +} + +function mapIdToRequestId(id, bidRequest) { + return bidRequest.bidderRequest.bids.filter((bid) => bid.adUnitCode === id)[0].bidId; +} + +function hasUspConsent(uspConsent) { + return typeof uspConsent !== 'string' || !(uspConsent[0] === '1' && uspConsent[2] === 'Y'); +} + +function hasGppConsent(gppConsent) { + return ( + !(gppConsent && Array.isArray(gppConsent.applicableSections)) || + gppConsent.applicableSections.every((section) => typeof section === 'number' && section <= 5) + ); +} + +function hasSyncConsent(gdprConsent, uspConsent, gppConsent) { + return hasPurpose1Consent(gdprConsent) && hasUspConsent(uspConsent) && hasGppConsent(gppConsent); } function getSyncUrl(responses) { diff --git a/modules/snigelBidAdapter.md b/modules/snigelBidAdapter.md index a83e133144f..f9bb1951d21 100644 --- a/modules/snigelBidAdapter.md +++ b/modules/snigelBidAdapter.md @@ -15,9 +15,13 @@ Please reach out to us [through our contact form](https://snigel.com/get-in-touc # Parameters -| Name | Required | Description | Example | -| :--- | :-------- | :---------- | :------ | -| placement | Yes | Placement identifier | top_leaderboard | +| Name | Required | Description | +| :-------- | :------- | :------------------- | +| accountId | Yes | Account identifier | +| site | Yes | Site identifier | +| placement | Yes | Placement identifier | + +Snigel will provide all of these parameters to you. # Test @@ -37,6 +41,8 @@ var adUnits = [ { bidder: "snigel", params: { + accountId: "1000", + site: "test.com", placement: "prebid_test_placement", }, }, diff --git a/modules/sonobiAnalyticsAdapter.js b/modules/sonobiAnalyticsAdapter.js index 0057944b201..04a855b5be6 100644 --- a/modules/sonobiAnalyticsAdapter.js +++ b/modules/sonobiAnalyticsAdapter.js @@ -6,7 +6,7 @@ import {ajaxBuilder} from '../src/ajax.js'; let ajax = ajaxBuilder(0); -const DEFAULT_EVENT_URL = 'apex.go.sonobi.com/keymaker'; +export const DEFAULT_EVENT_URL = 'apex.go.sonobi.com/keymaker'; const analyticsType = 'endpoint'; const QUEUE_TIMEOUT_DEFAULT = 200; const { diff --git a/modules/sonobiBidAdapter.js b/modules/sonobiBidAdapter.js index c99d0ab011d..1ce7665ddfc 100644 --- a/modules/sonobiBidAdapter.js +++ b/modules/sonobiBidAdapter.js @@ -1,11 +1,18 @@ import { registerBidder } from '../src/adapters/bidderFactory.js'; -import { parseSizesInput, logError, generateUUID, isEmpty, deepAccess, logWarn, logMessage, getGptSlotInfoForAdUnitCode, isFn, isPlainObject } from '../src/utils.js'; +import { parseSizesInput, logError, generateUUID, isEmpty, deepAccess, logWarn, logMessage, isFn, isPlainObject } from '../src/utils.js'; import { BANNER, VIDEO } from '../src/mediaTypes.js'; import { config } from '../src/config.js'; import { Renderer } from '../src/Renderer.js'; import { userSync } from '../src/userSync.js'; import { bidderSettings } from '../src/bidderSettings.js'; -import {getAllOrtbKeywords} from '../libraries/keywords/keywords.js'; +import { getAllOrtbKeywords } from '../libraries/keywords/keywords.js'; +import { getGptSlotInfoForAdUnitCode } from '../libraries/gptUtils/gptUtils.js'; + +/** + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid + */ + const BIDDER_CODE = 'sonobi'; const STR_ENDPOINT = 'https://apex.go.sonobi.com/trinity.json'; const PAGEVIEW_ID = generateUUID(); @@ -39,8 +46,8 @@ export const spec = { return false; } } else if (deepAccess(bid, 'mediaTypes.video')) { - if (deepAccess(bid, 'mediaTypes.video.context') === 'outstream' && !bid.params.sizes) { - // bids.params.sizes is required for outstream video adUnits + if (deepAccess(bid, 'mediaTypes.video.context') === 'outstream' && !deepAccess(bid, 'mediaTypes.video.playerSize')) { + // playerSize is required for outstream video adUnits return false; } if (deepAccess(bid, 'mediaTypes.video.context') === 'instream' && !deepAccess(bid, 'mediaTypes.video.playerSize')) { @@ -60,23 +67,15 @@ export const spec = { */ buildRequests: (validBidRequests, bidderRequest) => { const bids = validBidRequests.map(bid => { - let mediaType; - - if (deepAccess(bid, 'mediaTypes.video')) { - mediaType = 'video'; - } else if (deepAccess(bid, 'mediaTypes.banner')) { - mediaType = 'display'; - } - let slotIdentifier = _validateSlot(bid); if (/^[\/]?[\d]+[[\/].+[\/]?]?$/.test(slotIdentifier)) { slotIdentifier = slotIdentifier.charAt(0) === '/' ? slotIdentifier : '/' + slotIdentifier; return { - [`${slotIdentifier}|${bid.bidId}`]: `${_validateSize(bid)}|${_validateFloor(bid)}${_validateGPID(bid)}${_validateMediaType(mediaType)}` + [`${slotIdentifier}|${bid.bidId}`]: `${_validateSize(bid)}|${_validateFloor(bid)}${_validateGPID(bid)}${_validateMediaType(bid)}` } } else if (/^[0-9a-fA-F]{20}$/.test(slotIdentifier) && slotIdentifier.length === 20) { return { - [bid.bidId]: `${slotIdentifier}|${_validateSize(bid)}|${_validateFloor(bid)}${_validateGPID(bid)}${_validateMediaType(mediaType)}` + [bid.bidId]: `${slotIdentifier}|${_validateSize(bid)}|${_validateFloor(bid)}${_validateGPID(bid)}${_validateMediaType(bid)}` } } else { logError(`The ad unit code or Sonobi Placement id for slot ${bid.bidId} is invalid`); @@ -157,6 +156,11 @@ export const spec = { payload.coppa = 0; } + if (deepAccess(bidderRequest, 'ortb2.experianRtidData') && deepAccess(bidderRequest, 'ortb2.experianRtidKey')) { + payload.expData = deepAccess(bidderRequest, 'ortb2.experianRtidData'); + payload.expKey = deepAccess(bidderRequest, 'ortb2.experianRtidKey'); + } + // If there is no key_maker data, then don't make the request. if (isEmpty(data)) { return null; @@ -248,10 +252,7 @@ export const spec = { bidRequest, 'renderer.options' )); - let videoSize = deepAccess(bidRequest, 'params.sizes'); - if (Array.isArray(videoSize) && Array.isArray(videoSize[0])) { // handle case of multiple sizes - videoSize = videoSize[0]; // Only take the first size for outstream - } + let videoSize = deepAccess(bidRequest, 'mediaTypes.video.playerSize'); if (videoSize) { bids.width = videoSize[0]; bids.height = videoSize[1]; @@ -339,10 +340,28 @@ function _validateGPID(bid) { return '' } -function _validateMediaType(mediaType) { +function _validateMediaType(bidRequest) { + let mediaType; + if (deepAccess(bidRequest, 'mediaTypes.video')) { + mediaType = 'video'; + } else if (deepAccess(bidRequest, 'mediaTypes.banner')) { + mediaType = 'display'; + } + let mediaTypeValidation = ''; if (mediaType === 'video') { mediaTypeValidation = 'c=v,'; + if (deepAccess(bidRequest, 'mediaTypes.video.playbackmethod')) { + mediaTypeValidation = `${mediaTypeValidation}pm=${deepAccess(bidRequest, 'mediaTypes.video.playbackmethod').join(':')},`; + } + if (deepAccess(bidRequest, 'mediaTypes.video.placement')) { + let placement = deepAccess(bidRequest, 'mediaTypes.video.placement'); + mediaTypeValidation = `${mediaTypeValidation}p=${placement},`; + } + if (deepAccess(bidRequest, 'mediaTypes.video.plcmt')) { + let plcmt = deepAccess(bidRequest, 'mediaTypes.video.plcmt'); + mediaTypeValidation = `${mediaTypeValidation}pl=${plcmt},`; + } } else if (mediaType === 'display') { mediaTypeValidation = 'c=d,'; } diff --git a/modules/sovrnAnalyticsAdapter.md b/modules/sovrnAnalyticsAdapter.md index 80bc6d7f6b1..b4fe7c971a2 100644 --- a/modules/sovrnAnalyticsAdapter.md +++ b/modules/sovrnAnalyticsAdapter.md @@ -3,7 +3,7 @@ ``` Module Name: Sovrn Analytics Adapter Module Type: Analytics Adapter -Maintainer: jrosendahl@sovrn.com +Maintainer: exchange@sovrn.com ``` # Description diff --git a/modules/sovrnBidAdapter.js b/modules/sovrnBidAdapter.js index f21c4d1752a..e786095874e 100644 --- a/modules/sovrnBidAdapter.js +++ b/modules/sovrnBidAdapter.js @@ -1,13 +1,12 @@ import { _each, - getBidIdParameter, isArray, getUniqueIdentifierStr, deepSetValue, logError, deepAccess, isInteger, - logWarn + logWarn, getBidIdParameter } from '../src/utils.js'; import { registerBidder } from '../src/adapters/bidderFactory.js' import { @@ -16,6 +15,10 @@ import { VIDEO } from '../src/mediaTypes.js' +/** + * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid + */ + const ORTB_VIDEO_PARAMS = { 'mimes': (value) => Array.isArray(value) && value.length > 0 && value.every(v => typeof v === 'string'), 'minduration': (value) => isInteger(value), @@ -45,7 +48,6 @@ const ORTB_VIDEO_PARAMS = { const REQUIRED_VIDEO_PARAMS = { context: (value) => value !== ADPOD, mimes: ORTB_VIDEO_PARAMS.mimes, - minduration: ORTB_VIDEO_PARAMS.minduration, maxduration: ORTB_VIDEO_PARAMS.maxduration, protocols: ORTB_VIDEO_PARAMS.protocols } @@ -169,6 +171,16 @@ export const spec = { }; } + const tid = deepAccess(bidderRequest, 'ortb2.source.tid') + if (tid) { + deepSetValue(sovrnBidReq, 'source.tid', tid) + } + + const coppa = deepAccess(bidderRequest, 'ortb2.regs.coppa'); + if (coppa) { + deepSetValue(sovrnBidReq, 'regs.coppa', 1); + } + if (bidderRequest.gdprConsent) { deepSetValue(sovrnBidReq, 'regs.ext.gdpr', +bidderRequest.gdprConsent.gdprApplies); deepSetValue(sovrnBidReq, 'user.ext.consent', bidderRequest.gdprConsent.consentString) @@ -206,7 +218,7 @@ export const spec = { * Format Sovrn responses as Prebid bid responses * @param {id, seatbid} sovrnResponse A successful response from Sovrn. * @return {Bid[]} An array of formatted bids. - */ + */ interpretResponse: function({ body: {id, seatbid} }) { if (!id || !seatbid || !Array.isArray(seatbid)) return [] @@ -243,7 +255,7 @@ export const spec = { } }, - getUserSyncs: function(syncOptions, serverResponses, gdprConsent, uspConsent) { + getUserSyncs: function(syncOptions, serverResponses, gdprConsent, uspConsent, gppConsent) { try { const tracks = [] if (serverResponses && serverResponses.length !== 0) { @@ -257,6 +269,10 @@ export const spec = { if (uspConsent) { params.push(['us_privacy', uspConsent]); } + if (gppConsent) { + params.push(['gpp', gppConsent.gppString]); + params.push(['gpp_sid', gppConsent.applicableSections]) + } if (iidArr[0]) { params.push(['informer', iidArr[0]]); diff --git a/modules/sovrnBidAdapter.md b/modules/sovrnBidAdapter.md index 53e3158024d..ce131269eee 100644 --- a/modules/sovrnBidAdapter.md +++ b/modules/sovrnBidAdapter.md @@ -3,7 +3,7 @@ ``` Module Name: Sovrn Bid Adapter Module Type: Bidder Adapter -Maintainer: jrosendahl@sovrn.com +Maintainer: exchange@sovrn.com ``` # Description diff --git a/modules/sparteoBidAdapter.js b/modules/sparteoBidAdapter.js new file mode 100644 index 00000000000..0bccc1ec140 --- /dev/null +++ b/modules/sparteoBidAdapter.js @@ -0,0 +1,170 @@ +import { deepAccess, deepSetValue, logError, parseSizesInput, triggerPixel } from '../src/utils.js'; +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { BANNER, VIDEO } from '../src/mediaTypes.js'; +import {ortbConverter} from '../libraries/ortbConverter/converter.js' + +/** + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + */ + +const BIDDER_CODE = 'sparteo'; +const GVLID = 1028; +const TTL = 60; +const HTTP_METHOD = 'POST'; +const REQUEST_URL = 'https://bid.sparteo.com/auction'; +const USER_SYNC_URL_IFRAME = 'https://sync.sparteo.com/sync/iframe.html?from=prebidjs'; +let isSynced = window.sparteoCrossfire?.started || false; + +const converter = ortbConverter({ + context: { + // `netRevenue` and `ttl` are required properties of bid responses - provide a default for them + netRevenue: true, // or false if your adapter should set bidResponse.netRevenue = false + ttl: TTL // default bidResponse.ttl (when not specified in ORTB response.seatbid[].bid[].exp) + }, + request(buildRequest, imps, bidderRequest, context) { + const request = buildRequest(imps, bidderRequest, context); + + if (bidderRequest.bids[0].params.networkId) { + deepSetValue(request, 'site.publisher.ext.params.networkId', bidderRequest.bids[0].params.networkId); + } + + if (bidderRequest.bids[0].params.publisherId) { + deepSetValue(request, 'site.publisher.ext.params.publisherId', bidderRequest.bids[0].params.publisherId); + } + + return request; + }, + imp(buildImp, bidRequest, context) { + const imp = buildImp(bidRequest, context); + + deepSetValue(imp, 'ext.sparteo.params', bidRequest.params); + + return imp; + }, + bidResponse(buildBidResponse, bid, context) { + context.mediaType = deepAccess(bid, 'ext.prebid.type'); + + const response = buildBidResponse(bid, context); + + if (context.mediaType == 'video') { + response.nurl = bid.nurl; + response.vastUrl = deepAccess(bid, 'ext.prebid.cache.vastXml.url') ?? null; + } + + return response; + } +}); + +export const spec = { + code: BIDDER_CODE, + gvlid: GVLID, + supportedMediaTypes: [BANNER, VIDEO], + + /** + * Determines whether or not the given bid request is valid. + * + * @param {BidRequest} bid The bid params to validate. + * @return boolean True if this is a valid bid, and false otherwise. + */ + isBidRequestValid: function (bid) { + let bannerParams = deepAccess(bid, 'mediaTypes.banner'); + let videoParams = deepAccess(bid, 'mediaTypes.video'); + + if (!bid.params) { + logError('The bid params are missing'); + return false; + } + + if (!bid.params.networkId && !bid.params.publisherId) { + logError('The networkId or publisherId is required'); + return false; + } + + if (!bannerParams && !videoParams) { + logError('The placement must be of banner or video type'); + return false; + } + + /** + * BANNER checks + */ + + if (bannerParams) { + let sizes = bannerParams.sizes; + + if (!sizes || parseSizesInput(sizes).length == 0) { + logError('mediaTypes.banner.sizes must be set for banner placement at the right format.'); + return false; + } + } + + /** + * VIDEO checks + */ + + if (videoParams) { + if (parseSizesInput(videoParams.playerSize).length == 0) { + logError('mediaTypes.video.playerSize must be set for video placement at the right format.'); + return false; + } + } + + return true; + }, + + buildRequests: function (bidRequests, bidderRequest) { + const payload = converter.toORTB({bidRequests, bidderRequest}) + + return { + method: HTTP_METHOD, + url: bidRequests[0].params.endpoint ? bidRequests[0].params.endpoint : REQUEST_URL, + data: payload + }; + }, + + interpretResponse: function (serverResponse, requests) { + const bids = converter.fromORTB({response: serverResponse.body, request: requests.data}).bids; + + return bids; + }, + + getUserSyncs: function (syncOptions, serverResponses, gdprConsent, uspConsent) { + let syncurl = ''; + + if (!isSynced && !window.sparteoCrossfire?.started) { + // 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 += `&usp_consent=${uspConsent.consentString}`; + } + + if (syncOptions.iframeEnabled) { + isSynced = true; + + window.sparteoCrossfire = { + started: true + }; + + return [{ + type: 'iframe', + url: USER_SYNC_URL_IFRAME + syncurl + }]; + } + } + }, + + onTimeout: function (timeoutData) {}, + + onBidWon: function (bid) { + if (bid && bid.nurl) { + triggerPixel(bid.nurl, null); + } + }, + + onSetTargeting: function (bid) {} +}; + +registerBidder(spec); diff --git a/modules/sparteoBidAdapter.md b/modules/sparteoBidAdapter.md new file mode 100644 index 00000000000..774d9211d9d --- /dev/null +++ b/modules/sparteoBidAdapter.md @@ -0,0 +1,35 @@ +# Overview + +``` +Module Name: Sparteo Bidder Adapter +Module Type: Bidder Adapter +Maintainer: prebid@sparteo.com +``` + +# Description + +Module that connects to Sparteo's demand sources + +# Test Parameters +``` + var adUnits = [ + { + code: 'test-div', + mediaTypes: { + banner: { + sizes: [ + [1, 1] + ] + } + }, + bids: [ + { + bidder: 'sparteo', + params: { + networkId: '1234567a-eb1b-1fae-1d23-e1fbaef234cf' + } + } + ] + } + ]; +``` \ No newline at end of file diff --git a/modules/spotxBidAdapter.js b/modules/spotxBidAdapter.js index 874207adcf8..c1f1c5159fc 100644 --- a/modules/spotxBidAdapter.js +++ b/modules/spotxBidAdapter.js @@ -1,10 +1,32 @@ -import { logError, deepAccess, isArray, getBidIdParameter, getDNT, deepSetValue, isEmpty, _each, logMessage, logWarn, isBoolean, isNumber, isPlainObject, isFn, setScriptAttributes } from '../src/utils.js'; +import { + logError, + deepAccess, + isArray, + getDNT, + deepSetValue, + isEmpty, + _each, + logMessage, + logWarn, + isBoolean, + isNumber, + isPlainObject, + isFn, + setScriptAttributes, + getBidIdParameter +} from '../src/utils.js'; import { config } from '../src/config.js'; import { Renderer } from '../src/Renderer.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; import { VIDEO } from '../src/mediaTypes.js'; import { loadExternalScript } from '../src/adloader.js'; +/** + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid + * @typedef {import('../src/adapters/bidderFactory.js').ServerRequest} ServerRequest + */ + const BIDDER_CODE = 'spotx'; const URL = 'https://search.spotxchange.com/openrtb/2.3/dados/'; const ORTB_VERSION = '2.3'; diff --git a/modules/ssmasBidAdapter.js b/modules/ssmasBidAdapter.js new file mode 100644 index 00000000000..0b70a80e757 --- /dev/null +++ b/modules/ssmasBidAdapter.js @@ -0,0 +1,133 @@ +import { BANNER } from '../src/mediaTypes.js'; +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { triggerPixel, deepSetValue } from '../src/utils.js'; +import { ortbConverter } from '../libraries/ortbConverter/converter.js'; +import {config} from '../src/config.js'; + +export const SSMAS_CODE = 'ssmas'; +const SSMAS_SERVER = 'ads.ssmas.com'; +export const SSMAS_ENDPOINT = `https://${SSMAS_SERVER}/ortb`; +const SYNC_URL = `https://sync.ssmas.com/user_sync`; +export const SSMAS_REQUEST_METHOD = 'POST'; +const GDPR_VENDOR_ID = 1183; + +export const ssmasOrtbConverter = ortbConverter({ + context: { + netRevenue: true, + ttl: 300, + mediaType: BANNER, + }, + imp(buildImp, bidRequest, context) { + const imp = buildImp(bidRequest, context); + deepSetValue(imp, 'ext.placementId', bidRequest.params.placementId); + return imp; + }, +}); + +export const spec = { + code: SSMAS_CODE, + supportedMediaTypes: [BANNER], + gvlid: GDPR_VENDOR_ID, + + isBidRequestValid: (bid) => { + return !!bid.params.placementId && !!bid.bidId && bid.bidder === SSMAS_CODE; + }, + + buildRequests: (bidRequests, bidderRequest) => { + const data = ssmasOrtbConverter.toORTB({ bidRequests, bidderRequest }); + + const options = { + contentType: 'application/json', + withCredentials: false, + }; + + data.imp && data.imp.forEach(imp => { + if (imp.ext && imp.ext.placementId) { + imp.tagId = imp.ext.placementId; + } + }); + + data.regs = data.regs || {}; + data.regs.ext = data.regs.ext || {}; + + if (bidderRequest.gdprConsent) { + data.regs.ext.consent = bidderRequest.gdprConsent.consentString; + data.regs.ext.gdpr = bidderRequest.gdprConsent.gdprApplies ? 1 : 0; + } + if (bidderRequest.uspConsent) { + data.regs.ext.consent = bidderRequest.uspConsent.consentString; + data.regs.ext.ccpa = 1; + } + if (config.getConfig('coppa') === true) { + data.regs.coppa = 1; + } + + return [ + { + method: SSMAS_REQUEST_METHOD, + url: SSMAS_ENDPOINT, + data, + options, + }, + ]; + }, + + interpretResponse: (serverResponse, bidRequest) => { + const bids = ssmasOrtbConverter.fromORTB({ + response: serverResponse.body, + request: bidRequest.data, + }).bids; + + return bids.filter((bid) => { + return bid.cpm > 0; + }); + }, + + onBidWon: (bid) => { + if (bid.burl) { + triggerPixel(bid.burl); + } + }, + + getUserSyncs: ( + syncOptions, + serverResponses, + gdprConsent, + uspConsent + ) => { + const syncs = []; + + let params = ['pbjs=1']; + + if (gdprConsent) { + if (typeof gdprConsent.gdprApplies === 'boolean') { + params.push(`gdpr=${Boolean(gdprConsent.gdprApplies)}&gdpr_consent=${ + gdprConsent.consentString + }`); + } else { + params.push(`gdpr_consent=${gdprConsent.consentString}`); + } + } + + if (uspConsent && uspConsent.consentString) { + params.push(`ccpa_consent=${uspConsent.consentString}`); + } + + if (syncOptions.iframeEnabled && serverResponses.length > 0) { + syncs.push({ + type: 'iframe', + url: `${SYNC_URL}/iframe?${params.join('&')}` + }); + } + + // if (syncOptions.pixelEnabled && serverResponses.length > 0) { + // syncs.push({ + // type: 'image', + // url: `${SYNC_URL}/image?${params.join('&')}` + // }); + // } + return syncs; + }, +}; + +registerBidder(spec); diff --git a/modules/ssmasBidAdapter.md b/modules/ssmasBidAdapter.md new file mode 100644 index 00000000000..1b15764a54d --- /dev/null +++ b/modules/ssmasBidAdapter.md @@ -0,0 +1,35 @@ +# Overview + +Module Name: SSMas Bidder Adapter +Module Type: Bidder Adapter +Maintainer: hzchen.work@gmail.com + +# Description + +Module that connects to Sem Seo & Mas header bidding endpoint to fetch bids. +Supports Banner +Supported currencies: EUR + +Required parameters: +- placement id + +# Test Parameters +``` +var adUnits = [ + // Banner adUnit + { + code: 'banner-div', + mediaTypes: { + banner: { + sizes: [[300, 250]] + } + }, + bids: [{ + bidder: 'ssmas', + params: { + placementId: "10336" + } + }] + } +]; +``` diff --git a/modules/sspBCBidAdapter.js b/modules/sspBCBidAdapter.js index 93ce26e7a48..c351b76d7ea 100644 --- a/modules/sspBCBidAdapter.js +++ b/modules/sspBCBidAdapter.js @@ -12,13 +12,14 @@ const SYNC_URL = 'https://ssp.wp.pl/bidder/usersync'; const NOTIFY_URL = 'https://ssp.wp.pl/bidder/notify'; const GVLID = 676; const TMAX = 450; -const BIDDER_VERSION = '5.8'; +const BIDDER_VERSION = '5.92'; const DEFAULT_CURRENCY = 'PLN'; const W = window; const { navigator } = W; const oneCodeDetection = {}; const adUnitsCalled = {}; const adSizesCalled = {}; +const bidderRequestsMap = {}; const pageView = {}; var consentApiVersion; @@ -37,7 +38,7 @@ var nativeAssetMap = { /** * return native asset type, based on asset id - * @param {int} id - native asset id + * @param {number} id - native asset id * @returns {string} asset type */ const getNativeAssetType = id => { @@ -93,19 +94,17 @@ const getNotificationPayload = bidData => { const bids = isArray(bidData) ? bidData : [bidData]; if (bids.length > 0) { let result = { - requestId: undefined, siteId: [], slotId: [], tagid: [], } bids.forEach(bid => { - const { adUnitCode, auctionId, cpm, creativeId, meta, params: bidParams, requestId, timeout } = bid; + const { adUnitCode, cpm, creativeId, meta, mediaType, params: bidParams, bidderRequestId, requestId, timeout } = bid; const params = unpackParams(bidParams); // basic notification data const bidBasicData = { - // TODO: fix auctionId leak: https://github.com/prebid/Prebid.js/issues/9781 - requestId: auctionId || result.requestId, + requestId: bidderRequestId || bidderRequestsMap[requestId], timeout: timeout || result.timeout, pvid: pageView.id, } @@ -127,12 +126,14 @@ const getNotificationPayload = bidData => { if (cpm) { // non-empty bid data + const { advertiserDomains = [], networkName, pricepl } = meta; const bidNonEmptyData = { cpm, - cpmpl: meta && meta.pricepl, + cpmpl: pricepl, creativeId, - adomain: meta && meta.advertiserDomains && meta.advertiserDomains[0], - networkName: meta && meta.networkName, + adomain: advertiserDomains[0], + adtype: mediaType, + networkName, } result = { ...result, ...bidNonEmptyData } } @@ -163,7 +164,7 @@ const applyClientHints = ortbRequest => { Check / generate page view id Should be generated dureing first call to applyClientHints(), and re-generated if pathname has changed - */ + */ if (!pageView.id || location.pathname !== pageView.path) { pageView.path = location.pathname; pageView.id = Math.floor(1E20 * Math.random()).toString(); @@ -198,6 +199,22 @@ const applyClientHints = ortbRequest => { ortbRequest.user = { ...ortbRequest.user, ...ch }; }; +const applyTopics = (validBidRequest, ortbRequest) => { + const userData = validBidRequest.ortb2?.user?.data || []; + const topicsData = userData.filter(dataObj => { + const segtax = dataObj.ext?.segtax; + return segtax >= 600 && segtax <= 609; + })[0]; + + // format topics obj for exchange + if (topicsData) { + topicsData.id = `${topicsData.ext.segtax}`; + topicsData.name = 'topics'; + delete (topicsData.ext); + ortbRequest.user.data.push(topicsData); + } +}; + const applyUserIds = (validBidRequest, ortbRequest) => { const eids = validBidRequest.userIdAsEids if (eids && eids.length) { @@ -447,10 +464,15 @@ var mapVideo = (slot, videoFromBid) => { }; const mapImpression = slot => { - const { adUnitCode, bidId, params = {}, ortb2Imp = {} } = slot; + const { adUnitCode, bidderRequestId, bidId, params = {}, ortb2Imp = {} } = slot; const { id, siteId, video } = params; const { ext = {} } = ortb2Imp; + /* + store bidId <-> bidderRequestId mapping for bidWon notification + */ + bidderRequestsMap[bidId] = bidderRequestId; + /* check max size for this imp, and check/store number this size was called (for current view) send this info as ext.pbsize @@ -484,7 +506,7 @@ const mapImpression = slot => { } const isVideoAd = bid => { - const xmlTester = new RegExp(/^<\?xml/); + const xmlTester = new RegExp(/^<\?xml| { return bid.admNative || (bid.adm && bid.adm.match(xmlTester)); } -const parseNative = (nativeData) => { +const parseNative = (nativeData, adUnitCode) => { const { link = {}, imptrackers: impressionTrackers, jstracker } = nativeData; const { url: clickUrl, clicktrackers: clickTrackers = [] } = link; + const macroReplacer = tracker => tracker.replace(new RegExp('%native_dom_id%', 'g'), adUnitCode); + let javascriptTrackers = isArray(jstracker) ? jstracker : jstracker && [jstracker]; + + // replace known macros in js trackers + javascriptTrackers = javascriptTrackers && javascriptTrackers.map(macroReplacer); const result = { clickUrl, clickTrackers, impressionTrackers, - javascriptTrackers: isArray(jstracker) ? jstracker : jstracker && [jstracker], + javascriptTrackers, }; nativeData.assets.forEach(asset => { @@ -627,6 +654,8 @@ const spec = { return true; }, buildRequests(validBidRequests, bidderRequest) { + logWarn('DEBUG: buildRequests', bidderRequest.auctionId, bidderRequest.bidderRequestId); + // convert Native ORTB definition to old-style prebid native definition validBidRequests = convertOrtbRequestToProprietaryNative(validBidRequests); @@ -644,7 +673,7 @@ const spec = { const ref = bidderRequest.refererInfo.ref; const payload = { - id: bidderRequest.auctionId, + id: bidderRequest.bidderRequestId, site: { id: siteId ? `${siteId}` : undefined, publisher: publisherId ? { id: publisherId } : undefined, @@ -669,6 +698,7 @@ const spec = { applyGdpr(bidderRequest, payload); applyClientHints(payload); applyUserIds(validBidRequests[0], payload); + applyTopics(bidderRequest, payload); return { method: 'POST', @@ -725,6 +755,7 @@ const spec = { if (bidRequest && site.id && !strIncludes(site.id, 'bidid')) { // found a matching request; add this bid + const { adUnitCode } = bidRequest; // store site data for future notification oneCodeDetection[bidId] = [site.id, site.slot]; @@ -760,7 +791,7 @@ const spec = { // check native object try { const nativeData = serverBid.admNative || JSON.parse(serverBid.adm).native; - bid.native = parseNative(nativeData); + bid.native = parseNative(nativeData, adUnitCode); bid.width = 1; bid.height = 1; } catch (err) { @@ -807,6 +838,15 @@ const spec = { } }, + onBidViewable(bid) { + const payload = getNotificationPayload(bid); + if (payload) { + payload.event = 'bidViewable'; + sendNotification(payload); + return payload; + } + }, + onBidWon(bid) { const payload = getNotificationPayload(bid); if (payload) { diff --git a/modules/stroeerCoreBidAdapter.js b/modules/stroeerCoreBidAdapter.js index 0737bec5a12..307a50c7f78 100644 --- a/modules/stroeerCoreBidAdapter.js +++ b/modules/stroeerCoreBidAdapter.js @@ -1,4 +1,4 @@ -import {buildUrl, deepAccess, getWindowSelf, getWindowTop, isEmpty, isStr, logWarn} from '../src/utils.js'; +import { buildUrl, deepAccess, generateUUID, getWindowSelf, getWindowTop, isEmpty, isStr, logWarn } from '../src/utils.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import {BANNER, VIDEO} from '../src/mediaTypes.js'; import {find} from '../src/polyfill.js'; @@ -50,8 +50,7 @@ export const spec = { const refererInfo = bidderRequest.refererInfo; const basePayload = { - // TODO: fix auctionId leak: https://github.com/prebid/Prebid.js/issues/9781 - id: bidderRequest.auctionId, + id: generateUUID(), ref: refererInfo.ref, ssl: isSecureWindow(), mpa: isMainPageAccessible(), diff --git a/modules/stvBidAdapter.js b/modules/stvBidAdapter.js index 8d368b996fc..5cffc5853b5 100644 --- a/modules/stvBidAdapter.js +++ b/modules/stvBidAdapter.js @@ -3,6 +3,10 @@ import {registerBidder} from '../src/adapters/bidderFactory.js'; import {BANNER, VIDEO} from '../src/mediaTypes.js'; import {includes} from '../src/polyfill.js'; +/** + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + */ + const BIDDER_CODE = 'stv'; const ENDPOINT_URL = 'https://ads.smartstream.tv/r/'; const ENDPOINT_URL_DEV = 'https://ads.smartstream.tv/r/'; @@ -51,6 +55,7 @@ export const spec = { bid_id: bidId, pbver: '$prebid.version$', schain: '', + uids: '', }; if (!isVideoRequest(bidRequest)) { payload._f = 'html'; @@ -61,6 +66,11 @@ export const spec = { delete payload.schain; } + payload.uids = serializeUids(bidRequest); + if (payload.uids == '') { + delete payload.uids; + } + payload.pfilter = { ...params }; delete payload.pfilter.placement; if (params.bcat !== undefined) { delete payload.pfilter.bcat; } @@ -201,7 +211,7 @@ function objectToQueryString(obj, prefix) { let v = obj[p]; str.push((v !== null && typeof v === 'object') ? objectToQueryString(v, k) - : (k == 'schain' ? k + '=' + v : encodeURIComponent(k) + '=' + encodeURIComponent(v))); + : (k == 'schain' || k == 'uids' ? k + '=' + v : encodeURIComponent(k) + '=' + encodeURIComponent(v))); } } return str.join('&'); @@ -227,13 +237,60 @@ function serializeSChain(schain) { ret += encodeURIComponent(node.name ?? ''); ret += ','; ret += encodeURIComponent(node.domain ?? ''); - ret += ','; - ret += encodeURIComponent(node.ext ?? ''); + if (node.ext) { + ret += ','; + ret += encodeURIComponent(node.ext ?? ''); + } } return ret; } +function serializeUids(bidRequest) { + let uids = []; + + let id5 = deepAccess(bidRequest, 'userId.id5id.uid'); + if (id5) { + uids.push(encodeURIComponent('id5:' + id5)); + let id5Linktype = deepAccess(bidRequest, 'userId.id5id.ext.linkType'); + if (id5Linktype) { + uids.push(encodeURIComponent('id5_linktype:' + id5Linktype)); + } + } + let netId = deepAccess(bidRequest, 'userId.netId'); + if (netId) { + uids.push(encodeURIComponent('netid:' + netId)); + } + let uId2 = deepAccess(bidRequest, 'userId.uid2.id'); + if (uId2) { + uids.push(encodeURIComponent('uid2:' + uId2)); + } + let sharedId = deepAccess(bidRequest, 'userId.sharedid.id'); + if (sharedId) { + uids.push(encodeURIComponent('sharedid:' + sharedId)); + } + let liverampId = deepAccess(bidRequest, 'userId.idl_env'); + if (liverampId) { + uids.push(encodeURIComponent('liverampid:' + liverampId)); + } + let criteoId = deepAccess(bidRequest, 'userId.criteoId'); + if (criteoId) { + uids.push(encodeURIComponent('criteoid:' + criteoId)); + } + // documentation missing... + let utiqId = deepAccess(bidRequest, 'userId.utiq.id'); + if (utiqId) { + uids.push(encodeURIComponent('utiq:' + utiqId)); + } else { + utiqId = deepAccess(bidRequest, 'userId.utiq'); + if (utiqId) { + uids.push(encodeURIComponent('utiq:' + utiqId)); + } + } + + return uids.join(','); +} + /** * Check if it's a banner bid request * diff --git a/modules/sublimeBidAdapter.js b/modules/sublimeBidAdapter.js index db2b02aaef1..a29265ce9cd 100644 --- a/modules/sublimeBidAdapter.js +++ b/modules/sublimeBidAdapter.js @@ -2,6 +2,12 @@ import { logInfo, generateUUID, formatQS, triggerPixel, deepAccess } from '../sr import { registerBidder } from '../src/adapters/bidderFactory.js'; import { config } from '../src/config.js'; +/** + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid + * @typedef {import('../src/adapters/bidderFactory.js').ServerRequest} ServerRequest + */ + const BIDDER_CODE = 'sublime'; const BIDDER_GVLID = 114; const DEFAULT_BID_HOST = 'pbjs.sskzlabs.com'; diff --git a/modules/taboolaBidAdapter.js b/modules/taboolaBidAdapter.js index 8a42013b0be..3b83514b6fd 100644 --- a/modules/taboolaBidAdapter.js +++ b/modules/taboolaBidAdapter.js @@ -3,18 +3,23 @@ import {registerBidder} from '../src/adapters/bidderFactory.js'; import {BANNER} from '../src/mediaTypes.js'; import {config} from '../src/config.js'; -import {deepAccess, getWindowSelf, replaceAuctionPrice} from '../src/utils.js'; +import {deepAccess, deepSetValue, getWindowSelf, replaceAuctionPrice} from '../src/utils.js'; import {getStorageManager} from '../src/storageManager.js'; import {ajax} from '../src/ajax.js'; +import {ortbConverter} from '../libraries/ortbConverter/converter.js'; const BIDDER_CODE = 'taboola'; const GVLID = 42; const CURRENCY = 'USD'; export const END_POINT_URL = 'https://display.bidder.taboola.com/OpenRTB/TaboolaHB/auction'; export const USER_SYNC_IMG_URL = 'https://trc.taboola.com/sg/prebidJS/1/cm'; +export const USER_SYNC_IFRAME_URL = 'https://cdn.taboola.com/scripts/prebid_iframe_sync.html'; const USER_ID = 'user-id'; const STORAGE_KEY = `taboola global:${USER_ID}`; const COOKIE_KEY = 'trc_cookie_storage'; +const TGID_COOKIE_KEY = 't_gid'; +const TBLA_ID_COOKIE_KEY = 'tbla_id'; +export const EVENT_ENDPOINT = 'https://beacon.bidder.taboola.com'; /** * extract User Id by that order: @@ -38,10 +43,18 @@ export const userData = { const {cookiesAreEnabled, getCookie} = userData.storageManager; if (cookiesAreEnabled()) { const cookieData = getCookie(COOKIE_KEY); - const userId = userData.getCookieDataByKey(cookieData, USER_ID); + let userId = userData.getCookieDataByKey(cookieData, USER_ID); if (userId) { return userId; } + userId = getCookie(TGID_COOKIE_KEY); + if (userId) { + return userId; + } + const tblaId = getCookie(TBLA_ID_COOKIE_KEY); + if (tblaId) { + return tblaId; + } } }, getCookieDataByKey(cookieData, key) { @@ -69,6 +82,30 @@ export const internal = { } } +const converter = ortbConverter({ + context: { + netRevenue: true, + mediaType: BANNER, + ttl: 300 + }, + imp(buildImp, bidRequest, context) { + let imp = buildImp(bidRequest, context); + fillTaboolaImpData(bidRequest, imp); + return imp; + }, + request(buildRequest, imps, bidderRequest, context) { + const reqData = buildRequest(imps, bidderRequest, context); + fillTaboolaReqData(bidderRequest, context.bidRequests[0], reqData) + return reqData; + }, + bidResponse(buildBidResponse, bid, context) { + const bidResponse = buildBidResponse(bid, context); + bidResponse.nurl = bid.nurl; + bidResponse.ad = replaceAuctionPrice(bid.adm, bid.price); + return bidResponse + } +}); + export const spec = { supportedMediaTypes: [BANNER], gvlid: GVLID, @@ -81,85 +118,35 @@ export const spec = { }, buildRequests: (validBidRequests, bidderRequest) => { const [bidRequest] = validBidRequests; - const {refererInfo, gdprConsent = {}, uspConsent} = bidderRequest; + const data = converter.toORTB({bidderRequest: bidderRequest, bidRequests: validBidRequests}); const {publisherId} = bidRequest.params; - const site = getSiteProperties(bidRequest.params, refererInfo, bidderRequest.ortb2); - const device = {ua: navigator.userAgent}; - const imps = getImps(validBidRequests); - const user = { - buyeruid: userData.getUserId(gdprConsent, uspConsent), - ext: {} - }; - const regs = { - coppa: 0, - ext: {} - }; - - if (gdprConsent.gdprApplies) { - user.ext.consent = bidderRequest.gdprConsent.consentString; - regs.ext.gdpr = 1; - } - - if (uspConsent) { - regs.ext.us_privacy = uspConsent; - } - - if (bidderRequest.ortb2?.regs?.gpp) { - regs.ext.gpp = bidderRequest.ortb2.regs.gpp; - regs.ext.gpp_sid = bidderRequest.ortb2.regs.gpp_sid; - } - - if (config.getConfig('coppa')) { - regs.coppa = 1; - } - - const ortb2 = bidderRequest.ortb2 || { - bcat: [], - badv: [], - wlang: [] - }; - - const request = { - id: bidderRequest.bidderRequestId, - imp: imps, - site, - device, - source: {fd: 1}, - tmax: bidderRequest.timeout, - bcat: ortb2.bcat || bidRequest.params.bcat || [], - badv: ortb2.badv || bidRequest.params.badv || [], - wlang: ortb2.wlang || bidRequest.params.wlang || [], - user, - regs, - ext: { - pageType: ortb2?.ext?.data?.pageType || ortb2?.ext?.data?.section || bidRequest.params.pageType - } - }; - - const url = [END_POINT_URL, publisherId].join('/'); + const url = END_POINT_URL + '?publisher=' + publisherId; return { url, method: 'POST', - data: JSON.stringify(request), + data: data, bids: validBidRequests, options: { withCredentials: false }, }; }, - interpretResponse: (serverResponse, {bids}) => { - if (!bids) { + interpretResponse: (serverResponse, request) => { + if (!request || !request.bids || !request.data) { return []; } - const {bidResponses, cur: currency} = getBidResponses(serverResponse); + if (!serverResponse || !serverResponse.body) { + return []; + } - if (!bidResponses) { + if (!serverResponse.body.seatbid || !serverResponse.body.seatbid.length || !serverResponse.body.seatbid[0].bid || !serverResponse.body.seatbid[0].bid.length) { return []; } - return bidResponses.map((bidResponse) => getBid(bids, currency, bidResponse)).filter(Boolean); + const bids = converter.fromORTB({response: serverResponse.body, request: request.data}).bids; + return bids; }, onBidWon: (bid) => { if (bid.nurl) { @@ -182,6 +169,13 @@ export const spec = { queryParams.push('gpp=' + encodeURIComponent(gppConsent)); } + if (syncOptions.iframeEnabled) { + syncs.push({ + type: 'iframe', + url: USER_SYNC_IFRAME_URL + (queryParams.length ? '?' + queryParams.join('&') : '') + }); + } + if (syncOptions.pixelEnabled) { syncs.push({ type: 'image', @@ -190,6 +184,13 @@ export const spec = { } return syncs; }, + onTimeout: (timeoutData) => { + ajax(EVENT_ENDPOINT + '/timeout', null, JSON.stringify(timeoutData), {method: 'POST'}); + }, + + onBidderError: ({ error, bidderRequest }) => { + ajax(EVENT_ENDPOINT + '/bidError', null, JSON.stringify(error, bidderRequest), {method: 'POST'}); + }, }; function getSiteProperties({publisherId}, refererInfo, ortb2) { @@ -209,34 +210,76 @@ function getSiteProperties({publisherId}, refererInfo, ortb2) { } } -function getImps(validBidRequests) { - return validBidRequests.map((bid, id) => { - const {tagId, position} = bid.params; - const imp = { - id: id + 1, - banner: getBanners(bid, position), - tagid: tagId - } - if (typeof bid.getFloor === 'function') { - const floorInfo = bid.getFloor({ - currency: CURRENCY, - mediaType: BANNER, - size: '*' - }); - if (typeof floorInfo === 'object' && floorInfo.currency === CURRENCY && !isNaN(parseFloat(floorInfo.floor))) { - imp.bidfloor = parseFloat(floorInfo.floor); - imp.bidfloorcur = CURRENCY; - } - } else { - const {bidfloor = null, bidfloorcur = CURRENCY} = bid.params; - imp.bidfloor = bidfloor; - imp.bidfloorcur = bidfloorcur; - } - imp['ext'] = { - gpid: deepAccess(bid, 'ortb2Imp.ext.gpid') +function fillTaboolaReqData(bidderRequest, bidRequest, data) { + const {refererInfo, gdprConsent = {}, uspConsent} = bidderRequest; + const site = getSiteProperties(bidRequest.params, refererInfo, bidderRequest.ortb2); + const device = {ua: navigator.userAgent}; + const user = { + buyeruid: userData.getUserId(gdprConsent, uspConsent), + ext: {} + }; + const regs = { + coppa: 0, + ext: {} + }; + + if (gdprConsent.gdprApplies) { + user.ext.consent = bidderRequest.gdprConsent.consentString; + regs.ext.gdpr = 1; + } + + if (uspConsent) { + regs.ext.us_privacy = uspConsent; + } + + if (bidderRequest.ortb2?.regs?.gpp) { + regs.ext.gpp = bidderRequest.ortb2.regs.gpp; + regs.ext.gpp_sid = bidderRequest.ortb2.regs.gpp_sid; + } + + if (config.getConfig('coppa')) { + regs.coppa = 1; + } + + const ortb2 = bidderRequest.ortb2 || { + bcat: [], + badv: [], + wlang: [] + }; + + data.id = bidderRequest.bidderRequestId; + data.site = site; + data.device = device; + data.source = {fd: 1}; + data.tmax = (bidderRequest.timeout == undefined) ? undefined : parseInt(bidderRequest.timeout); + data.bcat = ortb2.bcat || bidRequest.params.bcat || []; + data.badv = ortb2.badv || bidRequest.params.badv || []; + data.wlang = ortb2.wlang || bidRequest.params.wlang || []; + data.user = user; + data.regs = regs; + deepSetValue(data, 'ext.pageType', ortb2?.ext?.data?.pageType || ortb2?.ext?.data?.section || bidRequest.params.pageType); +} + +function fillTaboolaImpData(bid, imp) { + const {tagId, position} = bid.params; + imp.banner = getBanners(bid, position); + imp.tagid = tagId; + + if (typeof bid.getFloor === 'function') { + const floorInfo = bid.getFloor({ + currency: CURRENCY, + size: '*' + }); + if (typeof floorInfo === 'object' && floorInfo.currency === CURRENCY && !isNaN(parseFloat(floorInfo.floor))) { + imp.bidfloor = parseFloat(floorInfo.floor); + imp.bidfloorcur = CURRENCY; } - return imp; - }); + } else { + const {bidfloor = null, bidfloorcur = CURRENCY} = bid.params; + imp.bidfloor = bidfloor; + imp.bidfloorcur = bidfloorcur; + } + deepSetValue(imp, 'ext.gpid', deepAccess(bid, 'ortb2Imp.ext.gpid')); } function getBanners(bid, pos) { @@ -257,51 +300,4 @@ function getSizes(sizes) { } } -function getBidResponses({body}) { - if (!body) { - return []; - } - - const {seatbid, cur} = body; - - if (!seatbid.length || !seatbid[0].bid || !seatbid[0].bid.length) { - return []; - } - - return { - bidResponses: seatbid[0].bid, - cur - }; -} - -function getBid(bids, currency, bidResponse) { - if (!bidResponse) { - return; - } - let { - price: cpm, nurl, crid: creativeId, adm: ad, w: width, h: height, exp: ttl, adomain: advertiserDomains, meta = {} - } = bidResponse; - let requestId = bids[bidResponse.impid - 1].bidId; - if (advertiserDomains && advertiserDomains.length > 0) { - meta.advertiserDomains = advertiserDomains - } - - ad = replaceAuctionPrice(ad, cpm); - - return { - requestId, - ttl, - mediaType: BANNER, - cpm, - creativeId, - currency, - ad, - width, - height, - meta, - nurl, - netRevenue: true - }; -} - registerBidder(spec); diff --git a/modules/tagorasBidAdapter.js b/modules/tagorasBidAdapter.js new file mode 100644 index 00000000000..0138ba3daf9 --- /dev/null +++ b/modules/tagorasBidAdapter.js @@ -0,0 +1,342 @@ +import {_each, deepAccess, parseSizesInput, parseUrl, uniques, isFn} from '../src/utils.js'; +import {registerBidder} from '../src/adapters/bidderFactory.js'; +import {BANNER, VIDEO} from '../src/mediaTypes.js'; +import {getStorageManager} from '../src/storageManager.js'; +import {config} from '../src/config.js'; + +const DEFAULT_SUB_DOMAIN = 'exchange'; +const BIDDER_CODE = 'tagoras'; +const BIDDER_VERSION = '1.0.0'; +const CURRENCY = 'USD'; +const TTL_SECONDS = 60 * 5; +const UNIQUE_DEAL_ID_EXPIRY = 1000 * 60 * 15; +const storage = getStorageManager({bidderCode: BIDDER_CODE}); + +function getTopWindowQueryParams() { + try { + const parsedUrl = parseUrl(window.top.document.URL, {decodeSearchAsString: true}); + return parsedUrl.search; + } catch (e) { + return ''; + } +} + +export function createDomain(subDomain = DEFAULT_SUB_DOMAIN) { + return `https://${subDomain}.tagoras.io`; +} + +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, + ortb2Imp, + bidderRequestId, + bidRequestsCount, + bidderRequestsCount, + bidderWinsCount + } = bid; + let {bidFloor, ext} = params; + const hashUrl = hashCode(topWindowUrl); + const uniqueDealId = getUniqueDealId(hashUrl); + const cId = extractCID(params); + const pId = extractPID(params); + const subDomain = extractSubDomain(params); + + const gpid = deepAccess(bid, 'ortb2Imp.ext.gpid', deepAccess(bid, 'ortb2Imp.ext.data.pbadslot', '')); + + if (isFn(bid.getFloor)) { + const floorInfo = bid.getFloor({ + currency: 'USD', + mediaType: '*', + size: '*' + }); + + if (floorInfo.currency === 'USD') { + bidFloor = floorInfo.floor; + } + } + + let data = { + url: encodeURIComponent(topWindowUrl), + uqs: getTopWindowQueryParams(), + cb: Date.now(), + bidFloor: bidFloor, + bidId: bidId, + referrer: bidderRequest.refererInfo.ref, + adUnitCode: adUnitCode, + publisherId: pId, + sizes: sizes, + uniqueDealId: uniqueDealId, + bidderVersion: BIDDER_VERSION, + prebidVersion: '$prebid.version$', + res: `${screen.width}x${screen.height}`, + schain: schain, + mediaTypes: mediaTypes, + gpid: gpid, + transactionId: ortb2Imp?.ext?.tid, + bidderRequestId: bidderRequestId, + bidRequestsCount: bidRequestsCount, + bidderRequestsCount: bidderRequestsCount, + bidderWinsCount: bidderWinsCount, + bidderTimeout: bidderTimeout + }; + + appendUserIdsToRequestPayload(data, userId); + + const sua = deepAccess(bidderRequest, 'ortb2.device.sua'); + + if (sua) { + data.sua = sua; + } + + if (bidderRequest.gdprConsent) { + if (bidderRequest.gdprConsent.consentString) { + data.gdprConsent = bidderRequest.gdprConsent.consentString; + } + if (bidderRequest.gdprConsent.gdprApplies !== undefined) { + data.gdpr = bidderRequest.gdprConsent.gdprApplies ? 1 : 0; + } + } + if (bidderRequest.uspConsent) { + data.usPrivacy = bidderRequest.uspConsent; + } + + if (bidderRequest.gppConsent) { + data.gppString = bidderRequest.gppConsent.gppString; + data.gppSid = bidderRequest.gppConsent.applicableSections; + } else if (bidderRequest.ortb2?.regs?.gpp) { + data.gppString = bidderRequest.ortb2.regs.gpp; + data.gppSid = bidderRequest.ortb2.regs.gpp_sid; + } + + const dto = { + method: 'POST', + url: `${createDomain(subDomain)}/prebid/multi/${cId}`, + data: data + }; + + _each(ext, (value, key) => { + dto.data['ext.' + key] = value; + }); + + return dto; +} + +function appendUserIdsToRequestPayload(payloadRef, userIds) { + let key; + _each(userIds, (userId, idSystemProviderName) => { + key = `uid.${idSystemProviderName}`; + + switch (idSystemProviderName) { + case 'digitrustid': + payloadRef[key] = deepAccess(userId, 'data.id'); + break; + case 'lipb': + payloadRef[key] = userId.lipbid; + break; + case 'parrableId': + payloadRef[key] = userId.eid; + break; + case 'id5id': + payloadRef[key] = userId.uid; + break; + default: + payloadRef[key] = userId; + } + }); +} + +function buildRequests(validBidRequests, bidderRequest) { + const topWindowUrl = bidderRequest.refererInfo.page || bidderRequest.refererInfo.topmostLocation; + const bidderTimeout = config.getConfig('bidderTimeout'); + const requests = []; + validBidRequests.forEach(validBidRequest => { + const sizes = parseSizesInput(validBidRequest.sizes); + const request = buildRequest(validBidRequest, topWindowUrl, sizes, bidderRequest, bidderTimeout); + requests.push(request); + }); + return requests; +} + +function interpretResponse(serverResponse, request) { + if (!serverResponse || !serverResponse.body) { + return []; + } + const {bidId} = request.data; + const {results} = serverResponse.body; + + let output = []; + + try { + results.forEach(result => { + const { + creativeId, + ad, + price, + exp, + width, + height, + currency, + metaData, + advertiserDomains, + mediaType = BANNER + } = result; + if (!ad || !price) { + return; + } + + const response = { + requestId: bidId, + cpm: price, + width: width, + height: height, + creativeId: creativeId, + currency: currency || CURRENCY, + netRevenue: true, + ttl: exp || TTL_SECONDS, + }; + + if (metaData) { + Object.assign(response, { + meta: metaData + }) + } else { + Object.assign(response, { + meta: { + advertiserDomains: advertiserDomains || [] + } + }) + } + + if (mediaType === BANNER) { + Object.assign(response, { + ad: ad, + }); + } else { + Object.assign(response, { + vastXml: ad, + mediaType: VIDEO + }); + } + output.push(response); + }); + return output; + } catch (e) { + return []; + } +} + +function getUserSyncs(syncOptions, responses, gdprConsent = {}, uspConsent = '', gppConsent = {}) { + let syncs = []; + const {iframeEnabled, pixelEnabled} = syncOptions; + const {gdprApplies, consentString = ''} = gdprConsent; + const {gppString, applicableSections} = gppConsent; + + const cidArr = responses.filter(resp => deepAccess(resp, 'body.cid')).map(resp => resp.body.cid).filter(uniques); + let params = `?cid=${encodeURIComponent(cidArr.join(','))}&gdpr=${gdprApplies ? 1 : 0}&gdpr_consent=${encodeURIComponent(consentString || '')}&us_privacy=${encodeURIComponent(uspConsent || '')}` + + if (gppString && applicableSections?.length) { + params += '&gpp=' + encodeURIComponent(gppString); + params += '&gpp_sid=' + encodeURIComponent(applicableSections.join(',')); + } + + if (iframeEnabled) { + syncs.push({ + type: 'iframe', + url: `https://sync.tagoras.io/api/sync/iframe/${params}` + }); + } else if (pixelEnabled) { + syncs.push({ + type: 'image', + url: `https://sync.tagoras.io/api/sync/image/${params}` + }); + } + return syncs; +} + +export function hashCode(s, prefix = '_') { + const l = s.length; + let h = 0 + let i = 0; + if (l > 0) { + while (i < l) { + h = (h << 5) - h + s.charCodeAt(i++) | 0; + } + } + return prefix + h; +} + +export function getUniqueDealId(key, expiry = UNIQUE_DEAL_ID_EXPIRY) { + const storageKey = `u_${key}`; + const now = Date.now(); + const data = getStorageItem(storageKey); + let uniqueId; + + if (!data || !data.value || now - data.created > expiry) { + uniqueId = `${key}_${now.toString()}`; + setStorageItem(storageKey, uniqueId); + } else { + uniqueId = data.value; + } + + return uniqueId; +} + +export function getStorageItem(key) { + try { + return tryParseJSON(storage.getDataFromLocalStorage(key)); + } catch (e) { + } + + return null; +} + +export function setStorageItem(key, value, timestamp) { + try { + const created = timestamp || Date.now(); + const data = JSON.stringify({value, created}); + storage.setDataInLocalStorage(key, data); + } catch (e) { + } +} + +export function tryParseJSON(value) { + try { + return JSON.parse(value); + } catch (e) { + return value; + } +} + +export const spec = { + code: BIDDER_CODE, + version: BIDDER_VERSION, + supportedMediaTypes: [BANNER, VIDEO], + isBidRequestValid, + buildRequests, + interpretResponse, + getUserSyncs +}; + +registerBidder(spec); diff --git a/modules/tagorasBidAdapter.md b/modules/tagorasBidAdapter.md new file mode 100644 index 00000000000..83290bff525 --- /dev/null +++ b/modules/tagorasBidAdapter.md @@ -0,0 +1,35 @@ +# Overview + +**Module Name:** Tagoras Bidder Adapter + +**Module Type:** Bidder Adapter + +**Maintainer:** prebid@tagoras.io + +# Description + +Module that connects to Tagoras's demand sources. + +# Test Parameters +```js +var adUnits = [ + { + code: 'test-ad', + sizes: [[300, 250]], + bids: [ + { + bidder: 'tagoras', + params: { + cId: '562524b21b1c1f08117fc7f9', + pId: '59ac17c192832d0011283fe3', + bidFloor: 0.0001, + ext: { + param1: 'loremipsum', + param2: 'dolorsitamet' + } + } + } + ] + } +]; +``` diff --git a/modules/tapadIdSystem.js b/modules/tapadIdSystem.js index 149ba22eb92..2c24f062791 100644 --- a/modules/tapadIdSystem.js +++ b/modules/tapadIdSystem.js @@ -56,6 +56,12 @@ export const tapadIdSubmodule = { ); } } + }, + eids: { + 'tapadId': { + source: 'tapad.com', + atype: 1 + }, } } submodule('userId', tapadIdSubmodule); diff --git a/modules/tappxBidAdapter.js b/modules/tappxBidAdapter.js index 13a16224a79..f0c275acfb6 100644 --- a/modules/tappxBidAdapter.js +++ b/modules/tappxBidAdapter.js @@ -8,11 +8,15 @@ import { Renderer } from '../src/Renderer.js'; import {parseDomain} from '../src/refererDetection.js'; import {getGlobal} from '../src/prebidGlobal.js'; +/** + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + */ + const BIDDER_CODE = 'tappx'; const GVLID_CODE = 628; const TTL = 360; const CUR = 'USD'; -const TAPPX_BIDDER_VERSION = '0.1.2'; +const TAPPX_BIDDER_VERSION = '0.1.3'; const TYPE_CNN = 'prebidjs'; const LOG_PREFIX = '[TAPPX]: '; const VIDEO_SUPPORT = ['instream', 'outstream']; @@ -53,7 +57,7 @@ export const spec = { * * @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.host if ((new RegExp(`^(vz.*|zz.*)\\.*$`, 'i')).test(bid.params.host)) { // New endpoint @@ -146,22 +150,22 @@ function validBasic(bid) { return false; } - if (bid.params.tappxkey == null) { + if (!bid.params.tappxkey) { logWarn(LOG_PREFIX, 'Please review the mandatory Tappxkey parameter.'); return false; } - if (bid.params.host == null) { + if (!bid.params.host) { logWarn(LOG_PREFIX, 'Please review the mandatory Host parameter.'); return false; } - let classicEndpoint = true + let classicEndpoint = true; if ((new RegExp(`^(vz.*|zz.*)\\.*$`, 'i')).test(bid.params.host)) { - classicEndpoint = false + classicEndpoint = false; } - if (classicEndpoint && bid.params.endpoint == null) { + if (classicEndpoint && !bid.params.endpoint) { logWarn(LOG_PREFIX, 'Please review the mandatory endpoint Tappx parameters.'); return false; } @@ -191,7 +195,7 @@ function validMediaType(bid) { */ function interpretBid(serverBid, request) { let bidReturned = { - requestId: request.bids.bidId, + requestId: request.bids?.bidId, cpm: serverBid.price, currency: serverBid.cur ? serverBid.cur : CUR, width: serverBid.w, @@ -206,7 +210,7 @@ function interpretBid(serverBid, request) { if (typeof serverBid.nurl != 'undefined') { bidReturned.nurl = serverBid.nurl } if (typeof serverBid.burl != 'undefined') { bidReturned.burl = serverBid.burl } - if (typeof request.bids.mediaTypes !== 'undefined' && typeof request.bids.mediaTypes.video !== 'undefined') { + if (typeof request.bids?.mediaTypes !== 'undefined' && typeof request.bids?.mediaTypes.video !== 'undefined') { bidReturned.vastXml = serverBid.adm; bidReturned.vastUrl = serverBid.lurl; bidReturned.ad = serverBid.adm; @@ -214,13 +218,12 @@ function interpretBid(serverBid, request) { bidReturned.width = serverBid.w; bidReturned.height = serverBid.h; - if (request.bids.mediaTypes.video.context === 'outstream') { - const url = (serverBid.ext.purl) ? serverBid.ext.purl : false; - if (typeof url === 'undefined') { + if (request.bids?.mediaTypes.video.context === 'outstream') { + if (!serverBid.ext.purl) { logWarn(LOG_PREFIX, 'Error getting player outstream from tappx'); return false; } - bidReturned.renderer = createRenderer(bidReturned, request, url); + bidReturned.renderer = createRenderer(bidReturned, request, serverBid.ext.purl); } } else { bidReturned.ad = serverBid.adm; @@ -228,19 +231,19 @@ function interpretBid(serverBid, request) { } if (typeof bidReturned.adomain !== 'undefined' || bidReturned.adomain !== null) { - bidReturned.meta = { advertiserDomains: request.bids.adomain }; + bidReturned.meta = { advertiserDomains: request.bids?.adomain }; } return bidReturned; } /** -* Build and makes the request -* -* @param {*} validBidRequests -* @param {*} bidderRequest -* @return response ad -*/ + * Build and makes the request + * + * @param {*} validBidRequests + * @param {*} bidderRequest + * @return response ad + */ function buildOneRequest(validBidRequests, bidderRequest) { let hostInfo = _getHostInfo(validBidRequests); const ENDPOINT = hostInfo.endpoint; @@ -252,6 +255,7 @@ function buildOneRequest(validBidRequests, bidderRequest) { const BIDEXTRA = deepAccess(validBidRequests, 'params.ext'); const bannerMediaType = deepAccess(validBidRequests, 'mediaTypes.banner'); const videoMediaType = deepAccess(validBidRequests, 'mediaTypes.video'); + const ORTB2 = deepAccess(validBidRequests, 'ortb2'); // let requests = []; let payload = {}; @@ -276,7 +280,11 @@ function buildOneRequest(validBidRequests, bidderRequest) { site.name = bundle; site.page = bidderRequest?.refererInfo?.page || deepAccess(validBidRequests, 'params.site.page') || bidderRequest?.refererInfo?.topmostLocation || window.location.href || bundle; site.domain = bundle; - site.ref = bidderRequest?.refererInfo?.ref || window.top.document.referrer || ''; + try { + site.ref = bidderRequest?.refererInfo?.ref || window.top.document.referrer || ''; + } catch (e) { + site.ref = bidderRequest?.refererInfo?.ref || window.document.referrer || ''; + } site.ext = {}; site.ext.is_amp = bidderRequest?.refererInfo?.isAmp || 0; site.ext.page_da = deepAccess(validBidRequests, 'params.site.page') || '-'; @@ -304,6 +312,8 @@ function buildOneRequest(validBidRequests, bidderRequest) { let h; if (bannerMediaType) { + if (!Array.isArray(bannerMediaType.sizes)) { logWarn(LOG_PREFIX, 'Banner sizes array not found.'); } + let banner = {}; w = bannerMediaType.sizes[0][0]; h = bannerMediaType.sizes[0][1]; @@ -341,7 +351,9 @@ function buildOneRequest(validBidRequests, bidderRequest) { } if ((video.w === undefined || video.w == null || video.w <= 0) || - (video.h === undefined || video.h == null || video.h <= 0)) { + (video.h === undefined || video.h == null || video.h <= 0)) { + if (!Array.isArray(videoMediaType.playerSize)) { logWarn(LOG_PREFIX, 'Video playerSize array not found.'); } + w = videoMediaType.playerSize[0][0]; h = videoMediaType.playerSize[0][1]; video.w = w; @@ -410,6 +422,14 @@ function buildOneRequest(validBidRequests, bidderRequest) { let geo = {}; geo.country = deepAccess(validBidRequests, 'params.geo.country'); // < Device object + let configGeo = {}; + configGeo.country = ORTB2?.device?.geo; + + if (typeof configGeo.country !== 'undefined') { + device.geo = configGeo; + } else if (typeof geo.country !== 'undefined') { + device.geo = geo; + }; // > GDPR let user = {}; @@ -491,7 +511,7 @@ function getLanguage() { function getOs() { let ua = navigator.userAgent; - if (ua.indexOf('Windows') != -1) { return 'Windows'; } else if (ua.indexOf('Mac OS X') != -1) { return 'macOS'; } else if (ua.match(/Android/)) { return 'Android'; } else if (ua.match(/(iPhone|iPod|iPad)/)) { return 'iOS'; } else if (ua.indexOf('Linux') != -1) { return 'Linux'; } else { return 'Unknown'; } + if (ua.match(/Android/)) { return 'Android'; } else if (ua.match(/(iPhone|iPod|iPad)/)) { return 'iOS'; } else if (ua.indexOf('Mac OS X') != -1) { return 'macOS'; } else if (ua.indexOf('Windows') != -1) { return 'Windows'; } else if (ua.indexOf('Linux') != -1) { return 'Linux'; } else { return 'Unknown'; } } function getVendor() { diff --git a/modules/tappxBidAdapter.md b/modules/tappxBidAdapter.md index 677718c261c..55f18531f28 100644 --- a/modules/tappxBidAdapter.md +++ b/modules/tappxBidAdapter.md @@ -92,3 +92,20 @@ Ads sizes available: [300,250], [320,50], [320,480], [480,320], [728,90], [768,1 } ]; ``` +### Configuration + +Use `setConfig` to configure this submodule ortb2.device.geo, this will allow geolocation +`Geo` object to bring First Party Information. + +```javascript +var TIMEOUT = 1000; +pbjs.setConfig({ + ortb2:{ + device:{ + geo:{ + country:'US' + } + } + } +}); +``` diff --git a/modules/targetVideoBidAdapter.js b/modules/targetVideoBidAdapter.js index 1977686dd23..282f322c36a 100644 --- a/modules/targetVideoBidAdapter.js +++ b/modules/targetVideoBidAdapter.js @@ -3,6 +3,11 @@ import {getBidRequest} from '../src/utils.js'; import {BANNER, VIDEO} from '../src/mediaTypes.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; +/** + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid + */ + const SOURCE = 'pbjs'; const BIDDER_CODE = 'targetVideo'; const ENDPOINT_URL = 'https://ib.adnxs.com/ut/v3/prebid'; diff --git a/modules/teadsBidAdapter.js b/modules/teadsBidAdapter.js index 6e1006ecf00..1c12b0e3968 100644 --- a/modules/teadsBidAdapter.js +++ b/modules/teadsBidAdapter.js @@ -1,7 +1,12 @@ -import { getValue, logError, deepAccess, getBidIdParameter, parseSizesInput, isArray } from '../src/utils.js'; +import {getValue, logError, deepAccess, parseSizesInput, isArray, getBidIdParameter} from '../src/utils.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import {getStorageManager} from '../src/storageManager.js'; +/** + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid + */ + const BIDDER_CODE = 'teads'; const GVL_ID = 132; const ENDPOINT_URL = 'https://a.teads.tv/hb/bid-request'; @@ -45,14 +50,23 @@ export const spec = { */ buildRequests: function(validBidRequests, bidderRequest) { const bids = validBidRequests.map(buildRequestObject); + const topWindow = window.top; const payload = { referrer: getReferrerInfo(bidderRequest), pageReferrer: document.referrer, + pageTitle: getPageTitle().slice(0, 300), + pageDescription: getPageDescription().slice(0, 300), networkBandwidth: getConnectionDownLink(window.navigator), timeToFirstByte: getTimeToFirstByte(window), data: bids, deviceWidth: screen.width, + screenOrientation: screen.orientation?.type, + historyLength: topWindow.history?.length, + viewportHeight: topWindow.visualViewport?.height, + viewportWidth: topWindow.visualViewport?.width, + hardwareConcurrency: topWindow.navigator?.hardwareConcurrency, + deviceMemory: topWindow.navigator?.deviceMemory, hb_version: '$prebid.version$', ...getSharedViewerIdParameters(validBidRequests), ...getFirstPartyTeadsIdParameter(validBidRequests) @@ -170,6 +184,32 @@ function getReferrerInfo(bidderRequest) { return ref; } +function getPageTitle() { + try { + const ogTitle = window.top.document.querySelector('meta[property="og:title"]') + + return window.top.document.title || (ogTitle && ogTitle.content) || ''; + } catch (e) { + const ogTitle = document.querySelector('meta[property="og:title"]') + + return document.title || (ogTitle && ogTitle.content) || ''; + } +} + +function getPageDescription() { + let element; + + try { + element = window.top.document.querySelector('meta[name="description"]') || + window.top.document.querySelector('meta[property="og:description"]') + } catch (e) { + element = document.querySelector('meta[name="description"]') || + document.querySelector('meta[property="og:description"]') + } + + return (element && element.content) || ''; +} + function getConnectionDownLink(nav) { return nav && nav.connection && nav.connection.downlink >= 0 ? nav.connection.downlink.toString() : ''; } @@ -220,6 +260,7 @@ function buildRequestObject(bid) { let placementId = getValue(bid.params, 'placementId'); let pageId = getValue(bid.params, 'pageId'); const gpid = deepAccess(bid, 'ortb2Imp.ext.gpid'); + const videoPlcmt = deepAccess(bid, 'mediaTypes.video.plcmt'); reqObj.sizes = getSizes(bid); reqObj.bidId = getBidIdParameter('bidId', bid); @@ -229,6 +270,7 @@ function buildRequestObject(bid) { reqObj.adUnitCode = getBidIdParameter('adUnitCode', bid); reqObj.transactionId = bid.ortb2Imp?.ext?.tid || ''; if (gpid) { reqObj.gpid = gpid; } + if (videoPlcmt) { reqObj.videoPlcmt = videoPlcmt; } return reqObj; } diff --git a/modules/teadsIdSystem.js b/modules/teadsIdSystem.js index b4067bf75c3..8026fe77f87 100644 --- a/modules/teadsIdSystem.js +++ b/modules/teadsIdSystem.js @@ -12,6 +12,13 @@ import {getStorageManager} from '../src/storageManager.js'; import {uspDataHandler} from '../src/adapterManager.js'; import {MODULE_TYPE_UID} from '../src/activities/modules.js'; +/** + * @typedef {import('../modules/userId/index.js').Submodule} Submodule + * @typedef {import('../modules/userId/index.js').SubmoduleConfig} SubmoduleConfig + * @typedef {import('../modules/userId/index.js').ConsentData} ConsentData + * @typedef {import('../modules/userId/index.js').IdResponse} IdResponse + */ + const MODULE_NAME = 'teadsId'; const GVL_ID = 132; const FP_TEADS_ID_COOKIE_NAME = '_tfpvi'; @@ -34,9 +41,9 @@ export const storage = getStorageManager({moduleType: MODULE_TYPE_UID, moduleNam /** @type {Submodule} */ export const teadsIdSubmodule = { /** - * used to link submodule with config - * @type {string} - */ + * used to link submodule with config + * @type {string} + */ name: MODULE_NAME, /** * Vendor id of Teads @@ -44,21 +51,21 @@ export const teadsIdSubmodule = { */ gvlid: GVL_ID, /** - * decode the stored id value for passing to bid requests - * @function - * @param {string} value - * @returns {{teadsId:string}} - */ + * decode the stored id value for passing to bid requests + * @function + * @param {string} value + * @returns {{teadsId:string}} + */ decode(value) { return {teadsId: value} }, /** - * performs action to obtain id and return a value in the callback's response argument - * @function - * @param {SubmoduleConfig} [submoduleConfig] - * @param {ConsentData} [consentData] - * @returns {IdResponse|undefined} - */ + * performs action to obtain id and return a value in the callback's response argument + * @function + * @param {SubmoduleConfig} [submoduleConfig] + * @param {ConsentData} [consentData] + * @returns {IdResponse|undefined} + */ getId(submoduleConfig, consentData) { const resp = function (callback) { const url = buildAnalyticsTagUrl(submoduleConfig, consentData); diff --git a/modules/temedyaBidAdapter.js b/modules/temedyaBidAdapter.js index 4eac0bc641f..0e48768b605 100644 --- a/modules/temedyaBidAdapter.js +++ b/modules/temedyaBidAdapter.js @@ -3,6 +3,14 @@ import { registerBidder } from '../src/adapters/bidderFactory.js'; import { BANNER, NATIVE } from '../src/mediaTypes.js'; import { convertOrtbRequestToProprietaryNative } from '../src/native.js'; +/** + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid + * @typedef {import('../src/adapters/bidderFactory.js').ServerResponse} ServerResponse + * @typedef {import('../src/adapters/bidderFactory.js').ServerRequest} ServerRequest + * @typedef {import('../src/adapters/bidderFactory.js').validBidRequests} validBidRequests + */ + const BIDDER_CODE = 'temedya'; const ENDPOINT_URL = 'https://adm.vidyome.com/'; const ENDPOINT_METHOD = 'GET'; @@ -12,20 +20,20 @@ export const spec = { code: BIDDER_CODE, supportedMediaTypes: [BANNER, NATIVE], /** - * 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. - */ + * 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.widgetId); }, /** - * Make a server request from the list of BidRequests. - * - * @param {validBidRequests[]} - an array of bids - * @return ServerRequest Info describing the request to the server. - */ + * 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) { // convert Native ORTB definition to old-style prebid native definition validBidRequests = convertOrtbRequestToProprietaryNative(validBidRequests); @@ -54,11 +62,11 @@ export const spec = { }); }, /** - * 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. - */ + * 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) { try { const bidResponse = serverResponse.body; diff --git a/modules/theAdxBidAdapter.js b/modules/theAdxBidAdapter.js index 03f303498a1..f19f7cfe515 100644 --- a/modules/theAdxBidAdapter.js +++ b/modules/theAdxBidAdapter.js @@ -9,6 +9,15 @@ import { } from '../src/adapters/bidderFactory.js'; import { convertOrtbRequestToProprietaryNative } from '../src/native.js'; +/** + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid + * @typedef {import('../src/adapters/bidderFactory.js').ServerResponse} ServerResponse + * @typedef {import('../src/adapters/bidderFactory.js').SyncOptions} SyncOptions + * @typedef {import('../src/adapters/bidderFactory.js').UserSync} UserSync + * @typedef {import('../src/adapters/bidderFactory.js').validBidRequests} validBidRequests + */ + const BIDDER_CODE = 'theadx'; const ENDPOINT_URL = 'https://ssp.theadx.com/request'; @@ -159,7 +168,6 @@ export const spec = { withCredentials: true, }, bidder: 'theadx', - // TODO: is 'page' the right value here? referrer: encodeURIComponent(bidderRequest.refererInfo.page || ''), data: generatePayload(bidRequest, bidderRequest), mediaTypes: bidRequest['mediaTypes'], @@ -261,6 +269,7 @@ export const spec = { ad: creative, ttl: ttl || 3000, creativeId: bid.crid, + dealId: bid.dealid || null, netRevenue: true, currency: responseBody.cur, mediaType: mediaType, @@ -466,11 +475,19 @@ let generateImpBody = (bidRequest, bidderRequest) => { } else if (mediaTypes && mediaTypes.native) { native = generateNativeComponent(bidRequest, bidderRequest); } - const result = { id: bidRequest.index, tagid: bidRequest.params.tagId + '', }; + + // deals support + if (bidRequest.params.deals && Array.isArray(bidRequest.params.deals) && bidRequest.params.deals.length > 0) { + result.pmp = { + deals: bidRequest.params.deals, + private_auction: 0, + }; + } + if (banner) { result['banner'] = banner; } diff --git a/modules/theAdxBidAdapter.md b/modules/theAdxBidAdapter.md index 2392bfaa819..cdc7b8410a8 100644 --- a/modules/theAdxBidAdapter.md +++ b/modules/theAdxBidAdapter.md @@ -26,9 +26,9 @@ Module that connects to TheAdx demand sources { bidder: "theadx", params: { - pid: 1000, // publisher id - wid: 2000, //website id - tagId: 5000, //zone id + pid: 1, // publisher id + wid: 7, //website id + tagId: 19, //zone id } } ] @@ -43,9 +43,10 @@ Module that connects to TheAdx demand sources { bidder: "theadx", params: { - pid: 1000, // publisher id - wid: 2000, //website id - tagId: 5000, //zone id + pid: 1, // publisher id + wid: 7, //website id + tagId: 18, //zone id + deals:[{"id":"theadx:137"}] //optional } } ] @@ -80,9 +81,9 @@ Module that connects to TheAdx demand sources { bidder: "theadx", params: { - pid: 1000, // publisher id - wid: 2000, //website id - tagId: 5000, //zone id + pid: 1, // publisher id + wid: 7, //website id + tagId: 20, //zone id } } ] diff --git a/modules/timeoutRtdProvider.js b/modules/timeoutRtdProvider.js index 323a5291e2d..a46a39a2c2b 100644 --- a/modules/timeoutRtdProvider.js +++ b/modules/timeoutRtdProvider.js @@ -4,6 +4,10 @@ import * as ajax from '../src/ajax.js'; import { logInfo, deepAccess, logError } from '../src/utils.js'; import { getGlobal } from '../src/prebidGlobal.js'; +/** + * @typedef {import('../modules/rtdModule/index.js').RtdSubmodule} RtdSubmodule + */ + const SUBMODULE_NAME = 'timeout'; // this allows the stubbing of functions during testing @@ -67,7 +71,7 @@ function getConnectionSpeed() { * Calculate the time to be added to the timeout * @param {Array} adUnits * @param {Object} rules - * @return {int} + * @return {number} */ function calculateTimeoutModifier(adUnits, rules) { logInfo('Timeout rules', rules); diff --git a/modules/tncIdSystem.js b/modules/tncIdSystem.js index 24e3c79d4df..59254a9430f 100644 --- a/modules/tncIdSystem.js +++ b/modules/tncIdSystem.js @@ -57,6 +57,12 @@ export const tncidSubModule = { return { callback: function (cb) { return tncCallback(cb); } } + }, + eids: { + 'tncid': { + source: 'thenewco.it', + atype: 3 + }, } } diff --git a/modules/topicsFpdModule.js b/modules/topicsFpdModule.js index 4cb11af48fc..617ce0ad6f4 100644 --- a/modules/topicsFpdModule.js +++ b/modules/topicsFpdModule.js @@ -1,24 +1,32 @@ -import {logError, logWarn, mergeDeep, isEmpty, safeJSONParse, logInfo, hasDeviceAccess} from '../src/utils.js'; +import {isEmpty, logError, logWarn, mergeDeep, safeJSONParse} from '../src/utils.js'; import {getRefererInfo} from '../src/refererDetection.js'; import {submodule} from '../src/hook.js'; import {GreedyPromise} from '../src/utils/promise.js'; import {config} from '../src/config.js'; import {getCoreStorageManager} from '../src/storageManager.js'; import {includes} from '../src/polyfill.js'; -import {gdprDataHandler} from '../src/adapterManager.js'; +import {isActivityAllowed} from '../src/activities/rules.js'; +import {ACTIVITY_ENRICH_UFPD} from '../src/activities/activities.js'; +import {activityParams} from '../src/activities/activityParams.js'; +import {MODULE_TYPE_BIDDER} from '../src/activities/modules.js'; const MODULE_NAME = 'topicsFpd'; const DEFAULT_EXPIRATION_DAYS = 21; -const TCF_REQUIRED_PURPOSES = ['1', '2', '3', '4']; -let HAS_GDPR_CONSENT = true; +const DEFAULT_FETCH_RATE_IN_DAYS = 1; let LOAD_TOPICS_INITIALISE = false; -const HAS_DEVICE_ACCESS = hasDeviceAccess(); + +export function reset() { + LOAD_TOPICS_INITIALISE = false; +} const bidderIframeList = { - maxTopicCaller: 1, + maxTopicCaller: 2, bidders: [{ bidder: 'pubmatic', iframeURL: 'https://ads.pubmatic.com/AdServer/js/topics/topics_frame.html' + }, { + bidder: 'rtbhouse', + iframeURL: 'https://topics.authorizedvault.com/topicsapi.html' }] } export const coreStorage = getCoreStorageManager(MODULE_NAME); @@ -78,15 +86,21 @@ export function getTopicsData(name, topics, taxonomies = TAXONOMIES) { if (name != null) { datum.name = name; } + return datum; }) ); } +function isTopicsSupported(doc = document) { + return 'browsingTopics' in doc && doc.featurePolicy.allowsFeature('browsing-topics') +} + export function getTopics(doc = document) { let topics = null; + try { - if ('browsingTopics' in doc && doc.featurePolicy.allowsFeature('browsing-topics')) { + if (isTopicsSupported(doc)) { topics = GreedyPromise.resolve(doc.browsingTopics()); } } catch (e) { @@ -95,6 +109,7 @@ export function getTopics(doc = document) { if (topics == null) { topics = GreedyPromise.resolve([]); } + return topics; } @@ -123,19 +138,16 @@ export function processFpd(config, {global}, {data = topicsData} = {}) { */ export function getCachedTopics() { let cachedTopicData = []; - if (!HAS_GDPR_CONSENT || !HAS_DEVICE_ACCESS) { - return cachedTopicData; - } const topics = config.getConfig('userSync.topics') || bidderIframeList; const bidderList = topics.bidders || []; let storedSegments = new Map(safeJSONParse(coreStorage.getDataFromLocalStorage(topicStorageName))); storedSegments && storedSegments.forEach((value, cachedBidder) => { // Check bidder exist in config for cached bidder data and then only retrieve the cached data - let bidderConfigObj = bidderList.find(({bidder}) => cachedBidder == bidder) - if (bidderConfigObj) { + let bidderConfigObj = bidderList.find(({bidder}) => cachedBidder === bidder) + if (bidderConfigObj && isActivityAllowed(ACTIVITY_ENRICH_UFPD, activityParams(MODULE_TYPE_BIDDER, cachedBidder))) { if (!isCachedDataExpired(value[lastUpdated], bidderConfigObj?.expiry || DEFAULT_EXPIRATION_DAYS)) { Object.keys(value).forEach((segData) => { - segData != lastUpdated && cachedTopicData.push(value[segData]); + segData !== lastUpdated && cachedTopicData.push(value[segData]); }) } else { // delete the specific bidder map from the store and store the updated maps @@ -148,7 +160,7 @@ export function getCachedTopics() { } /** - * Recieve messages from iframe loaded for bidders to fetch topic + * Receive messages from iframe loaded for bidders to fetch topic * @param {MessageEvent} evt */ export function receiveMessage(evt) { @@ -157,7 +169,7 @@ export function receiveMessage(evt) { let data = safeJSONParse(evt.data); if (includes(getLoadedIframeURL(), evt.origin) && data && data.segment && !isEmpty(data.segment.topics)) { const {domain, topics, bidder} = data.segment; - const iframeTopicsData = getTopicsData(domain, topics)[0]; + const iframeTopicsData = getTopicsData(domain, topics); iframeTopicsData && storeInLocalStorage(bidder, iframeTopicsData); } } catch (err) { } @@ -165,18 +177,20 @@ export function receiveMessage(evt) { } /** -Function to store Topics data recieved from iframe in storage(name: "prebid:topics") -* @param {Topics} topics -*/ +Function to store Topics data received from iframe in storage(name: "prebid:topics") + * @param {Topics} topics + */ export function storeInLocalStorage(bidder, topics) { const storedSegments = new Map(safeJSONParse(coreStorage.getDataFromLocalStorage(topicStorageName))); - if (storedSegments.has(bidder)) { - storedSegments.get(bidder)[topics['ext']['segclass']] = topics; - storedSegments.get(bidder)[lastUpdated] = new Date().getTime(); - storedSegments.set(bidder, storedSegments.get(bidder)); - } else { - storedSegments.set(bidder, {[topics.ext.segclass]: topics, [lastUpdated]: new Date().getTime()}) - } + const topicsObj = { + [lastUpdated]: new Date().getTime() + }; + + topics.forEach((topic) => { + topicsObj[topic.ext.segclass] = topic; + }); + + storedSegments.set(bidder, topicsObj); coreStorage.setDataInLocalStorage(topicStorageName, JSON.stringify([...storedSegments])); } @@ -188,8 +202,8 @@ function isCachedDataExpired(storedTime, cacheTime) { } /** -* Function to get random bidders based on count passed with array of bidders -**/ + * Function to get random bidders based on count passed with array of bidders + */ function getRandomBidders(arr, count) { return ([...arr].sort(() => 0.5 - Math.random())).slice(0, count) } @@ -201,55 +215,43 @@ function listenMessagesFromTopicIframe() { window.addEventListener('message', receiveMessage, false); } -function checkTCFv2(vendorData, requiredPurposes = TCF_REQUIRED_PURPOSES) { - const {gdprApplies, purpose} = vendorData; - if (!gdprApplies || !purpose) { - return true; - } - return requiredPurposes.map((purposeNo) => { - const purposeConsent = purpose.consents ? purpose.consents[purposeNo] : false; - if (purposeConsent) { - return true; - } - return false; - }).reduce((a, b) => a && b, true); -} - -export function hasGDPRConsent() { - // Check for GDPR consent for purpose 1,2,3,4 and return false if consent has not been given - const gdprConsent = gdprDataHandler.getConsentData(); - const hasGdpr = (gdprConsent && typeof gdprConsent.gdprApplies === 'boolean' && gdprConsent.gdprApplies) ? 1 : 0; - const gdprConsentString = hasGdpr ? gdprConsent.consentString : ''; - if (hasGdpr) { - if ((!gdprConsentString || gdprConsentString === '') || !gdprConsent.vendorData) { - return false; - } - return checkTCFv2(gdprConsent.vendorData); - } - return true; -} - /** * function to load the iframes of the bidder to load the topics data */ -function loadTopicsForBidders() { - HAS_GDPR_CONSENT = hasGDPRConsent(); - if (!HAS_GDPR_CONSENT || !HAS_DEVICE_ACCESS) { - logInfo('Topics Module : Consent string is required to fetch the topics from third party domains.'); - return; - } +export function loadTopicsForBidders(doc = document) { + if (!isTopicsSupported(doc)) return; const topics = config.getConfig('userSync.topics') || bidderIframeList; + if (topics) { listenMessagesFromTopicIframe(); const randomBidders = getRandomBidders(topics.bidders || [], topics.maxTopicCaller || 1) - randomBidders && randomBidders.forEach(({ bidder, iframeURL }) => { + randomBidders && randomBidders.forEach(({ bidder, iframeURL, fetchUrl, fetchRate }) => { if (bidder && iframeURL) { - let ifrm = document.createElement('iframe'); + let ifrm = doc.createElement('iframe'); ifrm.name = 'ifrm_'.concat(bidder); ifrm.src = ''.concat(iframeURL, '?bidder=').concat(bidder); ifrm.style.display = 'none'; setLoadedIframeURL(new URL(iframeURL).origin); - iframeURL && window.document.documentElement.appendChild(ifrm); + iframeURL && doc.documentElement.appendChild(ifrm); + } + + if (bidder && fetchUrl) { + let storedSegments = new Map(safeJSONParse(coreStorage.getDataFromLocalStorage(topicStorageName))); + const bidderLsEntry = storedSegments.get(bidder); + + if (!bidderLsEntry || (bidderLsEntry && isCachedDataExpired(bidderLsEntry[lastUpdated], fetchRate || DEFAULT_FETCH_RATE_IN_DAYS))) { + window.fetch(`${fetchUrl}?bidder=${bidder}`, {browsingTopics: true}) + .then(response => { + return response.json(); + }) + .then(data => { + if (data && data.segment && !isEmpty(data.segment.topics)) { + const {domain, topics, bidder} = data.segment; + const fetchTopicsData = getTopicsData(domain, topics); + fetchTopicsData && storeInLocalStorage(bidder, fetchTopicsData); + } + }); + } } }) } else { diff --git a/modules/topicsFpdModule.md b/modules/topicsFpdModule.md index b1bc3cb0d5b..6ef0bf241dd 100644 --- a/modules/topicsFpdModule.md +++ b/modules/topicsFpdModule.md @@ -36,6 +36,10 @@ pbjs.setConfig({ bidder: 'pubmatic', iframeURL: 'https://ads.pubmatic.com/AdServer/js/topics/topics_frame.html', expiry: 7 // Configurable expiry days + },{ + bidder: 'rtbhouse', + iframeURL: 'https://topics.authorizedvault.com/topicsapi.html', + expiry: 7 // Configurable expiry days },{ bidder: 'rubicon', iframeURL: 'https://rubicon.com:8080/topics/fpd/topic.html', // dummy URL diff --git a/modules/tpmnBidAdapter.js b/modules/tpmnBidAdapter.js index bac99d578c5..3edc89c90ae 100644 --- a/modules/tpmnBidAdapter.js +++ b/modules/tpmnBidAdapter.js @@ -1,164 +1,245 @@ /* eslint-disable no-tabs */ import { registerBidder } from '../src/adapters/bidderFactory.js'; -import { parseUrl, deepAccess } from '../src/utils.js'; +import { ortbConverter } from '../libraries/ortbConverter/converter.js'; import { getStorageManager } from '../src/storageManager.js'; -import { BANNER } from '../src/mediaTypes.js'; +import { BANNER, VIDEO } from '../src/mediaTypes.js'; +import { Renderer } from '../src/Renderer.js'; import { config } from '../src/config.js'; +import * as utils from '../src/utils.js'; + +/** + * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid + */ -export const ADAPTER_VERSION = '1'; -const SUPPORTED_AD_TYPES = [BANNER]; const BIDDER_CODE = 'tpmn'; -const URL = 'https://ad.tpmn.co.kr/prebidhb.tpmn'; -const IFRAMESYNC = 'https://ad.tpmn.co.kr/sync.tpmn?type=iframe'; -export const storage = getStorageManager({bidderCode: BIDDER_CODE}); +const DEFAULT_BID_TTL = 500; +const DEFAULT_CURRENCY = 'USD'; +const SUPPORTED_AD_TYPES = [BANNER, VIDEO]; +// const BIDDER_ENDPOINT_URL = 'http://localhost:8081/ortb/pbjs_bidder'; +const BIDDER_ENDPOINT_URL = 'https://gat.tpmn.io/ortb/pbjs_bidder'; +const IFRAMESYNC = 'https://gat.tpmn.io/sync/iframe'; +const COMMON_PARAMS = [ + 'battr' +]; +export const VIDEO_RENDERER_URL = 'https://acdn.adnxs.com/video/outstream/ANOutstreamVideo.js'; +export const ADAPTER_VERSION = '2.0'; +export const storage = getStorageManager({bidderCode: BIDDER_CODE}); export const spec = { code: BIDDER_CODE, supportedMediaTypes: SUPPORTED_AD_TYPES, + isBidRequestValid, + buildRequests, + interpretResponse, + getUserSyncs, /** - *Determines whether or not the given bid request is valid. - * - * @param {object} bid The bid to validate. - * @return boolean True if this is a valid bid, and false otherwise. - */ - isBidRequestValid: function (bid) { - return 'params' in bid && - 'inventoryId' in bid.params && - 'publisherId' in bid.params && - !isNaN(Number(bid.params.inventoryId)) && - bid.params.inventoryId > 0 && - (typeof bid.mediaTypes.banner.sizes != 'undefined'); // only accepting appropriate sizes - }, - - /** - * @param {BidRequest[]} bidRequests - * @param {*} bidderRequest - * @return {ServerRequest} + * Register bidder specific code, which will execute if a bid from this bidder won the auction + * @param {Bid} bid The bid that won the auction */ - buildRequests: (bidRequests, bidderRequest) => { - if (bidRequests.length === 0) { - return []; + onBidWon: function (bid) { + if (bid.burl) { + utils.triggerPixel(bid.burl); } - const bids = bidRequests.map(bidToRequest); - const bidderApiUrl = URL; - const payload = { - 'bids': [...bids], - 'site': createSite(bidderRequest.refererInfo) - }; - return [{ - method: 'POST', - url: bidderApiUrl, - data: payload - }]; + } +} + +function isBidRequestValid(bid) { + return (isValidInventoryId(bid) && (isValidBannerRequest(bid) || isValidVideoRequest(bid))); +} + +function isValidInventoryId(bid) { + return 'params' in bid && 'inventoryId' in bid.params && utils.isNumber(bid.params.inventoryId); +} + +function isValidBannerRequest(bid) { + const bannerSizes = utils.deepAccess(bid, `mediaTypes.${BANNER}.sizes`); + return utils.isArray(bannerSizes) && bannerSizes.length > 0 && bannerSizes.every(size => utils.isNumber(size[0]) && utils.isNumber(size[1])); +} + +function isValidVideoRequest(bid) { + const videoSizes = utils.deepAccess(bid, `mediaTypes.${VIDEO}.playerSize`); + const videoMimes = utils.deepAccess(bid, `mediaTypes.${VIDEO}.mimes`); + + const isValidVideoSize = utils.isArray(videoSizes) && videoSizes.length > 0 && videoSizes.every(size => utils.isNumber(size[0]) && utils.isNumber(size[1])); + const isValidVideoMimes = utils.isArray(videoMimes) && videoMimes.length > 0; + return isValidVideoSize && isValidVideoMimes; +} + +function buildRequests(validBidRequests, bidderRequest) { + let requests = []; + try { + if (validBidRequests.length === 0 || !bidderRequest) return []; + let bannerBids = validBidRequests.filter(bid => utils.deepAccess(bid, 'mediaTypes.banner')); + let videoBids = validBidRequests.filter(bid => utils.deepAccess(bid, 'mediaTypes.video')); + + bannerBids.forEach(bid => { + requests.push(createRequest([bid], bidderRequest, BANNER)); + }); + + videoBids.forEach(bid => { + requests.push(createRequest([bid], bidderRequest, VIDEO)); + }); + } catch (err) { + utils.logWarn('buildRequests', err); + } + + return requests; +} + +function createRequest(bidRequests, bidderRequest, mediaType) { + const rtbData = CONVERTER.toORTB({ bidRequests, bidderRequest, context: { mediaType } }) + + const bid = bidRequests.find((b) => b.params.inventoryId) + + if (bid.params.inventoryId) rtbData.ext = {}; + if (bid.params.inventoryId) rtbData.ext.inventoryId = bid.params.inventoryId + + const ortb2Data = bidderRequest?.ortb2 || {}; + const bcat = ortb2Data?.bcat || bid.params.bcat || []; + const badv = ortb2Data?.badv || bid.params.badv || []; + const bapp = ortb2Data?.bapp || bid.params.bapp || []; + + if (bcat.length > 0) { + rtbData.bcat = bcat; + } + if (badv.length > 0) { + rtbData.badv = badv; + } + if (badv.length > 0) { + rtbData.bapp = bapp; + } + + return { + method: 'POST', + url: BIDDER_ENDPOINT_URL + '?v=' + ADAPTER_VERSION, + data: rtbData + } +} + +function interpretResponse(response, request) { + return CONVERTER.fromORTB({ request: request.data, response: response.body }).bids; +} + +registerBidder(spec); + +const CONVERTER = ortbConverter({ + context: { + netRevenue: true, + ttl: DEFAULT_BID_TTL, + currency: DEFAULT_CURRENCY }, - /** - * Unpack the response from the server into a list of bids. - * - * @param {serverResponse} serverResponse A successful response from the server. - * @return {Bid[]} An array of bids which were nested inside the server. - */ - interpretResponse: function (serverResponse, serverRequest) { - if (!Array.isArray(serverResponse.body)) { - return []; + imp(buildImp, bidRequest, context) { + let imp = buildImp(bidRequest, context); + if (!imp.bidfloor && bidRequest.params.bidFloor) { + imp.bidfloor = bidRequest.params.bidFloor; } - // server response body is an array of bid results - const bidResults = serverResponse.body; - // our server directly returns the format needed by prebid.js so no more - // transformation is needed here. - return bidResults; - }, - - getUserSyncs: function (syncOptions, serverResponses, gdprConsent, uspConsent) { - const syncArr = []; - if (syncOptions.iframeEnabled) { - let policyParam = ''; - if (gdprConsent && gdprConsent.consentString) { - if (typeof gdprConsent.gdprApplies === 'boolean') { - policyParam += `&gdpr=${Number(gdprConsent.gdprApplies)}&gdpr_consent=${gdprConsent.consentString}`; - } else { - policyParam += `&gdpr=0&gdpr_consent=${gdprConsent.consentString}`; + [VIDEO, BANNER].forEach(namespace => { + COMMON_PARAMS.forEach(param => { + if (bidRequest.params.hasOwnProperty(param)) { + utils.deepSetValue(imp, `${namespace}.${param}`, bidRequest.params[param]) } + }) + }) + return imp; + }, + bidResponse(buildBidResponse, bid, context) { + const {bidRequest} = context; + const bidResponse = buildBidResponse(bid, context); + if (bidResponse.mediaType === BANNER) { + bidResponse.ad = bid.adm; + } else if (bidResponse.mediaType === VIDEO) { + if (bidRequest.mediaTypes.video.context === 'outstream') { + bidResponse.rendererUrl = VIDEO_RENDERER_URL; + bidResponse.renderer = createRenderer(bidRequest); } - if (uspConsent && uspConsent.consentString) { - policyParam += `&ccpa_consent=${uspConsent.consentString}`; - } - const coppa = config.getConfig('coppa') ? 1 : 0; - policyParam += `&coppa=${coppa}`; - syncArr.push({ - type: 'iframe', - url: IFRAMESYNC + policyParam - }); - } else { - syncArr.push({ - type: 'image', - url: 'https://x.bidswitch.net/sync?ssp=tpmn' - }); - syncArr.push({ - type: 'image', - url: 'https://gocm.c.appier.net/tpmn' - }); - syncArr.push({ - type: 'image', - url: 'https://info.mmnneo.com/getGuidRedirect.info?url=https%3A%2F%2Fad.tpmn.co.kr%2Fcookiesync.tpmn%3Ftpmn_nid%3Dbf91e8b3b9d3f1af3fc1d657f090b4fb%26tpmn_buid%3D' - }); - syncArr.push({ - type: 'image', - url: 'https://sync.aralego.com/idSync?redirect=https%3A%2F%2Fad.tpmn.co.kr%2FpixelCt.tpmn%3Ftpmn_nid%3Dde91e8b3b9d3f1af3fc1d657f090b815%26tpmn_buid%3DSspCookieUserId' - }); } - return syncArr; + return bidResponse; }, -}; + overrides: { + imp: { + video(orig, imp, bidRequest, context) { + let videoParams = bidRequest.mediaTypes[VIDEO]; + if (videoParams) { + videoParams = Object.assign({}, videoParams, bidRequest.params.video); + bidRequest = {...bidRequest, mediaTypes: {[VIDEO]: videoParams}} + } + orig(imp, bidRequest, context); + }, + }, + } +}); -registerBidder(spec); +function createRenderer(bid) { + const renderer = Renderer.install({ + id: bid.bidId, + url: VIDEO_RENDERER_URL, + config: utils.deepAccess(bid, 'renderer.options'), + loaded: false, + adUnitCode: bid.adUnitCode + }); -/** - * Creates site description object - */ -function createSite(refInfo) { - let url = parseUrl(refInfo.page || ''); - let site = { - 'domain': url.hostname, - 'page': url.protocol + '://' + url.hostname + url.pathname - }; - if (refInfo.ref) { - site.ref = refInfo.ref + try { + renderer.setRender(outstreamRender); + } catch (err) { + utils.logWarn('Prebid Error calling setRender on renderer', err); } - let keywords = document.getElementsByTagName('meta')['keywords']; - if (keywords && keywords.content) { - site.keywords = keywords.content; - } - return site; + return renderer; } -function parseSize(size) { - let sizeObj = {} - sizeObj.width = parseInt(size[0], 10); - sizeObj.height = parseInt(size[1], 10); - return sizeObj; -} +function outstreamRender(bid, doc) { + bid.renderer.push(() => { + const win = (doc) ? doc.defaultView : window; + win.ANOutstreamVideo.renderAd({ + sizes: [bid.playerWidth, bid.playerHeight], + targetId: bid.adUnitCode, + rendererOptions: bid.renderer.getConfig(), + adResponse: { content: bid.vastXml } -function parseSizes(sizes) { - if (Array.isArray(sizes[0])) { // is there several sizes ? (ie. [[728,90],[200,300]]) - return sizes.map(size => parseSize(size)); - } - return [parseSize(sizes)]; // or a single one ? (ie. [728,90]) + }, handleOutstreamRendererEvents.bind(null, bid)); + }); } -function getBannerSizes(bidRequest) { - return parseSizes(deepAccess(bidRequest, 'mediaTypes.banner.sizes') || bidRequest.sizes); +function handleOutstreamRendererEvents(bid, id, eventName) { + bid.renderer.handleVideoEvent({ id, eventName }); } -function bidToRequest(bid) { - const bidObj = {}; - bidObj.sizes = getBannerSizes(bid); - - bidObj.inventoryId = bid.params.inventoryId; - bidObj.publisherId = bid.params.publisherId; - bidObj.bidId = bid.bidId; - bidObj.adUnitCode = bid.adUnitCode; - // TODO: fix auctionId leak: https://github.com/prebid/Prebid.js/issues/9781 - bidObj.auctionId = bid.auctionId; - - return bidObj; +function getUserSyncs(syncOptions, serverResponses, gdprConsent, uspConsent) { + const syncArr = []; + if (syncOptions.iframeEnabled) { + let policyParam = ''; + if (gdprConsent && gdprConsent.consentString) { + if (typeof gdprConsent.gdprApplies === 'boolean') { + policyParam += `&gdpr=${Number(gdprConsent.gdprApplies)}&gdpr_consent=${gdprConsent.consentString}`; + } else { + policyParam += `&gdpr=0&gdpr_consent=${gdprConsent.consentString}`; + } + } + if (uspConsent && uspConsent.consentString) { + policyParam += `&ccpa_consent=${uspConsent.consentString}`; + } + const coppa = config.getConfig('coppa') ? 1 : 0; + policyParam += `&coppa=${coppa}`; + syncArr.push({ + type: 'iframe', + url: IFRAMESYNC + '?' + policyParam + }); + } else { + syncArr.push({ + type: 'image', + url: 'https://x.bidswitch.net/sync?ssp=tpmn' + }); + syncArr.push({ + type: 'image', + url: 'https://gocm.c.appier.net/tpmn' + }); + syncArr.push({ + type: 'image', + url: 'https://info.mmnneo.com/getGuidRedirect.info?url=https%3A%2F%2Fad.tpmn.co.kr%2Fcookiesync.tpmn%3Ftpmn_nid%3Dbf91e8b3b9d3f1af3fc1d657f090b4fb%26tpmn_buid%3D' + }); + syncArr.push({ + type: 'image', + url: 'https://sync.aralego.com/idSync?redirect=https%3A%2F%2Fad.tpmn.co.kr%2FpixelCt.tpmn%3Ftpmn_nid%3Dde91e8b3b9d3f1af3fc1d657f090b815%26tpmn_buid%3DSspCookieUserId' + }); + } + return syncArr; } diff --git a/modules/tpmnBidAdapter.md b/modules/tpmnBidAdapter.md index 8387528bb0f..3b016d7e5b2 100644 --- a/modules/tpmnBidAdapter.md +++ b/modules/tpmnBidAdapter.md @@ -11,10 +11,27 @@ Maintainer: develop@tpmn.co.kr Connects to TPMN exchange for bids. NOTE: -- TPMN bid adapter only supports Banner at the moment. +- TPMN bid adapter only supports MediaType BANNER, VIDEO. - Multi-currency is not supported. +- Please contact the TPMN sales team via email for "inventoryId" issuance. -# Sample Ad Unit Config + +# Bid Parameters + +## bids.params (Banner, Video) +***Pay attention to the case sensitivity.*** + +{: .table .table-bordered .table-striped } +| Name | Scope | Description | Example | Type | +| -------------- | ----------- | ------------------------------------------ | ------------- | ------------ | +| `inventoryId` | required | Ad Inventory id TPMN | 123 | Number | +| `bidFloor` | recommended | Minimum price in USD. bidFloor applies to a specific unit. | 1.50 | Number | +| `bcat` | optional | IAB 5.1 Content Categories | ['IAB7-39'] | [String] | +| `badv` | optional | IAB Block list of advertisers by their domains | ['example.com'] | [String] | +| `bapp` | optional | IAB Block list of applications | ['com.blocked'] | [String] | + + +# Banner Ad Unit Config ``` var adUnits = [{ // Banner adUnit @@ -22,16 +39,77 @@ NOTE: mediaTypes: { banner: { sizes: [[300, 250], [320, 50]], // banner size + battr: [1,2,3] // optional } }, bids: [ { bidder: 'tpmn', params: { - inventoryId: '1', - publisherId: 'TPMN' + inventoryId: 1, // required + bidFloor: 2.0, // recommended + ... // bcat, badv, bapp // optional } } ] }]; +``` + + +# mediaTypes Parameters + +## mediaTypes.banner + +The following banner parameters are supported here so publishers may fully declare their banner inventory: + +{: .table .table-bordered .table-striped } +| Name | Scope | Description | Example | Type | +| --------- | ------------| ----------------------------------------------------------------- | --------- | --------- | +| `sizes` | required | Avalaible sizes supported for banner ad unit | [ [300, 250], [300, 600] ] | [[Integer, Integer], [Integer, Integer]] | +| `battr` | optional | IAB 5.3 Creative Attributes | [1,2,3] | [Number] | +## mediaTypes.video + +We support the following OpenRTB params that can be specified in `mediaTypes.video` or in `bids[].params.video` + +{: .table .table-bordered .table-striped } +| Name | Scope | Description | Example | Type | +| --------- | ------------| ----------------------------------------------------------------- | --------- | --------- | +| `context` | required | instream or outstream |'outstream' | string | +| `playerSize` | required | Avalaible sizes supported for video ad unit. | [[300, 250]] | [Integer, Integer] | +| `mimes` | required | List of content MIME types supported by the player. | ['video/mp4']| [String]| +| `protocols` | optional | Supported video bid response protocol values. | [2,3,5,6] | [integers]| +| `api` | optional | Supported API framework values. | [2] | [integers] | +| `maxduration` | optional | Maximum video ad duration in seconds. | 30 | Integer | +| `minduration` | optional | Minimum video ad duration in seconds. | 6 | Integer | +| `startdelay` | optional | Indicates the start delay in seconds for pre-roll, mid-roll, or post-roll ad placements. | 0 | Integer | +| `placement` | optional | Placement type for the impression. | 1 | Integer | +| `minbitrate` | optional | Minimum bit rate in Kbps. | 300 | Integer | +| `maxbitrate` | optional | Maximum bit rate in Kbps. | 9600 | Integer | +| `playbackmethod` | optional | Playback methods that may be in use. Only one method is typically used in practice. | [2]| [Integers] | +| `linearity` | optional | OpenRTB2 linearity. in-strea,overlay... | 1 | Integer | +| `skip` | optional | Indicates if the player will allow the video to be skipped, where 0 = no, 1 = yes . | 1 | Integer | +| `battr` | optional | IAB 5.3 Creative Attributes | [1,2,3] | [Number] | + + +# Video Ad Unit Config +``` + var adUnits = [{ + code: 'video-div', + mediaTypes: { + video: { + context: 'instream', // required + mimes: ['video/mp4'], // required + playerSize: [[ 640, 480 ]], // required + ... // skippable, startdelay, battr.. // optional + } + }, + bids: [{ + bidder: 'tpmn', + params: { + inventoryId: 2, // required + bidFloor: 2.0, // recommended + ... // bcat, badv, bapp // optional + } + }] + }]; ``` \ No newline at end of file diff --git a/modules/trafficgateBidAdapter.js b/modules/trafficgateBidAdapter.js index 6665c397fe9..fcd84306099 100644 --- a/modules/trafficgateBidAdapter.js +++ b/modules/trafficgateBidAdapter.js @@ -1,95 +1,150 @@ -import { getWindowLocation, deepAccess } from '../src/utils.js'; -import { registerBidder } from '../src/adapters/bidderFactory.js'; -import { BANNER, NATIVE, VIDEO } from '../src/mediaTypes.js'; -import { convertOrtbRequestToProprietaryNative } from '../src/native.js'; +import {registerBidder} from '../src/adapters/bidderFactory.js'; +import {BANNER, VIDEO} from '../src/mediaTypes.js'; +import {ortbConverter} from '../libraries/ortbConverter/converter.js'; +import {deepAccess, mergeDeep} from '../src/utils.js'; +import {convertTypes} from '../libraries/transformParamsUtils/convertTypes.js'; const BIDDER_CODE = 'trafficgate'; -const URL = 'https://[HOST].bc-plugin.com/?c=o&m=multi' +const URL = 'https://[HOST].bc-plugin.com/prebidjs' export const spec = { code: BIDDER_CODE, - supportedMediaTypes: [BANNER, VIDEO, NATIVE], + supportedMediaTypes: [BANNER, VIDEO], + isBidRequestValid, + buildRequests, + interpretResponse, + transformBidParams, + isBannerBid +}; - isBidRequestValid: function (bid) { - return !!(bid.bidId && bid.params && parseInt(bid.params.placementId) && bid.params.host) - }, - /** - * Make a server request from the list of BidRequests. - * - * @param {BidRequest[]} validBidRequests A non-empty list of valid bid requests that should be sent to the Server. - * @return ServerRequest Info describing the request to the server. - */ - buildRequests: function (validBidRequests) { - // convert Native ORTB definition to old-style prebid native definition - validBidRequests = convertOrtbRequestToProprietaryNative(validBidRequests); - if (validBidRequests && validBidRequests.length === 0) return []; - - const location = getWindowLocation() - const placements = [] - - validBidRequests.forEach((bidReq) => { - placements.push({ - placementId: bidReq.params.placementId, - bidId: bidReq.bidId, - traffic: getMediatype(bidReq) - }) - }) +registerBidder(spec) - return { - method: 'POST', - url: URL.replace('[HOST]', validBidRequests[0].params.host), - data: { - language: (navigator && navigator.language) ? navigator.language : '', - secure: +(location.protocol === 'https:'), - host: location.hostname, - page: location.pathname, - placements: placements +const converter = ortbConverter({ + context: { + netRevenue: true, + ttl: 300 + }, + imp(buildImp, bidRequest, context) { + const imp = buildImp(bidRequest, context); + mergeDeep(imp, { + ext: { + bidder: { + placementId: bidRequest.params.placementId, + host: bidRequest.params.host + } } + }); + if (bidRequest.params.customFloor && !imp.bidfloor) { + imp.bidfloor = bidRequest.params.customFloor; } + return imp; }, - - interpretResponse: function (opts) { - const body = opts.body - const response = [] - - for (let i = 0; i < body.length; i++) { - const item = body[i] - if (isBidResponseValid(item)) { - response.push(item) + request(buildRequest, imps, bidderRequest, context) { + const req = buildRequest(imps, bidderRequest, context); + mergeDeep(req, { + at: 1, + }) + const bid = context.bidRequests[0]; + if (bid.params.test) { + req.test = 1 + } + return req; + }, + bidResponse(buildBidResponse, bid, context) { + const bidResponse = buildBidResponse(bid, context); + if (bid.ext) { + bidResponse.meta.networkId = bid.ext.networkId; + bidResponse.meta.advertiserDomains = bid.ext.advertiserDomains; + } + return bidResponse; + }, + response(buildResponse, bidResponses, ortbResponse, context) { + const response = buildResponse(bidResponses, ortbResponse, context); + return response.bids + }, + overrides: { + imp: { + bidfloor(setBidFloor, imp, bidRequest, context) { + const floor = {}; + setBidFloor(floor, bidRequest, {...context, currency: 'USD'}); + if (floor.bidfloorcur === 'USD') { + Object.assign(imp, floor); + } + }, + video(orig, imp, bidRequest, context) { + if (FEATURES.VIDEO) { + let videoParams = bidRequest.mediaTypes[VIDEO]; + if (videoParams) { + videoParams = Object.assign({}, videoParams, bidRequest.params.video); + bidRequest = {...bidRequest, mediaTypes: {[VIDEO]: videoParams}} + } + orig(imp, bidRequest, context); + if (imp.video && videoParams?.context === 'outstream') { + imp.video.placement = imp.video.placement || 4; + } + } } } - - return response } -} +}); -registerBidder(spec) +function transformBidParams(params, isOpenRtb) { + return convertTypes({ + 'customFloor': 'number', + 'placementId': 'number', + 'host': 'string' + }, params); +} -function isBidResponseValid (bid) { - if (!bid.requestId || !bid.cpm || !bid.creativeId || - !bid.ttl || !bid.currency) { +function isBidRequestValid(bidRequest) { + const isValid = bidRequest.params.placementId && bidRequest.params.host; + if (!isValid) { return false } - switch (bid['mediaType']) { - case BANNER: - return Boolean(bid.width && bid.height && bid.ad) - case VIDEO: - return Boolean(bid.vastUrl) - case NATIVE: - return Boolean(bid.title && bid.image && bid.impressionTrackers) - default: - return false + if (isBannerBid(bidRequest)) { + return deepAccess(bidRequest, 'mediaTypes.banner.sizes.length') > 0; } + return true } -function getMediatype(bidRequest) { - if (deepAccess(bidRequest, 'mediaTypes.banner')) { - return BANNER; - } - if (deepAccess(bidRequest, 'mediaTypes.video')) { - return VIDEO; +function buildRequests(bids, bidderRequest) { + let videoBids = bids.filter(bid => isVideoBid(bid)); + let bannerBids = bids.filter(bid => isBannerBid(bid)); + let requests = bannerBids.length ? [createRequest(bannerBids, bidderRequest, BANNER)] : []; + videoBids.forEach(bid => { + requests.push(createRequest([bid], bidderRequest, VIDEO)); + }); + return requests; +} + +function createRequest(bidRequests, bidderRequest, mediaType) { + return { + method: 'POST', + url: URL.replace('[HOST]', bidRequests[0].params.host), + data: converter.toORTB({bidRequests, bidderRequest, context: {mediaType}}) } - if (deepAccess(bidRequest, 'mediaTypes.native')) { - return NATIVE; +} + +function isVideoBid(bid) { + return !!deepAccess(bid, 'mediaTypes.video'); +} + +function isBannerBid(bid) { + return !!deepAccess(bid, 'mediaTypes.banner'); +} + +function interpretResponse(resp, req) { + if (!resp.body) { + resp.body = {nbr: 0}; } + return converter.fromORTB({request: req.data, response: resp.body}); +} + +export const spec2 = { + code: BIDDER_CODE, + supportedMediaTypes: [BANNER, VIDEO], + + isBidRequestValid: function (bid) { + return !!(bid.bidId && bid.params && parseInt(bid.params.placementId) && bid.params.host) + }, } diff --git a/modules/trionBidAdapter.js b/modules/trionBidAdapter.js index b6375243a5a..e976396c71c 100644 --- a/modules/trionBidAdapter.js +++ b/modules/trionBidAdapter.js @@ -1,6 +1,7 @@ -import { getBidIdParameter, parseSizesInput, tryAppendQueryString } from '../src/utils.js'; +import {getBidIdParameter, parseSizesInput} from '../src/utils.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import { getStorageManager } from '../src/storageManager.js'; +import {tryAppendQueryString} from '../libraries/urlUtils/urlUtils.js'; const BID_REQUEST_BASE_URL = 'https://in-appadvertising.com/api/bidRequest'; const USER_SYNC_URL = 'https://in-appadvertising.com/api/userSync.html'; diff --git a/modules/tripleliftBidAdapter.js b/modules/tripleliftBidAdapter.js index df00495b88b..bfbf1409c1b 100644 --- a/modules/tripleliftBidAdapter.js +++ b/modules/tripleliftBidAdapter.js @@ -1,8 +1,9 @@ -import { tryAppendQueryString, logMessage, logError, isEmpty, isStr, isPlainObject, isArray, logWarn } from '../src/utils.js'; +import { logMessage, logError, isEmpty, isStr, isPlainObject, isArray, logWarn } from '../src/utils.js'; import { BANNER, VIDEO } from '../src/mediaTypes.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; import { config } from '../src/config.js'; import { getStorageManager } from '../src/storageManager.js'; +import {tryAppendQueryString} from '../libraries/urlUtils/urlUtils.js'; const GVLID = 28; const BIDDER_CODE = 'triplelift'; @@ -56,6 +57,10 @@ export const tripleliftAdapterSpec = { tlCall = tryAppendQueryString(tlCall, 'us_privacy', bidderRequest.uspConsent); } + if (bidderRequest && bidderRequest.fledgeEnabled) { + tlCall = tryAppendQueryString(tlCall, 'fledge', bidderRequest.fledgeEnabled); + } + if (config.getConfig('coppa') === true) { tlCall = tryAppendQueryString(tlCall, 'coppa', true); } @@ -75,12 +80,29 @@ export const tripleliftAdapterSpec = { interpretResponse: function(serverResponse, {bidderRequest}) { let bids = serverResponse.body.bids || []; - return bids.map(function(bid) { - return _buildResponseObject(bidderRequest, bid); - }); + const paapi = serverResponse.body.paapi || []; + + bids = bids.map(bid => _buildResponseObject(bidderRequest, bid)); + + if (paapi.length > 0) { + const fledgeAuctionConfigs = paapi.map(config => { + return { + bidId: bidderRequest.bids[config.imp_id].bidId, + config: config.auctionConfig + }; + }); + + logMessage('Response with FLEDGE:', { bids, fledgeAuctionConfigs }); + return { + bids, + fledgeAuctionConfigs + }; + } else { + return bids; + } }, - getUserSyncs: function(syncOptions, responses, gdprConsent, usPrivacy) { + getUserSyncs: function(syncOptions, responses, gdprConsent, usPrivacy, gppConsent) { let syncType = _getSyncType(syncOptions); if (!syncType) return; @@ -100,6 +122,15 @@ export const tripleliftAdapterSpec = { syncEndpoint = tryAppendQueryString(syncEndpoint, 'us_privacy', usPrivacy); } + if (gppConsent) { + if (gppConsent.gppString) { + syncEndpoint = tryAppendQueryString(syncEndpoint, 'gpp', gppConsent.gppString); + } + if (gppConsent.applicableSections && gppConsent.applicableSections.length !== 0) { + syncEndpoint = tryAppendQueryString(syncEndpoint, 'gpp_sid', _filterSid(gppConsent.applicableSections)); + } + } + return [{ type: syncType, url: syncEndpoint @@ -113,6 +144,13 @@ function _getSyncType(syncOptions) { if (syncOptions.pixelEnabled) return 'image'; } +function _filterSid(sid) { + return sid.filter(element => { + return Number.isInteger(element); + }) + .join(','); +} + function _buildPostBody(bidRequests, bidderRequest) { let data = {}; let { schain } = bidRequests[0]; @@ -169,6 +207,11 @@ function _buildPostBody(bidRequests, bidderRequest) { if (bidderRequest?.ortb2?.regs?.gpp) { data.regs = Object.assign({}, bidderRequest.ortb2.regs); } + + if (bidderRequest?.ortb2) { + data.ext.ortb2 = Object.assign({}, bidderRequest.ortb2); + } + return data; } @@ -197,7 +240,12 @@ function _getORTBVideo(bidRequest) { } catch (err) { logWarn('Video size not defined', err); } - if (video.context === 'instream') video.placement = 1; + // honor existing publisher settings + if (video.context === 'instream') { + if (!video.placement) { + video.placement = 1; + } + } if (video.context === 'outstream') { if (!video.placement) { video.placement = 3 diff --git a/modules/ttdBidAdapter.js b/modules/ttdBidAdapter.js index bbe207abc9e..d7705f2f5df 100644 --- a/modules/ttdBidAdapter.js +++ b/modules/ttdBidAdapter.js @@ -4,7 +4,16 @@ import { registerBidder } from '../src/adapters/bidderFactory.js'; import { BANNER, VIDEO } from '../src/mediaTypes.js'; import {isNumber} from '../src/utils.js'; -const BIDADAPTERVERSION = 'TTD-PREBID-2022.06.28'; +/** + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid + * @typedef {import('../src/adapters/bidderFactory.js').ServerResponse} ServerResponse + * @typedef {import('../src/adapters/bidderFactory.js').ServerRequest} ServerRequest + * @typedef {import('../src/adapters/bidderFactory.js').SyncOptions} SyncOptions + * @typedef {import('../src/adapters/bidderFactory.js').UserSync} UserSync + */ + +const BIDADAPTERVERSION = 'TTD-PREBID-2023.09.05'; const BIDDER_CODE = 'ttd'; const BIDDER_CODE_LONG = 'thetradedesk'; const BIDDER_ENDPOINT = 'https://direct.adsrvr.org/bid/bidder/'; @@ -406,7 +415,7 @@ export const spec = { buildRequests: function (validBidRequests, bidderRequest) { const firstPartyData = bidderRequest.ortb2 || {}; let topLevel = { - id: bidderRequest.auctionId, + id: bidderRequest.bidderRequestId, imp: validBidRequests.map(bidRequest => getImpression(bidRequest)), site: getSite(bidderRequest, firstPartyData), device: getDevice(firstPartyData), diff --git a/modules/ucfunnelBidAdapter.js b/modules/ucfunnelBidAdapter.js index 2225deaa900..19b933a8666 100644 --- a/modules/ucfunnelBidAdapter.js +++ b/modules/ucfunnelBidAdapter.js @@ -5,6 +5,12 @@ import { getStorageManager } from '../src/storageManager.js'; import { config } from '../src/config.js'; import { convertOrtbRequestToProprietaryNative } from '../src/native.js'; +/** + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid + * @typedef {import('../src/adapters/bidderFactory.js').ServerRequest} ServerRequest + */ + const COOKIE_NAME = 'ucf_uid'; const VER = 'ADGENT_PREBID-2018011501'; const BIDDER_CODE = 'ucfunnel'; @@ -64,11 +70,10 @@ export const spec = { * Format ucfunnel responses as Prebid bid responses * @param {ucfunnelResponseObj} ucfunnelResponse A successful response from ucfunnel. * @return {Bid[]} An array of formatted bids. - */ + */ interpretResponse: function (ucfunnelResponseObj, request) { const bidRequest = request.bidRequest; const ad = ucfunnelResponseObj ? ucfunnelResponseObj.body : {}; - const videoPlayerSize = parseSizes(bidRequest); let bid = { requestId: bidRequest.bidId, @@ -117,10 +122,10 @@ export const spec = { vastXml: ad.vastXml }); - if (videoPlayerSize && videoPlayerSize.length === 2) { + if (bidRequest.sizes && bidRequest.sizes.length > 0) { Object.assign(bid, { - width: videoPlayerSize[0], - height: videoPlayerSize[1] + width: bidRequest.sizes[0][0], + height: bidRequest.sizes[0][1] }); } break; @@ -128,8 +133,8 @@ export const spec = { default: var size = parseSizes(bidRequest); Object.assign(bid, { - width: ad.width || size[0], - height: ad.height || size[1], + width: ad.width || size[0][0], + height: ad.height || size[0][1], ad: ad.adm || '' }); } @@ -156,12 +161,6 @@ export const spec = { }; registerBidder(spec); -function transformSizes(requestSizes) { - if (typeof requestSizes === 'object' && requestSizes.length) { - return requestSizes[0]; - } -} - function getCookieSyncParameter(gdprApplies, apiVersion, consentString, uspConsent) { let param = '?'; if (gdprApplies == '1') { @@ -187,11 +186,10 @@ function parseSizes(bid) { params.video.playerWidth, params.video.playerHeight ]; - return size; + return [size]; } } - - return transformSizes(bid.sizes); + return bid.sizes; } function getSupplyChain(schain) { @@ -244,6 +242,20 @@ function getFloor(bid, size, mediaTypes) { return undefined; } +function addBidData(bidData, key, value) { + if (value) { + bidData[key] = value; + } +} + +function getFormat(size) { + let formatList = [] + for (var i = 0; i < size.length; i++) { + formatList.push(size[i].join(',')); + } + return (formatList.length > 0) ? formatList.join(';') : ''; +} + function getRequestData(bid, bidderRequest) { const size = parseSizes(bid); const language = navigator.language; @@ -264,14 +276,8 @@ function getRequestData(bid, bidderRequest) { schain: supplyChain }; - if (bidFloor) { - bidData.fp = bidFloor; - } - - if (gpid) { - bidData.gpid = gpid; - } - + addBidData(bidData, 'fp', bidFloor); + addBidData(bidData, 'gpid', gpid); addUserId(bidData, bid.userId); bidData.u = bidderRequest.refererInfo.page || bidderRequest.refererInfo.topmostLocation; @@ -293,10 +299,11 @@ function getRequestData(bid, bidderRequest) { } } - if (size != undefined && size.length == 2) { - bidData.w = size[0]; - bidData.h = size[1]; + if (size != undefined && size.length > 0 && size[0].length == 2) { + bidData.w = size[0][0]; + bidData.h = size[0][1]; } + addBidData(bidData, 'format', getFormat(size)); if (bidderRequest && bidderRequest.uspConsent) { Object.assign(bidData, { diff --git a/modules/uid2IdSystem.js b/modules/uid2IdSystem.js index f8178364e58..32d2322e9bd 100644 --- a/modules/uid2IdSystem.js +++ b/modules/uid2IdSystem.js @@ -7,13 +7,21 @@ */ import { logInfo, logWarn } from '../src/utils.js'; -import {submodule} from '../src/hook.js'; +import { submodule } from '../src/hook.js'; import {getStorageManager} from '../src/storageManager.js'; import {MODULE_TYPE_UID} from '../src/activities/modules.js'; // RE below lint exception: UID2 and EUID are separate modules, but the protocol is the same and shared code makes sense here. // eslint-disable-next-line prebid/validate-imports -import { Uid2GetId, Uid2CodeVersion } from './uid2IdSystem_shared.js'; +import { Uid2GetId, Uid2CodeVersion, extractIdentityFromParams } from './uid2IdSystem_shared.js'; +import {UID2_EIDS} from '../libraries/uid2Eids/uid2Eids.js'; + +/** + * @typedef {import('../modules/userId/index.js').Submodule} Submodule + * @typedef {import('../modules/userId/index.js').SubmoduleConfig} SubmoduleConfig + * @typedef {import('../modules/userId/index.js').ConsentData} ConsentData + * @typedef {import('../modules/userId/index.js').uid2Id} uid2Id + */ const MODULE_NAME = 'uid2'; const MODULE_REVISION = Uid2CodeVersion; @@ -32,6 +40,7 @@ function createLogger(logger, prefix) { logger(prefix + ' ', ...strings); } } + const _logInfo = createLogger(logInfo, LOG_PRE_FIX); const _logWarn = createLogger(logWarn, LOG_PRE_FIX); @@ -78,11 +87,20 @@ export const uid2IdSubmodule = { clientId: UID2_CLIENT_ID, internalStorage: ADVERTISING_COOKIE } + + if (FEATURES.UID2_CSTG) { + mappedConfig.cstg = { + serverPublicKey: config?.params?.serverPublicKey, + subscriptionId: config?.params?.subscriptionId, + ...extractIdentityFromParams(config?.params ?? {}) + } + } _logInfo(`UID2 configuration loaded and mapped.`, mappedConfig); const result = Uid2GetId(mappedConfig, storage, _logInfo, _logWarn); _logInfo(`UID2 getId returned`, result); return result; }, + eids: UID2_EIDS }; function decodeImpl(value) { diff --git a/modules/uid2IdSystem.md b/modules/uid2IdSystem.md index a795d9b1aa1..e546f6eafe1 100644 --- a/modules/uid2IdSystem.md +++ b/modules/uid2IdSystem.md @@ -1,11 +1,63 @@ ## UID2 User ID Submodule -UID2 requires initial tokens to be generated server-side. The UID2 module handles storing, providing, and optionally refreshing them. The module can operate in one of two different modes: *Client Refresh* mode or *Server Only* mode. +The UID2 module handles storing, providing, and optionally refreshing tokens. While initial tokens traditionally required server-side generation, the introduction of the *Client-Side Token Generation (CSTG)* mode offers publishers the flexibility to generate UID2 tokens directly from the module, eliminating this need. Publishers can choose to operate the module in one of three distinct modes: *Client Refresh* mode, *Server Only* mode and *Client-Side Token Generation* mode. *Server Only* mode was originally referred to as *legacy mode*, but it is a popular mode for new integrations where publishers prefer to handle token refresh server-side. +*Client-Side Token Generation* mode is included in UID2 module by default. However, it's important to note that this mode is created and made available recently. For publishers who do not intend to use it, you have the option to instruct the build to exclude the code related to this feature: + +``` + $ gulp build --modules=uid2IdSystem --disable UID2_CSTG +``` +If you do plan to use Client-Side Token Generation (CSTG) mode, please consult the UID2 Team first as they will provide required configuration values for you to use (see the Client-Side Token Generation (CSTG) mode section below for details) + **Important information:** UID2 is not designed to be used where GDPR applies. The module checks the passed-in consent data and will not operate if the `gdprApplies` flag is true. +## Client-Side Token Generation (CSTG) mode + +**This mode is created and made available recently. Please consult UID2 Team first as they will provide required configuration values for you to use.** + +For publishers seeking a purely client-side integration without the complexities of server-side involvement, the CSTG mode is highly recommended. This mode requires the provision of a public key, subscription ID and [directly identifying information (DII)](https://unifiedid.com/docs/ref-info/glossary-uid#gl-dii) - either emails or phone numbers. In the CSTG mode, the module takes on the responsibility of encrypting the DII, generating the UID2 token, and handling token refreshes when necessary. + +To configure the module to use this mode, you must: +1. Set `parmas.serverPublicKey` and `params.subscriptionId` (please reach out to the UID2 team to obtain these values) +2. Provide **ONLY ONE DII** by setting **ONLY ONE** of `params.email`/`params.phone`/`params.emailHash`/`params.phoneHash` + +Below is a table that provides guidance on when to use each directly identifying information (DII) parameter, along with information on whether normalization and hashing are required by the publisher for each parameter. + +| DII param | When to use it | Normalization required by publisher? | Hashing required by publisher? | +|------------------|-------------------------------------------------------|--------------------------------------|--------------------------------| +| params.email | When you have users' email address | No | No | +| params.phone | When you have user's phone number | Yes | No | +| params.emailHash | When you have user's hashed, normalized email address | Yes | Yes | +| params.phoneHash | When you have user's hashed, normalized phone number | Yes | Yes | + + +*Note that setting params.email will normalize email addresses, but params.phone requires phone numbers to be normalized.* + +Refer to [Normalization and Encoding](#normalization-and-encoding) for details on email address normalization, SHA-256 hashing and Base64 encoding. + +### CSTG example + +Configuration: +``` +pbjs.setConfig({ + userSync: { + userIds: [{ + name: 'uid2', + params: { + serverPublicKey: '...server public key...', + subscriptionId: '...subcription id...', + email: 'user@email.com', + //phone: '+0000000', + //emailHash: '...email hash...', + //phoneHash: '...phone hash ...' + } + }] + } +}); +``` + ## Client Refresh mode This is the recommended mode for most scenarios. In this mode, the full response body from the UID2 Token Generate or Token Refresh endpoint must be provided to the module. As long as the refresh token remains valid, the module will refresh the advertising token as needed. @@ -133,3 +185,80 @@ The below parameters apply only to the UID2 User ID Module integration. | params.uid2Cookie | Optional, Client refresh | String | The name of a cookie which holds the initial UID2 token, set by the server. The cookie should contain JSON in the same format as the uid2Token param. **If uid2Token is supplied, this param is ignored.** | See the sample token above. | | params.uid2ApiBase | Optional, Client refresh | String | Overrides the default UID2 API endpoint. | `"https://prod.uidapi.com"` _(default)_| | params.storage | Optional, Client refresh | String | Specify whether to use `cookie` or `localStorage` for module-internal storage. It is recommended to not provide this and allow the module to use the default. | `localStorage` _(default)_ | +| params.serverPublicKey | Optional, Client-side token generation | String | A public key for encrypting the DII payload for the Operator's CSTG endpoint. **This is required for client-side token generation.** | - | +| params.subscriptionId | Optional, Client-side token generation | String | A publisher Identifier. **This is required for client-side token generation.** | - | +| params.email | Optional, Client-side token generation | String | The user's email address. Provide this parameter if using email as the DII. | `"test@example.com"` | +| params.emailHash | Optional, Client-side token generation | String | A hashed, normalized representation of the user's email. Provide this parameter if using emailHash as the DII. | `"tMmiiTI7IaAcPpQPFQ65uMVCWH8av9jw4cwf/F5HVRQ="` | +| params.phone | Optional, Client-side token generation | String | The user's phone number. Provide this parameter if using phone as the DII. | `"+15555555555"` | +| params.phoneHash | Optional, Client-side token generation | String | A hashed, normalized representation of the user's phone number. Provide this parameter if using phoneHash as the DII. | `"tMmiiTI7IaAcPpQPFQ65uMVCWH8av9jw4cwf/F5HVRQ="` | + +# Normalization and Encoding + +This section provides information about normalizing and encoding [directly Identifying information (DII)](https://unifiedid.com/docs/ref-info/glossary-uid#gl-dii). It's important that, in working with UID2, normalizing and encoding are performed correctly. + +## Introduction +When you're taking user information such as an email address, and following the steps to create a raw UID2 and/or a UID2 advertising token, it's very important that you follow all the required steps. Whether you normalize the information or not, whether you hash it or not, follow the steps exactly. By doing so, you can ensure that the UID2 value you create can be securely and anonymously matched up with other instances of online behavior by the same user. + +>Note: Raw UID2s, and their associated UID2 tokens, are case sensitive. When working with UID2, it's important to pass all IDs and tokens without changing the case. Mismatched IDs can cause ID parsing or token decryption errors. + +## Types of Directly Identifying Information +UID2 supports the following types of directly identifying information (DII): +- Email address +- Phone number + +## Email Address Normalization + +If you send unhashed email addresses to the UID2 Operator Service, the service normalizes the email addresses and then hashes them. If you want to hash the email addresses yourself before sending them, you must normalize them before you hash them. + +> IMPORTANT: Normalizing before hashing ensures that the generated UID2 value will always be the same, so that the data can be matched. If you do not normalize before hashing, this might result in a different UID2, reducing the effectiveness of targeted advertising. + +To normalize an email address, complete the following steps: + +1. Remove leading and trailing spaces. +2. Convert all ASCII characters to lowercase. +3. In `gmail.com` email addresses, remove the following characters from the username part of the email address: + 1. The period (`.` (ASCII code 46)).
For example, normalize `jane.doe@gmail.com` to `janedoe@gmail.com`. + 2. The plus sign (`+` (ASCII code 43)) and all subsequent characters.
For example, normalize `janedoe+home@gmail.com` to `janedoe@gmail.com`. + +## Email Address Hash Encoding + +An email hash is a Base64-encoded SHA-256 hash of a normalized email address. The email address is first normalized, then hashed using the SHA-256 hashing algorithm, and then the resulting bytes of the hash value are encoded using Base64 encoding. Note that the bytes of the hash value are encoded, not the hex-encoded string representation. + +| Type | Example | Comments and Usage | +| :--- | :--- | :--- | +| Normalized email address | `user@example.com` | Normalization is always the first step. | +| SHA-256 hash of normalized email address | `b4c9a289323b21a01c3e940f150eb9b8c542587f1abfd8f0e1cc1ffc5e475514` | This 64-character string is a hex-encoded representation of the 32-byte SHA-256.| +| Hex to Base64 SHA-256 encoding of normalized email address | `tMmiiTI7IaAcPpQPFQ65uMVCWH8av9jw4cwf/F5HVRQ=` | This 44-character string is a Base64-encoded representation of the 32-byte SHA-256.
WARNING: The SHA-256 hash string in the example above is a hex-encoded representation of the hash value. You must Base64-encode the raw bytes of the hash or use a Base64 encoder that takes a hex-encoded value as input.
Use this encoding for `email_hash` values sent in the request body. | + +>WARNING: When applying Base64 encoding, be sure to Base64-encode the raw bytes of the hash or use a Base64 encoder that takes a hex-encoded value as input. + +## Phone Number Normalization + +If you send unhashed phone numbers to the UID2 Operator Service, the service normalizes the phone numbers and then hashes them. If you want to hash the phone numbers yourself before sending them, you must normalize them before you hash them. + +> IMPORTANT: Normalization before hashing ensures that the generated UID2 value will always be the same, so that the data can be matched. If you do not normalize before hashing, this might result in a different UID2, reducing the effectiveness of targeted advertising. + +Here's what you need to know about phone number normalization rules: + +- The UID2 Operator accepts phone numbers in the [E.164](https://en.wikipedia.org/wiki/E.164) format, which is the international phone number format that ensures global uniqueness. +- E.164 phone numbers can have a maximum of 15 digits. +- Normalized E.164 phone numbers use the following syntax, with no spaces, hyphens, parentheses, or other special characters:
+ `[+] [country code] [subscriber number including area code]` + Examples: + - US: `1 (123) 456-7890` is normalized to `+11234567890`. + - Singapore: `65 1243 5678` is normalized to `+6512345678`. + - Sydney, Australia: `(02) 1234 5678` is normalized to drop the leading zero for the city plus include the country code: `+61212345678`. + +## Phone Number Hash Encoding + +A phone number hash is a Base64-encoded SHA-256 hash of a normalized phone number. The phone number is first normalized, then hashed using the SHA-256 hashing algorithm, and the resulting hex value is encoded using Base64 encoding. + +The example below shows a simple input phone number, and the result as each step is applied to arrive at a secure, opaque, URL-safe value. + +| Type | Example | Comments and Usage | +| :--- | :--- | :--- | +| Normalized phone number | `+12345678901` | Normalization is always the first step. | +| SHA-256 hash of normalized phone number | `10e6f0b47054a83359477dcb35231db6de5c69fb1816e1a6b98e192de9e5b9ee` |This 64-character string is a hex-encoded representation of the 32-byte SHA-256. | +| Hex to Base64 SHA-256 encoding of normalized and hashed phone number | `EObwtHBUqDNZR33LNSMdtt5cafsYFuGmuY4ZLenlue4=` | This 44-character string is a Base64-encoded representation of the 32-byte SHA-256.
NOTE: The SHA-256 hash is a hexadecimal value. You must use a Base64 encoder that takes a hex value as input. Use this encoding for `phone_hash` values sent in the request body. | + +>WARNING: When applying Base64 encoding, be sure to use a function that takes a hex value as input. If you use a function that takes text as input, the result is a longer string which is invalid for the purposes of UID2. diff --git a/modules/uid2IdSystem_shared.js b/modules/uid2IdSystem_shared.js index 0f6894a9d3e..acc440eafc5 100644 --- a/modules/uid2IdSystem_shared.js +++ b/modules/uid2IdSystem_shared.js @@ -1,4 +1,7 @@ /* eslint-disable no-console */ +import { ajax } from '../src/ajax.js'; +import { cyrb53Hash } from '../src/utils.js'; + export const Uid2CodeVersion = '1.1'; function isValidIdentity(identity) { @@ -13,6 +16,7 @@ export class Uid2ApiClient { this._logInfo = logInfo; this._logWarn = logWarn; } + createArrayBuffer(text) { const arrayBuffer = new Uint8Array(text.length); for (let i = 0; i < text.length; i++) { @@ -36,49 +40,58 @@ export class Uid2ApiClient { } callRefreshApi(refreshDetails) { const url = this._baseUrl + '/v2/token/refresh'; - const req = new XMLHttpRequest(); - req.overrideMimeType('text/plain'); - req.open('POST', url, true); - req.setRequestHeader('X-UID2-Client-Version', this._clientVersion); let resolvePromise; let rejectPromise; const promise = new Promise((resolve, reject) => { resolvePromise = resolve; rejectPromise = reject; }); - req.onreadystatechange = () => { - if (req.readyState !== req.DONE) { return; } - try { - if (!refreshDetails.refresh_response_key || req.status !== 200) { - this._logInfo('Error status OR no response decryption key available, assuming unencrypted JSON'); - const response = JSON.parse(req.responseText); + this._logInfo('Sending refresh request', refreshDetails); + ajax(url, { + success: (responseText) => { + try { + if (!refreshDetails.refresh_response_key) { + this._logInfo('No response decryption key available, assuming unencrypted JSON'); + const response = JSON.parse(responseText); + const result = this.ResponseToRefreshResult(response); + if (typeof result === 'string') { rejectPromise(result); } else { resolvePromise(result); } + } else { + this._logInfo('Decrypting refresh API response'); + const encodeResp = this.createArrayBuffer(atob(responseText)); + window.crypto.subtle.importKey('raw', this.createArrayBuffer(atob(refreshDetails.refresh_response_key)), { name: 'AES-GCM' }, false, ['decrypt']).then((key) => { + this._logInfo('Imported decryption key') + // returns the symmetric key + window.crypto.subtle.decrypt({ + name: 'AES-GCM', + iv: encodeResp.slice(0, 12), + tagLength: 128, // The tagLength you used to encrypt (if any) + }, key, encodeResp.slice(12)).then((decrypted) => { + const decryptedResponse = String.fromCharCode(...new Uint8Array(decrypted)); + this._logInfo('Decrypted to:', decryptedResponse); + const response = JSON.parse(decryptedResponse); + const result = this.ResponseToRefreshResult(response); + if (typeof result === 'string') { rejectPromise(result); } else { resolvePromise(result); } + }, (reason) => this._logWarn(`Call to UID2 API failed`, reason)); + }, (reason) => this._logWarn(`Call to UID2 API failed`, reason)); + } + } catch (_err) { + rejectPromise(responseText); + } + }, + error: (error, xhr) => { + try { + this._logInfo('Error status, assuming unencrypted JSON'); + const response = JSON.parse(xhr.responseText); const result = this.ResponseToRefreshResult(response); if (typeof result === 'string') { rejectPromise(result); } else { resolvePromise(result); } - } else { - this._logInfo('Decrypting refresh API response'); - const encodeResp = this.createArrayBuffer(atob(req.responseText)); - window.crypto.subtle.importKey('raw', this.createArrayBuffer(atob(refreshDetails.refresh_response_key)), { name: 'AES-GCM' }, false, ['decrypt']).then((key) => { - this._logInfo('Imported decryption key') - // returns the symmetric key - window.crypto.subtle.decrypt({ - name: 'AES-GCM', - iv: encodeResp.slice(0, 12), - tagLength: 128, // The tagLength you used to encrypt (if any) - }, key, encodeResp.slice(12)).then((decrypted) => { - const decryptedResponse = String.fromCharCode(...new Uint8Array(decrypted)); - this._logInfo('Decrypted to:', decryptedResponse); - const response = JSON.parse(decryptedResponse); - const result = this.ResponseToRefreshResult(response); - if (typeof result === 'string') { rejectPromise(result); } else { resolvePromise(result); } - }, (reason) => this._logWarn(`Call to UID2 API failed`, reason)); - }, (reason) => this._logWarn(`Call to UID2 API failed`, reason)); + } catch (_e) { + rejectPromise(error) } - } catch (err) { - rejectPromise(err); } - }; - this._logInfo('Sending refresh request', refreshDetails); - req.send(refreshDetails.refresh_token); + }, refreshDetails.refresh_token, { method: 'POST', + customHeaders: { + 'X-UID2-Client-Version': this._clientVersion + } }); return promise; } } @@ -160,18 +173,507 @@ function refreshTokenAndStore(baseUrl, token, clientId, storageManager, _logInfo originalToken: token, latestToken: response.identity, }; + let storedTokens = storageManager.getStoredValueWithFallback(); + if (storedTokens?.originalIdentity) tokens.originalIdentity = storedTokens.originalIdentity; storageManager.storeValue(tokens); return tokens; }); } +let clientSideTokenGenerator; +if (FEATURES.UID2_CSTG) { + const SERVER_PUBLIC_KEY_PREFIX_LENGTH = 9; + + clientSideTokenGenerator = { + isCSTGOptionsValid(maybeOpts, _logWarn) { + if (typeof maybeOpts !== 'object' || maybeOpts === null) { + _logWarn('CSTG opts must be an object'); + return false; + } + + const opts = maybeOpts; + if (typeof opts.serverPublicKey !== 'string') { + _logWarn('CSTG opts.serverPublicKey must be a string'); + return false; + } + const serverPublicKeyPrefix = /^UID2-X-[A-Z]-.+/; + if (!serverPublicKeyPrefix.test(opts.serverPublicKey)) { + _logWarn( + `CSTG opts.serverPublicKey must match the regular expression ${serverPublicKeyPrefix}` + ); + return false; + } + // We don't do any further validation of the public key, as we will find out + // later if it's valid by using importKey. + + if (typeof opts.subscriptionId !== 'string') { + _logWarn('CSTG opts.subscriptionId must be a string'); + return false; + } + if (opts.subscriptionId.length === 0) { + _logWarn('CSTG opts.subscriptionId is empty'); + return false; + } + return true; + }, + + getValidIdentity(opts, _logWarn) { + if (opts.emailHash) { + if (!UID2DiiNormalization.isBase64Hash(opts.emailHash)) { + _logWarn('CSTG opts.emailHash is invalid'); + return; + } + return { email_hash: opts.emailHash }; + } + + if (opts.phoneHash) { + if (!UID2DiiNormalization.isBase64Hash(opts.phoneHash)) { + _logWarn('CSTG opts.phoneHash is invalid'); + return; + } + return { phone_hash: opts.phoneHash }; + } + + if (opts.email) { + const normalizedEmail = UID2DiiNormalization.normalizeEmail(opts.email); + if (normalizedEmail === undefined) { + _logWarn('CSTG opts.email is invalid'); + return; + } + return { email: normalizedEmail }; + } + + if (opts.phone) { + if (!UID2DiiNormalization.isNormalizedPhone(opts.phone)) { + _logWarn('CSTG opts.phone is invalid'); + return; + } + return { phone: opts.phone }; + } + }, + + isStoredTokenInvalid(cstgIdentity, storedTokens, _logInfo, _logWarn) { + if (storedTokens) { + const identity = Object.values(cstgIdentity)[0]; + if (!this.isStoredTokenFromSameIdentity(storedTokens, identity)) { + _logInfo( + 'CSTG supplied new identity - ignoring stored value.', + storedTokens.originalIdentity, + cstgIdentity + ); + // Stored token wasn't originally sourced from the provided identity - ignore the stored value. A new user has logged in? + return true; + } + } + return false; + }, + + async generateTokenAndStore( + baseUrl, + cstgOpts, + cstgIdentity, + storageManager, + _logInfo, + _logWarn + ) { + _logInfo('UID2 cstg opts provided: ', JSON.stringify(cstgOpts)); + const client = new UID2CstgApiClient( + { baseUrl, cstg: cstgOpts }, + _logInfo, + _logWarn + ); + const response = await client.generateToken(cstgIdentity); + _logInfo('CSTG endpoint responded with:', response); + const tokens = { + originalIdentity: this.encodeOriginalIdentity(cstgIdentity), + latestToken: response.identity, + }; + storageManager.storeValue(tokens); + return tokens; + }, + + isStoredTokenFromSameIdentity(storedTokens, identity) { + if (!storedTokens.originalIdentity) return false; + return ( + cyrb53Hash(identity, storedTokens.originalIdentity.salt) === + storedTokens.originalIdentity.identity + ); + }, + + encodeOriginalIdentity(identity) { + const identityValue = Object.values(identity)[0]; + const salt = Math.floor(Math.random() * Math.pow(2, 32)); + return { + identity: cyrb53Hash(identityValue, salt), + salt, + }; + }, + }; + + class UID2DiiNormalization { + static EMAIL_EXTENSION_SYMBOL = '+'; + static EMAIL_DOT = '.'; + static GMAIL_DOMAIN = 'gmail.com'; + + static isBase64Hash(value) { + if (!(value && value.length === 44)) { + return false; + } + + try { + return btoa(atob(value)) === value; + } catch (err) { + return false; + } + } + + static isNormalizedPhone(phone) { + return /^\+[0-9]{10,15}$/.test(phone); + } + + static normalizeEmail(email) { + if (!email || !email.length) return; + + const parsedEmail = email.trim().toLowerCase(); + if (parsedEmail.indexOf(' ') > 0) return; + + const emailParts = this.splitEmailIntoAddressAndDomain(parsedEmail); + if (!emailParts) return; + + const { address, domain } = emailParts; + + const emailIsGmail = this.isGmail(domain); + const parsedAddress = this.normalizeAddressPart( + address, + emailIsGmail, + emailIsGmail + ); + + return parsedAddress ? `${parsedAddress}@${domain}` : undefined; + } + + static splitEmailIntoAddressAndDomain(email) { + const parts = email.split('@'); + if ( + parts.length !== 2 || + parts.some((part) => part === '') + ) { return; } + + return { + address: parts[0], + domain: parts[1], + }; + } + + static isGmail(domain) { + return domain === this.GMAIL_DOMAIN; + } + + static dropExtension(address, extensionSymbol = this.EMAIL_EXTENSION_SYMBOL) { + return address.split(extensionSymbol)[0]; + } + + static normalizeAddressPart(address, shouldRemoveDot, shouldDropExtension) { + let parsedAddress = address; + if (shouldRemoveDot) { parsedAddress = parsedAddress.replaceAll(this.EMAIL_DOT, ''); } + if (shouldDropExtension) parsedAddress = this.dropExtension(parsedAddress); + return parsedAddress; + } + } + + class UID2CstgApiClient { + constructor(opts, logInfo, logWarn) { + this._baseUrl = opts.baseUrl; + this._serverPublicKey = opts.cstg.serverPublicKey; + this._subscriptionId = opts.cstg.subscriptionId; + this._logInfo = logInfo; + this._logWarn = logWarn; + } + + hasStatusResponse(response) { + return typeof response === 'object' && response && response.status; + } + + isCstgApiSuccessResponse(response) { + return ( + this.hasStatusResponse(response) && + response.status === 'success' && + isValidIdentity(response.body) + ); + } + + isCstgApiClientErrorResponse(response) { + return ( + this.hasStatusResponse(response) && + response.status === 'client_error' && + typeof response.message === 'string' + ); + } + + isCstgApiForbiddenResponse(response) { + return ( + this.hasStatusResponse(response) && + response.status === 'invalid_http_origin' && + typeof response.message === 'string' + ); + } + + stripPublicKeyPrefix(serverPublicKey) { + return serverPublicKey.substring(SERVER_PUBLIC_KEY_PREFIX_LENGTH); + } + + async generateCstgRequest(cstgIdentity) { + if ('email_hash' in cstgIdentity || 'phone_hash' in cstgIdentity) { + return cstgIdentity; + } + if ('email' in cstgIdentity) { + const emailHash = await UID2CstgCrypto.hash(cstgIdentity.email); + return { email_hash: emailHash }; + } + if ('phone' in cstgIdentity) { + const phoneHash = await UID2CstgCrypto.hash(cstgIdentity.phone); + return { phone_hash: phoneHash }; + } + } + + async generateToken(cstgIdentity) { + const request = await this.generateCstgRequest(cstgIdentity); + this._logInfo('Building CSTG request for', request); + const box = await UID2CstgBox.build( + this.stripPublicKeyPrefix(this._serverPublicKey) + ); + const encoder = new TextEncoder(); + const now = Date.now(); + const { iv, ciphertext } = await box.encrypt( + encoder.encode(JSON.stringify(request)), + encoder.encode(JSON.stringify([now])) + ); + + const exportedPublicKey = await UID2CstgCrypto.exportPublicKey( + box.clientPublicKey + ); + const requestBody = { + payload: UID2CstgCrypto.bytesToBase64(new Uint8Array(ciphertext)), + iv: UID2CstgCrypto.bytesToBase64(new Uint8Array(iv)), + public_key: UID2CstgCrypto.bytesToBase64( + new Uint8Array(exportedPublicKey) + ), + timestamp: now, + subscription_id: this._subscriptionId, + }; + return this.callCstgApi(requestBody, box); + } + + async callCstgApi(requestBody, box) { + const url = this._baseUrl + '/v2/token/client-generate'; + let resolvePromise; + let rejectPromise; + const promise = new Promise((resolve, reject) => { + resolvePromise = resolve; + rejectPromise = reject; + }); + + this._logInfo('Sending CSTG request', requestBody); + ajax( + url, + { + success: async (responseText, xhr) => { + try { + const encodedResp = UID2CstgCrypto.base64ToBytes(responseText); + const decrypted = await box.decrypt( + encodedResp.slice(0, 12), + encodedResp.slice(12) + ); + const decryptedResponse = new TextDecoder().decode(decrypted); + const response = JSON.parse(decryptedResponse); + if (this.isCstgApiSuccessResponse(response)) { + resolvePromise({ + status: 'success', + identity: response.body, + }); + } else { + // A 200 should always be a success response. + // Something has gone wrong. + rejectPromise( + `API error: Response body was invalid for HTTP status 200: ${decryptedResponse}` + ); + } + } catch (err) { + rejectPromise(err); + } + }, + error: (error, xhr) => { + try { + if (xhr.status === 400) { + const response = JSON.parse(xhr.responseText); + if (this.isCstgApiClientErrorResponse(response)) { + rejectPromise(`Client error: ${response.message}`); + } else { + // A 400 should always be a client error. + // Something has gone wrong. + rejectPromise( + `API error: Response body was invalid for HTTP status 400: ${xhr.responseText}` + ); + } + } else if (xhr.status === 403) { + const response = JSON.parse(xhr.responseText); + if (this.isCstgApiForbiddenResponse(xhr)) { + rejectPromise(`Forbidden: ${response.message}`); + } else { + // A 403 should always be a forbidden response. + // Something has gone wrong. + rejectPromise( + `API error: Response body was invalid for HTTP status 403: ${xhr.responseText}` + ); + } + } else { + rejectPromise( + `API error: Unexpected HTTP status ${xhr.status}: ${error}` + ); + } + } catch (_e) { + rejectPromise(error); + } + }, + }, + JSON.stringify(requestBody), + { method: 'POST' } + ); + return promise; + } + } + + class UID2CstgBox { + static _namedCurve = 'P-256'; + constructor(clientPublicKey, sharedKey) { + this._clientPublicKey = clientPublicKey; + this._sharedKey = sharedKey; + } + + static async build(serverPublicKey) { + const clientKeyPair = await UID2CstgCrypto.generateKeyPair( + UID2CstgBox._namedCurve + ); + const importedServerPublicKey = await UID2CstgCrypto.importPublicKey( + serverPublicKey, + this._namedCurve + ); + const sharedKey = await UID2CstgCrypto.deriveKey( + importedServerPublicKey, + clientKeyPair.privateKey + ); + return new UID2CstgBox(clientKeyPair.publicKey, sharedKey); + } + + async encrypt(plaintext, additionalData) { + const iv = window.crypto.getRandomValues(new Uint8Array(12)); + const ciphertext = await window.crypto.subtle.encrypt( + { + name: 'AES-GCM', + iv, + additionalData, + }, + this._sharedKey, + plaintext + ); + return { iv, ciphertext }; + } + + async decrypt(iv, ciphertext) { + return window.crypto.subtle.decrypt( + { + name: 'AES-GCM', + iv, + }, + this._sharedKey, + ciphertext + ); + } + + get clientPublicKey() { + return this._clientPublicKey; + } + } + + class UID2CstgCrypto { + static base64ToBytes(base64) { + const binString = atob(base64); + return Uint8Array.from(binString, (m) => m.codePointAt(0)); + } + + static bytesToBase64(bytes) { + const binString = Array.from(bytes, (x) => String.fromCodePoint(x)).join( + '' + ); + return btoa(binString); + } + + static async generateKeyPair(namedCurve) { + const params = { + name: 'ECDH', + namedCurve: namedCurve, + }; + return window.crypto.subtle.generateKey(params, false, ['deriveKey']); + } + + static async importPublicKey(publicKey, namedCurve) { + const params = { + name: 'ECDH', + namedCurve: namedCurve, + }; + return window.crypto.subtle.importKey( + 'spki', + this.base64ToBytes(publicKey), + params, + false, + [] + ); + } + + static exportPublicKey(publicKey) { + return window.crypto.subtle.exportKey('spki', publicKey); + } + + static async deriveKey(serverPublicKey, clientPrivateKey) { + return window.crypto.subtle.deriveKey( + { + name: 'ECDH', + public: serverPublicKey, + }, + clientPrivateKey, + { + name: 'AES-GCM', + length: 256, + }, + false, + ['encrypt', 'decrypt'] + ); + } + + static async hash(value) { + const hash = await window.crypto.subtle.digest( + 'SHA-256', + new TextEncoder().encode(value) + ); + return this.bytesToBase64(new Uint8Array(hash)); + } + } +} + export function Uid2GetId(config, prebidStorageManager, _logInfo, _logWarn) { let suppliedToken = null; const preferLocalStorage = (config.storage !== 'cookie'); const storageManager = new Uid2StorageManager(prebidStorageManager, preferLocalStorage, config.internalStorage, _logInfo); _logInfo(`Module is using ${preferLocalStorage ? 'local storage' : 'cookies'} for internal storage.`); - if (config.paramToken) { + const isCstgEnabled = + clientSideTokenGenerator && + clientSideTokenGenerator.isCSTGOptionsValid(config.cstg, _logWarn); + if (isCstgEnabled) { + _logInfo(`Module is using client-side token generation.`); + // Ignores config.paramToken and config.serverCookieName if any is provided + suppliedToken = null; + } else if (config.paramToken) { suppliedToken = config.paramToken; _logInfo('Read token from params', suppliedToken); } else if (config.serverCookieName) { @@ -184,7 +686,7 @@ export function Uid2GetId(config, prebidStorageManager, _logInfo, _logWarn) { if (storedTokens && typeof storedTokens === 'string') { // Stored value is a plain token - if no token is supplied, just use the stored value. - if (!suppliedToken) { + if (!suppliedToken && !isCstgEnabled) { _logInfo('Returning legacy cookie value.'); return { id: storedTokens }; } @@ -200,11 +702,31 @@ export function Uid2GetId(config, prebidStorageManager, _logInfo, _logWarn) { storedTokens = null; } } - // At this point, any legacy values or superseded stored tokens have been nulled out. + + if (FEATURES.UID2_CSTG && isCstgEnabled) { + const cstgIdentity = clientSideTokenGenerator.getValidIdentity(config.cstg, _logWarn); + if (cstgIdentity) { + if (storedTokens && clientSideTokenGenerator.isStoredTokenInvalid(cstgIdentity, storedTokens, _logInfo, _logWarn)) { + storedTokens = null; + } + + if (!storedTokens || Date.now() > storedTokens.latestToken.refresh_expires) { + const promise = clientSideTokenGenerator.generateTokenAndStore(config.apiBaseUrl, config.cstg, cstgIdentity, storageManager, _logInfo, _logWarn); + _logInfo('Generate token using CSTG'); + return { callback: (cb) => { + promise.then((result) => { + _logInfo('Token generation responded, passing the new token on.', result); + cb(result); + }); + } }; + } + } + } + const useSuppliedToken = !(storedTokens?.latestToken) || (suppliedToken && suppliedToken.identity_expires > storedTokens.latestToken.identity_expires); const newestAvailableToken = useSuppliedToken ? suppliedToken : storedTokens.latestToken; _logInfo('UID2 module selected latest token', useSuppliedToken, newestAvailableToken); - if (!newestAvailableToken || Date.now() > newestAvailableToken.refresh_expires) { + if ((!newestAvailableToken || Date.now() > newestAvailableToken.refresh_expires)) { _logInfo('Newest available token is expired and not refreshable.'); return { id: null }; } @@ -227,6 +749,21 @@ export function Uid2GetId(config, prebidStorageManager, _logInfo, _logWarn) { originalToken: suppliedToken ?? storedTokens?.originalToken, latestToken: newestAvailableToken, }; + if (FEATURES.UID2_CSTG && isCstgEnabled) { + tokens.originalIdentity = storedTokens?.originalIdentity; + } storageManager.storeValue(tokens); return { id: tokens }; } + +export function extractIdentityFromParams(params) { + const keysToCheck = ['emailHash', 'phoneHash', 'email', 'phone']; + + for (let key of keysToCheck) { + if (params.hasOwnProperty(key)) { + return { [key]: params[key] }; + } + } + + return {}; +} diff --git a/modules/underdogmediaBidAdapter.js b/modules/underdogmediaBidAdapter.js index 7561cdb60de..54b74c7ccd4 100644 --- a/modules/underdogmediaBidAdapter.js +++ b/modules/underdogmediaBidAdapter.js @@ -4,7 +4,6 @@ import { getWindowSelf, getWindowTop, isGptPubadsDefined, - isSlotMatchingAdUnitCode, logInfo, logMessage, logWarn, @@ -12,6 +11,7 @@ import { } from '../src/utils.js'; import {config} from '../src/config.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; +import {isSlotMatchingAdUnitCode} from '../libraries/gptUtils/gptUtils.js'; const BIDDER_CODE = 'underdogmedia'; const UDM_ADAPTER_VERSION = '7.30V'; @@ -71,7 +71,7 @@ export const spec = { let data = { dt: 10, gdpr: {}, - pbTimeout: config.getConfig('bidderTimeout'), + pbTimeout: +config.getConfig('bidderTimeout') || 3001, // KP: convert to number and if NaN we default to 3001. Particular value to let us know that there was a problem in converting pbTimeout pbjsVersion: prebidVersion, placements: [], ref: deepAccess(bidderRequest, 'refererInfo.page') ? bidderRequest.refererInfo.page : undefined, diff --git a/modules/undertoneBidAdapter.js b/modules/undertoneBidAdapter.js index 9dcbefd374b..c7e8102ffc9 100644 --- a/modules/undertoneBidAdapter.js +++ b/modules/undertoneBidAdapter.js @@ -139,6 +139,7 @@ export const spec = { domain: domain, placementId: bidReq.params.placementId != undefined ? bidReq.params.placementId : null, publisherId: bidReq.params.publisherId, + gpid: deepAccess(bidReq, 'ortb2Imp.ext.gpid', deepAccess(bidReq, 'ortb2Imp.ext.data.pbadslot', '')), sizes: bidReq.sizes, params: bidReq.params }; diff --git a/modules/undertoneBidAdapter.md b/modules/undertoneBidAdapter.md index 8e0b234fd7a..1cfc912e360 100644 --- a/modules/undertoneBidAdapter.md +++ b/modules/undertoneBidAdapter.md @@ -23,7 +23,7 @@ Module that connects to Undertone's demand sources { bidder: "undertone", params: { - placementId: '10433394', + placementId: 1234, publisherId: 12345 } } diff --git a/modules/unicornBidAdapter.js b/modules/unicornBidAdapter.js index 66aaf4a17e5..43eb943f6d5 100644 --- a/modules/unicornBidAdapter.js +++ b/modules/unicornBidAdapter.js @@ -3,6 +3,11 @@ import {BANNER} from '../src/mediaTypes.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import {getStorageManager} from '../src/storageManager.js'; +/** + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + * @typedef {import('../src/adapters/bidderFactory.js').ServerRequest} ServerRequest + */ + const BIDDER_CODE = 'unicorn'; const UNICORN_ENDPOINT = 'https://ds.uncn.jp/pb/0/bid.json'; const UNICORN_DEFAULT_CURRENCY = 'JPY'; @@ -87,10 +92,33 @@ function buildOpenRtbBidRequestPayload(validBidRequests, bidderRequest) { accountId: deepAccess(validBidRequests[0], 'params.accountId') } }; + const eids = initializeEids(validBidRequests[0]); + if (eids.length > 0) { + request.user.eids = eids; + } + logInfo('[UNICORN] OpenRTB Formatted Request:', request); return JSON.stringify(request); } +const initializeEids = (bidRequest) => { + let eids = []; + + let id5 = deepAccess(bidRequest, 'userId.id5id.uid'); + if (id5) { + eids.push({ + source: 'id5-sync.com', + uids: [ + { + id: id5 + } + ] + }); + } + + return eids; +} + const interpretResponse = (serverResponse, request) => { logInfo('[UNICORN] interpretResponse.serverResponse:', serverResponse); logInfo('[UNICORN] interpretResponse.request:', request); diff --git a/modules/unifiedIdSystem.js b/modules/unifiedIdSystem.js index 8ec5fcd3f90..e88aec3a90f 100644 --- a/modules/unifiedIdSystem.js +++ b/modules/unifiedIdSystem.js @@ -9,6 +9,12 @@ import { logError } from '../src/utils.js'; import {ajax} from '../src/ajax.js'; import {submodule} from '../src/hook.js' +/** + * @typedef {import('../modules/userId/index.js').Submodule} Submodule + * @typedef {import('../modules/userId/index.js').SubmoduleConfig} SubmoduleConfig + * @typedef {import('../modules/userId/index.js').IdResponse} IdResponse + */ + const MODULE_NAME = 'unifiedId'; /** @type {Submodule} */ @@ -67,6 +73,17 @@ export const unifiedIdSubmodule = { ajax(url, callbacks, undefined, {method: 'GET', withCredentials: true}); }; return {callback: resp}; + }, + eids: { + 'tdid': { + source: 'adserver.org', + atype: 1, + getUidExt: function() { + return { + rtiPartner: 'TDID' + }; + } + }, } }; diff --git a/modules/unrulyBidAdapter.js b/modules/unrulyBidAdapter.js index d893bfd3038..b825003f36f 100644 --- a/modules/unrulyBidAdapter.js +++ b/modules/unrulyBidAdapter.js @@ -56,6 +56,12 @@ const RemoveDuplicateSizes = (validBid) => { } }; +const ConfigureProtectedAudience = (validBid, protectedAudienceEnabled) => { + if (!protectedAudienceEnabled && validBid.ortb2Imp && validBid.ortb2Imp.ext) { + delete validBid.ortb2Imp.ext.ae; + } +} + const getRequests = (conf, validBidRequests, bidderRequest) => { const {bids, bidderRequestId, bidderCode, ...bidderRequestData} = bidderRequest; const invalidBidsCount = bidderRequest.bids.length - validBidRequests.length; @@ -65,6 +71,7 @@ const getRequests = (conf, validBidRequests, bidderRequest) => { const currSiteId = validBid.params.siteId; addBidFloorInfo(validBid); RemoveDuplicateSizes(validBid); + ConfigureProtectedAudience(validBid, conf.protectedAudienceEnabled); requestBySiteId[currSiteId] = requestBySiteId[currSiteId] || []; requestBySiteId[currSiteId].push(validBid); }); @@ -73,7 +80,14 @@ const getRequests = (conf, validBidRequests, bidderRequest) => { Object.keys(requestBySiteId).forEach((key) => { let data = { - bidderRequest: Object.assign({}, {bids: requestBySiteId[key], invalidBidsCount, ...bidderRequestData}) + bidderRequest: Object.assign({}, + { + bids: requestBySiteId[key], + invalidBidsCount, + prebidVersion: '$prebid.version$', + ...bidderRequestData + } + ) }; request.push(Object.assign({}, {data, ...conf})); @@ -206,21 +220,49 @@ export const adapter = { endPoint = deepAccess(validBidRequests[0], 'params.endpoint') || endPoint; } - const url = endPoint; - const method = 'POST'; - const options = {contentType: 'application/json'}; - return getRequests({url, method, options}, validBidRequests, bidderRequest); + return getRequests({ + 'url': endPoint, + 'method': 'POST', + 'options': { + 'contentType': 'application/json' + }, + 'protectedAudienceEnabled': bidderRequest.fledgeEnabled + }, validBidRequests, bidderRequest); }, - interpretResponse: function (serverResponse = {}) { + interpretResponse: function (serverResponse) { + if (!(serverResponse && serverResponse.body && (serverResponse.body.auctionConfigs || serverResponse.body.bids))) { + return []; + } + const serverResponseBody = serverResponse.body; + let bids = []; + let fledgeAuctionConfigs = null; + if (serverResponseBody.bids.length) { + bids = handleBidResponseByMediaType(serverResponseBody.bids); + } - const noBidsResponse = []; - const isInvalidResponse = !serverResponseBody || !serverResponseBody.bids; + if (serverResponseBody.auctionConfigs) { + let auctionConfigs = serverResponseBody.auctionConfigs; + let bidIdList = Object.keys(auctionConfigs); + if (bidIdList.length) { + bidIdList.forEach((bidId) => { + fledgeAuctionConfigs = [{ + 'bidId': bidId, + 'config': auctionConfigs[bidId] + }]; + }) + } + } - return isInvalidResponse - ? noBidsResponse - : handleBidResponseByMediaType(serverResponseBody.bids); + if (!fledgeAuctionConfigs) { + return bids; + } + + return { + bids, + fledgeAuctionConfigs + }; } }; diff --git a/modules/userId/eids.js b/modules/userId/eids.js index e8e83ca19a0..e5f7e3b8fb2 100644 --- a/modules/userId/eids.js +++ b/modules/userId/eids.js @@ -1,421 +1,10 @@ -import { pick, isFn, isStr, isPlainObject, deepAccess } from '../../src/utils.js'; +import {deepAccess, deepClone, isFn, isPlainObject, isStr} from '../../src/utils.js'; -// Each user-id sub-module is expected to mention respective config here -export const USER_IDS_CONFIG = { - - // key-name : {config} - - // GrowthCode - 'growthCodeId': { - getValue: function(data) { - return data.gc_id - }, - source: 'growthcode.io', - atype: 1, - getUidExt: function(data) { - const extendedData = pick(data, [ - 'h1', - 'h2', - 'h3', - ]); - if (Object.keys(extendedData).length) { - return extendedData; - } - } - }, - - // utiq - 'utiq': { - source: 'utiq.com', - atype: 1, - getValue: function (data) { - return data; - }, - }, - - // intentIqId - 'intentIqId': { - source: 'intentiq.com', - atype: 1 - }, - - // naveggId - 'naveggId': { - source: 'navegg.com', - atype: 1 - }, - - // pairId - 'pairId': { - source: 'google.com', - atype: 571187 - }, - - // justId - 'justId': { - source: 'justtag.com', - atype: 1 - }, - - // pubCommonId - 'pubcid': { - source: 'pubcid.org', - atype: 1 - }, - - // unifiedId - 'tdid': { - source: 'adserver.org', - atype: 1, - getUidExt: function() { - return { - rtiPartner: 'TDID' - }; - } - }, - - // id5Id - 'id5id': { - getValue: function(data) { - return data.uid - }, - source: 'id5-sync.com', - atype: 1, - getUidExt: function(data) { - if (data.ext) { - return data.ext; - } - } - }, - - // ftrack - 'ftrackId': { - source: 'flashtalking.com', - atype: 1, - getValue: function(data) { - let value = ''; - if (data && data.ext && data.ext.DeviceID) { - value = data.ext.DeviceID; - } - return value; - }, - getUidExt: function(data) { - return data && data.ext; - } - }, - - // parrableId - 'parrableId': { - source: 'parrable.com', - atype: 1, - getValue: function(parrableId) { - if (parrableId.eid) { - return parrableId.eid; - } - if (parrableId.ccpaOptout) { - // If the EID was suppressed due to a non consenting ccpa optout then - // we still wish to provide this as a reason to the adapters - return ''; - } - return null; - }, - getUidExt: function(parrableId) { - const extendedData = pick(parrableId, [ - 'ibaOptout', - 'ccpaOptout' - ]); - if (Object.keys(extendedData).length) { - return extendedData; - } - } - }, - - // identityLink - 'idl_env': { - source: 'liveramp.com', - atype: 3 - }, - - // liveIntentId - 'lipb': { - getValue: function(data) { - return data.lipbid; - }, - source: 'liveintent.com', - atype: 3, - getEidExt: function(data) { - if (Array.isArray(data.segments) && data.segments.length) { - return { - segments: data.segments - }; - } - } - }, - - // bidswitchId - 'bidswitch': { - source: 'bidswitch.net', - atype: 3, - getValue: function(data) { - return data.id; - } - }, - - // medianetId - 'medianet': { - source: 'media.net', - atype: 3, - getValue: function(data) { - return data.id; - } - }, - - // britepoolId - 'britepoolid': { - source: 'britepool.com', - atype: 3 - }, - - // dmdId - 'dmdId': { - source: 'hcn.health', - atype: 3 - }, - - // lotamePanoramaId - lotamePanoramaId: { - source: 'crwdcntrl.net', - atype: 1, - }, - - // criteo - 'criteoId': { - source: 'criteo.com', - atype: 1 - }, - - // merkleId - 'merkleId': { - atype: 3, - getSource: function(data) { - if (data?.ext?.ssp) { - return `${data.ext.ssp}.merkleinc.com` - } - return 'merkleinc.com' - }, - getValue: function(data) { - return data.id; - }, - getUidExt: function(data) { - if (data.keyID) { - return { - keyID: data.keyID - } - } - if (data.ext) { - return data.ext; - } - } - }, - - // NetId - 'netId': { - source: 'netid.de', - atype: 1 - }, - - // zeotapIdPlus - 'IDP': { - source: 'zeotap.com', - atype: 1 - }, - - // hadronId - 'hadronId': { - source: 'audigent.com', - atype: 1 - }, - - // quantcastId - 'quantcastId': { - source: 'quantcast.com', - atype: 1 - }, - - // IDx - 'idx': { - source: 'idx.lat', - atype: 1 - }, - - // Verizon Media ConnectID - 'connectid': { - source: 'verizonmedia.com', - atype: 3 - }, - - // Neustar Fabrick - 'fabrickId': { - source: 'neustar.biz', - atype: 1 - }, - - // MediaWallah OpenLink - 'mwOpenLinkId': { - source: 'mediawallahscript.com', - atype: 1 - }, - - 'tapadId': { - source: 'tapad.com', - atype: 1 - }, - - // Novatiq Snowflake - 'novatiq': { - getValue: function(data) { - if (data.snowflake.id === undefined) { - return data.snowflake; - } - - return data.snowflake.id; - }, - source: 'novatiq.com', - }, - - 'uid2': { - source: 'uidapi.com', - atype: 3, - getValue: function(data) { - return data.id; - } - }, - - 'euid': { - source: 'euid.eu', - atype: 3, - getValue: function(data) { - return data.id; - } - }, - - 'deepintentId': { - source: 'deepintent.com', - atype: 3 - }, - - // Admixer Id - 'admixerId': { - source: 'admixer.net', - atype: 3 - }, - - // Adtelligent Id - 'adtelligentId': { - source: 'adtelligent.com', - atype: 3 - }, - - amxId: { - source: 'amxdt.net', - atype: 1, - }, - - 'publinkId': { - source: 'epsilon.com', - atype: 3 - }, - - 'kpuid': { - source: 'kpuid.com', - atype: 3 - }, - - 'imppid': { - source: 'ppid.intimatemerger.com', - atype: 1 - }, - - 'imuid': { - source: 'intimatemerger.com', - atype: 1 - }, - - // Yahoo ConnectID - 'connectId': { - source: 'yahoo.com', - atype: 3 - }, - - // Adquery ID - 'qid': { - source: 'adquery.io', - atype: 1 - }, - - // DAC ID - 'dacId': { - source: 'impact-ad.jp', - atype: 1 - }, - - // 33across ID - '33acrossId': { - source: '33across.com', - atype: 1, - getValue: function(data) { - return data.envelope; - } - }, - - // tncId - 'tncid': { - source: 'thenewco.it', - atype: 3 - }, - - // Gravito MP ID - 'gravitompId': { - source: 'gravito.net', - atype: 1 - }, - - // czechAdId - 'czechAdId': { - source: 'czechadid.cz', - atype: 1 - }, - - // OneKey Data - 'oneKeyData': { - getValue: function(data) { - if (data && Array.isArray(data.identifiers) && data.identifiers[0]) { - return data.identifiers[0].value; - } - }, - source: 'paf', - atype: 1, - getEidExt: function(data) { - if (data && data.preferences) { - return {preferences: data.preferences}; - } - }, - getUidExt: function(data) { - if (data && Array.isArray(data.identifiers) && data.identifiers[0]) { - const id = data.identifiers[0]; - return { - version: id.version, - type: id.type, - source: id.source - }; - } - } - } -}; +export const EID_CONFIG = new Map(); // this function will create an eid object for the given UserId sub-module function createEidObject(userIdData, subModuleKey) { - const conf = USER_IDS_CONFIG[subModuleKey]; + const conf = EID_CONFIG.get(subModuleKey); if (conf && userIdData) { let eid = {}; eid.source = isFn(conf['getSource']) ? conf['getSource'](userIdData) : conf['source']; @@ -443,34 +32,23 @@ function createEidObject(userIdData, subModuleKey) { return null; } -// this function will generate eids array for all available IDs in bidRequest.userId -// this function will be called by userId module -// if any adapter does not want any particular userId to be passed then adapter can use Array.filter(e => e.source != 'tdid') export function createEidsArray(bidRequestUserId) { - let eids = []; - - for (const subModuleKey in bidRequestUserId) { - if (bidRequestUserId.hasOwnProperty(subModuleKey)) { - if (subModuleKey === 'pubProvidedId') { - eids = eids.concat(bidRequestUserId['pubProvidedId']); - } else if (Array.isArray(bidRequestUserId[subModuleKey])) { - bidRequestUserId[subModuleKey].forEach((config, index, arr) => { - const eid = createEidObject(config, subModuleKey); - - if (eid) { - eids.push(eid); - } - }) - } else { - const eid = createEidObject(bidRequestUserId[subModuleKey], subModuleKey); - if (eid) { - eids.push(eid); - } - } + const allEids = {}; + function collect(eid) { + const key = JSON.stringify([eid.source?.toLowerCase(), eid.ext]); + if (allEids.hasOwnProperty(key)) { + allEids[key].uids.push(...eid.uids); + } else { + allEids[key] = eid; } } - return eids; + Object.entries(bidRequestUserId).forEach(([name, values]) => { + values = Array.isArray(values) ? values : [values]; + const eids = name === 'pubProvidedId' ? deepClone(values) : values.map(value => createEidObject(value, name)); + eids.filter(eid => eid != null).forEach(collect); + }) + return Object.values(allEids); } /** @@ -481,11 +59,12 @@ export function buildEidPermissions(submodules) { submodules.filter(i => isPlainObject(i.idObj) && Object.keys(i.idObj).length) .forEach(i => { Object.keys(i.idObj).forEach(key => { + const eidConf = EID_CONFIG.get(key) || {}; if (deepAccess(i, 'config.bidders') && Array.isArray(i.config.bidders) && - deepAccess(USER_IDS_CONFIG, key + '.source')) { + eidConf.source) { eidPermissions.push( { - source: USER_IDS_CONFIG[key].source, + source: eidConf.source, bidders: i.config.bidders } ); diff --git a/modules/userId/eids.md b/modules/userId/eids.md index 90052cd6301..c8d27cc9d52 100644 --- a/modules/userId/eids.md +++ b/modules/userId/eids.md @@ -77,6 +77,7 @@ userIdAsEids = [ uids: [{ id: 'the-ids-object-stringified', atype: 1 + }] }, { @@ -110,15 +111,76 @@ userIdAsEids = [ source: 'bidswitch.net', uids: [{ id: 'some-random-id-value', - atype: 3 + atype: 3, + ext: { + provider: 'liveintent.com' + } }] }, + { + source: 'liveintent.indexexchange.com', + uids: [{ + id: 'some-random-id-value', + atype: 3, + ext: { + provider: 'liveintent.com' + } + }] + }, + + { + source: 'liveintent.sovrn.com'', + uids: [{ + id: 'some-random-id-value', + atype: 3, + ext: { + provider: 'liveintent.com' + } + }] + }, + + { + source: 'openx.net'', + uids: [{ + id: 'some-random-id-value', + atype: 3, + ext: { + provider: 'liveintent.com' + } + }] + }, + + { + source: 'pubmatic.com'', + uids: [{ + id: 'some-random-id-value', + atype: 3, + ext: { + provider: 'liveintent.com' + } + }] + }, + { source: 'media.net', uids: [{ id: 'some-random-id-value', - atype: 3 + atype: 3, + ext: { + provider: 'liveintent.com' + } + }] + }, + + { + source: 'rubiconproject.com', + uids: [{ + id: 'some-random-id-value', + atype: 3, + ext: { + provider: 'liveintent.com' + } }] }, @@ -255,6 +317,20 @@ userIdAsEids = [ id: 'some-random-id-value', atype: 3 }] + }, + { + source: 'euid.eu', + uids: [{ + id: 'some-random-id-value', + atype: 3 + }] + }, + { + source: 'mygaru.com', + uids: [{ + id: 'some-random-id-value', + atype: 1 + }] } ] ``` diff --git a/modules/userId/index.js b/modules/userId/index.js index 91654981c22..5a088b27319 100644 --- a/modules/userId/index.js +++ b/modules/userId/index.js @@ -133,7 +133,7 @@ import {getGlobal} from '../../src/prebidGlobal.js'; import adapterManager, {gdprDataHandler} from '../../src/adapterManager.js'; import CONSTANTS from '../../src/constants.json'; import {module, ready as hooksReady} from '../../src/hook.js'; -import {buildEidPermissions, createEidsArray, USER_IDS_CONFIG} from './eids.js'; +import {buildEidPermissions, createEidsArray, EID_CONFIG} from './eids.js'; import { getCoreStorageManager, getStorageManager, @@ -141,7 +141,6 @@ import { STORAGE_TYPE_LOCALSTORAGE } from '../../src/storageManager.js'; import { - cyrb53Hash, deepAccess, deepSetValue, delayExecution, @@ -162,7 +161,7 @@ import {defer, GreedyPromise} from '../../src/utils/promise.js'; import {registerOrtbProcessor, REQUEST} from '../../src/pbjsORTB.js'; import {newMetrics, timedAuctionHook, useMetrics} from '../../src/utils/perfMetrics.js'; import {findRootDomain} from '../../src/fpd/rootDomain.js'; -import {GDPR_GVLIDS} from '../../src/consentHandler.js'; +import {allConsent, GDPR_GVLIDS} from '../../src/consentHandler.js'; import {MODULE_TYPE_UID} from '../../src/activities/modules.js'; import {isActivityAllowed} from '../../src/activities/rules.js'; import {ACTIVITY_ENRICH_EIDS} from '../../src/activities/activities.js'; @@ -173,10 +172,6 @@ const COOKIE = STORAGE_TYPE_COOKIES; const LOCAL_STORAGE = STORAGE_TYPE_LOCALSTORAGE; const DEFAULT_SYNC_DELAY = 500; const NO_AUCTION_DELAY = 0; -const CONSENT_DATA_COOKIE_STORAGE_CONFIG = { - name: '_pbjs_userid_consent_data', - expires: 30 // 30 days expiration, which should match how often consent is refreshed by CMPs -}; export const PBJS_USER_ID_OPTOUT_NAME = '_pbjs_id_optout'; export const coreStorage = getCoreStorageManager('userId'); export const dep = { @@ -195,6 +190,9 @@ let initializedSubmodules; /** @type {SubmoduleConfig[]} */ let configRegistry = []; +/** @type {Object} */ +let idPriority = {}; + /** @type {Submodule[]} */ let submoduleRegistry = []; @@ -229,6 +227,7 @@ function submoduleMetrics(moduleName) { /** @param {Submodule[]} submodules */ export function setSubmoduleRegistry(submodules) { submoduleRegistry = submodules; + updateEIDConfig(submodules); } function cookieSetter(submodule, storageMgr) { @@ -257,11 +256,13 @@ export function setStoredValue(submodule, value) { if (storage.type === COOKIE) { const setCookie = cookieSetter(submodule); setCookie(null, valueStr, expiresStr); + setCookie('_cst', getConsentHash(), expiresStr); if (typeof storage.refreshInSeconds === 'number') { setCookie('_last', new Date().toUTCString(), expiresStr); } } else if (storage.type === LOCAL_STORAGE) { mgr.setDataInLocalStorage(`${storage.name}_exp`, expiresStr); + mgr.setDataInLocalStorage(`${storage.name}_cst`, getConsentHash()); mgr.setDataInLocalStorage(storage.name, encodeURIComponent(valueStr)); if (typeof storage.refreshInSeconds === 'number') { mgr.setDataInLocalStorage(`${storage.name}_last`, new Date().toUTCString()); @@ -279,11 +280,11 @@ export function deleteStoredValue(submodule) { const setCookie = cookieSetter(submodule, coreStorage); const expiry = (new Date(Date.now() - 1000 * 60 * 60 * 24)).toUTCString(); deleter = (suffix) => setCookie(suffix, '', expiry) - suffixes = ['', '_last']; + suffixes = ['', '_last', '_cst']; break; case LOCAL_STORAGE: deleter = (suffix) => coreStorage.removeDataFromLocalStorage(submodule.config.storage.name + suffix) - suffixes = ['', '_last', '_exp']; + suffixes = ['', '_last', '_exp', '_cst']; break; } if (deleter) { @@ -338,75 +339,11 @@ function getStoredValue(submodule, key = undefined) { return storedValue; } -/** - * makes an object that can be stored with only the keys we need to check. - * excluding the vendorConsents object since the consentString is enough to know - * if consent has changed without needing to have all the details in an object - * @param consentData - * @returns {{apiVersion: number, gdprApplies: boolean, consentString: string}} - */ -function makeStoredConsentDataHash(consentData) { - const storedConsentData = { - consentString: '', - gdprApplies: false, - apiVersion: 0 - }; - - if (consentData) { - storedConsentData.consentString = consentData.consentString; - storedConsentData.gdprApplies = consentData.gdprApplies; - storedConsentData.apiVersion = consentData.apiVersion; - } - - return cyrb53Hash(JSON.stringify(storedConsentData)); -} - -/** - * puts the current consent data into cookie storage - * @param consentData - */ -export function setStoredConsentData(consentData) { - try { - const expiresStr = (new Date(Date.now() + (CONSENT_DATA_COOKIE_STORAGE_CONFIG.expires * (60 * 60 * 24 * 1000)))).toUTCString(); - coreStorage.setCookie(CONSENT_DATA_COOKIE_STORAGE_CONFIG.name, makeStoredConsentDataHash(consentData), expiresStr, 'Lax'); - } catch (error) { - logError(error); - } -} - -/** - * get the stored consent data from local storage, if any - * @returns {string} - */ -function getStoredConsentData() { - try { - return coreStorage.getCookie(CONSENT_DATA_COOKIE_STORAGE_CONFIG.name); - } catch (e) { - logError(e); - } -} - -/** - * test if the consent object stored locally matches the current consent data. if they - * don't match or there is nothing stored locally, it means a refresh of the user id - * submodule is needed - * @param storedConsentData - * @param consentData - * @returns {boolean} - */ -function storedConsentDataMatchesConsentData(storedConsentData, consentData) { - return ( - typeof storedConsentData !== 'undefined' && - storedConsentData !== null && - storedConsentData === makeStoredConsentDataHash(consentData) - ); -} - /** * @param {SubmoduleContainer[]} submodules * @param {function} cb - callback for after processing is done. */ -function processSubmoduleCallbacks(submodules, cb) { +function processSubmoduleCallbacks(submodules, cb, allModules) { cb = uidMetrics().fork().startTiming('userId.callbacks.total').stopBefore(cb); const done = delayExecution(() => { clearTimeout(timeoutID); @@ -422,7 +359,7 @@ function processSubmoduleCallbacks(submodules, cb) { } // cache decoded value (this is copied to every adUnit bid) submodule.idObj = submodule.submodule.decode(idObj, submodule.config); - updatePPID(submodule.idObj); + updatePPID(getCombinedSubmoduleIds(allModules)); } else { logInfo(`${MODULE_NAME}: ${submodule.submodule.name} - request id responded with an empty value`); } @@ -447,14 +384,7 @@ function getCombinedSubmoduleIds(submodules) { if (!Array.isArray(submodules) || !submodules.length) { return {}; } - const combinedSubmoduleIds = submodules.filter(i => isPlainObject(i.idObj) && Object.keys(i.idObj).length).reduce((carry, i) => { - Object.keys(i.idObj).forEach(key => { - carry[key] = i.idObj[key]; - }); - return carry; - }, {}); - - return combinedSubmoduleIds; + return getPrioritizedCombinedSubmoduleIds(submodules) } /** @@ -466,9 +396,14 @@ function getSubmoduleId(submodules, sourceName) { if (!Array.isArray(submodules) || !submodules.length) { return {}; } - const submodule = submodules.filter(sub => isPlainObject(sub.idObj) && - Object.keys(sub.idObj).length && USER_IDS_CONFIG[Object.keys(sub.idObj)[0]]?.source === sourceName); - return !isEmpty(submodule) ? submodule[0].idObj : []; + + const prioritisedIds = getPrioritizedCombinedSubmoduleIds(submodules); + const eligibleIdName = Object.keys(prioritisedIds).find(idName => { + const config = EID_CONFIG.get(idName); + return config?.source === sourceName || (isFn(config?.getSource) && config.getSource() === sourceName); + }); + + return eligibleIdName ? {[eligibleIdName]: prioritisedIds[eligibleIdName]} : []; } /** @@ -480,15 +415,39 @@ function getCombinedSubmoduleIdsForBidder(submodules, bidder) { if (!Array.isArray(submodules) || !submodules.length || !bidder) { return {}; } - return submodules + const eligibleSubmodules = submodules .filter(i => !i.config.bidders || !isArray(i.config.bidders) || includes(i.config.bidders, bidder)) - .filter(i => isPlainObject(i.idObj) && Object.keys(i.idObj).length) - .reduce((carry, i) => { - Object.keys(i.idObj).forEach(key => { - carry[key] = i.idObj[key]; - }); - return carry; - }, {}); + + return getPrioritizedCombinedSubmoduleIds(eligibleSubmodules); +} + +function collectByPriority(submodules, getIds, getName) { + return Object.fromEntries(Object.entries(submodules.reduce((carry, submod) => { + const ids = getIds(submod); + ids && Object.keys(ids).forEach(key => { + const maybeCurrentIdPriority = idPriority[key]?.indexOf(getName(submod)); + const currentIdPriority = isNumber(maybeCurrentIdPriority) ? maybeCurrentIdPriority : -1; + const currentIdState = {priority: currentIdPriority, value: ids[key]}; + if (carry[key]) { + const winnerIdState = currentIdState.priority > carry[key].priority ? currentIdState : carry[key]; + carry[key] = winnerIdState; + } else { + carry[key] = currentIdState; + } + }); + return carry; + }, {})).map(([k, v]) => [k, v.value])); +} + +/** + * @param {SubmoduleContainer[]} submodules + */ +function getPrioritizedCombinedSubmoduleIds(submodules) { + return collectByPriority( + submodules.filter(i => isPlainObject(i.idObj) && Object.keys(i.idObj).length), + (submod) => submod.idObj, + (submod) => submod.submodule.name + ) } /** @@ -547,21 +506,21 @@ function idSystemInitializer({delay = GreedyPromise.timeout} = {}) { } } - function timeGdpr() { - return gdprDataHandler.promise.finally(initMetrics.startTiming('userId.init.gdpr')); + function timeConsent() { + return allConsent.promise.finally(initMetrics.startTiming('userId.init.consent')) } let done = cancelAndTry( GreedyPromise.all([hooksReady, startInit.promise]) - .then(timeGdpr) - .then(checkRefs((consentData) => { - initSubmodules(initModules, allModules, consentData); + .then(timeConsent) + .then(checkRefs(() => { + initSubmodules(initModules, allModules); })) .then(() => startCallbacks.promise.finally(initMetrics.startTiming('userId.callbacks.pending'))) .then(checkRefs(() => { const modWithCb = initModules.filter(item => isFn(item.callback)); if (modWithCb.length) { - return new GreedyPromise((resolve) => processSubmoduleCallbacks(modWithCb, resolve)); + return new GreedyPromise((resolve) => processSubmoduleCallbacks(modWithCb, resolve, initModules)); } })) ); @@ -589,18 +548,17 @@ function idSystemInitializer({delay = GreedyPromise.timeout} = {}) { done = cancelAndTry( done .catch(() => null) - .then(timeGdpr) // fetch again in case a refresh was forced before this was resolved - .then(checkRefs((consentData) => { + .then(timeConsent) // fetch again in case a refresh was forced before this was resolved + .then(checkRefs(() => { const cbModules = initSubmodules( initModules, allModules.filter((sm) => submoduleNames == null || submoduleNames.includes(sm.submodule.name)), - consentData, true ).filter((sm) => { return sm.callback != null; }); if (cbModules.length) { - return new GreedyPromise((resolve) => processSubmoduleCallbacks(cbModules, resolve)); + return new GreedyPromise((resolve) => processSubmoduleCallbacks(cbModules, resolve, initModules)); } })) ); @@ -710,20 +668,20 @@ function encryptSignals(signals, version = 1) { } /** -* This function will be exposed in the global-name-space so that publisher can register the signals-ESP. -*/ + * This function will be exposed in the global-name-space so that publisher can register the signals-ESP. + */ function registerSignalSources() { if (!isGptPubadsDefined()) { return; } - window.googletag.encryptedSignalProviders = window.googletag.encryptedSignalProviders || []; + window.googletag.secureSignalProviders = window.googletag.secureSignalProviders || []; const encryptedSignalSources = config.getConfig('userSync.encryptedSignalSources'); if (encryptedSignalSources) { const registerDelay = encryptedSignalSources.registerDelay || 0; setTimeout(() => { encryptedSignalSources['sources'] && encryptedSignalSources['sources'].forEach(({ source, encrypt, customFunc }) => { source.forEach((src) => { - window.googletag.encryptedSignalProviders.push({ + window.googletag.secureSignalProviders.push({ id: src, collectorFunction: () => getEncryptedEidsForSource(src, encrypt, customFunc) }); @@ -783,7 +741,27 @@ function getUserIdsAsync() { ); } -function populateSubmoduleId(submodule, consentData, storedConsentData, forceRefresh) { +export function getConsentHash() { + // transform decimal string into base64 to save some space on cookies + let hash = Number(allConsent.hash); + const bytes = []; + while (hash > 0) { + bytes.push(String.fromCharCode(hash & 255)); + hash = hash >>> 8; + } + return btoa(bytes.join()); +} + +function consentChanged(submodule) { + const storedConsent = getStoredValue(submodule, 'cst'); + return !storedConsent || storedConsent !== getConsentHash(); +} + +function populateSubmoduleId(submodule, forceRefresh, allSubmodules) { + // TODO: the ID submodule API only takes GDPR consent; it should be updated now that GDPR + // is only a tiny fraction of a vast consent universe + const gdprConsent = gdprDataHandler.getConsentData(); + // There are two submodule configuration types to handle: storage or value // 1. storage: retrieve user id data from cookie/html storage or with the submodule's getId method // 2. value: pass directly to bids @@ -797,12 +775,12 @@ function populateSubmoduleId(submodule, consentData, storedConsentData, forceRef refreshNeeded = storedDate && (Date.now() - storedDate.getTime() > submodule.config.storage.refreshInSeconds * 1000); } - if (!storedId || refreshNeeded || forceRefresh || !storedConsentDataMatchesConsentData(storedConsentData, consentData)) { + if (!storedId || refreshNeeded || forceRefresh || consentChanged(submodule)) { // No id previously saved, or a refresh is needed, or consent has changed. Request a new id from the submodule. - response = submodule.submodule.getId(submodule.config, consentData, storedId); + response = submodule.submodule.getId(submodule.config, gdprConsent, storedId); } else if (typeof submodule.submodule.extendId === 'function') { // If the id exists already, give submodule a chance to decide additional actions that need to be taken - response = submodule.submodule.extendId(submodule.config, consentData, storedId); + response = submodule.submodule.extendId(submodule.config, gdprConsent, storedId); } if (isPlainObject(response)) { @@ -826,13 +804,13 @@ function populateSubmoduleId(submodule, consentData, storedConsentData, forceRef // cache decoded value (this is copied to every adUnit bid) submodule.idObj = submodule.config.value; } else { - const response = submodule.submodule.getId(submodule.config, consentData, undefined); + const response = submodule.submodule.getId(submodule.config, gdprConsent, undefined); if (isPlainObject(response)) { if (typeof response.callback === 'function') { submodule.callback = response.callback; } if (response.id) { submodule.idObj = submodule.submodule.decode(response.id, submodule.config); } } } - updatePPID(submodule.idObj); + updatePPID(getCombinedSubmoduleIds(allSubmodules)); } function updatePPID(userIds = getUserIds()) { @@ -852,7 +830,7 @@ function updatePPID(userIds = getUserIds()) { } } -function initSubmodules(dest, submodules, consentData, forceRefresh = false) { +function initSubmodules(dest, submodules, forceRefresh = false) { return uidMetrics().fork().measureTime('userId.init.modules', function () { if (!submodules.length) return []; // to simplify log messages from here on @@ -872,14 +850,10 @@ function initSubmodules(dest, submodules, consentData, forceRefresh = false) { return []; } - // we always want the latest consentData stored, even if we don't execute any submodules - const storedConsentData = getStoredConsentData(); - setStoredConsentData(consentData); - const initialized = submodules.reduce((carry, submodule) => { return submoduleMetrics(submodule.submodule.name).measureTime('init', () => { try { - populateSubmoduleId(submodule, consentData, storedConsentData, forceRefresh); + populateSubmoduleId(submodule, forceRefresh, submodules); carry.push(submodule); } catch (e) { logError(`Error in userID module '${submodule.submodule.name}':`, e); @@ -912,14 +886,12 @@ function updateInitializedSubmodules(dest, submodule) { /** * list of submodule configurations with valid 'storage' or 'value' obj definitions - * * storage config: contains values for storing/retrieving User ID data in browser storage - * * value config: object properties that are copied to bids (without saving to storage) + * storage config: contains values for storing/retrieving User ID data in browser storage + * value config: object properties that are copied to bids (without saving to storage) * @param {SubmoduleConfig[]} configRegistry - * @param {Submodule[]} submoduleRegistry - * @param {string[]} activeStorageTypes * @returns {SubmoduleConfig[]} */ -function getValidSubmoduleConfigs(configRegistry, submoduleRegistry) { +function getValidSubmoduleConfigs(configRegistry) { if (!Array.isArray(configRegistry)) { return []; } @@ -970,11 +942,21 @@ function canUseStorage(submodule) { return false; } +function updateEIDConfig(submodules) { + EID_CONFIG.clear(); + Object.entries(collectByPriority( + submodules, + (mod) => mod.eids, + (mod) => mod.name + )).forEach(([id, conf]) => EID_CONFIG.set(id, conf)); +} + /** * update submodules by validating against existing configs and storage types */ function updateSubmodules() { - const configs = getValidSubmoduleConfigs(configRegistry, submoduleRegistry); + updateEIDConfig(submoduleRegistry); + const configs = getValidSubmoduleConfigs(configRegistry); if (!configs.length) { return; } @@ -1010,6 +992,25 @@ function updateSubmodules() { } } +/** + * This function will update the idPriority according to the provided configuration + * @param {Object} idPriorityConfig + * @param {SubmoduleContainer[]} submodules + */ +function updateIdPriority(idPriorityConfig, submodules) { + if (idPriorityConfig) { + const result = {}; + const aliasToName = new Map(submodules.map(s => s.submodule.aliasName ? [s.submodule.aliasName, s.submodule.name] : [])); + Object.keys(idPriorityConfig).forEach(key => { + const priority = isArray(idPriorityConfig[key]) ? [...idPriorityConfig[key]].reverse() : [] + result[key] = priority.map(s => aliasToName.has(s) ? aliasToName.get(s) : s); + }); + idPriority = result; + } else { + idPriority = {}; + } +} + export function requestDataDeletion(next, ...args) { logInfo('UserID: received data deletion request; deleting all stored IDs...') submodules.forEach(submodule => { @@ -1069,13 +1070,16 @@ export function init(config, {delay = GreedyPromise.timeout} = {}) { configListener = config.getConfig('userSync', conf => { // Note: support for 'usersync' was dropped as part of Prebid.js 4.0 const userSync = conf.userSync; - ppidSource = userSync.ppid; - if (userSync && userSync.userIds) { - configRegistry = userSync.userIds; - syncDelay = isNumber(userSync.syncDelay) ? userSync.syncDelay : DEFAULT_SYNC_DELAY; - auctionDelay = isNumber(userSync.auctionDelay) ? userSync.auctionDelay : NO_AUCTION_DELAY; - updateSubmodules(); - initIdSystem({ready: true}); + if (userSync) { + ppidSource = userSync.ppid; + if (userSync.userIds) { + configRegistry = userSync.userIds; + syncDelay = isNumber(userSync.syncDelay) ? userSync.syncDelay : DEFAULT_SYNC_DELAY; + auctionDelay = isNumber(userSync.auctionDelay) ? userSync.auctionDelay : NO_AUCTION_DELAY; + updateSubmodules(); + updateIdPriority(userSync.idPriority, submodules); + initIdSystem({ready: true}); + } } }); diff --git a/modules/userId/userId.md b/modules/userId/userId.md index 15f609ddced..7a01e128814 100644 --- a/modules/userId/userId.md +++ b/modules/userId/userId.md @@ -5,6 +5,9 @@ Example showing `cookie` storage for user id data for each of the submodules ``` pbjs.setConfig({ userSync: { + idPriority: { + uid2: ['uid2', 'liveIntentId'] + } userIds: [{ name: "33acrossId", storage: { @@ -155,6 +158,9 @@ pbjs.setConfig({ }, { name: "gravitompId" + }, + { + name: "mygaruId" } ], syncDelay: 5000, diff --git a/modules/utiqSystem.js b/modules/utiqSystem.js index 441bba5660f..473dc5854a9 100644 --- a/modules/utiqSystem.js +++ b/modules/utiqSystem.js @@ -124,6 +124,15 @@ export const utiqSubmodule = { return { callback: result }; } }, + eids: { + 'utiq': { + source: 'utiq.com', + atype: 1, + getValue: function (data) { + return data; + }, + }, + } }; submodule('userId', utiqSubmodule); diff --git a/modules/vdoaiBidAdapter.js b/modules/vdoaiBidAdapter.js index fa1e2898a4a..ada843a6e45 100644 --- a/modules/vdoaiBidAdapter.js +++ b/modules/vdoaiBidAdapter.js @@ -1,6 +1,12 @@ -import {getAdUnitSizes} from '../src/utils.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import {BANNER, VIDEO} from '../src/mediaTypes.js'; +import {getAdUnitSizes} from '../libraries/sizeUtils/sizeUtils.js'; + +/** + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid + * @typedef {import('../src/adapters/bidderFactory.js').ServerResponse} ServerResponse + */ const BIDDER_CODE = 'vdoai'; const ENDPOINT_URL = 'https://prebid.vdo.ai/auction'; diff --git a/modules/ventesBidAdapter.js b/modules/ventesBidAdapter.js index 73ae0a7e5f1..78c580c4116 100644 --- a/modules/ventesBidAdapter.js +++ b/modules/ventesBidAdapter.js @@ -1,8 +1,9 @@ import {BANNER, NATIVE, VIDEO} from '../src/mediaTypes.js'; -import {convertCamelToUnderscore, isArray, isNumber, isPlainObject, isStr, replaceAuctionPrice} from '../src/utils.js'; +import {isArray, isNumber, isPlainObject, isStr, replaceAuctionPrice} from '../src/utils.js'; import {find} from '../src/polyfill.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import { convertOrtbRequestToProprietaryNative } from '../src/native.js'; +import {convertCamelToUnderscore} from '../libraries/appnexusUtils/anUtils.js'; const BID_METHOD = 'POST'; const BIDDER_URL = 'https://ad.ventesavenues.in/va/ad'; diff --git a/modules/verizonMediaIdSystem.js b/modules/verizonMediaIdSystem.js index 27577ad0de4..26fa89cfe03 100644 --- a/modules/verizonMediaIdSystem.js +++ b/modules/verizonMediaIdSystem.js @@ -10,6 +10,13 @@ import {submodule} from '../src/hook.js'; import {formatQS, logError} from '../src/utils.js'; import {includes} from '../src/polyfill.js'; +/** + * @typedef {import('../modules/userId/index.js').Submodule} Submodule + * @typedef {import('../modules/userId/index.js').SubmoduleConfig} SubmoduleConfig + * @typedef {import('../modules/userId/index.js').ConsentData} ConsentData + * @typedef {import('../modules/userId/index.js').IdResponse} IdResponse + */ + const MODULE_NAME = 'verizonMediaId'; const VENDOR_ID = 25; const PLACEHOLDER = '__PIXEL_ID__'; @@ -99,6 +106,12 @@ export const verizonMediaIdSubmodule = { */ getAjaxFn() { return ajax; + }, + eids: { + 'connectid': { + source: 'verizonmedia.com', + atype: 3 + }, } }; diff --git a/modules/viantOrtbBidAdapter.js b/modules/viantOrtbBidAdapter.js new file mode 100644 index 00000000000..0f7953a192a --- /dev/null +++ b/modules/viantOrtbBidAdapter.js @@ -0,0 +1,113 @@ +import {registerBidder} from '../src/adapters/bidderFactory.js'; +import {BANNER, NATIVE, VIDEO} from '../src/mediaTypes.js'; +import * as utils from '../src/utils.js'; +import {ortbConverter} from '../libraries/ortbConverter/converter.js' +import {deepAccess, getBidIdParameter, logError} from '../src/utils.js'; + +const BIDDER_CODE = 'viant'; +const ENDPOINT = 'https://bidders-us-east-1.adelphic.net/d/rtb/v25/prebid/bidder' + +const DEFAULT_BID_TTL = 300; +const DEFAULT_CURRENCY = 'USD'; +const DEFAULT_NET_REVENUE = true; + +export const spec = { + code: BIDDER_CODE, + supportedMediaTypes: [BANNER, VIDEO, NATIVE], + + isBidRequestValid: function (bid) { + if (bid && typeof bid.params !== 'object') { + logError(BIDDER_CODE + ': params is not defined or is incorrect in the bidder settings.'); + return false; + } + if (!getBidIdParameter('publisherId', bid.params)) { + logError(BIDDER_CODE + ': publisherId is not present in bidder params.'); + return false; + } + const mediaTypesBanner = deepAccess(bid, 'mediaTypes.banner'); + const mediaTypesVideo = deepAccess(bid, 'mediaTypes.video'); + const mediaTypesNative = deepAccess(bid, 'mediaTypes.native'); + if (!mediaTypesBanner && !mediaTypesVideo && !mediaTypesNative) { + utils.logWarn(BIDDER_CODE + ': one of mediaTypes.banner or mediaTypes.video or mediaTypes.native must be passed'); + return false; + } + return true; + }, + + buildRequests, + + interpretResponse(response, request) { + if (!response.body) { + response.body = {nbr: 0}; + } + const bids = converter.fromORTB({request: request.data, response: response.body}).bids; + return bids; + }, + + /** + * Register bidder specific code, which will execute if a bid from this bidder won the auction + * @param {Bid} bid The bid that won the auction + */ + onBidWon: function (bid) { + if (bid.burl) { + utils.triggerPixel(bid.burl); + } else if (bid.nurl) { + utils.triggerPixel(bid.nurl); + } + } +} + +function buildRequests(bids, bidderRequest) { + let videoBids = bids.filter(bid => isVideoBid(bid)); + let bannerBids = bids.filter(bid => isBannerBid(bid)); + let nativeBids = bids.filter(bid => isNativeBid(bid)); + let requests = bannerBids.length ? [createRequest(bannerBids, bidderRequest, BANNER)] : []; + videoBids.forEach(bid => { + requests.push(createRequest([bid], bidderRequest, VIDEO)); + }); + nativeBids.forEach(bid => { + requests.push(createRequest([bid], bidderRequest, NATIVE)); + }); + return requests; +} + +function createRequest(bidRequests, bidderRequest, mediaType) { + const data = converter.toORTB({bidRequests, bidderRequest, context: {mediaType}}); + if (bidderRequest.gdprConsent && typeof bidderRequest.gdprConsent.gdprApplies === 'boolean') { + if (!data.regs) data.regs = {}; + if (!data.regs.ext) data.regs.ext = {}; + data.regs.ext.gdpr = bidderRequest.gdprConsent.gdprApplies ? 1 : 0; + } + if (bidderRequest.uspConsent) { + if (!data.regs) data.regs = {}; + if (!data.regs.ext) data.regs.ext = {}; + data.regs.ext.us_privacy = bidderRequest.uspConsent; + } + return { + method: 'POST', + url: ENDPOINT, + data: data + } +} + +function isVideoBid(bid) { + return deepAccess(bid, 'mediaTypes.video'); +} + +function isBannerBid(bid) { + return deepAccess(bid, 'mediaTypes.banner'); +} + +function isNativeBid(bid) { + return deepAccess(bid, 'mediaTypes.native'); +} + +const converter = ortbConverter({ + context: { + netRevenue: DEFAULT_NET_REVENUE, + ttl: DEFAULT_BID_TTL, + currency: DEFAULT_CURRENCY + } +}); + +registerBidder(spec); diff --git a/modules/viantOrtbBidAdapter.md b/modules/viantOrtbBidAdapter.md new file mode 100644 index 00000000000..def93722b7b --- /dev/null +++ b/modules/viantOrtbBidAdapter.md @@ -0,0 +1,54 @@ +# Overview + +Module Name: VIANT Bidder Adapter +Module Type: Bidder Adapter +Maintainer: Marketplace@adelphic.com + +# Description + +An adapter to get a bid from VIANT DSP. + +# Test Parameters +```javascript + var adUnits = [ // Banner adUnit with only required parameters + { + code: 'test-div-minimal', + mediaTypes: { + banner: { + sizes: [[728, 90]] + } + }, + bids: [ + { + bidder: 'viant', + params: { + supplySourceId: 'supplier', + publisherId: '464' + } + } + ] + }, + { + code: 'test-div-minimal-video', + mediaTypes: { + video: { + playerSize: [640, 480], + context: 'outstream' + } + }, + bids: [ + { + bidder: 'viant', + params: { + supplySourceId: 'supplier', + publisherId: '464' // required + } + } + ] + } + ]; +``` + +Where: + +* placementId - Placement ID of the ad unit (required) diff --git a/modules/vibrantmediaBidAdapter.js b/modules/vibrantmediaBidAdapter.js index 7368967ff3f..8809aae32bd 100644 --- a/modules/vibrantmediaBidAdapter.js +++ b/modules/vibrantmediaBidAdapter.js @@ -12,6 +12,13 @@ import {registerBidder} from '../src/adapters/bidderFactory.js'; import {BANNER, NATIVE, VIDEO} from '../src/mediaTypes.js'; import {OUTSTREAM} from '../src/video.js'; +/** + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid + * @typedef {import('../src/adapters/bidderFactory.js').ServerResponse} ServerResponse + * @typedef {import('../src/adapters/bidderFactory.js').BidderSpec} BidderSpec + */ + const BIDDER_CODE = 'vibrantmedia'; const VIBRANT_MEDIA_PREBID_URL = 'https://prebid.intellitxt.com/prebid'; const VALID_PIXEL_URL_REGEX = /^https?:\/\/[a-zA-Z0-9-]+(\.[a-zA-Z0-9-]+)+([/?].*)?$/; diff --git a/modules/vidazooBidAdapter.js b/modules/vidazooBidAdapter.js index 8341eb332d2..b5323181c6c 100644 --- a/modules/vidazooBidAdapter.js +++ b/modules/vidazooBidAdapter.js @@ -1,9 +1,10 @@ -import {_each, deepAccess, parseSizesInput, parseUrl, uniques, isFn} from '../src/utils.js'; +import {_each, deepAccess, isFn, parseSizesInput, parseUrl, uniques, isArray} from '../src/utils.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import {BANNER, VIDEO} from '../src/mediaTypes.js'; import {getStorageManager} from '../src/storageManager.js'; import {bidderSettings} from '../src/bidderSettings.js'; import {config} from '../src/config.js'; +import {chunk} from '../libraries/chunk/chunk.js'; const GVLID = 744; const DEFAULT_SUB_DOMAIN = 'prebid'; @@ -48,7 +49,7 @@ function isBidRequestValid(bid) { return !!(extractCID(params) && extractPID(params)); } -function buildRequest(bid, topWindowUrl, sizes, bidderRequest, bidderTimeout) { +function buildRequestData(bid, topWindowUrl, sizes, bidderRequest, bidderTimeout) { const { params, bidId, @@ -56,7 +57,6 @@ function buildRequest(bid, topWindowUrl, sizes, bidderRequest, bidderTimeout) { adUnitCode, schain, mediaTypes, - auctionId, ortb2Imp, bidderRequestId, bidRequestsCount, @@ -69,9 +69,7 @@ function buildRequest(bid, topWindowUrl, sizes, bidderRequest, bidderTimeout) { const dealId = getNextDealId(hashUrl); const uniqueDealId = getUniqueDealId(hashUrl); const sId = getVidazooSessionId(); - const cId = extractCID(params); const pId = extractPID(params); - const subDomain = extractSubDomain(params); const ptrace = getCacheOpt(); const isStorageAllowed = bidderSettings.get(BIDDER_CODE, 'storageAllowed'); @@ -114,8 +112,6 @@ function buildRequest(bid, topWindowUrl, sizes, bidderRequest, bidderTimeout) { gpid: gpid, cat: cat, pagecat: pagecat, - // TODO: fix auctionId leak: https://github.com/prebid/Prebid.js/issues/9781 - auctionId: auctionId, transactionId: ortb2Imp?.ext?.tid, bidderRequestId: bidderRequestId, bidRequestsCount: bidRequestsCount, @@ -153,17 +149,46 @@ function buildRequest(bid, topWindowUrl, sizes, bidderRequest, bidderTimeout) { data.gppSid = bidderRequest.ortb2.regs.gpp_sid; } + _each(ext, (value, key) => { + data['ext.' + key] = value; + }); + + return data; +} + +function buildRequest(bid, topWindowUrl, sizes, bidderRequest, bidderTimeout) { + const {params} = bid; + const cId = extractCID(params); + const subDomain = extractSubDomain(params); + const data = buildRequestData(bid, topWindowUrl, sizes, bidderRequest, bidderTimeout); const dto = { method: 'POST', url: `${createDomain(subDomain)}/prebid/multi/${cId}`, data: data }; + return dto; +} - _each(ext, (value, key) => { - dto.data['ext.' + key] = value; +function buildSingleRequest(bidRequests, bidderRequest, topWindowUrl, bidderTimeout) { + const {params} = bidRequests[0]; + const cId = extractCID(params); + const subDomain = extractSubDomain(params); + const data = bidRequests.map(bid => { + const sizes = parseSizesInput(bid.sizes); + return buildRequestData(bid, topWindowUrl, sizes, bidderRequest, bidderTimeout) + }); + const chunkSize = Math.min(20, config.getConfig('vidazoo.chunkSize') || 10); + + const chunkedData = chunk(data, chunkSize); + return chunkedData.map(chunk => { + return { + method: 'POST', + url: `${createDomain(subDomain)}/prebid/multi/${cId}`, + data: { + bids: chunk + } + }; }); - - return dto; } function appendUserIdsToRequestPayload(payloadRef, userIds) { @@ -193,12 +218,34 @@ function buildRequests(validBidRequests, bidderRequest) { // TODO: does the fallback make sense here? const topWindowUrl = bidderRequest.refererInfo.page || bidderRequest.refererInfo.topmostLocation; const bidderTimeout = config.getConfig('bidderTimeout'); + + const singleRequestMode = config.getConfig('vidazoo.singleRequest'); + const requests = []; - validBidRequests.forEach(validBidRequest => { - const sizes = parseSizesInput(validBidRequest.sizes); - const request = buildRequest(validBidRequest, topWindowUrl, sizes, bidderRequest, bidderTimeout); - requests.push(request); - }); + + if (singleRequestMode) { + // banner bids are sent as a single request + const bannerBidRequests = validBidRequests.filter(bid => isArray(bid.mediaTypes) ? bid.mediaTypes.includes(BANNER) : bid.mediaTypes[BANNER] !== undefined); + if (bannerBidRequests.length > 0) { + const singleRequests = buildSingleRequest(bannerBidRequests, bidderRequest, topWindowUrl, bidderTimeout); + requests.push(...singleRequests); + } + + // video bids are sent as a single request for each bid + + const videoBidRequests = validBidRequests.filter(bid => bid.mediaTypes[VIDEO] !== undefined); + videoBidRequests.forEach(validBidRequest => { + const sizes = parseSizesInput(validBidRequest.sizes); + const request = buildRequest(validBidRequest, topWindowUrl, sizes, bidderRequest, bidderTimeout); + requests.push(request); + }); + } else { + validBidRequests.forEach(validBidRequest => { + const sizes = parseSizesInput(validBidRequest.sizes); + const request = buildRequest(validBidRequest, topWindowUrl, sizes, bidderRequest, bidderTimeout); + requests.push(request); + }); + } return requests; } @@ -206,13 +253,15 @@ function interpretResponse(serverResponse, request) { if (!serverResponse || !serverResponse.body) { return []; } - const {bidId} = request.data; + + const singleRequestMode = config.getConfig('vidazoo.singleRequest'); + const reqBidId = deepAccess(request, 'data.bidId'); const {results} = serverResponse.body; let output = []; try { - results.forEach(result => { + results.forEach((result, i) => { const { creativeId, ad, @@ -221,6 +270,7 @@ function interpretResponse(serverResponse, request) { width, height, currency, + bidId, advertiserDomains, metaData, mediaType = BANNER @@ -230,7 +280,7 @@ function interpretResponse(serverResponse, request) { } const response = { - requestId: bidId, + requestId: (singleRequestMode && bidId) ? bidId : reqBidId, cpm: price, width: width, height: height, @@ -264,19 +314,27 @@ function interpretResponse(serverResponse, request) { } output.push(response); }); + return output; } catch (e) { return []; } } -function getUserSyncs(syncOptions, responses, gdprConsent = {}, uspConsent = '') { +function getUserSyncs(syncOptions, responses, gdprConsent = {}, uspConsent = '', gppConsent = {}) { let syncs = []; const {iframeEnabled, pixelEnabled} = syncOptions; const {gdprApplies, consentString = ''} = gdprConsent; + const {gppString, applicableSections} = gppConsent; const cidArr = responses.filter(resp => deepAccess(resp, 'body.cid')).map(resp => resp.body.cid).filter(uniques); - const params = `?cid=${encodeURIComponent(cidArr.join(','))}&gdpr=${gdprApplies ? 1 : 0}&gdpr_consent=${encodeURIComponent(consentString || '')}&us_privacy=${encodeURIComponent(uspConsent || '')}` + let params = `?cid=${encodeURIComponent(cidArr.join(','))}&gdpr=${gdprApplies ? 1 : 0}&gdpr_consent=${encodeURIComponent(consentString || '')}&us_privacy=${encodeURIComponent(uspConsent || '')}`; + + if (gppString && applicableSections?.length) { + params += '&gpp=' + encodeURIComponent(gppString); + params += '&gpp_sid=' + encodeURIComponent(applicableSections.join(',')); + } + if (iframeEnabled) { syncs.push({ type: 'iframe', diff --git a/modules/videobyteBidAdapter.js b/modules/videobyteBidAdapter.js index c4dae78e862..8cedf9ac16a 100644 --- a/modules/videobyteBidAdapter.js +++ b/modules/videobyteBidAdapter.js @@ -2,6 +2,14 @@ import { logMessage, logError, deepAccess, isFn, isPlainObject, isStr, isNumber, import {registerBidder} from '../src/adapters/bidderFactory.js'; import {VIDEO} from '../src/mediaTypes.js'; +/** + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid + * @typedef {import('../src/adapters/bidderFactory.js').ServerResponse} ServerResponse + * @typedef {import('../src/adapters/bidderFactory.js').SyncOptions} SyncOptions + * @typedef {import('../src/adapters/bidderFactory.js').UserSync} UserSync + */ + const BIDDER_CODE = 'videobyte'; const DEFAULT_BID_TTL = 300; const DEFAULT_CURRENCY = 'USD'; diff --git a/modules/videoheroesBidAdapter.js b/modules/videoheroesBidAdapter.js index 2f02734a31f..ee2c2deef8b 100644 --- a/modules/videoheroesBidAdapter.js +++ b/modules/videoheroesBidAdapter.js @@ -4,6 +4,11 @@ import { BANNER, NATIVE, VIDEO } from '../src/mediaTypes.js'; import { config } from '../src/config.js'; import { convertOrtbRequestToProprietaryNative } from '../src/native.js'; +/** + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid + */ + const BIDDER_CODE = 'videoheroes'; const DEFAULT_CUR = 'USD'; const ENDPOINT_URL = `https://point.contextualadv.com/?t=2&partner=hash`; diff --git a/modules/videojsVideoProvider.js b/modules/videojsVideoProvider.js index 23a23447faa..7764e8af995 100644 --- a/modules/videojsVideoProvider.js +++ b/modules/videojsVideoProvider.js @@ -201,8 +201,15 @@ export function VideojsProvider(providerConfig, vjs_, adState_, timeState_, call return; } - player.ima.changeAdTag(adTagUrl); - player.ima.requestAds(); + // The VideoJS IMA plugin version 1.11.0 will throw when the ad is empty. + try { + player.ima.changeAdTag(adTagUrl); + player.ima.requestAds(); + } catch (e) { + /* + Handling is not required; ad errors are emitted automatically by video.js + */ + } } function onEvent(type, callback, payload) { diff --git a/modules/videonowBidAdapter.js b/modules/videonowBidAdapter.js index bfbc07fdff1..563f692693a 100644 --- a/modules/videonowBidAdapter.js +++ b/modules/videonowBidAdapter.js @@ -2,6 +2,12 @@ import {registerBidder} from '../src/adapters/bidderFactory.js'; import {BANNER} from '../src/mediaTypes.js'; import {_each, getBidIdParameter, getValue, logError, logInfo} from '../src/utils.js'; +/** + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid + * @typedef {import('../src/adapters/bidderFactory.js').ServerResponse} ServerResponse + */ + const BIDDER_CODE = 'videonow'; const RTB_URL = 'https://adx.videonow.ru/yhb' const DEFAULT_CURRENCY = 'RUB' diff --git a/modules/videoreachBidAdapter.js b/modules/videoreachBidAdapter.js index 9fd5853c75e..8835398d7cc 100644 --- a/modules/videoreachBidAdapter.js +++ b/modules/videoreachBidAdapter.js @@ -1,4 +1,4 @@ -import { getValue, getBidIdParameter } from '../src/utils.js'; +import {getBidIdParameter, getValue} from '../src/utils.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; const BIDDER_CODE = 'videoreach'; const ENDPOINT_URL = 'https://a.videoreach.com/hb/'; diff --git a/modules/viqeoBidAdapter.js b/modules/viqeoBidAdapter.js index 5762a794c8e..28f4de1fd52 100644 --- a/modules/viqeoBidAdapter.js +++ b/modules/viqeoBidAdapter.js @@ -3,6 +3,14 @@ import {logError, logInfo, _each, mergeDeep, isFn, isNumber, isPlainObject} from import {VIDEO} from '../src/mediaTypes.js'; import {Renderer} from '../src/Renderer.js'; +/** + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid + * @typedef {import('../src/adapters/bidderFactory.js').ServerResponse} ServerResponse + * @typedef {import('../src/adapters/bidderFactory.js').ServerRequest} ServerRequest + * @typedef {import('../src/adapters/bidderFactory.js').BidderSpec} BidderSpec + */ + const BIDDER_CODE = 'viqeo'; const DEFAULT_MIMES = ['application/javascript']; const VIQEO_ENDPOINT = 'https://ads.betweendigital.com/openrtb_bid'; diff --git a/modules/visxBidAdapter.js b/modules/visxBidAdapter.js index a066d93d69e..a86c958392e 100644 --- a/modules/visxBidAdapter.js +++ b/modules/visxBidAdapter.js @@ -1,8 +1,11 @@ -import { triggerPixel, parseSizesInput, deepAccess, logError, getGptSlotInfoForAdUnitCode } from '../src/utils.js'; -import { registerBidder } from '../src/adapters/bidderFactory.js'; -import { config } from '../src/config.js'; -import { BANNER, VIDEO } from '../src/mediaTypes.js'; -import { INSTREAM as VIDEO_INSTREAM } from '../src/video.js'; +import {deepAccess, logError, parseSizesInput, triggerPixel} from '../src/utils.js'; +import {registerBidder} from '../src/adapters/bidderFactory.js'; +import {config} from '../src/config.js'; +import {BANNER, VIDEO} from '../src/mediaTypes.js'; +import {INSTREAM as VIDEO_INSTREAM} from '../src/video.js'; +import {getStorageManager} from '../src/storageManager.js'; +import {getGptSlotInfoForAdUnitCode} from '../libraries/gptUtils/gptUtils.js'; + const BIDDER_CODE = 'visx'; const GVLID = 154; const BASE_URL = 'https://t.visx.net'; @@ -12,6 +15,7 @@ const TIME_TO_LIVE = 360; const DEFAULT_CUR = 'EUR'; const ADAPTER_SYNC_PATH = '/push_sync'; const TRACK_TIMEOUT_PATH = '/track/bid_timeout'; +const RUNTIME_STATUS_RESPONSE_TIME = 999000; const LOG_ERROR_MESS = { noAuid: 'Bid from response has no auid parameter - ', noAdm: 'Bid from response has no adm parameter - ', @@ -29,6 +33,8 @@ const LOG_ERROR_MESS = { videoMissing: 'Bid request videoType property is missing - ' }; const currencyWhiteList = ['EUR', 'USD', 'GBP', 'PLN']; +export const storage = getStorageManager({bidderCode: BIDDER_CODE}); +const _bidResponseTimeLogged = []; export const spec = { code: BIDDER_CODE, gvlid: GVLID, @@ -117,10 +123,13 @@ export const spec = { ...(payloadSchain && { schain: payloadSchain }) } }; + + const vads = _getUserId(); const user = { ext: { ...(payloadUserEids && { eids: payloadUserEids }), - ...(payload.gdpr_consent && { consent: payload.gdpr_consent }) + ...(payload.gdpr_consent && { consent: payload.gdpr_consent }), + ...(vads && { vads }) } }; const regs = ('gdpr_applies' in payload) && { @@ -201,6 +210,12 @@ export const spec = { if (bid.ext && bid.ext.events && bid.ext.events.win) { triggerPixel(bid.ext.events.win); } + // Call 'track/runtime' with the corresponding bid.requestId - only once per auction + if (bid.ext && bid.ext.events && bid.ext.events.runtime && !_bidResponseTimeLogged.includes(bid.auctionId)) { + _bidResponseTimeLogged.push(bid.auctionId); + const _roundedTime = _roundResponseTime(bid.timeToRespond, 50); + triggerPixel(bid.ext.events.runtime.replace('{STATUS_CODE}', RUNTIME_STATUS_RESPONSE_TIME + _roundedTime)); + } }, onTimeout: function(timeoutData) { // Call '/track/bid_timeout' with timeout data @@ -314,6 +329,10 @@ function _addBidResponse(serverBid, bidsMap, currency, bidResponses) { if (serverBid.ext && serverBid.ext.prebid) { bidResponse.ext = serverBid.ext.prebid; + if (serverBid.ext.visx && serverBid.ext.visx.events) { + const prebidExtEvents = bidResponse.ext.events || {}; + bidResponse.ext.events = Object.assign(prebidExtEvents, serverBid.ext.visx.events); + } } const visxTargeting = deepAccess(serverBid, 'ext.prebid.targeting'); @@ -382,4 +401,60 @@ function _isAdSlotExists(adUnitCode) { return false; } +// Generate user id (25 chars) with NanoID +// https://github.com/ai/nanoid/ +function _generateUserId() { + for ( + var t = 'useandom-26T198340PX75pxJACKVERYMINDBUSHWOLF_GQZbfghjklqvwyzrict', + e = new Date().getTime() % 1073741824, + i = '', + o = 0; + o < 5; + o++ + ) { + i += t[e % 64]; + e = Math.floor(e / 64); + } + for (o = 20; o--;) i += t[(64 * Math.random()) | 0]; + return i; +} + +function _getUserId() { + const USER_ID_KEY = '__vads'; + let vads; + + if (storage.cookiesAreEnabled()) { + vads = storage.getCookie(USER_ID_KEY); + } else if (storage.localStorageIsEnabled()) { + vads = storage.getDataFromLocalStorage(USER_ID_KEY); + } + + if (vads && vads.length) { + return vads; + } + + vads = _generateUserId(); + if (storage.cookiesAreEnabled()) { + const expires = new Date(Date.now() + 2592e6).toUTCString(); + storage.setCookie(USER_ID_KEY, vads, expires); + return vads; + } else if (storage.localStorageIsEnabled()) { + storage.setDataInLocalStorage(USER_ID_KEY, vads); + return vads; + } + + return null; +} + +function _roundResponseTime(time, timeRange) { + if (time <= 0) { + return 0; // Special code for scriptLoadTime of 0 ms or less + } else if (time > 5000) { + return 100; // Constant code for scriptLoadTime greater than 5000 ms + } else { + const roundedValue = Math.floor((time - 1) / timeRange) + 1; + return roundedValue; + } +} + registerBidder(spec); diff --git a/modules/voxBidAdapter.js b/modules/voxBidAdapter.js index acf1cc9579c..f1670de76d0 100644 --- a/modules/voxBidAdapter.js +++ b/modules/voxBidAdapter.js @@ -4,20 +4,39 @@ import {BANNER, VIDEO} from '../src/mediaTypes.js'; import {find} from '../src/polyfill.js'; import {auctionManager} from '../src/auctionManager.js'; import {Renderer} from '../src/Renderer.js'; +import {config} from '../src/config.js' + +/** + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid + * @typedef {import('../src/adapters/bidderFactory.js').ServerResponse} ServerResponse + * @typedef {import('../src/adapters/bidderFactory.js').validBidRequests} validBidRequests + */ + +const { getConfig } = config; const BIDDER_CODE = 'vox'; const SSP_ENDPOINT = 'https://ssp.hybrid.ai/auction/prebid'; const VIDEO_RENDERER_URL = 'https://acdn.adnxs.com/video/outstream/ANOutstreamVideo.js'; const TTL = 60; +const GVLID = 206; function buildBidRequests(validBidRequests) { - return _map(validBidRequests, function(validBidRequest) { - const params = validBidRequest.params; + return _map(validBidRequests, function(bid) { + const currency = getConfig('currency.adServerCurrency'); + const floorInfo = bid.getFloor ? bid.getFloor({ + currency: currency || 'USD' + }) : {}; + + const params = bid.params; const bidRequest = { - bidId: validBidRequest.bidId, + floorInfo, + schain: bid.schain, + userId: bid.userId, + bidId: bid.bidId, // TODO: fix transactionId leak: https://github.com/prebid/Prebid.js/issues/9781 - transactionId: validBidRequest.transactionId, - sizes: validBidRequest.sizes, + transactionId: bid.transactionId, + sizes: bid.sizes, placement: params.placement, placeId: params.placementId, imageUrl: params.imageUrl @@ -172,6 +191,7 @@ function wrapBanner(bid, bidData) { export const spec = { code: BIDDER_CODE, + gvlid: GVLID, supportedMediaTypes: [BANNER, VIDEO], /** diff --git a/modules/vrtcalBidAdapter.js b/modules/vrtcalBidAdapter.js index ff7839d0e02..07dc35525c3 100644 --- a/modules/vrtcalBidAdapter.js +++ b/modules/vrtcalBidAdapter.js @@ -1,11 +1,16 @@ import {registerBidder} from '../src/adapters/bidderFactory.js'; import { BANNER } from '../src/mediaTypes.js'; import {ajax} from '../src/ajax.js'; -import {isFn, isPlainObject} from '../src/utils.js'; import { config } from '../src/config.js'; +import {deepAccess, isFn, isPlainObject} from '../src/utils.js'; + +const GVLID = 706; +const VRTCAL_USER_SYNC_URL_IFRAME = `https://usync.vrtcal.com/i?ssp=1804&synctype=iframe`; +const VRTCAL_USER_SYNC_URL_REDIRECT = `https://usync.vrtcal.com/i?ssp=1804&synctype=redirect`; export const spec = { code: 'vrtcal', + gvlid: GVLID, supportedMediaTypes: [BANNER], isBidRequestValid: function (bid) { return true; @@ -62,13 +67,14 @@ export const spec = { site: { id: 'VRTCAL_FILLED', name: 'VRTCAL_FILLED', - cat: ['VRTCAL_FILLED'], - domain: decodeURIComponent(window.location.href).replace('https://', '').replace('http://', '').split('/')[0] - + cat: deepAccess(bid, 'ortb2.site.cat', []), + domain: decodeURIComponent(window.location.href).replace('https://', '').replace('http://', '').split('/')[0], + page: window.location.href }, device: { - ua: 'VRTCAL_FILLED', - ip: 'VRTCAL_FILLED' + language: navigator.language, + ua: navigator.userAgent, + ip: deepAccess(bid, 'params.bidOverride.device.ip') || deepAccess(bid, 'params.ext.ip') || undefined }, regs: { coppa: coppa, @@ -144,7 +150,34 @@ export const spec = { ); ajax(winUrl, null); return true; + }, + + getUserSyncs: function(syncOptions, serverResponses, gdprConsent = {}, uspConsent = '', gppConsent = {}) { + const syncs = []; + const gdprFlag = `&gdpr=${gdprConsent.gdprApplies ? 1 : 0}`; + const gdprString = `&gdpr_consent=${encodeURIComponent((gdprConsent.consentString || ''))}`; + const usPrivacy = `&us_privacy=${encodeURIComponent(uspConsent)}`; + const gpp = gppConsent.gppString ? gppConsent.gppString : ''; + const gppSid = Array.isArray(gppConsent.applicableSections) ? gppConsent.applicableSections.join(',') : ''; + let vrtcalSyncURL = '' + + if (syncOptions.iframeEnabled) { + vrtcalSyncURL = `${VRTCAL_USER_SYNC_URL_IFRAME}${usPrivacy}${gdprFlag}${gdprString}&gpp=${gpp}&gpp_sid=${gppSid}&surl=`; + syncs.push({ + type: 'iframe', + url: vrtcalSyncURL + }); + } else { + vrtcalSyncURL = `${VRTCAL_USER_SYNC_URL_REDIRECT}${usPrivacy}${gdprFlag}${gdprString}&gpp=${gpp}&gpp_sid=${gppSid}&surl=`; + syncs.push({ + type: 'image', + url: vrtcalSyncURL + }); + } + + return syncs; } + }; registerBidder(spec); diff --git a/modules/weboramaRtdProvider.js b/modules/weboramaRtdProvider.js index 6ba502d2c8b..a0963b844e1 100644 --- a/modules/weboramaRtdProvider.js +++ b/modules/weboramaRtdProvider.js @@ -7,25 +7,29 @@ * @requires module:modules/realTimeData */ -/** profile metadata +/** + * profile metadata * @typedef dataCallbackMetadata * @property {boolean} user if true it is user-centric data * @property {string} source describe the source of data, if 'contextual' or 'wam' * @property {boolean} isDefault if true it the default profile defined in the configuration */ -/** profile from contextual, wam or sfbx +/** + * profile from contextual, wam or sfbx * @typedef {Object.} Profile */ -/** onData callback type +/** + * onData callback type * @callback dataCallback * @param {Profile} data profile data * @param {dataCallbackMetadata} meta metadata * @returns {void} */ -/** setPrebidTargeting callback type +/** + * setPrebidTargeting callback type * @callback setPrebidTargetingCallback * @param {string} adUnitCode * @param {Profile} data @@ -33,7 +37,8 @@ * @returns {boolean} */ -/** sendToBidders callback type +/** + * sendToBidders callback type * @callback sendToBiddersCallback * @param {Object} bid * @param {string} adUnitCode @@ -90,7 +95,8 @@ * @property {?boolean} enabled if false, will ignore this configuration */ -/** common configuration between contextual, wam and sfbx +/** + * common configuration between contextual, wam and sfbx * @typedef {WeboCtxConf|WeboUserDataConf|SfbxLiteDataConf} CommonConf */ @@ -109,8 +115,7 @@ import { isBoolean, isPlainObject, logWarn, - mergeDeep, - tryAppendQueryString + mergeDeep } from '../src/utils.js'; import { submodule @@ -123,6 +128,7 @@ import { } from '../src/storageManager.js'; import adapterManager from '../src/adapterManager.js'; import {MODULE_TYPE_RTD} from '../src/activities/modules.js'; +import {tryAppendQueryString} from '../libraries/urlUtils/urlUtils.js'; /** @type {string} */ const MODULE_NAME = 'realTimeData'; @@ -188,7 +194,8 @@ class WeboramaRtdProvider { constructor(components) { this.#components = components; } - /** Initialize module + /** + * Initialize module * @method * @param {Object} moduleConfig * @param {?ModuleParams} moduleConfig.params @@ -219,7 +226,8 @@ class WeboramaRtdProvider { return Object.values(this.#components).some((c) => c.initialized); } - /** function that will allow RTD sub-modules to modify the AdUnit object for each auction + /** + * function that will allow RTD sub-modules to modify the AdUnit object for each auction * @method * @param {Object} reqBidsConfigObj * @param {doneCallback} onDone @@ -253,7 +261,8 @@ class WeboramaRtdProvider { }); } - /** function that provides ad server targeting data to RTD-core + /** + * function that provides ad server targeting data to RTD-core * @method * @param {string[]} adUnitsCodes * @param {Object} moduleConfig @@ -294,7 +303,8 @@ class WeboramaRtdProvider { } } - /** Initialize subsection module + /** + * Initialize subsection module * @method * @private * @param {ModuleParams} moduleParams @@ -330,7 +340,8 @@ class WeboramaRtdProvider { return true; } - /** normalize submodule configuration + /** + * normalize submodule configuration * @method * @private * @param {ModuleParams} moduleParams @@ -363,7 +374,8 @@ class WeboramaRtdProvider { } } - /** coerce setPrebidTargeting to a callback + /** + * coerce setPrebidTargeting to a callback * @method * @private * @param {CommonConf} submoduleParams @@ -379,7 +391,8 @@ class WeboramaRtdProvider { } } - /** coerce sendToBidders to a callback + /** + * coerce sendToBidders to a callback * @method * @private * @param {CommonConf} submoduleParams @@ -426,7 +439,8 @@ class WeboramaRtdProvider { * @typedef {Object} AdUnit * @property {Object[]} bids */ - /** function that handles bid request data + /** + * function that handles bid request data * @method * @private * @param {Object} reqBidsConfigObj @@ -476,18 +490,21 @@ class WeboramaRtdProvider { }); } - /** onSuccess callback type + /** + * onSuccess callback type * @callback successCallback * @param {?Object} data * @returns {void} */ - /** onDone callback type + /** + * onDone callback type * @callback doneCallback * @returns {void} */ - /** Fetch Bigsea Contextual Profile + /** + * Fetch Bigsea Contextual Profile * @method * @private * @param {WeboCtxConf} weboCtxConf @@ -566,7 +583,8 @@ class WeboramaRtdProvider { ajax(urlProfileAPI, callback, null, options); } - /** set bigsea contextual profile on module state + /** + * set bigsea contextual profile on module state * @method * @private * @param {?Object} data @@ -579,7 +597,8 @@ class WeboramaRtdProvider { } } - /** function that provides data handlers based on the configuration + /** + * function that provides data handlers based on the configuration * @method * @private * @param {ModuleParams} moduleParams @@ -667,7 +686,8 @@ class WeboramaRtdProvider { onData: dataConf.onData, }; } - /** handle individual bid + /** + * handle individual bid * @method * @private * @param {Object} reqBidsConfigObj @@ -694,7 +714,8 @@ class WeboramaRtdProvider { } } - /** function that handles bid request data + /** + * function that handles bid request data * @method * @private * @param {ProfileHandler} ph profile handler @@ -705,7 +726,8 @@ class WeboramaRtdProvider { return [deepClone(ph.data), deepClone(ph.metadata)]; } - /** handle appnexus/xandr bid + /** + * handle appnexus/xandr bid * @method * @private * @param {Object} reqBidsConfigObj @@ -723,7 +745,8 @@ class WeboramaRtdProvider { // this.#setBidderOrtb2(reqBidsConfigObj.ortb2Fragments?.bidder, bid.bidder, base, profile); } - /** handle generic bid via ortb2 arbitrary data + /** + * handle generic bid via ortb2 arbitrary data * @method * @private * @param {Object} reqBidsConfigObj @@ -844,7 +867,8 @@ export function isValidProfile(profile) { * @returns {buildProfileHandlerCallback} */ function getContextualProfile(component /* equivalent to this */) { - /** return contextual profile + /** + * return contextual profile * @param {WeboCtxConf} weboCtxConf * @returns {[Profile,boolean]} contextual profile + isDefault boolean flag */ @@ -865,7 +889,8 @@ function getContextualProfile(component /* equivalent to this */) { * @returns {buildProfileHandlerCallback} */ function getWeboUserDataProfile(component /* equivalent to this */) { - /** return weboUserData profile + /** + * return weboUserData profile * @param {WeboUserDataConf} weboUserDataConf * @returns {[Profile,boolean]} weboUserData profile + isDefault boolean flag */ @@ -885,7 +910,8 @@ function getWeboUserDataProfile(component /* equivalent to this */) { * @returns {buildProfileHandlerCallback} */ function getSfbxLiteDataProfile(component /* equivalent to this */) { - /** return weboUserData profile + /** + * return weboUserData profile * @param {SfbxLiteDataConf} sfbxLiteDataConf * @returns {[Profile,boolean]} sfbxLiteData profile + isDefault boolean flag */ @@ -909,7 +935,8 @@ function getSfbxLiteDataProfile(component /* equivalent to this */) { * @returns {void} */ -/** return generic webo data profile +/** + * return generic webo data profile * @param {WeboUserDataConf|SfbxLiteDataConf} weboDataConf * @param {cacheGetCallback} cacheGet * @param {cacheSetCallback} cacheSet diff --git a/modules/welectBidAdapter.js b/modules/welectBidAdapter.js index d88a3f4c3e2..533e6401cd5 100644 --- a/modules/welectBidAdapter.js +++ b/modules/welectBidAdapter.js @@ -1,6 +1,13 @@ import { deepAccess } from '../src/utils.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; +/** + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid + * @typedef {import('../src/adapters/bidderFactory.js').ServerResponse} ServerResponse + * @typedef {import('../src/adapters/bidderFactory.js').validBidRequests} validBidRequests + */ + const BIDDER_CODE = 'welect'; const DEFAULT_DOMAIN = 'www.welect.de'; diff --git a/modules/winrBidAdapter.js b/modules/winrBidAdapter.js index 5cc063d16b4..cf1158474b4 100644 --- a/modules/winrBidAdapter.js +++ b/modules/winrBidAdapter.js @@ -1,6 +1,4 @@ import { - convertCamelToUnderscore, - convertTypes, deepAccess, getBidRequest, getParameterByName, @@ -16,7 +14,14 @@ import {BANNER} from '../src/mediaTypes.js'; import {find, includes} from '../src/polyfill.js'; import {getStorageManager} from '../src/storageManager.js'; import {hasPurpose1Consent} from '../src/utils/gpdr.js'; -import {getANKeywordParam, transformBidderParamKeywords} from '../libraries/appnexusKeywords/anKeywords.js'; +import {getANKeywordParam, transformBidderParamKeywords} from '../libraries/appnexusUtils/anKeywords.js'; +import {convertCamelToUnderscore} from '../libraries/appnexusUtils/anUtils.js'; +import {convertTypes} from '../libraries/transformParamsUtils/convertTypes.js'; + +/** + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid + */ const BIDDER_CODE = 'winr'; const URL = 'https://ib.adnxs.com/ut/v3/prebid'; diff --git a/modules/xeBidAdapter.js b/modules/xeBidAdapter.js index 94a66e9980e..a813b9aa2a3 100644 --- a/modules/xeBidAdapter.js +++ b/modules/xeBidAdapter.js @@ -1,7 +1,16 @@ import { config } from '../src/config.js'; import { BANNER, VIDEO } from '../src/mediaTypes.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; -import { getAdUnitSizes, parseSizesInput, isFn, deepAccess, getBidIdParameter, logError, isArray } from '../src/utils.js'; +import {parseSizesInput, isFn, deepAccess, logError, isArray, getBidIdParameter} from '../src/utils.js'; +import {getAdUnitSizes} from '../libraries/sizeUtils/sizeUtils.js'; + +/** + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid + * @typedef {import('../src/adapters/bidderFactory.js').ServerResponse} ServerResponse + * @typedef {import('../src/adapters/bidderFactory.js').SyncOptions} SyncOptions + * @typedef {import('../src/adapters/bidderFactory.js').UserSync} UserSync + */ const CUR = 'USD'; const BIDDER_CODE = 'xe'; @@ -141,12 +150,12 @@ function interpretResponse(serverResponse, { bidderRequest }) { } /** -* 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 = '') { const syncs = []; const pixels = deepAccess(serverResponses, '0.body.data.0.ext.pixels'); @@ -171,11 +180,11 @@ function getUserSyncs(syncOptions, serverResponses, gdprConsent = {}, uspConsent } /** -* Get valid floor value from getFloor fuction. -* -* @param {Object} bid Current bid request. -* @return {null|Number} Returns floor value when bid.getFloor is function and returns valid floor object with USD currency, otherwise returns null. -*/ + * Get valid floor value from getFloor fuction. + * + * @param {Object} bid Current bid request. + * @return {null|Number} Returns floor value when bid.getFloor is function and returns valid floor object with USD currency, otherwise returns null. + */ export function getBidFloor(bid) { if (!isFn(bid.getFloor)) { return null; diff --git a/modules/yahoosspBidAdapter.js b/modules/yahoosspBidAdapter.js index 7a414c0d2ee..0453350a85a 100644 --- a/modules/yahoosspBidAdapter.js +++ b/modules/yahoosspBidAdapter.js @@ -6,9 +6,10 @@ import { Renderer } from '../src/Renderer.js'; import {hasPurpose1Consent} from '../src/utils/gpdr.js'; const INTEGRATION_METHOD = 'prebid.js'; -const BIDDER_CODE = 'yahoossp'; +const BIDDER_CODE = 'yahooAds'; +const BIDDER_ALIASES = ['yahoossp', 'yahooAdvertising'] const GVLID = 25; -const ADAPTER_VERSION = '1.0.2'; +const ADAPTER_VERSION = '1.1.0'; const PREBID_VERSION = '$prebid.version$'; const DEFAULT_BID_TTL = 300; const TEST_MODE_DCN = '8a969516017a7a396ec539d97f540011'; @@ -49,13 +50,17 @@ const SUPPORTED_USER_ID_SOURCES = [ 'quantcast.com', 'tapad.com', 'uidapi.com', - 'verizonmedia.com', 'yahoo.com', 'zeotap.com' ]; /* Utility functions */ +function getConfigValue(bid, key) { + const bidderCode = bid.bidder || bid.bidderCode; + return config.getConfig(`${bidderCode}.${key}`); +} + function getSize(size) { return { w: parseInt(size[0]), @@ -106,7 +111,7 @@ function extractUserSyncUrls(syncOptions, pixels) { * @param {object} consentData * @param {object} consentData.gpp * @param {string} consentData.gpp.gppConsent - * @param {array} consentData.gpp.applicableSections + * @param {Array} consentData.gpp.applicableSections * @param {object} consentData.gdpr * @param {object} consentData.gdpr.consentString * @param {object} consentData.gdpr.gdprApplies @@ -114,11 +119,12 @@ function extractUserSyncUrls(syncOptions, pixels) { */ function updateConsentQueryParams(url, consentData) { const parameterMap = { - 'gdpr_consent': consentData.gdpr.consentString, - 'gdpr': consentData.gdpr.gdprApplies ? '1' : '0', - 'us_privacy': consentData.uspConsent, - 'gpp': consentData.gpp.gppString, - 'gpp_sid': consentData.gpp.applicableSections ? consentData.gpp.applicableSections.join(',') : '' + 'gdpr_consent': consentData.gdpr ? consentData.gdpr.consentString : '', + 'gdpr': consentData.gdpr && consentData.gdpr.gdprApplies ? '1' : '0', + 'us_privacy': consentData.uspConsent ? consentData.uspConsent : '', + 'gpp': consentData.gpp ? consentData.gpp.gppString : '', + 'gpp_sid': consentData.gpp && Array.isArray(consentData.gpp.applicableSections) + ? consentData.gpp.applicableSections.join(',') : '' } const existingUrl = new URL(url); @@ -155,8 +161,8 @@ function getPubIdMode(bid) { return pubIdMode; }; -function getAdapterMode() { - let adapterMode = config.getConfig('yahoossp.mode'); +function getAdapterMode(bid) { + let adapterMode = getConfigValue(bid, 'mode'); adapterMode = adapterMode ? adapterMode.toLowerCase() : undefined; if (typeof adapterMode === 'undefined' || adapterMode === BANNER) { return BANNER; @@ -177,7 +183,7 @@ function getResponseFormat(bid) { }; function getFloorModuleData(bid) { - const adapterMode = getAdapterMode(); + const adapterMode = getAdapterMode(bid); const getFloorRequestObject = { currency: deepAccess(bid, 'params.bidOverride.cur') || DEFAULT_CURRENCY, mediaType: adapterMode, @@ -187,7 +193,7 @@ function getFloorModuleData(bid) { }; function filterBidRequestByMode(validBidRequests) { - const mediaTypesMode = getAdapterMode(); + const mediaTypesMode = getAdapterMode(validBidRequests[0]); let result = []; if (mediaTypesMode === BANNER) { result = validBidRequests.filter(bid => { @@ -244,7 +250,7 @@ function validateAppendObject(validationType, allowedKeys, inputObject, appendTo }; function getTtl(bidderRequest) { - const globalTTL = config.getConfig('yahoossp.ttl'); + const globalTTL = getConfigValue(bidderRequest, 'ttl'); return globalTTL ? validateTTL(globalTTL) : validateTTL(deepAccess(bidderRequest, 'params.ttl')); }; @@ -276,8 +282,8 @@ function generateOpenRtbObject(bidderRequest, bid) { ext: { 'us_privacy': bidderRequest.uspConsent ? bidderRequest.uspConsent : '', gdpr: bidderRequest.gdprConsent && bidderRequest.gdprConsent.gdprApplies ? 1 : 0, - gpp: bidderRequest.gppConsent.gppString, - gpp_sid: bidderRequest.gppConsent.applicableSections + gpp: bidderRequest.gppConsent ? bidderRequest.gppConsent.gppString : '', + gpp_sid: bidderRequest.gppConsent ? bidderRequest.gppConsent.applicableSections : [] } }, source: { @@ -332,7 +338,7 @@ function generateOpenRtbObject(bidderRequest, bid) { }; function appendImpObject(bid, openRtbObject) { - const mediaTypeMode = getAdapterMode(); + const mediaTypeMode = getAdapterMode(bid); if (openRtbObject && bid) { const impObject = { @@ -494,20 +500,21 @@ function appendFirstPartyData(outBoundBidRequest, bid) { function generateServerRequest({payload, requestOptions, bidderRequest}) { const pubIdMode = getPubIdMode(bidderRequest); - let sspEndpoint = config.getConfig('yahoossp.endpoint') || SSP_ENDPOINT_DCN_POS; + const overrideEndpoint = getConfigValue(bidderRequest, 'endpoint'); + let sspEndpoint = overrideEndpoint || SSP_ENDPOINT_DCN_POS; if (pubIdMode === true) { - sspEndpoint = config.getConfig('yahoossp.endpoint') || SSP_ENDPOINT_PUBID; + sspEndpoint = overrideEndpoint || SSP_ENDPOINT_PUBID; }; if (deepAccess(bidderRequest, 'params.testing.e2etest') === true) { - logInfo('yahoossp adapter e2etest mode is active'); + logInfo('Adapter e2etest mode is active'); requestOptions.withCredentials = false; if (pubIdMode === true) { payload.site.id = TEST_MODE_PUBID_DCN; } else { - const mediaTypeMode = getAdapterMode(); + const mediaTypeMode = getAdapterMode(bidderRequest); payload.site.id = TEST_MODE_DCN; payload.imp.forEach(impObject => { impObject.ext.e2eTestMode = true; @@ -516,8 +523,9 @@ function generateServerRequest({payload, requestOptions, bidderRequest}) { } else if (mediaTypeMode === VIDEO) { impObject.tagid = TEST_MODE_VIDEO_POS; // video passback } else { - logWarn('yahoossp adapter e2etest mode does not support yahoossp.mode="all". \n Please specify either "banner" or "video"'); - logWarn('yahoossp adapter e2etest mode: Please make sure your adUnit matches the yahoossp.mode video or banner'); + const bidderCode = bidderRequest.bidderCode; + logWarn(`e2etest mode does not support ${bidderCode}.mode="all". \n Please specify either "banner" or "video"`); + logWarn(`Adapter e2etest mode: Please make sure your adUnit matches the ${bidderCode}.mode video or banner`); } }); } @@ -528,7 +536,7 @@ function generateServerRequest({payload, requestOptions, bidderRequest}) { method: 'POST', data: payload, options: requestOptions, - bidderRequest: bidderRequest + bidderRequest // Additional data for use in interpretResponse() }; }; @@ -547,7 +555,7 @@ function createRenderer(bidderRequest, bidResponse) { }, deepAccess(bidderRequest, 'params.testing.renderer.setTimeout') || DEFAULT_RENDERER_TIMEOUT); }); } catch (error) { - logWarn('yahoossp renderer error: setRender() failed', error); + logWarn('Renderer error: setRender() failed', error); } return renderer; } @@ -557,7 +565,7 @@ function createRenderer(bidderRequest, bidResponse) { export const spec = { code: BIDDER_CODE, gvlid: GVLID, - aliases: [], + aliases: BIDDER_ALIASES, supportedMediaTypes: [BANNER, VIDEO], isBidRequestValid: function(bid) { @@ -570,14 +578,14 @@ export const spec = { ) { return true; } else { - logWarn('yahoossp bidder params missing or incorrect, please pass object with either: dcn & pos OR pubId'); + logWarn('Bidder params missing or incorrect, please pass object with either: dcn & pos OR pubId'); return false; } }, buildRequests: function(validBidRequests, bidderRequest) { if (isEmpty(validBidRequests) || isEmpty(bidderRequest)) { - logWarn('yahoossp Adapter: buildRequests called with either empty "validBidRequests" or "bidderRequest"'); + logWarn('buildRequests called with either empty "validBidRequests" or "bidderRequest"'); return undefined; }; @@ -592,13 +600,12 @@ export const spec = { const filteredBidRequests = filterBidRequestByMode(validBidRequests); - if (config.getConfig('yahoossp.singleRequestMode') === true) { + if (getConfigValue(bidderRequest, 'singleRequestMode') === true) { const payload = generateOpenRtbObject(bidderRequest, filteredBidRequests[0]); filteredBidRequests.forEach(bid => { appendImpObject(bid, payload); }); - - return generateServerRequest({payload, requestOptions, bidderRequest}); + return [generateServerRequest({payload, requestOptions, bidderRequest})]; } return filteredBidRequests.map(bid => { @@ -608,12 +615,11 @@ export const spec = { }); }, - interpretResponse: function(serverResponse, { data, bidderRequest }) { + interpretResponse: function(serverResponse, { bidderRequest }) { const response = []; if (!serverResponse.body || !Array.isArray(serverResponse.body.seatbid)) { return response; } - let seatbids = serverResponse.body.seatbid; seatbids.forEach(seatbid => { let bid; @@ -628,7 +634,6 @@ export const spec = { let bidResponse = { adId: deepAccess(bid, 'adId') ? bid.adId : bid.impid || bid.crid, - adUnitCode: bidderRequest.adUnitCode, requestId: bid.impid, cpm: cpm, width: bid.w, diff --git a/modules/yahoosspBidAdapter.md b/modules/yahoosspBidAdapter.md index 35a640b92b1..62fe0f22a55 100644 --- a/modules/yahoosspBidAdapter.md +++ b/modules/yahoosspBidAdapter.md @@ -1,10 +1,10 @@ # Overview -**Module Name:** yahoossp Bid Adapter +**Module Name:** Yahoo Advertising Bid Adapter **Module Type:** Bidder Adapter -**Maintainer:** hb-fe-tech@yahooinc.com +**Maintainer:** prebid-tech-team@yahooinc.com # Description -The Yahoo SSP Bid Adapter is an OpenRTB interface that consolidates all previous "Oath.inc" adapters such as: "aol", "oneMobile", "oneDisplay" & "oneVideo" supply-side platforms. +The Yahoo Advertising Bid Adapter is an OpenRTB interface that consolidates all previous "Oath.inc" adapters such as: "aol", "oneMobile", "oneDisplay" & "oneVideo" supply-side platforms. # Supported Features: * Media Types: Banner & Video @@ -19,51 +19,63 @@ The Yahoo SSP Bid Adapter is an OpenRTB interface that consolidates all previous * First Party Data (ortb2 & ortb2Imp) * Custom TTL (time to live) +# Adapter Aliases +Whilst the primary bidder code for this bid adapter is `yahooAds`, the aliases `yahoossp` and `yahooAdvertising` can be used to enable this adapter. If you wish to set Prebid configuration specifically for this bid adapter, then the configuration key _must_ match the used bidder code. All examples in this documentation use the primiry bidder code, but switching `yahooAds` with one of the relevant aliases may be required for your setup. Let's take [setting the request mode](#adapter-request-mode) as an example; if you used the `yahoossp` alias, then the corresponding `setConfig` API call would look like this: + +```javascript +pbjs.setConfig({ + yahoossp: { + mode: 'banner' // 'all', 'video', 'banner' (default) + } +}); +``` # Adapter Request mode -Since the yahoossp adapter now supports both Banner and Video adUnits a controller was needed to allow you to define when the adapter should generate a bid-requests to our Yahoo SSP. +Since the Yahoo Advertising bid adapter supports both Banner and Video adUnits, a controller was needed to allow you to define when the adapter should generate a bid-requests to the Yahoo bid endpoint. **Important!** By default the adapter mode is set to "banner" only. -This means that you do not need to explicitly declare the yahoossp.mode in the Global config to initiate banner adUnit requests. +This means that you do not need to explicitly declare the `yahooAds.mode` property in the global config to initiate banner adUnit requests. ## Request modes: * **undefined** - (Default) Will generate bid-requests for "Banner" formats only. * **banner** - Will generate bid-requests for "Banner" formats only (Explicit declaration). * **video** - Will generate bid-requests for "Video" formats only (Explicit declaration). -* **all** - Will generate bid-requests for both "Banner" & "Video" formats +* **all** - Will generate bid-requests for both "Banner" & "Video" formats. -**Important!** When setting yahoossp.mode = 'all' Make sure your Yahoo SSP Placement (pos id) supports both Banner & Video placements. -If it does not, the Yahoo SSP will respond only in the format it is set too. +**Important!** When setting `yahooAds.mode` to `'all'`, make sure your Yahoo Placement (pos id) supports both Banner & Video placements. +If it does not, the Yahoo bid server will respond only in the format it is set too. +### Example: explicitly setting the request mode ```javascript pbjs.setConfig({ - yahoossp: { + yahooAds: { mode: 'banner' // 'all', 'video', 'banner' (default) } }); ``` + # Integration Options -The `yahoossp` bid adapter supports 2 types of integration: +The Yahoo Advertising bid adapter supports 2 types of integration: 1. **dcn & pos** DEFAULT (Site/App & Position targeting) - For Display partners/publishers. -2. **pubId** (Publisher ID) - For legacy "oneVideo" AND New partners/publishers. -**Important:** pubId integration (option 2) is only possible when your Seller account is setup for "Inventory Mapping". +2. **pubId** (Publisher ID) - For legacy "oneVideo" AND new partners/publishers. +**Important:** pubId integration (option 2) is only possible when your seller account is setup for "Inventory Mapping". **Please Note:** Most examples in this file are using dcn & pos. ## Who is currently eligible for "pubId" integration At this time, only the following partners/publishers are eligble for pubId integration: -1. New partners/publishers that do not have any existing accounts on Yahoo SSP (aka: aol, oneMobile, oneDisplay). +1. New partners/publishers that do not have an existing account with Yahoo Advertising (aka: aol, oneMobile, oneDisplay). 2. Video SSP (oneVideo) partners/publishers that A. Do not have any display/banner inventory. - B. Do not have any existing accounts on Yahoo SSP (aka: aol, oneMobile, oneDisplay). + B. Do not have an existing account with Yahoo Advertising (aka: aol, oneMobile, oneDisplay). # Mandatory Bidder Parameters ## dcn & pos (DEFAULT) -The minimal requirements for the 'yahoossp' bid adapter to generate an outbound bid-request to our Yahoo SSP are: +The minimal requirements for the Yahoo Advertising bid adapter to generate an outbound bid-request to Yahoo's bid endpoint are: 1. At least 1 adUnit including mediaTypes: banner or video 2. **bidder.params** object must include: - A. **dcn:** Yahoo SSP Site/App inventory parameter. - B. **pos:** Yahoo SSP position inventory parameter. + A. **dcn:** Yahoo Advertising Site/App inventory parameter. + B. **pos:** Yahoo Advertising position inventory parameter. ### Example: dcn & pos Mandatory Parameters (Single banner adUnit) ```javascript @@ -76,10 +88,10 @@ const adUnits = [{ }, bids: [ { - bidder: 'yahoossp', + bidder: 'yahooAds', params: { - dcn: '8a969516017a7a396ec539d97f540011', // Site/App ID provided from SSP - pos: '8a969978017a7aaabab4ab0bc01a0009' // Placement ID provided from SSP + dcn: '8a969516017a7a396ec539d97f540011', // Site/App ID provided by Yahoo Advertising + pos: '8a969978017a7aaabab4ab0bc01a0009' // Placement ID provided by Yahoo Advertising } } ] @@ -87,10 +99,10 @@ const adUnits = [{ ``` ## pubId -The minimal requirements for the 'yahoossp' bid adapter to generate an outbound bid-request to our Yahoo SSP are: +The minimal requirements for the Yahoo Advertising bid adapter to generate an outbound bid-request to Yahoo's bid endpoint are: 1. At least 1 adUnit including mediaTypes: banner or video 2. **bidder.params** object must include: - A. **pubId:** Yahoo SSP Publisher ID (AKA oneVideo pubId/Exchange name) + A. **pubId:** Yahoo Advertising Publisher ID (AKA oneVideo pubId/Exchange name) ### Example: pubId Mandatory Parameters (Single banner adUnit) ```javascript @@ -103,14 +115,15 @@ const adUnits = [{ }, bids: [ { - bidder: 'yahoossp', + bidder: 'yahooAds', params: { - pubId: 'DemoPublisher', // Publisher External ID provided from Yahoo SSP. + pubId: 'DemoPublisher', // Publisher defined external ID as configured by Yahoo Advertising. } } ] }]; ``` + # Advanced adUnit Examples: ## Banner ```javascript @@ -124,21 +137,22 @@ const adUnits = [{ } }, bids: [{ - bidder: 'yahoossp', + bidder: 'yahooAds', params: { - dcn: '8a969516017a7a396ec539d97f540011', // Site/App ID provided from Yahoo SSP - pos: '8a969978017a7aaabab4ab0bc01a0009', // Placement ID provided from Yahoo SSP + dcn: '8a969516017a7a396ec539d97f540011', // Site/App ID provided by Yahoo Advertising + pos: '8a969978017a7aaabab4ab0bc01a0009', // Placement ID provided by Yahoo Advertising } } }] }]; ``` + ## Video Instream -**Important!** Make sure that the Yahoo SSP Placement type (in-stream) matches the adUnit video inventory type. +**Important!** Make sure that the Yahoo Advertising Placement type (in-stream) matches the adUnit video inventory type. **Note:** Make sure to set the adapter mode to allow video requests by setting it to mode: 'video' OR mode: 'all'. ```javascript pbjs.setConfig({ - yahoossp: { + yahooAds: { mode: 'video' } }); @@ -156,20 +170,21 @@ const adUnits = [{ } }, bids: [{ - bidder: 'yahoossp', + bidder: 'yahooAds', params: { - dcn: '8a969516017a7a396ec539d97f540011', // Site/App ID provided from Yahoo SSP - pos: '8a96958a017a7a57ac375d50c0c700cc', // Placement ID provided from Yahoo SSP + dcn: '8a969516017a7a396ec539d97f540011', // Site/App ID provided by Yahoo Advertising + pos: '8a96958a017a7a57ac375d50c0c700cc', // Placement ID provided by Yahoo Advertising } }] }]; + ``` ## Video Outstream -**Important!** Make sure that the Yahoo SSP Placement type (in-feed/ in-article) matches the adUnit video inventory type. +**Important!** Make sure that the Yahoo Advertsing placement type (in-feed/ in-article) matches the adUnit video inventory type. **Note:** Make sure to set the adapter mode to allow video requests by setting it to mode: 'video' OR mode: 'all' ```javascript pbjs.setConfig({ - yahoossp: { + yahooAds: { mode: 'video' } }); @@ -187,22 +202,22 @@ const adUnits = [{ } }, bids: [{ - bidder: 'yahoossp', + bidder: 'yahooAds', params: { - dcn: '8a969516017a7a396ec539d97f540011', // Site/App ID provided from Yahoo SSP - pos: '8a96958a017a7a57ac375d50c0c700cc', // Placement ID provided from Yahoo SSP + dcn: '8a969516017a7a396ec539d97f540011', // Site/App ID provided by Yahoo Advertising + pos: '8a96958a017a7a57ac375d50c0c700cc', // Placement ID provided by Yahoo Advertising } }] }]; + ``` ## Multi-Format -**Important!** If you intend to use the yahoossp bidder for both Banner and Video formats please make sure: -1. Set the adapter as mode: 'all' - to call the Yahoo SSP for both banner & video formats. -2. Make sure the Yahoo SSP placement (pos id) supports both banner & video format requests. - +**Important!** If you intend to use the Yahoo Advertising bidder for both Banner and Video formats please make sure: +1. Set the adapter as mode: 'all' - to configure the bid adapter to call the bid endpoint for both banner & video formats. +2. Make sure the Yahoo Advertising placement (pos id) supports both banner & video format requests. ```javascript pbjs.setConfig({ - yahoossp: { + yahooAds: { mode: 'all' } }); @@ -223,19 +238,18 @@ const adUnits = [{ } }, bids: [{ - bidder: 'yahoossp', + bidder: 'yahooAds', params: { - dcn: '8a969516017a7a396ec539d97f540011', // Site/App ID provided from Yahoo SSP - pos: '8a96958a017a7a57ac375d50c0c700cc', // Placement ID provided from Yahoo SSP + dcn: '8a969516017a7a396ec539d97f540011', // Site/App ID provided by Yahoo Advertising + pos: '8a96958a017a7a57ac375d50c0c700cc', // Placement ID provided by Yahoo Advertising } }] }]; ``` # Optional: Schain module support -The yahoossp adapter supports the Prebid.org Schain module and will pass it through to our Yahoo SSP +The Yahoo Advertising bid adapter supports the Prebid.org Schain module and will pass it through to our bid endpoint. For further details please see, https://docs.prebid.org/dev-docs/modules/schain.html - ## Global Schain Example: ```javascript pbjs.setConfig({ @@ -256,7 +270,7 @@ For further details please see, https://docs.prebid.org/dev-docs/modules/schain. ## Bidder Specific Schain Example: ```javascript pbjs.setBidderConfig({ - "bidders": ['yahoossp'], // can list more bidders here if they share the same config + "bidders": ['yahooAds'], // can list more bidders here if they share the same config "config": { "schain": { "validation": "strict", @@ -275,12 +289,10 @@ For further details please see, https://docs.prebid.org/dev-docs/modules/schain. ``` # Optional: Price floors module & bidfloor -The yahoossp adapter supports the Prebid.org Price Floors module and will use it to define the outbound bidfloor and currency. -By default the adapter will always check the existance of Module price floor. -If a module price floor does not exist you can set a custom bid floor for your impression using "params.bidOverride.imp.bidfloor". +The Yahoo Advertising bid adapter supports the Prebid.org Price Floors module and will use it to define the outbound bidfloor and currency, if the relevant floors have been defined in the configuration. +A cusom method for defining bid floors is also supported, this can be enabled by setting the `params.bidOverride.imp.bidfloor` bidder parameter. **Note:** All override params apply to all requests generated using this configuration regardless of format type. - ```javascript const adUnits = [{ code: 'override-pricefloor', @@ -292,10 +304,10 @@ const adUnits = [{ } }, bids: [{ - bidder: 'yahoossp', + bidder: 'yahooAds', params: { - dcn: '8a969516017a7a396ec539d97f540011', // Site/App ID provided from Yahoo SSP - pos: '8a969978017a7aaabab4ab0bc01a0009', // Placement ID provided from Yahoo SSP + dcn: '8a969516017a7a396ec539d97f540011', // Site/App ID provided by Yahoo Advertising + pos: '8a969978017a7aaabab4ab0bc01a0009', // Placement ID provided by Yahoo Advertising bidOverride :{ imp: { bidfloor: 5.00 // bidOverride priceFloor @@ -310,21 +322,21 @@ const adUnits = [{ For further details please see, https://docs.prebid.org/dev-docs/modules/floors.html # Optional: Self-served E2E testing mode -If you want to see how the yahoossp adapter works and loads you are invited to try it out using our testing mode. +If you want to see how the Yahoo Advertising bid adapter works and loads you are invited to try it out using our testing mode. This is useful for integration testing and response parsing when checking banner vs video capabilities. ## How to use E2E test mode: -1. Set the yahoossp global config mode to either 'banner' or 'video' - depending on the adUnit you want to test. +1. Set the `yahooAds` global config mode to either `'banner'` or `'video'` - depending on the adUnit you want to test. 2. Add params.testing.e2etest: true to your adUnit bidder config - See examples below. **Note:** When using E2E Test Mode you do not need to pass mandatory bidder params dcn or pos. -**Important!** E2E Testing Mode only works when the Bidder Request Mode is set explicitly to either 'banner' or 'video'. +**Important!** E2E Testing Mode only works when the Bidder Request Mode is set explicitly to either `'banner'` or `'video'`. ## Activating E2E Test for "Banner" ```javascript pbjs.setConfig({ - yahoossp: { + yahooAds: { mode: 'banner' // select 'banner' or 'video' to define what response to load } }); @@ -338,7 +350,7 @@ const adUnits = [{ }, bids: [ { - bidder: 'yahoossp', + bidder: 'yahooAds', params: { testing: { e2etest: true // Activate E2E Test mode @@ -353,7 +365,7 @@ const adUnits = [{ **Note:** We recommend using Video Outstream as it would load the video response using our Outstream Renderer feature ```javascript pbjs.setConfig({ - yahoossp: { + yahooAds: { mode: 'video' } }); @@ -371,7 +383,7 @@ const adUnits = [{ } }, bids: [{ - bidder: 'yahoossp', + bidder: 'yahooAds', params: { testing: { e2etest: true // Activate E2E Test mode @@ -382,8 +394,8 @@ const adUnits = [{ ``` # Optional: First Party Data -The yahoossp adapter now supports first party data passed via: -1. Global ortb2 object using pbjs.setConfig() +The Yahoo Advertising bid adapter supports first party data passed via: +1. Global ortb2 object using `pbjs.setConfig()` 2. adUnit ortb2Imp object declared within an adUnit. For further details please see, https://docs.prebid.org/features/firstPartyData.html ## Global First Party Data "ortb2" @@ -392,15 +404,15 @@ For further details please see, https://docs.prebid.org/features/firstPartyData. pbjs.setConfig({ ortb2: { site: { - name: 'yahooAdTech', - domain: 'yahooadtech.com', + name: 'Yahoo Advertising', + domain: 'yahooadvertising.com', cat: ['IAB2'], sectioncat: ['IAB2-2'], pagecat: ['IAB2-2'], - page: 'https://page.yahooadtech.com/here.html', - ref: 'https://ref.yahooadtech.com/there.html', + page: 'https://page.yahooadvertising.com.com/here.html', + ref: 'https://ref.yahooadvertising.com.com/there.html', keywords:'yahoo, ad, tech', - search: 'SSP', + search: 'header bidding', content: { id: '1234', title: 'Title', @@ -521,7 +533,7 @@ pbjs.setConfig({ ## AdUnit First Party Data "ortb2Imp" Most DSPs are adopting the Global Placement ID (GPID). -Please pass your placement specific GPID value to Yahoo SSP using `adUnit.ortb2Imp.ext.data.pbadslot`. +Please pass your placement specific GPID value by setting `adUnit.ortb2Imp.ext.data.pbadslot`. ```javascript const adUnits = [{ code: 'placement', @@ -541,7 +553,7 @@ const adUnits = [{ } }, bids: [{ - bidder: 'yahoossp', + bidder: 'yahooAds', params: { pubdId: 'DemoPublisher' } @@ -551,7 +563,7 @@ const adUnits = [{ ``` # Optional: Bidder bidOverride Parameters -The yahoossp adapter allows passing override data to the outbound bid-request in that overrides First Party Data. +The Yahoo Advertising bid adapter allows passing override data to the outbound bid-request that overrides First Party Data. **Important!** We highly recommend using prebid modules to pass data instead of bidder speicifc overrides. The use of these parameters are a last resort to force a specific feature or use case in your implementation. @@ -577,7 +589,6 @@ Currently the bidOverride object only accepts the following: * device * ip - ```javascript const adUnits = [{ code: 'bidOverride-adUnit', @@ -590,7 +601,7 @@ const adUnits = [{ } }, bids: [{ - bidder: 'yahoossp', + bidder: 'yahooAds', params: { dcn: '8a969516017a7a396ec539d97f540011', pos: '8a96958a017a7a57ac375d50c0c700cc', @@ -615,7 +626,7 @@ const adUnits = [{ } }, site: { - page: 'https://yahoossp-bid-adapter.com', + page: 'https://yahooAdvertising-bid-adapter.com', }, device: { ip: "1.2.3.4" @@ -630,7 +641,7 @@ const adUnits = [{ Custom key-value paris can be used for both inventory targeting and reporting. You must set up key-value pairs in the Yahoo SSP before sending them via the adapter otherwise the Ad Server will not be listening and picking them up. -Important! Key-value pairs can only contain values of types: String, Number, Array of strings OR Array of numbers +Important! Key-value pairs can only contain values of the following data types: String, Number, Array of strings OR Array of numbers ```javascript const adUnits = [{ @@ -644,7 +655,7 @@ const adUnits = [{ } }, bids: [{ - bidder: 'yahoossp', + bidder: 'yahooAds', params: { dcn: '8a969516017a7a396ec539d97f540011', pos: '8a96958a017a7a57ac375d50c0c700cc', @@ -660,14 +671,14 @@ const adUnits = [{ ``` # Optional: Custom Cache Time To Live (ttl): -The yahoossp adapter supports passing of "Time To Live" (ttl) that indicates to prebid chache for how long to keep the chaced winning bid alive. Value is Number in seconds and you can enter any number between 1 - 3600 (seconds). -The setting can be defined globally using setConfig or within the adUnit.params. -Global level setConfig overrides adUnit.params. +The Yahoo Advertising bid adapter supports passing of "Time To Live" (ttl) to indicate to prebid how long the bid response from Yahoo Advertising should be retained by Prebid for. This configuration value must be a Number in seconds, with the valid range being 1 - 3600 inclusive. +The setting can be defined globally using `setConfig` or within the adUnit.params. +Global level `setConfig` overrides adUnit.params. If no value is being passed default is 300 seconds. ## Global TTL ```javascript pbjs.setConfig({ - yahoossp: { + yahooAds: { ttl: 300 } }); @@ -683,7 +694,7 @@ const adUnits = [{ } }, bids: [{ - bidder: 'yahoossp', + bidder: 'yahooAds', params: { dcn: '8a969516017a7a396ec539d97f540011', pos: '8a96958a017a7a57ac375d50c0c700cc', @@ -702,7 +713,7 @@ const adUnits = [{ } }, bids: [{ - bidder: 'yahoossp', + bidder: 'yahooAds', params: { dcn: '8a969516017a7a396ec539d97f540011', pos: '8a96958a017a7a57ac375d50c0c700cc', @@ -710,10 +721,11 @@ const adUnits = [{ } }] }] + ``` # Optional: Video Features ## Rewarded video flag -To indicate to Yahoo SSP that this adUnit is a rewarded video you can pass the following in the params.bidOverride.imp.video.rewarded: 1 +To indicate to Yahoo Advertising that this adUnit is a rewarded video you can set the `params.bidOverride.imp.video.rewarded` property to `1` ```javascript const adUnits = [{ @@ -727,7 +739,7 @@ const adUnits = [{ } }, bids: [{ - bidder: 'yahoossp', + bidder: 'yahooAds', params: { dcn: '8a969516017a7a396ec539d97f540011', pos: '8a96958a017a7a57ac375d50c0c700cc', @@ -744,7 +756,7 @@ const adUnits = [{ ``` ## Site/App Targeting for "pubId" Inventory Mapping -To target your adUnit explicitly to a specific Site/App Object in Yahoo SSP, you can pass one of the following: +To target your adUnit explicitly to a specific Site/App Object in Yahoo Advertising, you can pass one of the following: 1. params.siteId = External Site ID || Video SSP RTBIS Id (in String format). 2. params.bidOverride.site.id = External Site ID || Video SSP RTBIS Id (in String format). **Important:** Site override is a only supported when using "pubId" mode. @@ -762,7 +774,7 @@ const adUnits = [{ } }, bids: [{ - bidder: 'yahoossp', + bidder: 'yahooAds', params: { pubId: 'DemoPublisher', siteId: '1234567'; @@ -772,13 +784,13 @@ const adUnits = [{ ``` ## Placement Targeting for "pubId" Inventory Mapping -To target your adUnit explicitly to a specific Placement within a Site/App Object in Yahoo SSP, you can pass the following params.placementId = External Placement ID || Placement Alias +To target your adUnit explicitly to a specific Placement within a Site/App Object in Yahoo Advertising, you can pass the following params.placementId = External Placement ID || Placement Alias **Important!** Placement override is a only supported when using "pubId" mode. -**Important!** It is highly recommended that you pass both `siteId` AND `placementId` together to avoid inventory miss matching. +**Important!** It is highly recommended that you pass both `siteId` AND `placementId` together to avoid inventory mismatching. ### Site & Placement override -**Important!** If the placement ID does not reside under the defined Site/App object, the request will not resolve and no response will be sent back from the ad-server. +**Important!** If the placement ID does not reside under the defined Site/App object, the request will not resolve and no response will be sent back from the bid-server. ```javascript const adUnits = [{ code: 'pubId-site-targeting-adUnit', @@ -791,7 +803,7 @@ const adUnits = [{ } }, bids: [{ - bidder: 'yahoossp', + bidder: 'yahooAds', params: { pubId: 'DemoPublisher', siteId: '1234567', @@ -801,7 +813,7 @@ const adUnits = [{ }] ``` ### Placement only override -**Important!** Using this method is not advised if you have multiple Site/Apps that are broken out of a Run Of Network (RON) Site/App. If the placement ID does not reside under a matching Site/App object, the request will not resolve and no response will be sent back from the ad-server. +**Important!** Using this method is not advised if you have multiple Site/Apps that are broken out of a Run Of Network (RON) Site/App. If the placement ID does not reside under a matching Site/App object, the request will not resolve and no response will be sent back from the bid-server. ```javascript const adUnits = [{ code: 'pubId-site-targeting-adUnit', @@ -814,7 +826,7 @@ const adUnits = [{ } }, bids: [{ - bidder: 'yahoossp', + bidder: 'yahooAds', params: { pubId: 'DemoPublisher', placementId: 'header-250x300' @@ -824,9 +836,8 @@ const adUnits = [{ ``` # Optional: Legacy override Parameters -This adapter does not support passing legacy overrides via 'bidder.params.ext' since most of the data should be passed using prebid modules (First Party Data, Schain, Price Floors etc.). +This adapter does not support passing legacy overrides via `bidder.params.ext` since most of the data should be passed using prebid modules (First Party Data, Schain, Price Floors etc.). If you do not know how to pass a custom parameter that you previously used, please contact us using the information provided above. Thank you, -Yahoo SSP - +Yahoo Advertsing diff --git a/modules/yandexBidAdapter.js b/modules/yandexBidAdapter.js index 2870229f1db..cb1cbd89c84 100644 --- a/modules/yandexBidAdapter.js +++ b/modules/yandexBidAdapter.js @@ -1,12 +1,58 @@ -import { formatQS, deepAccess, triggerPixel, _each, _map } from '../src/utils.js'; +import { formatQS, deepAccess, deepSetValue, triggerPixel, _each, _map } from '../src/utils.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; import { BANNER, NATIVE } from '../src/mediaTypes.js' import { convertOrtbRequestToProprietaryNative } from '../src/native.js'; +import { config } from '../src/config.js'; + +/** + * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + * @typedef {import('../src/adapters/bidderFactory.js').BidderSpec} BidderSpec + * @typedef {import('../src/adapters/bidderFactory.js').ServerRequest} ServerRequest + * @typedef {import('../src/adapters/bidderFactory.js').ServerResponse} ServerResponse + * @typedef {import('../src/adapters/bidderFactory.js').SyncOptions} SyncOptions + * @typedef {import('../src/adapters/bidderFactory.js').UserSync} UserSync + * @typedef {import('../src/auction.js').BidderRequest} BidderRequest + * @typedef {import('../src/mediaTypes.js').MediaType} MediaType + * @typedef {import('../src/utils.js').MediaTypes} MediaTypes + * @typedef {import('../modules/priceFloors.js').getFloor} GetFloor + */ + +/** + * @typedef {Object} CustomServerRequestFields + * @property {BidRequest} bidRequest + */ + +/** + * @typedef {ServerRequest & CustomServerRequestFields} YandexServerRequest + */ + +/** + * Yandex bidder-specific params which the publisher used in their bid request. + * + * @typedef {Object} YandexBidRequestParams + * @property {string} placementId Possible formats: `R-I-123456-2`, `R-123456-1`, `123456-789`. + * @property {number} [pageId] Deprecated. Please use `placementId` instead. + * @property {number} [impId] Deprecated. Please use `placementId` instead. + */ + +/** + * @typedef {Object} AdditionalBidRequestFields + * @property {GetFloor} [getFloor] + * @property {MediaTypes} [mediaTypes] + */ + +/** + * @typedef {BidRequest & AdditionalBidRequestFields} ExtendedBidRequest + */ const BIDDER_CODE = 'yandex'; const BIDDER_URL = 'https://bs.yandex.ru/prebid'; const DEFAULT_TTL = 180; const DEFAULT_CURRENCY = 'EUR'; +/** + * @type {MediaType[]} + */ const SUPPORTED_MEDIA_TYPES = [ BANNER, NATIVE ]; const SSP_ID = 10500; @@ -41,11 +87,18 @@ export const NATIVE_ASSETS = { const NATIVE_ASSETS_IDS = {}; _each(NATIVE_ASSETS, (asset, key) => { NATIVE_ASSETS_IDS[asset[0]] = key }); +/** @type {BidderSpec} */ export const spec = { code: BIDDER_CODE, aliases: ['ya'], // short code supportedMediaTypes: SUPPORTED_MEDIA_TYPES, + /** + * Determines whether or not the given bid request is valid. + * + * @param {BidRequest} bid The bid request to validate. + * @returns {boolean} True if this is a valid bid, and false otherwise. + */ isBidRequestValid: function(bid) { const { params } = bid; if (!params) { @@ -58,6 +111,13 @@ export const spec = { return true; }, + /** + * Make a server request from the list of BidRequests. + * + * @param {ExtendedBidRequest[]} validBidRequests An array of bids. + * @param {BidderRequest} bidderRequest Bidder request object. + * @returns {YandexServerRequest[]} Objects describing the requests to the server. + */ buildRequests: function(validBidRequests, bidderRequest) { validBidRequests = convertOrtbRequestToProprietaryNative(validBidRequests); @@ -76,9 +136,11 @@ export const spec = { timeout = bidderRequest.timeout; } + const adServerCurrency = config.getConfig('currency.adServerCurrency'); + return validBidRequests.map((bidRequest) => { const { params } = bidRequest; - const { targetRef, withCredentials = true } = params; + const { targetRef, withCredentials = true, cur } = params; const { pageId, impId } = extractPlacementIds(params); @@ -107,20 +169,37 @@ export const spec = { imp.bidfloorcur = bidfloor.currency; } + const currency = cur || adServerCurrency; + if (currency) { + queryParams['ssp-cur'] = currency; + } + + const data = { + id: bidRequest.bidId, + imp: [imp], + site: { + ref: referrer, + page, + domain, + }, + tmax: timeout, + }; + + const eids = deepAccess(bidRequest, 'userIdAsEids'); + if (eids && eids.length) { + deepSetValue(data, 'user.ext.eids', eids); + } + + const userData = deepAccess(bidRequest, 'ortb2.user.data'); + if (userData && userData.length) { + deepSetValue(data, 'user.data', userData); + } + const queryParamsString = formatQS(queryParams); return { method: 'POST', url: BIDDER_URL + `/${pageId}?${queryParamsString}`, - data: { - id: bidRequest.bidId, - imp: [imp], - site: { - ref: referrer, - page, - domain, - }, - tmax: timeout, - }, + data, options: { withCredentials, }, @@ -131,28 +210,24 @@ export const spec = { interpretResponse: interpretResponse, - onBidWon: function (bid) { - let nurl = bid['nurl']; + /** + * Register bidder specific code, which will execute if a bid from this bidder won the auction. + * @param {Bid} bid The bid that won the auction. + */ + onBidWon: function(bid) { + const nurl = addRTT(bid['nurl'], bid.timeToRespond); if (!nurl) { return; } - let cpm, currency; - if (bid.hasOwnProperty('originalCurrency') && bid.hasOwnProperty('originalCpm')) { - cpm = bid.originalCpm; - currency = bid.originalCurrency; - } else { - cpm = bid.cpm; - currency = bid.currency; - } - cpm = deepAccess(bid, 'adserverTargeting.hb_pb') || cpm; - - const pixel = replaceAuctionPrice(nurl, cpm, currency); - triggerPixel(pixel); + triggerPixel(nurl); } } +/** + * @param {YandexBidRequestParams} bidRequestParams + */ function extractPlacementIds(bidRequestParams) { const { placementId } = bidRequestParams; const result = { pageId: null, impId: null }; @@ -187,6 +262,9 @@ function extractPlacementIds(bidRequestParams) { return result; } +/** + * @param {ExtendedBidRequest} bidRequest + */ function getBidfloor(bidRequest) { const floors = []; @@ -206,6 +284,9 @@ function getBidfloor(bidRequest) { return floors.sort((a, b) => b.floor - a.floor)[0]; } +/** + * @param {ExtendedBidRequest} bidRequest + */ function mapBanner(bidRequest) { if (deepAccess(bidRequest, 'mediaTypes.banner')) { const sizes = bidRequest.sizes || bidRequest.mediaTypes.banner.sizes; @@ -223,6 +304,9 @@ function mapBanner(bidRequest) { } } +/** + * @param {ExtendedBidRequest} bidRequest + */ function mapNative(bidRequest) { const adUnitNativeAssets = deepAccess(bidRequest, 'mediaTypes.native'); if (adUnitNativeAssets) { @@ -295,6 +379,13 @@ function mapImageAsset(adUnitImageAssetParams, nativeAssetType) { return img; } +/** + * Unpack the response from the server into a list of bids. + * + * @param {ServerResponse} serverResponse A successful response from the server. + * @param {YandexServerRequest} yandexServerRequest + * @return {Bid[]} An array of bids which were nested inside the server. + */ function interpretResponse(serverResponse, { bidRequest }) { let response = serverResponse.body; if (!response.seatbid) { @@ -309,6 +400,7 @@ function interpretResponse(serverResponse, { bidRequest }) { return bidsReceived.map(bidReceived => { const price = bidReceived.price; + /** @type {Bid} */ let prBid = { requestId: bidRequest.bidId, cpm: price, @@ -316,7 +408,7 @@ function interpretResponse(serverResponse, { bidRequest }) { width: bidReceived.w, height: bidReceived.h, creativeId: bidReceived.adid, - nurl: bidReceived.nurl, + nurl: replaceAuctionPrice(bidReceived.nurl, price, currency), netRevenue: true, ttl: DEFAULT_TTL, @@ -381,4 +473,24 @@ function replaceAuctionPrice(url, price, currency) { .replace(/\${AUCTION_CURRENCY}/, currency); } +function addRTT(url, rtt) { + if (!url) return; + + if (url.indexOf(`\${RTT}`) > -1) { + return url.replace(/\${RTT}/, rtt ?? -1); + } + + const urlObj = new URL(url); + + if (Number.isInteger(rtt)) { + urlObj.searchParams.set('rtt', rtt); + } else { + urlObj.searchParams.delete('rtt'); + } + + url = urlObj.toString(); + + return url; +} + registerBidder(spec); diff --git a/modules/yieldlabBidAdapter.js b/modules/yieldlabBidAdapter.js index b0136cd21ea..e4fba5beb62 100644 --- a/modules/yieldlabBidAdapter.js +++ b/modules/yieldlabBidAdapter.js @@ -5,6 +5,15 @@ import { BANNER, NATIVE, VIDEO } from '../src/mediaTypes.js'; import { Renderer } from '../src/Renderer.js'; import { convertOrtbRequestToProprietaryNative } from '../src/native.js'; +/** + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid + * @typedef {import('../src/adapters/bidderFactory.js').ServerResponse} ServerResponse + * @typedef {import('../src/adapters/bidderFactory.js').ServerRequest} ServerRequest + * @typedef {import('../src/adapters/bidderFactory.js').SyncOptions} SyncOptions + * @typedef {import('../src/adapters/bidderFactory.js').UserSync} UserSync + */ + const ENDPOINT = 'https://ad.yieldlab.net'; const BIDDER_CODE = 'yieldlab'; const BID_RESPONSE_TTL_SEC = 300; diff --git a/modules/yieldloveBidAdapter.js b/modules/yieldloveBidAdapter.js new file mode 100644 index 00000000000..4568206b20a --- /dev/null +++ b/modules/yieldloveBidAdapter.js @@ -0,0 +1,149 @@ +import {registerBidder} from '../src/adapters/bidderFactory.js'; +import * as utils from '../src/utils.js'; +import {BANNER} from '../src/mediaTypes.js'; + +const ENDPOINT_URL = 'https://s2s.yieldlove-ad-serving.net/openrtb2/auction'; + +const DEFAULT_BID_TTL = 300; /* 5 minutes */ +const DEFAULT_CURRENCY = 'EUR'; + +const participatedBidders = [] + +export const spec = { + gvlid: 251, + code: 'yieldlove', + aliases: [], + supportedMediaTypes: [BANNER], + + isBidRequestValid: function (bid) { + return !!(bid.params.pid && bid.params.rid) + }, + + buildRequests: function (validBidRequests, bidderRequest) { + const anyValidBidRequest = validBidRequests[0] + + const impressions = validBidRequests.map(bidRequest => { + return { + ext: { + prebid: { + storedrequest: { + id: bidRequest.params.pid?.toString() + } + } + }, + banner: { + format: bidRequest.sizes.map(sizeArr => ({ + w: sizeArr[0], + h: sizeArr[1], + })) + }, + secure: 1, + id: bidRequest.bidId + } + }) + + const s2sRequest = { + device: { + ua: window.navigator.userAgent, + w: window.innerWidth, + h: window.innerHeight, + }, + site: { + ver: '1.9.0', + publisher: { + id: anyValidBidRequest.params.rid + }, + page: window.location.href, + domain: anyValidBidRequest.params.rid + }, + ext: { + prebid: { + targeting: {}, + cache: { + bids: {} + }, + storedrequest: { + id: anyValidBidRequest.params.rid + }, + } + }, + user: { + ext: { + consent: bidderRequest.gdprConsent?.consentString + }, + }, + id: utils.generateUUID(), + imp: impressions, + regs: { + ext: { + gdpr: 1 + } + } + } + + return { + method: 'POST', + url: ENDPOINT_URL, + data: s2sRequest, + options: { + contentType: 'text/plain', + withCredentials: true + }, + }; + }, + + interpretResponse: function (serverResponse) { + const bidResponses = [] + const seatBids = serverResponse.body?.seatbid || [] + seatBids.reduce((bids, cur) => { + if (cur.bid && cur.bid.length > 0) bids = bids.concat(cur.bid) + return bids + }, []).forEach(bid => { + bidResponses.push({ + requestId: bid.impid, + cpm: bid.price, + width: bid.w, + height: bid.h, + ad: bid.adm, + ttl: DEFAULT_BID_TTL, + creativeId: bid.crid, + netRevenue: true, + currency: DEFAULT_CURRENCY + }) + }) + + const bidders = serverResponse.body?.ext.responsetimemillis || {} + Object.keys(bidders).forEach(bidder => { + if (!participatedBidders.includes(bidder)) participatedBidders.push(bidder) + }) + + if (bidResponses.length === 0) { + utils.logInfo('interpretResponse :: no bid'); + } + + return bidResponses; + }, + + getUserSyncs: function (syncOptions, serverResponses, gdprConsent, uspConsent) { + const syncs = [] + + let gdprParams = '' + gdprParams = `gdpr=${Number(gdprConsent?.gdprApplies)}&` + gdprParams += `gdpr_consent=${gdprConsent?.consentString || ''}` + + let bidderParams = '' + if (participatedBidders.length > 0) { + bidderParams = `bidders=${participatedBidders.join(',')}` + } + + syncs.push({ + type: 'iframe', + url: `https://cdn-a.yieldlove.com/load-cookie.html?endpoint=yieldlove&max_sync_count=100&${gdprParams}&${bidderParams}` + }) + + return syncs + }, + +}; + +registerBidder(spec); diff --git a/modules/yieldloveBidAdapter.md b/modules/yieldloveBidAdapter.md new file mode 100644 index 00000000000..8fd71b55f88 --- /dev/null +++ b/modules/yieldloveBidAdapter.md @@ -0,0 +1,38 @@ +# Overview + +``` +Module Name: Yieldlove Bid Adapter +Module Type: Bidder Adapter +Maintainer: adapter@yieldlove.com +``` + + +# Description + +Connects to **[Yieldlove](https://www.yieldlove.com/)**s S2S platform for bids. + +```js +const adUnits = [ + { + code: 'test-div', + mediaTypes: { banner: { sizes: [[300, 250]] }}, + bids: [ + { + bidder: 'yieldlove', + params: { + pid: 34437, + rid: 'website.com' + } + } + ] + } +] +``` + + +# Bid Parameters + +| Name | Scope | Description | Example | Type | +|---------------|--------------|---------------------------------------------------------|----------------------------|--------------| +| rid | **required** | Publisher ID on the Yieldlove platform | `website.com` | String | +| pid | **required** | Placement ID on the Yieldlove platform | `34437` | Number | diff --git a/modules/yieldmoBidAdapter.js b/modules/yieldmoBidAdapter.js index 40276aa7a6c..78c2e7430e7 100644 --- a/modules/yieldmoBidAdapter.js +++ b/modules/yieldmoBidAdapter.js @@ -18,6 +18,13 @@ import {registerBidder} from '../src/adapters/bidderFactory.js'; import {Renderer} from '../src/Renderer.js'; import {find, includes} from '../src/polyfill.js'; +/** + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid + * @typedef {import('../src/adapters/bidderFactory.js').ServerResponse} ServerResponse + * @typedef {import('../src/adapters/bidderFactory.js').ServerRequest} ServerRequest + */ + const BIDDER_CODE = 'yieldmo'; const GVLID = 173; const CURRENCY = 'USD'; @@ -29,7 +36,7 @@ const VIDEO_PATH = '/exchange/prebidvideo'; const STAGE_DOMAIN = 'https://ads-stg.yieldmo.com'; const PROD_DOMAIN = 'https://ads.yieldmo.com'; const OUTSTREAM_VIDEO_PLAYER_URL = 'https://prebid-outstream.yieldmo.com/bundle.js'; -const OPENRTB_VIDEO_BIDPARAMS = ['mimes', 'startdelay', 'placement', 'startdelay', 'skipafter', 'protocols', 'api', +const OPENRTB_VIDEO_BIDPARAMS = ['mimes', 'startdelay', 'placement', 'plcmt', 'skipafter', 'protocols', 'api', 'playbackmethod', 'maxduration', 'minduration', 'pos', 'skip', 'skippable']; const OPENRTB_VIDEO_SITEPARAMS = ['name', 'domain', 'cat', 'keywords']; const LOCAL_WINDOW = getWindowTop(); @@ -78,14 +85,17 @@ export const spec = { bust: new Date().getTime().toString(), dnt: getDNT(), description: getPageDescription(), + tmax: bidderRequest.timeout || 400, userConsent: JSON.stringify({ // case of undefined, stringify will remove param - gdprApplies: deepAccess(bidderRequest, 'gdprConsent.gdprApplies') || '', + gdprApplies: + deepAccess(bidderRequest, 'gdprConsent.gdprApplies') || '', cmp: deepAccess(bidderRequest, 'gdprConsent.consentString') || '', gpp: deepAccess(bidderRequest, 'gppConsent.gppString') || '', - gpp_sid: deepAccess(bidderRequest, 'gppConsent.applicableSections') || [] + gpp_sid: + deepAccess(bidderRequest, 'gppConsent.applicableSections') || [], }), - us_privacy: deepAccess(bidderRequest, 'uspConsent') || '' + us_privacy: deepAccess(bidderRequest, 'uspConsent') || '', }; if (canAccessTopWindow()) { @@ -432,12 +442,12 @@ function openRtbImpression(bidRequest) { } }; - const mediaTypesParams = deepAccess(bidRequest, 'mediaTypes.video'); + const mediaTypesParams = deepAccess(bidRequest, 'mediaTypes.video', {}); Object.keys(mediaTypesParams) .filter(param => includes(OPENRTB_VIDEO_BIDPARAMS, param)) .forEach(param => imp.video[param] = mediaTypesParams[param]); - const videoParams = deepAccess(bidRequest, 'params.video'); + const videoParams = deepAccess(bidRequest, 'params.video', {}); Object.keys(videoParams) .filter(param => includes(OPENRTB_VIDEO_BIDPARAMS, param)) .forEach(param => imp.video[param] = videoParams[param]); @@ -446,7 +456,7 @@ function openRtbImpression(bidRequest) { imp.video.skip = 1; delete imp.video.skippable; } - if (imp.video.placement !== 1) { + if (imp.video.plcmt !== 1 || imp.video.placement !== 1) { imp.video.startdelay = DEFAULT_START_DELAY; imp.video.playbackmethod = [ DEFAULT_PLAYBACK_METHOD ]; } @@ -607,8 +617,14 @@ function validateVideoParams(bid) { } validate('video.protocols', val => isDefined(val), paramRequired); - validate('video.protocols', val => isArrayOfNums(val) && val.every(v => (v >= 1 && v <= 6)), - paramInvalid, 'array of numbers, ex: [2,3]'); + validate( + 'video.protocols', + (val) => + isArrayOfNums(val) && + val.every((v) => v >= 1 && v <= 12 && v != 9 && v != 10), // 9 and 10 are for DAST which are not supported. + paramInvalid, + 'array of numbers between 1 and 12 except for 9 or 10 , ex: [2,3, 7, 11]' + ); validate('video.api', val => isDefined(val), paramRequired); validate('video.api', val => isArrayOfNums(val) && val.every(v => (v >= 1 && v <= 6)), diff --git a/modules/zeotapIdPlusIdSystem.js b/modules/zeotapIdPlusIdSystem.js index 4cb827cbdff..32b7a5b77ad 100644 --- a/modules/zeotapIdPlusIdSystem.js +++ b/modules/zeotapIdPlusIdSystem.js @@ -9,6 +9,11 @@ import {submodule} from '../src/hook.js'; import {getStorageManager} from '../src/storageManager.js'; import {MODULE_TYPE_UID} from '../src/activities/modules.js'; +/** + * @typedef {import('../modules/userId/index.js').Submodule} Submodule + * @typedef {import('../modules/userId/index.js').SubmoduleConfig} SubmoduleConfig + */ + const ZEOTAP_COOKIE_NAME = 'IDP'; const ZEOTAP_VENDOR_ID = 301; const ZEOTAP_MODULE_NAME = 'zeotapIdPlus'; @@ -60,6 +65,12 @@ export const zeotapIdPlusSubmodule = { getId() { const id = readCookie() || readFromLocalStorage(); return id ? { id } : undefined; + }, + eids: { + 'IDP': { + source: 'zeotap.com', + atype: 1 + }, } }; submodule('userId', zeotapIdPlusSubmodule); diff --git a/modules/zetaBidAdapter.js b/modules/zetaBidAdapter.js index 527030efc9a..cc77d53d30d 100644 --- a/modules/zetaBidAdapter.js +++ b/modules/zetaBidAdapter.js @@ -2,6 +2,14 @@ import {deepAccess, logWarn} from '../src/utils.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import {BANNER} from '../src/mediaTypes.js'; +/** + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid + * @typedef {import('../src/adapters/bidderFactory.js').ServerResponse} ServerResponse + * @typedef {import('../src/adapters/bidderFactory.js').SyncOptions} SyncOptions + * @typedef {import('../src/adapters/bidderFactory.js').UserSync} UserSync + */ + const BIDDER_CODE = 'zeta_global'; const PREBID_DEFINER_ID = '44253' const ENDPOINT_URL = 'https://prebid.rfihub.com/prebid'; @@ -15,11 +23,11 @@ export const spec = { 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. - */ + * Determines whether or not the given bid request is valid. + * + * @param {BidRequest} bid The bid params to validate. + * @return boolean True if this is a valid bid, and false otherwise. + */ isBidRequestValid: function(bid) { // check for all required bid fields if (!(bid && @@ -50,12 +58,12 @@ export const spec = { }, /** - * Make a server request from the list of BidRequests. - * - * @param {Bids[]} validBidRequests - an array of bidRequest objects - * @param {BidderRequest} bidderRequest - master bidRequest object - * @return ServerRequest Info describing the request to the server. - */ + * Make a server request from the list of BidRequests. + * + * @param {Bids[]} validBidRequests - an array of bidRequest objects + * @param {BidderRequest} bidderRequest - master bidRequest object + * @return ServerRequest Info describing the request to the server. + */ buildRequests: function(validBidRequests, bidderRequest) { const secure = 1; // treat all requests as secure const request = validBidRequests[0]; @@ -117,12 +125,12 @@ export const spec = { }, /** - * Unpack the response from the server into a list of bids. - * - * @param {ServerResponse} serverResponse A successful response from the server. - * @param bidRequest The payload from the server's response. - * @return {Bid[]} An array of bids which were nested inside the server. - */ + * Unpack the response from the server into a list of bids. + * + * @param {ServerResponse} serverResponse A successful response from the server. + * @param bidRequest The payload from the server's response. + * @return {Bid[]} An array of bids which were nested inside the server. + */ interpretResponse: function(serverResponse, bidRequest) { let bidResponse = []; if (Object.keys(serverResponse.body).length !== 0) { diff --git a/modules/zeta_global_sspAnalyticsAdapter.js b/modules/zeta_global_sspAnalyticsAdapter.js index ee3bb9cd5d6..751b55ec673 100644 --- a/modules/zeta_global_sspAnalyticsAdapter.js +++ b/modules/zeta_global_sspAnalyticsAdapter.js @@ -1,4 +1,4 @@ -import { logInfo, logError } from '../src/utils.js'; +import {logInfo, logError} from '../src/utils.js'; import { ajax } from '../src/ajax.js'; import adapterManager from '../src/adapterManager.js'; import CONSTANTS from '../src/constants.json'; @@ -10,6 +10,10 @@ const ADAPTER_CODE = 'zeta_global_ssp'; const BASE_URL = 'https://ssp.disqus.com/prebid/event'; const LOG_PREFIX = 'ZetaGlobalSsp-Analytics: '; +const cache = { + auctions: {} +}; + /// /////////// VARIABLES //////////////////////////////////// let publisherId; // int @@ -24,20 +28,87 @@ function sendEvent(eventType, event) { ); } +function getZetaParams(event) { + if (event.adUnits) { + for (const i in event.adUnits) { + const unit = event.adUnits[i]; + if (unit.bids) { + for (const j in unit.bids) { + const bid = unit.bids[j]; + if (bid.bidder === ADAPTER_CODE && bid.params) { + return bid.params; + } + } + } + } + } + return null; +} + /// /////////// ADAPTER EVENT HANDLER FUNCTIONS ////////////// function adRenderSucceededHandler(args) { let eventType = CONSTANTS.EVENTS.AD_RENDER_SUCCEEDED logInfo(LOG_PREFIX + 'handle ' + eventType + ' event'); - sendEvent(eventType, args); + const event = { + adId: args.adId, + bid: { + adId: args.bid?.adId, + auctionId: args.bid?.auctionId, + adUnitCode: args.bid?.adUnitCode, + bidId: args.bid?.bidId, + requestId: args.bid?.requestId, + bidderCode: args.bid?.bidderCode, + mediaTypes: args.bid?.mediaTypes, + sizes: args.bid?.sizes, + adserverTargeting: args.bid?.adserverTargeting, + cpm: args.bid?.cpm, + creativeId: args.bid?.creativeId, + mediaType: args.bid?.mediaType, + renderer: args.bid?.renderer, + size: args.bid?.size, + timeToRespond: args.bid?.timeToRespond, + params: args.bid?.params + }, + doc: { + location: args.doc?.location + } + } + + // set zetaParams from cache + if (event.bid && event.bid.auctionId) { + const zetaParams = cache.auctions[event.bid.auctionId]; + if (zetaParams) { + event.bid.params = [ zetaParams ]; + } + } + + sendEvent(eventType, event); } function auctionEndHandler(args) { let eventType = CONSTANTS.EVENTS.AUCTION_END; logInfo(LOG_PREFIX + 'handle ' + eventType + ' event'); - sendEvent(eventType, args); + const event = { + adUnitCodes: args.adUnitCodes, + adUnits: args.adUnits, + auctionEnd: args.auctionEnd, + auctionId: args.auctionId, + bidderRequests: args.bidderRequests, + bidsReceived: args.bidsReceived, + noBids: args.noBids, + winningBids: args.winningBids + } + + // save zetaParams to cache + const zetaParams = getZetaParams(event); + if (zetaParams && event.auctionId) { + cache.auctions[event.auctionId] = zetaParams; + } + + sendEvent(eventType, event); } /// /////////// ADAPTER DEFINITION /////////////////////////// diff --git a/modules/zeta_global_sspBidAdapter.js b/modules/zeta_global_sspBidAdapter.js index 687afb6c692..68cf7c4e4cd 100644 --- a/modules/zeta_global_sspBidAdapter.js +++ b/modules/zeta_global_sspBidAdapter.js @@ -5,6 +5,12 @@ import {config} from '../src/config.js'; import {parseDomain} from '../src/refererDetection.js'; import {ajax} from '../src/ajax.js'; +/** + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid + * @typedef {import('../src/adapters/bidderFactory.js').ServerResponse} ServerResponse + */ + const BIDDER_CODE = 'zeta_global_ssp'; const ENDPOINT_URL = 'https://ssp.disqus.com/bid/prebid'; const TIMEOUT_URL = 'https://ssp.disqus.com/timeout/prebid'; @@ -45,7 +51,7 @@ export const spec = { supportedMediaTypes: [BANNER, VIDEO], /** - * Determines whether or not the given bid request is valid. + * Determines whether the given bid request is valid. * * @param {BidRequest} bid The bid params to validate. * @return boolean True if this is a valid bid, and false otherwise. @@ -54,7 +60,8 @@ export const spec = { // check for all required bid fields if (!(bid && bid.bidId && - bid.params)) { + bid.params && + bid.params.sid)) { logWarn('Invalid bid request - missing required bid data'); return false; } @@ -162,7 +169,8 @@ export const spec = { } provideEids(validBidRequests[0], payload); - const url = params.shortname ? ENDPOINT_URL.concat('?shortname=', params.shortname) : ENDPOINT_URL; + provideSegments(bidderRequest, payload); + const url = params.sid ? ENDPOINT_URL.concat('?sid=', params.sid) : ENDPOINT_URL; return { method: 'POST', url: url, @@ -334,6 +342,25 @@ function provideEids(request, payload) { } } +function provideSegments(bidderRequest, payload) { + const data = bidderRequest.ortb2?.user?.data; + if (isArray(data)) { + const segments = data.filter(d => d?.segment).map(d => d.segment).filter(s => isArray(s)).flatMap(s => s).filter(s => s?.id); + if (segments.length > 0) { + if (!payload.user) { + payload.user = {}; + } + if (!isArray(payload.user.data)) { + payload.user.data = []; + } + const payloadData = { + segment: segments + }; + payload.user.data.push(payloadData); + } + } +} + function provideMediaType(zetaBid, bid, bidRequest) { if (zetaBid.ext && zetaBid.ext.prebid && zetaBid.ext.prebid.type) { bid.mediaType = zetaBid.ext.prebid.type === VIDEO ? VIDEO : BANNER; diff --git a/modules/zmaticooBidAdapter.js b/modules/zmaticooBidAdapter.js new file mode 100644 index 00000000000..8566d58c6de --- /dev/null +++ b/modules/zmaticooBidAdapter.js @@ -0,0 +1,141 @@ +import {deepAccess, logWarn} from '../src/utils.js'; +import {registerBidder} from '../src/adapters/bidderFactory.js'; +import {BANNER} from '../src/mediaTypes.js'; + +const BIDDER_CODE = 'zmaticoo'; +const ENDPOINT_URL = 'https://bid.zmaticoo.com/prebid/bid'; +const DEFAULT_CUR = 'USD'; +const TTL = 200; +const NET_REV = true; + +export const spec = { + code: BIDDER_CODE, + supportedMediaTypes: [BANNER], + + /** + * Determines whether or not the given bid request is valid. + * + * @param {BidRequest} bid The bid params to validate. + * @return boolean True if this is a valid bid, and false otherwise. + */ + isBidRequestValid: function (bid) { + // check for all required bid fields + if (!(bid && bid.bidId && bid.params)) { + logWarn('Invalid bid request - missing required bid data'); + return false; + } + + if (!(bid.params.pubId)) { + logWarn('Invalid bid request - missing required field pubId'); + return false; + } + + if (!(bid.params.device && bid.params.device.ip)) { + logWarn('Invalid bid request - missing required device data'); + return false; + } + return true; + }, + + /** + * Make a server request from the list of BidRequests. + * + * @param {Bids[]} validBidRequests - an array of bidRequest objects + * @param {BidderRequest} bidderRequest - master bidRequest object + * @return ServerRequest Info describing the request to the server. + */ + buildRequests: function (validBidRequests, bidderRequest) { + const secure = 1; + const request = validBidRequests[0]; + const params = request.params; + let impData = { + id: request.bidId, + secure: secure, + banner: buildBanner(request), + ext: { + bidder: { + pubId: params.pubId + } + } + }; + let payload = { + id: bidderRequest.bidderRequestId, + imp: [impData], + site: params.site ? params.site : {}, + app: params.app ? params.app : {}, + device: params.device ? params.device : {}, + user: params.user ? params.user : {}, + at: params.at, + tmax: params.tmax, + wseat: params.wseat, + bseat: params.bseat, + allimps: params.allimps, + cur: [DEFAULT_CUR], + wlang: params.wlang, + bcat: deepAccess(bidderRequest.ortb2Imp, 'bcat') || params.bcat, + badv: params.badv, + bapp: params.bapp, + source: params.source ? params.source : {}, + regs: params.regs ? params.regs : {}, + ext: params.ext ? params.ext : {} + }; + + payload.device.ua = navigator.userAgent; + payload.device.ip = navigator.ip; + payload.site.page = bidderRequest.refererInfo.page; + payload.site.mobile = /(ios|ipod|ipad|iphone|android)/i.test(navigator.userAgent) ? 1 : 0; + if (params.test) { + payload.test = params.test; + } + if (request.gdprConsent) { + payload.regs.ext = Object.assign(payload.regs.ext, {gdpr: request.gdprConsent.gdprApplies === true ? 1 : 0}); + } + if (request.gdprConsent && request.gdprConsent.gdprApplies) { + payload.user.ext = Object.assign(payload.user.ext, {consent: request.gdprConsent.consentString}); + } + const postUrl = ENDPOINT_URL; + return { + method: 'POST', url: postUrl, data: JSON.stringify(payload), + }; + }, + + /** + * Unpack the response from the server into a list of bids. + * + * @param {ServerResponse} serverResponse A successful response from the server. + * @param bidRequest The payload from the server's response. + * @return {Bid[]} An array of bids which were nested inside the server. + */ + interpretResponse: function (serverResponse, bidRequest) { + let bidResponse = []; + if (Object.keys(serverResponse.body).length !== 0) { + let zresponse = serverResponse.body; + let zbid = zresponse.seatbid[0].bid[0]; + let bid = { + requestId: zbid.impid, + cpm: zbid.price, + currency: zbid.cur, + width: zbid.w, + height: zbid.h, + ad: zbid.adm, + ttl: TTL, + creativeId: zbid.crid, + netRevenue: NET_REV + }; + bidResponse.push(bid); + } + return bidResponse; + } +} + +function buildBanner(request) { + let sizes = request.sizes; + if (request.mediaTypes && request.mediaTypes.banner && request.mediaTypes.banner.sizes) { + sizes = request.mediaTypes.banner.sizes; + } + return { + w: sizes[0][0], h: sizes[0][1] + }; +} + +registerBidder(spec); diff --git a/modules/zmaticooBidAdapter.md b/modules/zmaticooBidAdapter.md new file mode 100644 index 00000000000..95959ef218c --- /dev/null +++ b/modules/zmaticooBidAdapter.md @@ -0,0 +1,45 @@ +# Overview + +``` +Module Name: zMaticoo Bidder Adapter +Module Type: Bidder Adapter +Maintainer: adam.li@eclicktech.com.cn +``` + +# Description + +zMaticoo Bidder Adapter for Prebid.js. + +# Test Parameters + +``` + var adUnits = [ + { + mediaTypes: { + banner: { + sizes: [[320, 50]], // a display size + } + }, + bids: [ + { + bidder: 'zmaticoo', + bidId: '12345', + params: { + user: { + uid: '12345', + buyeruid: '12345' + }, + device: { + ip: '111.222.33.44', + geo: { + country: 'USA' + } + }, + pubId: 'prebid-fgh', + test: 1 + } + } + ] + } + ]; +``` diff --git a/package-lock.json b/package-lock.json index af7eda1ea6b..87110627d26 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "prebid.js", - "version": "8.1.0-pre", + "version": "8.34.0-pre", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "prebid.js", - "version": "7.51.0-pre", + "version": "8.32.0-pre", "license": "Apache-2.0", "dependencies": { "@babel/core": "^7.16.7", @@ -16,13 +16,13 @@ "core-js": "^3.13.0", "core-js-pure": "^3.13.0", "criteo-direct-rsa-validate": "^1.1.0", - "crypto-js": "^3.3.0", + "crypto-js": "^4.2.0", "dlv": "1.1.3", "dset": "3.1.2", "express": "^4.15.4", "fun-hooks": "^0.9.9", "just-clone": "^1.0.2", - "live-connect-js": "^5.0.0" + "live-connect-js": "^6.3.4" }, "devDependencies": { "@babel/eslint-parser": "^7.16.5", @@ -47,6 +47,7 @@ "eslint": "^7.27.0", "eslint-config-standard": "^10.2.1", "eslint-plugin-import": "^2.20.2", + "eslint-plugin-jsdoc": "^38.1.6", "eslint-plugin-node": "^11.1.0", "eslint-plugin-prebid": "file:./plugins/eslint", "eslint-plugin-promise": "^5.1.0", @@ -90,6 +91,7 @@ "lodash": "^4.17.21", "mocha": "^10.0.0", "morgan": "^1.10.0", + "node-html-parser": "^6.1.5", "opn": "^5.4.0", "resolve-from": "^5.0.0", "sinon": "^4.1.3", @@ -127,11 +129,12 @@ } }, "node_modules/@babel/code-frame": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.18.6.tgz", - "integrity": "sha512-TDCmlK5eOvH+eH7cdAFlNXeVJqWIQ7gW9tY1GJIpUtFb6CmjVyq2VM3u71bOyR8CRihcCgMUYoDNyLXao3+70Q==", + "version": "7.22.13", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.13.tgz", + "integrity": "sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==", "dependencies": { - "@babel/highlight": "^7.18.6" + "@babel/highlight": "^7.22.13", + "chalk": "^2.4.2" }, "engines": { "node": ">=6.9.0" @@ -193,12 +196,13 @@ } }, "node_modules/@babel/generator": { - "version": "7.20.1", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.20.1.tgz", - "integrity": "sha512-u1dMdBUmA7Z0rBB97xh8pIhviK7oItYOkjbsCxTWMknyvbQRBwX7/gn4JXurRdirWMFh+ZtYARqkA6ydogVZpg==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.0.tgz", + "integrity": "sha512-lN85QRR+5IbYrMWM6Y4pE/noaQtg4pNiqeNGX60eqOfo6gtEj6uw/JagelB8vVztSd7R6M5n1+PQkDbHbBRU4g==", "dependencies": { - "@babel/types": "^7.20.0", + "@babel/types": "^7.23.0", "@jridgewell/gen-mapping": "^0.3.2", + "@jridgewell/trace-mapping": "^0.3.17", "jsesc": "^2.5.1" }, "engines": { @@ -310,9 +314,9 @@ } }, "node_modules/@babel/helper-environment-visitor": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.18.9.tgz", - "integrity": "sha512-3r/aACDJ3fhQ/EVgFy0hpj8oHyHpQc+LPtJoY9SzTThAsStm4Ptegq92vqKoE3vD706ZVFWITnMnxucw+S9Ipg==", + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz", + "integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==", "engines": { "node": ">=6.9.0" } @@ -329,23 +333,23 @@ } }, "node_modules/@babel/helper-function-name": { - "version": "7.19.0", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.19.0.tgz", - "integrity": "sha512-WAwHBINyrpqywkUH0nTnNgI5ina5TFn85HKS0pbPDfxFfhyR/aNQEn4hGi1P1JyT//I0t4OgXUlofzWILRvS5w==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz", + "integrity": "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==", "dependencies": { - "@babel/template": "^7.18.10", - "@babel/types": "^7.19.0" + "@babel/template": "^7.22.15", + "@babel/types": "^7.23.0" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-hoist-variables": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.18.6.tgz", - "integrity": "sha512-UlJQPkFqFULIcyW5sbzgbkxn2FKRgwWiRexcuaR8RNJRy8+LLveqPjwZV/bwrLZCN0eUHD/x8D0heK1ozuoo6Q==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz", + "integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==", "dependencies": { - "@babel/types": "^7.18.6" + "@babel/types": "^7.22.5" }, "engines": { "node": ">=6.9.0" @@ -465,28 +469,28 @@ } }, "node_modules/@babel/helper-split-export-declaration": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.18.6.tgz", - "integrity": "sha512-bde1etTx6ZyTmobl9LLMMQsaizFVZrquTEHOqKeQESMKo4PlObf+8+JA25ZsIpZhT/WEd39+vOdLXAFG/nELpA==", + "version": "7.22.6", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz", + "integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==", "dependencies": { - "@babel/types": "^7.18.6" + "@babel/types": "^7.22.5" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-string-parser": { - "version": "7.19.4", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.19.4.tgz", - "integrity": "sha512-nHtDoQcuqFmwYNYPz3Rah5ph2p8PFeFCsZk9A/48dPc/rGocJ5J3hAAZ7pb76VWX3fZKu+uEr/FhH5jLx7umrw==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz", + "integrity": "sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.19.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz", - "integrity": "sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==", + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", + "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", "engines": { "node": ">=6.9.0" } @@ -527,12 +531,12 @@ } }, "node_modules/@babel/highlight": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.18.6.tgz", - "integrity": "sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g==", + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.20.tgz", + "integrity": "sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg==", "dependencies": { - "@babel/helper-validator-identifier": "^7.18.6", - "chalk": "^2.0.0", + "@babel/helper-validator-identifier": "^7.22.20", + "chalk": "^2.4.2", "js-tokens": "^4.0.0" }, "engines": { @@ -540,9 +544,9 @@ } }, "node_modules/@babel/parser": { - "version": "7.20.1", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.20.1.tgz", - "integrity": "sha512-hp0AYxaZJhxULfM1zyp7Wgr+pSUKBcP3M+PHnSzWGdXOzg/kHWIgiUWARvubhUKGOEw3xqY4x+lyZ9ytBVcELw==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.0.tgz", + "integrity": "sha512-vvPKKdMemU85V9WE/l5wZEmImpCtLqbnTvqDS2U1fJ96KrxoW7KrXhNsNCblQlg8Ck4b85yxdTyelsMUgFUXiw==", "bin": { "parser": "bin/babel-parser.js" }, @@ -1612,31 +1616,31 @@ } }, "node_modules/@babel/template": { - "version": "7.18.10", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.18.10.tgz", - "integrity": "sha512-TI+rCtooWHr3QJ27kJxfjutghu44DLnasDMwpDqCXVTal9RLp3RSYNh4NdBrRP2cQAoG9A8juOQl6P6oZG4JxA==", + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz", + "integrity": "sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==", "dependencies": { - "@babel/code-frame": "^7.18.6", - "@babel/parser": "^7.18.10", - "@babel/types": "^7.18.10" + "@babel/code-frame": "^7.22.13", + "@babel/parser": "^7.22.15", + "@babel/types": "^7.22.15" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/traverse": { - "version": "7.20.1", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.20.1.tgz", - "integrity": "sha512-d3tN8fkVJwFLkHkBN479SOsw4DMZnz8cdbL/gvuDuzy3TS6Nfw80HuQqhw1pITbIruHyh7d1fMA47kWzmcUEGA==", - "dependencies": { - "@babel/code-frame": "^7.18.6", - "@babel/generator": "^7.20.1", - "@babel/helper-environment-visitor": "^7.18.9", - "@babel/helper-function-name": "^7.19.0", - "@babel/helper-hoist-variables": "^7.18.6", - "@babel/helper-split-export-declaration": "^7.18.6", - "@babel/parser": "^7.20.1", - "@babel/types": "^7.20.0", + "version": "7.23.2", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.2.tgz", + "integrity": "sha512-azpe59SQ48qG6nu2CzcMLbxUudtN+dOM9kDbUqGq3HXUJRlo7i8fvPoxQUzYgLZ4cMVmuZgm8vvBpNeRhd6XSw==", + "dependencies": { + "@babel/code-frame": "^7.22.13", + "@babel/generator": "^7.23.0", + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-function-name": "^7.23.0", + "@babel/helper-hoist-variables": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "@babel/parser": "^7.23.0", + "@babel/types": "^7.23.0", "debug": "^4.1.0", "globals": "^11.1.0" }, @@ -1645,12 +1649,12 @@ } }, "node_modules/@babel/types": { - "version": "7.20.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.20.0.tgz", - "integrity": "sha512-Jlgt3H0TajCW164wkTOTzHkZb075tMQMULzrLUoUeKmO7eFL96GgDxf7/Axhc5CAuKE3KFyVW1p6ysKsi2oXAg==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.0.tgz", + "integrity": "sha512-0oIyUfKoI3mSqMvsxBdclDwxXKXAUA8v/apZbc+iSyARYou1o8ZGDxbUYyLFoW2arqS2jDGqJuZvv1d/io1axg==", "dependencies": { - "@babel/helper-string-parser": "^7.19.4", - "@babel/helper-validator-identifier": "^7.19.1", + "@babel/helper-string-parser": "^7.22.5", + "@babel/helper-validator-identifier": "^7.22.20", "to-fast-properties": "^2.0.0" }, "engines": { @@ -1666,6 +1670,20 @@ "node": ">=0.1.90" } }, + "node_modules/@es-joy/jsdoccomment": { + "version": "0.22.2", + "resolved": "https://registry.npmjs.org/@es-joy/jsdoccomment/-/jsdoccomment-0.22.2.tgz", + "integrity": "sha512-pM6WQKcuAtdYoqCsXSvVSu3Ij8K0HY50L8tIheOKHDl0wH1uA4zbP88etY8SIeP16NVCMCTFU+Q2DahSKheGGQ==", + "dev": true, + "dependencies": { + "comment-parser": "1.3.1", + "esquery": "^1.4.0", + "jsdoc-type-pratt-parser": "~2.2.5" + }, + "engines": { + "node": "^12 || ^14 || ^16 || ^17" + } + }, "node_modules/@eslint/eslintrc": { "version": "0.4.3", "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-0.4.3.tgz", @@ -6489,6 +6507,12 @@ "integrity": "sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ==", "dev": true }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "dev": true + }, "node_modules/brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -6847,9 +6871,9 @@ } }, "node_modules/cac/node_modules/semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", "dev": true, "bin": { "semver": "bin/semver" @@ -7480,6 +7504,15 @@ "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", "dev": true }, + "node_modules/comment-parser": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/comment-parser/-/comment-parser-1.3.1.tgz", + "integrity": "sha512-B52sN2VNghyq5ofvUsqZjmk6YkihBX5vMSChmSK9v4ShjKf3Vk5Xcmgpw4o+iIgtrnM/u5FiMpz9VKb8lpBveA==", + "dev": true, + "engines": { + "node": ">= 12.0.0" + } + }, "node_modules/commondir": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", @@ -7838,9 +7871,9 @@ } }, "node_modules/crypto-js": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-3.3.0.tgz", - "integrity": "sha512-DIT51nX0dCfKltpRiXV+/TVZq+Qq2NgF4644+K7Ttnla7zEzqc+kjJyiB96BHNyUTBxyjzRcZYpUdZa+QAqi6Q==" + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz", + "integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==" }, "node_modules/css": { "version": "3.0.0", @@ -7853,6 +7886,22 @@ "source-map-resolve": "^0.6.0" } }, + "node_modules/css-select": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz", + "integrity": "sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==", + "dev": true, + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.1.0", + "domhandler": "^5.0.2", + "domutils": "^3.0.1", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, "node_modules/css-shorthand-properties": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/css-shorthand-properties/-/css-shorthand-properties-1.1.1.tgz", @@ -7865,6 +7914,18 @@ "integrity": "sha512-FUV3xaJ63buRLgHrLQVlVgQnQdR4yqdLGaDu7g8CQcWjInDfM9plBTPI9FRfpahju1UBSaMckeb2/46ApS/V1Q==", "dev": true }, + "node_modules/css-what": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", + "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==", + "dev": true, + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, "node_modules/css/node_modules/source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -8691,12 +8752,67 @@ "void-elements": "^2.0.0" } }, + "node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "dev": true, + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, "node_modules/dom-walk": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/dom-walk/-/dom-walk-0.1.2.tgz", "integrity": "sha512-6QvTW9mrGeIegrFXdtQi9pk7O/nSK6lSdXW2eqUspN5LWD7UTji2Fqw5V2YLjBpHEoU9Xl/eUWNpDeZvoyOv2w==", "dev": true }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ] + }, + "node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "dev": true, + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz", + "integrity": "sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==", + "dev": true, + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, "node_modules/dset": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/dset/-/dset-3.1.2.tgz", @@ -8957,6 +9073,18 @@ "integrity": "sha512-GHrMyVZQWvTIdDtpiEXdHZnFQKzeO09apj8Cbl4pKWy4i0Oprcq17usfDt5aO63swf0JOeMWjWQE/LzgSRuWpA==", "dev": true }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "dev": true, + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/errno": { "version": "0.1.8", "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.8.tgz", @@ -9458,6 +9586,55 @@ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "dev": true }, + "node_modules/eslint-plugin-jsdoc": { + "version": "38.1.6", + "resolved": "https://registry.npmjs.org/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-38.1.6.tgz", + "integrity": "sha512-n4s95oYlg0L43Bs8C0dkzIldxYf8pLCutC/tCbjIdF7VDiobuzPI+HZn9Q0BvgOvgPNgh5n7CSStql25HUG4Tw==", + "dev": true, + "dependencies": { + "@es-joy/jsdoccomment": "~0.22.1", + "comment-parser": "1.3.1", + "debug": "^4.3.4", + "escape-string-regexp": "^4.0.0", + "esquery": "^1.4.0", + "regextras": "^0.8.0", + "semver": "^7.3.5", + "spdx-expression-parse": "^3.0.1" + }, + "engines": { + "node": "^12 || ^14 || ^16 || ^17" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0" + } + }, + "node_modules/eslint-plugin-jsdoc/node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint-plugin-jsdoc/node_modules/semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/eslint-plugin-node": { "version": "11.1.0", "resolved": "https://registry.npmjs.org/eslint-plugin-node/-/eslint-plugin-node-11.1.0.tgz", @@ -9653,9 +9830,9 @@ } }, "node_modules/eslint/node_modules/semver": { - "version": "7.3.8", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", - "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", "dev": true, "dependencies": { "lru-cache": "^6.0.0" @@ -9903,9 +10080,9 @@ } }, "node_modules/execa/node_modules/semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", "dev": true, "bin": { "semver": "bin/semver" @@ -10832,9 +11009,9 @@ } }, "node_modules/follow-redirects": { - "version": "1.15.2", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz", - "integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==", + "version": "1.15.4", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.4.tgz", + "integrity": "sha512-Cr4D/5wlrb0z9dgERpUL3LrmPKVDsETIJhaCMeDfuFYcqa5bldGV6wBsAN6X/vxlXQtFBMrXdXxdL8CbDTGniw==", "dev": true, "funding": [ { @@ -12090,9 +12267,9 @@ } }, "node_modules/gulp-cli/node_modules/semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", "dev": true, "bin": { "semver": "bin/semver" @@ -12432,9 +12609,9 @@ } }, "node_modules/gulp-eslint/node_modules/cross-spawn/node_modules/semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", "dev": true, "bin": { "semver": "bin/semver" @@ -15506,6 +15683,15 @@ "integrity": "sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==", "dev": true }, + "node_modules/jsdoc-type-pratt-parser": { + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/jsdoc-type-pratt-parser/-/jsdoc-type-pratt-parser-2.2.5.tgz", + "integrity": "sha512-2a6eRxSxp1BW040hFvaJxhsCMI9lT8QB8t14t+NY5tC5rckIR0U9cr2tjOeaFirmEOy6MHvmJnY7zTBHq431Lw==", + "dev": true, + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/jsesc": { "version": "2.5.2", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", @@ -15796,9 +15982,9 @@ } }, "node_modules/karma-coverage-istanbul-reporter/node_modules/semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", "dev": true, "bin": { "semver": "bin/semver" @@ -16232,23 +16418,23 @@ "dev": true }, "node_modules/live-connect-common": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/live-connect-common/-/live-connect-common-1.0.0.tgz", - "integrity": "sha512-LBZsvykcGeVRYI1eqqXrrNZsoBdL2a8cpyrYPIiGAF/CpixbyRbvqGslaFw511lH294QB16J3fYYg21aYuaM2Q==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/live-connect-common/-/live-connect-common-3.0.3.tgz", + "integrity": "sha512-ZPycT04ROBUvPiksnLTunrKC3ROhBSeO99fQ+4qMIkgKwP2CvS44L7fK+0WFV4nAi+65KbzSng7JWcSlckfw8w==", "engines": { - "node": ">=8" + "node": ">=18" } }, "node_modules/live-connect-js": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/live-connect-js/-/live-connect-js-5.0.0.tgz", - "integrity": "sha512-Bv0wQQ+/1VU0/YczEpObbWtHbuXwaHGxwg1+Pe7ZlDgBLb334CrqSQvOL1uyZw3//zs+fSO94yYaQzjjkTd5OQ==", + "version": "6.3.4", + "resolved": "https://registry.npmjs.org/live-connect-js/-/live-connect-js-6.3.4.tgz", + "integrity": "sha512-lg2XeCaj/eEbK66QGGDEdz9IdT/K3ExZ83Qo6xGVLdP5XJ33xAUCk/gds34rRTmpIwUfAnboOpyj3UoYtS3QUQ==", "dependencies": { - "live-connect-common": "^1.0.0", + "live-connect-common": "^v3.0.3", "tiny-hashes": "1.0.1" }, "engines": { - "node": ">=8" + "node": ">=18" } }, "node_modules/livereload-js": { @@ -18706,6 +18892,16 @@ } } }, + "node_modules/node-html-parser": { + "version": "6.1.6", + "resolved": "https://registry.npmjs.org/node-html-parser/-/node-html-parser-6.1.6.tgz", + "integrity": "sha512-C/MGDQ2NjdjzUq41bW9kW00MPZecAe/oo89vZEFLDfWoQVDk/DdML1yuxVVKLDMFIFax2VTq6Vpfzyn7z5yYgQ==", + "dev": true, + "dependencies": { + "css-select": "^5.1.0", + "he": "1.2.0" + } + }, "node_modules/node-releases": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.6.tgz", @@ -18739,9 +18935,9 @@ } }, "node_modules/normalize-package-data/node_modules/semver": { - "version": "7.3.8", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", - "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", "dev": true, "dependencies": { "lru-cache": "^6.0.0" @@ -18807,6 +19003,18 @@ "node": ">=4" } }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "dev": true, + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, "node_modules/number-is-nan": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", @@ -20441,6 +20649,15 @@ "node": ">=4" } }, + "node_modules/regextras": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/regextras/-/regextras-0.8.0.tgz", + "integrity": "sha512-k519uI04Z3SaY0fLX843MRXnDeG2+vHOFsyhiPZvNLe7r8rD2YNRjq4BQLZZ0oAr2NrtvZlICsXysGNFPGa3CQ==", + "dev": true, + "engines": { + "node": ">=0.1.14" + } + }, "node_modules/regjsgen": { "version": "0.7.1", "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.7.1.tgz", @@ -21034,9 +21251,9 @@ } }, "node_modules/semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "bin": { "semver": "bin/semver.js" } @@ -24904,9 +25121,9 @@ } }, "node_modules/word-wrap": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", - "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.4.tgz", + "integrity": "sha512-2V81OA4ugVo5pRo46hAoD2ivUJx8jXmWXfUkY4KFNw0hEptvN0QfH3K4nHiwzGeKl5rFKedV48QVoqYavy4YpA==", "dev": true, "engines": { "node": ">=0.10.0" @@ -25289,11 +25506,12 @@ } }, "@babel/code-frame": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.18.6.tgz", - "integrity": "sha512-TDCmlK5eOvH+eH7cdAFlNXeVJqWIQ7gW9tY1GJIpUtFb6CmjVyq2VM3u71bOyR8CRihcCgMUYoDNyLXao3+70Q==", + "version": "7.22.13", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.13.tgz", + "integrity": "sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==", "requires": { - "@babel/highlight": "^7.18.6" + "@babel/highlight": "^7.22.13", + "chalk": "^2.4.2" } }, "@babel/compat-data": { @@ -25335,12 +25553,13 @@ } }, "@babel/generator": { - "version": "7.20.1", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.20.1.tgz", - "integrity": "sha512-u1dMdBUmA7Z0rBB97xh8pIhviK7oItYOkjbsCxTWMknyvbQRBwX7/gn4JXurRdirWMFh+ZtYARqkA6ydogVZpg==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.0.tgz", + "integrity": "sha512-lN85QRR+5IbYrMWM6Y4pE/noaQtg4pNiqeNGX60eqOfo6gtEj6uw/JagelB8vVztSd7R6M5n1+PQkDbHbBRU4g==", "requires": { - "@babel/types": "^7.20.0", + "@babel/types": "^7.23.0", "@jridgewell/gen-mapping": "^0.3.2", + "@jridgewell/trace-mapping": "^0.3.17", "jsesc": "^2.5.1" }, "dependencies": { @@ -25421,9 +25640,9 @@ } }, "@babel/helper-environment-visitor": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.18.9.tgz", - "integrity": "sha512-3r/aACDJ3fhQ/EVgFy0hpj8oHyHpQc+LPtJoY9SzTThAsStm4Ptegq92vqKoE3vD706ZVFWITnMnxucw+S9Ipg==" + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz", + "integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==" }, "@babel/helper-explode-assignable-expression": { "version": "7.18.6", @@ -25434,20 +25653,20 @@ } }, "@babel/helper-function-name": { - "version": "7.19.0", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.19.0.tgz", - "integrity": "sha512-WAwHBINyrpqywkUH0nTnNgI5ina5TFn85HKS0pbPDfxFfhyR/aNQEn4hGi1P1JyT//I0t4OgXUlofzWILRvS5w==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz", + "integrity": "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==", "requires": { - "@babel/template": "^7.18.10", - "@babel/types": "^7.19.0" + "@babel/template": "^7.22.15", + "@babel/types": "^7.23.0" } }, "@babel/helper-hoist-variables": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.18.6.tgz", - "integrity": "sha512-UlJQPkFqFULIcyW5sbzgbkxn2FKRgwWiRexcuaR8RNJRy8+LLveqPjwZV/bwrLZCN0eUHD/x8D0heK1ozuoo6Q==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz", + "integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==", "requires": { - "@babel/types": "^7.18.6" + "@babel/types": "^7.22.5" } }, "@babel/helper-member-expression-to-functions": { @@ -25534,22 +25753,22 @@ } }, "@babel/helper-split-export-declaration": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.18.6.tgz", - "integrity": "sha512-bde1etTx6ZyTmobl9LLMMQsaizFVZrquTEHOqKeQESMKo4PlObf+8+JA25ZsIpZhT/WEd39+vOdLXAFG/nELpA==", + "version": "7.22.6", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz", + "integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==", "requires": { - "@babel/types": "^7.18.6" + "@babel/types": "^7.22.5" } }, "@babel/helper-string-parser": { - "version": "7.19.4", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.19.4.tgz", - "integrity": "sha512-nHtDoQcuqFmwYNYPz3Rah5ph2p8PFeFCsZk9A/48dPc/rGocJ5J3hAAZ7pb76VWX3fZKu+uEr/FhH5jLx7umrw==" + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz", + "integrity": "sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==" }, "@babel/helper-validator-identifier": { - "version": "7.19.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz", - "integrity": "sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==" + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", + "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==" }, "@babel/helper-validator-option": { "version": "7.18.6", @@ -25578,19 +25797,19 @@ } }, "@babel/highlight": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.18.6.tgz", - "integrity": "sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g==", + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.20.tgz", + "integrity": "sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg==", "requires": { - "@babel/helper-validator-identifier": "^7.18.6", - "chalk": "^2.0.0", + "@babel/helper-validator-identifier": "^7.22.20", + "chalk": "^2.4.2", "js-tokens": "^4.0.0" } }, "@babel/parser": { - "version": "7.20.1", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.20.1.tgz", - "integrity": "sha512-hp0AYxaZJhxULfM1zyp7Wgr+pSUKBcP3M+PHnSzWGdXOzg/kHWIgiUWARvubhUKGOEw3xqY4x+lyZ9ytBVcELw==" + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.0.tgz", + "integrity": "sha512-vvPKKdMemU85V9WE/l5wZEmImpCtLqbnTvqDS2U1fJ96KrxoW7KrXhNsNCblQlg8Ck4b85yxdTyelsMUgFUXiw==" }, "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { "version": "7.18.6", @@ -26282,39 +26501,39 @@ } }, "@babel/template": { - "version": "7.18.10", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.18.10.tgz", - "integrity": "sha512-TI+rCtooWHr3QJ27kJxfjutghu44DLnasDMwpDqCXVTal9RLp3RSYNh4NdBrRP2cQAoG9A8juOQl6P6oZG4JxA==", + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz", + "integrity": "sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==", "requires": { - "@babel/code-frame": "^7.18.6", - "@babel/parser": "^7.18.10", - "@babel/types": "^7.18.10" + "@babel/code-frame": "^7.22.13", + "@babel/parser": "^7.22.15", + "@babel/types": "^7.22.15" } }, "@babel/traverse": { - "version": "7.20.1", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.20.1.tgz", - "integrity": "sha512-d3tN8fkVJwFLkHkBN479SOsw4DMZnz8cdbL/gvuDuzy3TS6Nfw80HuQqhw1pITbIruHyh7d1fMA47kWzmcUEGA==", - "requires": { - "@babel/code-frame": "^7.18.6", - "@babel/generator": "^7.20.1", - "@babel/helper-environment-visitor": "^7.18.9", - "@babel/helper-function-name": "^7.19.0", - "@babel/helper-hoist-variables": "^7.18.6", - "@babel/helper-split-export-declaration": "^7.18.6", - "@babel/parser": "^7.20.1", - "@babel/types": "^7.20.0", + "version": "7.23.2", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.2.tgz", + "integrity": "sha512-azpe59SQ48qG6nu2CzcMLbxUudtN+dOM9kDbUqGq3HXUJRlo7i8fvPoxQUzYgLZ4cMVmuZgm8vvBpNeRhd6XSw==", + "requires": { + "@babel/code-frame": "^7.22.13", + "@babel/generator": "^7.23.0", + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-function-name": "^7.23.0", + "@babel/helper-hoist-variables": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "@babel/parser": "^7.23.0", + "@babel/types": "^7.23.0", "debug": "^4.1.0", "globals": "^11.1.0" } }, "@babel/types": { - "version": "7.20.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.20.0.tgz", - "integrity": "sha512-Jlgt3H0TajCW164wkTOTzHkZb075tMQMULzrLUoUeKmO7eFL96GgDxf7/Axhc5CAuKE3KFyVW1p6ysKsi2oXAg==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.0.tgz", + "integrity": "sha512-0oIyUfKoI3mSqMvsxBdclDwxXKXAUA8v/apZbc+iSyARYou1o8ZGDxbUYyLFoW2arqS2jDGqJuZvv1d/io1axg==", "requires": { - "@babel/helper-string-parser": "^7.19.4", - "@babel/helper-validator-identifier": "^7.19.1", + "@babel/helper-string-parser": "^7.22.5", + "@babel/helper-validator-identifier": "^7.22.20", "to-fast-properties": "^2.0.0" } }, @@ -26324,6 +26543,17 @@ "integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==", "dev": true }, + "@es-joy/jsdoccomment": { + "version": "0.22.2", + "resolved": "https://registry.npmjs.org/@es-joy/jsdoccomment/-/jsdoccomment-0.22.2.tgz", + "integrity": "sha512-pM6WQKcuAtdYoqCsXSvVSu3Ij8K0HY50L8tIheOKHDl0wH1uA4zbP88etY8SIeP16NVCMCTFU+Q2DahSKheGGQ==", + "dev": true, + "requires": { + "comment-parser": "1.3.1", + "esquery": "^1.4.0", + "jsdoc-type-pratt-parser": "~2.2.5" + } + }, "@eslint/eslintrc": { "version": "0.4.3", "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-0.4.3.tgz", @@ -30243,6 +30473,12 @@ } } }, + "boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "dev": true + }, "brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -30515,9 +30751,9 @@ } }, "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", "dev": true }, "strip-ansi": { @@ -30989,6 +31225,12 @@ "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", "dev": true }, + "comment-parser": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/comment-parser/-/comment-parser-1.3.1.tgz", + "integrity": "sha512-B52sN2VNghyq5ofvUsqZjmk6YkihBX5vMSChmSK9v4ShjKf3Vk5Xcmgpw4o+iIgtrnM/u5FiMpz9VKb8lpBveA==", + "dev": true + }, "commondir": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", @@ -31278,9 +31520,9 @@ } }, "crypto-js": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-3.3.0.tgz", - "integrity": "sha512-DIT51nX0dCfKltpRiXV+/TVZq+Qq2NgF4644+K7Ttnla7zEzqc+kjJyiB96BHNyUTBxyjzRcZYpUdZa+QAqi6Q==" + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz", + "integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==" }, "css": { "version": "3.0.0", @@ -31301,6 +31543,19 @@ } } }, + "css-select": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz", + "integrity": "sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==", + "dev": true, + "requires": { + "boolbase": "^1.0.0", + "css-what": "^6.1.0", + "domhandler": "^5.0.2", + "domutils": "^3.0.1", + "nth-check": "^2.0.1" + } + }, "css-shorthand-properties": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/css-shorthand-properties/-/css-shorthand-properties-1.1.1.tgz", @@ -31313,6 +31568,12 @@ "integrity": "sha512-FUV3xaJ63buRLgHrLQVlVgQnQdR4yqdLGaDu7g8CQcWjInDfM9plBTPI9FRfpahju1UBSaMckeb2/46ApS/V1Q==", "dev": true }, + "css-what": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", + "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==", + "dev": true + }, "custom-event": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/custom-event/-/custom-event-1.0.1.tgz", @@ -31924,12 +32185,49 @@ "void-elements": "^2.0.0" } }, + "dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "dev": true, + "requires": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + } + }, "dom-walk": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/dom-walk/-/dom-walk-0.1.2.tgz", "integrity": "sha512-6QvTW9mrGeIegrFXdtQi9pk7O/nSK6lSdXW2eqUspN5LWD7UTji2Fqw5V2YLjBpHEoU9Xl/eUWNpDeZvoyOv2w==", "dev": true }, + "domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "dev": true + }, + "domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "dev": true, + "requires": { + "domelementtype": "^2.3.0" + } + }, + "domutils": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz", + "integrity": "sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==", + "dev": true, + "requires": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + } + }, "dset": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/dset/-/dset-3.1.2.tgz", @@ -32160,6 +32458,12 @@ "integrity": "sha512-GHrMyVZQWvTIdDtpiEXdHZnFQKzeO09apj8Cbl4pKWy4i0Oprcq17usfDt5aO63swf0JOeMWjWQE/LzgSRuWpA==", "dev": true }, + "entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "dev": true + }, "errno": { "version": "0.1.8", "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.8.tgz", @@ -32530,9 +32834,9 @@ "dev": true }, "semver": { - "version": "7.3.8", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", - "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", "dev": true, "requires": { "lru-cache": "^6.0.0" @@ -32666,6 +32970,39 @@ } } }, + "eslint-plugin-jsdoc": { + "version": "38.1.6", + "resolved": "https://registry.npmjs.org/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-38.1.6.tgz", + "integrity": "sha512-n4s95oYlg0L43Bs8C0dkzIldxYf8pLCutC/tCbjIdF7VDiobuzPI+HZn9Q0BvgOvgPNgh5n7CSStql25HUG4Tw==", + "dev": true, + "requires": { + "@es-joy/jsdoccomment": "~0.22.1", + "comment-parser": "1.3.1", + "debug": "^4.3.4", + "escape-string-regexp": "^4.0.0", + "esquery": "^1.4.0", + "regextras": "^0.8.0", + "semver": "^7.3.5", + "spdx-expression-parse": "^3.0.1" + }, + "dependencies": { + "escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true + }, + "semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } + } + } + }, "eslint-plugin-node": { "version": "11.1.0", "resolved": "https://registry.npmjs.org/eslint-plugin-node/-/eslint-plugin-node-11.1.0.tgz", @@ -32900,9 +33237,9 @@ "dev": true }, "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", "dev": true }, "shebang-command": { @@ -33674,9 +34011,9 @@ } }, "follow-redirects": { - "version": "1.15.2", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz", - "integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==", + "version": "1.15.4", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.4.tgz", + "integrity": "sha512-Cr4D/5wlrb0z9dgERpUL3LrmPKVDsETIJhaCMeDfuFYcqa5bldGV6wBsAN6X/vxlXQtFBMrXdXxdL8CbDTGniw==", "dev": true }, "for-each": { @@ -34681,9 +35018,9 @@ } }, "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", "dev": true }, "string-width": { @@ -34969,9 +35306,9 @@ }, "dependencies": { "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", "dev": true } } @@ -37291,6 +37628,12 @@ "integrity": "sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==", "dev": true }, + "jsdoc-type-pratt-parser": { + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/jsdoc-type-pratt-parser/-/jsdoc-type-pratt-parser-2.2.5.tgz", + "integrity": "sha512-2a6eRxSxp1BW040hFvaJxhsCMI9lT8QB8t14t+NY5tC5rckIR0U9cr2tjOeaFirmEOy6MHvmJnY7zTBHq431Lw==", + "dev": true + }, "jsesc": { "version": "2.5.2", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", @@ -37585,9 +37928,9 @@ } }, "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", "dev": true }, "source-map": { @@ -37883,16 +38226,16 @@ "dev": true }, "live-connect-common": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/live-connect-common/-/live-connect-common-1.0.0.tgz", - "integrity": "sha512-LBZsvykcGeVRYI1eqqXrrNZsoBdL2a8cpyrYPIiGAF/CpixbyRbvqGslaFw511lH294QB16J3fYYg21aYuaM2Q==" + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/live-connect-common/-/live-connect-common-3.0.3.tgz", + "integrity": "sha512-ZPycT04ROBUvPiksnLTunrKC3ROhBSeO99fQ+4qMIkgKwP2CvS44L7fK+0WFV4nAi+65KbzSng7JWcSlckfw8w==" }, "live-connect-js": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/live-connect-js/-/live-connect-js-5.0.0.tgz", - "integrity": "sha512-Bv0wQQ+/1VU0/YczEpObbWtHbuXwaHGxwg1+Pe7ZlDgBLb334CrqSQvOL1uyZw3//zs+fSO94yYaQzjjkTd5OQ==", + "version": "6.3.4", + "resolved": "https://registry.npmjs.org/live-connect-js/-/live-connect-js-6.3.4.tgz", + "integrity": "sha512-lg2XeCaj/eEbK66QGGDEdz9IdT/K3ExZ83Qo6xGVLdP5XJ33xAUCk/gds34rRTmpIwUfAnboOpyj3UoYtS3QUQ==", "requires": { - "live-connect-common": "^1.0.0", + "live-connect-common": "^v3.0.3", "tiny-hashes": "1.0.1" } }, @@ -39734,6 +40077,16 @@ "whatwg-url": "^5.0.0" } }, + "node-html-parser": { + "version": "6.1.6", + "resolved": "https://registry.npmjs.org/node-html-parser/-/node-html-parser-6.1.6.tgz", + "integrity": "sha512-C/MGDQ2NjdjzUq41bW9kW00MPZecAe/oo89vZEFLDfWoQVDk/DdML1yuxVVKLDMFIFax2VTq6Vpfzyn7z5yYgQ==", + "dev": true, + "requires": { + "css-select": "^5.1.0", + "he": "1.2.0" + } + }, "node-releases": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.6.tgz", @@ -39761,9 +40114,9 @@ }, "dependencies": { "semver": { - "version": "7.3.8", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", - "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", "dev": true, "requires": { "lru-cache": "^6.0.0" @@ -39809,6 +40162,15 @@ } } }, + "nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "dev": true, + "requires": { + "boolbase": "^1.0.0" + } + }, "number-is-nan": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", @@ -41027,6 +41389,12 @@ "unicode-match-property-value-ecmascript": "^2.0.0" } }, + "regextras": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/regextras/-/regextras-0.8.0.tgz", + "integrity": "sha512-k519uI04Z3SaY0fLX843MRXnDeG2+vHOFsyhiPZvNLe7r8rD2YNRjq4BQLZZ0oAr2NrtvZlICsXysGNFPGa3CQ==", + "dev": true + }, "regjsgen": { "version": "0.7.1", "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.7.1.tgz", @@ -41495,9 +41863,9 @@ } }, "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==" }, "semver-greatest-satisfied-range": { "version": "1.1.0", @@ -44558,9 +44926,9 @@ } }, "word-wrap": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", - "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.4.tgz", + "integrity": "sha512-2V81OA4ugVo5pRo46hAoD2ivUJx8jXmWXfUkY4KFNw0hEptvN0QfH3K4nHiwzGeKl5rFKedV48QVoqYavy4YpA==", "dev": true }, "wordwrap": { diff --git a/package.json b/package.json index 4f0f512bd8b..38697197d0b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "prebid.js", - "version": "8.1.0-pre", + "version": "8.34.0-pre", "description": "Header Bidding Management Library", "main": "src/prebid.js", "scripts": { @@ -58,6 +58,7 @@ "eslint": "^7.27.0", "eslint-config-standard": "^10.2.1", "eslint-plugin-import": "^2.20.2", + "eslint-plugin-jsdoc": "^38.1.6", "eslint-plugin-node": "^11.1.0", "eslint-plugin-prebid": "file:./plugins/eslint", "eslint-plugin-promise": "^5.1.0", @@ -101,6 +102,7 @@ "lodash": "^4.17.21", "mocha": "^10.0.0", "morgan": "^1.10.0", + "node-html-parser": "^6.1.5", "opn": "^5.4.0", "resolve-from": "^5.0.0", "sinon": "^4.1.3", @@ -126,13 +128,13 @@ "core-js": "^3.13.0", "core-js-pure": "^3.13.0", "criteo-direct-rsa-validate": "^1.1.0", - "crypto-js": "^3.3.0", + "crypto-js": "^4.2.0", "dlv": "1.1.3", "dset": "3.1.2", "express": "^4.15.4", "fun-hooks": "^0.9.9", "just-clone": "^1.0.2", - "live-connect-js": "^5.0.0" + "live-connect-js": "^6.3.4" }, "optionalDependencies": { "fsevents": "^2.3.2" diff --git a/plugins/pbjsGlobals.js b/plugins/pbjsGlobals.js index 6d1eeb0c57d..62d29b567ed 100644 --- a/plugins/pbjsGlobals.js +++ b/plugins/pbjsGlobals.js @@ -34,7 +34,8 @@ module.exports = function(api, options) { '$$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/` + '$$PREBID_DIST_URL_BASE$$': options.prebidDistUrlBase || `https://cdn.jsdelivr.net/npm/prebid.js@${getNpmVersion(prebid.version)}/dist/`, + '$$LIVE_INTENT_MODULE_MODE$$': (process && process.env && process.env.LiveConnectMode) || 'standard' }; let identifierToStringLiteral = [ diff --git a/src/activities/params.js b/src/activities/params.js index ff181bb55a4..036a6657cf8 100644 --- a/src/activities/params.js +++ b/src/activities/params.js @@ -1,4 +1,5 @@ import {MODULE_TYPE_BIDDER} from './modules.js'; +import {hook} from '../hook.js'; /** * Component ID - who is trying to perform the activity? @@ -54,6 +55,8 @@ export function activityParamsBuilder(resolveAlias) { if (moduleType === MODULE_TYPE_BIDDER) { defaults[ACTIVITY_PARAM_ADAPTER_CODE] = resolveAlias(moduleName); } - return Object.assign(defaults, params); + return buildActivityParams(Object.assign(defaults, params)); } } + +export const buildActivityParams = hook('sync', params => params); diff --git a/src/activities/redactor.js b/src/activities/redactor.js index d50df72648c..694a96b2b14 100644 --- a/src/activities/redactor.js +++ b/src/activities/redactor.js @@ -8,7 +8,17 @@ import { ACTIVITY_TRANSMIT_UFPD } from './activities.js'; -export const ORTB_UFPD_PATHS = ['user.data', 'user.ext.data']; +export const ORTB_UFPD_PATHS = [ + 'data', + 'ext.data', + 'yob', + 'gender', + 'keywords', + 'kwarray', + 'id', + 'buyeruid', + 'customdata' +].map(f => `user.${f}`).concat('device.ext.cdep'); export const ORTB_EIDS_PATHS = ['user.eids', 'user.ext.eids']; export const ORTB_GEO_PATHS = ['user.geo.lat', 'user.geo.lon', 'device.geo.lat', 'device.geo.lon']; diff --git a/src/adRendering.js b/src/adRendering.js index 0a847d7cc25..f8fe0044f9b 100644 --- a/src/adRendering.js +++ b/src/adRendering.js @@ -1,33 +1,39 @@ -import {logError} from './utils.js'; +import {deepAccess, logError, logWarn, replaceMacros} from './utils.js'; import * as events from './events.js'; -import CONSTANTS from './constants.json'; +import constants from './constants.json'; +import {config} from './config.js'; +import {executeRenderer, isRendererRequired} from './Renderer.js'; +import {VIDEO} from './mediaTypes.js'; +import {auctionManager} from './auctionManager.js'; -const {AD_RENDER_FAILED, AD_RENDER_SUCCEEDED} = CONSTANTS.EVENTS; +const {AD_RENDER_FAILED, AD_RENDER_SUCCEEDED, STALE_RENDER, BID_WON} = constants.EVENTS; /** * Emit the AD_RENDER_FAILED event. * - * @param reason one of the values in CONSTANTS.AD_RENDER_FAILED_REASON - * @param message failure description - * @param bid? bid response object that failed to render - * @param id? adId that failed to render + * @param {Object} data + * @param data.reason one of the values in CONSTANTS.AD_RENDER_FAILED_REASON + * @param data.message failure description + * @param [data.bid] bid response object that failed to render + * @param [data.id] adId that failed to render */ export function emitAdRenderFail({ reason, message, bid, id }) { const data = { reason, message }; if (bid) data.bid = bid; if (id) data.adId = id; - logError(message); + logError(`Error rendering ad (id: ${id}): ${message}`); events.emit(AD_RENDER_FAILED, data); } /** * Emit the AD_RENDER_SUCCEEDED event. * (Note: Invocation of this function indicates that the render function did not generate an error, it does not guarantee that tracking for this event has occurred yet.) - * @param doc document object that was used to `.write` the ad. Should be `null` if unavailable (e.g. for documents in + * @param {Object} data + * @param data.doc document object that was used to `.write` the ad. Should be `null` if unavailable (e.g. for documents in * a cross-origin frame). - * @param bid bid response object for the ad that was rendered - * @param id adId that was rendered. + * @param [data.bid] bid response object for the ad that was rendered + * @param [data.id] adId that was rendered. */ export function emitAdRenderSucceeded({ doc, bid, id }) { const data = { doc }; @@ -36,3 +42,61 @@ export function emitAdRenderSucceeded({ doc, bid, id }) { events.emit(AD_RENDER_SUCCEEDED, data); } + +export function handleRender(renderFn, {adId, options, bidResponse, doc}) { + if (bidResponse == null) { + emitAdRenderFail({ + reason: constants.AD_RENDER_FAILED_REASON.CANNOT_FIND_AD, + message: `Cannot find ad '${adId}'`, + id: adId + }); + return; + } + if (bidResponse.status === constants.BID_STATUS.RENDERED) { + logWarn(`Ad id ${adId} has been rendered before`); + events.emit(STALE_RENDER, bidResponse); + if (deepAccess(config.getConfig('auctionOptions'), 'suppressStaleRender')) { + return; + } + } + try { + const {adId, ad, adUrl, width, height, renderer, cpm, originalCpm, mediaType} = bidResponse; + // rendering for outstream safeframe + if (isRendererRequired(renderer)) { + executeRenderer(renderer, bidResponse, doc); + } else if (adId) { + if (mediaType === VIDEO) { + emitAdRenderFail({ + reason: constants.AD_RENDER_FAILED_REASON.PREVENT_WRITING_ON_MAIN_DOCUMENT, + message: 'Cannot render video ad', + bid: bidResponse, + id: adId + }); + return; + } + const repl = { + AUCTION_PRICE: originalCpm || cpm, + CLICKTHROUGH: options?.clickUrl || '' + }; + renderFn({ + ad: replaceMacros(ad, repl), + adUrl: replaceMacros(adUrl, repl), + adId, + width, + height + }); + } + } catch (e) { + emitAdRenderFail({ + reason: constants.AD_RENDER_FAILED_REASON.EXCEPTION, + message: e.message, + id: adId, + bid: bidResponse + }); + return; + } + // save winning bids + auctionManager.addWinningBid(bidResponse); + + events.emit(BID_WON, bidResponse); +} diff --git a/src/adServerManager.js b/src/adServerManager.js index af8fe34920e..7e1290b3983 100644 --- a/src/adServerManager.js +++ b/src/adServerManager.js @@ -34,7 +34,7 @@ const prebid = getGlobal(); /** * @typedef {Object} VideoSupport * - * @function {VideoAdUrlBuilder} buildVideoAdUrl + * @property {VideoAdUrlBuilder} buildVideoAdUrl */ /** diff --git a/src/adapterManager.js b/src/adapterManager.js index b600802b522..189fbf31f2e 100644 --- a/src/adapterManager.js +++ b/src/adapterManager.js @@ -1,8 +1,6 @@ /** @module adaptermanger */ import { - _each, - bind, deepAccess, deepClone, flatten, @@ -12,7 +10,8 @@ import { getUniqueIdentifierStr, getUserConfiguredParams, groupBy, - isArray, isPlainObject, + isArray, + isPlainObject, isValidMediaTypes, logError, logInfo, @@ -30,7 +29,7 @@ import {hook} from './hook.js'; import {find, includes} from './polyfill.js'; import {adunitCounter} from './adUnits.js'; import {getRefererInfo} from './refererDetection.js'; -import {GDPR_GVLIDS, GdprConsentHandler, GppConsentHandler, UspConsentHandler} from './consentHandler.js'; +import {GDPR_GVLIDS, gdprDataHandler, gppDataHandler, uspDataHandler, } from './consentHandler.js'; import * as events from './events.js'; import CONSTANTS from './constants.json'; import {useMetrics} from './utils/perfMetrics.js'; @@ -41,6 +40,8 @@ import {ACTIVITY_FETCH_BIDS, ACTIVITY_REPORT_ANALYTICS} from './activities/activ import {ACTIVITY_PARAM_ANL_CONFIG, ACTIVITY_PARAM_S2S_NAME, activityParamsBuilder} from './activities/params.js'; import {redactor} from './activities/redactor.js'; +export {gdprDataHandler, gppDataHandler, uspDataHandler, coppaDataHandler} from './consentHandler.js'; + export const PBS_ADAPTER_NAME = 'pbsBidAdapter'; export const PARTITIONS = { CLIENT: 'client', @@ -192,16 +193,6 @@ function getAdUnitCopyForClientAdapters(adUnits) { return adUnitsClientCopy; } -export let gdprDataHandler = new GdprConsentHandler(); -export let uspDataHandler = new UspConsentHandler(); -export let gppDataHandler = new GppConsentHandler(); - -export let coppaDataHandler = { - getCoppa: function() { - return !!(config.getConfig('coppa')) - } -}; - /** * Filter and/or modify media types for ad units based on the given labels. * @@ -382,13 +373,13 @@ adapterManager.callBids = (adUnits, bidRequests, addBidResponse, doneCb, request return; } - let [clientBidRequests, serverBidRequests] = bidRequests.reduce((partitions, bidRequest) => { + let [clientBidderRequests, serverBidderRequests] = bidRequests.reduce((partitions, bidRequest) => { partitions[Number(typeof bidRequest.src !== 'undefined' && bidRequest.src === CONSTANTS.S2S.SRC)].push(bidRequest); return partitions; }, [[], []]); var uniqueServerBidRequests = []; - serverBidRequests.forEach(serverBidRequest => { + serverBidderRequests.forEach(serverBidRequest => { var index = -1; for (var i = 0; i < uniqueServerBidRequests.length; ++i) { if (serverBidRequest.uniquePbsTid === uniqueServerBidRequests[i].uniquePbsTid) { @@ -415,14 +406,19 @@ adapterManager.callBids = (adUnits, bidRequests, addBidResponse, doneCb, request let uniquePbsTid = uniqueServerBidRequests[counter].uniquePbsTid; let adUnitsS2SCopy = uniqueServerBidRequests[counter].adUnitsS2SCopy; - let uniqueServerRequests = serverBidRequests.filter(serverBidRequest => serverBidRequest.uniquePbsTid === uniquePbsTid); + let uniqueServerRequests = serverBidderRequests.filter(serverBidRequest => serverBidRequest.uniquePbsTid === uniquePbsTid); if (s2sAdapter) { let s2sBidRequest = {'ad_units': adUnitsS2SCopy, s2sConfig, ortb2Fragments}; if (s2sBidRequest.ad_units.length) { let doneCbs = uniqueServerRequests.map(bidRequest => { bidRequest.start = timestamp(); - return doneCb.bind(bidRequest); + return function (timedOut) { + if (!timedOut) { + onTimelyResponse(bidRequest.bidderRequestId); + } + doneCb.apply(bidRequest, arguments); + } }); const bidders = getBidderCodes(s2sBidRequest.ad_units).filter((bidder) => adaptersServerSide.includes(bidder)); @@ -437,9 +433,9 @@ adapterManager.callBids = (adUnits, bidRequests, addBidResponse, doneCb, request // make bid requests s2sAdapter.callBids( s2sBidRequest, - serverBidRequests, + serverBidderRequests, addBidResponse, - () => doneCbs.forEach(done => done()), + (timedOut) => doneCbs.forEach(done => done(timedOut)), s2sAjax ); } @@ -451,35 +447,34 @@ adapterManager.callBids = (adUnits, bidRequests, addBidResponse, doneCb, request }); // handle client adapter requests - clientBidRequests.forEach(bidRequest => { - bidRequest.start = timestamp(); + clientBidderRequests.forEach(bidderRequest => { + bidderRequest.start = timestamp(); // TODO : Do we check for bid in pool from here and skip calling adapter again ? - const adapter = _bidderRegistry[bidRequest.bidderCode]; - config.runWithBidder(bidRequest.bidderCode, () => { + const adapter = _bidderRegistry[bidderRequest.bidderCode]; + config.runWithBidder(bidderRequest.bidderCode, () => { logMessage(`CALLING BIDDER`); - events.emit(CONSTANTS.EVENTS.BID_REQUESTED, bidRequest); + events.emit(CONSTANTS.EVENTS.BID_REQUESTED, bidderRequest); }); let ajax = ajaxBuilder(requestBidsTimeout, requestCallbacks ? { - request: requestCallbacks.request.bind(null, bidRequest.bidderCode), + request: requestCallbacks.request.bind(null, bidderRequest.bidderCode), done: requestCallbacks.done } : undefined); - const adapterDone = doneCb.bind(bidRequest); + const adapterDone = doneCb.bind(bidderRequest); try { config.runWithBidder( - bidRequest.bidderCode, - bind.call( - adapter.callBids, + bidderRequest.bidderCode, + adapter.callBids.bind( adapter, - bidRequest, + bidderRequest, addBidResponse, adapterDone, ajax, - onTimelyResponse, - config.callbackWithBidder(bidRequest.bidderCode) + () => onTimelyResponse(bidderRequest.bidderRequestId), + config.callbackWithBidder(bidderRequest.bidderCode) ) ); } catch (e) { - logError(`${bidRequest.bidderCode} Bid Adapter emitted an uncaught error when parsing their bidRequest`, {e, bidRequest}); + logError(`${bidderRequest.bidderCode} Bid Adapter emitted an uncaught error when parsing their bidRequest`, {e, bidRequest: bidderRequest}); adapterDone(); } }); @@ -547,6 +542,9 @@ adapterManager.aliasBidAdapter = function (bidderCode, alias, options) { } else { let spec = bidAdapter.getSpec(); let gvlid = options && options.gvlid; + if (spec.gvlid != null && gvlid == null) { + logWarn(`Alias '${alias}' will NOT re-use the GVL ID of the original adapter ('${spec.code}', gvlid: ${spec.gvlid}). Functionality that requires TCF consent may not work as expected.`) + } let skipPbsAliasing = options && options.skipPbsAliasing; newAdapter = newBidder(Object.assign({}, spec, { code: alias, gvlid, skipPbsAliasing })); _aliasRegistry[alias] = bidderCode; @@ -593,7 +591,7 @@ adapterManager.enableAnalytics = function (config) { config = [config]; } - _each(config, adapterConfig => { + config.forEach(adapterConfig => { const entry = _analyticsRegistry[adapterConfig.provider]; if (entry && entry.adapter) { if (dep.isAllowed(ACTIVITY_REPORT_ANALYTICS, activityParams(MODULE_TYPE_ANALYTICS, adapterConfig.provider, {[ACTIVITY_PARAM_ANL_CONFIG]: adapterConfig}))) { diff --git a/src/adapters/bidderFactory.js b/src/adapters/bidderFactory.js index c8f03bcfc94..3d55f2c06af 100644 --- a/src/adapters/bidderFactory.js +++ b/src/adapters/bidderFactory.js @@ -15,7 +15,7 @@ import { logError, logWarn, memoize, parseQueryStringParameters, - parseSizesInput, + parseSizesInput, pick, uniques } from '../utils.js'; import {hook} from '../hook.js'; @@ -25,7 +25,12 @@ import {useMetrics} from '../utils/perfMetrics.js'; import {isActivityAllowed} from '../activities/rules.js'; import {activityParams} from '../activities/activityParams.js'; import {MODULE_TYPE_BIDDER} from '../activities/modules.js'; -import {ACTIVITY_TRANSMIT_TID} from '../activities/activities.js'; +import {ACTIVITY_TRANSMIT_TID, ACTIVITY_TRANSMIT_UFPD} from '../activities/activities.js'; + +/** + * @typedef {import('../mediaTypes.js').MediaType} MediaType + * @typedef {import('../Renderer.js').Renderer} Renderer + */ /** * This file aims to support Adapters during the Prebid 0.x -> 1.x transition. @@ -57,7 +62,7 @@ import {ACTIVITY_TRANSMIT_TID} from '../activities/activities.js'; * @property {string} code A code which will be used to uniquely identify this bidder. This should be the same * one as is used in the call to registerBidAdapter * @property {string[]} [aliases] A list of aliases which should also resolve to this bidder. - * @property {MediaType[]} [supportedMediaTypes]: A list of Media Types which the adapter supports. + * @property {MediaType[]} [supportedMediaTypes] A list of Media Types which the adapter supports. * @property {function(object): boolean} isBidRequestValid Determines whether or not the given bid has all the params * needed to make a valid request. * @property {function(BidRequest[], bidderRequest): ServerRequest|ServerRequest[]} buildRequests Build the request to the Server @@ -105,7 +110,7 @@ import {ACTIVITY_TRANSMIT_TID} from '../activities/activities.js'; * * @property {*} body The response body. If this is legal JSON, then it will be parsed. Otherwise it'll be a * string with the body's content. - * @property {{get: function(string): string} headers The response headers. + * @property {{get: function(string): string}} headers The response headers. * Call this like `ServerResponse.headers.get("Content-Type")` */ @@ -126,7 +131,7 @@ import {ACTIVITY_TRANSMIT_TID} from '../activities/activities.js'; * @property {object} [video] Object for storing video response data * @property {object} [meta] Object for storing bid meta data * @property {string} [meta.primaryCatId] The IAB primary category ID - * @property [Renderer] renderer A Renderer which can be used as a default for this bid, + * @property {Renderer} renderer A Renderer which can be used as a default for this bid, * if the publisher doesn't override it. This is only relevant for Outstream Video bids. */ @@ -150,6 +155,7 @@ import {ACTIVITY_TRANSMIT_TID} from '../activities/activities.js'; // common params for all mediaTypes const COMMON_BID_RESPONSE_KEYS = ['cpm', 'ttl', 'creativeId', 'netRevenue', 'currency']; +const TIDS = ['auctionId', 'transactionId']; /** * Register a bidder with prebid, using the given spec. @@ -184,7 +190,7 @@ export function registerBidder(spec) { } } -function guardTids(bidderCode) { +export function guardTids(bidderCode) { if (isActivityAllowed(ACTIVITY_TRANSMIT_TID, activityParams(MODULE_TYPE_BIDDER, bidderCode))) { return { bidRequest: (br) => br, @@ -192,12 +198,20 @@ function guardTids(bidderCode) { }; } function get(target, prop, receiver) { - if (['transactionId', 'auctionId'].includes(prop)) { + if (TIDS.includes(prop)) { return null; } return Reflect.get(target, prop, receiver); } - const bidRequest = memoize((br) => new Proxy(br, {get}), (arg) => arg.bidId) + function privateAccessProxy(target, handler) { + const proxy = new Proxy(target, handler); + // always allow methods (such as getFloor) private access to TIDs + Object.entries(target) + .filter(([_, v]) => typeof v === 'function') + .forEach(([prop, fn]) => proxy[prop] = fn.bind(target)); + return proxy; + } + const bidRequest = memoize((br) => privateAccessProxy(br, {get}), (arg) => arg.bidId); /** * Return a view on bidd(er) requests where auctionId/transactionId are nulled if the bidder is not allowed `transmitTid`. * @@ -207,7 +221,7 @@ function guardTids(bidderCode) { */ return { bidRequest, - bidderRequest: (br) => new Proxy(br, { + bidderRequest: (br) => privateAccessProxy(br, { get(target, prop, receiver) { if (prop === 'bids') return br.bids.map(bidRequest); return get(target, prop, receiver); @@ -279,14 +293,11 @@ export function newBidder(spec) { onTimelyResponse(spec.code); responses.push(resp) }, - /** Process eventual BidderAuctionResponse.fledgeAuctionConfig field in response. - * @param {Array} fledgeAuctionConfigs - */ onFledgeAuctionConfigs: (fledgeAuctionConfigs) => { fledgeAuctionConfigs.forEach((fledgeAuctionConfig) => { const bidRequest = bidRequestMap[fledgeAuctionConfig.bidId]; if (bidRequest) { - addComponentAuction(bidRequest.adUnitCode, fledgeAuctionConfig.config); + addComponentAuction(bidRequest, fledgeAuctionConfig.config); } else { logWarn('Received fledge auction configuration for an unknown bidId', fledgeAuctionConfig); } @@ -294,7 +305,9 @@ export function newBidder(spec) { }, // If the server responds with an error, there's not much we can do beside logging. onError: (errorMessage, error) => { - onTimelyResponse(spec.code); + if (!error.timedOut) { + onTimelyResponse(spec.code); + } adapterManager.callBidderError(spec.code, error, bidderRequest) events.emit(CONSTANTS.EVENTS.BIDDER_ERROR, { error, bidderRequest }); logError(`Server call for ${spec.code} failed: ${errorMessage} ${error.status}. Continuing without bids.`); @@ -312,7 +325,7 @@ export function newBidder(spec) { bid.originalCpm = bid.cpm; bid.originalCurrency = bid.currency; bid.meta = bid.meta || Object.assign({}, bid[bidRequest.bidder]); - const prebidBid = Object.assign(createBid(CONSTANTS.STATUS.GOOD, bidRequest), bid); + const prebidBid = Object.assign(createBid(CONSTANTS.STATUS.GOOD, bidRequest), bid, pick(bidRequest, TIDS)); addBidWithCode(bidRequest.adUnitCode, prebidBid); } else { logWarn(`Bidder ${spec.code} made bid for unknown request ID: ${bid.requestId}. Ignoring.`); @@ -445,6 +458,15 @@ export const processBidderRequests = hook('sync', function (spec, bids, bidderRe onRequest(request); const networkDone = requestMetrics.startTiming('net'); + + function getOptions(defaults) { + const ro = request.options; + return Object.assign(defaults, ro, { + browsingTopics: ro?.hasOwnProperty('browsingTopics') && !ro.browsingTopics + ? false + : (bidderSettings.get(spec.code, 'topicsHeader') ?? true) && isActivityAllowed(ACTIVITY_TRANSMIT_UFPD, activityParams(MODULE_TYPE_BIDDER, spec.code)) + }) + } switch (request.method) { case 'GET': ajax( @@ -454,10 +476,10 @@ export const processBidderRequests = hook('sync', function (spec, bids, bidderRe error: onFailure }, undefined, - Object.assign({ + getOptions({ method: 'GET', withCredentials: true - }, request.options) + }) ); break; case 'POST': @@ -468,11 +490,11 @@ export const processBidderRequests = hook('sync', function (spec, bids, bidderRe error: onFailure }, typeof request.data === 'string' ? request.data : JSON.stringify(request.data), - Object.assign({ + getOptions({ method: 'POST', contentType: 'text/plain', withCredentials: true - }, request.options) + }) ); break; default: @@ -510,7 +532,7 @@ export const registerSyncInner = hook('async', function(spec, responses, gdprCon } }, 'registerSyncs') -export const addComponentAuction = hook('sync', (adUnitCode, fledgeAuctionConfig) => { +export const addComponentAuction = hook('sync', (request, fledgeAuctionConfig) => { }, 'addComponentAuction'); // check that the bid has a width and height set diff --git a/src/adloader.js b/src/adloader.js index 3aae348d106..f60955736bd 100644 --- a/src/adloader.js +++ b/src/adloader.js @@ -24,7 +24,14 @@ const _approvedLoadExternalJSList = [ 'confiant', 'arcspan', 'airgrid', - 'clean.io' + 'clean.io', + 'a1Media', + 'geoedge', + 'mediafilter', + 'qortex', + 'dynamicAdBoost', + 'contxtful', + 'id5' ] /** @@ -34,7 +41,7 @@ const _approvedLoadExternalJSList = [ * @param {string} moduleCode bidderCode or module code of the module requesting this resource * @param {function} [callback] callback function to be called after the script is loaded * @param {Document} [doc] the context document, in which the script will be loaded, defaults to loaded document - * @param {object} an object of attributes to be added to the script with setAttribute by [key] and [value]; Only the attributes passed in the first request of a url will be added. + * @param {object} attributes an object of attributes to be added to the script with setAttribute by [key] and [value]; Only the attributes passed in the first request of a url will be added. */ export function loadExternalScript(url, moduleCode, callback, doc, attributes) { if (!moduleCode || !url) { diff --git a/src/ajax.js b/src/ajax.js index 5e926f3210d..ef4c2e4bcb4 100644 --- a/src/ajax.js +++ b/src/ajax.js @@ -1,99 +1,148 @@ -import { config } from './config.js'; -import { logMessage, logError, parseUrl, buildUrl, _each } from './utils.js'; +import {config} from './config.js'; +import {buildUrl, logError, parseUrl} from './utils.js'; -const XHR_DONE = 4; - -/** - * Simple IE9+ and cross-browser ajax request function - * Note: x-domain requests in IE9 do not support the use of cookies - * - * @param url string url - * @param callback {object | function} callback - * @param data mixed data - * @param options object - */ -export const ajax = ajaxBuilder(); - -export function ajaxBuilder(timeout = 3000, {request, done} = {}) { - return function(url, callback, data, options = {}) { - try { - let x; - let method = options.method || (data ? 'POST' : 'GET'); - let parser = document.createElement('a'); - parser.href = url; - - let callbacks = typeof callback === 'object' && callback !== null ? callback : { - success: function() { - logMessage('xhr success'); - }, - error: function(e) { - logError('xhr error', null, e); - } - }; - - if (typeof callback === 'function') { - callbacks.success = callback; - } - - x = new window.XMLHttpRequest(); - - x.onreadystatechange = function () { - if (x.readyState === XHR_DONE) { - if (typeof done === 'function') { - done(parser.origin); - } - let status = x.status; - if ((status >= 200 && status < 300) || status === 304) { - callbacks.success(x.responseText, x); - } else { - callbacks.error(x.statusText, x); - } - } - }; - - // Disabled timeout temporarily to avoid xhr failed requests. https://github.com/prebid/Prebid.js/issues/2648 - if (!config.getConfig('disableAjaxTimeout')) { - x.ontimeout = function () { - logError(' xhr timeout after ', x.timeout, 'ms'); - }; +export const dep = { + fetch: window.fetch.bind(window), + makeRequest: (r, o) => new Request(r, o), + timeout(timeout, resource) { + const ctl = new AbortController(); + let cancelTimer = setTimeout(() => { + ctl.abort(); + logError(`Request timeout after ${timeout}ms`, resource); + cancelTimer = null; + }, timeout); + return { + signal: ctl.signal, + done() { + cancelTimer && clearTimeout(cancelTimer) } + } + } +} - if (method === 'GET' && data) { - let urlInfo = parseUrl(url, options); - Object.assign(urlInfo.search, data); - url = buildUrl(urlInfo); - } +const GET = 'GET'; +const POST = 'POST'; +const CTYPE = 'Content-Type'; - x.open(method, url, true); - // IE needs timeout to be set after open - see #1410 - // Disabled timeout temporarily to avoid xhr failed requests. https://github.com/prebid/Prebid.js/issues/2648 - if (!config.getConfig('disableAjaxTimeout')) { - x.timeout = timeout; - } +/** + * transform legacy `ajax` parameters into a fetch request. + * @returns {Request} + */ +export function toFetchRequest(url, data, options = {}) { + const method = options.method || (data ? POST : GET); + if (method === GET && data) { + const urlInfo = parseUrl(url, options); + Object.assign(urlInfo.search, data); + url = buildUrl(urlInfo); + } + const headers = new Headers(options.customHeaders); + headers.set(CTYPE, options.contentType || 'text/plain'); + const rqOpts = { + method, + headers + } + if (method !== GET && data) { + rqOpts.body = data; + } + if (options.withCredentials) { + rqOpts.credentials = 'include'; + } + if (options.browsingTopics && isSecureContext) { + // the Request constructor will throw an exception if the browser supports topics + // but we're not in a secure context + rqOpts.browsingTopics = true; + } + return dep.makeRequest(url, rqOpts); +} - if (options.withCredentials) { - x.withCredentials = true; - } - _each(options.customHeaders, (value, header) => { - x.setRequestHeader(header, value); - }); - if (options.preflight) { - x.setRequestHeader('X-Requested-With', 'XMLHttpRequest'); - } - x.setRequestHeader('Content-Type', options.contentType || 'text/plain'); +/** + * Return a version of `fetch` that automatically cancels requests after `timeout` milliseconds. + * + * If provided, `request` and `done` should be functions accepting a single argument. + * `request` is invoked at the beginning of each request, and `done` at the end; both are passed its origin. + * + * @returns {function(*, {}?): Promise} + */ +export function fetcherFactory(timeout = 3000, {request, done} = {}) { + let fetcher = (resource, options) => { + let to; + if (timeout != null && options?.signal == null && !config.getConfig('disableAjaxTimeout')) { + to = dep.timeout(timeout, resource); + options = Object.assign({signal: to.signal}, options); + } + let pm = dep.fetch(resource, options); + if (to?.done != null) pm = pm.finally(to.done); + return pm; + }; - if (typeof request === 'function') { - request(parser.origin); - } + if (request != null || done != null) { + fetcher = ((fetch) => function (resource, options) { + const origin = new URL(resource?.url == null ? resource : resource.url, document.location).origin; + let req = fetch(resource, options); + request && request(origin); + if (done) req = req.finally(() => done(origin)); + return req; + })(fetcher); + } + return fetcher; +} - if (method === 'POST' && data) { - x.send(data); - } else { - x.send(); +function toXHR({status, statusText = '', headers, url}, responseText) { + let xml = 0; + function getXML(onError) { + if (xml === 0) { + try { + xml = new DOMParser().parseFromString(responseText, headers?.get(CTYPE)?.split(';')?.[0]) + } catch (e) { + xml = null; + onError && onError(e) } - } catch (error) { - logError('xhr construction', error); - typeof callback === 'object' && callback !== null && callback.error(error); } + return xml; + } + return { + readyState: XMLHttpRequest.DONE, + status, + statusText, + responseText, + response: responseText, + responseType: '', + responseURL: url, + get responseXML() { + return getXML(logError); + }, + getResponseHeader: (header) => headers?.has(header) ? headers.get(header) : null, + toJSON() { + return Object.assign({responseXML: getXML()}, this) + }, + timedOut: false } } + +/** + * attach legacy `ajax` callbacks to a fetch promise. + */ +export function attachCallbacks(fetchPm, callback) { + const {success, error} = typeof callback === 'object' && callback != null ? callback : { + success: typeof callback === 'function' ? callback : () => null, + error: (e, x) => logError('Network error', e, x) + }; + fetchPm.then(response => response.text().then((responseText) => [response, responseText])) + .then(([response, responseText]) => { + const xhr = toXHR(response, responseText); + response.ok || response.status === 304 ? success(responseText, xhr) : error(response.statusText, xhr); + }, (reason) => error('', Object.assign( + toXHR({status: 0}, ''), + {reason, timedOut: reason?.name === 'AbortError'})) + ); +} + +export function ajaxBuilder(timeout = 3000, {request, done} = {}) { + const fetcher = fetcherFactory(timeout, {request, done}); + return function (url, callback, data, options = {}) { + attachCallbacks(fetcher(toFetchRequest(url, data, options)), callback); + }; +} + +export const ajax = ajaxBuilder(); +export const fetch = fetcherFactory(); diff --git a/src/auction.js b/src/auction.js index 69febccc94e..37bb2604167 100644 --- a/src/auction.js +++ b/src/auction.js @@ -4,19 +4,26 @@ * In Prebid 0.x, $$PREBID_GLOBAL$$ had _bidsRequested and _bidsReceived as public properties. * Starting 1.0, Prebid will support concurrent auctions. Each auction instance will store private properties, bidsRequested and bidsReceived. * - * AuctionManager will create instance of auction and will store all the auctions. + * AuctionManager will create an instance of auction and will store all the auctions. * */ /** - * @typedef {Object} AdUnit An object containing the adUnit configuration. - * - * @property {string} code A code which will be used to uniquely identify this bidder. This should be the same - * one as is used in the call to registerBidAdapter - * @property {Array.} sizes A list of size for adUnit. - * @property {object} params Any bidder-specific params which the publisher used in their bid request. - * This is guaranteed to have passed the spec.areParamsValid() test. - */ + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid + * @typedef {import('../src/config.js').MediaTypePriceGranularity} MediaTypePriceGranularity + * @typedef {import('../src/mediaTypes.js').MediaType} MediaType + */ + +/** + * @typedef {Object} AdUnit An object containing the adUnit configuration. + * + * @property {string} code A code which will be used to uniquely identify this bidder. This should be the same + * one as is used in the call to registerBidAdapter + * @property {Array.} sizes A list of size for adUnit. + * @property {object} params Any bidder-specific params which the publisher used in their bid request. + * This is guaranteed to have passed the spec.areParamsValid() test. + */ /** * @typedef {Array.} size @@ -58,11 +65,8 @@ */ import { - _each, - adUnitsFilter, - bind, + callBurl, deepAccess, - flatten, generateUUID, getValue, isEmpty, @@ -90,7 +94,7 @@ import {bidderSettings} from './bidderSettings.js'; import * as events from './events.js'; import adapterManager from './adapterManager.js'; import CONSTANTS from './constants.json'; -import {GreedyPromise} from './utils/promise.js'; +import {defer, GreedyPromise} from './utils/promise.js'; import {useMetrics} from './utils/perfMetrics.js'; import {adjustCpm} from './utils/cpm.js'; import {getGlobal} from './prebidGlobal.js'; @@ -122,19 +126,20 @@ export function resetAuctionState() { } /** - * Creates new auction instance - * - * @param {Object} requestConfig - * @param {AdUnit} requestConfig.adUnits - * @param {AdUnitCode} requestConfig.adUnitCodes - * @param {function():void} requestConfig.callback - * @param {number} requestConfig.cbTimeout - * @param {Array.} requestConfig.labels - * @param {string} requestConfig.auctionId - * @param {{global: {}, bidder: {}}} ortb2Fragments first party data, separated into global - * (from getConfig('ortb2') + requestBids({ortb2})) and bidder (a map from bidderCode to ortb2) - * @returns {Auction} auction instance - */ + * Creates new auction instance + * + * @param {Object} requestConfig + * @param {AdUnit} requestConfig.adUnits + * @param {AdUnitCode} requestConfig.adUnitCodes + * @param {function():void} requestConfig.callback + * @param {number} requestConfig.cbTimeout + * @param {Array.} requestConfig.labels + * @param {string} requestConfig.auctionId + * @param {{global: {}, bidder: {}}} requestConfig.ortb2Fragments first party data, separated into global + * (from getConfig('ortb2') + requestBids({ortb2})) and bidder (a map from bidderCode to ortb2) + * @param {Object} requestConfig.metrics + * @returns {Auction} auction instance + */ export function newAuction({adUnits, adUnitCodes, callback, cbTimeout, labels, auctionId, ortb2Fragments, metrics}) { metrics = useMetrics(metrics); const _adUnits = adUnits; @@ -142,7 +147,8 @@ export function newAuction({adUnits, adUnitCodes, callback, cbTimeout, labels, a const _adUnitCodes = adUnitCodes; const _auctionId = auctionId || generateUUID(); const _timeout = cbTimeout; - const _timelyBidders = new Set(); + const _timelyRequests = new Set(); + const done = defer(); let _bidsRejected = []; let _callback = callback; let _bidderRequests = []; @@ -151,7 +157,7 @@ export function newAuction({adUnits, adUnitCodes, callback, cbTimeout, labels, a let _winningBids = []; let _auctionStart; let _auctionEnd; - let _timer; + let _timeoutTimer; let _auctionStatus; let _nonBids = []; @@ -182,25 +188,22 @@ export function newAuction({adUnits, adUnitCodes, callback, cbTimeout, labels, a } function startAuctionTimer() { - const timedOut = true; - const timeoutCallback = executeCallback.bind(null, timedOut); - let timer = setTimeout(timeoutCallback, _timeout); - _timer = timer; + _timeoutTimer = setTimeout(() => executeCallback(true), _timeout); } - function executeCallback(timedOut, cleartimer) { - // clear timer when done calls executeCallback - if (cleartimer) { - clearTimeout(_timer); + function executeCallback(timedOut) { + if (!timedOut) { + clearTimeout(_timeoutTimer); + } else { + events.emit(CONSTANTS.EVENTS.AUCTION_TIMEOUT, getProperties()); } - if (_auctionEnd === undefined) { - let timedOutBidders = []; + let timedOutRequests = []; if (timedOut) { logMessage(`Auction ${_auctionId} timedOut`); - timedOutBidders = getTimedOutBids(_bidderRequests, _timelyBidders); - if (timedOutBidders.length) { - events.emit(CONSTANTS.EVENTS.BID_TIMEOUT, timedOutBidders); + timedOutRequests = _bidderRequests.filter(rq => !_timelyRequests.has(rq.bidderRequestId)).flatMap(br => br.bids) + if (timedOutRequests.length) { + events.emit(CONSTANTS.EVENTS.BID_TIMEOUT, timedOutRequests); } } @@ -209,14 +212,14 @@ export function newAuction({adUnits, adUnitCodes, callback, cbTimeout, labels, a metrics.checkpoint('auctionEnd'); metrics.timeBetween('requestBids', 'auctionEnd', 'requestBids.total'); metrics.timeBetween('callBids', 'auctionEnd', 'requestBids.callBids'); + done.resolve(); events.emit(CONSTANTS.EVENTS.AUCTION_END, getProperties()); bidsBackCallback(_adUnits, function () { try { if (_callback != null) { - const adUnitCodes = _adUnitCodes; const bids = _bidsReceived - .filter(bind.call(adUnitsFilter, this, adUnitCodes)) + .filter(bid => _adUnitCodes.includes(bid.adUnitCode)) .reduce(groupByPlacement, {}); _callback.apply(pbjsInstance, [bids, timedOut, _auctionId]); _callback = null; @@ -225,8 +228,8 @@ export function newAuction({adUnits, adUnitCodes, callback, cbTimeout, labels, a logError('Error executing bidsBackHandler', null, e); } finally { // Calling timed out bidders - if (timedOutBidders.length) { - adapterManager.callTimedOutBidders(adUnits, timedOutBidders, _timeout); + if (timedOutRequests.length) { + adapterManager.callTimedOutBidders(adUnits, timedOutRequests, _timeout); } // Only automatically sync if the publisher has not chosen to "enableOverride" let userSyncConfig = config.getConfig('userSync') || {}; @@ -244,11 +247,11 @@ export function newAuction({adUnits, adUnitCodes, callback, cbTimeout, labels, a // when all bidders have called done callback atleast once it means auction is complete logInfo(`Bids Received for Auction with id: ${_auctionId}`, _bidsReceived); _auctionStatus = AUCTION_COMPLETED; - executeCallback(false, true); + executeCallback(false); } - function onTimelyResponse(bidderCode) { - _timelyBidders.add(bidderCode); + function onTimelyResponse(bidderRequestId) { + _timelyRequests.add(bidderRequestId); } function callBids() { @@ -368,6 +371,7 @@ export function newAuction({adUnits, adUnitCodes, callback, cbTimeout, labels, a function addWinningBid(winningBid) { const winningAd = adUnits.find(adUnit => adUnit.transactionId === winningBid.transactionId); _winningBids = _winningBids.concat(winningBid); + callBurl(winningBid); adapterManager.callBidWonBidder(winningBid.adapterCode || winningBid.bidder, winningBid, adUnits); if (winningAd && !winningAd.deferBilling) adapterManager.callBidBillableBidder(winningBid); } @@ -386,12 +390,12 @@ export function newAuction({adUnits, adUnitCodes, callback, cbTimeout, labels, a addBidReceived, addBidRejected, addNoBid, - executeCallback, callBids, addWinningBid, setBidTargeting, getWinningBids: () => _winningBids, getAuctionStart: () => _auctionStart, + getAuctionEnd: () => _auctionEnd, getTimeout: () => _timeout, getAuctionId: () => _auctionId, getAuctionStatus: () => _auctionStatus, @@ -403,20 +407,30 @@ export function newAuction({adUnits, adUnitCodes, callback, cbTimeout, labels, a getNonBids: () => _nonBids, getFPD: () => ortb2Fragments, getMetrics: () => metrics, + end: done.promise }; } /** * Hook into this to intercept bids before they are added to an auction. * + * @type {Function} * @param adUnitCode * @param bid - * @param {function(String)} reject: a function that, when called, rejects `bid` with the given reason. + * @param {function(String): void} reject a function that, when called, rejects `bid` with the given reason. */ export const addBidResponse = hook('sync', function(adUnitCode, bid, reject) { this.dispatch.call(null, adUnitCode, bid); }, 'addBidResponse'); +/** + * Delay hook for adapter responses. + * + * `ready` is a promise; auctions wait for it to resolve before closing. Modules can hook into this + * to delay the end of auctions while they perform initialization that does not need to delay their start. + */ +export const responsesReady = hook('sync', (ready) => ready, 'responsesReady'); + export const addBidderRequests = hook('sync', function(bidderRequests) { this.dispatch.call(this.context, bidderRequests); }, 'addBidderRequests'); @@ -432,32 +446,6 @@ export function auctionCallbacks(auctionDone, auctionInstance, {index = auctionM let allAdapterCalledDone = false; let bidderRequestsDone = new Set(); let bidResponseMap = {}; - const ready = {}; - - function waitFor(requestId, result) { - if (ready[requestId] == null) { - ready[requestId] = GreedyPromise.resolve(); - } - ready[requestId] = ready[requestId].then(() => GreedyPromise.resolve(result).catch(() => {})) - } - - function guard(bidderRequest, fn) { - let timeout = bidderRequest.timeout; - if (timeout == null || timeout > auctionInstance.getTimeout()) { - timeout = auctionInstance.getTimeout(); - } - const timeRemaining = auctionInstance.getAuctionStart() + timeout - Date.now(); - const wait = ready[bidderRequest.bidderRequestId]; - const orphanWait = ready['']; // also wait for "orphan" responses that are not associated with any request - if ((wait != null || orphanWait != null) && timeRemaining > 0) { - GreedyPromise.race([ - GreedyPromise.timeout(timeRemaining), - GreedyPromise.resolve(orphanWait).then(() => wait) - ]).then(fn); - } else { - fn(); - } - } function afterBidAdded() { outstandingBidsAdded--; @@ -476,7 +464,7 @@ export function auctionCallbacks(auctionDone, auctionInstance, {index = auctionM function acceptBidResponse(adUnitCode, bid) { handleBidResponse(adUnitCode, bid, (done) => { let bidResponse = getPreparedBidForAuction(bid); - + events.emit(CONSTANTS.EVENTS.BID_ACCEPTED, bidResponse); if (FEATURES.VIDEO && bidResponse.mediaType === VIDEO) { tryAddVideoBid(auctionInstance, bidResponse, done); } else { @@ -532,8 +520,7 @@ export function auctionCallbacks(auctionDone, auctionInstance, {index = auctionM return { addBidResponse: (function () { function addBid(adUnitCode, bid) { - const bidderRequest = index.getBidderRequest(bid); - waitFor((bidderRequest && bidderRequest.bidderRequestId) || '', addBidResponse.call({ + addBidResponse.call({ dispatch: acceptBidResponse, }, adUnitCode, bid, (() => { let rejected = false; @@ -543,23 +530,17 @@ export function auctionCallbacks(auctionDone, auctionInstance, {index = auctionM rejected = true; } } - })())); + })()) } addBid.reject = rejectBidResponse; return addBid; })(), adapterDone: function () { - guard(this, adapterDone.bind(this)) + responsesReady(GreedyPromise.resolve()).finally(() => adapterDone.call(this)); } } } -export function doCallbacksIfTimedout(auctionInstance, bidResponse) { - if (bidResponse.timeToRespond > auctionInstance.getTimeout() + config.getConfig('timeoutBuffer')) { - auctionInstance.executeCallback(true); - } -} - // Add a bid to the auction. export function addBidToAuction(auctionInstance, bidResponse) { setupBidTargeting(bidResponse); @@ -567,8 +548,6 @@ export function addBidToAuction(auctionInstance, bidResponse) { useMetrics(bidResponse.metrics).timeSince('addBidResponse', 'addBidResponse.total'); auctionInstance.addBidReceived(bidResponse); events.emit(CONSTANTS.EVENTS.BID_RESPONSE, bidResponse); - - doCallbacksIfTimedout(auctionInstance, bidResponse); } // Video bids may fail if the cache is down, or there's trouble on the network. @@ -615,16 +594,11 @@ const _storeInCache = (batch) => { const { auctionInstance, bidResponse, afterBidAdded } = batch[i]; if (error) { logWarn(`Failed to save to the video cache: ${error}. Video bid must be discarded.`); - - doCallbacksIfTimedout(auctionInstance, bidResponse); } else { if (cacheId.uuid === '') { logWarn(`Supplied video cache key was already in use by Prebid Cache; caching attempt was rejected. Video bid must be discarded.`); - - doCallbacksIfTimedout(auctionInstance, bidResponse); } else { bidResponse.videoCacheKey = cacheId.uuid; - if (!bidResponse.vastUrl) { bidResponse.vastUrl = getCacheUrl(bidResponse.videoCacheKey); } @@ -785,8 +759,9 @@ export function getMediaTypeGranularity(mediaType, mediaTypes, mediaTypePriceGra /** * This function returns the price granularity defined. It can be either publisher defined or default value - * @param bid bid response object - * @param index + * @param {Bid} bid bid response object + * @param {object} obj + * @param {object} obj.index * @returns {string} granularity */ export const getPriceGranularity = (bid, {index = auctionManager.index} = {}) => { @@ -820,6 +795,16 @@ export const getPriceByGranularity = (granularity) => { } } +/** + * This function returns a function to get crid from bid response + * @returns {function} + */ +export const getCreativeId = () => { + return (bid) => { + return (bid.creativeId) ? bid.creativeId : ''; + } +} + /** * This function returns a function to get first advertiser domain from bid response meta * @returns {function} @@ -830,6 +815,16 @@ export const getAdvertiserDomain = () => { } } +/** + * This function returns a function to get dsp name or id from bid response meta + * @returns {function} + */ +export const getDSP = () => { + return (bid) => { + return (bid.meta && (bid.meta.networkId || bid.meta.networkName)) ? deepAccess(bid, 'meta.networkName') || deepAccess(bid, 'meta.networkId') : ''; + } +} + /** * This function returns a function to get the primary category id from bid response meta * @returns {function} @@ -866,13 +861,14 @@ function defaultAdserverTargeting() { createKeyVal(TARGETING_KEYS.FORMAT, 'mediaType'), createKeyVal(TARGETING_KEYS.ADOMAIN, getAdvertiserDomain()), createKeyVal(TARGETING_KEYS.ACAT, getPrimaryCatId()), + createKeyVal(TARGETING_KEYS.DSP, getDSP()), + createKeyVal(TARGETING_KEYS.CRID, getCreativeId()), ] } /** * @param {string} mediaType * @param {string} bidderCode - * @param {BidRequest} bidReq * @returns {*} */ export function getStandardBidderSettings(mediaType, bidderCode) { @@ -939,7 +935,7 @@ function setKeys(keyValues, bidderSettings, custBidObj, bidReq) { var targeting = bidderSettings[CONSTANTS.JSON_MAPPING.ADSERVER_TARGETING]; custBidObj.size = custBidObj.getSize(); - _each(targeting, function (kvPair) { + (targeting || []).forEach(function (kvPair) { var key = kvPair.key; var value = kvPair.val; @@ -957,7 +953,7 @@ function setKeys(keyValues, bidderSettings, custBidObj, bidReq) { if ( ((typeof bidderSettings.suppressEmptyKeys !== 'undefined' && bidderSettings.suppressEmptyKeys === true) || - key === CONSTANTS.TARGETING_KEYS.DEAL || key === CONSTANTS.TARGETING_KEYS.ACAT) && // hb_deal & hb_acat are suppressed automatically if not set + key === CONSTANTS.TARGETING_KEYS.DEAL || key === CONSTANTS.TARGETING_KEYS.ACAT || key === CONSTANTS.TARGETING_KEYS.DSP || key === CONSTANTS.TARGETING_KEYS.CRID) && // hb_deal & hb_acat are suppressed automatically if not set ( isEmptyStr(value) || value === null || @@ -992,24 +988,3 @@ function groupByPlacement(bidsByPlacement, bid) { bidsByPlacement[bid.adUnitCode].bids.push(bid); return bidsByPlacement; } - -/** - * Returns a list of bids that we haven't received a response yet where the bidder did not call done - * @param {BidRequest[]} bidderRequests List of bids requested for auction instance - * @param {Set} timelyBidders Set of bidders which responded in time - * - * @typedef {Object} TimedOutBid - * @property {string} bidId The id representing the bid - * @property {string} bidder The string name of the bidder - * @property {string} adUnitCode The code used to uniquely identify the ad unit on the publisher's page - * @property {string} auctionId The id representing the auction - * - * @return {Array} List of bids that Prebid hasn't received a response for - */ -function getTimedOutBids(bidderRequests, timelyBidders) { - const timedOutBids = bidderRequests - .map(bid => (bid.bids || []).filter(bid => !timelyBidders.has(bid.bidder))) - .reduce(flatten, []); - - return timedOutBids; -} diff --git a/src/auctionIndex.js b/src/auctionIndex.js index bdd2b42f9c6..bb3f535b41a 100644 --- a/src/auctionIndex.js +++ b/src/auctionIndex.js @@ -1,24 +1,30 @@ +/** + * @typedef {Object} AuctionIndex + * + * @property {function({ auctionId: * }): *} getAuction Returns auction instance for `auctionId` + * @property {function({ transactionId: * }): *} getAdUnit Returns `adUnit` object for `transactionId`. + * You should prefer `getMediaTypes` for looking up bid media types. + * @property {function({ transactionId: *, requestId: * }): *} getMediaTypes Returns mediaTypes object from bidRequest (through `requestId`) falling back to the adUnit (through `transactionId`). + * The bidRequest is given precedence because its mediaTypes can differ from the adUnit's (if bidder-specific labels are in use). + * Bids that have no associated request do not have labels either, and use the adUnit's mediaTypes. + * @property {function({ requestId: *, bidderRequestId: * }): *} getBidderRequest Returns bidderRequest that matches both requestId and bidderRequestId (if either or both are provided). + * Bid responses are not guaranteed to have a corresponding request. + * @property {function({ requestId: * }): *} getBidRequest Returns bidRequest object for requestId. + * Bid responses are not guaranteed to have a corresponding request. + */ + /** * Retrieves request-related bid data. * All methods are designed to work with Bid (response) objects returned by bid adapters. */ export function AuctionIndex(getAuctions) { Object.assign(this, { - /** - * @param auctionId - * @returns {*} Auction instance for `auctionId` - */ getAuction({auctionId}) { if (auctionId != null) { return getAuctions() .find(auction => auction.getAuctionId() === auctionId); } }, - /** - * NOTE: you should prefer {@link #getMediaTypes} for looking up bid media types. - * @param transactionId - * @returns adUnit object for `transactionId` - */ getAdUnit({transactionId}) { if (transactionId != null) { return getAuctions() @@ -26,14 +32,6 @@ export function AuctionIndex(getAuctions) { .find(au => au.transactionId === transactionId); } }, - /** - * @param transactionId - * @param requestId? - * @returns {*} mediaTypes object from bidRequest (through requestId) falling back to the adUnit (through transactionId). - * - * The bidRequest is given precedence because its mediaTypes can differ from the adUnit's (if bidder-specific labels are in use). - * Bids that have no associated request do not have labels either, and use the adUnit's mediaTypes. - */ getMediaTypes({transactionId, requestId}) { if (requestId != null) { const req = this.getBidRequest({requestId}); @@ -47,13 +45,6 @@ export function AuctionIndex(getAuctions) { } } }, - /** - * @param requestId? - * @param bidderRequestId? - * @returns {*} bidderRequest that matches both requestId and bidderRequestId (if either or both are provided). - * - * NOTE: Bid responses are not guaranteed to have a corresponding request. - */ getBidderRequest({requestId, bidderRequestId}) { if (requestId != null || bidderRequestId != null) { let bers = getAuctions().flatMap(a => a.getBidRequests()); @@ -67,12 +58,6 @@ export function AuctionIndex(getAuctions) { } } }, - /** - * @param requestId - * @returns {*} bidRequest object for requestId - * - * NOTE: Bid responses are not guaranteed to have a corresponding request. - */ getBidRequest({requestId}) { if (requestId != null) { return getAuctions() diff --git a/src/auctionManager.js b/src/auctionManager.js index 90d5fb543e2..8f3cbb56333 100644 --- a/src/auctionManager.js +++ b/src/auctionManager.js @@ -17,14 +17,19 @@ * @property {function(): Object} getStandardBidderAdServerTargeting - returns standard bidder targeting for all the adapters. Refer http://prebid.org/dev-docs/publisher-api-reference.html#module_pbjs.bidderSettings for more details * @property {function(Object): void} addWinningBid - add a winning bid to an auction based on auctionId * @property {function(): void} clearAllAuctions - clear all auctions for testing + * @property {AuctionIndex} index */ -import { uniques, flatten, logWarn } from './utils.js'; +import { uniques, logWarn } from './utils.js'; import { newAuction, getStandardBidderSettings, AUCTION_COMPLETED } from './auction.js'; -import {find} from './polyfill.js'; import {AuctionIndex} from './auctionIndex.js'; import CONSTANTS from './constants.json'; import {useMetrics} from './utils/perfMetrics.js'; +import {ttlCollection} from './utils/ttlCollection.js'; +import {getTTL, onTTLBufferChange} from './bidTTL.js'; +import {config} from './config.js'; + +const CACHE_TTL_SETTING = 'minBidCacheTTL'; /** * Creates new instance of auctionManager. There will only be one instance of auctionManager but @@ -33,15 +38,42 @@ import {useMetrics} from './utils/perfMetrics.js'; * @returns {AuctionManager} auctionManagerInstance */ export function newAuctionManager() { - const _auctions = []; + let minCacheTTL = null; + + const _auctions = ttlCollection({ + startTime: (au) => au.end.then(() => au.getAuctionEnd()), + ttl: (au) => minCacheTTL == null ? null : au.end.then(() => { + return Math.max(minCacheTTL, ...au.getBidsReceived().map(getTTL)) * 1000 + }), + }); + + onTTLBufferChange(() => { + if (minCacheTTL != null) _auctions.refresh(); + }) + + config.getConfig(CACHE_TTL_SETTING, (cfg) => { + const prev = minCacheTTL; + minCacheTTL = cfg?.[CACHE_TTL_SETTING]; + minCacheTTL = typeof minCacheTTL === 'number' ? minCacheTTL : null; + if (prev !== minCacheTTL) { + _auctions.refresh(); + } + }) + const auctionManager = {}; + function getAuction(auctionId) { + for (const auction of _auctions) { + if (auction.getAuctionId() === auctionId) return auction; + } + } + auctionManager.addWinningBid = function(bid) { const metrics = useMetrics(bid.metrics); metrics.checkpoint('bidWon'); metrics.timeBetween('auctionEnd', 'bidWon', 'render.pending'); metrics.timeBetween('requestBids', 'bidWon', 'render.e2e'); - const auction = find(_auctions, auction => auction.getAuctionId() === bid.auctionId); + const auction = getAuction(bid.auctionId); if (auction) { bid.status = CONSTANTS.BID_STATUS.RENDERED; auction.addWinningBid(bid); @@ -50,48 +82,44 @@ export function newAuctionManager() { } }; - auctionManager.getAllWinningBids = function() { - return _auctions.map(auction => auction.getWinningBids()) - .reduce(flatten, []); - }; - - auctionManager.getBidsRequested = function() { - return _auctions.map(auction => auction.getBidRequests()) - .reduce(flatten, []); - }; - - auctionManager.getNoBids = function() { - return _auctions.map(auction => auction.getNoBids()) - .reduce(flatten, []); - }; - - auctionManager.getBidsReceived = function() { - return _auctions.map((auction) => { - if (auction.getAuctionStatus() === AUCTION_COMPLETED) { - return auction.getBidsReceived(); + Object.entries({ + getAllWinningBids: { + name: 'getWinningBids', + }, + getBidsRequested: { + name: 'getBidRequests' + }, + getNoBids: {}, + getAdUnits: {}, + getBidsReceived: { + pre(auction) { + return auction.getAuctionStatus() === AUCTION_COMPLETED; } - }).reduce(flatten, []) - .filter(bid => bid); - }; + }, + getAdUnitCodes: { + post: uniques, + } + }).forEach(([mgrMethod, {name = mgrMethod, pre, post}]) => { + const mapper = pre == null + ? (auction) => auction[name]() + : (auction) => pre(auction) ? auction[name]() : []; + const filter = post == null + ? (items) => items + : (items) => items.filter(post) + auctionManager[mgrMethod] = () => { + return filter(_auctions.toArray().flatMap(mapper)); + } + }) + + function allBidsReceived() { + return _auctions.toArray().flatMap(au => au.getBidsReceived()) + } auctionManager.getAllBidsForAdUnitCode = function(adUnitCode) { - return _auctions.map((auction) => { - return auction.getBidsReceived(); - }).reduce(flatten, []) + return allBidsReceived() .filter(bid => bid && bid.adUnitCode === adUnitCode) }; - auctionManager.getAdUnits = function() { - return _auctions.map(auction => auction.getAdUnits()) - .reduce(flatten, []); - }; - - auctionManager.getAdUnitCodes = function() { - return _auctions.map(auction => auction.getAdUnitCodes()) - .reduce(flatten, []) - .filter(uniques); - }; - auctionManager.createAuction = function(opts) { const auction = newAuction(opts); _addAuction(auction); @@ -99,7 +127,8 @@ export function newAuctionManager() { }; auctionManager.findBidByAdId = function(adId) { - return find(_auctions.map(auction => auction.getBidsReceived()).reduce(flatten, []), bid => bid.adId === adId); + return allBidsReceived() + .find(bid => bid.adId === adId); }; auctionManager.getStandardBidderAdServerTargeting = function() { @@ -111,24 +140,25 @@ export function newAuctionManager() { if (bid) bid.status = status; if (bid && status === CONSTANTS.BID_STATUS.BID_TARGETING_SET) { - const auction = find(_auctions, auction => auction.getAuctionId() === bid.auctionId); + const auction = getAuction(bid.auctionId); if (auction) auction.setBidTargeting(bid); } } auctionManager.getLastAuctionId = function() { - return _auctions.length && _auctions[_auctions.length - 1].getAuctionId() + const auctions = _auctions.toArray(); + return auctions.length && auctions[auctions.length - 1].getAuctionId() }; auctionManager.clearAllAuctions = function() { - _auctions.length = 0; + _auctions.clear(); } function _addAuction(auction) { - _auctions.push(auction); + _auctions.add(auction); } - auctionManager.index = new AuctionIndex(() => _auctions); + auctionManager.index = new AuctionIndex(() => _auctions.toArray()); return auctionManager; } diff --git a/src/bidTTL.js b/src/bidTTL.js new file mode 100644 index 00000000000..55ba0c026b0 --- /dev/null +++ b/src/bidTTL.js @@ -0,0 +1,25 @@ +import {config} from './config.js'; +import {logError} from './utils.js'; +let TTL_BUFFER = 1; + +const listeners = []; + +config.getConfig('ttlBuffer', (cfg) => { + if (typeof cfg.ttlBuffer === 'number') { + const prev = TTL_BUFFER; + TTL_BUFFER = cfg.ttlBuffer; + if (prev !== TTL_BUFFER) { + listeners.forEach(l => l(TTL_BUFFER)) + } + } else { + logError('Invalid value for ttlBuffer', cfg.ttlBuffer); + } +}) + +export function getTTL(bid) { + return bid.ttl - (bid.hasOwnProperty('ttlBuffer') ? bid.ttlBuffer : TTL_BUFFER); +} + +export function onTTLBufferChange(listener) { + listeners.push(listener); +} diff --git a/src/config.js b/src/config.js index 48909d677e9..e3bb5f146ed 100644 --- a/src/config.js +++ b/src/config.js @@ -12,11 +12,20 @@ * @property {(string|Object)} [video-outstream] */ -import { isValidPriceConfig } from './cpmBucketManager.js'; -import {find, includes, arrayFrom as from} from './polyfill.js'; +import {isValidPriceConfig} from './cpmBucketManager.js'; +import {arrayFrom as from, find, includes} from './polyfill.js'; import { - mergeDeep, deepClone, getParameterByName, isPlainObject, logMessage, logWarn, logError, - isArray, isStr, isBoolean, deepAccess, bind + deepAccess, + deepClone, + getParameterByName, + isArray, + isBoolean, + isPlainObject, + isStr, + logError, + logMessage, + logWarn, + mergeDeep } from './utils.js'; import CONSTANTS from './constants.json'; @@ -50,13 +59,6 @@ const GRANULARITY_OPTIONS = { const ALL_TOPICS = '*'; -/** - * @typedef {object} PrebidConfig - * - * @property {string} cache.url Set a url if we should use prebid-cache to store video bids before adding - * bids to the auction. **NOTE** This must be set if you want to use the dfpAdServerVideo module. - */ - export function newConfig() { let listeners = []; let defaults; @@ -505,7 +507,7 @@ export function newConfig() { return function(cb) { return function(...args) { if (typeof cb === 'function') { - return runWithBidder(bidder, bind.call(cb, this, ...args)) + return runWithBidder(bidder, cb.bind(this, ...args)) } else { logWarn('config.callbackWithBidder callback is not a function'); } @@ -542,4 +544,8 @@ export function newConfig() { }; } +/** + * Set a `cache.url` if we should use prebid-cache to store video bids before adding bids to the auction. + * This must be set if you want to use the dfpAdServerVideo module. + */ export const config = newConfig(); diff --git a/src/consentHandler.js b/src/consentHandler.js index 4776a8ece02..5b5d8b805cd 100644 --- a/src/consentHandler.js +++ b/src/consentHandler.js @@ -1,5 +1,6 @@ -import {isStr, timestamp} from './utils.js'; +import {cyrb53Hash, isStr, timestamp} from './utils.js'; import {defer, GreedyPromise} from './utils/promise.js'; +import {config} from './config.js'; /** * Placeholder gvlid for when vendor consent is not required. When this value is used as gvlid, the gdpr @@ -9,12 +10,23 @@ import {defer, GreedyPromise} from './utils/promise.js'; */ export const VENDORLESS_GVLID = Object.freeze({}); +/** + * Placeholder gvlid for when device.ext.cdep is present (Privacy Sandbox cookie deprecation label). When this value is used as gvlid, the gdpr + * enforcement module will look to see that publisher consent was given. + * + * see https://github.com/prebid/Prebid.js/issues/10516 + */ +export const FIRST_PARTY_GVLID = Object.freeze({}); + export class ConsentHandler { #enabled; #data; #defer; #ready; + #dirty = true; + #hash; generatedTime; + hashFields; constructor() { this.reset(); @@ -74,15 +86,24 @@ export class ConsentHandler { setConsentData(data, time = timestamp()) { this.generatedTime = time; + this.#dirty = true; this.#resolve(data); } getConsentData() { return this.#data; } + + get hash() { + if (this.#dirty) { + this.#hash = cyrb53Hash(JSON.stringify(this.#data && this.hashFields ? this.hashFields.map(f => this.#data[f]) : this.#data)) + this.#dirty = false; + } + return this.#hash; + } } -export class UspConsentHandler extends ConsentHandler { +class UspConsentHandler extends ConsentHandler { getConsentMeta() { const consentData = this.getConsentData(); if (consentData && this.generatedTime) { @@ -94,7 +115,8 @@ export class UspConsentHandler extends ConsentHandler { } } -export class GdprConsentHandler extends ConsentHandler { +class GdprConsentHandler extends ConsentHandler { + hashFields = ['gdprApplies', 'consentString'] getConsentMeta() { const consentData = this.getConsentData(); if (consentData && consentData.vendorData && this.generatedTime) { @@ -108,7 +130,8 @@ export class GdprConsentHandler extends ConsentHandler { } } -export class GppConsentHandler extends ConsentHandler { +class GppConsentHandler extends ConsentHandler { + hashFields = ['applicableSections', 'gppString']; getConsentMeta() { const consentData = this.getConsentData(); if (consentData && this.generatedTime) { @@ -159,4 +182,55 @@ export function gvlidRegistry() { } } +export const gdprDataHandler = new GdprConsentHandler(); +export const uspDataHandler = new UspConsentHandler(); +export const gppDataHandler = new GppConsentHandler(); +export const coppaDataHandler = (() => { + function getCoppa() { + return !!(config.getConfig('coppa')) + } + return { + getCoppa, + getConsentData: getCoppa, + getConsentMeta: getCoppa, + reset() {}, + get promise() { + return GreedyPromise.resolve(getCoppa()) + }, + get hash() { + return getCoppa() ? '1' : '0' + } + } +})(); + export const GDPR_GVLIDS = gvlidRegistry(); + +const ALL_HANDLERS = { + gdpr: gdprDataHandler, + usp: uspDataHandler, + gpp: gppDataHandler, + coppa: coppaDataHandler, +} + +export function multiHandler(handlers = ALL_HANDLERS) { + handlers = Object.entries(handlers); + function collector(method) { + return function () { + return Object.fromEntries(handlers.map(([name, handler]) => [name, handler[method]()])) + } + } + return Object.assign( + { + get promise() { + return GreedyPromise.all(handlers.map(([name, handler]) => handler.promise.then(val => [name, val]))) + .then(entries => Object.fromEntries(entries)); + }, + get hash() { + return cyrb53Hash(handlers.map(([_, handler]) => handler.hash).join(':')); + } + }, + Object.fromEntries(['getConsentData', 'getConsentMeta', 'reset'].map(n => [n, collector(n)])), + ) +} + +export const allConsent = multiHandler(); diff --git a/src/constants.json b/src/constants.json index afc4503cbfb..24882c160f8 100644 --- a/src/constants.json +++ b/src/constants.json @@ -23,6 +23,7 @@ }, "EVENTS": { "AUCTION_INIT": "auctionInit", + "AUCTION_TIMEOUT": "auctionTimeout", "AUCTION_END": "auctionEnd", "BID_ADJUSTMENT": "bidAdjustment", "BID_TIMEOUT": "bidTimeout", @@ -45,7 +46,8 @@ "AUCTION_DEBUG": "auctionDebug", "BID_VIEWABLE": "bidViewable", "STALE_RENDER": "staleRender", - "BILLABLE_EVENT": "billableEvent" + "BILLABLE_EVENT": "billableEvent", + "BID_ACCEPTED": "bidAccepted" }, "AD_RENDER_FAILED_REASON": { "PREVENT_WRITING_ON_MAIN_DOCUMENT": "preventWritingOnMainDocument", @@ -77,7 +79,9 @@ "CACHE_ID": "hb_cache_id", "CACHE_HOST": "hb_cache_host", "ADOMAIN": "hb_adomain", - "ACAT": "hb_acat" + "ACAT": "hb_acat", + "CRID": "hb_crid", + "DSP": "hb_dsp" }, "DEFAULT_TARGETING_KEYS": { "BIDDER": "hb_bidder", @@ -161,11 +165,20 @@ "MAIN": 3 }, "NATIVE_KEYS_THAT_ARE_NOT_ASSETS": [ - "privacyLink", + "privacyIcon", "clickUrl", "sendTargetingKeys", "adTemplate", "rendererUrl", "type" - ] + ], + "FLOOR_VALUES": { + "NO_DATA": "noData", + "AD_UNIT": "adUnit", + "SET_CONFIG": "setConfig", + "FETCH": "fetch", + "SUCCESS": "success", + "ERROR": "error", + "TIMEOUT": "timeout" + } } diff --git a/src/events.js b/src/events.js index 62f8c070deb..d98991180bf 100644 --- a/src/events.js +++ b/src/events.js @@ -3,23 +3,38 @@ */ import * as utils from './utils.js' import CONSTANTS from './constants.json'; +import {ttlCollection} from './utils/ttlCollection.js'; +import {config} from './config.js'; +const TTL_CONFIG = 'eventHistoryTTL'; -var slice = Array.prototype.slice; -var push = Array.prototype.push; +let eventTTL = null; -// define entire events -// var allEvents = ['bidRequested','bidResponse','bidWon','bidTimeout']; -var allEvents = utils._map(CONSTANTS.EVENTS, function (v) { - return v; +// keep a record of all events fired +const eventsFired = ttlCollection({ + monotonic: true, + ttl: () => eventTTL, +}) + +config.getConfig(TTL_CONFIG, (val) => { + const previous = eventTTL; + val = val?.[TTL_CONFIG]; + eventTTL = typeof val === 'number' ? val * 1000 : null; + if (previous !== eventTTL) { + eventsFired.refresh(); + } }); -var idPaths = CONSTANTS.EVENT_ID_PATHS; +let slice = Array.prototype.slice; +let push = Array.prototype.push; + +// define entire events +let allEvents = Object.values(CONSTANTS.EVENTS); + +const idPaths = CONSTANTS.EVENT_ID_PATHS; -// keep a record of all events fired -var eventsFired = []; const _public = (function () { - var _handlers = {}; - var _public = {}; + let _handlers = {}; + let _public = {}; /** * @@ -30,31 +45,30 @@ const _public = (function () { function _dispatch(eventString, args) { utils.logMessage('Emitting event for: ' + eventString); - var eventPayload = args[0] || {}; - var idPath = idPaths[eventString]; - var key = eventPayload[idPath]; - var event = _handlers[eventString] || { que: [] }; - var eventKeys = utils._map(event, function (v, k) { - return k; - }); + let eventPayload = args[0] || {}; + let idPath = idPaths[eventString]; + let key = eventPayload[idPath]; + let event = _handlers[eventString] || { que: [] }; + var eventKeys = Object.keys(event); - var callbacks = []; + let callbacks = []; // record the event: - eventsFired.push({ + eventsFired.add({ eventType: eventString, args: eventPayload, id: key, elapsedTime: utils.getPerformanceNow(), }); - /** Push each specific callback to the `callbacks` array. + /** + * Push each specific callback to the `callbacks` array. * If the `event` map has a key that matches the value of the * event payload id path, e.g. `eventPayload[idPath]`, then apply * each function in the `que` array as an argument to push to the * `callbacks` array - * */ - if (key && utils.contains(eventKeys, key)) { + */ + if (key && eventKeys.includes(key)) { push.apply(callbacks, event[key].que); } @@ -62,24 +76,26 @@ const _public = (function () { push.apply(callbacks, event.que); /** call each of the callbacks */ - utils._each(callbacks, function (fn) { + (callbacks || []).forEach(function (fn) { if (!fn) return; try { fn.apply(null, args); } catch (e) { - utils.logError('Error executing handler:', 'events.js', e); + utils.logError('Error executing handler:', 'events.js', e, eventString); } }); } function _checkAvailableEvent(event) { - return utils.contains(allEvents, event); + return allEvents.includes(event) } + _public.has = _checkAvailableEvent; + _public.on = function (eventString, handler, id) { // check whether available event or not if (_checkAvailableEvent(eventString)) { - var event = _handlers[eventString] || { que: [] }; + let event = _handlers[eventString] || { que: [] }; if (id) { event[id] = event[id] || { que: [] }; @@ -95,12 +111,12 @@ const _public = (function () { }; _public.emit = function (event) { - var args = slice.call(arguments, 1); + let args = slice.call(arguments, 1); _dispatch(event, args); }; _public.off = function (eventString, handler, id) { - var event = _handlers[eventString]; + let event = _handlers[eventString]; if (utils.isEmpty(event) || (utils.isEmpty(event.que) && utils.isEmpty(event[id]))) { return; @@ -111,15 +127,15 @@ const _public = (function () { } if (id) { - utils._each(event[id].que, function (_handler) { - var que = event[id].que; + (event[id].que || []).forEach(function (_handler) { + let que = event[id].que; if (_handler === handler) { que.splice(que.indexOf(_handler), 1); } }); } else { - utils._each(event.que, function (_handler) { - var que = event.que; + (event.que || []).forEach(function (_handler) { + let que = event.que; if (_handler === handler) { que.splice(que.indexOf(_handler), 1); } @@ -142,13 +158,7 @@ const _public = (function () { * @return {Array} array of events fired */ _public.getEvents = function () { - var arrayCopy = []; - utils._each(eventsFired, function (value) { - var newProp = Object.assign({}, value); - arrayCopy.push(newProp); - }); - - return arrayCopy; + return eventsFired.toArray().map(val => Object.assign({}, val)) }; return _public; @@ -156,8 +166,8 @@ const _public = (function () { utils._setEventEmitter(_public.emit.bind(_public)); -export const {on, off, get, getEvents, emit, addEvents} = _public; +export const {on, off, get, getEvents, emit, addEvents, has} = _public; export function clearEvents() { - eventsFired.length = 0; + eventsFired.clear(); } diff --git a/src/fpd/enrichment.js b/src/fpd/enrichment.js index f812d8435d9..911509455e0 100644 --- a/src/fpd/enrichment.js +++ b/src/fpd/enrichment.js @@ -6,6 +6,10 @@ 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'; +import {isActivityAllowed} from '../activities/rules.js'; +import {activityParams} from '../activities/activityParams.js'; +import {ACTIVITY_ACCESS_DEVICE} from '../activities/activities.js'; +import {MODULE_TYPE_PREBID} from '../activities/modules.js'; export const dep = { getRefererInfo, @@ -24,8 +28,10 @@ const oneClient = clientSectionChecker('FPD') * @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 promArr = [fpd, getSUA().catch(() => null), tryToGetCdepLabel().catch(() => null)]; + + return GreedyPromise.all(promArr) + .then(([ortb2, sua, cdep]) => { const ri = dep.getRefererInfo(); mergeLegacySetConfigs(ortb2); Object.entries(ENRICHMENTS).forEach(([section, getEnrichments]) => { @@ -34,9 +40,18 @@ export const enrichFPD = hook('sync', (fpd) => { ortb2[section] = mergeDeep({}, data, ortb2[section]); } }); + if (sua) { deepSetValue(ortb2, 'device.sua', Object.assign({}, sua, ortb2.device.sua)); } + + if (cdep) { + const ext = { + cdep + } + deepSetValue(ortb2, 'device.ext', Object.assign({}, ext, ortb2.device.ext)); + } + ortb2 = oneClient(ortb2); for (let section of CLIENT_SECTIONS) { if (hasSection(ortb2, section)) { @@ -44,6 +59,7 @@ export const enrichFPD = hook('sync', (fpd) => { break; } } + return ortb2; }); }); @@ -78,6 +94,10 @@ function removeUndef(obj) { return getDefinedParams(obj, Object.keys(obj)) } +function tryToGetCdepLabel() { + return GreedyPromise.resolve('cookieDeprecationLabel' in navigator && isActivityAllowed(ACTIVITY_ACCESS_DEVICE, activityParams(MODULE_TYPE_PREBID, 'cdep')) && navigator.cookieDeprecationLabel.getValue()); +} + const ENRICHMENTS = { site(ortb2, ri) { if (CLIENT_SECTIONS.filter(p => p !== 'site').some(hasSection.bind(null, ortb2))) { @@ -93,6 +113,7 @@ const ENRICHMENTS = { 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, diff --git a/src/fpd/sua.js b/src/fpd/sua.js index 30b2be4c13b..565c3e1fd52 100644 --- a/src/fpd/sua.js +++ b/src/fpd/sua.js @@ -15,6 +15,12 @@ export const HIGH_ENTROPY_HINTS = [ 'fullVersionList' ] +export const LOW_ENTROPY_HINTS = [ + 'brands', + 'mobile', + 'platform' +] + /** * Returns low entropy UA client hints encoded as an ortb2.6 device.sua object; or null if no UA client hints are available. */ @@ -32,7 +38,7 @@ export const getLowEntropySUA = lowEntropySUAAccessor(); export const getHighEntropySUA = highEntropySUAAccessor(); export function lowEntropySUAAccessor(uaData = window.navigator?.userAgentData) { - const sua = isEmpty(uaData) ? null : Object.freeze(uaDataToSUA(SUA_SOURCE_LOW_ENTROPY, uaData)); + const sua = (uaData && LOW_ENTROPY_HINTS.some(h => typeof uaData[h] !== 'undefined')) ? Object.freeze(uaDataToSUA(SUA_SOURCE_LOW_ENTROPY, uaData)) : null; return function () { return sua; } @@ -85,7 +91,7 @@ export function uaDataToSUA(source, uaData) { if (uaData.fullVersionList || uaData.brands) { sua.browsers = (uaData.fullVersionList || uaData.brands).map(({brand, version}) => toBrandVersion(brand, version)); } - if (uaData.hasOwnProperty('mobile')) { + if (typeof uaData['mobile'] !== 'undefined') { sua.mobile = uaData.mobile ? 1 : 0; } ['model', 'bitness', 'architecture'].forEach(prop => { diff --git a/src/mediaTypes.js b/src/mediaTypes.js index eea286f7af5..2afa2aefaf9 100644 --- a/src/mediaTypes.js +++ b/src/mediaTypes.js @@ -10,11 +10,11 @@ * @typedef {('adpod')} VideoContext */ -/** @type MediaType */ +/** @type {MediaType} */ export const NATIVE = 'native'; -/** @type MediaType */ +/** @type {MediaType} */ export const VIDEO = 'video'; -/** @type MediaType */ +/** @type {MediaType} */ export const BANNER = 'banner'; -/** @type VideoContext */ +/** @type {VideoContext} */ export const ADPOD = 'adpod'; diff --git a/src/native.js b/src/native.js index 927423c8d72..affdc855353 100644 --- a/src/native.js +++ b/src/native.js @@ -1,7 +1,6 @@ import { deepAccess, deepClone, - getKeyByValue, insertHtmlIntoIframe, isArray, isBoolean, @@ -17,6 +16,11 @@ import {auctionManager} from './auctionManager.js'; import CONSTANTS from './constants.json'; import {NATIVE} from './mediaTypes.js'; +/** + * @typedef {import('../src/adapters/bidderFactory.js').BidRequest} BidRequest + * @typedef {import('../src/adapters/bidderFactory.js').Bid} Bid + */ + export const nativeAdapters = []; export const NATIVE_TARGETING_KEYS = Object.keys(CONSTANTS.NATIVE_KEYS).map( @@ -422,12 +426,14 @@ function assetsMessage(data, adObject, keys, {index = auctionManager.index} = {} return message; } +const NATIVE_KEYS_INVERTED = Object.fromEntries(Object.entries(CONSTANTS.NATIVE_KEYS).map(([k, v]) => [v, k])); + /** * Constructs a message object containing asset values for each of the * requested data keys. */ export function getAssetMessage(data, adObject) { - const keys = data.assets.map((k) => getKeyByValue(CONSTANTS.NATIVE_KEYS, k)); + const keys = data.assets.map((k) => NATIVE_KEYS_INVERTED[k]); return assetsMessage(data, adObject, keys); } @@ -480,6 +486,11 @@ export function toOrtbNativeRequest(legacyNativeAssets) { continue; } + if (key === 'privacyLink') { + ortb.privacy = 1; + continue; + } + const asset = legacyNativeAssets[key]; let required = 0; if (asset.required && isBoolean(asset.required)) { @@ -623,6 +634,9 @@ export function fromOrtbNativeRequest(openRTBRequest) { oldNativeObject[prebidAssetName].len = asset.data.len; } } + if (openRTBRequest.privacy) { + oldNativeObject.privacyLink = { required: false }; + } // video was not supported by old prebid assets } return oldNativeObject; @@ -696,8 +710,11 @@ export function legacyPropertiesToOrtbNative(legacyNative) { // in general, native trackers seem to be neglected and/or broken response.jstracker = Array.isArray(value) ? value.join('') : value; break; + case 'privacyLink': + response.privacy = value; + break; } - }) + }); return response; } @@ -780,8 +797,8 @@ export function toLegacyResponse(ortbResponse, ortbRequest) { legacyResponse.impressionTrackers = []; let jsTrackers = []; - if (ortbRequest?.imptrackers) { - legacyResponse.impressionTrackers.push(...ortbRequest.imptrackers); + if (ortbResponse.imptrackers) { + legacyResponse.impressionTrackers.push(...ortbResponse.imptrackers); } for (const eventTracker of ortbResponse?.eventtrackers || []) { if (eventTracker.event === TRACKER_EVENTS.impression && eventTracker.method === TRACKER_METHODS.img) { diff --git a/src/prebid.js b/src/prebid.js index b949ece65ea..df224ab83cf 100644 --- a/src/prebid.js +++ b/src/prebid.js @@ -2,19 +2,11 @@ import {getGlobal} from './prebidGlobal.js'; import { - adUnitsFilter, - bind, - callBurl, - contains, - createInvisibleIframe, deepAccess, deepClone, deepSetValue, flatten, generateUUID, - getHighestCpm, - inIframe, - insertElement, isArray, isArrayOfNums, isEmpty, @@ -26,8 +18,6 @@ import { logMessage, logWarn, mergeDeep, - replaceAuctionPrice, - replaceClickThrough, transformAdServerTargetingObj, uniques, unsupportedBidderMessage @@ -41,23 +31,24 @@ import {hook, wrapHook} from './hook.js'; import {loadSession} from './debugging.js'; import {includes} from './polyfill.js'; import {adunitCounter} from './adUnits.js'; -import {executeRenderer, isRendererRequired} from './Renderer.js'; import {createBid} from './bidfactory.js'; import {storageCallbacks} from './storageManager.js'; -import {emitAdRenderFail, emitAdRenderSucceeded} from './adRendering.js'; -import {default as adapterManager, gdprDataHandler, getS2SBidderSet, gppDataHandler, uspDataHandler} from './adapterManager.js'; +import {default as adapterManager, getS2SBidderSet} from './adapterManager.js'; import CONSTANTS from './constants.json'; import * as events from './events.js'; import {newMetrics, useMetrics} from './utils/perfMetrics.js'; import {defer, GreedyPromise} from './utils/promise.js'; import {enrichFPD} from './fpd/enrichment.js'; +import {allConsent} from './consentHandler.js'; +import {renderAdDirect} from '../libraries/creativeRender/direct.js'; +import {getHighestCpm} from './utils/reducers.js'; +import {fillVideoDefaults} from './video.js'; const pbjsInstance = getGlobal(); const { triggerUserSyncs } = userSync; /* private variables */ -const { ADD_AD_UNITS, BID_WON, REQUEST_BIDS, SET_TARGETING, STALE_RENDER } = CONSTANTS.EVENTS; -const { PREVENT_WRITING_ON_MAIN_DOCUMENT, NO_AD, EXCEPTION, CANNOT_FIND_AD, MISSING_DOC_OR_ADID } = CONSTANTS.AD_RENDER_FAILED_REASON; +const { ADD_AD_UNITS, REQUEST_BIDS, SET_TARGETING } = CONSTANTS.EVENTS; const eventValidators = { bidWon: checkDefinedPlacement @@ -89,7 +80,7 @@ function checkDefinedPlacement(id) { .reduce(flatten) .filter(uniques); - if (!contains(adUnitCodes, id)) { + if (!adUnitCodes.includes(id)) { logError('The "' + id + '" placement is not defined.'); return; } @@ -97,13 +88,6 @@ function checkDefinedPlacement(id) { return true; } -function setRenderSize(doc, width, height) { - if (doc.defaultView && doc.defaultView.frameElement) { - doc.defaultView.frameElement.width = width; - doc.defaultView.frameElement.height = height; - } -} - function validateSizes(sizes, targLength) { let cleanSizes = []; if (isArray(sizes) && ((targLength) ? sizes.length === targLength : sizes.length > 0)) { @@ -268,6 +252,12 @@ export const checkAdUnitSetup = hook('sync', function (adUnits) { return validatedAdUnits; }, 'checkAdUnitSetup'); +function fillAdUnitDefaults(adUnits) { + if (FEATURES.VIDEO) { + adUnits.forEach(au => fillVideoDefaults(au)) + } +} + /// /////////////////////////////// // // // Start Public APIs // @@ -330,28 +320,14 @@ pbjsInstance.getAdserverTargeting = function (adUnitCode) { return targeting.getAllTargeting(adUnitCode); }; -/** - * returns all consent data - * @return {Object} Map of consent types and data - * @alias module:pbjs.getConsentData - */ -function getConsentMetadata() { - return { - gdpr: gdprDataHandler.getConsentMeta(), - usp: uspDataHandler.getConsentMeta(), - gpp: gppDataHandler.getConsentMeta(), - coppa: !!(config.getConfig('coppa')) - } -} - pbjsInstance.getConsentMetadata = function () { logInfo('Invoking $$PREBID_GLOBAL$$.getConsentMetadata'); - return getConsentMetadata(); + return allConsent.getConsentMeta() }; function getBids(type) { const responses = auctionManager[type]() - .filter(bind.call(adUnitsFilter, this, auctionManager.getAdUnitCodes())); + .filter(bid => auctionManager.getAdUnitCodes().includes(bid.adUnitCode)) // find the last auction id to get responses for most recent auction only const currentAuctionId = auctionManager.getLastAuctionId(); @@ -467,19 +443,6 @@ pbjsInstance.setTargetingForAst = function (adUnitCodes) { events.emit(SET_TARGETING, targeting.getAllTargeting()); }; -/** - * This function will check for presence of given node in given parent. If not present - will inject it. - * @param {Node} node node, whose existance is in question - * @param {Document} doc document element do look in - * @param {string} tagName tag name to look in - */ -function reinjectNodeIfRemoved(node, doc, tagName) { - const injectionNode = doc.querySelector(tagName); - if (!node.parentNode || node.parentNode !== injectionNode) { - insertElement(node, doc, tagName); - } -} - /** * This function will render the ad (based on params) in the given iframe document passed through. * Note that doc SHOULD NOT be the parent document page as we can't doc.write() asynchronously @@ -490,103 +453,7 @@ function reinjectNodeIfRemoved(node, doc, tagName) { pbjsInstance.renderAd = hook('async', function (doc, id, options) { logInfo('Invoking $$PREBID_GLOBAL$$.renderAd', arguments); logMessage('Calling renderAd with adId :' + id); - - if (!id) { - const message = `Error trying to write ad Id :${id} to the page. Missing adId`; - emitAdRenderFail({ reason: MISSING_DOC_OR_ADID, message, id }); - return; - } - - try { - // lookup ad by ad Id - const bid = auctionManager.findBidByAdId(id); - if (!bid) { - const message = `Error trying to write ad. Cannot find ad by given id : ${id}`; - emitAdRenderFail({ reason: CANNOT_FIND_AD, message, id }); - return; - } - - if (bid.status === CONSTANTS.BID_STATUS.RENDERED) { - logWarn(`Ad id ${bid.adId} has been rendered before`); - events.emit(STALE_RENDER, bid); - if (deepAccess(config.getConfig('auctionOptions'), 'suppressStaleRender')) { - return; - } - } - - // replace macros according to openRTB with price paid = bid.cpm - bid.ad = replaceAuctionPrice(bid.ad, bid.originalCpm || bid.cpm); - bid.adUrl = replaceAuctionPrice(bid.adUrl, bid.originalCpm || bid.cpm); - // replacing clickthrough if submitted - if (options && options.clickThrough) { - const {clickThrough} = options; - bid.ad = replaceClickThrough(bid.ad, clickThrough); - bid.adUrl = replaceClickThrough(bid.adUrl, clickThrough); - } - - // save winning bids - auctionManager.addWinningBid(bid); - - // emit 'bid won' event here - events.emit(BID_WON, bid); - - const {height, width, ad, mediaType, adUrl, renderer} = bid; - - // video module - 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) { - const message = `Error trying to write ad Id :${id} to the page. Missing document`; - emitAdRenderFail({ reason: MISSING_DOC_OR_ADID, message, id }); - return; - } - - const creativeComment = document.createComment(`Creative ${bid.creativeId} served by ${bid.bidder} Prebid.js Header Bidding`); - insertElement(creativeComment, doc, 'html'); - - if (isRendererRequired(renderer)) { - executeRenderer(renderer, bid, doc); - reinjectNodeIfRemoved(creativeComment, doc, 'html'); - emitAdRenderSucceeded({ doc, bid, id }); - } else if ((doc === document && !inIframe()) || mediaType === 'video') { - const message = `Error trying to write ad. Ad render call ad id ${id} was prevented from writing to the main document.`; - emitAdRenderFail({reason: PREVENT_WRITING_ON_MAIN_DOCUMENT, message, bid, id}); - } else if (ad) { - doc.write(ad); - doc.close(); - setRenderSize(doc, width, height); - reinjectNodeIfRemoved(creativeComment, doc, 'html'); - callBurl(bid); - emitAdRenderSucceeded({ doc, bid, id }); - } else if (adUrl) { - const iframe = createInvisibleIframe(); - iframe.height = height; - iframe.width = width; - iframe.style.display = 'inline'; - iframe.style.overflow = 'hidden'; - iframe.src = adUrl; - - insertElement(iframe, doc, 'body'); - setRenderSize(doc, width, height); - reinjectNodeIfRemoved(creativeComment, doc, 'html'); - callBurl(bid); - emitAdRenderSucceeded({ doc, bid, id }); - } else { - const message = `Error trying to write ad. No ad for bid response id: ${id}`; - emitAdRenderFail({reason: NO_AD, message, bid, id}); - } - } catch (e) { - const message = `Error trying to write ad Id :${id} to the page:${e.message}`; - emitAdRenderFail({ reason: EXCEPTION, message, id }); - } + renderAdDirect(doc, id, options); }); /** @@ -671,6 +538,7 @@ pbjsInstance.requestBids = (function() { export const startAuction = hook('async', function ({ bidsBackHandler, timeout: cbTimeout, adUnits, ttlBuffer, adUnitCodes, labels, auctionId, ortb2Fragments, metrics, defer } = {}) { const s2sBidders = getS2SBidderSet(config.getConfig('s2sConfig') || []); + fillAdUnitDefaults(adUnits); adUnits = useMetrics(metrics).measureTime('requestBids.validate', () => checkAdUnitSetup(adUnits)); function auctionDone(bids, timedOut, auctionId) { diff --git a/src/secureCreatives.js b/src/secureCreatives.js index c719bc191f2..0ea93e7e4fb 100644 --- a/src/secureCreatives.js +++ b/src/secureCreatives.js @@ -6,26 +6,24 @@ import * as events from './events.js'; import {fireNativeTrackers, getAllAssetsMessage, getAssetMessage} from './native.js'; import constants from './constants.json'; -import {deepAccess, isApnGetTagDefined, isGptPubadsDefined, logError, logWarn, replaceAuctionPrice} from './utils.js'; +import {isApnGetTagDefined, isGptPubadsDefined, logError, logWarn} from './utils.js'; import {auctionManager} from './auctionManager.js'; import {find, includes} from './polyfill.js'; -import {executeRenderer, isRendererRequired} from './Renderer.js'; -import {config} from './config.js'; -import {emitAdRenderFail, emitAdRenderSucceeded} from './adRendering.js'; +import {emitAdRenderFail, emitAdRenderSucceeded, handleRender} from './adRendering.js'; +import {PREBID_EVENT, PREBID_NATIVE, PREBID_REQUEST, PREBID_RESPONSE} from '../libraries/creativeRender/constants.js'; const BID_WON = constants.EVENTS.BID_WON; -const STALE_RENDER = constants.EVENTS.STALE_RENDER; const WON_AD_IDS = new WeakSet(); const HANDLER_MAP = { - 'Prebid Request': handleRenderRequest, - 'Prebid Event': handleEventRequest, -} + [PREBID_REQUEST]: handleRenderRequest, + [PREBID_EVENT]: handleEventRequest, +}; if (FEATURES.NATIVE) { Object.assign(HANDLER_MAP, { - 'Prebid Native': handleNativeRequest, - }) + [PREBID_NATIVE]: handleNativeRequest, + }); } export function listenMessagesFromCreative() { @@ -35,18 +33,18 @@ export function listenMessagesFromCreative() { export function getReplier(ev) { if (ev.origin == null && ev.ports.length === 0) { return function () { - const msg = 'Cannot post message to a frame with null origin. Please update creatives to use MessageChannel, see https://github.com/prebid/Prebid.js/issues/7870' - logError(msg) + const msg = 'Cannot post message to a frame with null origin. Please update creatives to use MessageChannel, see https://github.com/prebid/Prebid.js/issues/7870'; + logError(msg); throw new Error(msg); - } + }; } else if (ev.ports.length > 0) { return function (message) { ev.ports[0].postMessage(JSON.stringify(message)); - } + }; } else { return function (message) { ev.source.postMessage(JSON.stringify(message), ev.origin); - } + }; } } @@ -69,39 +67,13 @@ export function receiveMessage(ev) { } } -function handleRenderRequest(reply, data, adObject) { - if (adObject == null) { - emitAdRenderFail({ - reason: constants.AD_RENDER_FAILED_REASON.CANNOT_FIND_AD, - message: `Cannot find ad for cross-origin render request: '${data.adId}'`, - id: data.adId - }); - return; - } - if (adObject.status === constants.BID_STATUS.RENDERED) { - logWarn(`Ad id ${adObject.adId} has been rendered before`); - events.emit(STALE_RENDER, adObject); - if (deepAccess(config.getConfig('auctionOptions'), 'suppressStaleRender')) { - return; - } - } - - try { - _sendAdToCreative(adObject, reply); - } catch (e) { - emitAdRenderFail({ - reason: constants.AD_RENDER_FAILED_REASON.EXCEPTION, - message: e.message, - id: data.adId, - bid: adObject - }); - return; - } - - // save winning bids - auctionManager.addWinningBid(adObject); - - events.emit(BID_WON, adObject); +function handleRenderRequest(reply, message, bidResponse) { + handleRender(function (adData) { + resizeRemoteCreative(bidResponse); + reply(Object.assign({ + message: PREBID_RESPONSE, + }, adData)); + }, {options: message.options, adId: message.adId, bidResponse}); } function handleNativeRequest(reply, data, adObject) { @@ -164,29 +136,11 @@ function handleEventRequest(reply, data, adObject) { }); break; default: - logError(`Received x-origin event request for unsupported event: '${data.event}' (adId: '${data.adId}')`) - } -} - -export function _sendAdToCreative(adObject, reply) { - const { adId, ad, adUrl, width, height, renderer, cpm, originalCpm } = adObject; - // rendering for outstream safeframe - if (isRendererRequired(renderer)) { - executeRenderer(renderer, adObject); - } else if (adId) { - resizeRemoteCreative(adObject); - reply({ - message: 'Prebid Response', - ad: replaceAuctionPrice(ad, originalCpm || cpm), - adUrl: replaceAuctionPrice(adUrl, originalCpm || cpm), - adId, - width, - height - }); + logError(`Received x-origin event request for unsupported event: '${data.event}' (adId: '${data.adId}')`); } } -function resizeRemoteCreative({ adId, adUnitCode, width, height }) { +export function resizeRemoteCreative({adId, adUnitCode, width, height}) { // resize both container div + iframe ['div', 'iframe'].forEach(elmType => { // not select element that gets removed after dfp render @@ -208,9 +162,9 @@ function resizeRemoteCreative({ adId, adUnitCode, width, height }) { function getElementIdBasedOnAdServer(adId, adUnitCode) { if (isGptPubadsDefined()) { - return getDfpElementId(adId) + return getDfpElementId(adId); } else if (isApnGetTagDefined()) { - return getAstElementId(adUnitCode) + return getAstElementId(adUnitCode); } else { return adUnitCode; } diff --git a/src/targeting.js b/src/targeting.js index a75c9a2b52f..ddbc3cebaf3 100644 --- a/src/targeting.js +++ b/src/targeting.js @@ -1,8 +1,6 @@ import { deepAccess, deepClone, - getHighestCpm, - getOldestHighestCpmBid, groupBy, isAdUnitCodeMatchingSlot, isArray, @@ -24,19 +22,12 @@ import {hook} from './hook.js'; import {bidderSettings} from './bidderSettings.js'; import {find, includes} from './polyfill.js'; import CONSTANTS from './constants.json'; +import {getHighestCpm, getOldestHighestCpmBid} from './utils/reducers.js'; +import {getTTL} from './bidTTL.js'; var pbTargetingKeys = []; const MAX_DFP_KEYLENGTH = 20; -let DEFAULT_TTL_BUFFER = 1; - -config.getConfig('ttlBuffer', (cfg) => { - if (typeof cfg.ttlBuffer === 'number') { - DEFAULT_TTL_BUFFER = cfg.ttlBuffer; - } else { - logError('Invalid value for ttlBuffer', cfg.ttlBuffer); - } -}) const CFG_ALLOW_TARGETING_KEYS = `targetingControls.allowTargetingKeys`; const CFG_ADD_TARGETING_KEYS = `targetingControls.addTargetingKeys`; @@ -47,7 +38,7 @@ export const TARGETING_KEYS = Object.keys(CONSTANTS.TARGETING_KEYS).map( ); // return unexpired bids -const isBidNotExpired = (bid) => (bid.responseTimestamp + (bid.ttl - (bid.hasOwnProperty('ttlBuffer') ? bid.ttlBuffer : DEFAULT_TTL_BUFFER)) * 1000) > timestamp(); +const isBidNotExpired = (bid) => (bid.responseTimestamp + getTTL(bid) * 1000) > timestamp(); // return bids whose status is not set. Winning bids can only have a status of `rendered`. const isUnusedBid = (bid) => bid && ((bid.status && !includes([CONSTANTS.BID_STATUS.RENDERED], bid.status)) || !bid.status); @@ -94,26 +85,26 @@ export const getHighestCpmBidsFromBidPool = hook('sync', function(bidsReceived, }); /** -* A descending sort function that will sort the list of objects based on the following two dimensions: -* - bids with a deal are sorted before bids w/o a deal -* - then sort bids in each grouping based on the hb_pb value -* eg: the following list of bids would be sorted like: -* [{ -* "hb_adid": "vwx", -* "hb_pb": "28", -* "hb_deal": "7747" -* }, { -* "hb_adid": "jkl", -* "hb_pb": "10", -* "hb_deal": "9234" -* }, { -* "hb_adid": "stu", -* "hb_pb": "50" -* }, { -* "hb_adid": "def", -* "hb_pb": "2" -* }] -*/ + * A descending sort function that will sort the list of objects based on the following two dimensions: + * - bids with a deal are sorted before bids w/o a deal + * - then sort bids in each grouping based on the hb_pb value + * eg: the following list of bids would be sorted like: + * [{ + * "hb_adid": "vwx", + * "hb_pb": "28", + * "hb_deal": "7747" + * }, { + * "hb_adid": "jkl", + * "hb_pb": "10", + * "hb_deal": "9234" + * }, { + * "hb_adid": "stu", + * "hb_pb": "50" + * }, { + * "hb_adid": "def", + * "hb_pb": "2" + * }] + */ export function sortByDealAndPriceBucketOrCpm(useCpm = false) { return function(a, b) { if (a.adserverTargeting.hb_deal !== undefined && b.adserverTargeting.hb_deal === undefined) { @@ -479,6 +470,12 @@ export function newTargeting(auctionManager) { .filter(bid => deepAccess(bid, 'video.context') !== ADPOD) .filter(isBidUsable); + bidsReceived + .forEach(bid => { + bid.latestTargetedAuctionId = latestAuctionForAdUnit[bid.adUnitCode]; + return bid; + }); + return getHighestCpmBidsFromBidPool(bidsReceived, getOldestHighestCpmBid); } @@ -500,7 +497,7 @@ export function newTargeting(auctionManager) { }; /** - * @param {(string|string[])} adUnitCode adUnitCode or array of adUnitCodes + * @param {(string|string[])} adUnitCodes adUnitCode or array of adUnitCodes * Sets targeting for AST */ targeting.setTargetingForAst = function(adUnitCodes) { @@ -533,7 +530,7 @@ export function newTargeting(auctionManager) { /** * Get targeting key value pairs for winning bid. - * @param {string[]} AdUnit code array + * @param {string[]} adUnitCodes code array * @return {targetingArray} winning bids targeting */ function getWinningBidTargeting(adUnitCodes, bidsReceived) { @@ -633,7 +630,7 @@ export function newTargeting(auctionManager) { /** * Get custom targeting key value pairs for bids. - * @param {string[]} AdUnit code array + * @param {string[]} adUnitCodes code array * @return {targetingArray} bids with custom targeting defined in bidderSettings */ function getCustomBidTargeting(adUnitCodes, bidsReceived) { @@ -647,7 +644,7 @@ export function newTargeting(auctionManager) { /** * Get targeting key value pairs for non-winning bids. - * @param {string[]} AdUnit code array + * @param {string[]} adUnitCodes code array * @return {targetingArray} all non-winning bids targeting */ function getBidLandscapeTargeting(adUnitCodes, bidsReceived) { diff --git a/src/userSync.js b/src/userSync.js index 936836eb12e..1b684de6de0 100644 --- a/src/userSync.js +++ b/src/userSync.js @@ -182,8 +182,8 @@ export function newUserSync(deps) { * @function incrementAdapterBids * @summary Increment the count of user syncs queue for the adapter * @private - * @params {object} numAdapterBids The object contain counts for all adapters - * @params {string} bidder The name of the bidder adding a sync + * @param {object} numAdapterBids The object contain counts for all adapters + * @param {string} bidder The name of the bidder adding a sync * @returns {object} The updated version of numAdapterBids */ function incrementAdapterBids(numAdapterBids, bidder) { @@ -199,10 +199,9 @@ export function newUserSync(deps) { * @function registerSync * @summary Add sync for this bidder to a queue to be fired later * @public - * @params {string} type The type of the sync including image, iframe - * @params {string} bidder The name of the adapter. e.g. "rubicon" - * @params {string} url Either the pixel url or iframe url depending on the type - + * @param {string} type The type of the sync including image, iframe + * @param {string} bidder The name of the adapter. e.g. "rubicon" + * @param {string} url Either the pixel url or iframe url depending on the type * @example Using Image Sync * // registerSync(type, adapter, pixelUrl) * userSync.registerSync('image', 'rubicon', 'http://example.com/pixel') @@ -244,7 +243,7 @@ export function newUserSync(deps) { * @param {string} type The type of the sync; either image or iframe * @param {string} bidder The name of the adapter. e.g. "rubicon" * @returns {boolean} true => bidder is not allowed to register; false => bidder can register - */ + */ function shouldBidderBeBlocked(type, bidder) { let filterConfig = usConfig.filterSettings; @@ -309,7 +308,7 @@ export function newUserSync(deps) { * @function syncUsers * @summary Trigger all the user syncs based on publisher-defined timeout * @public - * @params {int} timeout The delay in ms before syncing data - default 0 + * @param {number} timeout The delay in ms before syncing data - default 0 */ publicApi.syncUsers = (timeout = 0) => { if (timeout) { @@ -358,7 +357,7 @@ export const userSync = newUserSync(Object.defineProperties({ * * @property {boolean} enableOverride * @property {boolean} syncEnabled - * @property {int} syncsPerBidder + * @property {number} syncsPerBidder * @property {string[]} enabledBidders * @property {Object} filterSettings */ diff --git a/src/utils.js b/src/utils.js index ece29732723..67ed7339675 100644 --- a/src/utils.js +++ b/src/utils.js @@ -1,6 +1,6 @@ import {config} from './config.js'; import clone from 'just-clone'; -import {find, includes} from './polyfill.js'; +import {includes} from './polyfill.js'; import CONSTANTS from './constants.json'; import {GreedyPromise} from './utils/promise.js'; import {getGlobal} from './prebidGlobal.js'; @@ -8,7 +8,6 @@ import {getGlobal} from './prebidGlobal.js'; export { default as deepAccess } from 'dlv/index.js'; export { dset as deepSetValue } from 'dset'; -var tArr = 'Array'; var tStr = 'String'; var tFn = 'Function'; var tNumb = 'Number'; @@ -64,17 +63,6 @@ export function getPrebidInternal() { return prebidInternal; } -var uniqueRef = {}; -export let bind = function(a, b) { return b; }.bind(null, 1, uniqueRef)() === uniqueRef - ? Function.prototype.bind - : function(bind) { - var self = this; - var args = Array.prototype.slice.call(arguments, 1); - return function() { - return self.apply(bind, args.concat(Array.prototype.slice.call(arguments))); - }; - }; - /* utility method to get incremental integer starting from 1 */ var getIncrementalInteger = (function () { var count = 0; @@ -114,19 +102,7 @@ function _getRandomData() { } export function getBidIdParameter(key, paramsObj) { - if (paramsObj && paramsObj[key]) { - return paramsObj[key]; - } - - return ''; -} - -export function tryAppendQueryString(existingUrl, key, value) { - if (value) { - return existingUrl + key + '=' + encodeURIComponent(value) + '&'; - } - - return existingUrl; + return paramsObj?.[key] || ''; } // parse a query string object passed in bid params @@ -145,84 +121,30 @@ export function parseQueryStringParameters(queryObj) { export function transformAdServerTargetingObj(targeting) { // we expect to receive targeting for a single slot at a time if (targeting && Object.getOwnPropertyNames(targeting).length > 0) { - return getKeys(targeting) - .map(key => `${key}=${encodeURIComponent(getValue(targeting, key))}`).join('&'); + return Object.keys(targeting) + .map(key => `${key}=${encodeURIComponent(targeting[key])}`).join('&'); } else { return ''; } } -/** - * Read an adUnit object and return the sizes used in an [[728, 90]] format (even if they had [728, 90] defined) - * Preference is given to the `adUnit.mediaTypes.banner.sizes` object over the `adUnit.sizes` - * @param {object} adUnit one adUnit object from the normal list of adUnits - * @returns {Array.} array of arrays containing numeric sizes - */ -export function getAdUnitSizes(adUnit) { - if (!adUnit) { - return; - } - - let sizes = []; - if (adUnit.mediaTypes && adUnit.mediaTypes.banner && Array.isArray(adUnit.mediaTypes.banner.sizes)) { - let bannerSizes = adUnit.mediaTypes.banner.sizes; - if (Array.isArray(bannerSizes[0])) { - sizes = bannerSizes; - } else { - sizes.push(bannerSizes); - } - // TODO - remove this else block when we're ready to deprecate adUnit.sizes for bidders - } else if (Array.isArray(adUnit.sizes)) { - if (Array.isArray(adUnit.sizes[0])) { - sizes = adUnit.sizes; - } else { - sizes.push(adUnit.sizes); - } - } - return sizes; -} - /** * Parse a GPT-Style general size Array like `[[300, 250]]` or `"300x250,970x90"` into an array of sizes `["300x250"]` or '['300x250', '970x90']' * @param {(Array.|Array.)} sizeObj Input array or double array [300,250] or [[300,250], [728,90]] * @return {Array.} Array of strings like `["300x250"]` or `["300x250", "728x90"]` */ export function parseSizesInput(sizeObj) { - var parsedSizes = []; - - // if a string for now we can assume it is a single size, like "300x250" if (typeof sizeObj === 'string') { // multiple sizes will be comma-separated - var sizes = sizeObj.split(','); - - // regular expression to match strigns like 300x250 - // start of line, at least 1 number, an "x" , then at least 1 number, and the then end of the line - var sizeRegex = /^(\d)+x(\d)+$/i; - if (sizes) { - for (var curSizePos in sizes) { - if (hasOwn(sizes, curSizePos) && sizes[curSizePos].match(sizeRegex)) { - parsedSizes.push(sizes[curSizePos]); - } - } - } + return sizeObj.split(',').filter(sz => sz.match(/^(\d)+x(\d)+$/i)) } else if (typeof sizeObj === 'object') { - var sizeArrayLength = sizeObj.length; - - // don't process empty array - if (sizeArrayLength > 0) { - // if we are a 2 item array of 2 numbers, we must be a SingleSize array - if (sizeArrayLength === 2 && typeof sizeObj[0] === 'number' && typeof sizeObj[1] === 'number') { - parsedSizes.push(parseGPTSingleSizeArray(sizeObj)); - } else { - // otherwise, we must be a MultiSize array - for (var i = 0; i < sizeArrayLength; i++) { - parsedSizes.push(parseGPTSingleSizeArray(sizeObj[i])); - } - } + if (sizeObj.length === 2 && typeof sizeObj[0] === 'number' && typeof sizeObj[1] === 'number') { + return [parseGPTSingleSizeArray(sizeObj)]; + } else { + return sizeObj.map(parseGPTSingleSizeArray) } } - - return parsedSizes; + return []; } // Parse a GPT style single size array, (i.e [300, 250]) @@ -345,6 +267,9 @@ export function createInvisibleIframe() { f.frameBorder = '0'; f.src = 'about:blank'; f.style.display = 'none'; + f.style.height = '0px'; + f.style.width = '0px'; + f.allowtransparency = 'true'; return f; } @@ -375,9 +300,7 @@ export function isStr(object) { return isA(object, tStr); } -export function isArray(object) { - return isA(object, tArr); -} +export const isArray = Array.isArray.bind(Array); export function isNumber(object) { return isA(object, tNumb); @@ -402,12 +325,7 @@ export function isEmpty(object) { if (isArray(object) || isStr(object)) { return !(object.length > 0); } - - for (var k in object) { - if (hasOwnProperty.call(object, k)) return false; - } - - return true; + return Object.keys(object).length <= 0; } /** @@ -426,38 +344,12 @@ export function isEmptyStr(str) { * @param {Function(value, key, object)} fn */ export function _each(object, fn) { - if (isEmpty(object)) return; - if (isFn(object.forEach)) return object.forEach(fn, this); - - var k = 0; - var l = object.length; - - if (l > 0) { - for (; k < l; k++) fn(object[k], k, object); - } else { - for (k in object) { - if (hasOwnProperty.call(object, k)) fn.call(this, object[k], k); - } - } + if (isFn(object?.forEach)) return object.forEach(fn, this); + Object.entries(object || {}).forEach(([k, v]) => fn.call(this, v, k)); } export function contains(a, obj) { - if (isEmpty(a)) { - return false; - } - - if (isFn(a.indexOf)) { - return a.indexOf(obj) !== -1; - } - - var i = a.length; - while (i--) { - if (a[i] === obj) { - return true; - } - } - - return false; + return isFn(a?.includes) && a.includes(obj); } /** @@ -468,24 +360,10 @@ export function contains(a, obj) { * @return {Array} */ export function _map(object, callback) { - if (isEmpty(object)) return []; - if (isFn(object.map)) return object.map(callback); - var output = []; - _each(object, function (value, key) { - output.push(callback(value, key, object)); - }); - - return output; + if (isFn(object?.map)) return object.map(callback); + return Object.entries(object || {}).map(([k, v]) => callback(v, k, object)) } -export function hasOwn(objectToCheck, propertyToCheckFor) { - if (objectToCheck.hasOwnProperty) { - return objectToCheck.hasOwnProperty(propertyToCheckFor); - } else { - return (typeof objectToCheck[propertyToCheckFor] !== 'undefined') && (objectToCheck.constructor.prototype[propertyToCheckFor] !== objectToCheck[propertyToCheckFor]); - } -}; - /* * Inserts an element(elm) as targets child, by default as first child * @param {HTMLElement} elm @@ -568,27 +446,14 @@ export function insertHtmlIntoIframe(htmlCode) { if (!htmlCode) { return; } - - let iframe = document.createElement('iframe'); - iframe.id = getUniqueIdentifierStr(); - iframe.width = 0; - iframe.height = 0; - iframe.hspace = '0'; - iframe.vspace = '0'; - iframe.marginWidth = '0'; - iframe.marginHeight = '0'; - iframe.style.display = 'none'; - iframe.style.height = '0px'; - iframe.style.width = '0px'; - iframe.scrolling = 'no'; - iframe.frameBorder = '0'; - iframe.allowtransparency = 'true'; - + const iframe = createInvisibleIframe(); internal.insertElement(iframe, document, 'body'); - iframe.contentWindow.document.open(); - iframe.contentWindow.document.write(htmlCode); - iframe.contentWindow.document.close(); + ((doc) => { + doc.open(); + doc.write(htmlCode); + doc.close(); + })(iframe.contentWindow.document); } /** @@ -654,19 +519,6 @@ export function createTrackPixelIframeHtml(url, encodeUri = true, sandbox = '') `; } -export function getValueString(param, val, defaultValue) { - if (val === undefined || val === null) { - return defaultValue; - } - if (isStr(val)) { - return val; - } - if (isNumber(val)) { - return val.toString(); - } - internal.logWarn('Unsuported type for param: ' + param + ' required type: String'); -} - export function uniques(value, index, arry) { return arry.indexOf(value) === index; } @@ -679,38 +531,14 @@ export function getBidRequest(id, bidderRequests) { if (!id) { return; } - let bidRequest; - bidderRequests.some(bidderRequest => { - let result = find(bidderRequest.bids, bid => ['bidId', 'adId', 'bid_id'].some(type => bid[type] === id)); - if (result) { - bidRequest = result; - } - return result; - }); - return bidRequest; -} - -export function getKeys(obj) { - return Object.keys(obj); + return bidderRequests.flatMap(br => br.bids) + .find(bid => ['bidId', 'adId', 'bid_id'].some(prop => bid[prop] === id)) } export function getValue(obj, key) { return obj[key]; } -/** - * Get the key of an object for a given value - */ -export function getKeyByValue(obj, value) { - for (let prop in obj) { - if (obj.hasOwnProperty(prop)) { - if (obj[prop] === value) { - return prop; - } - } - } -} - export function getBidderCodes(adUnits = pbjsInstance.adUnits) { // this could memoize adUnits return adUnits.map(unit => unit.bids.map(bid => bid.bidder) @@ -729,26 +557,6 @@ export function isApnGetTagDefined() { } } -// This function will get highest cpm value bid, in case of tie it will return the bid with lowest timeToRespond -export const getHighestCpm = getHighestCpmCallback('timeToRespond', (previous, current) => previous > current); - -// This function will get the oldest hightest cpm value bid, in case of tie it will return the bid which came in first -// Use case for tie: https://github.com/prebid/Prebid.js/issues/2448 -export const getOldestHighestCpmBid = getHighestCpmCallback('responseTimestamp', (previous, current) => previous > current); - -// This function will get the latest hightest cpm value bid, in case of tie it will return the bid which came in last -// Use case for tie: https://github.com/prebid/Prebid.js/issues/2539 -export const getLatestHighestCpmBid = getHighestCpmCallback('responseTimestamp', (previous, current) => previous < current); - -function getHighestCpmCallback(useTieBreakerProperty, tieBreakerCallback) { - return (previous, current) => { - if (previous.cpm === current.cpm) { - return tieBreakerCallback(previous[useTieBreakerProperty], current[useTieBreakerProperty]) ? current : previous; - } - return previous.cpm < current.cpm ? current : previous; - } -} - /** * Fisher–Yates shuffle * http://stackoverflow.com/a/6274398 @@ -775,10 +583,6 @@ export function shuffle(array) { return array; } -export function adUnitsFilter(filter, bid) { - return includes(filter, bid && bid.adUnitCode); -} - export function deepClone(obj) { return clone(obj); } @@ -795,9 +599,15 @@ export function isSafariBrowser() { return /^((?!chrome|android|crios|fxios).)*safari/i.test(navigator.userAgent); } -export function replaceAuctionPrice(str, cpm) { +export function replaceMacros(str, subs) { if (!str) return; - return str.replace(/\$\{AUCTION_PRICE\}/g, cpm); + return Object.entries(subs).reduce((str, [key, val]) => { + return str.replace(new RegExp('\\$\\{' + key + '\\}', 'g'), val || ''); + }, str); +} + +export function replaceAuctionPrice(str, cpm) { + return replaceMacros(str, {AUCTION_PRICE: cpm}) } export function replaceClickThrough(str, clicktag) { @@ -843,7 +653,7 @@ export function checkCookieSupport() { * * @param {function} func The function which should be executed, once the returned function has been executed * numRequiredCalls times. - * @param {int} numRequiredCalls The number of times which the returned function needs to be called before + * @param {number} numRequiredCalls The number of times which the returned function needs to be called before * func is. */ export function delayExecution(func, numRequiredCalls) { @@ -862,7 +672,7 @@ export function delayExecution(func, numRequiredCalls) { /** * https://stackoverflow.com/a/34890276/428704 * @export - * @param {array} xs + * @param {Array} xs * @param {string} key * @returns {Object} {${key_value}: ${groupByArray}, key_value: {groupByArray}} */ @@ -925,8 +735,7 @@ export function isValidMediaTypes(mediaTypes) { export function getUserConfiguredParams(adUnits, adUnitCode, bidder) { return adUnits .filter(adUnit => adUnit.code === adUnitCode) - .map((adUnit) => adUnit.bids) - .reduce(flatten, []) + .flatMap((adUnit) => adUnit.bids) .filter((bidderData) => bidderData.bidder === bidder) .map((bidderData) => bidderData.params || {}); } @@ -938,7 +747,7 @@ export function getDNT() { return navigator.doNotTrack === '1' || window.doNotTrack === '1' || navigator.msDoNotTrack === '1' || navigator.doNotTrack === 'yes'; } -const compareCodeAndSlot = (slot, adUnitCode) => slot.getAdUnitPath() === adUnitCode || slot.getSlotElementId() === adUnitCode; +export const compareCodeAndSlot = (slot, adUnitCode) => slot.getAdUnitPath() === adUnitCode || slot.getSlotElementId() === adUnitCode; /** * Returns filter function to match adUnitCode in slot @@ -949,41 +758,6 @@ export function isAdUnitCodeMatchingSlot(slot) { return (adUnitCode) => compareCodeAndSlot(slot, adUnitCode); } -/** - * Returns filter function to match adUnitCode in slot - * @param {string} adUnitCode AdUnit code - * @return {function} filter function - */ -export function isSlotMatchingAdUnitCode(adUnitCode) { - return (slot) => compareCodeAndSlot(slot, adUnitCode); -} - -/** - * @summary Uses the adUnit's code in order to find a matching gpt slot object on the page - */ -export function getGptSlotForAdUnitCode(adUnitCode) { - let matchingSlot; - if (isGptPubadsDefined()) { - // find the first matching gpt slot on the page - matchingSlot = find(window.googletag.pubads().getSlots(), isSlotMatchingAdUnitCode(adUnitCode)); - } - return matchingSlot; -}; - -/** - * @summary Uses the adUnit's code in order to find a matching gptSlot on the page - */ -export function getGptSlotInfoForAdUnitCode(adUnitCode) { - const matchingSlot = getGptSlotForAdUnitCode(adUnitCode); - if (matchingSlot) { - return { - gptSlot: matchingSlot.getAdUnitPath(), - divId: matchingSlot.getSlotElementId() - } - } - return {}; -}; - /** * Constructs warning message for when unsupported bidders are dropped from an adunit * @param {Object} adUnit ad unit from which the bidder is being dropped @@ -1005,33 +779,14 @@ export function unsupportedBidderMessage(adUnit, bidder) { * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/isInteger * @param {*} value */ -export function isInteger(value) { - if (Number.isInteger) { - return Number.isInteger(value); - } else { - return typeof value === 'number' && isFinite(value) && Math.floor(value) === value; - } -} - -/** - * Converts a string value in camel-case to underscore eg 'placementId' becomes 'placement_id' - * @param {string} value string value to convert - */ -export function convertCamelToUnderscore(value) { - return value.replace(/(?:^|\.?)([A-Z])/g, function (x, y) { return '_' + y.toLowerCase() }).replace(/^_/, ''); -} +export const isInteger = Number.isInteger.bind(Number); /** * Returns a new object with undefined properties removed from given object * @param obj the object to clean */ export function cleanObj(obj) { - return Object.keys(obj).reduce((newObj, key) => { - if (typeof obj[key] !== 'undefined') { - newObj[key] = obj[key]; - } - return newObj; - }, {}) + return Object.fromEntries(Object.entries(obj).filter(([_, v]) => typeof v !== 'undefined')) } /** @@ -1068,104 +823,10 @@ export function pick(obj, properties) { }, {}); } -/** - * Try to convert a value to a type. - * If it can't be done, the value will be returned. - * - * @param {string} typeToConvert The target type. e.g. "string", "number", etc. - * @param {*} value The value to be converted into typeToConvert. - */ -function tryConvertType(typeToConvert, value) { - if (typeToConvert === 'string') { - return value && value.toString(); - } else if (typeToConvert === 'number') { - return Number(value); - } else { - return value; - } -} - -export function convertTypes(types, params) { - Object.keys(types).forEach(key => { - if (params[key]) { - if (isFn(types[key])) { - params[key] = types[key](params[key]); - } else { - params[key] = tryConvertType(types[key], params[key]); - } - - // don't send invalid values - if (isNaN(params[key])) { - delete params.key; - } - } - }); - return params; -} - export function isArrayOfNums(val, size) { return (isArray(val)) && ((size) ? val.length === size : true) && (val.every(v => isInteger(v))); } -/** - * Creates an array of n length and fills each item with the given value - */ -export function fill(value, length) { - let newArray = []; - - for (let i = 0; i < length; i++) { - let valueToPush = isPlainObject(value) ? deepClone(value) : value; - newArray.push(valueToPush); - } - - return newArray; -} - -/** - * http://npm.im/chunk - * Returns an array with *size* chunks from given array - * - * Example: - * ['a', 'b', 'c', 'd', 'e'] chunked by 2 => - * [['a', 'b'], ['c', 'd'], ['e']] - */ -export function chunk(array, size) { - let newArray = []; - - for (let i = 0; i < Math.ceil(array.length / size); i++) { - let start = i * size; - let end = start + size; - newArray.push(array.slice(start, end)); - } - - return newArray; -} - -export function getMinValueFromArray(array) { - return Math.min(...array); -} - -export function getMaxValueFromArray(array) { - return Math.max(...array); -} - -/** - * This function will create compare function to sort on object property - * @param {string} property - * @returns {function} compare function to be used in sorting - */ -export function compareOn(property) { - return function compare(a, b) { - if (a[property] < b[property]) { - return 1; - } - if (a[property] > b[property]) { - return -1; - } - return 0; - } -} - export function parseQS(query) { return !query ? {} : query .replace(/^\?/, '') @@ -1328,15 +989,6 @@ export function cyrb53Hash(str, seed = 0) { return (4294967296 * (2097151 & h2) + (h1 >>> 0)).toString(); } -/** - * returns a window object, which holds the provided document or null - * @param {Document} doc - * @returns {Window} - */ -export function getWindowFromDocument(doc) { - return (doc) ? doc.defaultView : null; -} - /** * returns the result of `JSON.parse(data)`, or undefined if that throws an error. * @param data @@ -1375,36 +1027,33 @@ export function memoize(fn, key = function (arg) { return arg; }) { * @param {object} attributes */ export function setScriptAttributes(script, attributes) { - for (let key in attributes) { - if (attributes.hasOwnProperty(key)) { - script.setAttribute(key, attributes[key]); - } - } + Object.entries(attributes).forEach(([k, v]) => script.setAttribute(k, v)) } /** - * Encode a string for inclusion in HTML. - * See https://pragmaticwebsecurity.com/articles/spasecurity/json-stringify-xss.html and - * https://codeql.github.com/codeql-query-help/javascript/js-bad-code-sanitization/ - * @return {string} + * Perform a binary search for `el` on an ordered array `arr`. + * + * @returns the lowest nonnegative integer I that satisfies: + * key(arr[i]) >= key(el) for each i between I and arr.length + * + * (if one or more matches are found for `el`, returns the index of the first; + * if the element is not found, return the index of the first element that's greater; + * if no greater element exists, return `arr.length`) */ -export const escapeUnsafeChars = (() => { - const escapes = { - '<': '\\u003C', - '>': '\\u003E', - '/': '\\u002F', - '\\': '\\\\', - '\b': '\\b', - '\f': '\\f', - '\n': '\\n', - '\r': '\\r', - '\t': '\\t', - '\0': '\\0', - '\u2028': '\\u2028', - '\u2029': '\\u2029' - }; - - return function(str) { - return str.replace(/[<>\b\f\n\r\t\0\u2028\u2029\\]/g, x => escapes[x]) +export function binarySearch(arr, el, key = (el) => el) { + let left = 0; + let right = arr.length && arr.length - 1; + const target = key(el); + while (right - left > 1) { + const middle = left + Math.round((right - left) / 2); + if (target > key(arr[middle])) { + left = middle; + } else { + right = middle; + } } -})(); + while (arr.length > left && target > key(arr[left])) { + left++; + } + return left; +} diff --git a/src/utils/currency.js b/src/utils/currency.js deleted file mode 100644 index ab7e5faa1ea..00000000000 --- a/src/utils/currency.js +++ /dev/null @@ -1,16 +0,0 @@ -import {getGlobal} from '../prebidGlobal.js'; - -/** - * "best effort" wrapper around currency conversion; always returns an amount that may or may not be correct. - */ -export function beConvertCurrency(amount, from, to) { - if (from === to) return amount; - let result = amount; - if (typeof getGlobal().convertCurrency === 'function') { - try { - result = getGlobal().convertCurrency(amount, from, to); - } catch (e) { - } - } - return result; -} diff --git a/src/utils/reducers.js b/src/utils/reducers.js new file mode 100644 index 00000000000..28851be8aaa --- /dev/null +++ b/src/utils/reducers.js @@ -0,0 +1,44 @@ +export function simpleCompare(a, b) { + if (a === b) return 0; + return a < b ? -1 : 1; +} + +export function keyCompare(key = (item) => item) { + return (a, b) => simpleCompare(key(a), key(b)) +} + +export function reverseCompare(compare = simpleCompare) { + return (a, b) => -compare(a, b) || 0; +} + +export function tiebreakCompare(...compares) { + return function (a, b) { + for (const cmp of compares) { + const val = cmp(a, b); + if (val !== 0) return val; + } + return 0; + } +} + +export function minimum(compare = simpleCompare) { + return (min, item) => compare(item, min) < 0 ? item : min; +} + +export function maximum(compare = simpleCompare) { + return minimum(reverseCompare(compare)); +} + +const cpmCompare = keyCompare((bid) => bid.cpm); +const timestampCompare = keyCompare((bid) => bid.responseTimestamp); + +// This function will get highest cpm value bid, in case of tie it will return the bid with lowest timeToRespond +export const getHighestCpm = maximum(tiebreakCompare(cpmCompare, reverseCompare(keyCompare((bid) => bid.timeToRespond)))) + +// This function will get the oldest hightest cpm value bid, in case of tie it will return the bid which came in first +// Use case for tie: https://github.com/prebid/Prebid.js/issues/2448 +export const getOldestHighestCpmBid = maximum(tiebreakCompare(cpmCompare, reverseCompare(timestampCompare))) + +// This function will get the latest hightest cpm value bid, in case of tie it will return the bid which came in last +// Use case for tie: https://github.com/prebid/Prebid.js/issues/2539 +export const getLatestHighestCpmBid = maximum(tiebreakCompare(cpmCompare, timestampCompare)) diff --git a/src/utils/ttlCollection.js b/src/utils/ttlCollection.js new file mode 100644 index 00000000000..392ed1c9ad7 --- /dev/null +++ b/src/utils/ttlCollection.js @@ -0,0 +1,139 @@ +import {GreedyPromise} from './promise.js'; +import {binarySearch, timestamp} from '../utils.js'; + +/** + * Create a set-like collection that automatically forgets items after a certain time. + * + * @param {({}) => Number|Promise} startTime? a function taking an item added to this collection, + * and returning (a promise to) a timestamp to be used as the starting time for the item + * (the item will be dropped after `ttl(item)` milliseconds have elapsed since this timestamp). + * Defaults to the time the item was added to the collection. + * @param {({}) => Number|void|Promise} ttl a function taking an item added to this collection, + * and returning (a promise to) the duration (in milliseconds) the item should be kept in it. + * May return null to indicate that the item should be persisted indefinitely. + * @param {boolean} monotonic? set to true for better performance, but only if, given any two items A and B in this collection: + * if A was added before B, then: + * - startTime(A) + ttl(A) <= startTime(B) + ttl(B) + * - Promise.all([startTime(A), ttl(A)]) never resolves later than Promise.all([startTime(B), ttl(B)]) + * @param {number} slack? maximum duration (in milliseconds) that an item is allowed to persist + * once past its TTL. This is also roughly the interval between "garbage collection" sweeps. + */ +export function ttlCollection( + { + startTime = timestamp, + ttl = () => null, + monotonic = false, + slack = 5000 + } = {} +) { + const items = new Map(); + const pendingPurge = []; + const markForPurge = monotonic + ? (entry) => pendingPurge.push(entry) + : (entry) => pendingPurge.splice(binarySearch(pendingPurge, entry, (el) => el.expiry), 0, entry) + let nextPurge, task; + + function reschedulePurge() { + task && clearTimeout(task); + if (pendingPurge.length > 0) { + const now = timestamp(); + nextPurge = Math.max(now, pendingPurge[0].expiry + slack); + task = setTimeout(() => { + const now = timestamp(); + let cnt = 0; + for (const entry of pendingPurge) { + if (entry.expiry > now) break; + items.delete(entry.item) + cnt++; + } + pendingPurge.splice(0, cnt); + task = null; + reschedulePurge(); + }, nextPurge - now); + } else { + task = null; + } + } + + function mkEntry(item) { + const values = {}; + const thisCohort = currentCohort; + let expiry; + + function update() { + if (thisCohort === currentCohort && values.start != null && values.delta != null) { + expiry = values.start + values.delta; + markForPurge(entry); + if (task == null || nextPurge > expiry + slack) { + reschedulePurge(); + } + } + } + + const [init, refresh] = Object.entries({ + start: startTime, + delta: ttl + }).map(([field, getter]) => { + let currentCall; + return function() { + const thisCall = currentCall = {}; + GreedyPromise.resolve(getter(item)).then((val) => { + if (thisCall === currentCall) { + values[field] = val; + update(); + } + }); + } + }) + + const entry = { + item, + refresh, + get expiry() { + return expiry; + }, + }; + + init(); + refresh(); + return entry; + } + + let currentCohort = {}; + + return { + [Symbol.iterator]: () => items.keys(), + /** + * Add an item to this collection. + * @param item + */ + add(item) { + !items.has(item) && items.set(item, mkEntry(item)); + }, + /** + * Clear this collection. + */ + clear() { + pendingPurge.length = 0; + reschedulePurge(); + items.clear(); + currentCohort = {}; + }, + /** + * @returns {[]} all the items in this collection, in insertion order. + */ + toArray() { + return Array.from(items.keys()); + }, + /** + * Refresh the TTL for each item in this collection. + */ + refresh() { + pendingPurge.length = 0; + reschedulePurge(); + for (const entry of items.values()) { + entry.refresh(); + } + }, + }; +} diff --git a/src/video.js b/src/video.js index 7930e318874..ff137892a2b 100644 --- a/src/video.js +++ b/src/video.js @@ -1,25 +1,21 @@ -import adapterManager from './adapterManager.js'; -import { deepAccess, logError } from './utils.js'; -import { config } from '../src/config.js'; -import {includes} from './polyfill.js'; -import { hook } from './hook.js'; +import {deepAccess, logError} from './utils.js'; +import {config} from '../src/config.js'; +import {hook} from './hook.js'; import {auctionManager} from './auctionManager.js'; -const VIDEO_MEDIA_TYPE = 'video'; export const OUTSTREAM = 'outstream'; export const INSTREAM = 'instream'; -/** - * Helper functions for working with video-enabled adUnits - */ -export const videoAdUnit = adUnit => { - const mediaType = adUnit.mediaType === VIDEO_MEDIA_TYPE; - const mediaTypes = deepAccess(adUnit, 'mediaTypes.video'); - return mediaType || mediaTypes; -}; -export const videoBidder = bid => includes(adapterManager.videoAdapters, bid.bidder); -export const hasNonVideoBidder = adUnit => - adUnit.bids.filter(bid => !videoBidder(bid)).length; +export function fillVideoDefaults(adUnit) { + const video = adUnit?.mediaTypes?.video; + if (video != null && video.plcmt == null) { + if (video.context === OUTSTREAM || [2, 3, 4].includes(video.placement)) { + video.plcmt = 4; + } else if (video.context !== OUTSTREAM && [2, 6].includes(video.playbackmethod)) { + video.plcmt = 2; + } + } +} /** * @typedef {object} VideoBid diff --git a/test/mocks/xhr.js b/test/mocks/xhr.js index 424100f870c..e7b1d96f0a4 100644 --- a/test/mocks/xhr.js +++ b/test/mocks/xhr.js @@ -1,12 +1,236 @@ import {getUniqueIdentifierStr} from '../../src/utils.js'; +import {GreedyPromise} from '../../src/utils/promise.js'; +import {fakeXhr} from 'nise'; +import {dep} from 'src/ajax.js'; -export let server = sinon.createFakeServer(); -export let xhr = global.XMLHttpRequest; +export const xhr = sinon.useFakeXMLHttpRequest(); +export const server = mockFetchServer(); -beforeEach(function() { - server.restore(); - server = sinon.createFakeServer(); - xhr = global.XMLHttpRequest; +/** + * An (incomplete) replica of nise's fakeServer, but backing fetch used in ajax.js (rather than XHR). + */ +function mockFetchServer() { + const sandbox = sinon.createSandbox(); + const bodies = new WeakMap(); + const requests = []; + const {DONE, UNSENT} = XMLHttpRequest; + + function makeRequest(resource, options) { + const requestBody = options?.body || bodies.get(resource); + const request = new Request(resource, options); + bodies.set(request, requestBody); + return request; + } + + function mockXHR(resource, options) { + let resolve, reject; + const promise = new GreedyPromise((res, rej) => { + resolve = res; + reject = rej; + }); + + function error(reason = new TypeError('Failed to fetch')) { + mockReq.status = 0; + reject(reason); + } + + const request = makeRequest(resource, options); + request.signal.onabort = () => error(new DOMException('The user aborted a request')); + let responseHeaders; + + const mockReq = { + fetch: { + request, + requestBody: bodies.get(request), + promise, + }, + readyState: UNSENT, + url: request.url, + method: request.method, + requestBody: bodies.get(request), + status: 0, + statusText: '', + requestHeaders: new Proxy(request.headers, { + get(target, prop) { + return typeof prop === 'string' && target.has(prop) ? target.get(prop) : {}[prop]; + }, + has(target, prop) { + return typeof prop === 'string' && target.has(prop); + }, + ownKeys(target) { + return Array.from(target.keys()); + }, + getOwnPropertyDescriptor(target, prop) { + if (typeof prop === 'string' && target.has(prop)) { + return { + enumerable: true, + configurable: true, + writable: false, + value: target.get(prop) + } + } + } + }), + withCredentials: request.credentials === 'include', + setStatus(status) { + // nise replaces invalid status with 200 + status = typeof status === 'number' ? status : 200; + mockReq.status = status; + mockReq.statusText = fakeXhr.FakeXMLHttpRequest.statusCodes[status] || ''; + }, + setResponseHeaders(headers) { + responseHeaders = headers; + }, + setResponseBody(body) { + if (mockReq.status === 0) { + error(); + return; + } + const resp = Object.defineProperties(new Response(body, { + status: mockReq.status, + statusText: mockReq.statusText, + headers: responseHeaders || {}, + }), { + url: { + get: () => mockReq.fetch.request.url, + } + }); + mockReq.readyState = DONE; + // tests expect respond() to run everything immediately, + // so make body available syncronously + resp.text = () => GreedyPromise.resolve(body || ''); + Object.assign(mockReq.fetch, { + response: resp, + responseBody: body || '' + }) + resolve(resp); + }, + respond(status = 200, headers, body) { + mockReq.setStatus(status); + mockReq.setResponseHeaders(headers); + mockReq.setResponseBody(body); + }, + error + }; + return mockReq; + } + + let enabled = false; + let timeoutsEnabled = false; + + function enable() { + if (!enabled) { + sandbox.stub(dep, 'fetch').callsFake((resource, options) => { + const req = mockXHR(resource, options); + requests.push(req); + return req.fetch.promise; + }); + sandbox.stub(dep, 'makeRequest').callsFake(makeRequest); + const timeout = dep.timeout; + sandbox.stub(dep, 'timeout').callsFake(function () { + if (timeoutsEnabled) { + return timeout.apply(null, arguments); + } else { + return {}; + } + }); + enabled = true; + } + } + + enable(); + + const responders = []; + + function respondWith() { + let response, urlMatcher, methodMatcher; + urlMatcher = methodMatcher = () => true; + switch (arguments.length) { + case 1: + ([response] = arguments); + break; + case 2: + ([urlMatcher, response] = arguments); + break; + case 3: + ([methodMatcher, urlMatcher, response] = arguments); + methodMatcher = ((toMatch) => (method) => method === toMatch)(methodMatcher); + break; + default: + throw new Error('Invalid respondWith invocation'); + } + if (typeof urlMatcher.exec === 'function') { + urlMatcher = ((rx) => (url) => rx.exec(url)?.slice(1))(urlMatcher); + } else if (typeof urlMatcher === 'string') { + urlMatcher = ((toMatch) => (url) => url === toMatch)(urlMatcher); + } + responders.push((req) => { + if (req.readyState !== DONE && methodMatcher(req.method)) { + const arg = urlMatcher(req.url); + if (arg) { + if (typeof response === 'function') { + response(req, ...(Array.isArray(arg) ? arg : [])); + } else if (typeof response === 'string') { + req.respond(200, null, response); + } else { + req.respond.apply(req, response); + } + } + } + }); + } + + function resetState() { + requests.length = 0; + responders.length = 0; + timeoutsEnabled = false; + } + + return { + requests, + enable, + restore() { + resetState(); + sandbox.restore(); + enabled = false; + }, + reset() { + sandbox.resetHistory(); + resetState(); + }, + respondWith, + respond() { + if (arguments.length > 0) { + respondWith.apply(null, arguments); + } + requests.forEach(req => { + for (let i = responders.length - 1; i >= 0; i--) { + responders[i](req); + if (req.readyState === DONE) break; + } + if (req.readyState !== DONE) { + req.respond(404, {}, ''); + } + }); + }, + /** + * the timeout mechanism is quite different between XHR and fetch + * by default, mocked fetch does not time out - to reflect fakeServer XHRs + * note that many tests will fire requests without caring or waiting for their response - + * if they are timed out later, during unrelated tests, the log messages might interfere with their + * assertions + */ + get autoTimeout() { + return timeoutsEnabled; + }, + set autoTimeout(val) { + timeoutsEnabled = !!val; + } + }; +} + +beforeEach(function () { + server.reset(); }); const bid = getUniqueIdentifierStr().substring(4); @@ -20,12 +244,35 @@ afterEach(function () { return (s) => s.split('\n').map(s => `${preamble} ${s}`).join('\n'); })(); + function format(obj, body = null) { + if (obj == null) return obj; + const fmt = {}; + let node = obj; + while (node != null) { + Object.keys(node).forEach((k) => { + const val = obj[k]; + if (typeof val !== 'function' && !fmt.hasOwnProperty(k)) { + fmt[k] = val; + } + }); + node = Object.getPrototypeOf(node); + } + if (obj.headers != null) { + fmt.headers = Object.fromEntries(obj.headers.entries()) + } + fmt.body = body; + return fmt; + } + - console.log(prepend(`XHR mock state after failure (for test '${this.currentTest.fullTitle()}'): ${server.requests.length} requests`)) + console.log(prepend(`XHR mock state after failure (for test '${this.currentTest.fullTitle()}'): ${server.requests.length} requests`)); server.requests.forEach((req, i) => { console.log(prepend(`Request #${i}:`)); - console.log(prepend(JSON.stringify(req, null, 2))); - }) + console.log(prepend(JSON.stringify({ + request: format(req.fetch.request, req.fetch.requestBody), + response: format(req.fetch.response, req.fetch.responseBody) + }, null, 2))); + }); } }); /* eslint-enable */ diff --git a/test/spec/appnexusKeywords_spec.js b/test/spec/appnexusKeywords_spec.js index 9bf567a27c5..68faeff0b82 100644 --- a/test/spec/appnexusKeywords_spec.js +++ b/test/spec/appnexusKeywords_spec.js @@ -1,4 +1,4 @@ -import {transformBidderParamKeywords} from '../../libraries/appnexusKeywords/anKeywords.js'; +import {transformBidderParamKeywords} from '../../libraries/appnexusUtils/anKeywords.js'; import {expect} from 'chai/index.js'; import * as utils from '../../src/utils.js'; diff --git a/test/spec/auctionmanager_spec.js b/test/spec/auctionmanager_spec.js index dd59b16d492..137d009bd18 100644 --- a/test/spec/auctionmanager_spec.js +++ b/test/spec/auctionmanager_spec.js @@ -5,7 +5,7 @@ import { adjustBids, getMediaTypeGranularity, getPriceByGranularity, - addBidResponse + addBidResponse, resetAuctionState, responsesReady } from 'src/auction.js'; import CONSTANTS from 'src/constants.json'; import * as auctionModule from 'src/auction.js'; @@ -23,6 +23,7 @@ import {AuctionIndex} from '../../src/auctionIndex.js'; import {expect} from 'chai'; import {deepClone} from '../../src/utils.js'; import { IMAGE as ortbNativeRequest } from 'src/native.js'; +import {PrebidServer} from '../../modules/prebidServerBidAdapter/index.js'; var assert = require('assert'); @@ -48,6 +49,7 @@ function mockBid(opts) { let bidderCode = opts && opts.bidderCode; return { + adUnitCode: opts?.adUnitCode || ADUNIT_CODE, 'ad': 'creative', 'cpm': '1.99', 'width': 300, @@ -68,6 +70,11 @@ function mockBid(opts) { transactionId: this.transactionId, auctionId: this.auctionId } + }, + _ctx: { + adUnits: opts?.adUnits, + src: opts?.src, + uniquePbsTid: opts?.uniquePbsTid, } }; } @@ -96,6 +103,9 @@ function mockBidRequest(bid, opts) { 'bidderCode': bidderCode || bid.bidderCode, 'auctionId': opts && opts.auctionId, 'bidderRequestId': requestId, + src: bid?._ctx?.src, + adUnitsS2SCopy: bid?._ctx?.src === CONSTANTS.S2S.SRC ? bid?._ctx?.adUnits : undefined, + uniquePbsTid: bid?._ctx?.src === CONSTANTS.S2S.SRC ? bid?._ctx?.uniquePbsTid : undefined, 'bids': [ { 'bidder': bidderCode || bid.bidderCode, @@ -108,7 +118,8 @@ function mockBidRequest(bid, opts) { 'bidId': bid.requestId, 'bidderRequestId': requestId, 'auctionId': opts && opts.auctionId, - 'mediaTypes': mediaType + 'mediaTypes': mediaType, + src: bid?._ctx?.src } ], 'auctionStart': 1505250713622, @@ -160,6 +171,7 @@ describe('auctionmanager.js', function () { indexAuctions = []; indexStub = sinon.stub(auctionManager, 'index'); indexStub.get(() => new AuctionIndex(() => indexAuctions)); + resetAuctionState(); }); afterEach(() => { @@ -185,9 +197,11 @@ describe('auctionmanager.js', function () { adId: '1adId', source: 'client', mediaType: 'banner', + creativeId: 'monkeys', meta: { advertiserDomains: ['adomain'], - primaryCatId: 'IAB-test' + primaryCatId: 'IAB-test', + networkId: '123987' } }; @@ -202,6 +216,8 @@ describe('auctionmanager.js', function () { expected[ CONSTANTS.TARGETING_KEYS.FORMAT ] = bid.mediaType; expected[ CONSTANTS.TARGETING_KEYS.ADOMAIN ] = bid.meta.advertiserDomains[0]; expected[ CONSTANTS.TARGETING_KEYS.ACAT ] = bid.meta.primaryCatId; + expected[ CONSTANTS.TARGETING_KEYS.DSP ] = bid.meta.networkId; + expected[ CONSTANTS.TARGETING_KEYS.CRID ] = bid.creativeId; if (bid.mediaType === 'video') { expected[ CONSTANTS.TARGETING_KEYS.UUID ] = bid.videoCacheKey; expected[ CONSTANTS.TARGETING_KEYS.CACHE_ID ] = bid.videoCacheKey; @@ -304,6 +320,18 @@ describe('auctionmanager.js', function () { return bidResponse.meta.advertiserDomains[0]; } }, + { + key: CONSTANTS.TARGETING_KEYS.CRID, + val: function (bidResponse) { + return bidResponse.creativeId; + } + }, + { + key: CONSTANTS.TARGETING_KEYS.DSP, + val: function (bidResponse) { + return bidResponse.meta.networkId; + } + }, { key: CONSTANTS.TARGETING_KEYS.ACAT, val: function (bidResponse) { @@ -388,6 +416,18 @@ describe('auctionmanager.js', function () { return bidResponse.meta.advertiserDomains[0]; } }, + { + key: CONSTANTS.TARGETING_KEYS.CRID, + val: function (bidResponse) { + return bidResponse.creativeId; + } + }, + { + key: CONSTANTS.TARGETING_KEYS.DSP, + val: function (bidResponse) { + return bidResponse.meta.networkId; + } + }, { key: CONSTANTS.TARGETING_KEYS.ACAT, val: function (bidResponse) { @@ -487,7 +527,7 @@ describe('auctionmanager.js', function () { s2sConfig: { accountId: '1', enabled: true, - defaultVendor: 'appnexus', + defaultVendor: 'appnexuspsp', bidders: ['appnexus'], timeout: 1000, adapter: 'prebidServer' @@ -735,9 +775,10 @@ describe('auctionmanager.js', function () { }); describe('createAuction', () => { - let adUnits, stubMakeBidRequests, stubCallAdapters + let adUnits, stubMakeBidRequests, stubCallAdapters, bids; beforeEach(() => { + bids = []; stubMakeBidRequests = sinon.stub(adapterManager, 'makeBidRequests').returns([{ bidderCode: BIDDER_CODE, bids: [{ @@ -745,6 +786,7 @@ describe('auctionmanager.js', function () { }] }]); stubCallAdapters = sinon.stub(adapterManager, 'callBids').callsFake((au, reqs, addBid, done) => { + bids.forEach(bid => addBid(bid.adUnitCode, bid)); reqs.forEach(r => done.apply(r)); }); adUnits = [{ @@ -759,6 +801,7 @@ describe('auctionmanager.js', function () { afterEach(() => { stubMakeBidRequests.restore(); stubCallAdapters.restore(); + auctionManager.clearAllAuctions(); }); it('passes global and bidder ortb2 to the auction', () => { @@ -786,6 +829,79 @@ describe('auctionmanager.js', function () { }); expect(auction.getNonBids()[0]).to.equal('test'); }); + + describe('stale auctions', () => { + let clock, auction; + beforeEach(() => { + clock = sinon.useFakeTimers(); + auction = auctionManager.createAuction({adUnits}); + indexAuctions.push(auction); + }); + afterEach(() => { + clock.restore(); + config.resetConfig(); + }); + + it('are dropped after their last bid becomes stale (if minBidCacheTTL is set)', () => { + config.setConfig({ + minBidCacheTTL: 0 + }); + bids = [ + { + adUnitCode: ADUNIT_CODE, + transactionId: ADUNIT_CODE, + ttl: 10 + }, { + adUnitCode: ADUNIT_CODE, + transactionId: ADUNIT_CODE, + ttl: 100 + } + ]; + auction.callBids(); + return auction.end.then(() => { + clock.tick(50 * 1000); + expect(auctionManager.getBidsReceived().length).to.equal(2); + clock.tick(56 * 1000); + expect(auctionManager.getBidsReceived()).to.eql([]); + }); + }); + + it('are dropped after `minBidCacheTTL` seconds if they had no bid', () => { + auction.callBids(); + config.setConfig({ + minBidCacheTTL: 2 + }); + return auction.end.then(() => { + expect(auctionManager.getNoBids().length).to.eql(1); + clock.tick(10 * 10000); + expect(auctionManager.getNoBids().length).to.eql(0); + }) + }); + + Object.entries({ + 'bids': { + bd: [{ + adUnitCode: ADUNIT_CODE, + transactionId: ADUNIT_CODE, + ttl: 10 + }], + entries: () => auctionManager.getBidsReceived() + }, + 'no bids': { + bd: [], + entries: () => auctionManager.getNoBids() + } + }).forEach(([t, {bd, entries}]) => { + it(`with ${t} are never dropped if minBidCacheTTL is not set`, () => { + bids = bd; + auction.callBids(); + return auction.end.then(() => { + clock.tick(100 * 1000); + expect(entries().length > 0).to.be.true; + }) + }) + }); + }) }); describe('addBidResponse #1', function () { @@ -811,6 +927,11 @@ describe('auctionmanager.js', function () { beforeEach(function () { ajaxStub = sinon.stub(ajaxLib, 'ajaxBuilder').callsFake(mockAjaxBuilder); adUnits = [{ + mediaTypes: { + banner: { + sizes: [] + } + }, code: ADUNIT_CODE, transactionId: ADUNIT_CODE, bids: [ @@ -996,36 +1117,47 @@ describe('auctionmanager.js', function () { assert.strictEqual(addedBid.renderer.url, myBid.renderer.url); }); - it('bid for a regular unit and a video unit', function() { - let renderer = { - url: 'renderer.js', - render: (bid) => bid - }; - Object.assign(adUnits[0], {renderer}); - // make sure that if the renderer is only on the second ad unit, prebid - // still correctly uses it - let bid = mockBid(); - let bidRequests = [mockBidRequest(bid, {auctionId: auction.getAuctionId()})]; - - bidRequests[0].bids[1] = Object.assign({ - bidId: utils.getUniqueIdentifierStr() - }, bidRequests[0].bids[0]); - Object.assign(bidRequests[0].bids[0], { - adUnitCode: ADUNIT_CODE1, - transactionId: ADUNIT_CODE1, - }); + describe('bid for a regular unit and a video unit', () => { + beforeEach(() => { + const renderer = { + url: 'renderer.js', + render: (bid) => bid + }; + Object.assign(adUnits[0], {renderer}); + // make sure that if the renderer is only on the second ad unit, prebid + // still correctly uses it + let bid = mockBid(); + let bidRequests = [mockBidRequest(bid, {auctionId: auction.getAuctionId()})]; + + bidRequests[0].bids[1] = Object.assign({ + bidId: utils.getUniqueIdentifierStr() + }, bidRequests[0].bids[0]); + Object.assign(bidRequests[0].bids[0], { + adUnitCode: ADUNIT_CODE1, + transactionId: ADUNIT_CODE1, + }); - makeRequestsStub.returns(bidRequests); + makeRequestsStub.returns(bidRequests); - // this should correspond with the second bid in the bidReq because of the ad unit code - bid.mediaType = 'video-outstream'; - spec.interpretResponse.returns(bid); + // this should correspond with the second bid in the bidReq because of the ad unit code + bid.mediaType = 'video-outstream'; + spec.interpretResponse.returns(bid); + }); - auction.callBids(); + it('should use renderers on bid response', () => { + auction.callBids(); - const addedBid = find(auction.getBidsReceived(), bid => bid.adUnitCode == ADUNIT_CODE); - assert.equal(addedBid.renderer.url, 'renderer.js'); - }); + const addedBid = find(auction.getBidsReceived(), bid => bid.adUnitCode === ADUNIT_CODE); + assert.equal(addedBid.renderer.url, 'renderer.js'); + }); + + it('should resolve .end', () => { + auction.callBids(); + return auction.end.then(() => { + expect(auction.getBidsReceived().length).to.eql(1); + }) + }); + }) it('sets bidResponse.ttlBuffer from adUnit.ttlBuffer', () => { adUnits[0].ttlBuffer = 0; @@ -1035,12 +1167,19 @@ describe('auctionmanager.js', function () { }); describe('when auction timeout is 20', function () { - let eventsEmitSpy; + let eventsEmitSpy, auctionDone; - function setupBids(auctionId) { - bids = [mockBid(), mockBid({ bidderCode: BIDDER_CODE1 })]; - let bidRequests = bids.map(bid => mockBidRequest(bid, {auctionId})); + function respondToRequest(requestIndex) { + server.requests[requestIndex].respond(200, {}, 'response body'); + } + + function runAuction() { + let bidRequests = bids.map(bid => mockBidRequest(bid, {auctionId: auction.getAuctionId()})); makeRequestsStub.returns(bidRequests); + return new Promise((resolve) => { + auctionDone = resolve; + auction.callBids(); + }) } beforeEach(function () { @@ -1049,86 +1188,156 @@ describe('auctionmanager.js', function () { transactionId: ADUNIT_CODE, bids: [ {bidder: BIDDER_CODE, params: {placementId: 'id'}}, + {bidder: BIDDER_CODE1, params: {placementId: 'id'}}, ] }]; adUnitCodes = [ADUNIT_CODE]; eventsEmitSpy = sinon.spy(events, 'emit'); + bids = [mockBid(), mockBid({ bidderCode: BIDDER_CODE1 })]; + const spec1 = mockBidder(BIDDER_CODE, [bids[0]]); + registerBidder(spec1); + const spec2 = mockBidder(BIDDER_CODE1, [bids[1]]); + registerBidder(spec2); + auction = auctionModule.newAuction({adUnits, adUnitCodes, callback: () => auctionDone(), cbTimeout: 20}); + indexAuctions = [auction]; }); + afterEach(function () { events.emit.restore(); }); - it('should emit BID_TIMEOUT and AUCTION_END for timed out bids', function (done) { - const spec1 = mockBidder(BIDDER_CODE, [bids[0]]); - registerBidder(spec1); - const spec2 = mockBidder(BIDDER_CODE1, [bids[1]]); - registerBidder(spec2); + it('resolves .end on timeout', () => { + let endResolved = false; + auction.end.then(() => { + endResolved = true; + }) + const pm = runAuction().then(() => { + expect(endResolved).to.be.true; + }); + respondToRequest(0); + return pm; + }); - function respondToRequest(requestIndex) { - server.requests[requestIndex].respond(200, {}, 'response body'); - } - function auctionCallback() { + describe('AUCTION_TIMEOUT event', () => { + let handler; + beforeEach(() => { + handler = sinon.spy(); + events.on(CONSTANTS.EVENTS.AUCTION_TIMEOUT, handler); + }) + afterEach(() => { + events.off(CONSTANTS.EVENTS.AUCTION_TIMEOUT, handler); + }); + + Object.entries({ + 'is fired on timeout': [true, [0]], + 'is NOT fired otherwise': [false, [0, 1]], + }).forEach(([t, [shouldFire, respond]]) => { + it(t, () => { + const pm = runAuction().then(() => { + if (shouldFire) { + sinon.assert.calledWith(handler, sinon.match({auctionId: auction.getAuctionId()})) + } else { + sinon.assert.notCalled(handler); + } + }); + respond.forEach(respondToRequest); + return pm; + }) + }); + }); + + it('should emit BID_TIMEOUT and AUCTION_END for timed out bids', function () { + const pm = runAuction().then(() => { const bidTimeoutCall = eventsEmitSpy.withArgs(CONSTANTS.EVENTS.BID_TIMEOUT).getCalls()[0]; const timedOutBids = bidTimeoutCall.args[1]; assert.equal(timedOutBids.length, 1); assert.equal(timedOutBids[0].bidder, BIDDER_CODE1); // Check that additional properties are available - assert.equal(timedOutBids[0].params.placementId, 'id'); + assert.equal(timedOutBids[0].params[0].placementId, 'id'); const auctionEndCall = eventsEmitSpy.withArgs(CONSTANTS.EVENTS.AUCTION_END).getCalls()[0]; const auctionProps = auctionEndCall.args[1]; assert.equal(auctionProps.adUnits, adUnits); assert.equal(auctionProps.timeout, 20); assert.equal(auctionProps.auctionStatus, AUCTION_COMPLETED) - done(); - } - auction = auctionModule.newAuction({adUnits, adUnitCodes, callback: auctionCallback, cbTimeout: 20}); - setupBids(auction.getAuctionId()); - - auction.callBids(); + }); respondToRequest(0); + return pm; }); - it('should NOT emit BID_TIMEOUT when all bidders responded in time', function (done) { - const spec1 = mockBidder(BIDDER_CODE, [bids[0]]); - registerBidder(spec1); - const spec2 = mockBidder(BIDDER_CODE1, [bids[1]]); - registerBidder(spec2); - function respondToRequest(requestIndex) { - server.requests[requestIndex].respond(200, {}, 'response body'); - } - function auctionCallback() { + it('should NOT emit BID_TIMEOUT when all bidders responded in time', function () { + const pm = runAuction().then(() => { assert.ok(eventsEmitSpy.withArgs(CONSTANTS.EVENTS.BID_TIMEOUT).notCalled, 'did not emit event BID_TIMEOUT'); - done(); - } - auction = auctionModule.newAuction({adUnits, adUnitCodes, callback: auctionCallback, cbTimeout: 20}); - setupBids(auction.getAuctionId()); - auction.callBids(); + }); respondToRequest(0); respondToRequest(1); + return pm; }); - it('should NOT emit BID_TIMEOUT for bidders which responded in time but with an empty bid', function (done) { - const spec1 = mockBidder(BIDDER_CODE, []); - registerBidder(spec1); - const spec2 = mockBidder(BIDDER_CODE1, []); - registerBidder(spec2); - function respondToRequest(requestIndex) { - server.requests[requestIndex].respond(200, {}, 'response body'); - } - function auctionCallback() { + it('should NOT emit BID_TIMEOUT for bidders which responded in time but with an empty bid', function () { + const pm = runAuction().then(() => { const bidTimeoutCall = eventsEmitSpy.withArgs(CONSTANTS.EVENTS.BID_TIMEOUT).getCalls()[0]; const timedOutBids = bidTimeoutCall.args[1]; assert.equal(timedOutBids.length, 1); assert.equal(timedOutBids[0].bidder, BIDDER_CODE1); - done(); - } - auction = auctionModule.newAuction({adUnits, adUnitCodes, callback: auctionCallback, cbTimeout: 20}); - setupBids(auction.getAuctionId()); - auction.callBids(); + }); respondToRequest(0); + return pm; }); + + it('should NOT emit BID_TIMEOUT for bidders that replied through S2S', () => { + adapterManager.registerBidAdapter(new PrebidServer(), 'pbs'); + config.setConfig({ + s2sConfig: [{ + accountId: '1', + enabled: true, + defaultVendor: 'appnexuspsp', + bidders: ['mock-s2s-1'], + adapter: 'pbs' + }, { + accountId: '1', + enabled: true, + defaultVendor: 'rubicon', + bidders: ['mock-s2s-2'], + adapter: 'pbs' + }] + }) + adUnits[0].bids.push({bidder: 'mock-s2s-1'}, {bidder: 'mock-s2s-2'}) + const s2sAdUnits = deepClone(adUnits); + bids.unshift( + mockBid({bidderCode: 'mock-s2s-1', src: CONSTANTS.S2S.SRC, adUnits: s2sAdUnits, uniquePbsTid: '1'}), + mockBid({bidderCode: 'mock-s2s-2', src: CONSTANTS.S2S.SRC, adUnits: s2sAdUnits, uniquePbsTid: '2'}) + ); + Object.assign(s2sAdUnits[0], { + mediaTypes: { + banner: { + sizes: [[300, 250], [300, 600]] + } + }, + bids: [ + { + bidder: 'mock-s2s-1', + bid_id: bids[0].requestId + }, + { + bidder: 'mock-s2s-2', + bid_id: bids[1].requestId + } + ] + }) + + const pm = runAuction().then(() => { + const toBids = eventsEmitSpy.withArgs(CONSTANTS.EVENTS.BID_TIMEOUT).getCalls()[0].args[1] + expect(toBids.map(bid => bid.bidder)).to.eql([ + 'mock-s2s-2', + BIDDER_CODE, + BIDDER_CODE1, + ]) + }); + respondToRequest(1); + return pm; + }) }); }); @@ -1623,116 +1832,54 @@ describe('auctionmanager.js', function () { sinon.assert.calledWith(auction.addBidReceived, sinon.match({cpm: 1.23})); }) - describe('when addBidResponse hook returns promises', () => { - let resolvers, callbacks, bids; + describe('when responsesReady defers', () => { + let resolve, reject, promise, callbacks, bids; - function hook(next, ...args) { - next.bail(new Promise((resolve, reject) => { - resolvers.resolve.push(resolve); - resolvers.reject.push(reject); - }).finally(() => next(...args))); + function hook(next, ready) { + next(ready.then(() => promise)); } - function invokeCallbacks() { - bids.forEach((bid) => callbacks.addBidResponse(ADUNIT_CODE, bid)); - bidRequests.forEach(bidRequest => callbacks.adapterDone.call(bidRequest)); - } + before(() => { + responsesReady.before(hook); + }); - function delay(ms = 0) { - return new Promise((resolve) => { - setTimeout(resolve, ms) - }); - } + after(() => { + responsesReady.getHooks({hook}).remove(); + }); beforeEach(() => { + // eslint-disable-next-line promise/param-names + promise = new Promise((rs, rj) => { + resolve = rs; + reject = rj; + }); bids = [ mockBid({bidderCode: BIDDER_CODE1}), mockBid({bidderCode: BIDDER_CODE}) ] bidRequests = bids.map((b) => mockBidRequest(b)); - resolvers = {resolve: [], reject: []}; - addBidResponse.before(hook); callbacks = auctionCallbacks(doneSpy, auction); Object.assign(auction, { addNoBid: sinon.spy() }); }); - afterEach(() => { - addBidResponse.getHooks({hook: hook}).remove(); - }); - - it('should wait for bids without a request bids before calling auctionDone', () => { - callbacks.addBidResponse(ADUNIT_CODE, Object.assign(mockBid(), {requestId: null})); - invokeCallbacks(); - resolvers.resolve.slice(1, 3).forEach((fn) => fn()); - return delay().then(() => { - expect(doneSpy.called).to.be.false; - resolvers.resolve[0](); - return delay(); - }).then(() => { - expect(doneSpy.called).to.be.true; - }); - }); - Object.entries({ - 'all succeed': ['resolve', 'resolve'], - 'some fail': ['resolve', 'reject'], - 'all fail': ['reject', 'reject'] - }).forEach(([test, results]) => { - describe(`(and ${test})`, () => { - it('should wait for them to complete before calling auctionDone', () => { - invokeCallbacks(); - return delay().then(() => { - expect(doneSpy.called).to.be.false; - expect(auction.addNoBid.called).to.be.false; - resolvers[results[0]][0](); - return delay(); - }).then(() => { - expect(doneSpy.called).to.be.false; - expect(auction.addNoBid.called).to.be.false; - resolvers[results[1]][1](); - return delay(); - }).then(() => { - expect(doneSpy.called).to.be.true; - }); - }); + 'resolve': () => resolve(), + 'reject': () => reject(), + }).forEach(([t, resolver]) => { + it(`should wait for responsesReady to ${t} before calling auctionDone`, (done) => { + bidRequests.forEach(bidRequest => callbacks.adapterDone.call(bidRequest)); + setTimeout(() => { + sinon.assert.notCalled(doneSpy); + resolver(); + setTimeout(() => { + sinon.assert.called(doneSpy); + done(); + }) + }) }); }); - - Object.entries({ - bidder: (timeout) => { - bidRequests.forEach((r) => r.timeout = timeout); - auction.getTimeout = () => timeout + 10000 - }, - auction: (timeout) => { - auction.getTimeout = () => timeout; - bidRequests.forEach((r) => r.timeout = timeout + 10000) - } - }).forEach(([test, setTimeout]) => { - it(`should respect ${test} timeout if they never complete`, () => { - const start = Date.now() - 2900; - auction.getAuctionStart = () => start; - setTimeout(3000); - invokeCallbacks(); - return delay().then(() => { - expect(doneSpy.called).to.be.false; - return delay(100); - }).then(() => { - expect(doneSpy.called).to.be.true; - }); - }); - - it(`should not wait if ${test} has already timed out`, () => { - const start = Date.now() - 2000; - auction.getAuctionStart = () => start; - setTimeout(1000); - invokeCallbacks(); - return delay().then(() => { - expect(doneSpy.called).to.be.true; - }); - }); - }) }); describe('when bids are rejected', () => { diff --git a/test/spec/creative/crossDomainCreative_spec.js b/test/spec/creative/crossDomainCreative_spec.js new file mode 100644 index 00000000000..765d5e5311a --- /dev/null +++ b/test/spec/creative/crossDomainCreative_spec.js @@ -0,0 +1,182 @@ +import {renderer} from '../../../libraries/creativeRender/crossDomain.js'; +import { + AD_RENDER_FAILED, AD_RENDER_SUCCEEDED, EXCEPTION, NO_AD, + PREBID_EVENT, + PREBID_REQUEST, + PREBID_RESPONSE +} from '../../../libraries/creativeRender/constants.js'; + +describe('cross-domain creative', () => { + let win, renderAd, messages, mkIframe, listeners; + + beforeEach(() => { + messages = []; + listeners = []; + mkIframe = sinon.stub(); + win = { + document: { + body: { + appendChild: sinon.stub(), + }, + createElement: sinon.stub().callsFake(tagname => { + switch (tagname.toLowerCase()) { + case 'a': + return document.createElement('a') + case 'iframe': { + return mkIframe(); + } + } + }) + }, + addEventListener: sinon.stub().callsFake((_, listener) => listeners.push(listener)), + parent: { + postMessage: sinon.stub().callsFake((payload, targetOrigin, transfer) => { + messages.push({payload: JSON.parse(payload), targetOrigin, transfer}); + }) + } + }; + renderAd = renderer(win); + }) + + it('derives postMessage target origin from pubUrl ', () => { + renderAd({pubUrl: 'https://domain.com:123/path'}); + expect(messages[0].targetOrigin).to.eql('https://domain.com:123') + }); + + it('generates request message with adId and clickUrl', () => { + renderAd({adId: '123', clickUrl: 'https://click-url.com'}); + expect(messages[0].payload).to.eql({ + message: PREBID_REQUEST, + adId: '123', + options: { + clickUrl: 'https://click-url.com' + } + }) + }) + + Object.entries({ + 'MessageChannel': (msg) => messages[0].transfer[0].postMessage(msg), + 'message listener': (msg) => listeners.forEach((fn) => fn({data: msg})) + }).forEach(([t, transport]) => { + describe(`when using ${t}`, () => { + function reply(msg) { + transport(JSON.stringify(msg)) + }; + + it('ignores messages that are not a prebid response message', () => { + renderAd({adId: '123'}); + reply({adId: '123', ad: 'markup'}); + sinon.assert.notCalled(mkIframe); + }) + + describe('signals AD_RENDER_FAILED', () => { + it('on exception', (done) => { + mkIframe.callsFake(() => { throw new Error('error message') }); + renderAd({adId: '123'}); + reply({message: PREBID_RESPONSE, adId: '123', ad: 'markup'}); + setTimeout(() => { + expect(messages[1].payload).to.eql({ + message: PREBID_EVENT, + adId: '123', + event: AD_RENDER_FAILED, + info: { + reason: EXCEPTION, + message: 'error message' + } + }) + done(); + }, 100) + }); + + it('on missing ad', (done) => { + renderAd({adId: '123'}); + reply({message: PREBID_RESPONSE, adId: '123'}); + setTimeout(() => { + sinon.assert.match(messages[1].payload, { + message: PREBID_EVENT, + adId: '123', + event: AD_RENDER_FAILED, + info: { + reason: NO_AD, + } + }) + done(); + }, 100) + }) + }); + + describe('rendering', () => { + let iframe; + + beforeEach(() => { + iframe = { + attrs: {}, + setAttribute: sinon.stub().callsFake((k, v) => iframe.attrs[k.toLowerCase()] = v), + contentDocument: { + open: sinon.stub(), + write: sinon.stub(), + close: sinon.stub(), + } + } + mkIframe.callsFake(() => iframe); + }); + + it('renders adUrl as iframe src', (done) => { + renderAd({adId: '123'}); + reply({message: PREBID_RESPONSE, adId: '123', adUrl: 'some-url'}); + setTimeout(() => { + sinon.assert.calledWith(win.document.body.appendChild, iframe); + expect(iframe.attrs.src).to.eql('some-url'); + done(); + }, 100) + }); + + it('renders ad through document.write', (done) => { + renderAd({adId: '123'}); + reply({message: PREBID_RESPONSE, adId: '123', ad: 'some-markup'}); + setTimeout(() => { + sinon.assert.calledWith(win.document.body.appendChild, iframe); + sinon.assert.called(iframe.contentDocument.open); + sinon.assert.calledWith(iframe.contentDocument.write, 'some-markup'); + sinon.assert.called(iframe.contentDocument.close); + done(); + }, 100) + }); + + Object.entries({ + adUrl: 'mock-ad-url', + ad: 'mock-ad-markup' + }).forEach(([prop, propValue]) => { + describe(`when message has ${prop}`, () => { + beforeEach((done) => { + renderAd({adId: '123'}); + reply({ + message: PREBID_RESPONSE, + adId: '123', + [prop]: propValue, + width: 100, + height: 200 + }); + setTimeout(done, 100); + }); + + it('emits AD_RENDER_SUCCEEDED', () => { + expect(messages[1].payload).to.eql({ + message: PREBID_EVENT, + adId: '123', + event: AD_RENDER_SUCCEEDED + }) + }); + + it('sets iframe height / width to ad height / width', () => { + sinon.assert.match(iframe.attrs, { + width: 100, + height: 200 + }) + }) + }) + }) + }); + }); + }); +}); diff --git a/test/spec/e2e/modules/e2e_consent_mgt_gdpr.spec.js b/test/spec/e2e/modules/e2e_consent_mgt_gdpr.spec.js index 5b5ea2ef2cd..d1803c9784a 100644 --- a/test/spec/e2e/modules/e2e_consent_mgt_gdpr.spec.js +++ b/test/spec/e2e/modules/e2e_consent_mgt_gdpr.spec.js @@ -1,4 +1,4 @@ -/** +/* TODO: old CMP no longer works; see if we can fix this with https://github.com/prebid/Prebid.js/issues/6377 const expect = require('chai').expect; const { testPageURL, switchFrame, waitForElement } = require('../../../helpers/testing-utils'); @@ -59,4 +59,4 @@ describe('Prebid.js GDPR Ad Unit Test', function () { expect(ele.isExisting()).to.be.true; }); }); -**/ + */ diff --git a/test/spec/fpd/enrichment_spec.js b/test/spec/fpd/enrichment_spec.js index 328846ca081..40692360dca 100644 --- a/test/spec/fpd/enrichment_spec.js +++ b/test/spec/fpd/enrichment_spec.js @@ -3,7 +3,10 @@ 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 * as activities from 'src/activities/rules.js' import {CLIENT_SECTIONS} from '../../../src/fpd/oneClient.js'; +import {ACTIVITY_ACCESS_DEVICE} from '../../../src/activities/activities.js'; +import {ACTIVITY_PARAM_COMPONENT} from '../../../src/activities/params.js'; describe('FPD enrichment', () => { let sandbox; @@ -213,7 +216,7 @@ describe('FPD enrichment', () => { ua: 'ua' }) }) - }) + }); }); }); @@ -275,7 +278,13 @@ describe('FPD enrichment', () => { describe('sua', () => { it('does not set device.sua if resolved sua is null', () => { - sandbox.stub(dep, 'getHighEntropySUA').returns(Promise.resolve()) + sandbox.stub(dep, 'getHighEntropySUA').returns(Promise.resolve()); + // Add hints so it will attempt to retrieve high entropy values + config.setConfig({ + firstPartyData: { + uaHints: ['bitness'], + } + }); return fpd().then(ortb2 => { expect(ortb2.device.sua).to.not.exist; }) @@ -304,6 +313,71 @@ describe('FPD enrichment', () => { }); }); + describe('privacy sandbox cookieDeprecationLabel', () => { + let isAllowed, cdep, shouldCleanupNav = false; + + before(() => { + if (!navigator.cookieDeprecationLabel) { + navigator.cookieDeprecationLabel = {}; + shouldCleanupNav = true; + } + }); + + after(() => { + if (shouldCleanupNav) { + delete navigator.cookieDeprecationLabel; + } + }); + + beforeEach(() => { + isAllowed = true; + sandbox.stub(activities, 'isActivityAllowed').callsFake((activity, params) => { + if (activity === ACTIVITY_ACCESS_DEVICE && params[ACTIVITY_PARAM_COMPONENT] === 'prebid.cdep') { + return isAllowed; + } else { + throw new Error('Unexpected activity check'); + } + }); + sandbox.stub(window.navigator, 'cookieDeprecationLabel').value({ + getValue: sinon.stub().callsFake(() => cdep) + }) + }) + + it('enrichment sets device.ext.cdep when allowed and navigator.getCookieDeprecationLabel exists', () => { + cdep = Promise.resolve('example-test-label'); + return fpd().then(ortb2 => { + expect(ortb2.device.ext.cdep).to.eql('example-test-label'); + }) + }); + + Object.entries({ + 'not allowed'() { + isAllowed = false; + }, + 'not supported'() { + delete navigator.cookieDeprecationLabel + } + }).forEach(([t, setup]) => { + it(`if ${t}, the navigator API is not called and no enrichment happens`, () => { + setup(); + cdep = Promise.resolve('example-test-label'); + return fpd().then(ortb2 => { + expect(ortb2.device.ext).to.not.exist; + if (navigator.cookieDeprecationLabel) { + sinon.assert.notCalled(navigator.cookieDeprecationLabel.getValue); + } + }) + }); + }) + + it('if the navigator API returns a promise that rejects, the enrichment does not halt forever', () => { + cdep = Promise.reject(new Error('oops, something went wrong')); + return fpd().then(ortb2 => { + expect(ortb2.device.ext).to.not.exist; + }) + }); + }); + it('leaves only one of app, site, dooh', () => { return fpd({ app: {p: 'val'}, diff --git a/test/spec/fpd/sua_spec.js b/test/spec/fpd/sua_spec.js index 431f47268d3..63e0068d0ef 100644 --- a/test/spec/fpd/sua_spec.js +++ b/test/spec/fpd/sua_spec.js @@ -165,6 +165,14 @@ describe('uaDataToSUA', () => { }); describe('lowEntropySUAAccessor', () => { + // Set up a mock data with readonly property + class MockUserAgentData {} + Object.defineProperty(MockUserAgentData.prototype, 'mobile', { + value: false, + writable: false, + enumerable: true + }); + function getSUA(uaData) { return lowEntropySUAAccessor(uaData)(); } @@ -181,6 +189,10 @@ describe('lowEntropySUAAccessor', () => { it('should return null if uaData is empty', () => { expect(getSUA({})).to.eql(null); }) + + it('should return mobile and source', () => { + expect(getSUA(new MockUserAgentData())).to.eql({mobile: 0, source: 1}) + }) }); describe('highEntropySUAAccessor', () => { diff --git a/test/spec/libraries/cmp/cmpClient_spec.js b/test/spec/libraries/cmp/cmpClient_spec.js new file mode 100644 index 00000000000..adbbbf5cb1d --- /dev/null +++ b/test/spec/libraries/cmp/cmpClient_spec.js @@ -0,0 +1,296 @@ +import {cmpClient, MODE_CALLBACK, MODE_RETURN} from '../../../../libraries/cmp/cmpClient.js'; + +describe('cmpClient', () => { + function mockWindow(props = {}) { + let listeners = []; + const win = { + addEventListener: sinon.stub().callsFake((evt, listener) => { + evt === 'message' && listeners.push(listener) + }), + removeEventListener: sinon.stub().callsFake((evt, listener) => { + evt === 'message' && (listeners = listeners.filter((l) => l !== listener)); + }), + postMessage: sinon.stub().callsFake((msg) => { + listeners.forEach(ln => ln({data: msg})) + }), + ...props, + }; + win.top = win.parent?.top || win; + return win; + } + + it('should return undefined when there is no CMP', () => { + expect(cmpClient({apiName: 'missing'}, mockWindow())).to.not.exist; + }); + + it('should return undefined when parent is inaccessible', () => { + const win = mockWindow(); + win.top = mockWindow(); + expect(cmpClient({apiName: 'missing'}, win)).to.not.exist; + }) + + describe('direct access', () => { + let mockApiFn; + beforeEach(() => { + mockApiFn = sinon.stub(); + }) + Object.entries({ + 'on same frame': () => mockWindow({mockApiFn}), + 'on parent frame': () => mockWindow({parent: mockWindow({parent: mockWindow({parent: mockWindow(), mockApiFn})})}), + }).forEach(([t, mkWindow]) => { + describe(t, () => { + let win, mkClient; + beforeEach(() => { + win = mkWindow(); + mkClient = (opts) => cmpClient(Object.assign({apiName: 'mockApiFn'}, opts), win) + }); + + it('should mark client function as direct', () => { + expect(mkClient().isDirect).to.equal(true); + }); + + it('should find and call the CMP api function', () => { + mkClient()({command: 'mockCmd'}); + sinon.assert.calledWith(mockApiFn, 'mockCmd'); + }); + + describe('should return a promise that', () => { + let cbResult; + beforeEach(() => { + cbResult = []; + mockApiFn.callsFake((cmd, callback) => { + if (typeof callback === 'function') { + callback.apply(this, cbResult); + } + return 'val' + }) + }) + + Object.entries({ + callback: [sinon.stub(), 'undefined', undefined], + 'callback, mode = MODE_CALLBACK': [sinon.stub(), 'undefined', undefined, MODE_CALLBACK], + 'callback, mode = MODE_RETURN': [sinon.stub(), 'api return value', 'val', MODE_RETURN], + 'no callback': [undefined, 'api return value', 'val'], + 'no callback, mode = MODE_CALLBACK': [undefined, 'callback arg', 'cbVal', MODE_CALLBACK], + 'no callback, mode = MODE_RETURN': [undefined, 'api return value', 'val', MODE_RETURN], + }).forEach(([t, [callback, tResult, expectedResult, mode]]) => { + describe(`when ${t} is provided`, () => { + Object.entries({ + 'no success flag': undefined, + 'success is set': true + }).forEach(([t, success]) => { + it(`resolves to ${tResult} (${t})`, (done) => { + cbResult = ['cbVal', success]; + mkClient({mode})({callback}).then((val) => { + expect(val).to.equal(expectedResult); + done(); + }) + }); + + it('should pass either a function or undefined as callback', () => { + mkClient({mode})({callback}); + sinon.assert.calledWith(mockApiFn, sinon.match.any, sinon.match(arg => typeof arg === 'undefined' || typeof arg === 'function')) + }) + }); + }) + }); + + it('rejects to undefined when callback is provided and success = false', (done) => { + cbResult = ['cbVal', false]; + mkClient()({callback: sinon.stub()}).catch(val => { + expect(val).to.not.exist; + done(); + }) + }); + + it('rejects to callback arg when callback is NOT provided, success = false, mode = MODE_CALLBACK', (done) => { + cbResult = ['cbVal', false]; + mkClient({mode: MODE_CALLBACK})().catch(val => { + expect(val).to.eql('cbVal'); + done(); + }) + }) + + it('rejects when CMP api throws', (done) => { + mockApiFn.reset(); + const e = new Error(); + mockApiFn.throws(e); + mkClient()({}).catch(val => { + expect(val).to.equal(e); + done(); + }); + }); + }) + + it('should use apiArgs to choose and order the arguments to pass to the API fn', () => { + mkClient({apiArgs: ['parameter', 'command']})({ + command: 'mockCmd', + parameter: 'mockParam', + callback() {} + }); + sinon.assert.calledWith(mockApiFn, 'mockParam', 'mockCmd'); + }); + + it('should not choke on .close()', () => { + mkClient({}).close(); + }) + }) + }) + }) + + describe('postMessage access', () => { + let messenger, win, response; + beforeEach(() => { + response = {}; + messenger = sinon.stub().callsFake((msg) => { + if (msg.mockApiCall) { + win.postMessage({mockApiReturn: {callId: msg.mockApiCall.callId, ...response}}); + } + }); + }); + + function mkClient(options) { + return cmpClient(Object.assign({apiName: 'mockApi'}, options), win); + } + + Object.entries({ + 'on same frame': () => { + win = mockWindow({frames: {mockApiLocator: true}}); + win.addEventListener('message', (evt) => messenger(evt.data)); + }, + 'on parent frame': () => { + win = mockWindow({parent: mockWindow({frames: {mockApiLocator: true}})}) + win.parent.addEventListener('message', evt => messenger(evt.data)) + } + }).forEach(([t, setup]) => { + describe(t, () => { + beforeEach(setup); + + it('should mark client as not direct', () => { + expect(mkClient().isDirect).to.equal(false); + }); + + it('should find and message the CMP frame', () => { + mkClient()({command: 'mockCmd', parameter: 'param'}); + sinon.assert.calledWithMatch(messenger, { + mockApiCall: { + command: 'mockCmd', + parameter: 'param' + } + }) + }); + + it('should use apiArgs to choose what to include in the message payload', () => { + mkClient({apiArgs: ['command']})({ + command: 'cmd', + parameter: 'param' + }); + sinon.assert.calledWithMatch(messenger, sinon.match((arg) => { + return arg.mockApiCall.command === 'cmd' && + !arg.mockApiCall.hasOwnProperty('parameter'); + })) + }); + + it('should not include callback in the payload, but still run it on response', () => { + const cb = sinon.stub(); + mkClient({apiArgs: ['command', 'callback']})({ + command: 'cmd', + callback: cb + }); + sinon.assert.calledWithMatch(messenger, sinon.match(arg => !arg.mockApiCall.hasOwnProperty('callback'))); + sinon.assert.called(cb); + }); + + it('should use callbackArgs to decide what to pass to callback', () => { + const cb = sinon.stub(); + response = {a: 'one', b: 'two'}; + mkClient({callbackArgs: ['a', 'b']})({callback: cb}); + sinon.assert.calledWith(cb, 'one', 'two'); + }) + + describe('should return a promise that', () => { + beforeEach(() => { + response = {returnValue: 'val'} + }) + Object.entries({ + 'callback': [sinon.stub(), 'undefined', undefined], + 'callback, mode = MODE_RETURN': [sinon.stub(), 'undefined', undefined, MODE_RETURN], + 'callback, mode = MODE_CALLBACK': [sinon.stub(), 'undefined', undefined, MODE_CALLBACK], + 'no callback': [undefined, 'response returnValue', 'val'], + 'no callback, mode = MODE_RETURN': [undefined, 'undefined', undefined, MODE_RETURN], + 'no callback, mode = MODE_CALLBACK': [undefined, 'response returnValue', 'val', MODE_CALLBACK], + }).forEach(([t, [callback, tResult, expectedResult, mode]]) => { + describe(`when ${t} is provided`, () => { + Object.entries({ + 'no success flag': {}, + 'with success flag': {success: true} + }).forEach(([t, resp]) => { + it(`resolves to ${tResult} (${t})`, () => { + Object.assign(response, resp); + mkClient({mode})({callback}).then((val) => { + expect(val).to.equal(expectedResult); + }) + }) + }); + + if (mode !== MODE_RETURN) { // in return mode, the promise never rejects + it(`rejects to ${tResult} when success = false`, (done) => { + response.success = false; + mkClient()({mode, callback}).catch((err) => { + expect(err).to.equal(expectedResult); + done(); + }); + }); + } + }) + }); + }); + + describe('messages with same callID', () => { + let callback, callId; + + function runCallback(returnValue) { + win.postMessage({mockApiReturn: {callId, returnValue}}); + } + + beforeEach(() => { + callId = null; + messenger.reset(); + messenger.callsFake((msg) => { + if (msg.mockApiCall) callId = msg.mockApiCall.callId; + }); + callback = sinon.stub(); + }); + + it('should re-use callback for messages with same callId', () => { + mkClient()({callback}); + expect(callId).to.exist; + runCallback('a'); + runCallback('b'); + sinon.assert.calledWith(callback, 'a'); + sinon.assert.calledWith(callback, 'b'); + }); + + it('should NOT re-use callback if once = true', () => { + mkClient()({callback}, true); + expect(callId).to.exist; + runCallback('a'); + runCallback('b'); + sinon.assert.calledWith(callback, 'a'); + sinon.assert.calledOnce(callback); + }); + + it('should NOT fire again after .close()', () => { + const client = mkClient(); + client({callback}); + runCallback('a'); + client.close(); + runCallback('b'); + sinon.assert.calledWith(callback, 'a'); + sinon.assert.calledOnce(callback); + }) + }); + }); + }); + }); +}); diff --git a/test/spec/libraries/currencyUtils_spec.js b/test/spec/libraries/currencyUtils_spec.js new file mode 100644 index 00000000000..9d3d73e6a5f --- /dev/null +++ b/test/spec/libraries/currencyUtils_spec.js @@ -0,0 +1,113 @@ +import {getGlobal} from 'src/prebidGlobal.js'; +import {convertCurrency, currencyCompare, currencyNormalizer} from 'libraries/currencyUtils/currency.js'; + +describe('currency utils', () => { + let sandbox; + before(() => { + if (!getGlobal().convertCurrency) { + getGlobal().convertCurrency = () => null; + getGlobal().convertCurrency.mock = true; + } + }); + + after(() => { + if (getGlobal().convertCurrency.mock) { + delete getGlobal().convertCurrency; + } + }) + + beforeEach(() => { + sandbox = sinon.sandbox.create(); + }); + + afterEach(() => { + sandbox.restore(); + }); + + describe('convertCurrency', () => { + Object.entries({ + 'not available': () => sandbox.stub(getGlobal(), 'convertCurrency').value(undefined), + 'throwing errors': () => sandbox.stub(getGlobal(), 'convertCurrency').callsFake(() => { throw new Error(); }), + }).forEach(([t, setup]) => { + describe(`when currency module is ${t}`, () => { + beforeEach(setup); + + it('should "convert" to the same currency', () => { + expect(convertCurrency(123, 'mock', 'mock', false)).to.eql(123); + }); + + it('should throw when suppressErrors = false', () => { + expect(() => convertCurrency(123, 'c1', 'c2', false)).to.throw(); + }); + + it('should return input value when suppressErrors = true', () => { + expect(convertCurrency(123, 'c1', 'c2', true)).to.eql(123); + }) + }) + }); + + describe('when currency module is working', () => { + beforeEach(() => { + sandbox.stub(getGlobal(), 'convertCurrency').callsFake((amt) => amt * 10) + }); + + it('should be used for actual conversions', () => { + expect(convertCurrency(123, 'c1', 'c2')).to.eql(1230); + sinon.assert.calledWith(getGlobal().convertCurrency, 123, 'c1', 'c2'); + }); + + it('should NOT be used when no conversion is necessary', () => { + expect(convertCurrency(123, 'cur', 'cur')).to.eql(123); + sinon.assert.notCalled(getGlobal().convertCurrency); + }) + }) + }); + + describe('Currency normalization', () => { + let mockConvert; + beforeEach(() => { + mockConvert = sinon.stub().callsFake((amt, from, to) => { + if (from === to) return amt; + return amt / from * to + }) + }); + + describe('currencyNormalizer', () => { + it('converts to toCurrency if set', () => { + const normalize = currencyNormalizer(10, true, mockConvert); + expect(normalize(1, 1)).to.eql(10); + expect(normalize(10, 100)).to.eql(1); + }); + + it('converts to first currency if toCurrency is not set', () => { + const normalize = currencyNormalizer(null, true, mockConvert); + expect(normalize(1, 1)).to.eql(1); + expect(normalize(1, 10)).to.eql(0.1); + }); + + [true, false].forEach(bestEffort => { + it(`passes bestEffort = ${bestEffort} to convert`, () => { + currencyNormalizer(null, bestEffort, mockConvert)(1, 1); + sinon.assert.calledWith(mockConvert, 1, 1, 1, bestEffort); + }) + }) + }); + + describe('currencyCompare', () => { + let compare + beforeEach(() => { + compare = currencyCompare((val) => [val.amount, val.cur], currencyNormalizer(null, false, mockConvert)) + }); + [ + [{amount: 1, cur: 1}, {amount: 1, cur: 10}, 1], + [{amount: 10, cur: 1}, {amount: 0.1, cur: 100}, 1], + [{amount: 1, cur: 1}, {amount: 10, cur: 10}, 0], + ].forEach(([a, b, expected]) => { + it(`should compare ${a.amount}/${a.cur} and ${b.amount}/${b.cur}`, () => { + expect(compare(a, b)).to.equal(expected); + expect(compare(b, a)).to.equal(-expected); + }); + }); + }) + }) +}) diff --git a/test/spec/libraries/mspa/activityControls_spec.js b/test/spec/libraries/mspa/activityControls_spec.js new file mode 100644 index 00000000000..f232dc2563f --- /dev/null +++ b/test/spec/libraries/mspa/activityControls_spec.js @@ -0,0 +1,256 @@ +import {mspaRule, setupRules, isTransmitUfpdConsentDenied, isTransmitGeoConsentDenied, isBasicConsentDenied, sensitiveNoticeIs, isConsentDenied} from '../../../../libraries/mspa/activityControls.js'; +import {ruleRegistry} from '../../../../src/activities/rules.js'; + +describe('Consent interpretation', () => { + function mkConsent(flags) { + return Object.assign({ + // not covered, opt in to targeted, sale, and share, all notices given, opt into precise geo + Gpc: 0, + KnownChildSensitiveDataConsents: [0, 0], + MspaCoveredTransaction: 2, + MspaOptOutOptionMode: 0, + MspaServiceProviderMode: 0, + PersonalDataConsents: 0, + SaleOptOut: 2, + SaleOptOutNotice: 1, + SensitiveDataLimitUseNotice: 1, + SensitiveDataProcessing: [0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0], + SensitiveDataProcessingOptOutNotice: 1, + SharingNotice: 1, + SharingOptOut: 2, + SharingOptOutNotice: 1, + TargetedAdvertisingOptOut: 2, + TargetedAdvertisingOptOutNotice: 1, + Version: 1 + }, flags) + } + describe('isBasicConsentDenied', () => { + it('should be false (basic consent conditions pass) with variety of notice and opt in', () => { + const result = isBasicConsentDenied(mkConsent()); + expect(result).to.equal(false); + }); + it('should be true (basic consent conditions do not pass) with personal data consent set to true (invalid state)', () => { + const result = isBasicConsentDenied(mkConsent({ + PersonalDataConsents: 2 + })); + expect(result).to.equal(true); + }); + it('should be true (basic consent conditions do not pass) with covered set to zero (invalid state)', () => { + const result = isBasicConsentDenied(mkConsent({ + MspaCoveredTransaction: 0 + })); + expect(result).to.equal(true); + }); + it('should not deny when consent for under-13 is null', () => { + expect(isBasicConsentDenied(mkConsent({ + KnownChildSensitiveDataConsents: [0, null] + }))).to.be.false; + }) + }); + + describe('isConsentDenied', () => { + it('should be false (consent given personalized ads / sale / share) with variety of notice and opt in', () => { + const result = isConsentDenied(mkConsent()); + expect(result).to.equal(false); + }); + it('should be true (no consent) on opt out of targeted ads via TargetedAdvertisingOptOut', () => { + const result = isConsentDenied(mkConsent({ + TargetedAdvertisingOptOut: 1 + })); + expect(result).to.equal(true); + }); + it('should be true (no consent) on opt out of targeted ads via no TargetedAdvertisingOptOutNotice', () => { + const result = isConsentDenied(mkConsent({ + TargetedAdvertisingOptOutNotice: 2 + })); + expect(result).to.equal(true); + }); + it('should be true (no consent) if TargetedAdvertisingOptOutNotice is 0 and TargetedAdvertisingOptOut is 2', () => { + const result = isConsentDenied(mkConsent({ + TargetedAdvertisingOptOutNotice: 0 + })); + expect(result).to.equal(true); + }); + it('requires also SharingNotice to accept opt-in for Sharing', () => { + expect(isConsentDenied(mkConsent({ + SharingNotice: 0 + }))).to.be.true; + }) + }); + + describe('isTransmitUfpdConsentDenied', () => { + it('should be false (consent given to add ufpd) with variety of notice and opt in', () => { + const result = isTransmitUfpdConsentDenied(mkConsent()); + expect(result).to.equal(false); + }); + Object.entries({ + 'health information': 2, + 'biometric data': 6, + }).forEach(([t, flagNo]) => { + it(`'should be true (consent denied to add ufpd) if no consent to process ${t}'`, () => { + const consent = mkConsent(); + consent.SensitiveDataProcessing[flagNo] = 1; + expect(isTransmitUfpdConsentDenied(consent)).to.be.true; + }) + }); + + ['SharingNotice', 'SensitiveDataLimitUseNotice'].forEach(flag => { + it(`should be true (consent denied to add ufpd) without ${flag}`, () => { + expect(isTransmitUfpdConsentDenied(mkConsent({ + [flag]: 2 + }))).to.be.true; + }) + }); + + ['SaleOptOut', 'TargetedAdvertisingOptOut'].forEach(flag => { + it(`should be true (consent denied to add ufpd) with ${flag}`, () => { + expect(isTransmitUfpdConsentDenied(mkConsent({ + [flag]: 1 + }))).to.be.true; + }) + }); + + it('should be true (basic consent conditions do not pass) with sensitive opt in but no notice', () => { + const cd = mkConsent({ + SensitiveDataLimitUseNotice: 0 + }); + cd.SensitiveDataProcessing[0] = 2; + expect(isTransmitUfpdConsentDenied(cd)).to.be.true; + }); + + it('should deny when sensitive notice is missing', () => { + const result = isTransmitUfpdConsentDenied(mkConsent({ + SensitiveDataLimitUseNotice: 2 + })); + expect(result).to.equal(true); + }); + + it('should not deny when biometric data opt-out is null', () => { + const cd = mkConsent(); + cd.SensitiveDataProcessing[6] = null; + expect(isTransmitUfpdConsentDenied(cd)).to.be.false; + }) + }); + + describe('isTransmitGeoConsentDenied', () => { + function geoConsent(geoOptOut, flags) { + const consent = mkConsent(flags); + consent.SensitiveDataProcessing[7] = geoOptOut; + return consent; + } + it('should be true (consent denied to add precise geo) -- sensitive flag denied', () => { + const result = isTransmitGeoConsentDenied(geoConsent(1)); + expect(result).to.equal(true); + }); + it('should be true (consent denied to add precise geo) -- sensitive data limit usage not given', () => { + const result = isTransmitGeoConsentDenied(geoConsent(1, { + SensitiveDataLimitUseNotice: 0 + })); + expect(result).to.equal(true); + }); + it('should be false (consent given to add precise geo) -- sensitive position 8 (index 7) is true', () => { + const result = isTransmitGeoConsentDenied(geoConsent(2)); + expect(result).to.equal(false); + }); + }) +}); + +describe('mspaRule', () => { + it('does not apply if SID is not applicable', () => { + const rule = mspaRule([1, 2], () => null, () => true, () => [3, 4]); + expect(rule()).to.not.exist; + }); + + it('does not apply when no SID is applicable', () => { + const rule = mspaRule([1], () => null, () => true, () => []); + expect(rule()).to.not.exist; + }); + + describe('when SID is applicable', () => { + let consent, denies; + function mkRule() { + return mspaRule([1, 2], () => consent, denies, () => [2]) + } + + beforeEach(() => { + consent = null; + denies = sinon.stub(); + }); + + it('should deny when no consent is available', () => { + expect(mkRule()().allow).to.equal(false); + }); + + Object.entries({ + 'denies': true, + 'allows': false + }).forEach(([t, denied]) => { + it(`should check if deny fn ${t}`, () => { + denies.returns(denied); + consent = {mock: 'value'}; + const result = mkRule()(); + sinon.assert.calledWith(denies, consent); + if (denied) { + expect(result.allow).to.equal(false); + } else { + expect(result).to.not.exist; + } + }) + }) + }) +}); + +describe('setupRules', () => { + let rules, registerRule, isAllowed, consent; + beforeEach(() => { + rules = { + mockActivity: sinon.stub().returns(true) + }; + ([registerRule, isAllowed] = ruleRegistry()); + consent = { + applicableSections: [1], + parsedSections: { + mockApi: [ + { + mock: 'consent' + } + ] + } + }; + }); + + function runSetup(api, sids, normalize) { + return setupRules(api, sids, normalize, rules, registerRule, () => consent) + } + + it('should use flatten section data for the given api', () => { + runSetup('mockApi', [1]); + expect(isAllowed('mockActivity', {})).to.equal(false); + sinon.assert.calledWith(rules.mockActivity, {mock: 'consent'}) + }); + + it('should not choke when no consent data is available', () => { + consent = null; + runSetup('mockApi', [1]); + expect(isAllowed('mockActivity', {})).to.equal(true); + }); + + it('should check applicableSections against given SIDs', () => { + runSetup('mockApi', [2]); + expect(isAllowed('mockActivity', {})).to.equal(true); + }); + + it('should pass flattened consent through normalizeConsent', () => { + const normalize = sinon.stub().returns({normalized: 'consent'}) + runSetup('mockApi', [1], normalize); + expect(isAllowed('mockActivity', {})).to.equal(false); + sinon.assert.calledWith(normalize, {mock: 'consent'}); + sinon.assert.calledWith(rules.mockActivity, {normalized: 'consent'}); + }); + + it('should return a function that unregisters activity controls', () => { + const dereg = runSetup('mockApi', [1]); + dereg(); + expect(isAllowed('mockActivity', {})).to.equal(true); + }); +}) diff --git a/test/spec/libraries/sizeUtils_spec.js b/test/spec/libraries/sizeUtils_spec.js new file mode 100644 index 00000000000..1c954c6accf --- /dev/null +++ b/test/spec/libraries/sizeUtils_spec.js @@ -0,0 +1,30 @@ +import {getAdUnitSizes} from '../../../libraries/sizeUtils/sizeUtils.js'; +import {expect} from 'chai/index.js'; + +describe('getAdUnitSizes', function () { + it('returns an empty response when adUnits is undefined', function () { + let sizes = getAdUnitSizes(); + expect(sizes).to.be.undefined; + }); + + it('returns an empty array when invalid data is present in adUnit object', function () { + let sizes = getAdUnitSizes({sizes: 300}); + expect(sizes).to.deep.equal([]); + }); + + it('retuns an array of arrays when reading from adUnit.sizes', function () { + let sizes = getAdUnitSizes({sizes: [300, 250]}); + expect(sizes).to.deep.equal([[300, 250]]); + + sizes = getAdUnitSizes({sizes: [[300, 250], [300, 600]]}); + expect(sizes).to.deep.equal([[300, 250], [300, 600]]); + }); + + it('returns an array of arrays when reading from adUnit.mediaTypes.banner.sizes', function () { + let sizes = getAdUnitSizes({mediaTypes: {banner: {sizes: [300, 250]}}}); + expect(sizes).to.deep.equal([[300, 250]]); + + sizes = getAdUnitSizes({mediaTypes: {banner: {sizes: [[300, 250], [300, 600]]}}}); + expect(sizes).to.deep.equal([[300, 250], [300, 600]]); + }); +}); diff --git a/test/spec/libraries/urlUtils_spec.js b/test/spec/libraries/urlUtils_spec.js new file mode 100644 index 00000000000..9dd66b05407 --- /dev/null +++ b/test/spec/libraries/urlUtils_spec.js @@ -0,0 +1,24 @@ +import {tryAppendQueryString} from '../../../libraries/urlUtils/urlUtils.js'; +import assert from 'assert'; + +describe('tryAppendQueryString', function () { + it('should append query string to existing url', function () { + var url = 'www.a.com?'; + var key = 'b'; + var value = 'c'; + + var output = tryAppendQueryString(url, key, value); + + var expectedResult = url + key + '=' + encodeURIComponent(value) + '&'; + assert.equal(output, expectedResult); + }); + + it('should return existing url, if the value is empty', function () { + var url = 'www.a.com?'; + var key = 'b'; + var value = ''; + + var output = tryAppendQueryString(url, key, value); + assert.equal(output, url); + }); +}); diff --git a/test/spec/modules/33acrossAnalyticsAdapter_spec.js b/test/spec/modules/33acrossAnalyticsAdapter_spec.js new file mode 100644 index 00000000000..9e0d928cd97 --- /dev/null +++ b/test/spec/modules/33acrossAnalyticsAdapter_spec.js @@ -0,0 +1,1163 @@ +// @ts-nocheck +import analyticsAdapter from 'modules/33acrossAnalyticsAdapter.js'; +import { log } from 'modules/33acrossAnalyticsAdapter.js'; +import * as mockGpt from 'test/spec/integration/faker/googletag.js'; +import * as events from 'src/events.js'; +import * as faker from 'faker'; +import CONSTANTS from 'src/constants.json'; +import { gdprDataHandler, gppDataHandler, uspDataHandler } from '../../../src/adapterManager'; +import { DEFAULT_ENDPOINT, POST_GAM_TIMEOUT, locals } from '../../../modules/33acrossAnalyticsAdapter'; +const { EVENTS, BID_STATUS } = CONSTANTS; + +describe('33acrossAnalyticsAdapter:', function () { + let sandbox; + let assert = getLocalAssert(); + + beforeEach(function () { + mockGpt.reset(); + + sandbox = sinon.createSandbox({ + useFakeTimers: { + now: new Date(2023, 3, 3, 0, 1, 33, 425), + }, + }); + + sandbox.stub(events, 'getEvents').returns([]); + + sandbox.spy(log, 'info'); + sandbox.spy(log, 'warn'); + sandbox.spy(log, 'error'); + + sandbox.stub(navigator, 'sendBeacon').callsFake(function (url, data) { + const json = JSON.parse(data); + assert.isValidAnalyticsReport(json); + + return true; + }); + }); + + afterEach(function () { + analyticsAdapter.disableAnalytics(); + mockGpt.enable(); + sandbox.restore(); + }); + + describe('enableAnalytics:', function () { + context('When pid is given', function () { + context('but endpoint is not', function () { + it('uses the default endpoint', function () { + analyticsAdapter.enableAnalytics({ + options: { + pid: 'test-pid', + }, + }); + + assert.equal(analyticsAdapter.getUrl(), DEFAULT_ENDPOINT); + }); + }); + + context('but the endpoint is invalid', function () { + it('logs an info message', function () { + analyticsAdapter.enableAnalytics({ + options: { + pid: 'test-pid', + endpoint: 'foo' + }, + }); + + assert.calledWithExactly(log.info, 'Invalid endpoint provided for "options.endpoint". Using default endpoint.'); + }); + }); + }); + + context('When endpoint is given', function () { + context('but pid is not', function () { + it('logs an error message', function () { + analyticsAdapter.enableAnalytics({ + options: { + endpoint: faker.internet.url() + }, + }); + + assert.calledWithExactly(log.error, 'No partnerId provided for "options.pid". No analytics will be sent.'); + }); + }); + }); + + context('When pid and endpoint are given', function () { + context('and an invalid timeout config value is given', function () { + it('logs an info message', function () { + [null, 'foo', -1].forEach(timeout => { + analyticsAdapter.enableAnalytics({ + options: { + pid: 'test-pid', + endpoint: 'http://test-endpoint', + timeout + }, + }); + analyticsAdapter.disableAnalytics(); + + assert.calledWithExactly(log.info, 'Invalid timeout provided for "options.timeout". Using default timeout of 10000ms.'); + log.info.resetHistory(); + }); + }); + }); + }); + }); + + // check that upcoming tests are derived from a valid report + describe('Report Mocks', function () { + it('the report should have the correct format', function () { + assert.isValidAnalyticsReport(createReportWithThreeBidWonEvents()); + }); + }); + + describe('Event Handling', function () { + beforeEach(function () { + this.defaultTimeout = 10000; + this.enableAnalytics = (options) => { + analyticsAdapter.enableAnalytics({ + options: { + endpoint: 'http://test-endpoint', + pid: 'test-pid', + timeout: this.defaultTimeout, + ...options + }, + }); + window.googletag.cmd.forEach(cmd => cmd()); + } + }); + + context('when an auction is complete', function () { + context('and the AnalyticsReport is sent successfully to the given endpoint', function () { + it('calls "sendBeacon" with all won bids', function () { + const endpoint = faker.internet.url(); + this.enableAnalytics({ endpoint }); + + navigator.sendBeacon + .withArgs(endpoint, JSON.stringify(createReportWithThreeBidWonEvents())); + + performStandardAuction(); + sandbox.clock.tick(this.defaultTimeout + 1000); + + const [url, jsonString] = navigator.sendBeacon.firstCall.args; + const { auctions } = JSON.parse(jsonString); + + assert.lengthOf(mapToBids(auctions).filter(bid => bid.hasWon), 3); + }); + + it('calls "sendBeacon" with the correct report string', function () { + const endpoint = faker.internet.url(); + this.enableAnalytics({ endpoint }); + + navigator.sendBeacon + .withArgs(endpoint, JSON.stringify(createReportWithThreeBidWonEvents())); + + performStandardAuction(); + sandbox.clock.tick(this.defaultTimeout + 1000); + + assert.calledOnceWithStringJsonEquivalent(navigator.sendBeacon, endpoint, createReportWithThreeBidWonEvents()); + }); + + it('logs an info message containing the report', function () { + const endpoint = faker.internet.url(); + this.enableAnalytics({ endpoint }); + + navigator.sendBeacon + .withArgs(endpoint, JSON.stringify(createReportWithThreeBidWonEvents())) + .returns(true); + + performStandardAuction(); + sandbox.clock.tick(this.defaultTimeout + 1000); + + assert.calledWithExactly(log.info, `Analytics report sent to ${endpoint}`, createReportWithThreeBidWonEvents()); + }); + + it('calls "sendBeacon" as soon as all values are available (before timeout)', function () { + const endpoint = faker.internet.url(); + this.enableAnalytics({ endpoint }); + + navigator.sendBeacon + .withArgs(endpoint, JSON.stringify(createReportWithThreeBidWonEvents())); + + performStandardAuction(); + sandbox.clock.tick(1); + + assert.calledOnceWithStringJsonEquivalent(navigator.sendBeacon, endpoint, createReportWithThreeBidWonEvents()); + }); + }); + + context('and a valid US Privacy configuration is present', function () { + ['1YNY', '1---', '1NY-', '1Y--', '1--Y', '1N--', '1--N', '1NNN'].forEach(consent => { + it(`calls "sendBeacon" with a report containing the "${consent}" privacy string`, function () { + sandbox.stub(uspDataHandler, 'getConsentData').returns(consent); + this.enableAnalytics(); + + const reportWithConsent = { + ...createReportWithThreeBidWonEvents(), + usPrivacy: consent + }; + navigator.sendBeacon + .withArgs('http://test-endpoint', reportWithConsent); + + performStandardAuction(); + sandbox.clock.tick(this.defaultTimeout + 1000); + + assert.calledOnceWithStringJsonEquivalent(navigator.sendBeacon, 'http://test-endpoint', reportWithConsent); + }); + }); + }); + + context('and a GDPR Privacy configuration is present', function () { + it('it calls "sendBeacon" with a report containing the GDPR consent string', function () { + sandbox.stub(gdprDataHandler, 'getConsentData').returns({ + consentString: 'foo', + gdprApplies: true + }); + this.enableAnalytics(); + + const reportWithConsent = { + ...createReportWithThreeBidWonEvents(), + gdpr: 1, + gdprConsent: 'foo' + }; + navigator.sendBeacon + .withArgs('http://test-endpoint', reportWithConsent); + + performStandardAuction(); + sandbox.clock.tick(this.defaultTimeout + 1); + + assert.calledOnceWithStringJsonEquivalent(navigator.sendBeacon, 'http://test-endpoint', reportWithConsent); + }); + }); + }); + + context('when an auction is complete and a GPP configuration is present', function () { + it('it calls "sendBeacon" with a report containing the GPP consent string', function () { + sandbox.stub(gppDataHandler, 'getConsentData').returns({ + gppString: 'gppString', + applicableSections: [7] + }); + this.enableAnalytics(); + + const reportWithConsent = { + ...createReportWithThreeBidWonEvents(), + gpp: 'gppString', + gppSid: [7] + }; + navigator.sendBeacon + .withArgs('http://test-endpoint', reportWithConsent); + + performStandardAuction(); + sandbox.clock.tick(this.defaultTimeout + 1); + + assert.calledOnceWithStringJsonEquivalent(navigator.sendBeacon, 'http://test-endpoint', reportWithConsent); + }); + }); + + context('when an error occurs while sending the AnalyticsReport', function () { + it('logs an error', function () { + this.enableAnalytics(); + navigator.sendBeacon.returns(false); + + performStandardAuction(); + sandbox.clock.tick(this.defaultTimeout + 1000); + + assert.calledWithExactly(log.error, 'Analytics report exceeded User-Agent data limits and was not sent.', createReportWithThreeBidWonEvents()); + }); + }); + + context('when an auction report was already sent', function () { + context('and a new bid won event is returned after the report completes', function () { + it('finishes the auction without error', function () { + const incompleteAnalyticsReport = createReportWithThreeBidWonEvents(); + incompleteAnalyticsReport.auctions.forEach(auction => { + auction.adUnits.forEach(adUnit => { + adUnit.bids.forEach(bid => { + delete bid.bidResponse; + bid.hasWon = 0; + bid.status = 'pending'; + }); + }); + }); + + this.enableAnalytics(); + const { prebid: [auction] } = getMockEvents(); + + events.emit(EVENTS.AUCTION_INIT, auction.AUCTION_INIT); + for (let bidRequestedEvent of auction.BID_REQUESTED) { + events.emit(EVENTS.BID_REQUESTED, bidRequestedEvent); + }; + + sandbox.clock.tick(this.defaultTimeout + 1000); + + for (let bidResponseEvent of auction.BID_RESPONSE) { + events.emit(EVENTS.BID_RESPONSE, bidResponseEvent); + }; + for (let bidWonEvent of auction.BID_WON) { + events.emit(EVENTS.BID_WON, bidWonEvent); + }; + + events.emit(EVENTS.AUCTION_END, auction.AUCTION_END); + + sandbox.clock.tick(1); + + assert.calledOnceWithStringJsonEquivalent(navigator.sendBeacon, 'http://test-endpoint', incompleteAnalyticsReport); + }); + }); + + context('and another auction completes after that', function () { + it('sends the new report', function () { + const endpoint = faker.internet.url(); + this.enableAnalytics({ endpoint }); + + navigator.sendBeacon + .withArgs(endpoint, JSON.stringify(createReportWithThreeBidWonEvents())); + + performStandardAuction(); + sandbox.clock.tick(this.defaultTimeout + 1000); + + performStandardAuction(); + sandbox.clock.tick(this.defaultTimeout + 1000); + + assert.calledTwice(navigator.sendBeacon); + }); + }); + }); + + context('when two auctions overlap', function() { + it('sends a report for each auction', function () { + const endpoint = faker.internet.url(); + this.enableAnalytics({ endpoint }); + + navigator.sendBeacon + .withArgs(endpoint, JSON.stringify(createReportWithThreeBidWonEvents())); + + performStandardAuction(); + performStandardAuction(); + sandbox.clock.tick(this.defaultTimeout + 1000); + + assert.calledTwice(navigator.sendBeacon); + }); + }); + + context('when an AUCTION_END event is received before BID_WON events', function () { + it('sends a report with the bids that have won after all bids are won', function () { + const endpoint = faker.internet.url(); + this.enableAnalytics({ endpoint }); + + navigator.sendBeacon + .withArgs(endpoint, JSON.stringify(createReportWithThreeBidWonEvents())); + + const { prebid: [auction] } = getMockEvents(); + + performStandardAuction({ exclude: [EVENTS.BID_WON] }); + + assert.notCalled(navigator.sendBeacon); + for (let bidWon of auction.BID_WON) { + events.emit(EVENTS.BID_WON, bidWon); + } + assert.calledOnceWithStringJsonEquivalent(navigator.sendBeacon, endpoint, createReportWithThreeBidWonEvents()); + }); + }); + + context('when a BID_WON event is received', function () { + context('and there is no record of that bid being requested', function () { + it('logs a warning message', function () { + this.enableAnalytics(); + + const mockEvents = getMockEvents(); + const { prebid } = mockEvents; + const [auction] = prebid; + + events.emit(EVENTS.AUCTION_INIT, auction.AUCTION_INIT); + + const fakeBidWonEvent = Object.assign(auction.BID_WON[0], { + transactionId: 'foo' + }) + + events.emit(EVENTS.BID_WON, fakeBidWonEvent); + + const { auctionId, requestId } = fakeBidWonEvent; + assert.calledWithExactly(log.error, `Cannot find bid "${requestId}" in auction "${auctionId}".`); + }); + }); + }); + + context('when a BID_REJECTED event is received', function () { + it(`marks the rejected bid as "rejected"`, function () { + this.enableAnalytics(); + + const auction = getMockEvents().prebid[0]; + + // Start the auction + events.emit(EVENTS.AUCTION_INIT, auction.AUCTION_INIT); + for (let bidRequestedEvent of auction.BID_REQUESTED) { + events.emit(EVENTS.BID_REQUESTED, bidRequestedEvent); + }; + + // Reject first bid + const bidToReject = auction.BID_REQUESTED[0].bids[0]; + events.emit(EVENTS.BID_REJECTED, auction.BID_REJECTED[0]); + + // Accept remaining bids + for (let i = 1; i < auction.BID_RESPONSE.length; ++i) { + events.emit(EVENTS.BID_RESPONSE, auction.BID_RESPONSE[i]); + }; + + // Complete the auction + events.emit(EVENTS.AUCTION_END, auction.AUCTION_END); + + sandbox.clock.tick(this.defaultTimeout + 1); + + // Verify that we detected that the first bid was rejected + const expectedRejectedBid = JSON.parse(navigator.sendBeacon.firstCall.args[1]).auctions[0].adUnits[0].bids[0]; + assert.strictEqual(expectedRejectedBid.status, 'rejected'); + }); + }); + + context('when a transaction does not reach its complete state', function () { + context('and a timeout config value has been given', function () { + context('and the timeout value has elapsed', function () { + it('logs a warning', function () { + const timeout = 2000; + this.enableAnalytics({ timeout }); + + performStandardAuction({exclude: ['bidWon', 'slotRenderEnded', 'auctionEnd']}); + + sandbox.clock.tick(timeout + 1000); + + assert.calledWithExactly(log.warn, 'Timed out waiting for ad transactions to complete. Sending report.'); + }); + + it(`marks timed out bids as "timeout"`, function () { + const timeout = 2000; + this.enableAnalytics({ timeout }); + const request = getMockEvents().prebid[0].BID_REQUESTED[0]; + const bidToTimeout = request.bids[0]; + + performStandardAuction({exclude: ['bidWon', 'slotRenderEnded', 'auctionEnd']}); + sandbox.clock.tick(1); + events.emit(EVENTS.BID_TIMEOUT, [{ + auctionId: request.auctionId, + bidId: bidToTimeout.bidId, + transactionId: bidToTimeout.transactionId, + }]); + sandbox.clock.tick(timeout + 1000); + + const timeoutBid = JSON.parse(navigator.sendBeacon.firstCall.args[1]).auctions[0].adUnits[0].bids[0]; + assert.strictEqual(timeoutBid.status, 'timeout'); + }); + }); + }); + + context('and a timeout config value has not been given', function () { + context('and the default timeout has elapsed', function () { + it('logs an error', function () { + this.enableAnalytics(); + + performStandardAuction({exclude: ['bidWon', 'slotRenderEnded', 'auctionEnd']}); + + sandbox.clock.tick(this.defaultTimeout + 1000); + + assert.calledWithExactly(log.warn, 'Timed out waiting for ad transactions to complete. Sending report.'); + }); + }) + }); + + context('and the `slotRenderEnded` event fired for all bids, but not all bids have won', function () { + context('and the GAM slot IDs are configured as the ad unit codes', function () { + it('sends a report after the all `slotRenderEnded` events have fired and timed out', function () { + const timeout = POST_GAM_TIMEOUT + 2000; + this.enableAnalytics({ timeout }); + + performStandardAuction({exclude: ['bidWon', 'auctionEnd']}); + sandbox.clock.tick(POST_GAM_TIMEOUT + 1); + + assert.strictEqual(navigator.sendBeacon.callCount, 1); + }); + }); + + context('and the slot element IDs are configured as the ad unit codes', function () { + it('sends a report after the all `slotRenderEnded` events have fired and timed out', function () { + const timeout = POST_GAM_TIMEOUT + 2000; + this.enableAnalytics({ timeout }); + + performStandardAuction({exclude: ['bidWon', 'auctionEnd'], useSlotElementIds: true}); + sandbox.clock.tick(POST_GAM_TIMEOUT + 1); + + assert.strictEqual(navigator.sendBeacon.callCount, 1); + }); + }); + + it('does NOT send a report if not all `slotRenderEnded` events have timed out', function () { + const timeout = POST_GAM_TIMEOUT + 2000; + this.enableAnalytics({ timeout }); + + performStandardAuction({exclude: ['bidWon', 'auctionEnd']}); + sandbox.clock.tick(POST_GAM_TIMEOUT - 1); + + assert.strictEqual(navigator.sendBeacon.callCount, 0); + }); + }); + + context('and the `slotRenderEnded` event has fired for an unknown slot code', function () { + it('logs a warning message', function () { + this.enableAnalytics(); + + const { prebid: [auction], gam } = getMockEvents(); + auction.AUCTION_INIT.adUnits[0].code = 'INVALID_AD_UNIT_CODE'; + + const slotRenderEnded = gam.slotRenderEnded[0]; + events.emit(EVENTS.AUCTION_INIT, auction.AUCTION_INIT); + events.emit(EVENTS.BID_REQUESTED, auction.BID_REQUESTED[0]); + mockGpt.emitEvent('slotRenderEnded', slotRenderEnded); + + sandbox.clock.tick(POST_GAM_TIMEOUT + 1); + + assert.calledWithExactly(log.warn, + 'Could not find configured ad unit matching GAM render of slot:', + { slotName: `${adUnitCodes[0]} - ${adSlotElementIds[0]}` }); + }); + }); + + context('and the incomplete report has been sent successfully', function () { + it('sends a report string with any bids with rendered status set to hasWon: 1', function () { + navigator.sendBeacon.returns(true); + + this.enableAnalytics(); + + performStandardAuction({exclude: ['auctionEnd']}); + sandbox.clock.tick(this.defaultTimeout + 1000); + + const incompleteSentBid = JSON.parse(navigator.sendBeacon.firstCall.args[1]).auctions[0].adUnits[1].bids[0]; + assert.strictEqual(incompleteSentBid.hasWon, 1); + }); + + it('reports bids with only targetingSet status as hasWon: 0', function () { + navigator.sendBeacon.returns(true); + + this.enableAnalytics(); + + performStandardAuction({exclude: ['bidWon', 'auctionEnd']}); + sandbox.clock.tick(this.defaultTimeout + 1000); + + const incompleteSentBid = JSON.parse(navigator.sendBeacon.firstCall.args[1]).auctions[0].adUnits[1].bids[0]; + assert.strictEqual(incompleteSentBid.hasWon, 0); + }); + + it('logs an info message', function () { + navigator.sendBeacon.returns(true); + + const endpoint = faker.internet.url(); + this.enableAnalytics({ endpoint }); + + performStandardAuction({exclude: ['bidWon', 'auctionEnd']}); + sandbox.clock.tick(this.defaultTimeout + 1000); + + assert.calledWith(log.info, `Analytics report sent to ${endpoint}`); + }); + }); + }); + + context('when the transaction manager has open transactions', function () { + it('reports those transactions as pending', function () { + this.enableAnalytics(); + + const { prebid: [auction] } = getMockEvents(); + events.emit(EVENTS.AUCTION_INIT, auction.AUCTION_INIT); + events.emit(EVENTS.BID_REQUESTED, auction.BID_REQUESTED[0]); + + const manager = locals.transactionManagers[auction.AUCTION_INIT.auctionId]; + assert.equal(manager.status().pending.length, auction.BID_REQUESTED[0].bids.length); + }); + + context('and a single bidWon event has triggered', function () { + it('completes the transaction', function () { + this.enableAnalytics(); + + const { prebid: [auction] } = getMockEvents(); + events.emit(EVENTS.AUCTION_INIT, auction.AUCTION_INIT); + events.emit(EVENTS.BID_REQUESTED, auction.BID_REQUESTED[0]); + events.emit(EVENTS.BID_WON, auction.BID_WON[0]); + + const manager = locals.transactionManagers[auction.AUCTION_INIT.auctionId]; + assert.deepEqual({ + completed: manager.status().completed.length, + pending: manager.status().pending.length + }, { + completed: 1, + pending: auction.BID_REQUESTED[0].bids.length - 1 + }); + }); + }); + + context('and a single slotRenderEnded event has triggered', function () { + context('and the Google Ad Manager timeout has not elapsed', function () { + it('does NOT complete the transaction', function () { + this.enableAnalytics(); + + const { prebid: [auction], gam } = getMockEvents(); + const slotRenderEnded = gam.slotRenderEnded[0]; + events.emit(EVENTS.AUCTION_INIT, auction.AUCTION_INIT); + events.emit(EVENTS.BID_REQUESTED, auction.BID_REQUESTED[0]); + mockGpt.emitEvent('slotRenderEnded', slotRenderEnded); + + const manager = locals.transactionManagers[auction.AUCTION_INIT.auctionId]; + assert.deepEqual({ + completed: manager.status().completed.length, + pending: manager.status().pending.length + }, { + completed: 0, + pending: auction.BID_REQUESTED[0].bids.length + }); + }); + }); + + context('and the Google Ad Manager timeout has elapsed', function () { + it('completes the transaction', function () { + const timeout = POST_GAM_TIMEOUT + 2000; + this.enableAnalytics({timeout}); + + const { prebid: [auction], gam } = getMockEvents(); + const slotRenderEnded = gam.slotRenderEnded[0]; + events.emit(EVENTS.AUCTION_INIT, auction.AUCTION_INIT); + events.emit(EVENTS.BID_REQUESTED, auction.BID_REQUESTED[0]); + mockGpt.emitEvent('slotRenderEnded', slotRenderEnded); + + sandbox.clock.tick(POST_GAM_TIMEOUT + 1); + const manager = locals.transactionManagers[auction.AUCTION_INIT.auctionId]; + assert.deepEqual({ + completed: manager.status().completed.length, + pending: manager.status().pending.length + }, { + completed: 1, + pending: auction.BID_REQUESTED[0].bids.length - 1 + }); + }); + }); + }); + }); + }); +}); + +const adUnitCodes = ['/19968336/header-bid-tag-0', '/19968336/header-bid-tag-1', '/17118521/header-bid-tag-2']; +const adSlotElementIds = ['ad-slot-div-0', 'ad-slot-div-1', 'ad-slot-div-2']; + +function performStandardAuction({ exclude = [], useSlotElementIds = false } = {}) { + const mockEvents = getMockEvents(); + const { prebid, gam } = mockEvents; + const [auction] = prebid; + + if (!exclude.includes(EVENTS.AUCTION_INIT)) { + if (useSlotElementIds) { + // With this option, identify the ad units by slot element IDs instead of GAM paths + auction.AUCTION_INIT.adUnits.forEach((adUnit, i) => { + adUnit.code = adSlotElementIds[i]; + }); + } + events.emit(EVENTS.AUCTION_INIT, auction.AUCTION_INIT); + } + + if (!exclude.includes(EVENTS.BID_REQUESTED)) { + for (let bidRequestedEvent of auction.BID_REQUESTED) { + events.emit(EVENTS.BID_REQUESTED, bidRequestedEvent); + }; + } + + if (!exclude.includes(EVENTS.BID_RESPONSE)) { + for (let bidResponseEvent of auction.BID_RESPONSE) { + events.emit(EVENTS.BID_RESPONSE, bidResponseEvent); + }; + } + + if (!exclude.includes(EVENTS.AUCTION_END)) { + events.emit(EVENTS.AUCTION_END, auction.AUCTION_END); + } + + if (!exclude.includes('slotRenderEnded')) { + for (let gEvent of gam.slotRenderEnded) { + mockGpt.emitEvent('slotRenderEnded', gEvent); + } + } + + if (!exclude.includes(EVENTS.BID_WON)) { + for (let bidWonEvent of auction.BID_WON) { + events.emit(EVENTS.BID_WON, bidWonEvent); + }; + } +} + +function mapToBids(auctions) { + return auctions.flatMap( + auction => auction.adUnits.flatMap( + au => au.bids + ) + ); +} + +function getLocalAssert() { + function isValidAnalyticsReport(report) { + assert.containsAllKeys(report, ['analyticsVersion', 'pid', 'src', 'pbjsVersion', 'auctions']); + if ('usPrivacy' in report) { + assert.match(report.usPrivacy, /[0|1][Y|N|-]{3}/); + } + if ('gdpr' in report) { + assert.oneOf(report.gdpr, [0, 1]); + } + if (report.gdpr === 1) { + assert.isString(report.gdprConsent); + } + if ('gpp' in report) { + assert.isString(report.gpp); + assert.isArray(report.gppSid); + } + if ('coppa' in report) { + assert.oneOf(report.coppa, [0, 1]); + } + + assert.equal(report.analyticsVersion, '1.0.0'); + assert.isString(report.pid); + assert.isString(report.src); + assert.equal(report.pbjsVersion, '$prebid.version$'); + assert.isArray(report.auctions); + assert.isAbove(report.auctions.length, 0); + report.auctions.forEach(isValidAuction); + } + function isValidAuction(auction) { + assert.hasAllKeys(auction, ['adUnits', 'auctionId', 'userIds']); + assert.isArray(auction.adUnits); + assert.isString(auction.auctionId); + assert.isArray(auction.userIds); + auction.adUnits.forEach(isValidAdUnit); + } + function isValidAdUnit(adUnit) { + assert.hasAllKeys(adUnit, ['transactionId', 'adUnitCode', 'slotId', 'mediaTypes', 'sizes', 'bids']); + assert.isString(adUnit.transactionId); + assert.isString(adUnit.adUnitCode); + assert.isString(adUnit.slotId); + assert.isArray(adUnit.mediaTypes); + assert.isArray(adUnit.sizes); + assert.isArray(adUnit.bids); + adUnit.mediaTypes.forEach(isValidMediaType); + adUnit.sizes.forEach(isValidSizeString); + adUnit.bids.forEach(isValidBid); + } + function isValidBid(bid) { + assert.containsAllKeys(bid, ['bidder', 'bidId', 'source', 'status', 'hasWon']); + if ('bidResponse' in bid) { + isValidBidResponse(bid.bidResponse); + } + assert.isString(bid.bidder); + assert.isString(bid.bidId); + assert.isString(bid.source); + assert.oneOf(bid.status, ['pending', 'timeout', 'targetingSet', 'rendered', 'success', 'rejected', 'no-bid', 'error']); + assert.oneOf(bid.hasWon, [0, 1]); + } + function isValidBidResponse(bidResponse) { + assert.containsAllKeys(bidResponse, ['mediaType', 'size', 'cur', 'cpm', 'cpmFloor']); + if ('cpmOrig' in bidResponse) { + assert.isNumber(bidResponse.cpmOrig); + } + isValidMediaType(bidResponse.mediaType); + isValidSizeString(bidResponse.size); + assert.isString(bidResponse.cur); + assert.isNumber(bidResponse.cpm); + assert.isNumber(bidResponse.cpmFloor); + } + function isValidMediaType(mediaType) { + assert.oneOf(mediaType, ['banner', 'video', 'native']); + } + function isValidSizeString(size) { + assert.match(size, /[0-9]+x[0-9]+/); + } + + function calledOnceWithStringJsonEquivalent(sinonSpy, ...args) { + sinon.assert.calledOnce(sinonSpy); + args.forEach((arg, i) => { + const stubCallArgs = sinonSpy.firstCall.args[i] + + if (typeof arg === 'object') { + assert.deepEqual(JSON.parse(stubCallArgs), arg); + } else { + assert.strictEqual(stubCallArgs, arg); + } + }); + } + + sinon.assert.expose(assert, { prefix: '' }); + return { + ...assert, + calledOnceWithStringJsonEquivalent, + isValidAnalyticsReport, + isValidAuction, + isValidAdUnit, + isValidBid, + isValidBidResponse, + isValidMediaType, + isValidSizeString, + } +}; + +function createReportWithThreeBidWonEvents() { + return { + pid: 'test-pid', + src: 'pbjs', + analyticsVersion: '1.0.0', + pbjsVersion: '$prebid.version$', + auctions: [{ + adUnits: [{ + transactionId: 'ef947609-7b55-4420-8407-599760d0e373', + adUnitCode: adUnitCodes[0], + slotId: adUnitCodes[0], + mediaTypes: ['banner'], + sizes: ['300x250', '300x600'], + bids: [{ + bidder: 'bidder0', + bidId: '20661fc5fbb5d9b', + source: 'client', + status: 'rendered', + bidResponse: { + cpm: 1.5, + cur: 'USD', + cpmOrig: 1.5, + cpmFloor: 1, + mediaType: 'banner', + size: '300x250' + }, + hasWon: 1 + }] + }, { + transactionId: 'abab4423-d962-41aa-adc7-0681f686c330', + adUnitCode: adUnitCodes[1], + slotId: adUnitCodes[1], + mediaTypes: ['banner'], + sizes: ['728x90', '970x250'], + bids: [{ + bidder: 'bidder0', + bidId: '21ad295f40dd7ab', + source: 'client', + status: 'rendered', + bidResponse: { + cpm: 1.5, + cur: 'USD', + cpmOrig: 1.5, + cpmFloor: 1, + mediaType: 'banner', + size: '728x90' + }, + hasWon: 1 + }] + }, { + transactionId: 'b43e7487-0a52-4689-a0f7-d139d08b1f9f', + adUnitCode: adUnitCodes[2], + slotId: adUnitCodes[2], + mediaTypes: ['banner'], + sizes: ['300x250'], + bids: [{ + bidder: 'bidder0', + bidId: '22108ac7b778717', + source: 'client', + status: 'rendered', + bidResponse: { + cpm: 1.5, + cur: 'USD', + cpmOrig: 1.5, + cpmFloor: 1, + mediaType: 'banner', + size: '728x90' + }, + hasWon: 1 + }] + }], + auctionId: 'auction-000', + userIds: ['33acrossId'] + }], + }; +} + +function getMockEvents() { + const auctionId = 'auction-000'; + const userId = { + '33acrossId': { + envelope: 'v1.0014', + }, + }; + + return { + gam: { + slotRenderEnded: [ + { + serviceName: 'publisher_ads', + slot: mockGpt.makeSlot({ code: adUnitCodes[0], divId: adSlotElementIds[0] }), + isEmpty: true, + slotContentChanged: true, + size: null, + advertiserId: null, + campaignId: null, + creativeId: null, + creativeTemplateId: null, + labelIds: null, + lineItemId: null, + isBackfill: false, + }, + { + serviceName: 'publisher_ads', + slot: mockGpt.makeSlot({ code: adUnitCodes[1], divId: adSlotElementIds[1] }), + isEmpty: false, + slotContentChanged: true, + size: [1, 1], + advertiserId: 12345, + campaignId: 400000001, + creativeId: 6789, + creativeTemplateId: null, + labelIds: null, + lineItemId: 1011, + isBackfill: false, + yieldGroupIds: null, + companyIds: null, + }, + { + serviceName: 'publisher_ads', + slot: mockGpt.makeSlot({ code: adUnitCodes[2], divId: adSlotElementIds[2] }), + isEmpty: false, + slotContentChanged: true, + size: [728, 90], + advertiserId: 12346, + campaignId: 299999000, + creativeId: 6790, + creativeTemplateId: null, + labelIds: null, + lineItemId: 1012, + isBackfill: false, + yieldGroupIds: null, + companyIds: null, + }, + ], + }, + prebid: [{ + AUCTION_INIT: { + auctionId, + adUnits: [ + { + code: adUnitCodes[0], + mediaTypes: { + banner: { + sizes: [ + [300, 250], + [300, 600], + ], + }, + }, + bids: [ + { + bidder: 'bidder0', + userId, + }, + ], + sizes: [ + [300, 250], + [300, 600], + ], + transactionId: 'ef947609-7b55-4420-8407-599760d0e373', + ortb2Imp: { + ext: { + gpid: adUnitCodes[0], + }, + }, + }, + { + code: adUnitCodes[1], + mediaTypes: { + banner: { + sizes: [ + [728, 90], + [970, 250], + ], + }, + }, + bids: [ + { + bidder: 'bidder0', + userId, + }, + ], + sizes: [ + [728, 90], + [970, 250], + ], + transactionId: 'abab4423-d962-41aa-adc7-0681f686c330', + ortb2Imp: { + ext: { + gpid: adUnitCodes[1], + }, + }, + }, + { + code: adUnitCodes[2], + mediaTypes: { + banner: { + sizes: [[300, 250]], + }, + }, + bids: [ + { + bidder: '33across', + userId, + }, + { + bidder: 'bidder0', + userId, + }, + ], + sizes: [[300, 250]], + transactionId: 'b43e7487-0a52-4689-a0f7-d139d08b1f9f', + ortb2Imp: { + ext: { + gpid: adUnitCodes[2], + }, + }, + }, + ], + bidderRequests: [ + { + bids: [ + { userId }, + ], + } + ], + }, + BID_REQUESTED: [ + { + auctionId, + bids: [ + { + bidder: 'bidder0', + transactionId: 'ef947609-7b55-4420-8407-599760d0e373', + bidId: '20661fc5fbb5d9b', + src: 'client', + }, + { + bidder: 'bidder0', + transactionId: 'abab4423-d962-41aa-adc7-0681f686c330', + bidId: '21ad295f40dd7ab', + src: 'client', + }, + { + bidder: 'bidder0', + transactionId: 'b43e7487-0a52-4689-a0f7-d139d08b1f9f', + bidId: '22108ac7b778717', + src: 'client', + }, + ], + }], + BID_RESPONSE: [{ + auctionId, + cpm: 1.5, + currency: 'USD', + floorData: { + floorValue: 1 + }, + mediaType: 'banner', + originalCpm: 1.5, + requestId: '20661fc5fbb5d9b', + size: '300x250', + source: 'client', + status: 'targetingSet' + }, + { + auctionId, + cpm: 1.5, + currency: 'USD', + floorData: { + floorValue: 1 + }, + mediaType: 'banner', + originalCpm: 1.5, + requestId: '21ad295f40dd7ab', + size: '728x90', + source: 'client', + status: 'targetingSet', + }, + { + auctionId, + cpm: 1.5, + currency: 'USD', + floorData: { + floorValue: 1 + }, + mediaType: 'banner', + originalCpm: 1.5, + requestId: '22108ac7b778717', + size: '728x90', + source: 'client', + status: 'targetingSet', + }], + BID_WON: [{ + auctionId, + cpm: 1.5, + currency: 'USD', + floorData: { + floorValue: 1 + }, + mediaType: 'banner', + originalCpm: 1.5, + requestId: '20661fc5fbb5d9b', + size: '300x250', + source: 'client', + status: 'rendered', + transactionId: 'ef947609-7b55-4420-8407-599760d0e373', + }, + { + auctionId, + cpm: 1.5, + currency: 'USD', + floorData: { + floorValue: 1 + }, + mediaType: 'banner', + originalCpm: 1.5, + requestId: '21ad295f40dd7ab', + size: '728x90', + source: 'client', + status: 'rendered', + transactionId: 'abab4423-d962-41aa-adc7-0681f686c330', + }, + { + auctionId, + cpm: 1.5, + currency: 'USD', + floorData: { + floorValue: 1 + }, + mediaType: 'banner', + originalCpm: 1.5, + requestId: '22108ac7b778717', + size: '728x90', + source: 'client', + status: 'rendered', + transactionId: 'b43e7487-0a52-4689-a0f7-d139d08b1f9f', + }], + BID_REJECTED: [{ + auctionId, + cpm: 1.5, + currency: 'USD', + floorData: { + floorValue: 2 + }, + mediaType: 'banner', + originalCpm: 1.5, + requestId: '20661fc5fbb5d9b', + width: 300, + height: 250, + source: 'client', + transactionId: 'ef947609-7b55-4420-8407-599760d0e373', + statusMessage: 'Bid available', + rejectionReason: 'Bid does not meet price floor', + }], + AUCTION_END: { + auctionId, + }, + }], + }; +} diff --git a/test/spec/modules/33acrossBidAdapter_spec.js b/test/spec/modules/33acrossBidAdapter_spec.js index aa2621f1fa0..9cc038428bc 100644 --- a/test/spec/modules/33acrossBidAdapter_spec.js +++ b/test/spec/modules/33acrossBidAdapter_spec.js @@ -200,6 +200,29 @@ describe('33acrossBidAdapter:', function () { return this; }; + this.withCoppa = coppaValue => { + Object.assign(ttxRequest.regs, { + coppa: coppaValue + }); + + return this; + }; + + this.withGppConsent = (consentString, applicableSections) => { + Object.assign(ttxRequest, { + regs: { + gpp: consentString, + gpp_sid: applicableSections, + ext: Object.assign( + {}, + ttxRequest.regs.ext + ) + } + }); + + return this; + }; + this.withSite = site => { Object.assign(ttxRequest, { site }); return this; @@ -1049,11 +1072,11 @@ describe('33acrossBidAdapter:', function () { }); it('returns corresponding test server requests with gdpr consent data', function() { - sandbox.stub(config, 'getConfig').callsFake(() => { - return { + sandbox.stub(config, 'getConfig') + .withArgs('ttxSettings') + .returns({ 'url': 'https://foo.com/hb/' - } - }); + }); const ttxRequest = new TtxRequestBuilder() .withBanner() @@ -1085,11 +1108,11 @@ describe('33acrossBidAdapter:', function () { }); it('returns corresponding test server requests with default gdpr consent data', function() { - sandbox.stub(config, 'getConfig').callsFake(() => { - return { + sandbox.stub(config, 'getConfig') + .withArgs('ttxSettings') + .returns({ 'url': 'https://foo.com/hb/' - } - }); + }); const ttxRequest = new TtxRequestBuilder() .withBanner() @@ -1128,11 +1151,11 @@ describe('33acrossBidAdapter:', function () { }); it('returns corresponding test server requests with us_privacy consent data', function() { - sandbox.stub(config, 'getConfig').callsFake(() => { - return { + sandbox.stub(config, 'getConfig') + .withArgs('ttxSettings') + .returns({ 'url': 'https://foo.com/hb/' - } - }); + }); const ttxRequest = new TtxRequestBuilder() .withBanner() @@ -1164,15 +1187,102 @@ describe('33acrossBidAdapter:', function () { }); it('returns corresponding test server requests with default us_privacy consent data', function() { - sandbox.stub(config, 'getConfig').callsFake(() => { - return { + sandbox.stub(config, 'getConfig') + .withArgs('ttxSettings') + .returns({ 'url': 'https://foo.com/hb/' + }); + + const ttxRequest = new TtxRequestBuilder() + .withBanner() + .withProduct() + .build(); + const serverRequest = new ServerRequestBuilder() + .withData(ttxRequest) + .withUrl('https://foo.com/hb/') + .build(); + const [ builtServerRequest ] = spec.buildRequests(bidRequests, bidderRequest); + + validateBuiltServerRequest(builtServerRequest, serverRequest); + }); + }); + + context('when coppa has been enabled', function() { + beforeEach(function() { + sandbox.stub(config, 'getConfig').withArgs('coppa').returns(true); + }); + + it('returns corresponding server requests with coppa: 1', function() { + const ttxRequest = new TtxRequestBuilder() + .withBanner() + .withProduct() + .withCoppa(1) + .build(); + + const serverRequest = new ServerRequestBuilder() + .withData(ttxRequest) + .build(); + const [ builtServerRequest ] = spec.buildRequests(bidRequests, bidderRequest); + + validateBuiltServerRequest(builtServerRequest, serverRequest); + }); + }); + + context('when coppa has been disabled', function() { + beforeEach(function() { + sandbox.stub(config, 'getConfig').withArgs('coppa').returns(false); + }); + + it('returns corresponding server requests with coppa: 0', function() { + const ttxRequest = new TtxRequestBuilder() + .withBanner() + .withProduct() + .withCoppa(0) + .build(); + const serverRequest = new ServerRequestBuilder() + .withData(ttxRequest) + .build(); + const [ builtServerRequest ] = spec.buildRequests(bidRequests, bidderRequest); + + validateBuiltServerRequest(builtServerRequest, serverRequest); + }); + }); + + context('when GPP consent data exists', function() { + beforeEach(function() { + bidderRequest = { + ...bidderRequest, + gppConsent: { + gppString: 'foo', + applicableSections: '123' } - }); + } + }); + it('returns corresponding server requests with GPP consent data', function() { const ttxRequest = new TtxRequestBuilder() .withBanner() .withProduct() + .withGppConsent('foo', '123') + .build(); + const serverRequest = new ServerRequestBuilder() + .withData(ttxRequest) + .build(); + const [ builtServerRequest ] = spec.buildRequests(bidRequests, bidderRequest); + + validateBuiltServerRequest(builtServerRequest, serverRequest); + }); + + it('returns corresponding test server requests with GPP consent data', function() { + sandbox.stub(config, 'getConfig').withArgs('ttxSettings') + .returns({ + 'url': 'https://foo.com/hb/' + }); + + const ttxRequest = new TtxRequestBuilder() + .withBanner() + .withProduct() + .withGppConsent('foo', '123') .build(); const serverRequest = new ServerRequestBuilder() .withData(ttxRequest) @@ -1747,11 +1857,11 @@ describe('33acrossBidAdapter:', function () { context('when SRA mode is enabled', function() { it('builds a single request with multiple imps corresponding to each group {siteId, productId}', function() { - sandbox.stub(config, 'getConfig').callsFake(() => { - return { + sandbox.stub(config, 'getConfig') + .withArgs('ttxSettings') + .returns({ enableSRAMode: true - } - }); + }); const bidRequests = new BidRequestsBuilder() .addBid() @@ -2219,11 +2329,11 @@ describe('33acrossBidAdapter:', function () { const expectedSyncs = [ { type: 'iframe', - url: `${syncs[0].url}&gdpr_consent=undefined&us_privacy=undefined` + url: `${syncs[0].url}&gdpr_consent=undefined&us_privacy=undefined&gpp=&gpp_sid=` }, { type: 'iframe', - url: `${syncs[1].url}&gdpr_consent=undefined&us_privacy=undefined` + url: `${syncs[1].url}&gdpr_consent=undefined&us_privacy=undefined&gpp=&gpp_sid=` } ] @@ -2239,11 +2349,11 @@ describe('33acrossBidAdapter:', function () { const expectedSyncs = [ { type: 'iframe', - url: `${syncs[0].url}&gdpr_consent=undefined&us_privacy=undefined&gdpr=1` + url: `${syncs[0].url}&gdpr_consent=undefined&us_privacy=undefined&gpp=&gpp_sid=&gdpr=1` }, { type: 'iframe', - url: `${syncs[1].url}&gdpr_consent=undefined&us_privacy=undefined&gdpr=1` + url: `${syncs[1].url}&gdpr_consent=undefined&us_privacy=undefined&gpp=&gpp_sid=&gdpr=1` } ]; @@ -2259,11 +2369,11 @@ describe('33acrossBidAdapter:', function () { const expectedSyncs = [ { type: 'iframe', - url: `${syncs[0].url}&gdpr_consent=consent123A&us_privacy=undefined&gdpr=1` + url: `${syncs[0].url}&gdpr_consent=consent123A&us_privacy=undefined&gpp=&gpp_sid=&gdpr=1` }, { type: 'iframe', - url: `${syncs[1].url}&gdpr_consent=consent123A&us_privacy=undefined&gdpr=1` + url: `${syncs[1].url}&gdpr_consent=consent123A&us_privacy=undefined&gpp=&gpp_sid=&gdpr=1` } ]; @@ -2279,11 +2389,11 @@ describe('33acrossBidAdapter:', function () { const expectedSyncs = [ { type: 'iframe', - url: `${syncs[0].url}&gdpr_consent=undefined&us_privacy=undefined&gdpr=0` + url: `${syncs[0].url}&gdpr_consent=undefined&us_privacy=undefined&gpp=&gpp_sid=&gdpr=0` }, { type: 'iframe', - url: `${syncs[1].url}&gdpr_consent=undefined&us_privacy=undefined&gdpr=0` + url: `${syncs[1].url}&gdpr_consent=undefined&us_privacy=undefined&gpp=&gpp_sid=&gdpr=0` } ]; expect(syncResults).to.deep.equal(expectedSyncs); @@ -2298,11 +2408,11 @@ describe('33acrossBidAdapter:', function () { const expectedSyncs = [ { type: 'iframe', - url: `${syncs[0].url}&gdpr_consent=consent123A&us_privacy=undefined` + url: `${syncs[0].url}&gdpr_consent=consent123A&us_privacy=undefined&gpp=&gpp_sid=` }, { type: 'iframe', - url: `${syncs[1].url}&gdpr_consent=consent123A&us_privacy=undefined` + url: `${syncs[1].url}&gdpr_consent=consent123A&us_privacy=undefined&gpp=&gpp_sid=` } ]; expect(syncResults).to.deep.equal(expectedSyncs); @@ -2317,11 +2427,11 @@ describe('33acrossBidAdapter:', function () { const expectedSyncs = [ { type: 'iframe', - url: `${syncs[0].url}&gdpr_consent=consent123A&us_privacy=undefined&gdpr=0` + url: `${syncs[0].url}&gdpr_consent=consent123A&us_privacy=undefined&gpp=&gpp_sid=&gdpr=0` }, { type: 'iframe', - url: `${syncs[1].url}&gdpr_consent=consent123A&us_privacy=undefined&gdpr=0` + url: `${syncs[1].url}&gdpr_consent=consent123A&us_privacy=undefined&gpp=&gpp_sid=&gdpr=0` } ]; expect(syncResults).to.deep.equal(expectedSyncs); @@ -2336,11 +2446,11 @@ describe('33acrossBidAdapter:', function () { const expectedSyncs = [ { type: 'iframe', - url: `${syncs[0].url}&gdpr_consent=undefined&us_privacy=undefined` + url: `${syncs[0].url}&gdpr_consent=undefined&us_privacy=undefined&gpp=&gpp_sid=` }, { type: 'iframe', - url: `${syncs[1].url}&gdpr_consent=undefined&us_privacy=undefined` + url: `${syncs[1].url}&gdpr_consent=undefined&us_privacy=undefined&gpp=&gpp_sid=` } ] @@ -2356,11 +2466,54 @@ describe('33acrossBidAdapter:', function () { const expectedSyncs = [ { type: 'iframe', - url: `${syncs[0].url}&gdpr_consent=undefined&us_privacy=foo` + url: `${syncs[0].url}&gdpr_consent=undefined&us_privacy=foo&gpp=&gpp_sid=` + }, + { + type: 'iframe', + url: `${syncs[1].url}&gdpr_consent=undefined&us_privacy=foo&gpp=&gpp_sid=` + } + ]; + + expect(syncResults).to.deep.equal(expectedSyncs); + }); + }); + + context('when there is no GPP data', function() { + it('returns sync urls with empty GPP params', function() { + spec.buildRequests(bidRequests); + + const syncResults = spec.getUserSyncs(syncOptions, {}); + const expectedSyncs = [ + { + type: 'iframe', + url: `${syncs[0].url}&gdpr_consent=undefined&us_privacy=undefined&gpp=&gpp_sid=` + }, + { + type: 'iframe', + url: `${syncs[1].url}&gdpr_consent=undefined&us_privacy=undefined&gpp=&gpp_sid=` + } + ] + + expect(syncResults).to.deep.equal(expectedSyncs); + }) + }); + + context('when there is GPP data', function() { + it('returns sync urls with GPP consent string & GPP Section ID as params', function() { + spec.buildRequests(bidRequests); + + const syncResults = spec.getUserSyncs(syncOptions, {}, {}, undefined, { + gppString: 'foo', + applicableSections: ['123', '456'] + }); + const expectedSyncs = [ + { + type: 'iframe', + url: `${syncs[0].url}&gdpr_consent=undefined&us_privacy=undefined&gpp=foo&gpp_sid=123%2C456` }, { type: 'iframe', - url: `${syncs[1].url}&gdpr_consent=undefined&us_privacy=foo` + url: `${syncs[1].url}&gdpr_consent=undefined&us_privacy=undefined&gpp=foo&gpp_sid=123%2C456` } ]; diff --git a/test/spec/modules/33acrossIdSystem_spec.js b/test/spec/modules/33acrossIdSystem_spec.js index 5070d2b8845..4f6d7c4a6c5 100644 --- a/test/spec/modules/33acrossIdSystem_spec.js +++ b/test/spec/modules/33acrossIdSystem_spec.js @@ -2,7 +2,7 @@ import { thirthyThreeAcrossIdSubmodule } from 'modules/33acrossIdSystem.js'; import * as utils from 'src/utils.js'; import { server } from 'test/mocks/xhr.js'; -import { uspDataHandler } from 'src/adapterManager.js'; +import { uspDataHandler, coppaDataHandler, gppDataHandler } from 'src/adapterManager.js'; describe('33acrossIdSystem', () => { describe('name', () => { @@ -67,41 +67,35 @@ describe('33acrossIdSystem', () => { expect(request.url).to.contain('gdpr=1'); }); + }); - context('and the consent string is given', () => { - it('should call endpoint with the GDPR consent string', () => { - [ - { consentString: '', expected: '' }, - { consentString: undefined, expected: '' }, - { consentString: 'foo', expected: 'foo' } - ].forEach(({ consentString, expected }, index) => { - const completeCallback = () => {}; - const { callback } = thirthyThreeAcrossIdSubmodule.getId({ - params: { - pid: '12345' - } - }, { - gdprApplies: true, - consentString - }); - - callback(completeCallback); - - expect(server.requests[index].url).to.contain(`gdpr_consent=${expected}`); - }); + context('when GDPR doesn\'t apply', () => { + it('should call endpoint with \'gdpr=0\'', () => { + const completeCallback = () => {}; + const { callback } = thirthyThreeAcrossIdSubmodule.getId({ + params: { + pid: '12345' + } + }, { + gdprApplies: false }); + + callback(completeCallback); + + const [request] = server.requests; + + expect(request.url).to.contain('gdpr=0'); }); }); - context('when GDPR doesn\'t apply', () => { - it('should call endpoint with \'gdpr=0\' and no GDPR consent string parameter', () => { + context('when the GDPR consent string is given', () => { + it('should call endpoint with the GDPR consent string', () => { const completeCallback = () => {}; const { callback } = thirthyThreeAcrossIdSubmodule.getId({ params: { pid: '12345' } }, { - gdprApplies: false, consentString: 'foo' }); @@ -109,8 +103,7 @@ describe('33acrossIdSystem', () => { const [request] = server.requests; - expect(request.url).to.contain('gdpr=0'); - expect(request.url).not.to.contain('gdpr_consent'); + expect(request.url).to.contain('gdpr_consent=foo'); }); }); @@ -157,6 +150,108 @@ describe('33acrossIdSystem', () => { }); }); + context('when coppa is enabled', () => { + it('should call endpoint with an enabled coppa signal', () => { + const completeCallback = () => {}; + const { callback } = thirthyThreeAcrossIdSubmodule.getId({ + params: { + pid: '12345' + } + }); + + sinon.stub(coppaDataHandler, 'getCoppa').returns(true); + + callback(completeCallback); + + const [request] = server.requests; + + expect(request.url).to.contain('coppa=1'); + + coppaDataHandler.getCoppa.restore(); + }); + }); + + context('when coppa is not enabled', () => { + it('should call endpoint with coppa signal not enabled', () => { + const completeCallback = () => {}; + const { callback } = thirthyThreeAcrossIdSubmodule.getId({ + params: { + pid: '12345' + } + }); + + sinon.stub(coppaDataHandler, 'getCoppa').returns(false); + + callback(completeCallback); + + const [request] = server.requests; + + expect(request.url).to.contain('coppa=0'); + + coppaDataHandler.getCoppa.restore(); + }); + }); + + context('when a GPP consent string is given', () => { + beforeEach(() => { + sinon.stub(gppDataHandler, 'getConsentData'); + }); + + afterEach(() => { + gppDataHandler.getConsentData.restore(); + }); + + it('should call endpoint with the GPP consent string', () => { + [ + { gppString: '', expected: '' }, + { gppString: undefined, expected: '' }, + { gppString: 'foo', expected: 'foo' }, + ].forEach(({ gppString, expected }, index) => { + const completeCallback = () => {}; + const { callback } = thirthyThreeAcrossIdSubmodule.getId({ + params: { + pid: '12345' + } + }); + + gppDataHandler.getConsentData.onCall(index).returns({ + gppString + }); + + callback(completeCallback); + + expect(server.requests[index].url).to.contain(`gpp=${expected}`); + }); + }); + + it('should call endpoint with the GPP applicable sections', () => { + const gppString = 'foo'; + + [ + { applicableSections: [], expected: '' }, + { applicableSections: undefined, expected: '' }, + { applicableSections: ['1'], expected: '1' }, + { applicableSections: ['1', '2'], expected: '1%2C2' }, + ].forEach(({ applicableSections, expected }, index) => { + const completeCallback = () => {}; + const { callback } = thirthyThreeAcrossIdSubmodule.getId({ + params: { + pid: '12345' + } + }); + + gppDataHandler.getConsentData.onCall(index).returns({ + gppString: 'foo', + applicableSections + }); + + callback(completeCallback); + + expect(server.requests[index].url).to.contain(`gpp_sid=${expected}`); + }); + }); + }); + context('when the partner ID is not given', () => { it('should log an error', () => { const logErrorSpy = sinon.spy(utils, 'logError'); diff --git a/test/spec/modules/a1MediaBidAdapter_spec.js b/test/spec/modules/a1MediaBidAdapter_spec.js new file mode 100644 index 00000000000..e1db2b9ad8d --- /dev/null +++ b/test/spec/modules/a1MediaBidAdapter_spec.js @@ -0,0 +1,248 @@ +import { spec } from 'modules/a1MediaBidAdapter.js'; +import { config } from 'src/config.js'; +import { BANNER, VIDEO, NATIVE } from 'src/mediaTypes.js'; +import 'modules/currency.js'; +import 'modules/priceFloors.js'; +import { replaceAuctionPrice } from '../../../src/utils'; + +const ortbBlockParams = { + battr: [ 13 ], + bcat: ['IAB1-1'] +}; +const getBidderRequest = (isMulti = false) => { + return { + bidderCode: 'a1media', + auctionId: 'ba87bfdf-493e-4a88-8e26-17b4cbc9adbd', + bidderRequestId: '104e8d2392bd6f', + bids: [ + { + bidder: 'a1media', + params: {}, + auctionId: 'ba87bfdf-493e-4a88-8e26-17b4cbc9adbd', + mediaTypes: { + banner: { + sizes: [ + [ 320, 100 ], + ] + }, + ...(isMulti && { + video: { + mimes: ['video/mp4'] + }, + native: { + title: { + required: true, + }} + }) + }, + ...(isMulti && { + nativeOrtbRequest: { + ver: '1.2', + assets: [ + { + id: 0, + required: 1, + title: { + len: 140 + } + } + ] + } + }), + adUnitCode: 'test-div', + transactionId: 'cab00498-028b-4061-8f9d-a8d66c8cb91d', + bidId: '2e9f38ea93bb9e', + bidderRequestId: '104e8d2392bd6f', + } + ], + } +}; +const getConvertedBidReq = () => { + return { + cur: [ + 'JPY' + ], + imp: [ + { + banner: { + format: [ + { + h: 100, + w: 320 + }, + ], + topframe: 0 + }, + bidfloor: 0, + bidfloorcur: 'JPY', + id: '2e9f38ea93bb9e' + } + ], + test: 0, + } +}; + +const getBidderResponse = () => { + return { + body: { + id: 'bid-response', + cur: 'JPY', + seatbid: [ + { + bid: [{ + impid: '2e9f38ea93bb9e', + crid: 'creative-id', + cur: 'JPY', + price: 9, + }] + } + ] + } + } +} +const bannerAdm = '
'; +const videoAdm = 'testvast1'; +const nativeAdm = '{"ver":"1.2","link":{"url":"test_url"},"assets":[{"id":1,"required":1,"title":{"text":"native_title"}}]}'; +const macroAdm = '
'; +const macroNurl = 'https://d11.contentsfeed.com/dsp/win/example.com/SITE/a1/${AUCTION_PRICE}'; +const interpretedNurl = `
`; + +describe('a1MediaBidAdapter', function() { + describe('isValidRequest', function() { + const bid = { + bidder: 'a1media', + }; + + it('should return true always', function () { + expect(spec.isBidRequestValid(bid)).to.equal(true); + }); + }); + + describe('buildRequests', function() { + let bidderRequest, convertedRequest; + beforeEach(function() { + bidderRequest = getBidderRequest(); + convertedRequest = getConvertedBidReq(); + }); + + it('should return expected request object', function() { + const bidRequest = spec.buildRequests(bidderRequest.bids, bidderRequest); + convertedRequest.id = bidRequest.data.id; + + expect(bidRequest.method).equal('POST'); + expect(bidRequest.url).equal('https://d11.contentsfeed.com/dsp/breq/a1'); + expect(bidRequest.data).deep.equal(convertedRequest); + }); + it('should set ortb blocking using params', function() { + bidderRequest.bids[0].params = ortbBlockParams; + + const bidRequest = spec.buildRequests(bidderRequest.bids, bidderRequest); + convertedRequest.id = bidRequest.data.id; + convertedRequest.bcat = ortbBlockParams.bcat; + convertedRequest.imp[0].banner.battr = ortbBlockParams.battr; + + expect(bidRequest.data).deep.equal(convertedRequest); + }); + + it('should set bidfloor when getFloor is available', function() { + bidderRequest.bids[0].getFloor = () => ({ currency: 'USD', floor: 999 }); + const bidRequest = spec.buildRequests(bidderRequest.bids, bidderRequest); + + expect(bidRequest.data.imp[0].bidfloor).equal(999); + expect(bidRequest.data.imp[0].bidfloorcur).equal('USD'); + }); + + it('should set cur when currency config is configured', function() { + config.setConfig({ + currency: { + adServerCurrency: 'USD', + } + }); + const bidRequest = spec.buildRequests(bidderRequest.bids, bidderRequest); + + expect(bidRequest.data.cur[0]).equal('USD'); + }); + + it('should set bidfloor and currency using params when modules not available', function() { + bidderRequest.bids[0].params.currency = 'USD'; + bidderRequest.bids[0].params.bidfloor = 0.99; + + const bidRequest = spec.buildRequests(bidderRequest.bids, bidderRequest); + convertedRequest.id = bidRequest.data.id; + convertedRequest.imp[0].bidfloor = 0.99; + convertedRequest.imp[0].bidfloorcur = 'USD'; + convertedRequest.cur[0] = 'USD'; + + expect(bidRequest.data).deep.equal(convertedRequest); + }); + }); + + describe('interpretResponse', function() { + describe('when request mediaType is single', function() { + let bidRequest, bidderResponse; + beforeEach(function() { + const bidderRequest = getBidderRequest(); + bidRequest = spec.buildRequests(bidderRequest.bids, bidderRequest); + bidderResponse = getBidderResponse(); + }); + it('should set cpm using price attribute', function() { + const bidResPrice = 9; + bidderResponse.body.seatbid[0].bid[0].price = bidResPrice; + const interpretedRes = spec.interpretResponse(bidderResponse, bidRequest); + expect(interpretedRes[0].cpm).equal(bidResPrice); + }); + it('should set mediaType using request mediaTypes', function() { + const interpretedRes = spec.interpretResponse(bidderResponse, bidRequest); + expect(interpretedRes[0].mediaType).equal(BANNER); + }); + }); + + describe('when request mediaType is multi', function() { + let bidRequest, bidderResponse; + beforeEach(function() { + const bidderRequest = getBidderRequest(true); + bidRequest = spec.buildRequests(bidderRequest.bids, bidderRequest); + bidderResponse = getBidderResponse(); + }); + it('should set mediaType to video', function() { + bidderResponse.body.seatbid[0].bid[0].adm = videoAdm; + const interpretedRes = spec.interpretResponse(bidderResponse, bidRequest); + expect(interpretedRes[0].mediaType).equal(VIDEO); + }); + it('should set mediaType to native', function() { + bidderResponse.body.seatbid[0].bid[0].adm = nativeAdm; + const interpretedRes = spec.interpretResponse(bidderResponse, bidRequest); + expect(interpretedRes[0].mediaType).equal(NATIVE); + }); + it('should set mediaType to banner when adm is neither native or video', function() { + bidderResponse.body.seatbid[0].bid[0].adm = bannerAdm; + const interpretedRes = spec.interpretResponse(bidderResponse, bidRequest); + expect(interpretedRes[0].mediaType).equal(BANNER); + }); + }); + + describe('resolve the AUCTION_PRICE macro', function() { + let bidRequest; + beforeEach(function() { + const bidderRequest = getBidderRequest(true); + bidRequest = spec.buildRequests(bidderRequest.bids, bidderRequest); + }); + it('should return empty array when bid response has not contents', function() { + const emptyResponse = { body: '' }; + const interpretedRes = spec.interpretResponse(emptyResponse, bidRequest); + expect(interpretedRes.length).equal(0); + }); + it('should replace macro keyword if is exist', function() { + const bidderResponse = getBidderResponse(); + bidderResponse.body.seatbid[0].bid[0].adm = macroAdm; + bidderResponse.body.seatbid[0].bid[0].nurl = macroNurl; + const interpretedRes = spec.interpretResponse(bidderResponse, bidRequest); + + const expectedResPrice = 9; + const expectedAd = replaceAuctionPrice(macroAdm, expectedResPrice) + replaceAuctionPrice(interpretedNurl, expectedResPrice); + + expect(interpretedRes[0].ad).equal(expectedAd); + }); + }); + }); +}) diff --git a/test/spec/modules/a1MediaRtdProvider_spec.js b/test/spec/modules/a1MediaRtdProvider_spec.js new file mode 100644 index 00000000000..2630e83fcf5 --- /dev/null +++ b/test/spec/modules/a1MediaRtdProvider_spec.js @@ -0,0 +1,105 @@ +import { subModuleObj } from 'modules/a1MediaRtdProvider.js'; +import { loadExternalScript } from '../../../src/adloader.js'; +import { A1_AUD_KEY, A1_SEG_KEY, getStorageData, storage } from '../../../modules/a1MediaRtdProvider.js'; +import { expect } from 'chai'; + +const configWithParams = { + name: 'a1Media', + waitForIt: true, + params: { + tagId: 'lb4test.min.js', + }, +}; +const configWithoutParams = { + name: 'a1Media', + waitForIt: true, + params: { + }, +}; + +const reqBidsConfigObj = { + ortb2Fragments: { + global: {} + } +}; +const a1TestOrtbObj = { + user: { + data: [ + { + name: 'a1mediagroup.com', + ext: { + segtax: 900 + }, + segment: [{id: 'test'}] + } + ], + ext: { + eids: [ + { + source: 'a1mediagroup.com', + uids: [ + { + id: 'tester', + atype: 1 + } + ] + } + ] + } + } +}; + +describe('a1MediaRtdProvider', function() { + describe('init', function() { + describe('initialize with expected params', function() { + it('successfully initialize with load script', function() { + expect(subModuleObj.init(configWithParams)).to.be.true; + expect(window.linkback.l).to.be.true; + expect(loadExternalScript.called).to.be.true; + expect(loadExternalScript.args[0][0]).to.deep.equal('https://linkback.contentsfeed.com/src/lb4test.min.js'); + }) + + it('successfully initialize but script is already exist', function() { + const linkback = { l: true }; + + expect(subModuleObj.init(configWithParams)).to.be.true; + expect(loadExternalScript.called).to.be.false; + }) + }); + + describe('initialize without expected params', function() { + afterEach(function() { + storage.setCookie(A1_SEG_KEY, '', 0); + }) + + it('successfully initialize when publisher side segment is exist in cookie', function() { + storage.setCookie(A1_SEG_KEY, 'test'); + expect(subModuleObj.init(configWithoutParams)).to.be.true; + expect(getStorageData(A1_SEG_KEY)).to.not.equal(''); + }) + it('fails initalize publisher sied segment is not exist', function() { + expect(subModuleObj.init(configWithoutParams)).to.be.false; + expect(getStorageData(A1_SEG_KEY)).to.equal(''); + }) + }) + }); + + describe('alterBidRequests', function() { + const callback = sinon.stub(); + + before(function() { + storage.setCookie(A1_SEG_KEY, 'test'); + storage.setDataInLocalStorage(A1_AUD_KEY, 'tester'); + }) + after(function() { + storage.setCookie(A1_SEG_KEY, '', 0); + storage.removeDataFromLocalStorage(A1_AUD_KEY); + }) + + it('alterBidRequests', function() { + subModuleObj.getBidRequestData(reqBidsConfigObj, callback); + expect(reqBidsConfigObj.ortb2Fragments.global).to.deep.include(a1TestOrtbObj); + expect(callback.calledOnce).to.be.true; + }) + }); +}) diff --git a/test/spec/modules/acuityadsBidAdapter_spec.js b/test/spec/modules/acuityadsBidAdapter_spec.js new file mode 100644 index 00000000000..31ef9dd6466 --- /dev/null +++ b/test/spec/modules/acuityadsBidAdapter_spec.js @@ -0,0 +1,428 @@ +import { expect } from 'chai'; +import { spec } from '../../../modules/acuityadsBidAdapter'; +import { BANNER, VIDEO, NATIVE } from '../../../src/mediaTypes.js'; +import { getUniqueIdentifierStr } from '../../../src/utils.js'; + +const bidder = 'acuityads' + +describe('AcuityAdsBidAdapter', function () { + const bids = [ + { + bidId: getUniqueIdentifierStr(), + bidder: bidder, + mediaTypes: { + [BANNER]: { + sizes: [[300, 250]] + } + }, + params: { + placementId: 'testBanner', + } + }, + { + bidId: getUniqueIdentifierStr(), + bidder: bidder, + mediaTypes: { + [VIDEO]: { + playerSize: [[300, 300]], + minduration: 5, + maxduration: 60 + } + }, + params: { + placementId: 'testVideo', + } + }, + { + bidId: getUniqueIdentifierStr(), + bidder: bidder, + mediaTypes: { + [NATIVE]: { + native: { + title: { + required: true + }, + body: { + required: true + }, + icon: { + required: true, + size: [64, 64] + } + } + } + }, + params: { + placementId: 'testNative', + } + } + ]; + + const invalidBid = { + bidId: getUniqueIdentifierStr(), + bidder: bidder, + mediaTypes: { + [BANNER]: { + sizes: [[300, 250]] + } + }, + params: { + + } + } + + const bidderRequest = { + uspConsent: '1---', + gdprConsent: 'COvFyGBOvFyGBAbAAAENAPCAAOAAAAAAAAAAAEEUACCKAAA.IFoEUQQgAIQwgIwQABAEAAAAOIAACAIAAAAQAIAgEAACEAAAAAgAQBAAAAAAAGBAAgAAAAAAAFAAECAAAgAAQARAEQAAAAAJAAIAAgAAAYQEAAAQmAgBC3ZAYzUw', + refererInfo: { + referer: 'https://test.com' + }, + timeout: 500, + ortb2: {} + }; + + describe('isBidRequestValid', function () { + it('Should return true if there are bidId, params and key parameters present', function () { + expect(spec.isBidRequestValid(bids[0])).to.be.true; + }); + it('Should return false if at least one of parameters is not present', function () { + expect(spec.isBidRequestValid(invalidBid)).to.be.false; + }); + }); + + describe('buildRequests', function () { + let serverRequest = spec.buildRequests(bids, bidderRequest); + + it('Creates a ServerRequest object with method, URL and data', function () { + expect(serverRequest).to.exist; + expect(serverRequest.method).to.exist; + expect(serverRequest.url).to.exist; + expect(serverRequest.data).to.exist; + }); + + it('Returns POST method', function () { + expect(serverRequest.method).to.equal('POST'); + }); + + it('Returns valid URL', function () { + expect(serverRequest.url).to.equal('https://prebid.admanmedia.com/pbjs'); + }); + + it('Returns general data valid', function () { + let data = serverRequest.data; + expect(data).to.be.an('object'); + expect(data).to.have.all.keys('deviceWidth', + 'deviceHeight', + 'language', + 'secure', + 'host', + 'page', + 'placements', + 'coppa', + 'ccpa', + 'gdpr', + 'tmax' + ); + expect(data.deviceWidth).to.be.a('number'); + expect(data.deviceHeight).to.be.a('number'); + expect(data.language).to.be.a('string'); + expect(data.secure).to.be.within(0, 1); + expect(data.host).to.be.a('string'); + expect(data.page).to.be.a('string'); + expect(data.coppa).to.be.a('number'); + expect(data.gdpr).to.be.a('string'); + expect(data.ccpa).to.be.a('string'); + expect(data.tmax).to.be.a('number'); + expect(data.placements).to.have.lengthOf(3); + }); + + it('Returns valid placements', function () { + const { placements } = serverRequest.data; + for (let i = 0, len = placements.length; i < len; i++) { + const placement = placements[i]; + expect(placement.placementId).to.be.oneOf(['testBanner', 'testVideo', 'testNative']); + expect(placement.adFormat).to.be.oneOf([BANNER, VIDEO, NATIVE]); + expect(placement.bidId).to.be.a('string'); + expect(placement.schain).to.be.an('object'); + expect(placement.bidfloor).to.exist.and.to.equal(0); + expect(placement.type).to.exist.and.to.equal('publisher'); + + if (placement.adFormat === BANNER) { + expect(placement.sizes).to.be.an('array'); + } + switch (placement.adFormat) { + case BANNER: + expect(placement.sizes).to.be.an('array'); + break; + case VIDEO: + expect(placement.playerSize).to.be.an('array'); + expect(placement.minduration).to.be.an('number'); + expect(placement.maxduration).to.be.an('number'); + break; + case NATIVE: + expect(placement.native).to.be.an('object'); + break; + } + } + }); + + it('Returns data with gdprConsent and without uspConsent', function () { + delete bidderRequest.uspConsent; + serverRequest = spec.buildRequests(bids, bidderRequest); + let data = serverRequest.data; + expect(data.gdpr).to.exist; + expect(data.gdpr).to.be.a('string'); + expect(data.gdpr).to.equal(bidderRequest.gdprConsent); + expect(data.ccpa).to.not.exist; + delete bidderRequest.gdprConsent; + }); + + it('Returns data with uspConsent and without gdprConsent', function () { + bidderRequest.uspConsent = '1---'; + delete bidderRequest.gdprConsent; + serverRequest = spec.buildRequests(bids, bidderRequest); + let data = serverRequest.data; + expect(data.ccpa).to.exist; + expect(data.ccpa).to.be.a('string'); + expect(data.ccpa).to.equal(bidderRequest.uspConsent); + expect(data.gdpr).to.not.exist; + }); + + describe('Returns data with gppConsent', function () { + it('bidderRequest.gppConsent', () => { + bidderRequest.gppConsent = { + gppString: 'abc123', + applicableSections: [8] + }; + + serverRequest = spec.buildRequests(bids, bidderRequest); + let data = serverRequest.data; + expect(data).to.be.an('object'); + expect(data).to.have.property('gpp'); + expect(data).to.have.property('gpp_sid'); + delete bidderRequest.gppConsent; + }) + + it('bidderRequest.ortb2.regs.gpp', () => { + bidderRequest.ortb2.regs = bidderRequest.ortb2.regs || {}; + bidderRequest.ortb2.regs.gpp = 'abc123'; + bidderRequest.ortb2.regs.gpp_sid = [8]; + + serverRequest = spec.buildRequests(bids, bidderRequest); + let data = serverRequest.data; + expect(data).to.be.an('object'); + expect(data).to.have.property('gpp'); + expect(data).to.have.property('gpp_sid'); + }) + }); + + it('Returns empty data if no valid requests are passed', function () { + serverRequest = spec.buildRequests([], bidderRequest); + let data = serverRequest.data; + expect(data.placements).to.be.an('array').that.is.empty; + }); + }); + + describe('interpretResponse', function () { + it('Should interpret banner response', function () { + const banner = { + body: [{ + mediaType: 'banner', + width: 300, + height: 250, + cpm: 0.4, + ad: 'Test', + requestId: '23fhj33i987f', + ttl: 120, + creativeId: '2', + netRevenue: true, + currency: 'USD', + dealId: '1', + meta: { + advertiserDomains: ['google.com'], + advertiserId: 1234 + } + }] + }; + let bannerResponses = spec.interpretResponse(banner); + expect(bannerResponses).to.be.an('array').that.is.not.empty; + let dataItem = bannerResponses[0]; + expect(dataItem).to.have.all.keys('requestId', 'cpm', 'width', 'height', 'ad', 'ttl', 'creativeId', + 'netRevenue', 'currency', 'dealId', 'mediaType', 'meta'); + expect(dataItem.requestId).to.equal(banner.body[0].requestId); + expect(dataItem.cpm).to.equal(banner.body[0].cpm); + expect(dataItem.width).to.equal(banner.body[0].width); + expect(dataItem.height).to.equal(banner.body[0].height); + expect(dataItem.ad).to.equal(banner.body[0].ad); + expect(dataItem.ttl).to.equal(banner.body[0].ttl); + expect(dataItem.creativeId).to.equal(banner.body[0].creativeId); + expect(dataItem.netRevenue).to.be.true; + expect(dataItem.currency).to.equal(banner.body[0].currency); + expect(dataItem.meta).to.be.an('object').that.has.any.key('advertiserDomains'); + }); + it('Should interpret video response', function () { + const video = { + body: [{ + vastUrl: 'test.com', + mediaType: 'video', + cpm: 0.5, + requestId: '23fhj33i987f', + ttl: 120, + creativeId: '2', + netRevenue: true, + currency: 'USD', + dealId: '1', + meta: { + advertiserDomains: ['google.com'], + advertiserId: 1234 + } + }] + }; + let videoResponses = spec.interpretResponse(video); + expect(videoResponses).to.be.an('array').that.is.not.empty; + + let dataItem = videoResponses[0]; + expect(dataItem).to.have.all.keys('requestId', 'cpm', 'vastUrl', 'ttl', 'creativeId', + 'netRevenue', 'currency', 'dealId', 'mediaType', 'meta'); + expect(dataItem.requestId).to.equal('23fhj33i987f'); + expect(dataItem.cpm).to.equal(0.5); + expect(dataItem.vastUrl).to.equal('test.com'); + expect(dataItem.ttl).to.equal(120); + expect(dataItem.creativeId).to.equal('2'); + expect(dataItem.netRevenue).to.be.true; + expect(dataItem.currency).to.equal('USD'); + expect(dataItem.meta).to.be.an('object').that.has.any.key('advertiserDomains'); + }); + it('Should interpret native response', function () { + const native = { + body: [{ + mediaType: 'native', + native: { + clickUrl: 'test.com', + title: 'Test', + image: 'test.com', + impressionTrackers: ['test.com'], + }, + ttl: 120, + cpm: 0.4, + requestId: '23fhj33i987f', + creativeId: '2', + netRevenue: true, + currency: 'USD', + meta: { + advertiserDomains: ['google.com'], + advertiserId: 1234 + } + }] + }; + let nativeResponses = spec.interpretResponse(native); + expect(nativeResponses).to.be.an('array').that.is.not.empty; + + let dataItem = nativeResponses[0]; + expect(dataItem).to.have.keys('requestId', 'cpm', 'ttl', 'creativeId', 'netRevenue', 'currency', 'mediaType', 'native', 'meta'); + expect(dataItem.native).to.have.keys('clickUrl', 'impressionTrackers', 'title', 'image') + expect(dataItem.requestId).to.equal('23fhj33i987f'); + expect(dataItem.cpm).to.equal(0.4); + expect(dataItem.native.clickUrl).to.equal('test.com'); + expect(dataItem.native.title).to.equal('Test'); + expect(dataItem.native.image).to.equal('test.com'); + expect(dataItem.native.impressionTrackers).to.be.an('array').that.is.not.empty; + expect(dataItem.native.impressionTrackers[0]).to.equal('test.com'); + expect(dataItem.ttl).to.equal(120); + expect(dataItem.creativeId).to.equal('2'); + expect(dataItem.netRevenue).to.be.true; + expect(dataItem.currency).to.equal('USD'); + expect(dataItem.meta).to.be.an('object').that.has.any.key('advertiserDomains'); + }); + it('Should return an empty array if invalid banner response is passed', function () { + const invBanner = { + body: [{ + width: 300, + cpm: 0.4, + ad: 'Test', + requestId: '23fhj33i987f', + ttl: 120, + creativeId: '2', + netRevenue: true, + currency: 'USD', + dealId: '1' + }] + }; + + let serverResponses = spec.interpretResponse(invBanner); + expect(serverResponses).to.be.an('array').that.is.empty; + }); + it('Should return an empty array if invalid video response is passed', function () { + const invVideo = { + body: [{ + mediaType: 'video', + cpm: 0.5, + requestId: '23fhj33i987f', + ttl: 120, + creativeId: '2', + netRevenue: true, + currency: 'USD', + dealId: '1' + }] + }; + let serverResponses = spec.interpretResponse(invVideo); + expect(serverResponses).to.be.an('array').that.is.empty; + }); + it('Should return an empty array if invalid native response is passed', function () { + const invNative = { + body: [{ + mediaType: 'native', + clickUrl: 'test.com', + title: 'Test', + impressionTrackers: ['test.com'], + ttl: 120, + requestId: '23fhj33i987f', + creativeId: '2', + netRevenue: true, + currency: 'USD', + }] + }; + let serverResponses = spec.interpretResponse(invNative); + expect(serverResponses).to.be.an('array').that.is.empty; + }); + it('Should return an empty array if invalid response is passed', function () { + const invalid = { + body: [{ + ttl: 120, + creativeId: '2', + netRevenue: true, + currency: 'USD', + dealId: '1' + }] + }; + let serverResponses = spec.interpretResponse(invalid); + expect(serverResponses).to.be.an('array').that.is.empty; + }); + }); + + describe('getUserSyncs', function() { + it('Should return array of objects with proper sync config , include GDPR', function() { + const syncData = spec.getUserSyncs({}, {}, { + consentString: 'ALL', + gdprApplies: true, + }, {}); + expect(syncData).to.be.an('array').which.is.not.empty; + expect(syncData[0]).to.be.an('object') + expect(syncData[0].type).to.be.a('string') + expect(syncData[0].type).to.equal('image') + expect(syncData[0].url).to.be.a('string') + expect(syncData[0].url).to.equal('https://cs.admanmedia.com/image?pbjs=1&gdpr=1&gdpr_consent=ALL&coppa=0') + }); + it('Should return array of objects with proper sync config , include CCPA', function() { + const syncData = spec.getUserSyncs({}, {}, {}, { + consentString: '1---' + }); + expect(syncData).to.be.an('array').which.is.not.empty; + expect(syncData[0]).to.be.an('object') + expect(syncData[0].type).to.be.a('string') + expect(syncData[0].type).to.equal('image') + expect(syncData[0].url).to.be.a('string') + expect(syncData[0].url).to.equal('https://cs.admanmedia.com/image?pbjs=1&ccpa_consent=1---&coppa=0') + }); + }); +}); diff --git a/test/spec/modules/ad2ictionBidAdapter_spec.js b/test/spec/modules/ad2ictionBidAdapter_spec.js new file mode 100644 index 00000000000..99800c6dd01 --- /dev/null +++ b/test/spec/modules/ad2ictionBidAdapter_spec.js @@ -0,0 +1,223 @@ +import { expect } from 'chai'; +import { + spec, + API_ENDPOINT, + API_VERSION_NUMBER, +} from 'modules/ad2ictionBidAdapter.js'; +import { newBidder } from 'src/adapters/bidderFactory.js'; + +describe('ad2ictionBidAdapter', 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: 'ad2iction', + params: { id: '11ab384c-e936-11ed-a6a7-f23c9173ed43' }, + mediaTypes: { + banner: { + sizes: [ + [300, 250], + [336, 280], + ], + }, + }, + adUnitCode: 'adunit-code', + sizes: [ + [300, 250], + [336, 280], + ], + bidId: '2a7a3b48778a1b', + bidderRequestId: '1e6509293abe6b', + }; + + it('should return true when required params found', function () { + expect(spec.isBidRequestValid(bid)).to.equal(true); + }); + + it('should return false when params id is not valid (letters)', function () { + const mockBid = { + ...bid, + params: { id: 1234 }, + }; + + expect(spec.isBidRequestValid(mockBid)).to.equal(false); + }); + + it('should return false when params id is not exist', function () { + const mockBid = { + ...bid, + }; + delete mockBid.params.id; + + expect(spec.isBidRequestValid(mockBid)).to.equal(false); + }); + }); + + describe('buildRequests', function () { + const mockValidBidRequests = [ + { + bidder: 'ad2iction', + params: { id: '11ab384c-e936-11ed-a6a7-f23c9173ed43' }, + adUnitCode: 'adunit-code', + sizes: [ + [300, 250], + [336, 280], + ], + bidId: '57ffc0667379e1', + bidderRequestId: '4ddea14478a651', + }, + ]; + + const mockBidderRequest = { + bidderCode: 'ad2iction', + bidderRequestId: '4ddea14478a651', + bids: [ + { + bidder: 'ad2iction', + params: { id: '11ab384c-e936-11ed-a6a7-f23c9173ed43' }, + adUnitCode: 'adunit-code', + transactionId: null, + sizes: [ + [300, 250], + [336, 280], + ], + bidId: '57ffc0667379e1', + bidderRequestId: '4ddea14478a651', + }, + ], + timeout: 1200, + refererInfo: { + ref: 'https://example.com/referer.html', + }, + ortb2: { + source: {}, + site: { + ref: 'https://example.com/referer.html', + }, + device: { + w: 390, + h: 844, + language: 'zh', + }, + }, + start: 1702526505498, + }; + + it('should send bid request to API_ENDPOINT via POST', function () { + const request = spec.buildRequests( + mockValidBidRequests, + mockBidderRequest + ); + + expect(request.url).to.equal(API_ENDPOINT); + expect(request.method).to.equal('POST'); + }); + + it('should send bid request with API version', function () { + const request = spec.buildRequests( + mockValidBidRequests, + mockBidderRequest + ); + + expect(request.data.v).to.equal(API_VERSION_NUMBER); + }); + + it('should send bid request with dada fields', function () { + const request = spec.buildRequests( + mockValidBidRequests, + mockBidderRequest + ); + + expect(request.data).to.include.all.keys('udid', '_'); + expect(request.data).to.have.property('refererInfo'); + expect(request.data).to.have.property('ortb2'); + }); + }); + + describe('interpretResponse', function () { + it('should return an empty array to indicate no valid bids', function () { + const mockServerResponse = {}; + + const bidResponses = spec.interpretResponse(mockServerResponse); + + expect(bidResponses).is.an('array').that.is.empty; + }); + + it('should return a valid bid response', function () { + const MOCK_AD_DOM = "
" + const mockServerResponse = { + body: [ + { + requestId: '23a3d87fb6bde9', + cpm: 1.61, + currency: 'USD', + width: '336', + height: '280', + creativeId: '46271', + netRevenue: 'false', + ad: MOCK_AD_DOM, + meta: { + advertiserDomains: [''], + }, + ttl: 360, + }, + { + requestId: '3ce3efc40c890b', + cpm: 1.61, + currency: 'USD', + width: '336', + height: '280', + creativeId: '46271', + netRevenue: 'false', + ad: MOCK_AD_DOM, + meta: { + advertiserDomains: [''], + }, + ttl: 360, + }, + ], + }; + + const exceptServerResponse = [ + { + requestId: '23a3d87fb6bde9', + cpm: 1.61, + currency: 'USD', + width: '336', + height: '280', + creativeId: '46271', + netRevenue: 'false', + ad: MOCK_AD_DOM, + meta: { + advertiserDomains: [''], + }, + ttl: 360, + }, + { + requestId: '3ce3efc40c890b', + cpm: 1.61, + currency: 'USD', + width: '336', + height: '280', + creativeId: '46271', + netRevenue: 'false', + ad: MOCK_AD_DOM, + meta: { + advertiserDomains: [''], + }, + ttl: 360, + }, + ] + + const bidResponses = spec.interpretResponse(mockServerResponse); + + expect(bidResponses).to.eql(exceptServerResponse); + }); + }); +}); diff --git a/test/spec/modules/adagioAnalyticsAdapter_spec.js b/test/spec/modules/adagioAnalyticsAdapter_spec.js index 581f3cb1b87..5ffd7b0b685 100644 --- a/test/spec/modules/adagioAnalyticsAdapter_spec.js +++ b/test/spec/modules/adagioAnalyticsAdapter_spec.js @@ -1,12 +1,14 @@ import adagioAnalyticsAdapter from 'modules/adagioAnalyticsAdapter.js'; import { expect } from 'chai'; import * as utils from 'src/utils.js'; +import { getGlobal } from 'src/prebidGlobal.js'; +import { server } from 'test/mocks/xhr.js'; let adapterManager = require('src/adapterManager').default; let events = require('src/events'); let constants = require('src/constants.json'); -describe('adagio analytics adapter', () => { +describe('adagio analytics adapter - adagio.js', () => { let sandbox; let adagioQueuePushSpy; @@ -174,3 +176,658 @@ describe('adagio analytics adapter', () => { }); }); }); + +const AUCTION_ID = '25c6d7f5-699a-4bfc-87c9-996f915341fa'; +const AUCTION_ID_ADAGIO = '6fc53663-bde5-427b-ab63-baa9ed296f47' +const AUCTION_ID_CACHE = 'b43d24a0-13d4-406d-8176-3181402bafc4'; +const AUCTION_ID_CACHE_ADAGIO = 'a9cae98f-efb5-477e-9259-27350044f8db'; + +const BID_ADAGIO = Object.assign({}, BID_ADAGIO, { + bidder: 'adagio', + auctionId: AUCTION_ID, + adUnitCode: '/19968336/header-bid-tag-1', + bidId: '3bd4ebb1c900e2', + partnerImpId: 'partnerImpressionID-2', + adId: 'fake_ad_id_2', + requestId: '3bd4ebb1c900e2', + width: 728, + height: 90, + mediaType: 'banner', + cpm: 1.42, + currency: 'USD', + originalCpm: 1.42, + originalCurrency: 'USD', + dealId: 'the-deal-id', + dealChannel: 'PMP', + mi: 'matched-impression', + seatBidId: 'aaaa-bbbb-cccc-dddd', + adserverTargeting: { + 'hb_bidder': 'another', + 'hb_adid': '3bd4ebb1c900e2', + 'hb_pb': '1.500', + 'hb_size': '728x90', + 'hb_source': 'server' + }, + meta: { + advertiserDomains: ['example.com'] + }, + pba: { + sid: '42', + e_pba_test: true + } +}); + +const BID_ANOTHER = Object.assign({}, BID_ANOTHER, { + bidder: 'another', + auctionId: AUCTION_ID, + adUnitCode: '/19968336/header-bid-tag-1', + bidId: '3bd4ebb1c900e2', + partnerImpId: 'partnerImpressionID-2', + adId: 'fake_ad_id_2', + requestId: '3bd4ebb1c900e2', + width: 728, + height: 90, + mediaType: 'banner', + cpm: 1.71, + currency: 'EUR', + originalCpm: 1.62, + originalCurrency: 'GBP', + dealId: 'the-deal-id', + dealChannel: 'PMP', + mi: 'matched-impression', + seatBidId: 'aaaa-bbbb-cccc-dddd', + adserverTargeting: { + 'hb_bidder': 'another', + 'hb_adid': '3bd4ebb1c900e2', + 'hb_pb': '1.500', + 'hb_size': '728x90', + 'hb_source': 'server' + }, + meta: { + advertiserDomains: ['example.com'] + } +}); + +const BID_CACHED = Object.assign({}, BID_ADAGIO, { + auctionId: AUCTION_ID_CACHE, + latestTargetedAuctionId: BID_ADAGIO.auctionId, +}); + +const PARAMS_ADG = { + organizationId: '1001', + site: 'test-com', + pageviewId: 'a68e6d70-213b-496c-be0a-c468ff387106', + environment: 'desktop', + pagetype: 'article', + placement: 'pave_top', + testName: 'test', + testVersion: 'version', +}; + +const AUCTION_INIT_ANOTHER = { + 'auctionId': AUCTION_ID, + 'timestamp': 1519767010567, + 'auctionStatus': 'inProgress', + 'adUnits': [ { + 'code': '/19968336/header-bid-tag-1', + 'mediaTypes': { + 'banner': { + 'sizes': [ + [ + 640, + 480 + ], + [ + 640, + 100 + ] + ] + } + }, + 'sizes': [[640, 480]], + 'bids': [ { + 'bidder': 'another', + 'params': { + 'publisherId': '1001' + }, + }, { + 'bidder': 'nobid', + 'params': { + 'publisherId': '1002' + }, + }, { + 'bidder': 'adagio', + 'params': { + ...PARAMS_ADG + }, + }, ], + 'transactionId': 'ca4af27a-6d02-4f90-949d-d5541fa12014' + }, { + 'code': '/19968336/footer-bid-tag-1', + 'mediaTypes': { + 'banner': { + 'sizes': [ + [ + 640, + 480 + ] + ] + } + }, + 'sizes': [[640, 480]], + 'bids': [ { + 'bidder': 'another', + 'params': { + 'publisherId': '1001' + }, + } ], + 'transactionId': 'ca4af27a-6d02-4f90-949d-d5541fa12014' + } ], + 'adUnitCodes': ['/19968336/header-bid-tag-1', '/19968336/footer-bid-tag-1'], + 'bidderRequests': [ { + 'bidderCode': 'another', + 'auctionId': AUCTION_ID, + 'bidderRequestId': '1be65d7958826a', + 'bids': [ { + 'bidder': 'another', + 'params': { + 'publisherId': '1001', + }, + 'mediaTypes': { + 'banner': { + 'sizes': [[640, 480]] + } + }, + 'adUnitCode': '/19968336/header-bid-tag-1', + 'transactionId': 'ca4af27a-6d02-4f90-949d-d5541fa12014', + 'sizes': [[640, 480]], + 'bidId': '2ecff0db240757', + 'bidderRequestId': '1be65d7958826a', + 'auctionId': AUCTION_ID, + 'src': 'client', + 'bidRequestsCount': 1 + }, { + 'bidder': 'another', + 'params': { + 'publisherId': '1001' + }, + 'mediaTypes': { + 'banner': { + 'sizes': [[640, 480]] + } + }, + 'adUnitCode': '/19968336/footer-bid-tag-1', + 'transactionId': 'ca4af27a-6d02-4f90-949d-d5541fa12014', + 'sizes': [[640, 480]], + 'bidId': '2ecff0db240757', + 'bidderRequestId': '1be65d7958826a', + 'auctionId': AUCTION_ID, + 'src': 'client', + 'bidRequestsCount': 1 + }, { + 'bidder': 'nobid', + 'params': { + 'publisherId': '1001' + }, + 'mediaTypes': { + 'banner': { + 'sizes': [[640, 480]] + } + }, + 'adUnitCode': '/19968336/footer-bid-tag-1', + 'transactionId': 'ca4af27a-6d02-4f90-949d-d5541fa12014', + 'sizes': [[640, 480]], + 'bidId': '2ecff0db240757', + 'bidderRequestId': '1be65d7958826a', + 'auctionId': AUCTION_ID, + 'src': 'client', + 'bidRequestsCount': 1 + } + ], + 'timeout': 3000, + 'refererInfo': { + 'topmostLocation': 'http://www.test.com/page.html', 'reachedTop': true, 'numIframes': 0, 'stack': ['http://www.test.com/page.html'] + } + }, { + 'bidderCode': 'adagio', + 'auctionId': AUCTION_ID, + 'bidderRequestId': '1be65d7958826a', + 'bids': [ { + 'bidder': 'adagio', + 'params': { + ...PARAMS_ADG, + adagioAuctionId: AUCTION_ID_ADAGIO + }, + 'mediaTypes': { + 'banner': { + 'sizes': [[640, 480]] + } + }, + 'adUnitCode': '/19968336/header-bid-tag-1', + 'transactionId': 'ca4af27a-6d02-4f90-949d-d5541fa12014', + 'sizes': [[640, 480]], + 'bidId': '2ecff0db240757', + 'bidderRequestId': '1be65d7958826a', + 'auctionId': AUCTION_ID, + 'src': 'client', + 'bidRequestsCount': 1 + } + ], + 'timeout': 3000, + 'refererInfo': { + 'topmostLocation': 'http://www.test.com/page.html', 'reachedTop': true, 'numIframes': 0, 'stack': ['http://www.test.com/page.html'] + } + } + ], + 'bidsReceived': [], + 'winningBids': [], + 'timeout': 3000 +}; + +const AUCTION_INIT_CACHE = { + 'auctionId': AUCTION_ID_CACHE, + 'timestamp': 1519767010567, + 'auctionStatus': 'inProgress', + 'adUnits': [ { + 'code': '/19968336/header-bid-tag-1', + 'mediaTypes': { + 'banner': { + 'sizes': [ + [ + 640, + 480 + ], + [ + 640, + 100 + ] + ] + } + }, + 'sizes': [[640, 480]], + 'bids': [ { + 'bidder': 'another', + 'params': { + 'publisherId': '1001' + }, + }, { + 'bidder': 'adagio', + 'params': { + ...PARAMS_ADG + }, + }, ], + 'transactionId': 'ca4af27a-6d02-4f90-949d-d5541fa12014' + }, { + 'code': '/19968336/footer-bid-tag-1', + 'mediaTypes': { + 'banner': { + 'sizes': [ + [ + 640, + 480 + ] + ] + } + }, + 'sizes': [[640, 480]], + 'bids': [ { + 'bidder': 'another', + 'params': { + 'publisherId': '1001' + }, + } ], + 'transactionId': 'ca4af27a-6d02-4f90-949d-d5541fa12014' + } ], + 'adUnitCodes': ['/19968336/header-bid-tag-1', '/19968336/footer-bid-tag-1'], + 'bidderRequests': [ { + 'bidderCode': 'another', + 'auctionId': AUCTION_ID_CACHE, + 'bidderRequestId': '1be65d7958826a', + 'bids': [ { + 'bidder': 'another', + 'params': { + 'publisherId': '1001', + }, + 'mediaTypes': { + 'banner': { + 'sizes': [[640, 480]] + } + }, + 'adUnitCode': '/19968336/header-bid-tag-1', + 'transactionId': 'ca4af27a-6d02-4f90-949d-d5541fa12014', + 'sizes': [[640, 480]], + 'bidId': '2ecff0db240757', + 'bidderRequestId': '1be65d7958826a', + 'auctionId': AUCTION_ID_CACHE, + 'src': 'client', + 'bidRequestsCount': 1 + }, { + 'bidder': 'another', + 'params': { + 'publisherId': '1001' + }, + 'mediaTypes': { + 'banner': { + 'sizes': [[640, 480]] + } + }, + 'adUnitCode': '/19968336/footer-bid-tag-1', + 'transactionId': 'ca4af27a-6d02-4f90-949d-d5541fa12014', + 'sizes': [[640, 480]], + 'bidId': '2ecff0db240757', + 'bidderRequestId': '1be65d7958826a', + 'auctionId': AUCTION_ID_CACHE, + 'src': 'client', + 'bidRequestsCount': 1 + } + ], + 'timeout': 3000, + 'refererInfo': { + 'topmostLocation': 'http://www.test.com/page.html', 'reachedTop': true, 'numIframes': 0, 'stack': ['http://www.test.com/page.html'] + } + }, { + 'bidderCode': 'adagio', + 'auctionId': AUCTION_ID_CACHE, + 'bidderRequestId': '1be65d7958826a', + 'bids': [ { + 'bidder': 'adagio', + 'params': { + ...PARAMS_ADG, + adagioAuctionId: AUCTION_ID_CACHE_ADAGIO + }, + 'mediaTypes': { + 'banner': { + 'sizes': [[640, 480]] + } + }, + 'adUnitCode': '/19968336/header-bid-tag-1', + 'transactionId': 'ca4af27a-6d02-4f90-949d-d5541fa12014', + 'sizes': [[640, 480]], + 'bidId': '2ecff0db240757', + 'bidderRequestId': '1be65d7958826a', + 'auctionId': AUCTION_ID_CACHE, + 'src': 'client', + 'bidRequestsCount': 1 + } + ], + 'timeout': 3000, + 'refererInfo': { + 'topmostLocation': 'http://www.test.com/page.html', 'reachedTop': true, 'numIframes': 0, 'stack': ['http://www.test.com/page.html'] + } + } + ], + 'bidsReceived': [], + 'winningBids': [], + 'timeout': 3000 +}; + +const AUCTION_END_ANOTHER = Object.assign({}, AUCTION_INIT_ANOTHER, { + bidsReceived: [BID_ANOTHER, BID_ADAGIO] +}); + +const AUCTION_END_ANOTHER_NOBID = Object.assign({}, AUCTION_INIT_ANOTHER, { + bidsReceived: [] +}); + +const MOCK = { + SET_TARGETING: { + [BID_ADAGIO.adUnitCode]: BID_ADAGIO.adserverTargeting, + [BID_ANOTHER.adUnitCode]: BID_ANOTHER.adserverTargeting + }, + AUCTION_INIT: { + another: AUCTION_INIT_ANOTHER, + bidcached: AUCTION_INIT_CACHE + }, + BID_RESPONSE: { + adagio: BID_ADAGIO, + another: BID_ANOTHER + }, + AUCTION_END: { + another: AUCTION_END_ANOTHER, + another_nobid: AUCTION_END_ANOTHER_NOBID + }, + BID_WON: { + adagio: Object.assign({}, BID_ADAGIO, { + 'status': 'rendered' + }), + another: Object.assign({}, BID_ANOTHER, { + 'status': 'rendered' + }), + bidcached: Object.assign({}, BID_CACHED, { + 'status': 'rendered' + }), + }, + AD_RENDER_SUCCEEDED: { + another: { + ad: '
ad
', + adId: 'fake_ad_id_2', + bid: BID_ANOTHER + }, + bidcached: { + ad: '
ad
', + adId: 'fake_ad_id_2', + bid: BID_CACHED + } + }, + AD_RENDER_FAILED: { + bidcached: { + adId: 'fake_ad_id_2', + bid: BID_CACHED + } + } +}; + +describe('adagio analytics adapter', () => { + let sandbox; + + beforeEach(() => { + sandbox = sinon.sandbox.create(); + + sandbox.stub(events, 'getEvents').returns([]); + + adapterManager.registerAnalyticsAdapter({ + code: 'adagio', + adapter: adagioAnalyticsAdapter + }); + }); + + afterEach(() => { + sandbox.restore(); + }); + + describe('track', () => { + beforeEach(() => { + adapterManager.enableAnalytics({ + provider: 'adagio' + }); + }); + + afterEach(() => { + adagioAnalyticsAdapter.disableAnalytics(); + }); + + it('builds and sends auction data', () => { + getGlobal().convertCurrency = (cpm, from, to) => { + const convKeys = { + 'GBP-EUR': 0.7, + 'EUR-GBP': 1.3, + 'USD-EUR': 0.8, + 'EUR-USD': 1.2, + 'USD-GBP': 0.6, + 'GBP-USD': 1.6, + }; + return cpm * (convKeys[`${from}-${to}`] || 1); + }; + + events.emit(constants.EVENTS.AUCTION_INIT, MOCK.AUCTION_INIT.another); + events.emit(constants.EVENTS.BID_RESPONSE, MOCK.BID_RESPONSE.adagio); + events.emit(constants.EVENTS.BID_RESPONSE, MOCK.BID_RESPONSE.another); + events.emit(constants.EVENTS.AUCTION_END, MOCK.AUCTION_END.another); + events.emit(constants.EVENTS.BID_WON, MOCK.BID_WON.another); + events.emit(constants.EVENTS.AD_RENDER_SUCCEEDED, MOCK.AD_RENDER_SUCCEEDED.another); + + expect(server.requests.length).to.equal(3, 'requests count'); + { + const { protocol, hostname, pathname, search } = utils.parseUrl(server.requests[0].url); + expect(protocol).to.equal('https'); + expect(hostname).to.equal('c.4dex.io'); + expect(pathname).to.equal('/pba.gif'); + expect(search.v).to.equal('1'); + expect(search.pbjsv).to.equal('$prebid.version$'); + expect(search.auct_id).to.equal(AUCTION_ID_ADAGIO); + expect(search.adu_code).to.equal('/19968336/header-bid-tag-1'); + expect(search.org_id).to.equal('1001'); + expect(search.site).to.equal('test-com'); + expect(search.pv_id).to.equal('a68e6d70-213b-496c-be0a-c468ff387106'); + expect(search.url_dmn).to.equal(window.location.hostname); + expect(search.pgtyp).to.equal('article'); + expect(search.plcmt).to.equal('pave_top'); + expect(search.mts).to.equal('ban'); + expect(search.ban_szs).to.equal('640x100,640x480'); + expect(search.bdrs).to.equal('adagio,another,nobid'); + expect(search.adg_mts).to.equal('ban'); + } + + { + const { protocol, hostname, pathname, search } = utils.parseUrl(server.requests[1].url); + expect(protocol).to.equal('https'); + expect(hostname).to.equal('c.4dex.io'); + expect(pathname).to.equal('/pba.gif'); + expect(search.v).to.equal('2'); + expect(search.e_sid).to.equal('42'); + expect(search.e_pba_test).to.equal('true'); + expect(search.bdrs_bid).to.equal('1,1,0'); + } + + { + const { protocol, hostname, pathname, search } = utils.parseUrl(server.requests[2].url); + expect(protocol).to.equal('https'); + expect(hostname).to.equal('c.4dex.io'); + expect(pathname).to.equal('/pba.gif'); + expect(search.v).to.equal('3'); + expect(search.auct_id).to.equal(AUCTION_ID_ADAGIO); + expect(search.adu_code).to.equal('/19968336/header-bid-tag-1'); + expect(search.win_bdr).to.equal('another'); + expect(search.win_mt).to.equal('ban'); + expect(search.win_ban_sz).to.equal('728x90'); + expect(search.win_cpm).to.equal('1.71'); + expect(search.cur).to.equal('EUR'); + expect(search.cur_rate).to.equal('1.2'); + expect(search.og_cpm).to.equal('1.62'); + expect(search.og_cur).to.equal('GBP'); + expect(search.og_cur_rate).to.equal('1.6'); + } + }); + + it('builds and sends auction data with a cached bid win', () => { + getGlobal().convertCurrency = (cpm, from, to) => { + const convKeys = { + 'GBP-EUR': 0.7, + 'EUR-GBP': 1.3, + 'USD-EUR': 0.8, + 'EUR-USD': 1.2, + 'USD-GBP': 0.6, + 'GBP-USD': 1.6, + }; + return cpm * (convKeys[`${from}-${to}`] || 1); + }; + + events.emit(constants.EVENTS.AUCTION_INIT, MOCK.AUCTION_INIT.bidcached); + events.emit(constants.EVENTS.AUCTION_INIT, MOCK.AUCTION_INIT.another); + events.emit(constants.EVENTS.BID_RESPONSE, MOCK.BID_RESPONSE.adagio); + events.emit(constants.EVENTS.BID_RESPONSE, MOCK.BID_RESPONSE.another); + events.emit(constants.EVENTS.AUCTION_END, MOCK.AUCTION_END.another_nobid); + events.emit(constants.EVENTS.BID_WON, MOCK.BID_WON.bidcached); + events.emit(constants.EVENTS.AD_RENDER_FAILED, MOCK.AD_RENDER_FAILED.bidcached); + + expect(server.requests.length).to.equal(5, 'requests count'); + { + const { protocol, hostname, pathname, search } = utils.parseUrl(server.requests[0].url); + expect(protocol).to.equal('https'); + expect(hostname).to.equal('c.4dex.io'); + expect(pathname).to.equal('/pba.gif'); + expect(search.v).to.equal('1'); + expect(search.pbjsv).to.equal('$prebid.version$'); + expect(search.auct_id).to.equal(AUCTION_ID_CACHE_ADAGIO); + expect(search.adu_code).to.equal('/19968336/header-bid-tag-1'); + expect(search.org_id).to.equal('1001'); + expect(search.site).to.equal('test-com'); + expect(search.pv_id).to.equal('a68e6d70-213b-496c-be0a-c468ff387106'); + expect(search.url_dmn).to.equal(window.location.hostname); + expect(search.pgtyp).to.equal('article'); + expect(search.plcmt).to.equal('pave_top'); + expect(search.mts).to.equal('ban'); + expect(search.ban_szs).to.equal('640x100,640x480'); + expect(search.bdrs).to.equal('adagio,another'); + expect(search.adg_mts).to.equal('ban'); + expect(search.t_n).to.equal('test'); + expect(search.t_v).to.equal('version'); + } + + { + const { protocol, hostname, pathname, search } = utils.parseUrl(server.requests[1].url); + expect(protocol).to.equal('https'); + expect(hostname).to.equal('c.4dex.io'); + expect(pathname).to.equal('/pba.gif'); + expect(search.v).to.equal('1'); + expect(search.pbjsv).to.equal('$prebid.version$'); + expect(search.auct_id).to.equal(AUCTION_ID_ADAGIO); + expect(search.adu_code).to.equal('/19968336/header-bid-tag-1'); + expect(search.org_id).to.equal('1001'); + expect(search.site).to.equal('test-com'); + expect(search.pv_id).to.equal('a68e6d70-213b-496c-be0a-c468ff387106'); + expect(search.url_dmn).to.equal(window.location.hostname); + expect(search.pgtyp).to.equal('article'); + expect(search.plcmt).to.equal('pave_top'); + expect(search.mts).to.equal('ban'); + expect(search.ban_szs).to.equal('640x100,640x480'); + expect(search.bdrs).to.equal('adagio,another,nobid'); + expect(search.adg_mts).to.equal('ban'); + } + + { + const { protocol, hostname, pathname, search } = utils.parseUrl(server.requests[2].url); + expect(protocol).to.equal('https'); + expect(hostname).to.equal('c.4dex.io'); + expect(pathname).to.equal('/pba.gif'); + expect(search.v).to.equal('2'); + expect(search.e_sid).to.equal('42'); + expect(search.e_pba_test).to.equal('true'); + expect(search.bdrs_bid).to.equal('0,0,0'); + } + + { + const { protocol, hostname, pathname, search } = utils.parseUrl(server.requests[3].url); + expect(protocol).to.equal('https'); + expect(hostname).to.equal('c.4dex.io'); + expect(pathname).to.equal('/pba.gif'); + expect(search.v).to.equal('3'); + expect(search.auct_id).to.equal(AUCTION_ID_ADAGIO); + expect(search.auct_id_c).to.equal(AUCTION_ID_CACHE_ADAGIO); + expect(search.adu_code).to.equal('/19968336/header-bid-tag-1'); + expect(search.win_bdr).to.equal('adagio'); + expect(search.win_mt).to.equal('ban'); + expect(search.win_ban_sz).to.equal('728x90'); + expect(search.win_cpm).to.equal('1.42'); + expect(search.cur).to.equal('USD'); + expect(search.cur_rate).to.equal('1'); + expect(search.og_cpm).to.equal('1.42'); + expect(search.og_cur).to.equal('USD'); + expect(search.og_cur_rate).to.equal('1'); + expect(search.rndr).to.not.exist; + } + + { + const { protocol, hostname, pathname, search } = utils.parseUrl(server.requests[4].url); + expect(protocol).to.equal('https'); + expect(hostname).to.equal('c.4dex.io'); + expect(pathname).to.equal('/pba.gif'); + expect(search.v).to.equal('4'); + expect(search.auct_id).to.equal(AUCTION_ID_ADAGIO); + expect(search.auct_id_c).to.equal(AUCTION_ID_CACHE_ADAGIO); + expect(search.adu_code).to.equal('/19968336/header-bid-tag-1'); + expect(search.rndr).to.equal('0'); + } + }); + }); +}); diff --git a/test/spec/modules/adagioBidAdapter_spec.js b/test/spec/modules/adagioBidAdapter_spec.js index 2fb40070184..744f3c69e83 100644 --- a/test/spec/modules/adagioBidAdapter_spec.js +++ b/test/spec/modules/adagioBidAdapter_spec.js @@ -256,7 +256,6 @@ describe('Adagio bid adapter', () => { describe('buildRequests()', function() { const expectedDataKeys = [ - 'id', 'organizationId', 'secure', 'device', @@ -269,7 +268,8 @@ describe('Adagio bid adapter', () => { 'prebidVersion', 'featuresVersion', 'data', - 'usIfr' + 'usIfr', + 'adgjs', ]; it('groups requests by organizationId', function() { @@ -322,6 +322,21 @@ describe('Adagio bid adapter', () => { expect(requests[0].data.adUnits[0].transactionId).to.not.exist; }); + it('should enrich prebid bid requests params', function() { + const expectedAuctionId = '373bcda7-9794-4f1c-be2c-0d223d11d579' + const expectedPageviewId = '56befc26-8cf0-472d-b105-73896df8eb89'; + sandbox.stub(utils, 'generateUUID').returns(expectedAuctionId); + sandbox.stub(adagio, 'getPageviewId').returns(expectedPageviewId); + + const bid01 = new BidRequestBuilder().withParams().build(); + const bidderRequest = new BidderRequestBuilder().build(); + + spec.buildRequests([bid01], bidderRequest); + + expect(bid01.params.adagioAuctionId).eq(expectedAuctionId); + expect(bid01.params.pageviewId).eq(expectedPageviewId); + }); + it('should enqueue computed features for collect usage', function() { sandbox.stub(Date, 'now').returns(12345); @@ -697,6 +712,95 @@ describe('Adagio bid adapter', () => { }); }); + describe('with GPP', function() { + const bid01 = new BidRequestBuilder().withParams().build(); + + const regsGpp = 'regs_gpp_consent_string'; + const regsApplicableSections = [2]; + + const ortb2Gpp = 'ortb2_gpp_consent_string'; + const ortb2GppSid = [1]; + + context('When GPP in regs module', function() { + it('send gpp and gppSid to the server', function() { + const bidderRequest = new BidderRequestBuilder({ + gppConsent: { + gppString: regsGpp, + applicableSections: regsApplicableSections, + } + }).build(); + + const requests = spec.buildRequests([bid01], bidderRequest); + + expect(requests[0].data.regs.gpp).to.equal(regsGpp); + expect(requests[0].data.regs.gppSid).to.equal(regsApplicableSections); + }); + }); + + context('When GPP partially defined in regs module', function() { + it('send gpp and gppSid coming from ortb2 to the server', function() { + const bidderRequest = new BidderRequestBuilder({ + gppConsent: { + gppString: regsGpp, + }, + ortb2: { + regs: { + gpp: ortb2Gpp, + gpp_sid: ortb2GppSid, + } + } + }).build(); + + const requests = spec.buildRequests([bid01], bidderRequest); + + expect(requests[0].data.regs.gpp).to.equal(ortb2Gpp); + expect(requests[0].data.regs.gppSid).to.equal(ortb2GppSid); + }); + + it('send empty gpp and gppSid if no ortb2 fields to the server', function() { + const bidderRequest = new BidderRequestBuilder({ + gppConsent: { + gppString: regsGpp, + } + }).build(); + + const requests = spec.buildRequests([bid01], bidderRequest); + + expect(requests[0].data.regs.gpp).to.equal(''); + expect(requests[0].data.regs.gppSid).to.be.empty; + }); + }); + + context('When GPP defined in ortb2 module', function() { + it('send gpp and gppSid coming from ortb2 to the server', function() { + const bidderRequest = new BidderRequestBuilder({ + ortb2: { + regs: { + gpp: ortb2Gpp, + gpp_sid: ortb2GppSid, + } + } + }).build(); + + const requests = spec.buildRequests([bid01], bidderRequest); + + expect(requests[0].data.regs.gpp).to.equal(ortb2Gpp); + expect(requests[0].data.regs.gppSid).to.equal(ortb2GppSid); + }); + }); + + context('When GPP not defined in any modules', function() { + it('send empty gpp and gppSid', function() { + const bidderRequest = new BidderRequestBuilder({}).build(); + + const requests = spec.buildRequests([bid01], bidderRequest); + + expect(requests[0].data.regs.gpp).to.equal(''); + expect(requests[0].data.regs.gppSid).to.be.empty; + }); + }); + }); + describe('with userID modules', function() { const userIdAsEids = [{ 'source': 'pubcid.org', @@ -757,11 +861,6 @@ describe('Adagio bid adapter', () => { } const requests = spec.buildRequests([bid01], bidderRequest); - expect(requests[0].data.adUnits[0].floors.length).to.equal(3); - expect(requests[0].data.adUnits[0].floors[0]).to.deep.equal({f: 1, mt: 'banner', s: '300x250'}); - expect(requests[0].data.adUnits[0].floors[1]).to.deep.equal({f: 1, mt: 'banner', s: '300x600'}); - expect(requests[0].data.adUnits[0].floors[2]).to.deep.equal({f: 1, mt: 'video', s: '600x480'}); - expect(requests[0].data.adUnits[0].mediaTypes.banner.sizes.length).to.equal(2); expect(requests[0].data.adUnits[0].mediaTypes.banner.bannerSizes[0]).to.deep.equal({size: [300, 250], floor: 1}); expect(requests[0].data.adUnits[0].mediaTypes.banner.bannerSizes[1]).to.deep.equal({size: [300, 600], floor: 1}); @@ -786,10 +885,6 @@ describe('Adagio bid adapter', () => { } const requests = spec.buildRequests([bid01], bidderRequest); - expect(requests[0].data.adUnits[0].floors.length).to.equal(2); - expect(requests[0].data.adUnits[0].floors[0]).to.deep.equal({f: 1, mt: 'video'}); - expect(requests[0].data.adUnits[0].floors[1]).to.deep.equal({f: 1, mt: 'native'}); - expect(requests[0].data.adUnits[0].mediaTypes.video.floor).to.equal(1); expect(requests[0].data.adUnits[0].mediaTypes.native.floor).to.equal(1); }); @@ -809,8 +904,6 @@ describe('Adagio bid adapter', () => { } const requests = spec.buildRequests([bid01], bidderRequest); - expect(requests[0].data.adUnits[0].floors.length).to.equal(1); - expect(requests[0].data.adUnits[0].floors[0]).to.deep.equal({mt: 'video'}); expect(requests[0].data.adUnits[0].mediaTypes.video.floor).to.be.undefined; }); }); @@ -854,6 +947,34 @@ describe('Adagio bid adapter', () => { expect(requests[0].data.usIfr).to.equal(false); }); }); + + describe('with GPID', function () { + const gpid = '/12345/my-gpt-tag-0'; + + it('should add preferred gpid to the request', function () { + const bid01 = new BidRequestBuilder().withParams().build(); + bid01.ortb2Imp = { + ext: { + gpid: gpid + } + }; + const bidderRequest = new BidderRequestBuilder().build(); + const requests = spec.buildRequests([bid01], bidderRequest); + expect(requests[0].data.adUnits[0].gpid).to.exist.and.equal(gpid); + }); + + it('should add backup gpid to the request', function () { + const bid01 = new BidRequestBuilder().withParams().build(); + bid01.ortb2Imp = { + ext: { + data: { pbadslot: gpid } + } + }; + const bidderRequest = new BidderRequestBuilder().build(); + const requests = spec.buildRequests([bid01], bidderRequest); + expect(requests[0].data.adUnits[0].gpid).to.exist.and.equal(gpid); + }); + }); }); describe('interpretResponse()', function() { diff --git a/test/spec/modules/adfBidAdapter_spec.js b/test/spec/modules/adfBidAdapter_spec.js index 88f595b6c00..c1acff522c0 100644 --- a/test/spec/modules/adfBidAdapter_spec.js +++ b/test/spec/modules/adfBidAdapter_spec.js @@ -1,4 +1,5 @@ // jshint esversion: 6, es3: false, node: true +/* eslint-disable no-console */ import { assert } from 'chai'; import { spec } from 'modules/adfBidAdapter.js'; import { config } from 'src/config.js'; @@ -446,6 +447,52 @@ describe('Adf adapter', function () { }); }); + it('should add correct params to getFloor', function () { + let result; + let mediaTypes = { video: { + playerSize: [ 100, 200 ] + } }; + const expectedFloors = [ 1, 1.3, 0.5 ]; + config.setConfig({ currency: { adServerCurrency: 'DKK' } }); + let validBidRequests = expectedFloors.map(getBidWithFloorTest); + getRequestImps(validBidRequests); + assert.deepEqual(result, { currency: 'DKK', size: '*', mediaType: '*' }); + + mediaTypes = { banner: { + sizes: [ [100, 200], [300, 400] ] + }}; + validBidRequests = expectedFloors.map(getBidWithFloorTest); + getRequestImps(validBidRequests); + + assert.deepEqual(result, { currency: 'DKK', size: '*', mediaType: '*' }); + + mediaTypes = { native: {} }; + validBidRequests = expectedFloors.map(getBidWithFloorTest); + getRequestImps(validBidRequests); + + assert.deepEqual(result, { currency: 'DKK', size: '*', mediaType: '*' }); + + mediaTypes = {}; + validBidRequests = expectedFloors.map(getBidWithFloorTest); + getRequestImps(validBidRequests); + + assert.deepEqual(result, { currency: 'DKK', size: '*', mediaType: '*' }); + + function getBidWithFloorTest(floor) { + return { + params: { mid: 1 }, + mediaTypes: mediaTypes, + getFloor: (args) => { + result = args; + return { + currency: 'DKK', + floor + }; + } + }; + } + }); + function getBidWithFloor(floor) { return { params: { mid: 1 }, diff --git a/test/spec/modules/adfusionBidAdapter_spec.js b/test/spec/modules/adfusionBidAdapter_spec.js new file mode 100644 index 00000000000..82705b727b4 --- /dev/null +++ b/test/spec/modules/adfusionBidAdapter_spec.js @@ -0,0 +1,130 @@ +import { expect } from 'chai'; +import { spec } from 'modules/adfusionBidAdapter'; +import 'modules/priceFloors.js'; +import 'modules/currency.js'; +import { newBidder } from 'src/adapters/bidderFactory'; + +describe('adfusionBidAdapter', function () { + const adapter = newBidder(spec); + describe('inherited functions', function () { + it('exists and is a function', function () { + expect(adapter.callBids).to.exist.and.to.be.a('function'); + }); + }); + + describe('isBidRequestValid', function () { + const bid = { + bidder: 'adfusion', + params: { + accountId: 1234, + }, + adUnitCode: '/adunit-code/test-path', + bidId: 'test-bid-id-1', + bidderRequestId: 'test-bid-request-1', + auctionId: 'test-auction-1', + transactionId: 'test-transactionId-1', + }; + + it('should return true when required params are found', function () { + expect(spec.isBidRequestValid(bid)).to.equal(true); + }); + + it('should return false when params.accountID is missing', function () { + let localbid = Object.assign({}, bid); + delete localbid.params.accountId; + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + }); + + describe('buildRequests', function () { + let bidRequests, bannerBidRequest, bidderRequest; + beforeEach(function () { + bidRequests = [ + { + bidder: 'adfusion', + params: { + accountId: 1234, + }, + mediaTypes: { + banner: { + sizes: [ + [300, 250], + [300, 600], + ], + }, + }, + adUnitCode: '/adunit-code/test-path', + bidId: 'test-bid-id-1', + bidderRequestId: 'test-bid-request-1', + auctionId: 'test-auction-1', + transactionId: 'test-transactionId-1', + }, + { + bidder: 'adfusion', + params: { + accountId: 1234, + }, + adUnitCode: 'adunit-code', + mediaTypes: { + video: { + playerSize: [640, 480], + }, + }, + bidId: 'test-bid-id-2', + bidderRequestId: 'test-bid-request-2', + auctionId: 'test-auction-2', + transactionId: 'test-transactionId-2', + }, + ]; + bannerBidRequest = { + bidder: 'adfusion', + params: { + accountId: 1234, + }, + mediaTypes: { + banner: { + sizes: [ + [300, 250], + [300, 600], + ], + }, + }, + adUnitCode: '/adunit-code/test-path', + bidId: 'test-bid-id-1', + bidderRequestId: 'test-bid-request-1', + auctionId: 'test-auction-1', + transactionId: 'test-transactionId-1', + }; + bidderRequest = { refererInfo: {} }; + }); + + it('should return an empty array when no bid requests', function () { + const bidRequest = spec.buildRequests([], bidderRequest); + expect(bidRequest).to.be.an('array'); + expect(bidRequest.length).to.equal(0); + }); + + it('should return a valid bid request object', function () { + const request = spec.buildRequests(bidRequests, bidderRequest); + expect(request).to.be.an('array'); + expect(request[0].data).to.be.an('object'); + expect(request[0].method).to.equal('POST'); + expect(request[0].currency).to.not.equal('USD'); + expect(request[0].url).to.not.equal(''); + expect(request[0].url).to.not.equal(undefined); + expect(request[0].url).to.not.equal(null); + }); + + it('should add bid floor', function () { + let bidRequest = Object.assign({}, bannerBidRequest); + let payload = spec.buildRequests([bidRequest], bidderRequest)[0].data; + expect(payload.imp[0].bidfloorcur).to.not.exist; + + let getFloorResponse = { currency: 'USD', floor: 3 }; + bidRequest.getFloor = () => getFloorResponse; + payload = spec.buildRequests([bidRequest], bidderRequest)[0].data; + expect(payload.imp[0].bidfloor).to.equal(3); + expect(payload.imp[0].bidfloorcur).to.equal('USD'); + }); + }); +}); diff --git a/test/spec/modules/adgenerationBidAdapter_spec.js b/test/spec/modules/adgenerationBidAdapter_spec.js index de8463731e0..adfd38d22cc 100644 --- a/test/spec/modules/adgenerationBidAdapter_spec.js +++ b/test/spec/modules/adgenerationBidAdapter_spec.js @@ -184,12 +184,12 @@ describe('AdgenerationAdapter', function () { } }; const data = { - banner: `posall=SSPLOC&id=58278&sdktype=0&hb=true&t=json3&sizes=300x250%2C320x100¤cy=JPY&pbver=${prebid.version}&sdkname=prebidjs&adapterver=1.6.0&imark=1&tp=https%3A%2F%2Fexample.com`, - bannerUSD: `posall=SSPLOC&id=58278&sdktype=0&hb=true&t=json3&sizes=300x250%2C320x100¤cy=USD&pbver=${prebid.version}&sdkname=prebidjs&adapterver=1.6.0&imark=1&tp=https%3A%2F%2Fexample.com`, - native: `posall=SSPLOC&id=58278&sdktype=0&hb=true&t=json3&sizes=1x1¤cy=JPY&pbver=${prebid.version}&sdkname=prebidjs&adapterver=1.6.0&tp=https%3A%2F%2Fexample.com`, - bannerWithHyperId: `posall=SSPLOC&id=58278&sdktype=0&hb=true&t=json3&sizes=320x100¤cy=JPY&pbver=${prebid.version}&sdkname=prebidjs&adapterver=1.6.0&imark=1&tp=https%3A%2F%2Fexample.com&hyper_id=novatiqId`, - bannerWithAdgextCriteoId: `posall=SSPLOC&id=58278&sdktype=0&hb=true&t=json3&sizes=320x100¤cy=JPY&pbver=${prebid.version}&sdkname=prebidjs&adapterver=1.6.0&adgext_criteo_id=criteo-id-test-1234567890&imark=1&tp=https%3A%2F%2Fexample.com`, - bannerWithAdgextIds: `posall=SSPLOC&id=58278&sdktype=0&hb=true&t=json3&sizes=320x100¤cy=JPY&pbver=${prebid.version}&sdkname=prebidjs&adapterver=1.6.0&adgext_id5_id=id5-id-test-1234567890&adgext_id5_id_link_type=2&adgext_imuid=i.KrAH6ZAZTJOnH5S4N2sogA&adgext_uid2=%5Bobject%20Object%5D&gpid=%252F1111%252Fhomepage%2523300x250&uach=%7B%22source%22%3A2%2C%22platform%22%3A%7B%22brand%22%3A%22macOS%22%7D%2C%22browsers%22%3A%5B%7B%22brand%22%3A%22Chromium%22%2C%22version%22%3A%5B%22112%22%5D%7D%2C%7B%22brand%22%3A%22Google%20Chrome%22%2C%22version%22%3A%5B%22112%22%5D%7D%2C%7B%22brand%22%3A%22Not%3AA-Brand%22%2C%22version%22%3A%5B%2299%22%5D%7D%5D%2C%22mobile%22%3A0%7D&schain=%257B%2522ver%2522%253A%25221.0%2522%252C%2522complete%2522%253A1%252C%2522nodes%2522%253A%255B%257B%2522asi%2522%253A%2522indirectseller.com%2522%252C%2522sid%2522%253A%252200001%2522%252C%2522hp%2522%253A1%257D%255D%257D&imark=1&tp=https%3A%2F%2Fexample.com`, + banner: `posall=SSPLOC&id=58278&sdktype=0&hb=true&t=json3&sizes=300x250%2C320x100¤cy=JPY&pbver=${prebid.version}&sdkname=prebidjs&adapterver=1.6.2&imark=1&tp=https%3A%2F%2Fexample.com`, + bannerUSD: `posall=SSPLOC&id=58278&sdktype=0&hb=true&t=json3&sizes=300x250%2C320x100¤cy=USD&pbver=${prebid.version}&sdkname=prebidjs&adapterver=1.6.2&imark=1&tp=https%3A%2F%2Fexample.com`, + native: `posall=SSPLOC&id=58278&sdktype=0&hb=true&t=json3&sizes=1x1¤cy=JPY&pbver=${prebid.version}&sdkname=prebidjs&adapterver=1.6.2&tp=https%3A%2F%2Fexample.com`, + bannerWithHyperId: `posall=SSPLOC&id=58278&sdktype=0&hb=true&t=json3&sizes=320x100¤cy=JPY&pbver=${prebid.version}&sdkname=prebidjs&adapterver=1.6.2&imark=1&tp=https%3A%2F%2Fexample.com&hyper_id=novatiqId`, + bannerWithAdgextCriteoId: `posall=SSPLOC&id=58278&sdktype=0&hb=true&t=json3&sizes=320x100¤cy=JPY&pbver=${prebid.version}&sdkname=prebidjs&adapterver=1.6.2&adgext_criteo_id=criteo-id-test-1234567890&imark=1&tp=https%3A%2F%2Fexample.com`, + bannerWithAdgextIds: `posall=SSPLOC&id=58278&sdktype=0&hb=true&t=json3&sizes=320x100¤cy=JPY&pbver=${prebid.version}&sdkname=prebidjs&adapterver=1.6.2&adgext_id5_id=id5-id-test-1234567890&adgext_id5_id_link_type=2&adgext_imuid=i.KrAH6ZAZTJOnH5S4N2sogA&adgext_uid2=AgAAAAVacu1uAxgAxH%2BHJ8%2BnWlS2H4uVqr6i%2BHBDCNREHD8WKsio%2Fx7D8xXFuq1cJycUU86yXfTH9Xe%2F4C8KkH%2B7UCiU7uQxhyD7Qxnv251pEs6K8oK%2BBPLYR%2B8BLY%2FsJKesa%2FkoKwx1FHgUzIBum582tSy2Oo%2B7C6wYUaaV4QcLr%2F4LPA%3D&gpid=%2F1111%2Fhomepage%23300x250&uach=%7B%22source%22%3A2%2C%22platform%22%3A%7B%22brand%22%3A%22macOS%22%7D%2C%22browsers%22%3A%5B%7B%22brand%22%3A%22Chromium%22%2C%22version%22%3A%5B%22112%22%5D%7D%2C%7B%22brand%22%3A%22Google%20Chrome%22%2C%22version%22%3A%5B%22112%22%5D%7D%2C%7B%22brand%22%3A%22Not%3AA-Brand%22%2C%22version%22%3A%5B%2299%22%5D%7D%5D%2C%22mobile%22%3A0%7D&schain=%7B%22ver%22%3A%221.0%22%2C%22complete%22%3A1%2C%22nodes%22%3A%5B%7B%22asi%22%3A%22indirectseller.com%22%2C%22sid%22%3A%2200001%22%2C%22hp%22%3A1%7D%5D%7D&imark=1&tp=https%3A%2F%2Fexample.com`, }; it('sends bid request to ENDPOINT via GET', function () { const request = spec.buildRequests(bidRequests, bidderRequest)[0]; diff --git a/test/spec/modules/adkernelAdnBidAdapter_spec.js b/test/spec/modules/adkernelAdnBidAdapter_spec.js index ff7ed9f145d..cfee5693cf5 100644 --- a/test/spec/modules/adkernelAdnBidAdapter_spec.js +++ b/test/spec/modules/adkernelAdnBidAdapter_spec.js @@ -426,8 +426,7 @@ describe('AdkernelAdn adapter', function () { describe('adapter configuration', () => { it('should have aliases', () => { - expect(spec.aliases).to.have.lengthOf(1); - expect(spec.aliases[0]).to.be.equal('engagesimply'); + expect(spec.aliases).to.be.an('array'); }); }); }); diff --git a/test/spec/modules/adkernelBidAdapter_spec.js b/test/spec/modules/adkernelBidAdapter_spec.js index 45498d2734a..ade34478c20 100644 --- a/test/spec/modules/adkernelBidAdapter_spec.js +++ b/test/spec/modules/adkernelBidAdapter_spec.js @@ -15,11 +15,13 @@ describe('Adkernel adapter', function () { auctionId: 'auc-001', mediaTypes: { banner: { - sizes: [[300, 250], [300, 200]] + sizes: [[300, 250], [300, 200]], + pos: 1 } }, ortb2Imp: { - battr: [6, 7, 9] + battr: [6, 7, 9], + pos: 2 } }, bid2_zone2 = { bidder: 'adkernel', @@ -103,7 +105,11 @@ describe('Adkernel adapter', function () { video: { context: 'instream', playerSize: [[640, 480]], - api: [1, 2] + api: [1, 2], + placement: 1, + plcmt: 1, + skip: 1, + pos: 1 } }, adUnitCode: 'ad-unit-1' @@ -244,6 +250,31 @@ describe('Adkernel adapter', function () { }], bidid: 'pTuOlf5KHUo', cur: 'EUR' + }, + multiformat_response = { + id: '47ce4badcf7482', + seatbid: [{ + bid: [{ + id: 'sZSYq5zYMxo_0', + impid: 'Bid_01b__mf', + crid: '100_003', + price: 0.00145, + adid: '158801', + adm: '', + nurl: 'https://rtb.com/win?i=sZSYq5zYMxo_0&f=nurl', + cid: '16855' + }, { + id: 'sZSYq5zYMxo_1', + impid: 'Bid_01v__mf', + crid: '100_003', + price: 0.25, + adid: '158801', + nurl: 'https://rtb.com/win?i=sZSYq5zYMxo_1&f=nurl', + cid: '16855' + }] + }], + bidid: 'pTuOlf5KHUo', + cur: 'USD' }; var sandbox; @@ -346,6 +377,11 @@ describe('Adkernel adapter', function () { expect(bidRequest.imp[0].banner.battr).to.be.eql([6, 7, 9]); }); + it('should respect mediatypes attributes over FPD', function() { + expect(bidRequest.imp[0].banner).to.have.property('pos'); + expect(bidRequest.imp[0].banner.pos).to.be.eql(1); + }); + it('shouldn\'t contain gdpr nor ccpa information for default request', function () { let [_, bidRequests] = buildRequest([bid1_zone1]); expect(bidRequests[0]).to.not.have.property('regs'); @@ -354,11 +390,16 @@ describe('Adkernel adapter', function () { it('should contain gdpr-related information if consent is configured', function () { let [_, bidRequests] = buildRequest([bid1_zone1], - buildBidderRequest('https://example.com/index.html', - {gdprConsent: {gdprApplies: true, consentString: 'test-consent-string', vendorData: {}}, uspConsent: '1YNN'})); + buildBidderRequest('https://example.com/index.html', { + gdprConsent: {gdprApplies: true, consentString: 'test-consent-string', vendorData: {}}, + uspConsent: '1YNN', + gppConsent: {gppString: 'DBABMA~CPXxRfAPXxRfAAfKABENB-CgAAAAAAAAAAYgAAAAAAAA', applicableSections: [2]}} + )); let bidRequest = bidRequests[0]; expect(bidRequest).to.have.property('regs'); expect(bidRequest.regs.ext).to.be.eql({'gdpr': 1, 'us_privacy': '1YNN'}); + expect(bidRequest.regs.gpp).to.be.eql('DBABMA~CPXxRfAPXxRfAAfKABENB-CgAAAAAAAAAAYgAAAAAAAA'); + expect(bidRequest.regs.gpp_sid).to.be.eql([2]); expect(bidRequest).to.have.property('user'); expect(bidRequest.user.ext).to.be.eql({'consent': 'test-consent-string'}); }); @@ -433,24 +474,40 @@ describe('Adkernel adapter', function () { }); it('should have openrtb video impression parameters', function() { - expect(bidRequests[0].imp[0].video).to.have.property('api'); - expect(bidRequests[0].imp[0].video.api).to.be.eql([1, 2]); + let video = bidRequests[0].imp[0].video; + expect(video).to.have.property('api'); + expect(video.api).to.be.eql([1, 2]); + expect(video.placement).to.be.eql(1); + expect(video.plcmt).to.be.eql(1); + expect(video.skip).to.be.eql(1); + expect(video.pos).to.be.eql(1); }); }); describe('multiformat request building', function () { - let _, bidRequests; + let pbRequests, bidRequests; before(function () { - [_, bidRequests] = buildRequest([bid_multiformat]); + [pbRequests, bidRequests] = buildRequest([bid_multiformat]); }); it('should contain single request', function () { expect(bidRequests).to.have.length(1); - expect(bidRequests[0].imp).to.have.length(1); }); - it('should contain banner-only impression', function () { - expect(bidRequests[0].imp).to.have.length(1); + it('should contain both impression', function () { + expect(bidRequests[0].imp).to.have.length(2); expect(bidRequests[0].imp[0]).to.have.property('banner'); - expect(bidRequests[0].imp[0]).to.not.have.property('video'); + expect(bidRequests[0].imp[1]).to.have.property('video'); + // check that splitted imps do not share same impid + expect(bidRequests[0].imp[0].id).to.be.not.eql('Bid_01'); + expect(bidRequests[0].imp[1].id).to.be.not.eql('Bid_01'); + expect(bidRequests[0].imp[1].id).to.be.not.eql(bidRequests[0].imp[0].id); + }); + it('x', function() { + let bids = spec.interpretResponse({body: multiformat_response}, pbRequests[0]); + expect(bids).to.have.length(2); + expect(bids[0].requestId).to.be.eql('Bid_01'); + expect(bids[0].mediaType).to.be.eql('banner'); + expect(bids[1].requestId).to.be.eql('Bid_01'); + expect(bids[1].mediaType).to.be.eql('video'); }); }); diff --git a/test/spec/modules/adlooxAdServerVideo_spec.js b/test/spec/modules/adlooxAdServerVideo_spec.js index a071c6bbe3f..58277bc830d 100644 --- a/test/spec/modules/adlooxAdServerVideo_spec.js +++ b/test/spec/modules/adlooxAdServerVideo_spec.js @@ -1,11 +1,11 @@ import adapterManager from 'src/adapterManager.js'; import analyticsAdapter from 'modules/adlooxAnalyticsAdapter.js'; -import { ajax } from 'src/ajax.js'; import { buildVideoUrl } from 'modules/adlooxAdServerVideo.js'; import { expect } from 'chai'; import * as events from 'src/events.js'; import { targeting } from 'src/targeting.js'; import * as utils from 'src/utils.js'; +import {server} from '../../mocks/xhr.js'; const analyticsAdapterName = 'adloox'; @@ -199,11 +199,9 @@ describe('Adloox Ad Server Video', function () { }); describe('process VAST', function () { - let server = null; let BID = null; let getWinningBidsStub; beforeEach(function () { - server = sinon.createFakeServer(); BID = utils.deepClone(bid); getWinningBidsStub = sinon.stub(targeting, 'getWinningBids') getWinningBidsStub.withArgs(adUnit.code).returns([ BID ]); @@ -212,8 +210,6 @@ describe('Adloox Ad Server Video', function () { getWinningBidsStub.restore(); getWinningBidsStub = undefined; BID = null; - server.restore(); - server = null; }); it('should return URL unchanged for non-VAST', function (done) { diff --git a/test/spec/modules/adlooxRtdProvider_spec.js b/test/spec/modules/adlooxRtdProvider_spec.js index 5b99789981f..0e26ef1afdb 100644 --- a/test/spec/modules/adlooxRtdProvider_spec.js +++ b/test/spec/modules/adlooxRtdProvider_spec.js @@ -1,12 +1,12 @@ import adapterManager from 'src/adapterManager.js'; import analyticsAdapter from 'modules/adlooxAnalyticsAdapter.js'; import {auctionManager} from 'src/auctionManager.js'; -import { config as _config } from 'src/config.js'; import { expect } from 'chai'; import * as events from 'src/events.js'; import * as prebidGlobal from 'src/prebidGlobal.js'; import { subModuleObj as rtdProvider } from 'modules/adlooxRtdProvider.js'; import * as utils from 'src/utils.js'; +import {server} from '../../mocks/xhr.js'; const analyticsAdapterName = 'adloox'; @@ -139,16 +139,12 @@ describe('Adloox RTD Provider', function () { expect(analyticsAdapter.context).is.null; }); - let server = null; let CONFIG = null; beforeEach(function () { - server = sinon.createFakeServer(); CONFIG = utils.deepClone(config); }); afterEach(function () { CONFIG = null; - server.restore(); - server = null; }); it('should fetch segments', function (done) { diff --git a/test/spec/modules/admanBidAdapter_spec.js b/test/spec/modules/admanBidAdapter_spec.js index f1d9a7047d3..a9413860072 100644 --- a/test/spec/modules/admanBidAdapter_spec.js +++ b/test/spec/modules/admanBidAdapter_spec.js @@ -106,8 +106,11 @@ describe('AdmanAdapter', function () { let placements = data['placements']; for (let i = 0; i < placements.length; i++) { let placement = placements[i]; - expect(placement).to.have.all.keys('placementId', 'eids', 'bidId', 'traffic', 'sizes', 'schain', 'bidFloor'); + expect(placement).to.have.all.keys('placementId', 'eids', 'bidId', 'traffic', 'sizes', 'schain', 'bidFloor', 'ext'); expect(placement.schain).to.be.an('object') + expect(placement.ext).to.be.an('object') + expect(placement.ext).to.have.key('tid') + expect(placement.ext.tid).to.equal(bidBanner.transactionId); expect(placement.placementId).to.be.a('number'); expect(placement.bidId).to.be.a('string'); expect(placement.traffic).to.be.a('string'); @@ -132,7 +135,10 @@ describe('AdmanAdapter', function () { let placement = placements[i]; expect(placement).to.have.all.keys('placementId', 'eids', 'bidId', 'traffic', 'schain', 'bidFloor', 'playerSize', 'minduration', 'maxduration', 'mimes', 'protocols', 'startdelay', 'placement', 'skip', - 'skipafter', 'minbitrate', 'maxbitrate', 'delivery', 'playbackmethod', 'api', 'linearity'); + 'skipafter', 'minbitrate', 'maxbitrate', 'delivery', 'playbackmethod', 'api', 'linearity', 'ext'); + expect(placement.ext).to.be.an('object') + expect(placement.ext).to.have.key('tid') + expect(placement.ext.tid).to.equal(bidBanner.transactionId); expect(placement.schain).to.be.an('object') expect(placement.placementId).to.be.a('number'); expect(placement.bidId).to.be.a('string'); diff --git a/test/spec/modules/admaticBidAdapter_spec.js b/test/spec/modules/admaticBidAdapter_spec.js index 1d2fb1e79cb..b4d84634962 100644 --- a/test/spec/modules/admaticBidAdapter_spec.js +++ b/test/spec/modules/admaticBidAdapter_spec.js @@ -1,12 +1,553 @@ -import {expect} from 'chai'; -import {spec, storage} from 'modules/admaticBidAdapter.js'; -import {newBidder} from 'src/adapters/bidderFactory.js'; -import {getStorageManager} from 'src/storageManager'; +import { expect } from 'chai'; +import { spec } from 'modules/admaticBidAdapter.js'; +import { newBidder } from 'src/adapters/bidderFactory.js'; +import { config } from 'src/config.js'; const ENDPOINT = 'https://layer.serve.admatic.com.tr/pb'; describe('admaticBidAdapter', () => { const adapter = newBidder(spec); + let validRequest = [ { + 'refererInfo': { + 'page': 'https://www.admatic.com.tr', + 'domain': 'https://www.admatic.com.tr', + }, + 'bidder': 'admatic', + 'params': { + 'networkId': 10433394, + 'host': 'layer.serve.admatic.com.tr' + }, + 'ortb2Imp': { 'ext': { 'instl': 1 } }, + 'ortb2': { 'badv': ['admatic.com.tr'] }, + 'mediaTypes': { + 'banner': { + 'sizes': [[300, 250], [728, 90]] + }, + 'native': { + }, + 'video': { + } + }, + getFloor: inputParams => { + if (inputParams.mediaType === BANNER && inputParams.size[0] === 300 && inputParams.size[1] === 250) { + return { + currency: 'USD', + floor: 1.0 + }; + } else if (inputParams.mediaType === BANNER && inputParams.size[0] === 728 && inputParams.size[1] === 90) { + return { + currency: 'USD', + floor: 2.0 + }; + } else if (inputParams.mediaType === VIDEO) { + return { + currency: 'USD', + floor: 1.0 + }; + } else if (inputParams.mediaType === NATIVE) { + return { + currency: 'USD', + floor: 1.0 + }; + } else { + return {} + } + }, + 'schain': { + 'ver': '1.0', + 'complete': 1, + 'nodes': [ + { + 'asi': 'pixad.com.tr', + 'sid': 'px-pub-3000856707', + 'hp': 1 + } + ] + }, + 'at': 1, + 'tmax': 1000, + 'user': { + 'ext': { + 'eids': [ + { + 'source': 'id5-sync.com', + 'uids': [ + { + 'id': '0', + 'atype': 1, + 'ext': { + 'linkType': 0, + 'pba': 'wMh3sAXcnhDq7CfSa6ji1g==' + } + } + ] + }, + { + 'source': 'pubcid.org', + 'uids': [ + { + 'id': '5a49273f-a424-454b-b478-169c3551aa72', + 'atype': 1 + } + ] + } + ] + } + }, + 'ortb': { + 'badv': [], + 'bcat': [], + 'site': { + 'page': 'http://localhost:8888/admatic.html', + 'ref': 'http://localhost:8888', + 'publisher': { + 'name': 'localhost' + } + }, + 'device': { + 'ua': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36' + } + }, + 'site': { + 'page': 'http://localhost:8888/admatic.html', + 'ref': 'http://localhost:8888', + 'publisher': { + 'name': 'localhost', + 'publisherId': 12321312 + } + }, + 'imp': [ + { + 'size': [ + { + 'w': 300, + 'h': 250 + }, + { + 'w': 728, + 'h': 90 + } + ], + 'mediatype': {}, + 'type': 'banner', + 'id': '2205da7a81846b', + 'floors': { + 'banner': { + '300x250': { 'currency': 'USD', 'floor': 1 }, + '728x90': { 'currency': 'USD', 'floor': 2 } + } + } + }, + { + 'size': [ + { + 'w': 338, + 'h': 280 + } + ], + 'type': 'video', + 'mediatype': { + 'context': 'instream', + 'mimes': [ + 'video/mp4' + ], + 'maxduration': 240, + 'api': [ + 1, + 2 + ], + 'playerSize': [ + [ + 338, + 280 + ] + ], + 'protocols': [ + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8 + ], + 'skip': 1, + 'playbackmethod': [ + 2 + ], + 'linearity': 1, + 'placement': 2 + }, + 'floors': { + 'video': { + '338x280': { 'currency': 'USD', 'floor': 1 } + } + }, + 'id': '45e86fc7ce7fc93' + }, + { + 'size': [ + { + 'w': 1, + 'h': 1 + } + ], + 'type': 'native', + 'mediatype': { + 'title': { + 'required': true, + 'len': 120 + }, + 'image': { + 'required': true + }, + 'icon': { + 'required': false, + 'sizes': [ + 640, + 480 + ] + }, + 'sponsoredBy': { + 'required': false + }, + 'body': { + 'required': false + }, + 'clickUrl': { + 'required': false + }, + 'displayUrl': { + 'required': false + } + }, + 'ext': { + 'instl': 0, + 'gpid': 'native-INS_b1b1269f-9570-fe3c-9bf4-f187827ec94a', + 'data': { + 'pbadslot': 'native-INS_b1b1269f-9570-fe3c-9bf4-f187827ec94a' + } + }, + 'floors': { + 'native': { + '*': { 'currency': 'USD', 'floor': 1 } + } + }, + 'id': '16e0c8982318f91' + } + ], + 'ext': { + 'cur': 'USD', + 'bidder': 'admatic' + } + } ]; + let bidderRequest = { + 'refererInfo': { + 'page': 'https://www.admatic.com.tr', + 'domain': 'https://www.admatic.com.tr', + }, + 'bidder': 'admatic', + 'params': { + 'networkId': 10433394, + 'host': 'layer.serve.admatic.com.tr' + }, + 'ortb2Imp': { 'ext': { 'instl': 1 } }, + 'ortb2': { 'badv': ['admatic.com.tr'] }, + 'mediaTypes': { + 'banner': { + 'sizes': [[300, 250], [728, 90]] + }, + 'native': { + }, + 'video': { + 'playerSize': [ + 336, + 280 + ] + } + }, + 'userId': { + 'id5id': { + 'uid': '0', + 'ext': { + 'linkType': 0, + 'pba': 'wMh3sAXcnhDq7CfSa6ji1g==' + } + }, + 'pubcid': '5a49273f-a424-454b-b478-169c3551aa72' + }, + 'userIdAsEids': [ + { + 'source': 'id5-sync.com', + 'uids': [ + { + 'id': '0', + 'atype': 1, + 'ext': { + 'linkType': 0, + 'pba': 'wMh3sAXcnhDq7CfSa6ji1g==' + } + } + ] + }, + { + 'source': 'pubcid.org', + 'uids': [ + { + 'id': '5a49273f-a424-454b-b478-169c3551aa72', + 'atype': 1 + } + ] + } + ], + getFloor: inputParams => { + if (inputParams.mediaType === BANNER && inputParams.size[0] === 300 && inputParams.size[1] === 250) { + return { + currency: 'USD', + floor: 1.0 + }; + } else if (inputParams.mediaType === BANNER && inputParams.size[0] === 728 && inputParams.size[1] === 90) { + return { + currency: 'USD', + floor: 2.0 + }; + } else if (inputParams.mediaType === VIDEO) { + return { + currency: 'USD', + floor: 1.0 + }; + } else if (inputParams.mediaType === NATIVE) { + return { + currency: 'USD', + floor: 1.0 + }; + } else { + return {} + } + }, + 'schain': { + 'ver': '1.0', + 'complete': 1, + 'nodes': [ + { + 'asi': 'pixad.com.tr', + 'sid': 'px-pub-3000856707', + 'hp': 1 + } + ] + }, + 'at': 1, + 'tmax': 1000, + 'user': { + 'ext': { + 'eids': [ + { + 'source': 'id5-sync.com', + 'uids': [ + { + 'id': '0', + 'atype': 1, + 'ext': { + 'linkType': 0, + 'pba': 'wMh3sAXcnhDq7CfSa6ji1g==' + } + } + ] + }, + { + 'source': 'pubcid.org', + 'uids': [ + { + 'id': '5a49273f-a424-454b-b478-169c3551aa72', + 'atype': 1 + } + ] + } + ] + } + }, + 'ortb': { + 'source': {}, + 'site': { + 'domain': 'localhost:8888', + 'publisher': { + 'domain': 'localhost:8888' + }, + 'page': 'http://localhost:8888/', + 'name': 'http://localhost:8888' + }, + 'badv': [], + 'bcat': [], + 'device': { + 'w': 896, + 'h': 979, + 'dnt': 0, + 'ua': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36', + 'language': 'tr', + 'sua': { + 'source': 1, + 'platform': { + 'brand': 'macOS' + }, + 'browsers': [ + { + 'brand': 'Google Chrome', + 'version': [ + '119' + ] + }, + { + 'brand': 'Chromium', + 'version': [ + '119' + ] + }, + { + 'brand': 'Not?A_Brand', + 'version': [ + '24' + ] + } + ], + 'mobile': 0 + } + } + }, + 'site': { + 'page': 'http://localhost:8888/admatic.html', + 'ref': 'http://localhost:8888', + 'publisher': { + 'name': 'localhost', + 'publisherId': 12321312 + } + }, + 'imp': [ + { + 'size': [ + { + 'w': 300, + 'h': 250 + }, + { + 'w': 728, + 'h': 90 + } + ], + 'id': '2205da7a81846b', + 'mediatype': {}, + 'type': 'banner', + 'floors': { + 'banner': { + '300x250': { 'currency': 'USD', 'floor': 1 }, + '728x90': { 'currency': 'USD', 'floor': 2 } + } + } + }, + { + 'size': [ + { + 'w': 338, + 'h': 280 + } + ], + 'type': 'video', + 'mediatype': { + 'context': 'instream', + 'mimes': [ + 'video/mp4' + ], + 'maxduration': 240, + 'api': [ + 1, + 2 + ], + 'playerSize': [ + [ + 338, + 280 + ] + ], + 'protocols': [ + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8 + ], + 'skip': 1, + 'playbackmethod': [ + 2 + ], + 'linearity': 1, + 'placement': 2 + }, + 'floors': { + 'video': { + '338x280': { 'currency': 'USD', 'floor': 1 } + } + }, + 'id': '45e86fc7ce7fc93' + }, + { + 'size': [ + { + 'w': 1, + 'h': 1 + } + ], + 'type': 'native', + 'mediatype': { + 'title': { + 'required': true, + 'len': 120 + }, + 'image': { + 'required': true + }, + 'icon': { + 'required': false, + 'sizes': [ + 640, + 480 + ] + }, + 'sponsoredBy': { + 'required': false + }, + 'body': { + 'required': false + }, + 'clickUrl': { + 'required': false + }, + 'displayUrl': { + 'required': false + } + }, + 'ext': { + 'instl': 0, + 'gpid': 'native-INS_b1b1269f-9570-fe3c-9bf4-f187827ec94a', + 'data': { + 'pbadslot': 'native-INS_b1b1269f-9570-fe3c-9bf4-f187827ec94a' + } + }, + 'floors': { + 'native': { + '*': { 'currency': 'USD', 'floor': 1 } + } + }, + 'id': '16e0c8982318f91' + } + ], + 'ext': { + 'cur': 'USD', + 'bidder': 'admatic' + } + }; describe('inherited functions', () => { it('exists and is a function', () => { @@ -16,6 +557,10 @@ describe('admaticBidAdapter', () => { describe('isBidRequestValid', function() { let bid = { + 'refererInfo': { + 'page': 'https://www.admatic.com.tr', + 'domain': 'https://www.admatic.com.tr', + }, 'bidder': 'admatic', 'params': { 'networkId': 10433394, @@ -28,6 +573,7 @@ describe('admaticBidAdapter', () => { 'bidderRequestId': '22edbae2733bf6', 'auctionId': '1d1a030790a475', 'creativeId': 'er2ee', + 'ortb2Imp': { 'ext': { 'instl': 1 } }, 'ortb2': { 'badv': ['admatic.com.tr'] } }; @@ -46,252 +592,147 @@ describe('admaticBidAdapter', () => { describe('buildRequests', function () { it('sends bid request to ENDPOINT via POST', function () { - let validRequest = [ { - 'bidder': 'admatic', - 'params': { - 'networkId': 10433394, - 'host': 'layer.serve.admatic.com.tr' - }, - 'ortb2': { 'badv': ['admatic.com.tr'] }, - 'mediaTypes': { - 'banner': { - 'sizes': [[300, 250], [728, 90]] - } - }, - getFloor: inputParams => { - if (inputParams.mediaType === BANNER && inputParams.size[0] === 300 && inputParams.size[1] === 250) { - return { - currency: 'USD', - floor: 1.0 - }; - } else if (inputParams.mediaType === BANNER && inputParams.size[0] === 728 && inputParams.size[1] === 90) { - return { - currency: 'USD', - floor: 2.0 - }; - } else { - return {} - } - }, - 'user': { - 'ua': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/107.0.0.0 Safari/537.36' - }, - 'blacklist': [], - 'site': { - 'page': 'http://localhost:8888/admatic.html', - 'ref': 'http://localhost:8888', - 'publisher': { - 'name': 'localhost', - 'publisherId': 12321312 + const request = spec.buildRequests(validRequest, bidderRequest); + expect(request.url).to.equal(ENDPOINT); + expect(request.method).to.equal('POST'); + }); + + it('should not populate GDPR if for non-EEA users', function () { + let bidRequest = Object.assign([], validRequest); + const request = spec.buildRequests( + bidRequest, + Object.assign({}, bidderRequest, { + gdprConsent: { + gdprApplies: true, + consentString: 'BOJ8RZsOJ8RZsABAB8AAAAAZ+A==' } - }, - 'imp': [ - { - 'size': [ - { - 'w': 300, - 'h': 250 - }, - { - 'w': 728, - 'h': 90 - } - ], - 'mediatype': {}, - 'type': 'banner', - 'id': '2205da7a81846b', - 'floors': { - 'banner': { - '300x250': { 'currency': 'USD', 'floor': 1 }, - '728x90': { 'currency': 'USD', 'floor': 2 } - } - } - }, - { - 'size': [ - { - 'w': 338, - 'h': 280 - } - ], - 'type': 'video', - 'mediatype': { - 'context': 'instream', - 'mimes': [ - 'video/mp4' - ], - 'maxduration': 240, - 'api': [ - 1, - 2 - ], - 'playerSize': [ - [ - 338, - 280 - ] - ], - 'protocols': [ - 1, - 2, - 3, - 4, - 5, - 6, - 7, - 8 - ], - 'skip': 1, - 'playbackmethod': [ - 2 - ], - 'linearity': 1, - 'placement': 2 - }, - 'id': '45e86fc7ce7fc93' + }) + ); + expect(request.data.regs.ext.gdpr).to.equal(1); + expect(request.data.regs.ext.consent).to.equal('BOJ8RZsOJ8RZsABAB8AAAAAZ-A'); + }); + + it('should populate GDPR and empty consent string if available for EEA users without consent string but with consent', function () { + let bidRequest = Object.assign([], validRequest); + const request = spec.buildRequests( + bidRequest, + Object.assign({}, bidderRequest, { + gdprConsent: { + gdprApplies: true } - ], - 'ext': { - 'cur': 'USD', - 'bidder': 'admatic' + }) + ); + expect(request.data.regs.ext.gdpr).to.equal(1); + expect(request.data.regs.ext.consent).to.equal(''); + }); + + it('should properly build a request when coppa flag is true', function () { + let bidRequest = Object.assign([], validRequest); + const request = spec.buildRequests( + bidRequest, + Object.assign({}, bidderRequest, { + coppa: true + }) + ); + expect(request.data.regs.ext.coppa).to.not.be.undefined; + expect(request.data.regs.ext.coppa).to.equal(1); + }); + + it('should properly build a request with gpp consent field', function () { + let bidRequest = Object.assign([], validRequest); + const ortb2 = { + regs: { + gpp: 'gpp_consent_string', + gpp_sid: [0, 1, 2] } - } ]; - let bidderRequest = { - 'bidder': 'admatic', - 'params': { - 'networkId': 10433394, - 'host': 'layer.serve.admatic.com.tr' - }, - 'ortb2': { 'badv': ['admatic.com.tr'] }, - 'mediaTypes': { - 'banner': { - 'sizes': [[300, 250], [728, 90]] - } - }, - getFloor: inputParams => { - if (inputParams.mediaType === BANNER && inputParams.size[0] === 300 && inputParams.size[1] === 250) { - return { - currency: 'USD', - floor: 1.0 - }; - } else if (inputParams.mediaType === BANNER && inputParams.size[0] === 728 && inputParams.size[1] === 90) { - return { - currency: 'USD', - floor: 2.0 - }; - } else { - return {} - } - }, - 'user': { - 'ua': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/107.0.0.0 Safari/537.36' - }, - 'blacklist': [], - 'site': { - 'page': 'http://localhost:8888/admatic.html', - 'ref': 'http://localhost:8888', - 'publisher': { - 'name': 'localhost', - 'publisherId': 12321312 - } - }, - 'imp': [ - { - 'size': [ - { - 'w': 300, - 'h': 250 - }, - { - 'w': 728, - 'h': 90 - } - ], - 'id': '2205da7a81846b', - 'mediatype': {}, - 'type': 'banner', - 'floors': { - 'banner': { - '300x250': { 'currency': 'USD', 'floor': 1 }, - '728x90': { 'currency': 'USD', 'floor': 2 } - } + }; + const request = spec.buildRequests(bidRequest, { ...bidderRequest, ortb2 }); + expect(request.data.regs.ext.gpp).to.equal('gpp_consent_string'); + expect(request.data.regs.ext.gpp_sid).to.deep.equal([0, 1, 2]); + }); + + it('should properly build a request with ccpa consent field', function () { + let bidRequest = Object.assign([], validRequest); + const request = spec.buildRequests( + bidRequest, + Object.assign({}, bidderRequest, { + uspConsent: '1---' + }) + ); + expect(request.data.regs.ext.uspIab).to.not.be.null; + expect(request.data.regs.ext.uspIab).to.equal('1---'); + }); + + it('should properly forward eids', function () { + const bidRequests = [ + { + bidder: 'admatic', + adUnitCode: 'bid-123', + transactionId: 'transaction-123', + mediaTypes: { + banner: { + sizes: [[728, 90]] } }, - { - 'size': [ - { - 'w': 338, - 'h': 280 - } - ], - 'type': 'video', - 'mediatype': { - 'context': 'instream', - 'mimes': [ - 'video/mp4' - ], - 'maxduration': 240, - 'api': [ - 1, - 2 - ], - 'playerSize': [ - [ - 338, - 280 - ] - ], - 'protocols': [ - 1, - 2, - 3, - 4, - 5, - 6, - 7, - 8 - ], - 'skip': 1, - 'playbackmethod': [ - 2 - ], - 'linearity': 1, - 'placement': 2 - }, - 'id': '45e86fc7ce7fc93' - } - ], - 'ext': { - 'cur': 'USD', - 'bidder': 'admatic' + userIdAsEids: [ + { + source: 'admatic.com.tr', + uids: [{ + id: 'abc', + atype: 1 + }] + } + ], + params: {} + }, + ]; + const request = spec.buildRequests(bidRequests, bidderRequest); + const ortbRequest = request.data; + expect(ortbRequest.user.ext.eids).to.deep.equal([ + { + source: 'admatic.com.tr', + uids: [{ + id: 'abc', + atype: 1 + }] } - }; - const request = spec.buildRequests(validRequest, bidderRequest); - expect(request.url).to.equal(ENDPOINT); - expect(request.method).to.equal('POST'); + ]); }); it('should properly build a banner request with floors', function () { - let bidRequests = [ + const request = spec.buildRequests(validRequest, bidderRequest); + request.data.imp[0].floors = { + 'banner': { + '300x250': { 'currency': 'USD', 'floor': 1 }, + '728x90': { 'currency': 'USD', 'floor': 2 } + } + }; + }); + + it('should properly build a video request with several player sizes with floors', function () { + const bidRequests = [ { 'bidder': 'admatic', - 'params': { - 'networkId': 10433394, - 'host': 'layer.serve.admatic.com.tr' - }, + 'adUnitCode': 'bid-123', + 'transactionId': 'transaction-123', 'mediaTypes': { - 'banner': { - 'sizes': [[300, 250], [728, 90]] + 'video': { + 'playerSize': [[300, 250], [728, 90]] } }, + 'ortb2Imp': { 'ext': { 'instl': 1 } }, 'ortb2': { 'badv': ['admatic.com.tr'] }, + 'params': { + 'networkId': 10433394, + 'host': 'layer.serve.admatic.com.tr' + }, getFloor: inputParams => { - if (inputParams.mediaType === BANNER && inputParams.size[0] === 300 && inputParams.size[1] === 250) { + if (inputParams.mediaType === VIDEO && inputParams.size[0] === 300 && inputParams.size[1] === 250) { return { currency: 'USD', floor: 1.0 }; - } else if (inputParams.mediaType === BANNER && inputParams.size[0] === 728 && inputParams.size[1] === 90) { + } else if (inputParams.mediaType === VIDEO && inputParams.size[0] === 728 && inputParams.size[1] === 90) { return { currency: 'USD', floor: 2.0 @@ -302,32 +743,50 @@ describe('admaticBidAdapter', () => { } }, ]; - let bidderRequest = { - 'bidder': 'admatic', - 'params': { - 'networkId': 10433394, - 'host': 'layer.serve.admatic.com.tr' - }, - 'ortb2': { 'badv': ['admatic.com.tr'] }, - 'adUnitCode': 'adunit-code', - 'sizes': [[300, 250], [728, 90]], - 'bidId': '30b31c1838de1e', - 'bidderRequestId': '22edbae2733bf6', - 'auctionId': '1d1a030790a475', - 'creativeId': 'er2ee', - 'mediaTypes': { - 'banner': { - 'sizes': [[300, 250], [728, 90]] - } + const bidderRequest = { + 'refererInfo': { + 'page': 'https://www.admatic.com.tr', + 'domain': 'https://www.admatic.com.tr', } }; const request = spec.buildRequests(bidRequests, bidderRequest); - request.data.imp[0].floors = { - 'banner': { - '300x250': { 'currency': 'USD', 'floor': 1 }, - '728x90': { 'currency': 'USD', 'floor': 2 } + }); + + it('should properly build a native request with floors', function () { + const bidRequests = [ + { + 'bidder': 'admatic', + 'adUnitCode': 'bid-123', + 'transactionId': 'transaction-123', + 'mediaTypes': { + 'native': { + } + }, + 'ortb2Imp': { 'ext': { 'instl': 1 } }, + 'ortb2': { 'badv': ['admatic.com.tr'] }, + 'params': { + 'networkId': 10433394, + 'host': 'layer.serve.admatic.com.tr' + }, + getFloor: inputParams => { + if (inputParams.mediaType === NATIVE) { + return { + currency: 'USD', + floor: 1.0 + }; + } else { + return {} + } + } + }, + ]; + const bidderRequest = { + 'refererInfo': { + 'page': 'https://www.admatic.com.tr', + 'domain': 'https://www.admatic.com.tr', } }; + const request = spec.buildRequests(bidRequests, bidderRequest); }); }); @@ -343,6 +802,7 @@ describe('admaticBidAdapter', () => { 'price': 0.01, 'type': 'banner', 'bidder': 'admatic', + 'mime_type': 'iframe', 'adomain': ['admatic.com.tr'], 'party_tag': '
', 'iurl': 'https://www.admatic.com.tr' @@ -354,6 +814,7 @@ describe('admaticBidAdapter', () => { 'height': 250, 'price': 0.01, 'type': 'video', + 'mime_type': 'iframe', 'bidder': 'admatic', 'adomain': ['admatic.com.tr'], 'party_tag': '', @@ -366,10 +827,24 @@ describe('admaticBidAdapter', () => { 'height': 250, 'price': 0.01, 'type': 'video', + 'mime_type': 'iframe', 'bidder': 'admatic', 'adomain': ['admatic.com.tr'], 'party_tag': 'https://www.admatic.com.tr', 'iurl': 'https://www.admatic.com.tr' + }, + { + 'id': 4, + 'creative_id': '3742', + 'width': 1, + 'height': 1, + 'price': 0.01, + 'type': 'native', + 'mime_type': 'iframe', + 'bidder': 'admatic', + 'adomain': ['admatic.com.tr'], + 'party_tag': '{"native":{"ver":"1.1","assets":[{"id":1,"title":{"text":"title"}},{"id":4,"data":{"value":"body"}},{"id":5,"data":{"value":"sponsored"}},{"id":6,"data":{"value":"cta"}},{"id":2,"img":{"url":"https://www.admatic.com.tr","w":1200,"h":628}},{"id":3,"img":{"url":"https://www.admatic.com.tr","w":640,"h":480}}],"link":{"url":"https://www.admatic.com.tr"},"imptrackers":["https://www.admatic.com.tr"]}}', + 'iurl': 'https://www.admatic.com.tr' } ], 'queryId': 'cdnbh24rlv0hhkpfpln0', @@ -388,6 +863,7 @@ describe('admaticBidAdapter', () => { ad: '
', creativeId: '374', meta: { + model: 'iframe', advertiserDomains: ['admatic.com.tr'] }, ttl: 60, @@ -405,6 +881,7 @@ describe('admaticBidAdapter', () => { vastXml: '', creativeId: '3741', meta: { + model: 'iframe', advertiserDomains: ['admatic.com.tr'] }, ttl: 60, @@ -422,6 +899,41 @@ describe('admaticBidAdapter', () => { vastXml: 'https://www.admatic.com.tr', creativeId: '3741', meta: { + model: 'iframe', + advertiserDomains: ['admatic.com.tr'] + }, + ttl: 60, + bidder: 'admatic' + }, + { + requestId: 4, + cpm: 0.01, + width: 1, + height: 1, + currency: 'TRY', + mediaType: 'native', + netRevenue: true, + native: { + 'clickUrl': 'https://www.admatic.com.tr', + 'impressionTrackers': ['https://www.admatic.com.tr'], + 'title': 'title', + 'body': 'body', + 'sponsoredBy': 'sponsored', + 'cta': 'cta', + 'image': { + 'url': 'https://www.admatic.com.tr', + 'width': 1200, + 'height': 628 + }, + 'icon': { + 'url': 'https://www.admatic.com.tr', + 'width': 640, + 'height': 480 + } + }, + creativeId: '3742', + meta: { + model: 'iframe', advertiserDomains: ['admatic.com.tr'] }, ttl: 60, diff --git a/test/spec/modules/admixerBidAdapter_spec.js b/test/spec/modules/admixerBidAdapter_spec.js index 8cf433460b7..85538efc957 100644 --- a/test/spec/modules/admixerBidAdapter_spec.js +++ b/test/spec/modules/admixerBidAdapter_spec.js @@ -4,11 +4,12 @@ import {newBidder} from 'src/adapters/bidderFactory.js'; import {config} from '../../../src/config.js'; const BIDDER_CODE = 'admixer'; -const BIDDER_CODE_ADX = 'admixeradx'; +const WL_BIDDER_CODE = 'admixerwl' 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'; +const CLIENT_ID = 5124; +const ENDPOINT_ID = 81264; describe('AdmixerAdapter', function () { const adapter = newBidder(spec); @@ -36,9 +37,28 @@ describe('AdmixerAdapter', function () { auctionId: '1d1a030790a475', }; + let wlBid = { + bidder: WL_BIDDER_CODE, + params: { + clientId: CLIENT_ID, + endpointId: ENDPOINT_ID, + }, + adUnitCode: 'adunit-code', + sizes: [ + [300, 250], + [300, 600], + ], + 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 true when params required by WL found', function () { + expect(spec.isBidRequestValid(wlBid)).to.equal(true); + }); it('should return false when required params are not passed', function () { let bid = Object.assign({}, bid); @@ -48,6 +68,14 @@ describe('AdmixerAdapter', function () { }; expect(spec.isBidRequestValid(bid)).to.equal(false); }); + it('should return false when params required by WL are not passed', function () { + let wlBid = Object.assign({}, wlBid); + delete wlBid.params; + wlBid.params = { + clientId: 0, + }; + expect(spec.isBidRequestValid(wlBid)).to.equal(false); + }); }); describe('buildRequests', function () { @@ -105,7 +133,10 @@ describe('AdmixerAdapter', function () { validRequest: [ { bidder: bidder, - params: { + params: bidder === 'admixerwl' ? { + clientId: CLIENT_ID, + endpointId: ENDPOINT_ID + } : { zone: ZONE_ID, }, adUnitCode: 'adunit-code', @@ -168,6 +199,12 @@ describe('AdmixerAdapter', function () { expect(request.url).to.equal('https://inv-nets.admixer.net/adxprebid.1.2.aspx'); expect(request.method).to.equal('POST'); }); + it('build request for admixerwl', function () { + const requestParams = requestParamsFor('admixerwl'); + const request = spec.buildRequests(requestParams.validRequest, requestParams.bidderRequest); + expect(request.url).to.equal(`https://inv-nets-adxwl.admixer.com/adxwlprebid.aspx?client=${CLIENT_ID}`); + expect(request.method).to.equal('POST'); + }); }); describe('checkFloorGetting', function () { diff --git a/test/spec/modules/adnuntiusBidAdapter_spec.js b/test/spec/modules/adnuntiusBidAdapter_spec.js index b1657ed1596..e109ca1829c 100644 --- a/test/spec/modules/adnuntiusBidAdapter_spec.js +++ b/test/spec/modules/adnuntiusBidAdapter_spec.js @@ -10,9 +10,10 @@ import {getGlobal} from '../../../src/prebidGlobal'; describe('adnuntiusBidAdapter', function() { const URL = 'https://ads.adnuntius.delivery/i?tzo='; const EURO_URL = 'https://europe.delivery.adnuntius.com/i?tzo='; - const GVLID = 855; const usi = utils.generateUUID() - const meta = [{ key: 'usi', value: usi }] + + const meta = [{key: 'valueless'}, {value: 'keyless'}, {key: 'voidAuIds'}, {key: 'voidAuIds', value: [{auId: '11118b6bc', exp: misc.getUnixTimestamp()}, {exp: misc.getUnixTimestamp(1)}]}, {key: 'valid', value: 'also-valid', exp: misc.getUnixTimestamp(1)}, {key: 'expired', value: 'fwefew', exp: misc.getUnixTimestamp()}, {key: 'usi', value: 'should be skipped because timestamp', exp: misc.getUnixTimestamp()}, {key: 'usi', value: usi, exp: misc.getUnixTimestamp(100)}, {key: 'usi', value: 'should be skipped because timestamp', exp: misc.getUnixTimestamp()}] + let storage; before(() => { getGlobal().bidderSettings = { @@ -20,8 +21,11 @@ describe('adnuntiusBidAdapter', function() { storageAllowed: true } }; - const storage = getStorageManager({ bidderCode: 'adnuntius' }) - storage.setDataInLocalStorage('adn.metaData', JSON.stringify(meta)) + storage = getStorageManager({bidderCode: 'adnuntius'}); + }); + + beforeEach(() => { + storage.setDataInLocalStorage('adn.metaData', JSON.stringify(meta)); }); after(() => { @@ -38,7 +42,7 @@ describe('adnuntiusBidAdapter', function() { const ENDPOINT_URL_VIDEO = `${ENDPOINT_URL_BASE}&userId=${usi}&tt=vast4`; const ENDPOINT_URL_NOCOOKIE = `${ENDPOINT_URL_BASE}&userId=${usi}&noCookies=true`; const ENDPOINT_URL_SEGMENTS = `${ENDPOINT_URL_BASE}&segments=segment1,segment2,segment3&userId=${usi}`; - const ENDPOINT_URL_CONSENT = `${EURO_URL}${tzo}&format=json&consentString=consentString&userId=${usi}`; + const ENDPOINT_URL_CONSENT = `${EURO_URL}${tzo}&format=json&consentString=consentString&gdpr=1&userId=${usi}`; const adapter = newBidder(spec); const bidderRequests = [ @@ -47,6 +51,7 @@ describe('adnuntiusBidAdapter', function() { bidder: 'adnuntius', params: { auId: '000000000008b6bc', + targetId: '123', network: 'adnuntius', maxDeals: 1 }, @@ -99,9 +104,75 @@ describe('adnuntiusBidAdapter', function() { const videoBidRequest = { bid: videoBidderRequest, - bidder: 'adnuntius' + bidder: 'adnuntius', + params: { + bidType: 'justsomestuff-error-handling' + } } + const deals = [ + { + 'destinationUrlEsc': 'http%3A%2F%2Fads.adnuntius.delivery%2Fc%2FyQtMUwYBn5P4v72WJMqLW4z7uJOBFXJTfjoRyz0z_wsAAAAQCtjQz9kbGWD4nuZy3q6HaCYxq6Lckz2kThplNb227EJdQ5032jcIGkf-UrPmXCU2EbXVaQ3Ok6_FNLuIDTONJyx6ZZCB10wGqA3OaSe1EqwQp84u1_5iQZAWDk73UYf7_vcIypn7ev-SICZ3qaevb2jYSRqTVZx6AiBZQQGlzlOOrbZU9AU1F-JwTds-YV3qtJHGlxI2peWFIuxFlOYyeX9Kzg%3Fct%3D673%26r%3Dhttp%253A%252F%252Fadnuntius.com', + 'assets': { + 'Image': { + 'cdnId': 'https://cdn.adnuntius.com/cdn/iBgqruUNbaUb2hmD3vws7WTi84jg_WB_-VOF_FeOZ7A.png', + 'width': '640', + 'height': '480' + } + }, + 'text': {}, + 'choices': {}, + 'clickUrl': 'https://ads.adnuntius.delivery/c/yQtMUwYBn5P4v72WJMqLW4z7uJOBFXJTfjoRyz0z_wsAAAAQCtjQz9kbGWD4nuZy3q6HaCYxq6Lckz2kThplNb227EJdQ5032jcIGkf-UrPmXCU2EbXVaQ3Ok6_FNLuIDTONJyx6ZZCB10wGqA3OaSe1EqwQp84u1_5iQZAWDk73UYf7_vcIypn7ev-SICZ3qaevb2jYSRqTVZx6AiBZQQGlzlOOrbZU9AU1F-JwTds-YV3qtJHGlxI2peWFIuxFlOYyeX9Kzg', + 'urls': { + 'destination': 'https://ads.adnuntius.delivery/c/yQtMUwYBn5P4v72WJMqLW4z7uJOBFXJTfjoRyz0z_wsAAAAQCtjQz9kbGWD4nuZy3q6HaCYxq6Lckz2kThplNb227EJdQ5032jcIGkf-UrPmXCU2EbXVaQ3Ok6_FNLuIDTONJyx6ZZCB10wGqA3OaSe1EqwQp84u1_5iQZAWDk73UYf7_vcIypn7ev-SICZ3qaevb2jYSRqTVZx6AiBZQQGlzlOOrbZU9AU1F-JwTds-YV3qtJHGlxI2peWFIuxFlOYyeX9Kzg?ct=673&r=http%3A%2F%2Fadnuntius.com' + }, + 'urlsEsc': { + 'destination': 'https%3A%2F%2Fads.adnuntius.delivery%2Fc%2FyQtMUwYBn5P4v72WJMqLW4z7uJOBFXJTfjoRyz0z_wsAAAAQCtjQz9kbGWD4nuZy3q6HaCYxq6Lckz2kThplNb227EJdQ5032jcIGkf-UrPmXCU2EbXVaQ3Ok6_FNLuIDTONJyx6ZZCB10wGqA3OaSe1EqwQp84u1_5iQZAWDk73UYf7_vcIypn7ev-SICZ3qaevb2jYSRqTVZx6AiBZQQGlzlOOrbZU9AU1F-JwTds-YV3qtJHGlxI2peWFIuxFlOYyeX9Kzg%3Fct%3D673%26r%3Dhttp%253A%252F%252Fadnuntius.com' + }, + 'destinationUrls': { + 'destination': 'https://adnuntius.com' + }, + 'cpm': { + 'amount': 9, + 'currency': 'USD' + }, + 'bid': { + 'amount': 0.009, + 'currency': 'USD' + }, + 'grossBid': { + 'amount': 0.009, + 'currency': 'USD' + }, + 'netBid': { + 'amount': 0.0081, + 'currency': 'USD' + }, + 'dealId': 'abc123xyz', + 'impressionTrackingUrls': [], + 'impressionTrackingUrlsEsc': [], + 'adId': 'adn-id-1064238860', + 'selectedColumn': '0', + 'selectedColumnPosition': '0', + 'renderedPixel': 'https://ads.adnuntius.delivery/b/yQtMUwYBn5P4v72WJMqLW4z7uJOBFXJTfjoRyz0z_wsAAAAQCtjQz9kbGWD4nuZy3q6HaCYxq6Lckz2kThplNb227EJdQ5032jcIGkf-UrPmXCU2EbXVaQ3Ok6_FNLuIDTONJyx6ZZCB10wGqA3OaSe1EqwQp84u1_5iQZAWDk73UYf7_vcIypn7ev-SICZ3qaevb2jYSRqTVZx6AiBZQQGlzlOOrbZU9AU1F-JwTds-YV3qtJHGlxI2peWFIuxFlOYyeX9Kzg.html', + 'renderedPixelEsc': 'http%3A%2F%2Fads.adnuntius.delivery%2Fb%2FyQtMUwYBn5P4v72WJMqLW4z7uJOBFXJTfjoRyz0z_wsAAAAQCtjQz9kbGWD4nuZy3q6HaCYxq6Lckz2kThplNb227EJdQ5032jcIGkf-UrPmXCU2EbXVaQ3Ok6_FNLuIDTONJyx6ZZCB10wGqA3OaSe1EqwQp84u1_5iQZAWDk73UYf7_vcIypn7ev-SICZ3qaevb2jYSRqTVZx6AiBZQQGlzlOOrbZU9AU1F-JwTds-YV3qtJHGlxI2peWFIuxFlOYyeX9Kzg.html', + 'visibleUrl': 'https://ads.adnuntius.delivery/s?rt=yQtMUwYBn5P4v72WJMqLW4z7uJOBFXJTfjoRyz0z_wsAAAAQCtjQz9kbGWD4nuZy3q6HaCYxq6Lckz2kThplNb227EJdQ5032jcIGkf-UrPmXCU2EbXVaQ3Ok6_FNLuIDTONJyx6ZZCB10wGqA3OaSe1EqwQp84u1_5iQZAWDk73UYf7_vcIypn7ev-SICZ3qaevb2jYSRqTVZx6AiBZQQGlzlOOrbZU9AU1F-JwTds-YV3qtJHGlxI2peWFIuxFlOYyeX9Kzg', + 'visibleUrlEsc': 'http%3A%2F%2Fads.adnuntius.delivery%2Fs%3Frt%3DyQtMUwYBn5P4v72WJMqLW4z7uJOBFXJTfjoRyz0z_wsAAAAQCtjQz9kbGWD4nuZy3q6HaCYxq6Lckz2kThplNb227EJdQ5032jcIGkf-UrPmXCU2EbXVaQ3Ok6_FNLuIDTONJyx6ZZCB10wGqA3OaSe1EqwQp84u1_5iQZAWDk73UYf7_vcIypn7ev-SICZ3qaevb2jYSRqTVZx6AiBZQQGlzlOOrbZU9AU1F-JwTds-YV3qtJHGlxI2peWFIuxFlOYyeX9Kzg', + 'viewUrl': 'https://ads.adnuntius.delivery/v?rt=yQtMUwYBn5P4v72WJMqLW4z7uJOBFXJTfjoRyz0z_wsAAAAQCtjQz9kbGWD4nuZy3q6HaCYxq6Lckz2kThplNb227EJdQ5032jcIGkf-UrPmXCU2EbXVaQ3Ok6_FNLuIDTONJyx6ZZCB10wGqA3OaSe1EqwQp84u1_5iQZAWDk73UYf7_vcIypn7ev-SICZ3qaevb2jYSRqTVZx6AiBZQQGlzlOOrbZU9AU1F-JwTds-YV3qtJHGlxI2peWFIuxFlOYyeX9Kzg', + 'viewUrlEsc': 'http%3A%2F%2Fads.adnuntius.delivery%2Fv%3Frt%3DyQtMUwYBn5P4v72WJMqLW4z7uJOBFXJTfjoRyz0z_wsAAAAQCtjQz9kbGWD4nuZy3q6HaCYxq6Lckz2kThplNb227EJdQ5032jcIGkf-UrPmXCU2EbXVaQ3Ok6_FNLuIDTONJyx6ZZCB10wGqA3OaSe1EqwQp84u1_5iQZAWDk73UYf7_vcIypn7ev-SICZ3qaevb2jYSRqTVZx6AiBZQQGlzlOOrbZU9AU1F-JwTds-YV3qtJHGlxI2peWFIuxFlOYyeX9Kzg', + 'rt': 'yQtMUwYBn5P4v72WJMqLW4z7uJOBFXJTfjoRyz0z_wsAAAAQCtjQz9kbGWD4nuZy3q6HaCYxq6Lckz2kThplNb227EJdQ5032jcIGkf-UrPmXCU2EbXVaQ3Ok6_FNLuIDTONJyx6ZZCB10wGqA3OaSe1EqwQp84u1_5iQZAWDk73UYf7_vcIypn7ev-SICZ3qaevb2jYSRqTVZx6AiBZQQGlzlOOrbZU9AU1F-JwTds-YV3qtJHGlxI2peWFIuxFlOYyeX9Kzg', + 'creativeWidth': '640', + 'creativeHeight': '480', + 'creativeId': 's90t0q03pm', + 'lineItemId': 'cr3hnkkxhnkw9ldy', + 'layoutId': 'buyers_network_image_layout_1', + 'layoutName': 'Image', + 'layoutExternalReference': '', + 'html': "\n\n\n \n \n \n\n\n
\n
\"\"/
\n
\n \n\n", + 'renderTemplate': '' + } + ]; + const serverResponse = { body: { 'adUnits': [ @@ -111,68 +182,7 @@ describe('adnuntiusBidAdapter', function() { 'html': "\n\n\n \n \n \n\n\n
\n
\"\"/
\n
\n \n\n", 'matchedAdCount': 1, 'responseId': 'adn-rsp--229633088', - 'deals': [ - { - 'destinationUrlEsc': 'http%3A%2F%2Fads.adnuntius.delivery%2Fc%2FyQtMUwYBn5P4v72WJMqLW4z7uJOBFXJTfjoRyz0z_wsAAAAQCtjQz9kbGWD4nuZy3q6HaCYxq6Lckz2kThplNb227EJdQ5032jcIGkf-UrPmXCU2EbXVaQ3Ok6_FNLuIDTONJyx6ZZCB10wGqA3OaSe1EqwQp84u1_5iQZAWDk73UYf7_vcIypn7ev-SICZ3qaevb2jYSRqTVZx6AiBZQQGlzlOOrbZU9AU1F-JwTds-YV3qtJHGlxI2peWFIuxFlOYyeX9Kzg%3Fct%3D673%26r%3Dhttp%253A%252F%252Fadnuntius.com', - 'assets': { - 'Image': { - 'cdnId': 'https://cdn.adnuntius.com/cdn/iBgqruUNbaUb2hmD3vws7WTi84jg_WB_-VOF_FeOZ7A.png', - 'width': '640', - 'height': '480' - } - }, - 'text': {}, - 'choices': {}, - 'clickUrl': 'https://ads.adnuntius.delivery/c/yQtMUwYBn5P4v72WJMqLW4z7uJOBFXJTfjoRyz0z_wsAAAAQCtjQz9kbGWD4nuZy3q6HaCYxq6Lckz2kThplNb227EJdQ5032jcIGkf-UrPmXCU2EbXVaQ3Ok6_FNLuIDTONJyx6ZZCB10wGqA3OaSe1EqwQp84u1_5iQZAWDk73UYf7_vcIypn7ev-SICZ3qaevb2jYSRqTVZx6AiBZQQGlzlOOrbZU9AU1F-JwTds-YV3qtJHGlxI2peWFIuxFlOYyeX9Kzg', - 'urls': { - 'destination': 'https://ads.adnuntius.delivery/c/yQtMUwYBn5P4v72WJMqLW4z7uJOBFXJTfjoRyz0z_wsAAAAQCtjQz9kbGWD4nuZy3q6HaCYxq6Lckz2kThplNb227EJdQ5032jcIGkf-UrPmXCU2EbXVaQ3Ok6_FNLuIDTONJyx6ZZCB10wGqA3OaSe1EqwQp84u1_5iQZAWDk73UYf7_vcIypn7ev-SICZ3qaevb2jYSRqTVZx6AiBZQQGlzlOOrbZU9AU1F-JwTds-YV3qtJHGlxI2peWFIuxFlOYyeX9Kzg?ct=673&r=http%3A%2F%2Fadnuntius.com' - }, - 'urlsEsc': { - 'destination': 'https%3A%2F%2Fads.adnuntius.delivery%2Fc%2FyQtMUwYBn5P4v72WJMqLW4z7uJOBFXJTfjoRyz0z_wsAAAAQCtjQz9kbGWD4nuZy3q6HaCYxq6Lckz2kThplNb227EJdQ5032jcIGkf-UrPmXCU2EbXVaQ3Ok6_FNLuIDTONJyx6ZZCB10wGqA3OaSe1EqwQp84u1_5iQZAWDk73UYf7_vcIypn7ev-SICZ3qaevb2jYSRqTVZx6AiBZQQGlzlOOrbZU9AU1F-JwTds-YV3qtJHGlxI2peWFIuxFlOYyeX9Kzg%3Fct%3D673%26r%3Dhttp%253A%252F%252Fadnuntius.com' - }, - 'destinationUrls': { - 'destination': 'https://adnuntius.com' - }, - 'cpm': { - 'amount': 9, - 'currency': 'USD' - }, - 'bid': { - 'amount': 0.009, - 'currency': 'USD' - }, - 'grossBid': { - 'amount': 0.009, - 'currency': 'USD' - }, - 'netBid': { - 'amount': 0.0081, - 'currency': 'USD' - }, - 'dealId': 'abc123xyz', - 'impressionTrackingUrls': [], - 'impressionTrackingUrlsEsc': [], - 'adId': 'adn-id-1064238860', - 'selectedColumn': '0', - 'selectedColumnPosition': '0', - 'renderedPixel': 'https://ads.adnuntius.delivery/b/yQtMUwYBn5P4v72WJMqLW4z7uJOBFXJTfjoRyz0z_wsAAAAQCtjQz9kbGWD4nuZy3q6HaCYxq6Lckz2kThplNb227EJdQ5032jcIGkf-UrPmXCU2EbXVaQ3Ok6_FNLuIDTONJyx6ZZCB10wGqA3OaSe1EqwQp84u1_5iQZAWDk73UYf7_vcIypn7ev-SICZ3qaevb2jYSRqTVZx6AiBZQQGlzlOOrbZU9AU1F-JwTds-YV3qtJHGlxI2peWFIuxFlOYyeX9Kzg.html', - 'renderedPixelEsc': 'http%3A%2F%2Fads.adnuntius.delivery%2Fb%2FyQtMUwYBn5P4v72WJMqLW4z7uJOBFXJTfjoRyz0z_wsAAAAQCtjQz9kbGWD4nuZy3q6HaCYxq6Lckz2kThplNb227EJdQ5032jcIGkf-UrPmXCU2EbXVaQ3Ok6_FNLuIDTONJyx6ZZCB10wGqA3OaSe1EqwQp84u1_5iQZAWDk73UYf7_vcIypn7ev-SICZ3qaevb2jYSRqTVZx6AiBZQQGlzlOOrbZU9AU1F-JwTds-YV3qtJHGlxI2peWFIuxFlOYyeX9Kzg.html', - 'visibleUrl': 'https://ads.adnuntius.delivery/s?rt=yQtMUwYBn5P4v72WJMqLW4z7uJOBFXJTfjoRyz0z_wsAAAAQCtjQz9kbGWD4nuZy3q6HaCYxq6Lckz2kThplNb227EJdQ5032jcIGkf-UrPmXCU2EbXVaQ3Ok6_FNLuIDTONJyx6ZZCB10wGqA3OaSe1EqwQp84u1_5iQZAWDk73UYf7_vcIypn7ev-SICZ3qaevb2jYSRqTVZx6AiBZQQGlzlOOrbZU9AU1F-JwTds-YV3qtJHGlxI2peWFIuxFlOYyeX9Kzg', - 'visibleUrlEsc': 'http%3A%2F%2Fads.adnuntius.delivery%2Fs%3Frt%3DyQtMUwYBn5P4v72WJMqLW4z7uJOBFXJTfjoRyz0z_wsAAAAQCtjQz9kbGWD4nuZy3q6HaCYxq6Lckz2kThplNb227EJdQ5032jcIGkf-UrPmXCU2EbXVaQ3Ok6_FNLuIDTONJyx6ZZCB10wGqA3OaSe1EqwQp84u1_5iQZAWDk73UYf7_vcIypn7ev-SICZ3qaevb2jYSRqTVZx6AiBZQQGlzlOOrbZU9AU1F-JwTds-YV3qtJHGlxI2peWFIuxFlOYyeX9Kzg', - 'viewUrl': 'https://ads.adnuntius.delivery/v?rt=yQtMUwYBn5P4v72WJMqLW4z7uJOBFXJTfjoRyz0z_wsAAAAQCtjQz9kbGWD4nuZy3q6HaCYxq6Lckz2kThplNb227EJdQ5032jcIGkf-UrPmXCU2EbXVaQ3Ok6_FNLuIDTONJyx6ZZCB10wGqA3OaSe1EqwQp84u1_5iQZAWDk73UYf7_vcIypn7ev-SICZ3qaevb2jYSRqTVZx6AiBZQQGlzlOOrbZU9AU1F-JwTds-YV3qtJHGlxI2peWFIuxFlOYyeX9Kzg', - 'viewUrlEsc': 'http%3A%2F%2Fads.adnuntius.delivery%2Fv%3Frt%3DyQtMUwYBn5P4v72WJMqLW4z7uJOBFXJTfjoRyz0z_wsAAAAQCtjQz9kbGWD4nuZy3q6HaCYxq6Lckz2kThplNb227EJdQ5032jcIGkf-UrPmXCU2EbXVaQ3Ok6_FNLuIDTONJyx6ZZCB10wGqA3OaSe1EqwQp84u1_5iQZAWDk73UYf7_vcIypn7ev-SICZ3qaevb2jYSRqTVZx6AiBZQQGlzlOOrbZU9AU1F-JwTds-YV3qtJHGlxI2peWFIuxFlOYyeX9Kzg', - 'rt': 'yQtMUwYBn5P4v72WJMqLW4z7uJOBFXJTfjoRyz0z_wsAAAAQCtjQz9kbGWD4nuZy3q6HaCYxq6Lckz2kThplNb227EJdQ5032jcIGkf-UrPmXCU2EbXVaQ3Ok6_FNLuIDTONJyx6ZZCB10wGqA3OaSe1EqwQp84u1_5iQZAWDk73UYf7_vcIypn7ev-SICZ3qaevb2jYSRqTVZx6AiBZQQGlzlOOrbZU9AU1F-JwTds-YV3qtJHGlxI2peWFIuxFlOYyeX9Kzg', - 'creativeWidth': '640', - 'creativeHeight': '480', - 'creativeId': 's90t0q03pm', - 'lineItemId': 'cr3hnkkxhnkw9ldy', - 'layoutId': 'buyers_network_image_layout_1', - 'layoutName': 'Image', - 'layoutExternalReference': '', - 'html': "\n\n\n \n \n \n\n\n
\n
\"\"/
\n
\n \n\n", - 'renderTemplate': '' - } - ], + 'deals': deals, 'ads': [ { 'destinationUrlEsc': 'http%3A%2F%2Fads.adnuntius.delivery%2Fc%2F5Mu-vFVsaf4dWWx8uyZoV7Mz0sPkF1_j9bSupMwX8dMAAAAQCtjQz9kbGWD4nuZy3q6HaCYxqqTYxj2sS0FkMriztxcORshj3zRbT0KsV7XnDXE0F-OsHX7Ok6_FNLuIDTOMJyx6ZZCB10wGqA3OaSe1Eq9D85h8gP1gGsobC0KsAISm_PYNkJ6ve6qZLnB79fX6XHLYSRqTBM8sBCRXQAetnVzeo7AHoQhkFeouS444YA_q4JCTlRI2peWFIuxFlOYyeX9Kzg%3Fct%3D673%26r%3Dhttp%253A%252F%252Fadnuntius.com', @@ -454,7 +464,78 @@ describe('adnuntiusBidAdapter', function() { expect(request[0]).to.have.property('url'); expect(request[0].url).to.equal(ENDPOINT_URL); expect(request[0]).to.have.property('data'); - expect(request[0].data).to.equal('{"adUnits":[{"auId":"000000000008b6bc","targetId":"adn-000000000008b6bc","maxDeals":1,"dimensions":[[640,480],[600,400]]},{"auId":"0000000000000551","targetId":"adn-0000000000000551","maxDeals":0,"dimensions":[[1640,1480],[1600,1400]]}],"metaData":{"usi":"' + usi + '"}}'); + expect(request[0].data).to.equal('{"adUnits":[{"auId":"000000000008b6bc","targetId":"123","maxDeals":1,"dimensions":[[640,480],[600,400]]},{"auId":"0000000000000551","targetId":"adn-0000000000000551","dimensions":[[1640,1480],[1600,1400]]}],"metaData":{"valid":"also-valid"}}'); + }); + + it('Test requests with no local storage', function() { + storage.setDataInLocalStorage('adn.metaData', JSON.stringify([{}])); + const request = spec.buildRequests(bidderRequests, {}); + expect(request.length).to.equal(1); + expect(request[0]).to.have.property('bid'); + const bid = request[0].bid[0] + expect(bid).to.have.property('bidId'); + expect(request[0]).to.have.property('url'); + expect(request[0].url).to.equal(ENDPOINT_URL_BASE); + expect(request[0]).to.have.property('data'); + expect(request[0].data).to.equal('{"adUnits":[{"auId":"000000000008b6bc","targetId":"123","maxDeals":1,"dimensions":[[640,480],[600,400]]},{"auId":"0000000000000551","targetId":"adn-0000000000000551","dimensions":[[1640,1480],[1600,1400]]}]}'); + + localStorage.removeItem('adn.metaData'); + const request2 = spec.buildRequests(bidderRequests, {}); + expect(request2.length).to.equal(1); + expect(request2[0]).to.have.property('url'); + expect(request2[0].url).to.equal(ENDPOINT_URL_BASE); + }); + + it('Test request changes for voided au ids', function() { + storage.setDataInLocalStorage('adn.metaData', JSON.stringify([{key: 'voidAuIds', value: [{auId: '11118b6bc', exp: misc.getUnixTimestamp(1)}, {auId: '0000000000000023', exp: misc.getUnixTimestamp(1)}]}])); + const bRequests = bidderRequests.concat([{ + bidId: 'adn-11118b6bc', + bidder: 'adnuntius', + params: { + auId: '11118b6bc', + network: 'adnuntius', + }, + mediaTypes: { + banner: { + sizes: [[1640, 1480], [1600, 1400]], + } + }, + }]); + bRequests.push({ + bidId: 'adn-23', + bidder: 'adnuntius', + params: { + auId: '23', + network: 'adnuntius', + }, + mediaTypes: { + banner: { + sizes: [[1640, 1480], [1600, 1400]], + } + }, + }); + bRequests.push({ + bidId: 'adn-13', + bidder: 'adnuntius', + params: { + auId: '13', + network: 'adnuntius', + }, + mediaTypes: { + banner: { + sizes: [[164, 140], [10, 1400]], + } + }, + }); + const request = spec.buildRequests(bRequests, {}); + expect(request.length).to.equal(1); + expect(request[0]).to.have.property('bid'); + const bid = request[0].bid[0] + expect(bid).to.have.property('bidId'); + expect(request[0]).to.have.property('url'); + expect(request[0].url).to.equal(ENDPOINT_URL_BASE); + expect(request[0]).to.have.property('data'); + expect(request[0].data).to.equal('{"adUnits":[{"auId":"000000000008b6bc","targetId":"123","maxDeals":1,"dimensions":[[640,480],[600,400]]},{"auId":"0000000000000551","targetId":"adn-0000000000000551","dimensions":[[1640,1480],[1600,1400]]},{"auId":"13","targetId":"adn-13","dimensions":[[164,140],[10,1400]]}]}'); }); it('Test Video requests', function() { @@ -472,7 +553,7 @@ describe('adnuntiusBidAdapter', function() { user: { data: [{ name: 'adnuntius', - segment: [{id: 'segment1'}, {id: 'segment2'}] + segment: [{id: 'segment1'}, {id: 'segment2'}, {invalidSegment: 'invalid'}, {id: 123}, {id: ['3332']}] }, { name: 'other', @@ -655,7 +736,7 @@ describe('adnuntiusBidAdapter', function() { expect(bidderRequests[0].params.maxDeals).to.equal(1); expect(data.adUnits[0].maxDeals).to.equal(bidderRequests[0].params.maxDeals); expect(bidderRequests[1].params).to.not.have.property('maxBids'); - expect(data.adUnits[1].maxDeals).to.equal(0); + expect(data.adUnits[1].maxDeals).to.equal(undefined); }); it('Should allow a maximum of 5 deals.', function() { config.setBidderConfig({ @@ -701,7 +782,7 @@ describe('adnuntiusBidAdapter', function() { expect(request[0]).to.have.property('data'); const data = JSON.parse(request[0].data); expect(data.adUnits.length).to.equal(1); - expect(data.adUnits[0].maxDeals).to.equal(0); + expect(data.adUnits[0].maxDeals).to.equal(undefined); }); it('Should set max deals using bidder config.', function() { config.setBidderConfig({ @@ -747,12 +828,20 @@ describe('adnuntiusBidAdapter', function() { describe('interpretResponse', function() { it('should return valid response when passed valid server response', function() { - const interpretedResponse = spec.interpretResponse(serverResponse, singleBidRequest); + config.setBidderConfig({ + bidders: ['adnuntius'], + config: { + bidType: 'netBid', + maxDeals: 1 + } + }); + + const interpretedResponse = config.runWithBidder('adnuntius', () => spec.interpretResponse(serverResponse, singleBidRequest)); expect(interpretedResponse).to.have.lengthOf(2); const deal = serverResponse.body.adUnits[0].deals[0]; expect(interpretedResponse[0].bidderCode).to.equal('adnuntius'); - expect(interpretedResponse[0].cpm).to.equal(deal.bid.amount * 1000); + expect(interpretedResponse[0].cpm).to.equal(deal.netBid.amount * 1000); expect(interpretedResponse[0].width).to.equal(Number(deal.creativeWidth)); expect(interpretedResponse[0].height).to.equal(Number(deal.creativeHeight)); expect(interpretedResponse[0].creativeId).to.equal(deal.creativeId); @@ -768,7 +857,7 @@ describe('adnuntiusBidAdapter', function() { const ad = serverResponse.body.adUnits[0].ads[0]; expect(interpretedResponse[1].bidderCode).to.equal('adnuntius'); - expect(interpretedResponse[1].cpm).to.equal(ad.bid.amount * 1000); + expect(interpretedResponse[1].cpm).to.equal(ad.netBid.amount * 1000); expect(interpretedResponse[1].width).to.equal(Number(ad.creativeWidth)); expect(interpretedResponse[1].height).to.equal(Number(ad.creativeHeight)); expect(interpretedResponse[1].creativeId).to.equal(ad.creativeId); @@ -781,6 +870,87 @@ describe('adnuntiusBidAdapter', function() { expect(interpretedResponse[1].ttl).to.equal(360); expect(interpretedResponse[1].dealId).to.equal('not-in-deal-array-here'); expect(interpretedResponse[1].dealCount).to.equal(0); + + const results = JSON.parse(storage.getDataFromLocalStorage('adn.metaData')); + const usiEntry = results.find(entry => entry.key === 'usi'); + expect(usiEntry.key).to.equal('usi'); + expect(usiEntry.value).to.equal('from-api-server dude'); + expect(usiEntry.exp).to.be.greaterThan(misc.getUnixTimestamp(90)); + + const voidAuIdsEntry = results.find(entry => entry.key === 'voidAuIds'); + expect(voidAuIdsEntry.key).to.equal('voidAuIds'); + expect(voidAuIdsEntry.exp).to.equal(undefined); + expect(voidAuIdsEntry.value[0].auId).to.equal('00000000000abcde'); + expect(voidAuIdsEntry.value[0].exp).to.be.greaterThan(misc.getUnixTimestamp()); + expect(voidAuIdsEntry.value[0].exp).to.be.lessThan(misc.getUnixTimestamp(2)); + expect(voidAuIdsEntry.value[1].auId).to.equal('00000000000fffff'); + expect(voidAuIdsEntry.value[1].exp).to.be.greaterThan(misc.getUnixTimestamp()); + expect(voidAuIdsEntry.value[1].exp).to.be.lessThan(misc.getUnixTimestamp(2)); + + const validEntry = results.find(entry => entry.key === 'valid'); + expect(validEntry.key).to.equal('valid'); + expect(validEntry.value).to.equal('also-valid'); + expect(validEntry.exp).to.be.greaterThan(misc.getUnixTimestamp()); + expect(validEntry.exp).to.be.lessThan(misc.getUnixTimestamp(2)); + + const randomApiEntry = results.find(entry => entry.key === 'randomApiKey'); + expect(randomApiEntry.key).to.equal('randomApiKey'); + expect(randomApiEntry.value).to.equal('randomApiValue'); + expect(randomApiEntry.exp).to.be.greaterThan(misc.getUnixTimestamp(90)); + }); + + it('should not process valid response when passed alt bidder that is an adndeal', function() { + const altBidder = { + bid: [ + { + bidder: 'adndeal1', + bidId: 'adn-0000000000000551', + } + ] + }; + serverResponse.body.adUnits[0].deals = []; + delete serverResponse.body.metaData.voidAuIds; // test response with no voidAuIds + + const interpretedResponse = spec.interpretResponse(serverResponse, altBidder); + expect(interpretedResponse).to.have.lengthOf(0); + + serverResponse.body.adUnits[0].deals = deals; + }); + + it('should return valid response when passed alt bidder', function() { + const altBidder = { + bid: [ + { + bidder: 'adn-alt', + bidId: 'adn-0000000000000551', + params: { + bidType: 'netBid' + } + } + ] + }; + serverResponse.body.adUnits[0].deals = []; + + const interpretedResponse = spec.interpretResponse(serverResponse, altBidder); + expect(interpretedResponse).to.have.lengthOf(1); + + const ad = serverResponse.body.adUnits[0].ads[0]; + expect(interpretedResponse[0].bidderCode).to.equal('adn-alt'); + expect(interpretedResponse[0].cpm).to.equal(ad.netBid.amount * 1000); + expect(interpretedResponse[0].width).to.equal(Number(ad.creativeWidth)); + expect(interpretedResponse[0].height).to.equal(Number(ad.creativeHeight)); + expect(interpretedResponse[0].creativeId).to.equal(ad.creativeId); + expect(interpretedResponse[0].currency).to.equal(ad.bid.currency); + expect(interpretedResponse[0].netRevenue).to.equal(false); + expect(interpretedResponse[0].meta).to.have.property('advertiserDomains'); + expect(interpretedResponse[0].meta.advertiserDomains).to.have.lengthOf(1); + expect(interpretedResponse[0].meta.advertiserDomains[0]).to.equal('adnuntius.com'); + expect(interpretedResponse[0].ad).to.equal(serverResponse.body.adUnits[0].html); + expect(interpretedResponse[0].ttl).to.equal(360); + expect(interpretedResponse[0].dealId).to.equal('not-in-deal-array-here'); + expect(interpretedResponse[0].dealCount).to.equal(0); + + serverResponse.body.adUnits[0].deals = deals; }); }); diff --git a/test/spec/modules/adpod_spec.js b/test/spec/modules/adpod_spec.js index a6164f919ef..14e530c1a9b 100644 --- a/test/spec/modules/adpod_spec.js +++ b/test/spec/modules/adpod_spec.js @@ -47,7 +47,6 @@ describe('adpod.js', function () { addBidToAuctionStub = sinon.stub(auction, 'addBidToAuction').callsFake(function (auctionInstance, bid) { auctionBids.push(bid); }); - doCallbacksIfTimedoutStub = sinon.stub(auction, 'doCallbacksIfTimedout'); clock = sinon.useFakeTimers(); config.setConfig({ cache: { @@ -61,7 +60,6 @@ describe('adpod.js', function () { logWarnStub.restore(); logInfoStub.restore(); addBidToAuctionStub.restore(); - doCallbacksIfTimedoutStub.restore(); clock.restore(); config.resetConfig(); auctionBids = []; @@ -633,7 +631,6 @@ describe('adpod.js', function () { callPrebidCacheHook(callbackFn, auctionInstance, bidResponse1, afterBidAddedSpy, videoMT); callPrebidCacheHook(callbackFn, auctionInstance, bidResponse2, afterBidAddedSpy, videoMT); - expect(doCallbacksIfTimedoutStub.calledTwice).to.equal(true); expect(logWarnStub.calledOnce).to.equal(true); expect(auctionBids.length).to.equal(0); }); diff --git a/test/spec/modules/adqueryBidAdapter_spec.js b/test/spec/modules/adqueryBidAdapter_spec.js index 75b012aac77..e9286329d57 100644 --- a/test/spec/modules/adqueryBidAdapter_spec.js +++ b/test/spec/modules/adqueryBidAdapter_spec.js @@ -80,7 +80,11 @@ describe('adqueryBidAdapter', function () { }) describe('buildRequests', function () { - let req = spec.buildRequests([ bidRequest ], { refererInfo: { } })[0] + let req; + beforeEach(() => { + req = spec.buildRequests([ bidRequest ], { refererInfo: { } })[0] + }) + let rdata it('should return request object', function () { diff --git a/test/spec/modules/adqueryIdSystem_spec.js b/test/spec/modules/adqueryIdSystem_spec.js index a6b4e9d1529..0a2cd60d89e 100644 --- a/test/spec/modules/adqueryIdSystem_spec.js +++ b/test/spec/modules/adqueryIdSystem_spec.js @@ -1,5 +1,6 @@ -import { adqueryIdSubmodule, storage } from 'modules/adqueryIdSystem.js'; -import { server } from 'test/mocks/xhr.js'; +import {adqueryIdSubmodule, storage} from 'modules/adqueryIdSystem.js'; +import {server} from 'test/mocks/xhr.js'; +import sinon from 'sinon'; const config = { storage: { @@ -18,10 +19,10 @@ describe('AdqueryIdSystem', function () { }); }); - describe('getId', function() { + describe('getId', function () { let getDataFromLocalStorageStub; - beforeEach(function() { + beforeEach(function () { getDataFromLocalStorageStub = sinon.stub(storage, 'getDataFromLocalStorage'); }); @@ -29,7 +30,7 @@ describe('AdqueryIdSystem', function () { getDataFromLocalStorageStub.restore(); }); - it('gets a adqueryId', function() { + it('gets a adqueryId', function () { const config = { params: {} }; @@ -37,36 +38,24 @@ describe('AdqueryIdSystem', function () { const callback = adqueryIdSubmodule.getId(config).callback; callback(callbackSpy); const request = server.requests[0]; - expect(request.url).to.eq(`https://bidder.adquery.io/prebid/qid`); - request.respond(200, { 'Content-Type': 'application/json' }, JSON.stringify({ qid: 'qid' })); - expect(callbackSpy.lastCall.lastArg).to.deep.equal({qid: 'qid'}); + expect(request.url).to.contains(`https://bidder.adquery.io/prebid/qid?qid=`); + request.respond(200, {'Content-Type': 'application/json'}, JSON.stringify({qid: '6dd9eab7dfeab7df6dd9ea'})); + expect(callbackSpy.lastCall.lastArg).to.deep.equal('6dd9eab7dfeab7df6dd9ea'); }); - it('gets a cached adqueryId', function() { - const config = { - params: {} - }; - getDataFromLocalStorageStub.withArgs('qid').returns('qid'); - - const callbackSpy = sinon.spy(); - const callback = adqueryIdSubmodule.getId(config).callback; - callback(callbackSpy); - expect(callbackSpy.lastCall.lastArg).to.deep.equal({qid: 'qid'}); - }); - - it('allows configurable id url', function() { + it('allows configurable id url', function () { const config = { params: { - url: 'https://bidder.adquery.io' + url: 'https://another_bidder.adquery.io/qid' } }; const callbackSpy = sinon.spy(); const callback = adqueryIdSubmodule.getId(config).callback; callback(callbackSpy); const request = server.requests[0]; - expect(request.url).to.eq('https://bidder.adquery.io'); - request.respond(200, { 'Content-Type': 'application/json' }, JSON.stringify({ qid: 'testqid' })); - expect(callbackSpy.lastCall.lastArg).to.deep.equal({qid: 'testqid'}); + expect(request.url).to.contains('https://another_bidder.adquery.io/qid'); + request.respond(200, {'Content-Type': 'application/json'}, JSON.stringify({qid: 'testqid'})); + expect(callbackSpy.lastCall.lastArg).to.deep.equal('testqid'); }); }); }); diff --git a/test/spec/modules/adstirBidAdapter_spec.js b/test/spec/modules/adstirBidAdapter_spec.js new file mode 100644 index 00000000000..290a6822f69 --- /dev/null +++ b/test/spec/modules/adstirBidAdapter_spec.js @@ -0,0 +1,412 @@ +import { expect } from 'chai'; +import { spec } from '../../../modules/adstirBidAdapter.js'; +import * as utils from 'src/utils.js'; +import { config } from 'src/config.js'; + +describe('AdstirAdapter', function () { + describe('isBidRequestValid', function () { + it('should return true if appId is non-empty string and adSpaceNo is integer', function () { + const bid = { + params: { + appId: 'MEDIA-XXXXXX', + adSpaceNo: 6, + } + } + expect(spec.isBidRequestValid(bid)).to.equal(true); + }); + it('should return false if appId is non-empty string, but adSpaceNo is not integer', function () { + const bid = { + params: { + appId: 'MEDIA-XXXXXX', + adSpaceNo: 'a', + } + } + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + it('should return false if appId is non-empty string, but adSpaceNo is null', function () { + const bid = { + params: { + appId: 'MEDIA-XXXXXX', + adSpaceNo: null, + } + } + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + it('should return false if appId is non-empty string, but adSpaceNo is undefined', function () { + const bid = { + params: { + appId: 'MEDIA-XXXXXX' + } + } + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + it('should return false if adSpaceNo is integer, but appId is empty string', function () { + const bid = { + params: { + appId: '', + adSpaceNo: 6, + } + } + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + it('should return false if adSpaceNo is integer, but appId is not string', function () { + const bid = { + params: { + appId: 123, + adSpaceNo: 6, + } + } + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + it('should return false if adSpaceNo is integer, but appId is null', function () { + const bid = { + params: { + appId: null, + adSpaceNo: 6, + } + } + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + it('should return false if adSpaceNo is integer, but appId is undefined', function () { + const bid = { + params: { + adSpaceNo: 6, + } + } + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + it('should return false if params is empty', function () { + const bid = { + params: {} + } + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + }); + + describe('buildRequests', function () { + const validBidRequests = [ + { + auctionId: 'b06c5141-fe8f-4cdf-9d7d-54415490a917', + bidder: 'adstir', + bidId: 'bidid1111', + params: { + appId: 'MEDIA-XXXXXX', + adSpaceNo: 1, + }, + transactionId: 'transactionid-1111', + mediaTypes: { + banner: { + sizes: [ + [300, 250], + [336, 280], + ], + } + }, + sizes: [ + [300, 250], + [336, 280], + ], + }, + { + auctionId: 'b06c5141-fe8f-4cdf-9d7d-54415490a917', + bidder: 'adstir', + bidId: 'bidid2222', + params: { + appId: 'MEDIA-XXXXXX', + adSpaceNo: 2, + }, + transactionId: 'transactionid-2222', + mediaTypes: { + banner: { + sizes: [ + [320, 50], + [320, 100], + ], + } + }, + sizes: [ + [320, 50], + [320, 100], + ], + }, + ]; + + const bidderRequest = { + auctionId: 'b06c5141-fe8f-4cdf-9d7d-54415490a917', + refererInfo: { + page: 'https://ad-stir.com/contact', + topmostLocation: 'https://ad-stir.com/contact', + reachedTop: true, + ref: 'https://test.example/q=adstir', + isAmp: false, + numIframes: 0, + stack: [ + 'https://ad-stir.com/contact', + ], + }, + }; + + it('one entry in validBidRequests corresponds to one server request object.', function () { + const requests = spec.buildRequests(validBidRequests, bidderRequest); + expect(requests.length).to.equal(validBidRequests.length); + requests.forEach(function (r, i) { + expect(r.method).to.equal('POST'); + expect(r.url).to.equal('https://ad.ad-stir.com/prebid'); + const d = JSON.parse(r.data); + expect(d.auctionId).to.equal('b06c5141-fe8f-4cdf-9d7d-54415490a917'); + + const v = validBidRequests[i]; + expect(d.appId).to.equal(v.params.appId); + expect(d.adSpaceNo).to.equal(v.params.adSpaceNo); + expect(d.bidId).to.equal(v.bidId); + expect(d.transactionId).to.equal(v.transactionId); + expect(d.mediaTypes).to.deep.equal(v.mediaTypes); + expect(d.sizes).to.deep.equal(v.sizes); + expect(d.ref.page).to.equal(bidderRequest.refererInfo.page); + expect(d.ref.tloc).to.equal(bidderRequest.refererInfo.topmostLocation); + expect(d.ref.referrer).to.equal(bidderRequest.refererInfo.ref); + expect(d.sua).to.equal(null); + expect(d.gdpr).to.equal(false); + expect(d.usp).to.equal(false); + expect(d.schain).to.equal(null); + expect(d.eids).to.deep.equal([]); + }); + }); + + it('ref.page, ref.tloc and ref.referrer correspond to refererInfo', function () { + const [ request ] = spec.buildRequests([validBidRequests[0]], { + auctionId: 'b06c5141-fe8f-4cdf-9d7d-54415490a917', + refererInfo: { + page: null, + topmostLocation: 'https://adserver.example/iframe1.html', + reachedTop: false, + ref: null, + isAmp: false, + numIframes: 2, + stack: [ + null, + 'https://adserver.example/iframe1.html', + 'https://adserver.example/iframe2.html' + ], + }, + }); + + const { ref } = JSON.parse(request.data); + expect(ref.page).to.equal(null); + expect(ref.tloc).to.equal('https://adserver.example/iframe1.html'); + expect(ref.referrer).to.equal(null); + }); + + it('when config.pageUrl is not set, ref.topurl equals to refererInfo.reachedTop', function () { + let bidderRequestClone = utils.deepClone(bidderRequest); + [true, false].forEach(function (reachedTop) { + bidderRequestClone.refererInfo.reachedTop = reachedTop; + const requests = spec.buildRequests(validBidRequests, bidderRequestClone); + const d = JSON.parse(requests[0].data); + expect(d.ref.topurl).to.equal(reachedTop); + }); + }); + + describe('when config.pageUrl is set, ref.topurl is always false', function () { + before(function () { + config.setConfig({ pageUrl: 'https://ad-stir.com/register' }); + }); + after(function () { + config.resetConfig(); + }); + + it('ref.topurl should be false', function () { + let bidderRequestClone = utils.deepClone(bidderRequest); + [true, false].forEach(function (reachedTop) { + bidderRequestClone.refererInfo.reachedTop = reachedTop; + const requests = spec.buildRequests(validBidRequests, bidderRequestClone); + const d = JSON.parse(requests[0].data); + expect(d.ref.topurl).to.equal(false); + }); + }); + }); + + it('gdprConsent.gdprApplies is sent', function () { + let bidderRequestClone = utils.deepClone(bidderRequest); + [true, false].forEach(function (gdprApplies) { + bidderRequestClone.gdprConsent = { gdprApplies }; + const requests = spec.buildRequests(validBidRequests, bidderRequestClone); + const d = JSON.parse(requests[0].data); + expect(d.gdpr).to.equal(gdprApplies); + }); + }); + + it('includes in the request parameters whether CCPA applies', function () { + let bidderRequestClone = utils.deepClone(bidderRequest); + const cases = [ + { uspConsent: '1---', expected: false }, + { uspConsent: '1YYY', expected: true }, + { uspConsent: '1YNN', expected: true }, + { uspConsent: '1NYN', expected: true }, + { uspConsent: '1-Y-', expected: true }, + ]; + cases.forEach(function ({ uspConsent, expected }) { + bidderRequestClone.uspConsent = uspConsent; + const requests = spec.buildRequests(validBidRequests, bidderRequestClone); + const d = JSON.parse(requests[0].data); + expect(d.usp).to.equal(expected); + }); + }); + + it('should add schain if available', function() { + const schain = { + 'ver': '1.0', + 'complete': 1, + 'nodes': [ + { + 'asi': 'exchange1.example', + 'sid': '1234!abcd', + 'hp': 1, + 'rid': 'bid-request-1', + 'name': 'publisher, Inc.', + 'domain': 'publisher.example' + }, + { + 'asi': 'exchange2.example', + 'sid': 'abcd', + 'hp': 1, + 'rid': 'bid-request-2', + 'name': 'intermediary', + 'domain': 'intermediary.example' + } + ] + }; + const serializedSchain = '1.0,1!exchange1.example,1234%21abcd,1,bid-request-1,publisher%2C%20Inc.,publisher.example!exchange2.example,abcd,1,bid-request-2,intermediary,intermediary.example'; + + const [ request ] = spec.buildRequests([utils.mergeDeep(utils.deepClone(validBidRequests[0]), { schain })], bidderRequest); + const d = JSON.parse(request.data); + expect(d.schain).to.deep.equal(serializedSchain); + }); + + it('should add schain even if some nodes params are blank', function() { + const schain = { + 'ver': '1.0', + 'complete': 1, + 'nodes': [ + { + 'asi': 'exchange1.example', + 'sid': '1234!abcd', + 'hp': 1, + }, + { + }, + { + 'asi': 'exchange2.example', + 'sid': 'abcd', + 'hp': 1, + }, + ] + }; + const serializedSchain = '1.0,1!exchange1.example,1234%21abcd,1,,,!,,,,,!exchange2.example,abcd,1,,,'; + + const [ request ] = spec.buildRequests([utils.mergeDeep(utils.deepClone(validBidRequests[0]), { schain })], bidderRequest); + const d = JSON.parse(request.data); + expect(d.schain).to.deep.equal(serializedSchain); + }); + + it('should add UA client hints to payload if available', function () { + const sua = { + browsers: [ + { + brand: 'Not?A_Brand', + version: [ + '8', + '0', + '0', + '0' + ] + }, + { + version: [ + '108', + '0', + '5359', + '40' + ] + }, + { + brand: 'Google Chrome', + version: [ + '108', + '0', + '5359', + '40' + ] + } + ], + platform: { + brand: 'Android', + version: [ + '11' + ] + }, + mobile: 1, + architecture: '', + bitness: '64', + model: 'Pixel 5', + source: 2 + } + + const validBidRequestsClone = utils.deepClone(validBidRequests); + validBidRequestsClone[0] = utils.mergeDeep(validBidRequestsClone[0], { + ortb2: { + device: { sua }, + } + }); + + const requests = spec.buildRequests(validBidRequestsClone, bidderRequest); + requests.forEach(function (r) { + const d = JSON.parse(r.data); + expect(d.sua).to.deep.equal(sua); + }); + }); + }); + + describe('interpretResponse', function () { + it('return empty array when no content', function () { + const bids = spec.interpretResponse({ body: '' }); + expect(bids).to.deep.equal([]); + }); + it('return empty array when seatbid empty', function () { + const bids = spec.interpretResponse({ body: { seatbid: [] } }); + expect(bids).to.deep.equal([]); + }); + it('return valid bids when serverResponse is valid', function () { + const serverResponse = { + 'body': { + 'seatbid': [ + { + 'bid': { + 'ad': '
test response
', + 'cpm': 5250, + 'creativeId': '5_1234ABCD', + 'currency': 'JPY', + 'height': 250, + 'meta': { + 'advertiserDomains': [ + 'adv.example' + ], + 'mediaType': 'banner', + 'networkId': 5 + }, + 'netRevenue': true, + 'requestId': '22a9457aed98a4', + 'transactionId': 'f18c078e-4d2a-4ecb-a886-2a0c52187213', + 'ttl': 60, + 'width': 300, + } + } + ] + }, + 'headers': {} + }; + const bids = spec.interpretResponse(serverResponse); + expect(bids[0]).to.deep.equal(serverResponse.body.seatbid[0].bid); + }); + }); +}); diff --git a/test/spec/modules/adtelligentBidAdapter_spec.js b/test/spec/modules/adtelligentBidAdapter_spec.js index 26284d539d4..0acbaa06f5b 100644 --- a/test/spec/modules/adtelligentBidAdapter_spec.js +++ b/test/spec/modules/adtelligentBidAdapter_spec.js @@ -11,18 +11,13 @@ const EXPECTED_ENDPOINTS = [ 'https://ghb.adtelligent.com/v2/auction/' ]; const aliasEP = { - 'appaloosa': 'https://ghb.hb.appaloosa.media/v2/auction/', - 'appaloosa_publisherSuffix': 'https://ghb.hb.appaloosa.media/v2/auction/', - 'onefiftytwomedia': 'https://ghb.ads.152media.com/v2/auction/', - 'navelix': 'https://ghb.hb.navelix.com/v2/auction/', - 'bidsxchange': 'https://ghb.hbd.bidsxchange.com/v2/auction/', + 'janet_publisherSuffix': 'https://ghb.bidder.jmgads.com/v2/auction/', 'streamkey': 'https://ghb.hb.streamkey.net/v2/auction/', 'janet': 'https://ghb.bidder.jmgads.com/v2/auction/', - 'pgam': 'https://ghb.pgamssp.com/v2/auction/', 'ocm': 'https://ghb.cenarius.orangeclickmedia.com/v2/auction/', - 'vidcrunchllc': 'https://ghb.platform.vidcrunch.com/v2/auction/', '9dotsmedia': 'https://ghb.platform.audiodots.com/v2/auction/', 'copper6': 'https://ghb.app.copper6.com/v2/auction/', + 'indicue': 'https://ghb.console.indicue.com/v2/auction/', }; const DEFAULT_ADATPER_REQ = { bidderCode: 'adtelligent' }; @@ -156,6 +151,10 @@ const displayBidderRequestWithConsents = { gdprApplies: true, consentString: 'test' }, + gppConsent: { + gppString: 'abc12345234', + applicableSections: [7, 8] + }, uspConsent: 'iHaveIt' }; @@ -364,6 +363,10 @@ describe('adtelligentBidAdapter', () => { expect(bidRequestWithPubSettingsData.GDPR).to.be.equal(1); expect(bidRequestWithPubSettingsData.GDPRConsent).to.be.equal(displayBidderRequestWithConsents.gdprConsent.consentString); }); + it('sets GPP flags', () => { + expect(bidRequestWithPubSettingsData.GPP).to.be.equal(displayBidderRequestWithConsents.gppConsent.gppString); + expect(bidRequestWithPubSettingsData.GPPSid).to.be.equal('7,8'); + }); it('sets USP', () => { expect(bidRequestWithPubSettingsData.USP).to.be.equal(displayBidderRequestWithConsents.uspConsent); }) diff --git a/test/spec/modules/adxcgBidAdapter_spec.js b/test/spec/modules/adxcgBidAdapter_spec.js index 65c7584b428..e07e3a6e5d4 100644 --- a/test/spec/modules/adxcgBidAdapter_spec.js +++ b/test/spec/modules/adxcgBidAdapter_spec.js @@ -1,835 +1,19 @@ // jshint esversion: 6, es3: false, node: true -import {assert} from 'chai'; -import {spec} from 'modules/adxcgBidAdapter.js'; -import {config} from 'src/config.js'; -import {createEidsArray} from 'modules/userId/eids.js'; +import { assert } from 'chai'; +import { spec } from 'modules/adxcgBidAdapter.js'; +import { config } from 'src/config.js'; +import { createEidsArray } from 'modules/userId/eids.js'; +/* eslint dot-notation:0, quote-props:0 */ +import { expect } from 'chai'; + +import { syncAddFPDToBidderRequest } from '../../helpers/fpd.js'; +import { deepClone } from '../../../src/utils'; + const utils = require('src/utils'); describe('Adxcg adapter', function () { let bids = []; - describe('isBidRequestValid', function () { - let bid = { - 'bidder': 'adxcg', - 'params': { - 'adzoneid': '19910113' - } - }; - - it('should return true when required params found', function () { - assert(spec.isBidRequestValid(bid)); - - bid.params = { - adzoneid: 4332, - }; - assert(spec.isBidRequestValid(bid)); - }); - - it('should return false when required params are missing', function () { - bid.params = {}; - assert.isFalse(spec.isBidRequestValid(bid)); - - bid.params = { - mname: 'some-placement' - }; - assert.isFalse(spec.isBidRequestValid(bid)); - - bid.params = { - inv: 1234 - }; - assert.isFalse(spec.isBidRequestValid(bid)); - }); - }); - - describe('buildRequests', function () { - beforeEach(function () { - config.resetConfig(); - }); - it('should send request with correct structure', function () { - let validBidRequests = [{ - bidId: 'bidId', - params: { - adzoneid: '19910113' - } - }]; - let request = spec.buildRequests(validBidRequests, {refererInfo: {page: 'page', domain: 'localhost'}}); - - assert.equal(request.method, 'POST'); - assert.equal(request.url, 'https://pbc.adxcg.net/rtb/ortb/pbc?adExchangeId=1'); - assert.deepEqual(request.options, {contentType: 'application/json'}); - assert.ok(request.data); - }); - - describe('user privacy', function () { - it('should send GDPR Consent data to exchange if gdprApplies', function () { - let validBidRequests = [{bidId: 'bidId', params: {test: 1}}]; - let bidderRequest = { - gdprConsent: {gdprApplies: true, consentString: 'consentDataString'}, - refererInfo: {referer: 'page'} - }; - let request = JSON.parse(spec.buildRequests(validBidRequests, bidderRequest).data); - - assert.equal(request.user.ext.consent, bidderRequest.gdprConsent.consentString); - assert.equal(request.regs.ext.gdpr, bidderRequest.gdprConsent.gdprApplies); - assert.equal(typeof request.regs.ext.gdpr, 'number'); - }); - - it('should send gdpr as number', function () { - let validBidRequests = [{bidId: 'bidId', params: {test: 1}}]; - let bidderRequest = { - gdprConsent: {gdprApplies: true, consentString: 'consentDataString'}, - refererInfo: {referer: 'page'} - }; - let request = JSON.parse(spec.buildRequests(validBidRequests, bidderRequest).data); - - assert.equal(typeof request.regs.ext.gdpr, 'number'); - assert.equal(request.regs.ext.gdpr, 1); - }); - - it('should send CCPA Consent data to exchange', function () { - let validBidRequests = [{bidId: 'bidId', params: {test: 1}}]; - let bidderRequest = {uspConsent: '1YA-', refererInfo: {referer: 'page'}}; - let request = JSON.parse(spec.buildRequests(validBidRequests, bidderRequest).data); - - assert.equal(request.regs.ext.us_privacy, '1YA-'); - - bidderRequest = { - uspConsent: '1YA-', - gdprConsent: {gdprApplies: true, consentString: 'consentDataString'}, - refererInfo: {referer: 'page'} - }; - request = JSON.parse(spec.buildRequests(validBidRequests, bidderRequest).data); - - assert.equal(request.regs.ext.us_privacy, '1YA-'); - assert.equal(request.user.ext.consent, 'consentDataString'); - assert.equal(request.regs.ext.gdpr, 1); - }); - - it('should not send GDPR Consent data to adxcg if gdprApplies is undefined', function () { - let validBidRequests = [{ - bidId: 'bidId', - params: {siteId: 'siteId'} - }]; - let bidderRequest = { - gdprConsent: {gdprApplies: false, consentString: 'consentDataString'}, - refererInfo: {referer: 'page'} - }; - let request = JSON.parse(spec.buildRequests(validBidRequests, bidderRequest).data); - - assert.equal(request.user.ext.consent, 'consentDataString'); - assert.equal(request.regs.ext.gdpr, 0); - - bidderRequest = {gdprConsent: {consentString: 'consentDataString'}, refererInfo: {referer: 'page'}}; - request = JSON.parse(spec.buildRequests(validBidRequests, bidderRequest).data); - - assert.equal(request.user, undefined); - assert.equal(request.regs, undefined); - }); - it('should send default GDPR Consent data to exchange', function () { - let validBidRequests = [{ - bidId: 'bidId', - params: {siteId: 'siteId'} - }]; - let request = JSON.parse(spec.buildRequests(validBidRequests, {refererInfo: {referer: 'page'}}).data); - - assert.equal(request.user, undefined); - assert.equal(request.regs, undefined); - }); - }); - - it('should add test and is_debug to request, if test is set in parameters', function () { - let validBidRequests = [{ - bidId: 'bidId', - params: {test: 1} - }]; - let request = JSON.parse(spec.buildRequests(validBidRequests, {refererInfo: {referer: 'page'}}).data); - - assert.ok(request.is_debug); - assert.equal(request.test, 1); - }); - - it('should have default request structure', function () { - let keys = 'site,geo,device,source,ext,imp'.split(','); - let validBidRequests = [{ - bidId: 'bidId', - params: {siteId: 'siteId'} - }]; - let request = JSON.parse(spec.buildRequests(validBidRequests, {refererInfo: {referer: 'page'}}).data); - let data = Object.keys(request); - - assert.deepEqual(keys, data); - }); - - it('should set request keys correct values', function () { - let validBidRequests = [{ - bidId: 'bidId', - params: {siteId: 'siteId'}, - }]; - let request = JSON.parse(spec.buildRequests(validBidRequests, { - refererInfo: {referer: 'page'}, - ortb2: {source: {tid: 'tid'}} - }).data); - - assert.equal(request.source.tid, 'tid'); - assert.equal(request.source.fd, 1); - }); - - it('should send info about device', function () { - config.setConfig({ - device: {w: 100, h: 100} - }); - let validBidRequests = [{ - bidId: 'bidId', - params: {adzoneid: '1000'} - }]; - let request = JSON.parse(spec.buildRequests(validBidRequests, {refererInfo: {page: 'page', domain: 'localhost'}}).data); - - assert.equal(request.device.ua, navigator.userAgent); - assert.equal(request.device.w, 100); - assert.equal(request.device.h, 100); - }); - - it('should send app info', function () { - config.setConfig({ - app: {id: 'appid'}, - }); - const ortb2 = {app: {name: 'appname'}} - let validBidRequests = [{ - bidId: 'bidId', - params: {adzoneid: '1000'}, - ortb2 - }]; - let request = JSON.parse(spec.buildRequests(validBidRequests, {refererInfo: {referer: 'page'}, ortb2}).data); - - assert.equal(request.app.id, 'appid'); - assert.equal(request.app.name, 'appname'); - assert.equal(request.site, undefined); - }); - - it('should send info about the site', function () { - config.setConfig({ - site: { - id: '123123', - publisher: { - domain: 'publisher.domain.com' - } - }, - }); - const ortb2 = { - site: { - publisher: { - id: 4441, - name: 'publisher\'s name' - } - } - }; - - let validBidRequests = [{ - bidId: 'bidId', - params: {adzoneid: '1000'}, - ortb2 - }]; - let refererInfo = {page: 'page', domain: 'localhost'}; - let request = JSON.parse(spec.buildRequests(validBidRequests, {refererInfo, ortb2}).data); - - assert.deepEqual(request.site, { - domain: 'localhost', - id: '123123', - page: refererInfo.page, - publisher: { - domain: 'publisher.domain.com', - id: 4441, - name: 'publisher\'s name' - } - }); - }); - - it('should pass extended ids', function () { - let validBidRequests = [{ - bidId: 'bidId', - params: {}, - userIdAsEids: createEidsArray({ - tdid: 'TTD_ID_FROM_USER_ID_MODULE', - pubcid: 'pubCommonId_FROM_USER_ID_MODULE' - }) - }]; - - let request = JSON.parse(spec.buildRequests(validBidRequests, {refererInfo: {referer: 'page'}}).data); - assert.deepEqual(request.user.ext.eids, [ - {source: 'adserver.org', uids: [{id: 'TTD_ID_FROM_USER_ID_MODULE', atype: 1, ext: {rtiPartner: 'TDID'}}]}, - {source: 'pubcid.org', uids: [{id: 'pubCommonId_FROM_USER_ID_MODULE', atype: 1}]} - ]); - }); - - it('should send currency if defined', function () { - config.setConfig({currency: {adServerCurrency: 'EUR'}}); - let validBidRequests = [{params: {}}]; - let refererInfo = {referer: 'page'}; - let request = JSON.parse(spec.buildRequests(validBidRequests, {refererInfo}).data); - - assert.deepEqual(request.cur, ['EUR']); - }); - - it('should pass supply chain object', function () { - let validBidRequests = [{ - bidId: 'bidId', - params: {}, - schain: { - validation: 'strict', - config: { - ver: '1.0' - } - } - }]; - - let request = JSON.parse(spec.buildRequests(validBidRequests, {refererInfo: {referer: 'page'}}).data); - assert.deepEqual(request.source.ext.schain, { - validation: 'strict', - config: { - ver: '1.0' - } - }); - }); - - describe('bids', function () { - it('should add more than one bid to the request', function () { - let validBidRequests = [{ - bidId: 'bidId', - params: {siteId: 'siteId'} - }, { - bidId: 'bidId2', - params: {siteId: 'siteId'} - }]; - let request = JSON.parse(spec.buildRequests(validBidRequests, {refererInfo: {referer: 'page'}}).data); - - assert.equal(request.imp.length, 2); - }); - it('should add incrementing values of id', function () { - let validBidRequests = [{ - bidId: 'bidId', - params: {adzoneid: '1000'}, - mediaTypes: {video: {}} - }, { - bidId: 'bidId2', - params: {adzoneid: '1000'}, - mediaTypes: {video: {}} - }, { - bidId: 'bidId3', - params: {adzoneid: '1000'}, - mediaTypes: {video: {}} - }]; - let imps = JSON.parse(spec.buildRequests(validBidRequests, {refererInfo: {referer: 'page'}}).data).imp; - - for (let i = 0; i < 3; i++) { - assert.equal(imps[i].id, i + 1); - } - }); - - it('should add adzoneid', function () { - let validBidRequests = [{bidId: 'bidId', params: {adzoneid: 1000}, mediaTypes: {video: {}}}, - {bidId: 'bidId2', params: {adzoneid: 1001}, mediaTypes: {video: {}}}, - {bidId: 'bidId3', params: {adzoneid: 1002}, mediaTypes: {video: {}}}]; - let imps = JSON.parse(spec.buildRequests(validBidRequests, {refererInfo: {referer: 'page'}}).data).imp; - for (let i = 0; i < 3; i++) { - assert.equal(imps[i].tagid, validBidRequests[i].params.adzoneid); - } - }); - - describe('price floors', function () { - it('should not add if floors module not configured', function () { - const validBidRequests = [{bidId: 'bidId', params: {adzoneid: 1000}, mediaTypes: {video: {}}}]; - let imp = getRequestImps(validBidRequests)[0]; - - assert.equal(imp.bidfloor, undefined); - assert.equal(imp.bidfloorcur, undefined); - }); - - it('should not add if floor price not defined', function () { - const validBidRequests = [getBidWithFloor()]; - let imp = getRequestImps(validBidRequests)[0]; - - assert.equal(imp.bidfloor, undefined); - assert.equal(imp.bidfloorcur, 'USD'); - }); - - it('should request floor price in adserver currency', function () { - config.setConfig({currency: {adServerCurrency: 'DKK'}}); - const validBidRequests = [getBidWithFloor()]; - let imp = getRequestImps(validBidRequests)[0]; - - assert.equal(imp.bidfloor, undefined); - assert.equal(imp.bidfloorcur, 'DKK'); - }); - - it('should add correct floor values', function () { - const expectedFloors = [1, 1.3, 0.5]; - const validBidRequests = expectedFloors.map(getBidWithFloor); - let imps = getRequestImps(validBidRequests); - - expectedFloors.forEach((floor, index) => { - assert.equal(imps[index].bidfloor, floor); - assert.equal(imps[index].bidfloorcur, 'USD'); - }); - }); - - function getBidWithFloor(floor) { - return { - params: {adzoneid: 1}, - mediaTypes: {video: {}}, - getFloor: ({currency}) => { - return { - currency: currency, - floor - }; - } - }; - } - }); - - describe('multiple media types', function () { - it('should use all configured media types for bidding', function () { - let validBidRequests = [{ - bidId: 'bidId', - params: {adzoneid: 1000}, - mediaTypes: { - banner: { - sizes: [[100, 100], [200, 300]] - }, - video: {} - } - }, { - bidId: 'bidId1', - params: {adzoneid: 1000}, - mediaTypes: { - video: {}, - native: {} - } - }, { - bidId: 'bidId2', - params: {adzoneid: 1000}, - nativeParams: { - title: {required: true, len: 140} - }, - mediaTypes: { - banner: { - sizes: [[100, 100], [200, 300]] - }, - native: {}, - video: {} - } - }]; - let [first, second, third] = JSON.parse(spec.buildRequests(validBidRequests, {refererInfo: {referer: 'page'}}).data).imp; - - assert.ok(first.banner); - assert.ok(first.video); - assert.equal(first.native, undefined); - - assert.ok(second.video); - assert.equal(second.banner, undefined); - assert.equal(second.native, undefined); - - assert.ok(third.native); - assert.ok(third.video); - assert.ok(third.banner); - }); - }); - - describe('banner', function () { - it('should convert sizes to openrtb format', function () { - let validBidRequests = [{ - bidId: 'bidId', - params: {adzoneid: 1000}, - mediaTypes: { - banner: { - sizes: [[100, 100], [200, 300]] - } - } - }]; - let {banner} = JSON.parse(spec.buildRequests(validBidRequests, {refererInfo: {referer: 'page'}}).data).imp[0]; - assert.deepEqual(banner, { - format: [{w: 100, h: 100}, {w: 200, h: 300}] - }); - }); - }); - - describe('video', function () { - it('should pass video mediatype config', function () { - let validBidRequests = [{ - bidId: 'bidId', - params: {adzoneid: 1000}, - mediaTypes: { - video: { - playerSize: [640, 480], - context: 'outstream', - mimes: ['video/mp4'] - } - } - }]; - let {video} = JSON.parse(spec.buildRequests(validBidRequests, {refererInfo: {referer: 'page'}}).data).imp[0]; - assert.deepEqual(video, { - playerSize: [640, 480], - context: 'outstream', - mimes: ['video/mp4'] - }); - }); - }); - - describe('native', function () { - describe('assets', function () { - it('should set correct asset id', function () { - let validBidRequests = [{ - bidId: 'bidId', - params: {adzoneid: 1000}, - nativeParams: { - title: {required: true, len: 140}, - image: {required: false, wmin: 836, hmin: 627, w: 325, h: 300, mimes: ['image/jpg', 'image/gif']}, - body: {len: 140} - } - }]; - let nativeRequest = JSON.parse(spec.buildRequests(validBidRequests, {refererInfo: {referer: 'page'}}).data).imp[0].native.request; - let assets = JSON.parse(nativeRequest).assets; - - assert.equal(assets[0].id, 0); - assert.equal(assets[1].id, 3); - assert.equal(assets[2].id, 4); - }); - it('should add required key if it is necessary', function () { - let validBidRequests = [{ - bidId: 'bidId', - params: {adzoneid: 1000}, - nativeParams: { - title: {required: true, len: 140}, - image: {required: false, wmin: 836, hmin: 627, w: 325, h: 300, mimes: ['image/jpg', 'image/gif']}, - body: {len: 140}, - sponsoredBy: {required: true, len: 140} - } - }]; - - let nativeRequest = JSON.parse(spec.buildRequests(validBidRequests, {refererInfo: {referer: 'page'}}).data).imp[0].native.request; - let assets = JSON.parse(nativeRequest).assets; - - assert.equal(assets[0].required, 1); - assert.ok(!assets[1].required); - assert.ok(!assets[2].required); - assert.equal(assets[3].required, 1); - }); - - it('should map img and data assets', function () { - let validBidRequests = [{ - bidId: 'bidId', - params: {adzoneid: 1000}, - nativeParams: { - title: {required: true, len: 140}, - image: {required: true, sizes: [150, 50]}, - icon: {required: false, sizes: [50, 50]}, - body: {required: false, len: 140}, - sponsoredBy: {required: true}, - cta: {required: false}, - clickUrl: {required: false} - } - }]; - - let nativeRequest = JSON.parse(spec.buildRequests(validBidRequests, {refererInfo: {referer: 'page'}}).data).imp[0].native.request; - let assets = JSON.parse(nativeRequest).assets; - assert.ok(assets[0].title); - assert.equal(assets[0].title.len, 140); - assert.deepEqual(assets[1].img, {type: 3, w: 150, h: 50}); - assert.deepEqual(assets[2].img, {type: 1, w: 50, h: 50}); - assert.deepEqual(assets[3].data, {type: 2, len: 140}); - assert.deepEqual(assets[4].data, {type: 1}); - assert.deepEqual(assets[5].data, {type: 12}); - assert.ok(!assets[6]); - }); - - describe('icon/image sizing', function () { - it('should flatten sizes and utilise first pair', function () { - const validBidRequests = [{ - bidId: 'bidId', - params: {adzoneid: 1000}, - nativeParams: { - image: { - sizes: [[200, 300], [100, 200]] - }, - } - }]; - - let nativeRequest = JSON.parse(spec.buildRequests(validBidRequests, {refererInfo: {referer: 'page'}}).data).imp[0].native.request; - let assets = JSON.parse(nativeRequest).assets; - assert.ok(assets[0].img); - assert.equal(assets[0].img.w, 200); - assert.equal(assets[0].img.h, 300); - }); - }); - - it('should utilise aspect_ratios', function () { - const validBidRequests = [{ - bidId: 'bidId', - params: {adzoneid: 1000}, - nativeParams: { - image: { - aspect_ratios: [{ - min_width: 100, - ratio_height: 3, - ratio_width: 1 - }] - }, - icon: { - aspect_ratios: [{ - min_width: 10, - ratio_height: 5, - ratio_width: 2 - }] - } - } - }]; - - let nativeRequest = JSON.parse(spec.buildRequests(validBidRequests, {refererInfo: {referer: 'page'}}).data).imp[0].native.request; - let assets = JSON.parse(nativeRequest).assets; - assert.ok(assets[0].img); - assert.equal(assets[0].img.wmin, 100); - assert.equal(assets[0].img.hmin, 300); - - assert.ok(assets[1].img); - assert.equal(assets[1].img.wmin, 10); - assert.equal(assets[1].img.hmin, 25); - }); - - it('should not throw error if aspect_ratios config is not defined', function () { - const validBidRequests = [{ - bidId: 'bidId', - params: {adzoneid: 1000}, - nativeParams: { - image: { - aspect_ratios: [] - }, - icon: { - aspect_ratios: [] - } - } - }]; - - assert.doesNotThrow(() => spec.buildRequests(validBidRequests, {refererInfo: {referer: 'page'}})); - }); - }); - - it('should expect any dimensions if min_width not passed', function () { - const validBidRequests = [{ - bidId: 'bidId', - params: {adzoneid: 1000}, - nativeParams: { - image: { - aspect_ratios: [{ - ratio_height: 3, - ratio_width: 1 - }] - } - } - }]; - - let nativeRequest = JSON.parse(spec.buildRequests(validBidRequests, {refererInfo: {referer: 'page'}}).data).imp[0].native.request; - let assets = JSON.parse(nativeRequest).assets; - assert.ok(assets[0].img); - assert.equal(assets[0].img.wmin, 0); - assert.equal(assets[0].img.hmin, 0); - assert.ok(!assets[1]); - }); - }); - }); - - function getRequestImps(validBidRequests) { - return JSON.parse(spec.buildRequests(validBidRequests, {refererInfo: {referer: 'page'}}).data).imp; - } - }); - - describe('interpretResponse', function () { - it('should return if no body in response', function () { - let serverResponse = {}; - let bidRequest = {}; - - assert.ok(!spec.interpretResponse(serverResponse, bidRequest)); - }); - it('should return more than one bids', function () { - let serverResponse = { - body: { - seatbid: [{ - bid: [{ - impid: '1', - native: {ver: '1.1', link: {url: 'link'}, assets: [{id: 1, title: {text: 'Asset title text'}}]} - }] - }, { - bid: [{ - impid: '2', - native: {ver: '1.1', link: {url: 'link'}, assets: [{id: 1, data: {value: 'Asset title text'}}]} - }] - }] - } - }; - let bidRequest = { - data: {}, - bids: [ - { - bidId: 'bidId1', - params: {adzoneid: 1000}, - nativeParams: { - title: {required: true, len: 140}, - image: {required: false, wmin: 836, hmin: 627, w: 325, h: 300, mimes: ['image/jpg', 'image/gif']}, - body: {len: 140} - } - }, - { - bidId: 'bidId2', - params: {adzoneid: 1000}, - nativeParams: { - title: {required: true, len: 140}, - image: {required: false, wmin: 836, hmin: 627, w: 325, h: 300, mimes: ['image/jpg', 'image/gif']}, - body: {len: 140} - } - } - ] - }; - - bids = spec.interpretResponse(serverResponse, bidRequest); - assert.equal(spec.interpretResponse(serverResponse, bidRequest).length, 2); - }); - - it('should set correct values to bid', function () { - let nativeExample1 = { - assets: [], - link: {url: 'link'}, - imptrackers: ['imptrackers url1', 'imptrackers url2'] - } - - let serverResponse = { - body: { - id: null, - bidid: null, - seatbid: [{ - bid: [ - { - impid: '1', - price: 93.1231, - crid: '12312312', - adm: JSON.stringify(nativeExample1), - dealid: 'deal-id', - adomain: ['demo.com'], - ext: { - crType: 'native', - advertiser_id: 'adv1', - advertiser_name: 'advname', - agency_name: 'agname', - mediaType: 'native' - } - } - ] - }], - cur: 'EUR' - } - }; - let bidRequest = { - data: {}, - bids: [ - { - bidId: 'bidId1', - params: {adzoneid: 1000}, - nativeParams: { - title: {required: true, len: 140}, - image: {required: false, wmin: 836, hmin: 627, w: 325, h: 300, mimes: ['image/jpg', 'image/gif']}, - body: {len: 140} - } - } - ] - }; - - const bids = spec.interpretResponse(serverResponse, bidRequest); - const bid = serverResponse.body.seatbid[0].bid[0]; - assert.deepEqual(bids[0].requestId, bidRequest.bids[0].bidId); - assert.deepEqual(bids[0].cpm, bid.price); - assert.deepEqual(bids[0].creativeId, bid.crid); - assert.deepEqual(bids[0].ttl, 300); - assert.deepEqual(bids[0].netRevenue, false); - assert.deepEqual(bids[0].currency, serverResponse.body.cur); - assert.deepEqual(bids[0].mediaType, 'native'); - assert.deepEqual(bids[0].meta.mediaType, 'native'); - assert.deepEqual(bids[0].meta.advertiserDomains, ['demo.com']); - - assert.deepEqual(bids[0].meta.advertiserName, 'advname'); - assert.deepEqual(bids[0].meta.agencyName, 'agname'); - - assert.deepEqual(bids[0].dealId, 'deal-id'); - }); - - it('should return empty when there is no bids in response', function () { - const serverResponse = { - body: { - id: null, - bidid: null, - seatbid: [{bid: []}], - cur: 'EUR' - } - }; - let bidRequest = { - data: {}, - bids: [{bidId: 'bidId1'}] - }; - const result = spec.interpretResponse(serverResponse, bidRequest)[0]; - assert.ok(!result); - }); - - describe('banner', function () { - it('should set ad content on response', function () { - let serverResponse = { - body: { - seatbid: [{ - bid: [{impid: '1', adm: '', ext: {crType: 'banner'}}] - }] - } - }; - let bidRequest = { - data: {}, - bids: [ - { - bidId: 'bidId1', - params: {adzoneid: 1000} - } - ] - }; - - bids = spec.interpretResponse(serverResponse, bidRequest); - assert.equal(bids.length, 1); - assert.equal(bids[0].ad, ''); - assert.equal(bids[0].mediaType, 'banner'); - assert.equal(bids[0].meta.mediaType, 'banner'); - }); - }); - - describe('video', function () { - it('should set vastXml on response', function () { - let serverResponse = { - body: { - seatbid: [{ - bid: [{impid: '1', adm: '', ext: {crType: 'video'}}] - }] - } - }; - let bidRequest = { - data: {}, - bids: [ - { - bidId: 'bidId1', - params: {adzoneid: 1000} - } - ] - }; - - bids = spec.interpretResponse(serverResponse, bidRequest); - assert.equal(bids.length, 1); - assert.equal(bids[0].vastXml, ''); - assert.equal(bids[0].mediaType, 'video'); - assert.equal(bids[0].meta.mediaType, 'video'); - }); - }); - }); - describe('getUserSyncs', function () { const usersyncUrl = 'https://usersync-url.com'; beforeEach(() => { @@ -846,55 +30,55 @@ describe('Adxcg adapter', function () { }) it('should return user sync if pixel enabled with adxcg config', function () { - const ret = spec.getUserSyncs({pixelEnabled: true}) - expect(ret).to.deep.equal([{type: 'image', url: usersyncUrl}]) + const ret = spec.getUserSyncs({ pixelEnabled: true }) + expect(ret).to.deep.equal([{ type: 'image', url: usersyncUrl }]) }) it('should not return user sync if pixel disabled', function () { - const ret = spec.getUserSyncs({pixelEnabled: false}) + const ret = spec.getUserSyncs({ pixelEnabled: false }) expect(ret).to.be.an('array').that.is.empty }) it('should not return user sync if url is not set', function () { config.resetConfig() - const ret = spec.getUserSyncs({pixelEnabled: true}) + const ret = spec.getUserSyncs({ pixelEnabled: true }) expect(ret).to.be.an('array').that.is.empty }) - it('should pass GDPR consent', function() { - expect(spec.getUserSyncs({ pixelEnabled: true }, {}, {gdprApplies: true, consentString: 'foo'}, undefined)).to.deep.equal([{ + it('should pass GDPR consent', function () { + expect(spec.getUserSyncs({ pixelEnabled: true }, {}, { gdprApplies: true, consentString: 'foo' }, undefined)).to.deep.equal([{ type: 'image', url: `${usersyncUrl}?gdpr=1&gdpr_consent=foo` }]); - expect(spec.getUserSyncs({ pixelEnabled: true }, {}, {gdprApplies: false, consentString: 'foo'}, undefined)).to.deep.equal([{ + expect(spec.getUserSyncs({ pixelEnabled: true }, {}, { gdprApplies: false, consentString: 'foo' }, undefined)).to.deep.equal([{ type: 'image', url: `${usersyncUrl}?gdpr=0&gdpr_consent=foo` }]); - expect(spec.getUserSyncs({ pixelEnabled: true }, {}, {gdprApplies: true, consentString: undefined}, undefined)).to.deep.equal([{ + expect(spec.getUserSyncs({ pixelEnabled: true }, {}, { gdprApplies: true, consentString: undefined }, undefined)).to.deep.equal([{ type: 'image', url: `${usersyncUrl}?gdpr=1&gdpr_consent=` }]); }); - it('should pass US consent', function() { + it('should pass US consent', function () { expect(spec.getUserSyncs({ pixelEnabled: true }, {}, undefined, '1NYN')).to.deep.equal([{ type: 'image', url: `${usersyncUrl}?us_privacy=1NYN` }]); }); - it('should pass GDPR and US consent', function() { - expect(spec.getUserSyncs({ pixelEnabled: true }, {}, {gdprApplies: true, consentString: 'foo'}, '1NYN')).to.deep.equal([{ + it('should pass GDPR and US consent', function () { + expect(spec.getUserSyncs({ pixelEnabled: true }, {}, { gdprApplies: true, consentString: 'foo' }, '1NYN')).to.deep.equal([{ type: 'image', url: `${usersyncUrl}?gdpr=1&gdpr_consent=foo&us_privacy=1NYN` }]); }); }); - describe('onBidWon', function() { - beforeEach(function() { + describe('onBidWon', function () { + beforeEach(function () { sinon.stub(utils, 'triggerPixel'); }); - afterEach(function() { + afterEach(function () { utils.triggerPixel.restore(); }); - it('Should trigger pixel if bid nurl', function() { + it('Should trigger pixel if bid nurl', function () { const bid = { nurl: 'http://example.com/win/${AUCTION_PRICE}', cpm: 2.1, @@ -904,4 +88,510 @@ describe('Adxcg adapter', function () { expect(utils.triggerPixel.callCount).to.equal(1) }) }) + + it('should return just to have at least 1 karma test ok', function () { + assert(true); + }); +}); + +describe('adxcg v8 oRtbConverter Adapter Tests', function () { + const slotConfigs = [{ + placementCode: '/DfpAccount1/slot1', + mediaTypes: { + banner: { + sizes: [[728, 90], [160, 600]] + } + }, + bidId: 'bid12345', + params: { + cp: 'p10000', + ct: 't10000', + cf: '300x250', + adzoneid: '77' + } + }, { + placementCode: '/DfpAccount2/slot2', + mediaTypes: { + banner: { + sizes: [[728, 90]] + } + }, + bidId: 'bid23456', + params: { + cp: 'p10000', + ct: 't20000', + cf: '728x90', + adzoneid: '77' + } + }]; + const nativeOrtbRequest = { + assets: [{ + id: 1, + required: 1, + img: { + type: 3, + w: 150, + h: 50, + } + }, + { + id: 2, + required: 1, + title: { + len: 80 + } + }, + { + id: 3, + required: 0, + data: { + type: 1 + } + }] + }; + const nativeSlotConfig = [{ + placementCode: '/DfpAccount1/slot3', + bidId: 'bid12345', + mediaTypes: { + native: { + sendTargetingKeys: false, + ortb: nativeOrtbRequest + } + }, + nativeOrtbRequest, + params: { + cp: 'p10000', + ct: 't10000', + adzoneid: '77' + } + }]; + const videoSlotConfig = [{ + placementCode: '/DfpAccount1/slotVideo', + bidId: 'bid12345', + mediaTypes: { + video: { + playerSize: [400, 300], + w: 400, + h: 300, + minduration: 5, + maxduration: 10, + startdelay: 0, + skip: 1, + minbitrate: 200, + protocols: [1, 2, 4] + } + }, + params: { + cp: 'p10000', + ct: 't10000', + adzoneid: '77' + } + }]; + const additionalParamsConfig = [{ + placementCode: '/DfpAccount1/slot1', + mediaTypes: { + banner: { + sizes: [[1, 1]] + } + }, + bidId: 'bid12345', + params: { + cp: 'p10000', + ct: 't10000', + cf: '1x1', + adzoneid: '77', + extra_key1: 'extra_val1', + extra_key2: 12345, + extra_key3: { + key1: 'val1', + key2: 23456, + }, + extra_key4: [1, 2, 3] + } + }]; + + const schainParamsSlotConfig = [{ + placementCode: '/DfpAccount1/slot1', + mediaTypes: { + banner: { + sizes: [[1, 1]] + } + }, + bidId: 'bid12345', + params: { + cp: 'p10000', + ct: 't10000', + cf: '1x1', + adzoneid: '77', + bcat: ['IAB-1', 'IAB-20'], + battr: [1, 2, 3], + bidfloor: 1.5, + badv: ['cocacola.com', 'lays.com'] + }, + schain: { + 'ver': '1.0', + 'complete': 1, + 'nodes': [ + { + 'asi': 'exchange1.com', + 'sid': '1234', + 'hp': 1, + 'rid': 'bid-request-1', + 'name': 'publisher', + 'domain': 'publisher.com' + } + ] + }, + }]; + + const bidderRequest = { + refererInfo: { + page: 'https://publisher.com/home', + ref: 'https://referrer' + } + }; + + it('Verify build request', function () { + const request = spec.buildRequests(slotConfigs, syncAddFPDToBidderRequest(bidderRequest)); + expect(request.url).to.equal('https://pbc.adxcg.net/rtb/ortb/pbc?adExchangeId=1'); + expect(request.method).to.equal('POST'); + const ortbRequest = request.data; + // site object + expect(ortbRequest.site).to.not.equal(null); + expect(ortbRequest.site.publisher).to.not.equal(null); + // expect(ortbRequest.site.publisher.id).to.equal('p10000'); + expect(ortbRequest.site.page).to.equal('https://publisher.com/home'); + expect(ortbRequest.imp).to.have.lengthOf(2); + // device object + expect(ortbRequest.device).to.not.equal(null); + expect(ortbRequest.device.ua).to.equal(navigator.userAgent); + // slot 1 + // expect(ortbRequest.imp[0].tagid).to.equal('t10000'); + expect(ortbRequest.imp[0].banner).to.not.equal(null); + expect(ortbRequest.imp[0].banner.format).to.deep.eq([{ 'w': 728, 'h': 90 }, { 'w': 160, 'h': 600 }]); + // slot 2 + // expect(ortbRequest.imp[1].tagid).to.equal('t20000'); + expect(ortbRequest.imp[1].banner).to.not.equal(null); + expect(ortbRequest.imp[1].banner.format).to.deep.eq([{ 'w': 728, 'h': 90 }]); + }); + + it('Verify parse response', function () { + const request = spec.buildRequests(slotConfigs, syncAddFPDToBidderRequest(bidderRequest)); + const ortbRequest = request.data; + const ortbResponse = { + seatbid: [{ + bid: [{ + impid: ortbRequest.imp[0].id, + price: 1.25, + adm: 'This is an Ad', + crid: 'Creative#123', + mtype: 1, + w: 300, + h: 250, + exp: 20, + adomain: ['advertiser.com'] + }] + }] + }; + const bids = spec.interpretResponse({ body: ortbResponse }, request); + expect(bids).to.have.lengthOf(1); + // verify first bid + const bid = bids[0]; + expect(bid.cpm).to.equal(1.25); + expect(bid.ad).to.equal('This is an Ad'); + expect(bid.width).to.equal(300); + expect(bid.height).to.equal(250); + expect(bid.creative_id).to.equal('Creative#123'); + expect(bid.creativeId).to.equal('Creative#123'); + expect(bid.netRevenue).to.equal(true); + expect(bid.currency).to.equal('EUR'); + expect(bid.ttl).to.equal(20); + expect(bid.meta).to.not.be.null; + expect(bid.meta.advertiserDomains).to.eql(['advertiser.com']); + }); + + it('Verify full passback', function () { + const request = spec.buildRequests(slotConfigs, bidderRequest); + const bids = spec.interpretResponse({ body: null }, request) + expect(bids).to.have.lengthOf(0); + }); + + if (FEATURES.NATIVE) { + it('Verify Native request', function () { + const request = spec.buildRequests(nativeSlotConfig, syncAddFPDToBidderRequest(bidderRequest)); + expect(request.url).to.equal('https://pbc.adxcg.net/rtb/ortb/pbc?adExchangeId=1'); + expect(request.method).to.equal('POST'); + const ortbRequest = request.data; + // native impression + expect(ortbRequest.imp[0].tagid).to.equal('77'); + expect(ortbRequest.imp[0].banner).to.be.undefined; + const nativePart = ortbRequest.imp[0]['native']; + expect(nativePart).to.not.equal(null); + expect(nativePart.request).to.not.equal(null); + // native request assets + const nativeRequest = JSON.parse(ortbRequest.imp[0]['native'].request); + expect(nativeRequest).to.not.equal(null); + expect(nativeRequest.assets).to.have.lengthOf(3); + // image asset + expect(nativeRequest.assets[0].id).to.equal(1); + expect(nativeRequest.assets[0].required).to.equal(1); + expect(nativeRequest.assets[0].title).to.be.undefined; + expect(nativeRequest.assets[0].img).to.not.equal(null); + expect(nativeRequest.assets[0].img.w).to.equal(150); + expect(nativeRequest.assets[0].img.h).to.equal(50); + expect(nativeRequest.assets[0].img.type).to.equal(3); + // title asset + expect(nativeRequest.assets[1].id).to.equal(2); + expect(nativeRequest.assets[1].required).to.equal(1); + expect(nativeRequest.assets[1].title).to.not.equal(null); + expect(nativeRequest.assets[1].title.len).to.equal(80); + // data asset + expect(nativeRequest.assets[2].id).to.equal(3); + expect(nativeRequest.assets[2].required).to.equal(0); + expect(nativeRequest.assets[2].title).to.be.undefined; + expect(nativeRequest.assets[2].data).to.not.equal(null); + expect(nativeRequest.assets[2].data.type).to.equal(1); + }); + + it('Verify Native response', function () { + const request = spec.buildRequests(nativeSlotConfig, syncAddFPDToBidderRequest(bidderRequest)); + expect(request.url).to.equal('https://pbc.adxcg.net/rtb/ortb/pbc?adExchangeId=1'); + expect(request.method).to.equal('POST'); + const ortbRequest = request.data; + const nativeResponse = { + assets: [ + { id: 1, img: { type: 3, url: 'https://images.cdn.brand.com/123' } }, + { id: 2, title: { text: 'Ad Title' } }, + { id: 3, data: { type: 1, value: 'Sponsored By: Brand' } } + ], + link: { url: 'https://brand.clickme.com/' }, + imptrackers: ['https://imp1.trackme.com/', 'https://imp1.contextweb.com/'] + + }; + const ortbResponse = { + seatbid: [{ + bid: [{ + impid: ortbRequest.imp[0].id, + price: 1.25, + adm: JSON.stringify(nativeResponse), + mtype: 4 + }] + }] + }; + const bids = spec.interpretResponse({ body: ortbResponse }, request); + // verify bid + const bid = bids[0]; + expect(bid.cpm).to.equal(1.25); + expect(bid.requestId).to.equal('bid12345'); + expect(bid.ad).to.be.undefined; + expect(bid.mediaType).to.equal('native'); + expect(bid['native']).to.not.be.null; + expect(bid['native'].ortb).to.not.be.null; + const nativeBid = bid['native'].ortb; + expect(nativeBid.assets).to.have.lengthOf(3); + expect(nativeBid.assets[0].id).to.equal(1); + expect(nativeBid.assets[0].img).to.not.be.null; + expect(nativeBid.assets[0].img.type).to.equal(3); + expect(nativeBid.assets[0].img.url).to.equal('https://images.cdn.brand.com/123'); + expect(nativeBid.assets[1].id).to.equal(2); + expect(nativeBid.assets[1].title).to.not.be.null; + expect(nativeBid.assets[1].title.text).to.equal('Ad Title'); + expect(nativeBid.assets[2].id).to.equal(3); + expect(nativeBid.assets[2].data).to.not.be.null; + expect(nativeBid.assets[2].data.type).to.equal(1); + expect(nativeBid.assets[2].data.value).to.equal('Sponsored By: Brand'); + expect(nativeBid.link).to.not.be.null; + expect(nativeBid.link.url).to.equal('https://brand.clickme.com/'); + expect(nativeBid.imptrackers).to.have.lengthOf(2); + expect(nativeBid.imptrackers[0]).to.equal('https://imp1.trackme.com/'); + expect(nativeBid.imptrackers[1]).to.equal('https://imp1.contextweb.com/'); + }); + } + + it('Verifies bidder code', function () { + expect(spec.code).to.equal('adxcg'); + }); + + it('Verifies bidder aliases', function () { + expect(spec.aliases).to.have.lengthOf(1); + expect(spec.aliases[0]).to.equal('mediaopti'); + }); + + it('Verifies supported media types', function () { + expect(spec.supportedMediaTypes).to.have.lengthOf(3); + expect(spec.supportedMediaTypes[0]).to.equal('banner'); + expect(spec.supportedMediaTypes[1]).to.equal('native'); + expect(spec.supportedMediaTypes[2]).to.equal('video'); + }); + + if (FEATURES.VIDEO) { + it('Verify Video request', function () { + const request = spec.buildRequests(videoSlotConfig, syncAddFPDToBidderRequest(bidderRequest)); + expect(request.url).to.equal('https://pbc.adxcg.net/rtb/ortb/pbc?adExchangeId=1'); + expect(request.method).to.equal('POST'); + const ortbRequest = request.data; + expect(ortbRequest).to.not.equal(null); + expect(ortbRequest.imp).to.have.lengthOf(1); + expect(ortbRequest.imp[0].video).to.not.be.null; + expect(ortbRequest.imp[0].native).to.be.undefined; + expect(ortbRequest.imp[0].banner).to.be.undefined; + expect(ortbRequest.imp[0].video.w).to.equal(400); + expect(ortbRequest.imp[0].video.h).to.equal(300); + expect(ortbRequest.imp[0].video.minduration).to.equal(5); + expect(ortbRequest.imp[0].video.maxduration).to.equal(10); + expect(ortbRequest.imp[0].video.startdelay).to.equal(0); + expect(ortbRequest.imp[0].video.skip).to.equal(1); + expect(ortbRequest.imp[0].video.minbitrate).to.equal(200); + expect(ortbRequest.imp[0].video.protocols).to.eql([1, 2, 4]); + }); + } + + it('Verify extra parameters', function () { + let request = spec.buildRequests(additionalParamsConfig, syncAddFPDToBidderRequest(bidderRequest)); + let ortbRequest = request.data; + expect(ortbRequest).to.not.equal(null); + expect(ortbRequest.imp).to.have.lengthOf(1); + expect(ortbRequest.imp[0].ext).to.not.equal(null); + expect(ortbRequest.imp[0].ext.prebid).to.not.equal(null); + expect(ortbRequest.imp[0].ext.prebid).to.not.be.null; + expect(ortbRequest.imp[0].ext.prebid.extra_key1).to.equal('extra_val1'); + expect(ortbRequest.imp[0].ext.prebid.extra_key2).to.equal(12345); + expect(ortbRequest.imp[0].ext.prebid.extra_key3).to.not.be.null; + expect(ortbRequest.imp[0].ext.prebid.extra_key3.key1).to.equal('val1'); + expect(ortbRequest.imp[0].ext.prebid.extra_key3.key2).to.equal(23456); + expect(ortbRequest.imp[0].ext.prebid.extra_key4).to.eql([1, 2, 3]); + expect(Object.keys(ortbRequest.imp[0].ext.prebid)).to.eql(['adzoneid', 'extra_key1', 'extra_key2', 'extra_key3', 'extra_key4']); + // attempting with a configuration with no unknown params. + request = spec.buildRequests(videoSlotConfig, bidderRequest); + ortbRequest = request.data; + expect(ortbRequest).to.not.equal(null); + expect(ortbRequest.imp).to.have.lengthOf(1); + // expect(ortbRequest.imp[0].ext).to.be.undefined; + }); + + it('Verify user level first party data', function () { + const bidderRequest = { + refererInfo: { + page: 'https://publisher.com/home', + ref: 'https://referrer' + }, + gdprConsent: { + gdprApplies: true, + consentString: 'serialized_gpdr_data' + }, + ortb2: { + user: { + yob: 1985, + gender: 'm', + ext: { + data: { + registered: true, + interests: ['cars'] + } + } + } + } + }; + let request = spec.buildRequests(slotConfigs, syncAddFPDToBidderRequest(bidderRequest)); + let ortbRequest = request.data; + expect(ortbRequest).to.not.equal(null); + expect(ortbRequest.user).to.not.equal(null); + }); + + it('Verify site level first party data', function () { + const bidderRequest = { + ortb2: { + site: { + content: { + data: [{ + name: 'www.iris.com', + ext: { + segtax: 500, + cids: ['iris_c73g5jq96mwso4d8'] + } + }] + }, + page: 'http://pub.com/news', + ref: 'http://google.com', + publisher: { + domain: 'pub.com' + } + } + } + }; + let request = spec.buildRequests(slotConfigs, syncAddFPDToBidderRequest(bidderRequest)); + let ortbRequest = request.data; + expect(ortbRequest).to.not.equal(null); + expect(ortbRequest.site).to.not.equal(null); + expect(ortbRequest.site).to.deep.equal({ + content: { + data: [{ + name: 'www.iris.com', + ext: { + segtax: 500, + cids: ['iris_c73g5jq96mwso4d8'] + } + }] + }, + page: 'http://pub.com/news', + ref: 'http://google.com', + publisher: { + // id: 'p10000', + domain: 'pub.com' + } + }); + }); + + it('Verify impression/slot level first party data', function () { + const bidderRequests = [{ + placementCode: '/DfpAccount1/slot1', + mediaTypes: { + banner: { + sizes: [[1, 1]] + } + }, + bidId: 'bid12345', + params: { + cp: 'p10000', + ct: 't10000', + adzoneid: '77', + extra_key1: 'extra_val1', + extra_key2: 12345 + }, + ortb2Imp: { + ext: { + data: { + pbadslot: 'homepage-top-rect', + adUnitSpecificAttribute: '123' + } + } + } + }]; + let request = spec.buildRequests(bidderRequests, bidderRequest); + let ortbRequest = request.data; + expect(ortbRequest).to.not.equal(null); + expect(ortbRequest.imp).to.not.equal(null); + expect(ortbRequest.imp).to.have.lengthOf(1); + expect(ortbRequest.imp[0].ext).to.not.equal(null); + expect(ortbRequest.imp[0].ext).to.deep.equal({ + prebid: { + adzoneid: '77', + extra_key1: 'extra_val1', + extra_key2: 12345 + }, + data: { + pbadslot: 'homepage-top-rect', + adUnitSpecificAttribute: '123' + } + }); + }); + + it('Verify bid request timeouts', function () { + const mkRequest = (bidderRequest) => spec.buildRequests(slotConfigs, bidderRequest).data; + // assert default is used when no bidderRequest.timeout value is available + expect(mkRequest(bidderRequest).tmax).to.equal(500) + + // assert bidderRequest value is used when available + expect(mkRequest(Object.assign({}, { timeout: 6000 }, bidderRequest)).tmax).to.equal(6000) + }); }); diff --git a/test/spec/modules/adyoulikeBidAdapter_spec.js b/test/spec/modules/adyoulikeBidAdapter_spec.js index 7310f736f7e..ffd6729397a 100644 --- a/test/spec/modules/adyoulikeBidAdapter_spec.js +++ b/test/spec/modules/adyoulikeBidAdapter_spec.js @@ -2,6 +2,7 @@ import { expect } from 'chai'; import { spec } from 'modules/adyoulikeBidAdapter.js'; import { newBidder } from 'src/adapters/bidderFactory.js'; +import { config } from 'src/config.js'; describe('Adyoulike Adapter', function () { const canonicalUrl = 'https://canonical.url/?t=%26'; @@ -707,32 +708,21 @@ describe('Adyoulike Adapter', function () { expect(payload.gdprConsent.consentRequired).to.be.null; }); - it('should add userid eids information to the request', function () { - let bidderRequest = { - 'auctionId': '1d1a030790a475', - 'bidderRequestId': '22edbae2733bf6', - 'timeout': 3000, - 'userIdAsEids': - [ - { - 'source': 'pubcid.org', - 'uids': [ - { - 'atype': 1, - 'id': '01EAJWWNEPN3CYMM5N8M5VXY22' - } - ] - } - ] - }; - - bidderRequest.bids = bidRequestWithSinglePlacement; - - const request = spec.buildRequests(bidRequestWithSinglePlacement, bidderRequest); + it('should add eids eids information to the request', function () { + let bidRequest = bidRequestWithSinglePlacement; + bidRequest[0].userIdAsEids = [{ + 'source': 'pubcid.org', + 'uids': [{ + 'atype': 1, + 'id': '01EAJWWNEPN3CYMM5N8M5VXY22' + }] + }] + + const request = spec.buildRequests(bidRequest, bidderRequest); const payload = JSON.parse(request.data); - expect(payload.userId).to.exist; - expect(payload.userId).to.deep.equal(bidderRequest.userIdAsEids); + expect(payload.eids).to.exist; + expect(payload.eids).to.deep.equal(bidRequest[0].userIdAsEids); }); it('sends bid request to endpoint with single placement', function () { @@ -898,4 +888,115 @@ describe('Adyoulike Adapter', function () { expect(spec.gvlid).to.equal(259) }) }); + + describe('getUserSyncs', function () { + const syncurl_iframe = 'https://visitor.omnitagjs.com/visitor/isync?uid=19340f4f097d16f41f34fc0274981ca4'; + + const emptySync = []; + + describe('with iframe enabled', function() { + const userSyncConfig = { iframeEnabled: true }; + + it('should not add parameters if not provided', function() { + expect(spec.getUserSyncs(userSyncConfig, {}, undefined, undefined)).to.deep.equal([{ + type: 'iframe', url: `${syncurl_iframe}` + }]); + }); + + it('should add GDPR parameters if provided', function() { + expect(spec.getUserSyncs(userSyncConfig, {}, {gdprApplies: true, consentString: undefined}, undefined)).to.deep.equal([{ + type: 'iframe', url: `${syncurl_iframe}&gdpr=1&gdpr_consent=` + }]); + + expect(spec.getUserSyncs(userSyncConfig, {}, {gdprApplies: true, consentString: 'foo?'}, undefined)).to.deep.equal([{ + type: 'iframe', url: `${syncurl_iframe}&gdpr=1&gdpr_consent=foo%3F` + }]); + expect(spec.getUserSyncs(userSyncConfig, {}, {gdprApplies: false, consentString: 'bar'}, undefined)).to.deep.equal([{ + type: 'iframe', url: `${syncurl_iframe}&gdpr=0&gdpr_consent=bar` + }]); + }); + + it('should add CCPA parameters if provided', function() { + expect(spec.getUserSyncs(userSyncConfig, {}, undefined, 'foo?')).to.deep.equal([{ + type: 'iframe', url: `${syncurl_iframe}&us_privacy=foo%3F` + }]); + }); + + describe('COPPA', function() { + let sandbox; + + this.beforeEach(function() { + sandbox = sinon.sandbox.create(); + }); + + this.afterEach(function() { + sandbox.restore(); + }); + + it('should add coppa parameters if provided', function() { + sandbox.stub(config, 'getConfig').callsFake(key => { + const config = { + 'coppa': true + }; + return config[key]; + }); + + expect(spec.getUserSyncs(userSyncConfig, {}, undefined, undefined)).to.deep.equal([{ + type: 'iframe', url: `${syncurl_iframe}&coppa=1` + }]); + }); + }); + + describe('GPP', function() { + it('should not apply if not gppConsent.gppString', function() { + const gppConsent = { gppString: '', applicableSections: [123] }; + const result = spec.getUserSyncs(userSyncConfig, {}, undefined, undefined, gppConsent); + expect(result).to.deep.equal([{ + type: 'iframe', url: `${syncurl_iframe}` + }]); + }); + + it('should not apply if not gppConsent.applicableSections', function() { + const gppConsent = { gppString: '', applicableSections: undefined }; + const result = spec.getUserSyncs(userSyncConfig, {}, undefined, undefined, gppConsent); + expect(result).to.deep.equal([{ + type: 'iframe', url: `${syncurl_iframe}` + }]); + }); + + it('should not apply if empty gppConsent.applicableSections', function() { + const gppConsent = { gppString: '', applicableSections: [] }; + const result = spec.getUserSyncs(userSyncConfig, {}, undefined, undefined, gppConsent); + expect(result).to.deep.equal([{ + type: 'iframe', url: `${syncurl_iframe}` + }]); + }); + + it('should apply if all above are available', function() { + const gppConsent = { gppString: 'foo?', applicableSections: [123] }; + const result = spec.getUserSyncs(userSyncConfig, {}, undefined, undefined, gppConsent); + expect(result).to.deep.equal([{ + type: 'iframe', url: `${syncurl_iframe}&gpp=foo%3F&gpp_sid=123` + }]); + }); + + it('should support multiple sections', function() { + const gppConsent = { gppString: 'foo', applicableSections: [123, 456] }; + const result = spec.getUserSyncs(userSyncConfig, {}, undefined, undefined, gppConsent); + expect(result).to.deep.equal([{ + type: 'iframe', url: `${syncurl_iframe}&gpp=foo&gpp_sid=123%2C456` + }]); + }); + }); + }); + + describe('with iframe disabled', function() { + const userSyncConfig = { iframeEnabled: false }; + + it('should return empty list of syncs', function() { + expect(spec.getUserSyncs(userSyncConfig, {}, undefined, undefined)).to.deep.equal(emptySync); + expect(spec.getUserSyncs(userSyncConfig, {}, {gdprApplies: true, consentString: 'foo'}, 'bar')).to.deep.equal(emptySync); + }); + }); + }); }); diff --git a/test/spec/modules/agmaAnalyticsAdapter_spec.js b/test/spec/modules/agmaAnalyticsAdapter_spec.js new file mode 100644 index 00000000000..ba71624e3b3 --- /dev/null +++ b/test/spec/modules/agmaAnalyticsAdapter_spec.js @@ -0,0 +1,388 @@ +import adapterManager from '../../../src/adapterManager.js'; +import agmaAnalyticsAdapter, { + getTiming, + getOrtb2Data, + getPayload, +} from '../../../modules/agmaAnalyticsAdapter.js'; +import { gdprDataHandler } from '../../../src/adapterManager.js'; +import { expect } from 'chai'; +import * as events from '../../../src/events.js'; +import constants from '../../../src/constants.json'; +import { generateUUID } from '../../../src/utils.js'; +import { server } from '../../mocks/xhr.js'; +import { config } from 'src/config.js'; + +const INGEST_URL = 'https://pbc.agma-analytics.de/v1'; +const extendedKey = [ + 'auctionIds', + 'code', + 'domain', + 'extended', + 'gdprApplies', + 'gdprConsentString', + 'language', + 'ortb2', + 'pageUrl', + 'pageViewId', + 'prebidVersion', + 'referrer', + 'screenHeight', + 'screenWidth', + 'scriptVersion', + 'timestamp', + 'timezoneOffset', + 'timing', + 'triggerEvent', + 'userIdsAsEids', +]; +const nonExtendedKey = [ + 'auctionIds', + 'code', + 'domain', + 'gdprApplies', + 'ortb2', + 'pageUrl', + 'pageViewId', + 'prebidVersion', + 'scriptVersion', + 'timing', + 'triggerEvent', +]; + +describe('AGMA Analytics Adapter', () => { + let agmaConfig, sandbox, clock; + + beforeEach(() => { + sandbox = sinon.sandbox.create(); + clock = sandbox.useFakeTimers(); + sandbox.stub(events, 'getEvents').returns([]); + agmaConfig = { + options: { + code: 'test', + }, + }; + }); + + afterEach(() => { + sandbox.restore(); + }); + + describe('configuration', () => { + it('registers itself with the adapter manager', () => { + const adapter = adapterManager.getAnalyticsAdapter('agma'); + expect(adapter).to.exist; + expect(adapter.gvlid).to.equal(1122); + }); + }); + + describe('getPayload', () => { + it('should use non extended payload with no consent info', () => { + sandbox.stub(gdprDataHandler, 'getConsentData').callsFake(() => null) + const payload = getPayload([generateUUID()], { + code: 'test', + }); + + expect(payload).to.have.all.keys([...nonExtendedKey, 'debug']); + }); + + it('should use non extended payload when agma is not in the TC String', () => { + sandbox.stub(gdprDataHandler, 'getConsentData').callsFake(() => ({ + vendorData: { + vendor: { + consents: { + 1122: false, + }, + }, + }, + })); + const payload = getPayload([generateUUID()], { + code: 'test', + }); + expect(payload).to.have.all.keys([...nonExtendedKey, 'debug']); + }); + + it('should use extended payload when agma is in the TC String', () => { + sandbox.stub(gdprDataHandler, 'getConsentData').callsFake(() => ({ + vendorData: { + vendor: { + consents: { + 1122: true, + }, + }, + }, + })); + const payload = getPayload([generateUUID()], { + code: 'test', + }); + expect(payload).to.have.all.keys([...extendedKey, 'debug']); + }); + }); + + describe('getTiming', () => { + let originalPerformance; + let originalWindowPerformanceNow; + + beforeEach(() => { + originalPerformance = global.performance; + originalWindowPerformanceNow = window.performance.now; + }); + + afterEach(() => { + global.performance = originalPerformance; + window.performance.now = originalWindowPerformanceNow; + }); + + it('returns TTFB using Timing API V2', () => { + global.performance = { + getEntriesByType: sinon + .stub() + .returns([{ responseStart: 100, startTime: 50 }]), + now: sinon.stub().returns(150), + }; + + const result = getTiming(); + + expect(result).to.deep.equal({ ttfb: 50, elapsedTime: 150 }); + }); + + it('returns TTFB using Timing API V1 when V2 is not available', () => { + global.performance = { + getEntriesByType: sinon.stub().throws(), + timing: { responseStart: 150, fetchStart: 50 }, + now: sinon.stub().returns(200), + }; + + const result = getTiming(); + + expect(result).to.deep.equal({ ttfb: 100, elapsedTime: 200 }); + }); + + it('returns null when Timing API is not available', () => { + global.performance = { + getEntriesByType: sinon.stub().throws(), + timing: undefined, + }; + + const result = getTiming(); + + expect(result).to.be.null; + }); + + it('returns ttfb as 0 if calculated value is negative', () => { + global.performance = { + getEntriesByType: sinon + .stub() + .returns([{ responseStart: 50, startTime: 150 }]), + now: sinon.stub().returns(200), + }; + + const result = getTiming(); + + expect(result).to.deep.equal({ ttfb: 0, elapsedTime: 200 }); + }); + + it('returns ttfb as 0 if calculated value exceeds performance.now()', () => { + global.performance = { + getEntriesByType: sinon + .stub() + .returns([{ responseStart: 50, startTime: 0 }]), + now: sinon.stub().returns(40), + }; + + const result = getTiming(); + + expect(result).to.deep.equal({ ttfb: 0, elapsedTime: 40 }); + }); + }); + + describe('getOrtb2Data', () => { + it('returns site and user from options when available', () => { + sandbox.stub(config, 'getConfig').callsFake((key) => { + return {}; + }); + + const ortb2 = { + user: 'user', + site: 'site', + }; + + const result = getOrtb2Data({ + ortb2, + }); + + expect(result).to.deep.equal(ortb2); + }); + + it('returns a combination of data from options and pGlobal.readConfig', () => { + sandbox.stub(config, 'getConfig').callsFake((key) => { + return { + ortb2: { + site: { + foo: 'bar', + }, + }, + }; + }); + + const ortb2 = { + user: 'user', + }; + const result = getOrtb2Data({ + ortb2, + }); + + expect(result).to.deep.equal({ + site: { + foo: 'bar', + }, + user: 'user', + }); + }); + }); + + describe('Event Payload', () => { + beforeEach(() => { + agmaAnalyticsAdapter.enableAnalytics({ + ...agmaConfig, + }); + server.respondWith('POST', INGEST_URL, [ + 200, + { + 'Content-Type': 'application/json', + 'Access-Control-Allow-Origin': '*', + }, + '', + ]); + }); + + afterEach(() => { + agmaAnalyticsAdapter.auctionIds = []; + if (agmaAnalyticsAdapter.timer) { + clearTimeout(agmaAnalyticsAdapter.timer); + } + agmaAnalyticsAdapter.disableAnalytics(); + }); + + it('should only send once per minute', () => { + sandbox.stub(gdprDataHandler, 'getConsentData').callsFake(() => ({ + gdprApplies: true, + consentString: 'consentDataString', + vendorData: { + vendor: { + consents: { + 1122: true, + }, + }, + }, + })); + const auction = { + auctionId: generateUUID(), + }; + + events.emit(constants.EVENTS.AUCTION_INIT, { + auctionId: generateUUID('1'), + auction, + }); + + clock.tick(200); + + events.emit(constants.EVENTS.AUCTION_INIT, { + auctionId: generateUUID('2'), + auction, + }); + events.emit(constants.EVENTS.AUCTION_INIT, { + auctionId: generateUUID('3'), + auction, + }); + events.emit(constants.EVENTS.AUCTION_INIT, { + auctionId: generateUUID('4'), + auction, + }); + + clock.tick(900); + + const [request] = server.requests; + const requestBody = JSON.parse(request.requestBody); + expect(request.url).to.equal(INGEST_URL); + expect(requestBody).to.have.all.keys(extendedKey); + expect(requestBody.triggerEvent).to.equal(constants.EVENTS.AUCTION_INIT); + expect(server.requests).to.have.length(1); + }); + + it('should send the extended payload with consent', () => { + sandbox.stub(gdprDataHandler, 'getConsentData').callsFake(() => ({ + gdprApplies: true, + consentString: 'consentDataString', + vendorData: { + vendor: { + consents: { + 1122: true, + }, + }, + }, + })); + const auction = { + auctionId: generateUUID(), + }; + + events.emit(constants.EVENTS.AUCTION_INIT, auction); + clock.tick(1100); + + const [request] = server.requests; + const requestBody = JSON.parse(request.requestBody); + expect(request.url).to.equal(INGEST_URL); + expect(requestBody).to.have.all.keys(extendedKey); + expect(requestBody.triggerEvent).to.equal(constants.EVENTS.AUCTION_INIT); + expect(server.requests).to.have.length(1); + expect(agmaAnalyticsAdapter.auctionIds).to.have.length(0); + }); + + it('should send the non extended payload with no explicit consent', () => { + sandbox.stub(gdprDataHandler, 'getConsentData').callsFake(() => ({ + gdprApplies: true, + consentString: 'consentDataString', + })); + + const auction = { + auctionId: generateUUID(), + }; + + events.emit(constants.EVENTS.AUCTION_INIT, auction); + clock.tick(1000); + + const [request] = server.requests; + const requestBody = JSON.parse(request.requestBody); + expect(request.url).to.equal(INGEST_URL); + expect(requestBody.triggerEvent).to.equal(constants.EVENTS.AUCTION_INIT); + expect(server.requests).to.have.length(1); + expect(agmaAnalyticsAdapter.auctionIds).to.have.length(0); + }); + + it('should set the trigger Event', () => { + sandbox.stub(gdprDataHandler, 'getConsentData').callsFake(() => null); + agmaAnalyticsAdapter.disableAnalytics(); + agmaAnalyticsAdapter.enableAnalytics({ + provider: 'agma', + options: { + code: 'test', + triggerEvent: constants.EVENTS.AUCTION_END + }, + }); + const auction = { + auctionId: generateUUID(), + }; + + events.emit(constants.EVENTS.AUCTION_INIT, auction); + events.emit(constants.EVENTS.AUCTION_END, auction); + clock.tick(1000); + + const [request] = server.requests; + const requestBody = JSON.parse(request.requestBody); + expect(request.url).to.equal(INGEST_URL); + expect(requestBody.auctionIds).to.have.length(1); + expect(requestBody.triggerEvent).to.equal(constants.EVENTS.AUCTION_END); + expect(server.requests).to.have.length(1); + expect(agmaAnalyticsAdapter.auctionIds).to.have.length(0); + }); + }); +}); diff --git a/test/spec/modules/aidemBidAdapter_spec.js b/test/spec/modules/aidemBidAdapter_spec.js index 8b401491ba0..3de348197b2 100644 --- a/test/spec/modules/aidemBidAdapter_spec.js +++ b/test/spec/modules/aidemBidAdapter_spec.js @@ -155,9 +155,9 @@ const DEFAULT_VALID_BANNER_REQUESTS = [ { adUnitCode: 'test-div', auctionId: 'b06c5141-fe8f-4cdf-9d7d-54415490a917', - bidId: '22c4871113f461', + bidId: '2705bfae8ea667', bidder: 'aidem', - bidderRequestId: '15246a574e859f', + bidderRequestId: '1bbb7854dfa0d8', mediaTypes: { banner: { sizes: [ @@ -171,11 +171,7 @@ const DEFAULT_VALID_BANNER_REQUESTS = [ placementId: '13144370' }, src: 'client', - ortb2Imp: { - ext: { - tid: '54a58774-7a41-494e-9aaf-fa7b79164f0c', - }, - }, + transactionId: 'db739693-9b4a-4669-9945-8eab938783cc' } ]; @@ -183,9 +179,9 @@ const DEFAULT_VALID_VIDEO_REQUESTS = [ { adUnitCode: 'test-div', auctionId: 'b06c5141-fe8f-4cdf-9d7d-54415490a917', - bidId: '22c4871113f461', + bidId: '2705bfae8ea667', bidder: 'aidem', - bidderRequestId: '15246a574e859f', + bidderRequestId: '1bbb7854dfa0d8', mediaTypes: { video: { minduration: 7, @@ -200,18 +196,23 @@ const DEFAULT_VALID_VIDEO_REQUESTS = [ placementId: '13144370' }, src: 'client', - ortb2Imp: { - ext: { - tid: '54a58774-7a41-494e-9aaf-fa7b79164f0c', - } - }, + transactionId: 'db739693-9b4a-4669-9945-8eab938783cc' } ]; const VALID_BIDDER_REQUEST = { - auctionId: '6e9b46c3-65a8-46ea-89f4-c5071110c85c', + auctionId: '19c97f22-5bd1-4b16-a128-80f75fb0a8a0', bidderCode: 'aidem', - bidderRequestId: '170ea5d2b1d073', + bidderRequestId: '1bbb7854dfa0d8', + bids: [ + { + params: { + placementId: '13144370', + siteId: '23434', + publisherId: '7689670753' + }, + } + ], refererInfo: { page: 'test-page', domain: 'test-domain', @@ -219,182 +220,68 @@ const VALID_BIDDER_REQUEST = { }, } -// Add mediatype const SERVER_RESPONSE_BANNER = { - body: { - id: 'efa1930a-bc3e-4fd0-8368-08bc40236b4f', - bid: [ - // BANNER - { - 'id': '2e614be960ee1d', - 'impid': '2e614be960ee1d', - 'price': 7.91, - 'mediatype': 'banner', - 'adid': '24277955', - 'adm': 'creativity_banner', - 'adomain': [ - 'aidem.com' - ], - 'iurl': 'http://www.aidem.com', - 'cat': [], - 'cid': '4193561', - 'crid': '24277955', - 'w': 300, - 'h': 250, - 'ext': { - 'dspid': 85, - 'advbrandid': 1246, - 'advbrand': 'AIDEM' - } - }, - ], - cur: 'USD' - }, -} - -const SERVER_RESPONSE_VIDEO = { - body: { - id: 'efa1930a-bc3e-4fd0-8368-08bc40236b4f', - bid: [ - // VIDEO - { - 'id': '2876a29392a47c', - 'impid': '2876a29392a47c', - 'price': 7.93, - 'mediatype': 'video', - 'adid': '24277955', - 'adm': 'https://hermes.aidemsrv.com/vast-tag/cl9mzhhd502uq09l720uegb02?auction_id={{AUCTION_ID}}&cachebuster={{CACHEBUSTER}}', - 'adomain': [ - 'aidem.com' - ], - 'iurl': 'http://www.aidem.com', - 'cat': [], - 'cid': '4193561', - 'crid': '24277955', - 'w': 640, - 'h': 480, - 'ext': { - 'dspid': 85, - 'advbrandid': 1246, - 'advbrand': 'AIDEM' - } - } - ], - cur: 'USD' - }, -} - -const WIN_NOTICE_WEB = { - 'adId': '3a20ee5dc78c1e', - 'adUnitCode': 'div-gpt-ad-1460505748561-0', - 'creativeId': '24277955', - 'cpm': 1, - 'netRevenue': false, - 'adserverTargeting': { - 'hb_bidder': 'aidem', - 'hb_adid': '3a20ee5dc78c1e', - 'hb_pb': '1.00', - 'hb_size': '300x250', - 'hb_source': 'client', - 'hb_format': 'banner', - 'hb_adomain': 'example.com' - }, - - 'auctionId': '85864730-6cbc-4e56-bc3c-a4a6596dca5b', - 'currency': [ - 'USD' - ], - 'mediaType': 'banner', - 'meta': { - 'advertiserDomains': [ - 'cloudflare.com' - ], - 'ext': {} - }, - 'size': '300x250', - 'params': [ + 'id': '19c97f22-5bd1-4b16-a128-80f75fb0a8a0', + 'seatbid': [ { - 'placementId': '13144370', - 'siteId': '23434', - 'publisherId': '7689670753' + 'bid': [ + { + 'id': 'beeswax/aidem', + 'impid': '2705bfae8ea667', + 'price': 0.00875, + 'burl': 'imp_burl', + 'adm': 'creativity_banner', + 'adid': '2:64:162:1001', + 'adomain': [ + 'aidem.com' + ], + 'cid': '64', + 'crid': 'aidem-1001', + 'cat': [], + 'w': 300, + 'h': 250, + 'mtype': 1 + } + ], + 'seat': 'aidemdsp', + 'group': 0 } ], - 'width': 300, - 'height': 250, - 'status': 'rendered', - 'transactionId': 'ce089116-4251-45c3-bdbb-3a03cb13816b', - 'ttl': 300, - 'requestTimestamp': 1666796241007, - 'responseTimestamp': 1666796241021, - metrics: { - getMetrics() { - return { - - } - } - } + 'cur': 'USD' } -const WIN_NOTICE_APP = { - 'adId': '3a20ee5dc78c1e', - 'adUnitCode': 'div-gpt-ad-1460505748561-0', - 'creativeId': '24277955', - 'cpm': 1, - 'netRevenue': false, - 'adserverTargeting': { - 'hb_bidder': 'aidem', - 'hb_adid': '3a20ee5dc78c1e', - 'hb_pb': '1.00', - 'hb_size': '300x250', - 'hb_source': 'client', - 'hb_format': 'banner', - 'hb_adomain': 'example.com' - }, - - 'auctionId': '85864730-6cbc-4e56-bc3c-a4a6596dca5b', - 'currency': [ - 'USD' - ], - 'mediaType': 'banner', - 'meta': { - 'advertiserDomains': [ - 'cloudflare.com' - ], - 'ext': { - 'app': { - 'app_bundle': '{{APP_BUNDLE}}', - 'app_id': '{{APP_ID}}', - 'app_name': '{{APP_NAME}}', - 'app_store_url': '{{APP_STORE_URL}}', - 'inventory_source': '{{INVENTORY_SOURCE}}' - }, - 'win_notice_ext': { - 'seatid': '{{SEAT_ID}}' - } - } - }, - 'size': '300x250', - 'params': [ +const SERVER_RESPONSE_VIDEO = { + 'id': '19c97f22-5bd1-4b16-a128-80f75fb0a8a0', + 'seatbid': [ { - 'placementId': '13144370', - 'siteId': '23434', - 'publisherId': '7689670753' + 'bid': [ + { + 'id': 'beeswax/aidem', + 'impid': '2705bfae8ea667', + 'price': 0.00875, + 'burl': 'imp_burl', + 'adm': 'creativity_banner', + 'adid': '2:64:162:1001', + 'adomain': [ + 'aidem.com' + ], + 'cid': '64', + 'crid': 'aidem-1001', + 'cat': [], + 'w': 300, + 'h': 250, + 'mtype': 2 + } + ], + 'seat': 'aidemdsp', + 'group': 0 } ], - 'width': 300, - 'height': 250, - 'status': 'rendered', - 'transactionId': 'ce089116-4251-45c3-bdbb-3a03cb13816b', - 'ttl': 300, - 'requestTimestamp': 1666796241007, - 'responseTimestamp': 1666796241021, - metrics: { - getMetrics() { - return { + 'cur': 'USD' +} - } - } - } +const WIN_NOTICE = { + burl: 'burl' } const ERROR_NOTICE = { @@ -512,109 +399,95 @@ describe('Aidem adapter', () => { const requests = spec.buildRequests(DEFAULT_VALID_BANNER_REQUESTS, VALID_BIDDER_REQUEST); expect(requests).to.be.an('object'); expect(requests.method).to.be.a('string') - expect(requests.data).to.be.a('string') + expect(requests.data).to.be.a('object') expect(requests.options).to.be.an('object').that.have.a.property('withCredentials') }); it('should have a well formatted banner payload', () => { - const requests = spec.buildRequests(DEFAULT_VALID_BANNER_REQUESTS, VALID_BIDDER_REQUEST); - const payload = JSON.parse(requests.data) - expect(payload).to.be.a('object').that.has.all.keys( - 'id', 'imp', 'device', 'cur', 'tz', 'regs', 'site', 'environment', 'at' + const {data} = spec.buildRequests(DEFAULT_VALID_BANNER_REQUESTS, VALID_BIDDER_REQUEST); + expect(data).to.be.a('object').that.has.all.keys( + 'id', 'imp', 'regs', 'site', 'environment', 'at', 'test' ) - expect(payload.imp).to.be.a('array').that.has.lengthOf(DEFAULT_VALID_BANNER_REQUESTS.length) + expect(data.imp).to.be.a('array').that.has.lengthOf(DEFAULT_VALID_BANNER_REQUESTS.length) - expect(payload.imp[0]).to.be.a('object').that.has.all.keys( - 'banner', 'id', 'mediatype', 'imp_ext', 'tid', 'tagid' + expect(data.imp[0]).to.be.a('object').that.has.all.keys( + 'banner', 'id', 'tagId' ) - expect(payload.imp[0].banner).to.be.a('object').that.has.all.keys( + expect(data.imp[0].banner).to.be.a('object').that.has.all.keys( 'format', 'topframe' ) }); - it('should have a well formatted video payload', () => { - const requests = spec.buildRequests(DEFAULT_VALID_VIDEO_REQUESTS, VALID_BIDDER_REQUEST); - const payload = JSON.parse(requests.data) - expect(payload).to.be.a('object').that.has.all.keys( - 'id', 'imp', 'device', 'cur', 'tz', 'regs', 'site', 'environment', 'at' - ) - expect(payload.imp).to.be.a('array').that.has.lengthOf(DEFAULT_VALID_VIDEO_REQUESTS.length) - - expect(payload.imp[0]).to.be.a('object').that.has.all.keys( - 'video', 'id', 'mediatype', 'imp_ext', 'tid', 'tagid' - ) - expect(payload.imp[0].video).to.be.a('object').that.has.all.keys( - 'format', 'mimes', 'minDuration', 'maxDuration', 'protocols' - ) - }); + if (FEATURES.VIDEO) { + it('should have a well formatted video payload', () => { + const {data} = spec.buildRequests(DEFAULT_VALID_VIDEO_REQUESTS, VALID_BIDDER_REQUEST); + expect(data).to.be.a('object').that.has.all.keys( + 'id', 'imp', 'regs', 'site', 'environment', 'at', 'test' + ) + expect(data.imp).to.be.a('array').that.has.lengthOf(DEFAULT_VALID_VIDEO_REQUESTS.length) - it('should have a well formatted bid floor payload if configured', () => { - const validBannerRequests = utils.deepClone(DEFAULT_VALID_BANNER_REQUESTS) - validBannerRequests[0].params.floor = { - value: 1.98, - currency: 'USD' - } - const requests = spec.buildRequests(validBannerRequests, VALID_BIDDER_REQUEST); - const payload = JSON.parse(requests.data) - const { floor } = payload.imp[0] - expect(floor).to.be.a('object').that.has.all.keys( - 'value', 'currency' - ) - }); + expect(data.imp[0]).to.be.a('object').that.has.all.keys( + 'video', 'id', 'tagId' + ) + expect(data.imp[0].video).to.be.a('object').that.has.all.keys( + 'mimes', 'minduration', 'maxduration', 'protocols', 'w', 'h' + ) + }); + } it('should hav wpar keys in environment object', function () { - const requests = spec.buildRequests(DEFAULT_VALID_VIDEO_REQUESTS, VALID_BIDDER_REQUEST); - const payload = JSON.parse(requests.data) - expect(payload).to.have.property('environment') - expect(payload.environment).to.be.a('object').that.have.property('wpar') - expect(payload.environment.wpar).to.be.a('object').that.has.keys('innerWidth', 'innerHeight') + const {data} = spec.buildRequests(DEFAULT_VALID_VIDEO_REQUESTS, VALID_BIDDER_REQUEST); + expect(data).to.have.property('environment') + expect(data.environment).to.be.a('object').that.have.property('wpar') + expect(data.environment.wpar).to.be.a('object').that.has.keys('innerWidth', 'innerHeight') }); }) describe('interpretResponse', () => { it('should return a valid bid array with a banner bid', () => { - const response = utils.deepClone(SERVER_RESPONSE_BANNER) - const interpreted = spec.interpretResponse(response) - expect(interpreted).to.be.a('array').that.has.lengthOf(1) - interpreted.forEach(value => { + const {data} = spec.buildRequests(DEFAULT_VALID_BANNER_REQUESTS, VALID_BIDDER_REQUEST) + const bids = spec.interpretResponse({body: SERVER_RESPONSE_BANNER}, { data }) + expect(bids).to.be.a('array').that.has.lengthOf(1) + bids.forEach(value => { expect(value).to.be.a('object').that.has.all.keys( - 'ad', 'cpm', 'creativeId', 'currency', 'height', 'mediaType', 'meta', 'netRevenue', 'requestId', 'ttl', 'width', 'dealId' + 'ad', 'cpm', 'creativeId', 'currency', 'height', 'mediaType', 'meta', 'netRevenue', 'requestId', 'ttl', 'width', 'burl', 'seatBidId', 'creative_id' ) }) }); - it('should return a valid bid array with a banner bid', () => { - const response = utils.deepClone(SERVER_RESPONSE_VIDEO) - const interpreted = spec.interpretResponse(response) - expect(interpreted).to.be.a('array').that.has.lengthOf(1) - interpreted.forEach(value => { - expect(value).to.be.a('object').that.has.all.keys( - 'vastUrl', 'cpm', 'creativeId', 'currency', 'height', 'mediaType', 'meta', 'netRevenue', 'requestId', 'ttl', 'width', 'dealId' - ) - }) - }); + if (FEATURES.VIDEO) { + it('should return a valid bid array with a video bid', () => { + const {data} = spec.buildRequests(DEFAULT_VALID_VIDEO_REQUESTS, VALID_BIDDER_REQUEST) + const bids = spec.interpretResponse({body: SERVER_RESPONSE_VIDEO}, { data }) + expect(bids).to.be.a('array').that.has.lengthOf(1) + bids.forEach(value => { + expect(value).to.be.a('object').that.has.all.keys( + 'vastUrl', 'vastXml', 'playerHeight', 'playerWidth', 'cpm', 'creativeId', 'currency', 'height', 'mediaType', 'meta', 'netRevenue', 'requestId', 'ttl', 'width', 'burl', 'seatBidId', 'creative_id' + ) + }) + }); + } it('should return a valid bid array with netRevenue', () => { - const response = utils.deepClone(SERVER_RESPONSE_BANNER) - response.body.bid[0].isNet = true - const interpreted = spec.interpretResponse(response) - expect(interpreted).to.be.a('array').that.has.lengthOf(1) - expect(interpreted[0].netRevenue).to.be.true - }); - - it('should return an empty bid array if one of seatbid entry is missing price property', () => { - const response = utils.deepClone(SERVER_RESPONSE_BANNER) - delete response.body.bid[0].price - const interpreted = spec.interpretResponse(response) - expect(interpreted).to.be.a('array').that.has.lengthOf(0) - }); - - it('should return an empty bid array if one of seatbid entry is missing adm property', () => { - const response = utils.deepClone(SERVER_RESPONSE_BANNER) - delete response.body.bid[0].adm - const interpreted = spec.interpretResponse(response) - expect(interpreted).to.be.a('array').that.has.lengthOf(0) - }); + const {data} = spec.buildRequests(DEFAULT_VALID_VIDEO_REQUESTS, VALID_BIDDER_REQUEST) + const bids = spec.interpretResponse({body: SERVER_RESPONSE_VIDEO}, { data }) + expect(bids).to.be.a('array').that.has.lengthOf(1) + expect(bids[0].netRevenue).to.be.true + }); + + // it('should return an empty bid array if one of seatbid entry is missing price property', () => { + // const response = utils.deepClone(SERVER_RESPONSE_BANNER) + // delete response.body.bid[0].price + // const interpreted = spec.interpretResponse(response) + // expect(interpreted).to.be.a('array').that.has.lengthOf(0) + // }); + + // it('should return an empty bid array if one of seatbid entry is missing adm property', () => { + // const response = utils.deepClone(SERVER_RESPONSE_BANNER) + // delete response.body.bid[0].adm + // const interpreted = spec.interpretResponse(response) + // expect(interpreted).to.be.a('array').that.has.lengthOf(0) + // }); }) describe('onBidWon', () => { @@ -622,34 +495,29 @@ describe('Aidem adapter', () => { expect(spec.onBidWon).to.exist.and.to.be.a('function') }); - it(`should send a valid bid won notice from web environment`, function () { - spec.onBidWon(WIN_NOTICE_WEB); - expect(server.requests.length).to.equal(1); - }); - - it(`should send a valid bid won notice from app environment`, function () { - spec.onBidWon(WIN_NOTICE_APP); + it(`should send a win notice`, function () { + spec.onBidWon(WIN_NOTICE); expect(server.requests.length).to.equal(1); }); }); - describe('onBidderError', () => { - it(`should exists and type function`, function () { - expect(spec.onBidderError).to.exist.and.to.be.a('function') - }); - - it(`should send a valid error notice`, function () { - spec.onBidderError({ bidderRequest: ERROR_NOTICE }) - expect(server.requests.length).to.equal(1); - const body = JSON.parse(server.requests[0].requestBody) - expect(body).to.be.a('object').that.has.all.keys('message', 'auctionId', 'bidderRequestId', 'url', 'metrics') - // const { bids } = JSON.parse(server.requests[0].requestBody) - // expect(bids).to.be.a('array').that.has.lengthOf(1) - // _each(bids, (bid) => { - // expect(bid).to.be.a('object').that.has.all.keys('adUnitCode', 'auctionId', 'bidId', 'bidderRequestId', 'transactionId', 'metrics') - // }) - }); - }); + // describe('onBidderError', () => { + // it(`should exists and type function`, function () { + // expect(spec.onBidderError).to.exist.and.to.be.a('function') + // }); + // + // it(`should send a valid error notice`, function () { + // spec.onBidderError({ bidderRequest: ERROR_NOTICE }) + // expect(server.requests.length).to.equal(1); + // const body = JSON.parse(server.requests[0].requestBody) + // expect(body).to.be.a('object').that.has.all.keys('message', 'auctionId', 'bidderRequestId', 'url', 'metrics') + // // const { bids } = JSON.parse(server.requests[0].requestBody) + // // expect(bids).to.be.a('array').that.has.lengthOf(1) + // // _each(bids, (bid) => { + // // expect(bid).to.be.a('object').that.has.all.keys('adUnitCode', 'auctionId', 'bidId', 'bidderRequestId', 'transactionId', 'metrics') + // // }) + // }); + // }); describe('setEndPoints', () => { it(`should exists and type function`, function () { @@ -659,64 +527,35 @@ describe('Aidem adapter', () => { it(`should not modify default endpoints`, function () { const endpoints = setEndPoints() const requestURL = new URL(endpoints.request) - const winNoticeURL = new URL(endpoints.notice.win) - const timeoutNoticeURL = new URL(endpoints.notice.timeout) - const errorNoticeURL = new URL(endpoints.notice.error) - expect(requestURL.host).to.equal('zero.aidemsrv.com') - expect(winNoticeURL.host).to.equal('zero.aidemsrv.com') - expect(timeoutNoticeURL.host).to.equal('zero.aidemsrv.com') - expect(errorNoticeURL.host).to.equal('zero.aidemsrv.com') - - expect(decodeURIComponent(requestURL.pathname)).to.equal('/bid/request') - expect(decodeURIComponent(winNoticeURL.pathname)).to.equal('/notice/win') - expect(decodeURIComponent(timeoutNoticeURL.pathname)).to.equal('/notice/timeout') - expect(decodeURIComponent(errorNoticeURL.pathname)).to.equal('/notice/error') + expect(decodeURIComponent(requestURL.pathname)).to.equal('/prebidjs/ortb/v2.6/bid/request') }); it(`should not change request endpoint`, function () { const endpoints = setEndPoints('default') const requestURL = new URL(endpoints.request) - expect(decodeURIComponent(requestURL.pathname)).to.equal('/bid/request') + expect(decodeURIComponent(requestURL.pathname)).to.equal('/prebidjs/ortb/v2.6/bid/request') }); it(`should change to local env`, function () { const endpoints = setEndPoints('local') const requestURL = new URL(endpoints.request) - const winNoticeURL = new URL(endpoints.notice.win) - const timeoutNoticeURL = new URL(endpoints.notice.timeout) - const errorNoticeURL = new URL(endpoints.notice.error) expect(requestURL.host).to.equal('127.0.0.1:8787') - expect(winNoticeURL.host).to.equal('127.0.0.1:8787') - expect(timeoutNoticeURL.host).to.equal('127.0.0.1:8787') - expect(errorNoticeURL.host).to.equal('127.0.0.1:8787') }); it(`should add a path prefix`, function () { const endpoints = setEndPoints('local', '/path') const requestURL = new URL(endpoints.request) - const winNoticeURL = new URL(endpoints.notice.win) - const timeoutNoticeURL = new URL(endpoints.notice.timeout) - const errorNoticeURL = new URL(endpoints.notice.error) - expect(decodeURIComponent(requestURL.pathname)).to.equal('/path/bid/request') - expect(decodeURIComponent(winNoticeURL.pathname)).to.equal('/path/notice/win') - expect(decodeURIComponent(timeoutNoticeURL.pathname)).to.equal('/path/notice/timeout') - expect(decodeURIComponent(errorNoticeURL.pathname)).to.equal('/path/notice/error') + expect(decodeURIComponent(requestURL.pathname)).to.equal('/path/prebidjs/ortb/v2.6/bid/request') }); it(`should add a path prefix and change request endpoint`, function () { const endpoints = setEndPoints('local', '/path') const requestURL = new URL(endpoints.request) - const winNoticeURL = new URL(endpoints.notice.win) - const timeoutNoticeURL = new URL(endpoints.notice.timeout) - const errorNoticeURL = new URL(endpoints.notice.error) - expect(decodeURIComponent(requestURL.pathname)).to.equal('/path/bid/request') - expect(decodeURIComponent(winNoticeURL.pathname)).to.equal('/path/notice/win') - expect(decodeURIComponent(timeoutNoticeURL.pathname)).to.equal('/path/notice/timeout') - expect(decodeURIComponent(errorNoticeURL.pathname)).to.equal('/path/notice/error') + expect(decodeURIComponent(requestURL.pathname)).to.equal('/path/prebidjs/ortb/v2.6/bid/request') }); }); @@ -758,8 +597,7 @@ describe('Aidem adapter', () => { coppa: true }); const { data } = spec.buildRequests(DEFAULT_VALID_BANNER_REQUESTS, VALID_BIDDER_REQUEST); - const request = JSON.parse(data) - expect(request.regs.coppa_applies).to.equal(true) + expect(data.regs.coppa_applies).to.equal(true) }); it(`should set gdpr to true`, function () { @@ -771,8 +609,7 @@ describe('Aidem adapter', () => { } }); const { data } = spec.buildRequests(DEFAULT_VALID_BANNER_REQUESTS, VALID_BIDDER_REQUEST); - const request = JSON.parse(data) - expect(request.regs.gdpr_applies).to.equal(true) + expect(data.regs.gdpr_applies).to.equal(true) }); it(`should set usp_consent string`, function () { @@ -789,8 +626,7 @@ describe('Aidem adapter', () => { } }); const { data } = spec.buildRequests(DEFAULT_VALID_BANNER_REQUESTS, VALID_BIDDER_REQUEST); - const request = JSON.parse(data) - expect(request.regs.us_privacy).to.equal('1YYY') + expect(data.regs.us_privacy).to.equal('1YYY') }); it(`should not set usp_consent string`, function () { @@ -807,8 +643,7 @@ describe('Aidem adapter', () => { } }); const { data } = spec.buildRequests(DEFAULT_VALID_BANNER_REQUESTS, VALID_BIDDER_REQUEST); - const request = JSON.parse(data) - expect(request.regs.us_privacy).to.undefined + expect(data.regs.us_privacy).to.undefined }); }); }); diff --git a/test/spec/modules/ajaBidAdapter_spec.js b/test/spec/modules/ajaBidAdapter_spec.js index 7cf5698f7d4..3137c9dc24e 100644 --- a/test/spec/modules/ajaBidAdapter_spec.js +++ b/test/spec/modules/ajaBidAdapter_spec.js @@ -62,8 +62,17 @@ describe('AjaAdapter', function () { model: 'SM-G955U', bitness: '64', architecture: '' + }, + ext: { + cdep: 'example_label_1' } } + }, + ortb2Imp: { + ext: { + tid: 'cea1eb09-d970-48dc-8585-634d3a7b0544', + gpid: '/1111/homepage#300x250' + } } } ]; @@ -78,7 +87,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&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&'); + expect(requests[0].data).to.equal('asi=123456&skt=5&gpid=%2F1111%2Fhomepage%23300x250&tid=cea1eb09-d970-48dc-8585-634d3a7b0544&cdep=example_label_1&prebid_id=30b31c1838de1e&prebid_ver=$prebid.version$&page_url=https%3A%2F%2Fhoge.com&ad_format_ids=2&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&'); }); }); @@ -116,7 +125,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&eids=%7B%22eids%22%3A%5B%7B%22source%22%3A%22pubcid.org%22%2C%22uids%22%3A%5B%7B%22id%22%3A%22some-random-id-value%22%2C%22atype%22%3A1%7D%5D%7D%5D%7D&'); + expect(requests[0].data).to.equal('asi=123456&skt=5&prebid_id=30b31c1838de1e&prebid_ver=$prebid.version$&page_url=https%3A%2F%2Fhoge.com&ad_format_ids=2&eids=%7B%22eids%22%3A%5B%7B%22source%22%3A%22pubcid.org%22%2C%22uids%22%3A%5B%7B%22id%22%3A%22some-random-id-value%22%2C%22atype%22%3A1%7D%5D%7D%5D%7D&'); }); }); @@ -173,138 +182,6 @@ describe('AjaAdapter', function () { expect(Object.keys(result[0])).to.have.members(Object.keys(expectedResponse[0])); }); - it('handles video responses', function () { - let response = { - 'is_ad_return': true, - 'ad': { - 'ad_type': 3, - 'prebid_id': '51ef8751f9aead', - 'price': 12.34, - 'currency': 'JPY', - 'creative_id': '123abc', - 'video': { - 'w': 300, - 'h': 250, - 'vtag': '', - 'purl': 'https://cdn/player', - 'progress': true, - 'loop': false, - 'inread': false, - 'adomain': [ - 'www.example.com' - ] - } - }, - 'syncs': [ - 'https://example.com' - ] - }; - - let bidderRequest; - let result = spec.interpretResponse({ body: response }, {bidderRequest}); - expect(result[0]).to.have.property('vastXml'); - expect(result[0]).to.have.property('renderer'); - expect(result[0]).to.have.property('mediaType', 'video'); - }); - - it('handles native response', function () { - let response = { - 'is_ad_return': true, - 'ad': { - 'ad_type': 2, - 'prebid_id': '51ef8751f9aead', - 'price': 12.34, - 'currency': 'JPY', - 'creative_id': '123abc', - 'native': { - 'template_and_ads': { - 'head': '', - 'body_wrapper': '', - 'body': '', - 'ads': [ - { - 'ad_format_id': 10, - 'assets': { - 'ad_spot_id': '123abc', - 'index': 0, - 'adchoice_url': 'https://aja-kk.co.jp/optout', - 'cta_text': 'cta', - 'img_icon': 'https://example.com/img_icon', - 'img_icon_width': '50', - 'img_icon_height': '50', - 'img_main': 'https://example.com/img_main', - 'img_main_width': '200', - 'img_main_height': '100', - 'lp_link': 'https://example.com/lp?k=v', - 'sponsor': 'sponsor', - 'title': 'ad_title', - 'description': 'ad_desc' - }, - 'imps': [ - 'https://example.com/imp' - ], - 'inviews': [ - 'https://example.com/inview' - ], - 'jstracker': '', - 'disable_trimming': false, - 'adomain': [ - 'www.example.com' - ] - } - ] - } - } - }, - 'syncs': [ - 'https://example.com' - ] - }; - - let expectedResponse = [ - { - 'requestId': '51ef8751f9aead', - 'cpm': 12.34, - 'creativeId': '123abc', - 'dealId': undefined, - 'mediaType': 'native', - 'currency': 'JPY', - 'ttl': 300, - 'netRevenue': true, - 'native': { - 'title': 'ad_title', - 'body': 'ad_desc', - 'cta': 'cta', - 'sponsoredBy': 'sponsor', - 'image': { - 'url': 'https://example.com/img_main', - 'width': 200, - 'height': 100 - }, - 'icon': { - 'url': 'https://example.com/img_icon', - 'width': 50, - 'height': 50 - }, - 'clickUrl': 'https://example.com/lp?k=v', - 'impressionTrackers': [ - 'https://example.com/imp' - ], - 'privacyLink': 'https://aja-kk.co.jp/optout' - }, - 'meta': { - 'advertiserDomains': [ - 'www.example.com' - ] - } - } - ]; - - let bidderRequest; - let result = spec.interpretResponse({ body: response }, {bidderRequest}) - expect(result).to.deep.equal(expectedResponse) - }); - it('handles nobid responses', function () { let response = { 'is_ad_return': false, diff --git a/test/spec/modules/alkimiBidAdapter_spec.js b/test/spec/modules/alkimiBidAdapter_spec.js index a396e5b8139..90a9e409e69 100644 --- a/test/spec/modules/alkimiBidAdapter_spec.js +++ b/test/spec/modules/alkimiBidAdapter_spec.js @@ -14,8 +14,7 @@ const REQUEST = { }, 'params': { bidFloor: 0.1, - token: 'e64782a4-8e68-4c38-965b-80ccf115d46f', - pos: 7 + token: 'e64782a4-8e68-4c38-965b-80ccf115d46f' }, 'userIdAsEids': [{ 'source': 'criteo.com', @@ -69,7 +68,7 @@ const BIDDER_VIDEO_RESPONSE = { 'ttl': 200, 'creativeId': 2, 'netRevenue': true, - 'winUrl': 'http://test.com', + 'winUrl': 'http://test.com?price=${AUCTION_PRICE}', 'mediaType': 'video', 'adomain': ['test.com'] }] @@ -96,10 +95,6 @@ describe('alkimiBidAdapter', function () { delete bid.params.token expect(spec.isBidRequestValid(bid)).to.equal(false) - bid = Object.assign({}, REQUEST) - delete bid.params.bidFloor - expect(spec.isBidRequestValid(bid)).to.equal(false) - bid = Object.assign({}, REQUEST) delete bid.params expect(spec.isBidRequestValid(bid)).to.equal(false) @@ -109,7 +104,6 @@ describe('alkimiBidAdapter', function () { describe('buildRequests', function () { let bidRequests = [REQUEST] let requestData = { - auctionId: '123', refererInfo: { page: 'http://test.com/path.html' }, @@ -118,7 +112,15 @@ describe('alkimiBidAdapter', function () { vendorData: {}, gdprApplies: true }, - uspConsent: 'uspConsent' + uspConsent: 'uspConsent', + ortb2: { + site: { + keywords: 'test1, test2' + }, + at: 2, + bcat: ['BSW1', 'BSW2'], + wseat: ['16', '165'] + } } const bidderRequest = spec.buildRequests(bidRequests, requestData) @@ -137,13 +139,14 @@ describe('alkimiBidAdapter', function () { it('sends bid request to ENDPOINT via POST', function () { expect(bidderRequest.method).to.equal('POST') - expect(bidderRequest.data.requestId).to.equal('123') + expect(bidderRequest.data.requestId).to.not.equal(undefined) expect(bidderRequest.data.referer).to.equal('http://test.com/path.html') expect(bidderRequest.data.schain).to.deep.contains({ ver: '1.0', complete: 1, nodes: [{ asi: 'alkimi-onboarding.com', sid: '00001', hp: 1 }] }) - expect(bidderRequest.data.signRequest.bids).to.deep.contains({ token: 'e64782a4-8e68-4c38-965b-80ccf115d46f', pos: 7, bidFloor: 0.1, sizes: [{width: 300, height: 250}], playerSizes: [], impMediaTypes: ['Banner'], adUnitCode: 'bannerAdUnitCode' }) + expect(bidderRequest.data.signRequest.bids).to.deep.contains({ token: 'e64782a4-8e68-4c38-965b-80ccf115d46f', bidFloor: 0.1, sizes: [{ width: 300, height: 250 }], playerSizes: [], impMediaTypes: ['Banner'], adUnitCode: 'bannerAdUnitCode', instl: undefined, exp: undefined, banner: { sizes: [[300, 250]] }, video: undefined }) expect(bidderRequest.data.signRequest.randomUUID).to.equal(undefined) expect(bidderRequest.data.bidIds).to.deep.contains('456') expect(bidderRequest.data.signature).to.equal(undefined) + expect(bidderRequest.data.ortb2).to.deep.contains({ at: 2, wseat: ['16', '165'], bcat: ['BSW1', 'BSW2'], site: { keywords: 'test1, test2' }, }) expect(bidderRequest.options.customHeaders).to.deep.equal({ 'Rtb-Direct': true }) expect(bidderRequest.options.contentType).to.equal('application/json') expect(bidderRequest.url).to.equal(ENDPOINT) @@ -192,9 +195,9 @@ describe('alkimiBidAdapter', function () { expect(result[0]).to.have.property('ttl').equal(200) expect(result[0]).to.have.property('creativeId').equal(2) expect(result[0]).to.have.property('netRevenue').equal(true) - expect(result[0]).to.have.property('winUrl').equal('http://test.com') + expect(result[0]).to.have.property('winUrl').equal('http://test.com?price=${AUCTION_PRICE}') expect(result[0]).to.have.property('mediaType').equal('video') - expect(result[0]).to.have.property('vastXml').equal('vast') + expect(result[0]).to.have.property('vastUrl').equal('http://test.com?price=800.4') expect(result[0].meta).to.exist.property('advertiserDomains') expect(result[0].meta).to.have.property('advertiserDomains').lengthOf(1) }) diff --git a/test/spec/modules/ampliffyBidAdapter_spec.js b/test/spec/modules/ampliffyBidAdapter_spec.js new file mode 100644 index 00000000000..5b86f692d7e --- /dev/null +++ b/test/spec/modules/ampliffyBidAdapter_spec.js @@ -0,0 +1,453 @@ +import { + parseXML, + isAllowedToBidUp, + spec, + getDefaultParams, + mergeParams, + paramsToQueryString, setCurrentURL +} from 'modules/ampliffyBidAdapter.js'; +import {expect} from 'chai'; +import {BANNER, VIDEO} from 'src/mediaTypes'; +import {newBidder} from 'src/adapters/bidderFactory'; + +describe('Ampliffy bid adapter Test', 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'); + }); + }); + // Global definitions for all tests + const xmlStr = ` + + + + ]]> + + + + ES + `; + const xml = new window.DOMParser().parseFromString(xmlStr, 'text/xml'); + let companion = xml.getElementsByTagName('Companion')[0]; + let htmlResource = companion.getElementsByTagName('HTMLResource')[0]; + let htmlContent = document.createElement('html'); + htmlContent.innerHTML = htmlResource.textContent; + + describe('Is allowed to bid up', function () { + it('Should return true using a URL that is in domainMap', () => { + let allowedToBidUp = isAllowedToBidUp(htmlContent, 'https://testSports.com?id=131313&text=aaaaa&foo=foo'); + expect(allowedToBidUp).to.be.true; + }) + + it('Should return false using an url that is not in domainMap', () => { + let allowedToBidUp = isAllowedToBidUp(htmlContent, 'https://test.com'); + expect(allowedToBidUp).to.be.false; + }) + + it('Should return false using an url that is excluded.', () => { + let allowedToBidUp = isAllowedToBidUp(htmlContent, 'https://www.no-allowed.com/busqueda/sexo/sexo?test=1#item1'); + expect(allowedToBidUp).to.be.false; + }) + }) + + describe('Helper functions', function () { + it('Should default params not to be null', () => { + const defaultParams = getDefaultParams(); + + expect(defaultParams).not.to.be.null; + }) + it('Should the merge two object params into a new object', () => { + const params1 = { + 'hello': 'world', + 'ampTest': 'this will be replaced' + } + const params2 = { + 'test': 1, + 'ampTest': 'This will be replace the param with the same name in other array' + } + const allParams = mergeParams(params1, params2); + + const paramsComplete = + { + 'hello': 'world', + 'ampTest': 'This will be replace the param with the same name in other array', + 'test': 1, + } + expect(allParams).not.to.be.null; + expect(JSON.stringify(allParams)).to.equal(JSON.stringify(paramsComplete)); + }) + it('Params to QueryString', () => { + const params = { + 'test': 1, + 'ampTest': 'ret', + 'empty': null, + 'quoteMark': '?', + 'test1': undefined + } + const queryString = paramsToQueryString(params); + + expect(queryString).not.to.be.null; + expect(queryString).to.equal('test=1&Test=ret&empty"eMark=%3F'); + }) + }) + + describe('isBidRequestValid', function () { + it('Should return true when required params found', function () { + const bidRequest = { + bidder: 'ampliffy', + params: { + server: 'bidder.ampliffy.com', + placementId: 1235465798, + format: 'all' + }, + mediaTypes: { + banner: { + sizes: [1, 1] + } + }, + } + expect(spec.isBidRequestValid(bidRequest)).to.be.true; + }) + it('Should return false when param format is display but mediaTypes are for video', function () { + const bidRequest = { + bidder: 'ampliffy', + params: { + server: 'bidder.ampliffy.com', + placementId: 1235465798, + format: 'display' + }, + mediaTypes: { + video: { + sizes: [1, 1] + } + }, + } + expect(spec.isBidRequestValid(bidRequest)).to.be.false; + }) + it('Should return false when param format is video but mediaTypes are for banner', function () { + const bidRequest = { + bidder: 'ampliffy', + params: { + server: 'bidder.ampliffy.com', + placementId: 1235465798, + format: 'video' + }, + mediaTypes: { + banner: { + sizes: [1, 1] + } + }, + } + expect(spec.isBidRequestValid(bidRequest)).to.be.false; + }) + it('Should return true when param format is video and mediaTypes are for video', function () { + const bidRequest = { + bidder: 'ampliffy', + params: { + server: 'bidder.ampliffy.com', + placementId: 1235465798, + format: 'video' + }, + mediaTypes: { + video: { + sizes: [1, 1] + } + }, + } + expect(spec.isBidRequestValid(bidRequest)).to.be.true; + }) + it('Should return true when param format is display and mediaTypes are for banner', function () { + const bidRequest = { + bidder: 'ampliffy', + params: { + server: 'bidder.ampliffy.com', + placementId: 1235465798, + format: 'display' + }, + mediaTypes: { + banner: { + sizes: [1, 1] + } + }, + } + expect(spec.isBidRequestValid(bidRequest)).to.be.true; + }) + it('Should return true when param format is all and mediaTypes are for banner', function () { + const bidRequest = { + bidder: 'ampliffy', + params: { + server: 'bidder.ampliffy.com', + placementId: 1235465798, + format: 'all' + }, + mediaTypes: { + banner: { + sizes: [1, 1] + } + }, + } + expect(spec.isBidRequestValid(bidRequest)).to.be.true; + }) + it('Should return true when param format is all and mediaTypes are for video', function () { + const bidRequest = { + bidder: 'ampliffy', + params: { + server: 'bidder.ampliffy.com', + placementId: 1235465798, + format: 'all' + }, + mediaTypes: { + video: { + sizes: [1, 1] + } + }, + } + expect(spec.isBidRequestValid(bidRequest)).to.be.true; + }) + it('Should return false without placementId param', function () { + const bidRequest = { + bidder: 'ampliffy', + params: {} + } + expect(spec.isBidRequestValid(bidRequest)).to.be.false; + }) + it('Should return false without param object', function () { + const bidRequest = { + bidder: 'ampliffy', + } + expect(spec.isBidRequestValid(bidRequest)).to.be.false; + }) + }); + + describe('Build request function', function () { + const bidderRequest = { + 'bidderCode': 'ampliffy', + 'auctionId': 'c4a771bf-1791-4513-82b3-96c48d19ddff', + 'bidderRequestId': '1134bdcbe47f25', + 'bids': [{ + 'bidder': 'ampliffy', + 'params': { + 'placementId': 1235465798, + 'type': 'bidder.', + 'region': 'alan-development.k8s.', + 'adnetwork': 'ampliffy.com', + 'SERVER': 'bidder.ampliffy.com' + }, + 'crumbs': {'pubcid': '29844d69-c4e5-4b00-8602-6dd09815363a'}, + 'ortb2Imp': {'ext': {'data': {'pbadslot': 'video1'}}}, + 'mediaTypes': { + 'video': { + 'context': 'instream', + 'playerSize': [[640, 480]], + 'mimes': ['video/mp4'], + 'protocols': [1, 2, 3, 4, 5, 6, 7, 8], + 'playbackmethod': [2], + 'skip': 1 + } + }, + 'adUnitCode': 'video1', + 'transactionId': 'f85c1b10-bad3-4c3f-a2bb-2c484c405bc9', + 'sizes': [[640, 480]], + 'bidId': '2bc71d9c058842', + 'bidderRequestId': '1134bdcbe47f25', + 'auctionId': 'c4a771bf-1791-4513-82b3-96c48d19ddff', + 'src': 'client', + 'bidRequestsCount': 1, + 'bidderRequestsCount': 1, + 'bidderWinsCount': 0 + }], + 'auctionStart': 1644029483655, + 'timeout': 3000, + 'refererInfo': { + 'referer': 'http://localhost:9999/integrationExamples/gpt/hello_world_video.html?pbjs_debug=true', + 'reachedTop': true, + 'isAmp': false, + 'numIframes': 0, + 'stack': ['http://localhost:9999/integrationExamples/gpt/hello_world_video.html?pbjs_debug=true'], + 'canonicalUrl': null + }, + 'start': 1644029483708 + } + const validBidRequests = [ + { + 'bidder': 'ampliffy', + 'params': { + 'placementId': 1235465798, + 'type': 'bidder.', + 'region': 'alan-development.k8s.', + 'adnetwork': 'ampliffy.com', + 'SERVER': 'bidder.ampliffy.com' + }, + 'crumbs': {'pubcid': '29844d69-c4e5-4b00-8602-6dd09815363a'}, + 'ortb2Imp': {'ext': {'data': {'pbadslot': 'video1'}}}, + 'mediaTypes': { + 'video': { + 'context': 'instream', + 'playerSize': [[640, 480]], + 'mimes': ['video/mp4'], + 'protocols': [1, 2, 3, 4, 5, 6, 7, 8], + 'playbackmethod': [2], + 'skip': 1 + } + }, + 'adUnitCode': 'video1', + 'transactionId': 'f85c1b10-bad3-4c3f-a2bb-2c484c405bc9', + 'sizes': [[640, 480]], + 'bidId': '2bc71d9c058842', + 'bidderRequestId': '1134bdcbe47f25', + 'auctionId': 'c4a771bf-1791-4513-82b3-96c48d19ddff', + 'src': 'client', + 'bidRequestsCount': 1, + 'bidderRequestsCount': 1, + 'bidderWinsCount': 0 + } + ]; + it('Should return one or more bid requests', function () { + expect(spec.buildRequests(validBidRequests, bidderRequest).length).to.be.greaterThan(0); + }); + }) + describe('Interpret response', function () { + let bidRequest = { + bidRequest: { + adUnitCode: 'div-gpt-ad-1460505748561-0', + auctionId: '469bb2e2-351f-4d01-b782-cdbca5e3e0ed', + bidId: '2d40b8dcd02ade', + bidRequestsCount: 1, + bidder: 'ampliffy', + bidderRequestId: '128c07edc4680f', + bidderRequestsCount: 1, + bidderWinsCount: 0, + crumbs: { + pubcid: '29844d69-c4e5-4b00-8602-6dd09815363a' + }, + mediaTypes: { + banner: { + sizes: [ + [300, 250], + [300, 600] + ] + } + }, + ortb2Imp: {ext: {}}, + params: {placementId: 13144370}, + sizes: [ + [300, 250], + [300, 600] + ], + src: 'client', + transactionId: '103b2b58-6ed1-45e9-9486-c942d6042e3' + }, + data: {bidId: '2d40b8dcd02ade'}, + method: 'GET', + url: 'https://test.com', + }; + + it('Should extract a CPM and currency from the xml', () => { + let cpmData = parseXML(xml); + expect(cpmData).to.not.be.a('null'); + expect(cpmData.cpm).to.equal('.23'); + expect(cpmData.currency).to.equal('USD'); + }); + + it('It should return no ads when the CPM is less than zero.', () => { + const xmlStr1 = ` + + + + + + + + +
+
+
+
+ + + ]]> +
+
+ + ES +
+
+
`; + let serverResponse = { + 'body': xmlStr1, + } + const bidResponses = spec.interpretResponse(serverResponse, bidRequest); + expect(bidResponses.length).to.equal(0); + }) + + it('It should return no ads when the creative url is not in the xml', () => { + const xmlStr1 = ` + + + + + + + + +
+
+
+
+ + ]]> + + + ES + + + `; + let serverResponse = { + 'body': xmlStr1, + } + const bidResponses = spec.interpretResponse(serverResponse, bidRequest); + expect(bidResponses.length).to.equal(0); + }) + it('It should return a banner ad.', () => { + let serverResponse = { + 'body': xmlStr, + } + setCurrentURL('https://www.sports.com'); + const bidResponses = spec.interpretResponse(serverResponse, bidRequest); + expect(bidResponses.length).greaterThan(0); + expect(bidResponses[0].mediaType).to.be.equal(BANNER); + expect(bidResponses[0].ad).not.to.be.null; + }) + it('It should return a video ad.', () => { + let serverResponse = { + 'body': xmlStr, + } + setCurrentURL('https://www.sports.com'); + bidRequest.bidRequest.mediaTypes = { + video: { + sizes: [ + [300, 250], + [300, 600] + ] + } + } + const bidResponses = spec.interpretResponse(serverResponse, bidRequest); + expect(bidResponses.length).greaterThan(0); + expect(bidResponses[0].mediaType).to.be.equal(VIDEO); + expect(bidResponses[0].vastUrl).not.to.be.null; + }) + }); +}); diff --git a/test/spec/modules/amxBidAdapter_spec.js b/test/spec/modules/amxBidAdapter_spec.js index 6f69e57d8bc..21fa2e2617c 100644 --- a/test/spec/modules/amxBidAdapter_spec.js +++ b/test/spec/modules/amxBidAdapter_spec.js @@ -3,6 +3,7 @@ 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 { server } from 'test/mocks/xhr.js'; import * as utils from 'src/utils.js'; const sampleRequestId = '82c91e127a9b93e'; @@ -11,7 +12,7 @@ const sampleDisplayCRID = '78827819'; // minimal example vast const sampleVideoAd = (addlImpression) => ` -00:00:15${addlImpression} +00:00:15${addlImpression} `.replace(/\n+/g, ''); const sampleFPD = { @@ -37,9 +38,11 @@ const sampleBidderRequest = { }, gppConsent: { gppString: 'example', - applicableSections: 'example' + applicableSections: 'example', }, - auctionId: utils.getUniqueIdentifierStr(), + + auctionId: null, + uspConsent: '1YYY', refererInfo: { reachedTop: true, @@ -90,7 +93,8 @@ const sampleBidRequestBase = { adUnitCode: 'div-gpt-ad-example', transactionId: utils.getUniqueIdentifierStr(), bidId: sampleRequestId, - auctionId: utils.getUniqueIdentifierStr(), + + auctionId: null, }; const schainConfig = { @@ -206,10 +210,12 @@ describe('AmxBidAdapter', () => { describe('getUserSync', () => { it('Will perform an iframe sync even if there is no server response..', () => { const syncs = spec.getUserSyncs({ iframeEnabled: true }); - expect(syncs).to.eql([{ - type: 'iframe', - url: 'https://prebid.a-mo.net/isyn?gdpr_consent=&gdpr=0&us_privacy=&gpp=&gpp_sid=' - }]); + 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', () => { @@ -273,8 +279,13 @@ describe('AmxBidAdapter', () => { }); it('will attach additional referrer info data', () => { - const { data } = spec.buildRequests([sampleBidRequestBase], sampleBidderRequest); - expect(data.ri.r).to.equal(sampleBidderRequest.refererInfo.topmostLocation); + 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); @@ -312,7 +323,7 @@ describe('AmxBidAdapter', () => { [sampleBidRequestBase], sampleBidderRequest ); - delete data.m; // don't deal with "m" in this test + delete data.m; // don't deal with 'm' in this test expect(data.gs).to.equal(sampleBidderRequest.gdprConsent.gdprApplies); expect(data.gc).to.equal(sampleBidderRequest.gdprConsent.consentString); expect(data.usp).to.equal(sampleBidderRequest.uspConsent); @@ -340,10 +351,8 @@ describe('AmxBidAdapter', () => { }); it('will attach sync configuration', () => { - const request = () => spec.buildRequests( - [sampleBidRequestBase], - sampleBidderRequest - ); + const request = () => + spec.buildRequests([sampleBidRequestBase], sampleBidderRequest); const setConfig = (filterSettings) => config.setConfig({ @@ -352,56 +361,73 @@ describe('AmxBidAdapter', () => { 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 }]] + 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); + expect(test(result), `input: ${JSON.stringify(result)}`).to.deep.equal( + expected + ); } }); @@ -494,7 +520,15 @@ describe('AmxBidAdapter', () => { it('can build a video request', () => { const { data } = spec.buildRequests( - [{...sampleBidRequestVideo, params: { ...sampleBidRequestVideo.params, adUnitId: 'custom-auid' }}], + [ + { + ...sampleBidRequestVideo, + params: { + ...sampleBidRequestVideo.params, + adUnitId: 'custom-auid', + }, + }, + ], sampleBidderRequest ); expect(Object.keys(data.m).length).to.equal(1); @@ -656,15 +690,49 @@ describe('AmxBidAdapter', () => { }); it('will log an event for timeout', () => { - spec.onTimeout({ - bidder: 'example', - bidId: 'test-bid-id', - adUnitCode: 'div-gpt-ad', - timeout: 300, - auctionId: utils.getUniqueIdentifierStr(), + // this will use sendBeacon.. + spec.onTimeout([ + { + bidder: 'example', + bidId: 'test-bid-id', + adUnitCode: 'div-gpt-ad', + ortb2: { + site: { + ref: 'https://example.com', + }, + }, + params: { + tagId: 'tag-id', + }, + timeout: 300, + auctionId: utils.getUniqueIdentifierStr(), + }, + ]); + + const [request] = server.requests; + request.respond(204, {'Content-Type': 'text/html'}, null); + expect(request.url).to.equal('https://1x1.a-mo.net/e'); + + if (typeof Request !== 'undefined' && 'keepalive' in Request.prototype) { + expect(request.fetch.request.keepalive).to.equal(true); + } + + const {c: common, e: events} = JSON.parse(request.requestBody) + expect(common).to.deep.equal({ + V: '$prebid.version$', + vg: '$$PREBID_GLOBAL$$', + U: null, + re: 'https://example.com', }); - expect(firedPixels.length).to.equal(1); - expect(firedPixels[0]).to.match(/\/hbx\/g_pbto/); + + expect(events.length).to.equal(1); + const [event] = events; + expect(event.n).to.equal('g_pbto') + expect(event.A).to.equal('example'); + expect(event.mid).to.equal('tag-id'); + expect(event.cn).to.equal(300); + expect(event.bid).to.equal('test-bid-id'); + expect(event.a).to.equal('div-gpt-ad'); }); it('will log an event for prebid win', () => { diff --git a/test/spec/modules/appnexusBidAdapter_spec.js b/test/spec/modules/appnexusBidAdapter_spec.js index 13ef31a68d4..c44ef475290 100644 --- a/test/spec/modules/appnexusBidAdapter_spec.js +++ b/test/spec/modules/appnexusBidAdapter_spec.js @@ -1093,7 +1093,18 @@ describe('AppNexusAdapter', function () { expect(payload.tags[0].use_pmt_rule).to.equal(true); }); - it('should add gpid to the request', function () { + it('should add preferred gpid to the request', function () { + let testGpid = '/12345/my-gpt-tag-0'; + let bidRequest = deepClone(bidRequests[0]); + bidRequest.ortb2Imp = { ext: { gpid: testGpid } }; + + const request = spec.buildRequests([bidRequest]); + const payload = JSON.parse(request.data); + + expect(payload.tags[0].gpid).to.exist.and.equal(testGpid) + }); + + it('should add backup gpid to the request', function () { let testGpid = '/12345/my-gpt-tag-0'; let bidRequest = deepClone(bidRequests[0]); bidRequest.ortb2Imp = { ext: { data: { pbadslot: testGpid } } }; @@ -1193,6 +1204,46 @@ describe('AppNexusAdapter', function () { expect(payload.privacy.gpp_sid).to.deep.equal([7]); }); + it('should add dsa information to the request via bidderRequest.ortb2.regs.ext.dsa', function () { + let bidderRequest = { + 'bidderCode': 'appnexus', + 'auctionId': '1d1a030790a475', + 'bidderRequestId': '22edbae2733bf6', + 'timeout': 3000, + 'ortb2': { + 'regs': { + 'ext': { + 'dsa': { + 'required': 1, + 'pubrender': 0, + 'datatopub': 1, + 'transparency': [{ + 'domain': 'good-domain', + 'params': [1, 2] + }, { + 'domain': 'bad-setup', + 'params': ['1', 3] + }] + } + } + } + } + }; + bidderRequest.bids = bidRequests; + + const request = spec.buildRequests(bidRequests, bidderRequest); + const payload = JSON.parse(request.data); + + expect(payload.dsa).to.exist; + expect(payload.dsa.dsainfo).to.equal(1); + expect(payload.dsa.pubrender).to.equal(0); + expect(payload.dsa.datatopub).to.equal(1); + expect(payload.dsa.transparency).to.deep.equal([{ + 'domain': 'good-domain', + 'params': [1, 2] + }]); + }); + it('supports sending hybrid mobile app parameters', function () { let appRequest = Object.assign({}, bidRequests[0], @@ -1564,6 +1615,15 @@ describe('AppNexusAdapter', function () { 'viewability': { 'config': '' }, + 'dsa': { + 'behalf': 'test-behalf', + 'paid': 'test-paid', + 'transparency': [{ + 'domain': 'good-domain', + 'params': [1, 2, 3] + }], + 'adrender': 1 + }, 'rtb': { 'banner': { 'content': '', @@ -1612,6 +1672,15 @@ describe('AppNexusAdapter', function () { 'nodes': [{ 'bsid': '958' }] + }, + 'dsa': { + 'behalf': 'test-behalf', + 'paid': 'test-paid', + 'transparency': [{ + 'domain': 'good-domain', + 'params': [1, 2, 3] + }], + 'adrender': 1 } } } diff --git a/test/spec/modules/asteriobidAnalyticsAdapter_spec.js b/test/spec/modules/asteriobidAnalyticsAdapter_spec.js new file mode 100644 index 00000000000..9be6c1dedac --- /dev/null +++ b/test/spec/modules/asteriobidAnalyticsAdapter_spec.js @@ -0,0 +1,151 @@ +import asteriobidAnalytics, {storage} from 'modules/asteriobidAnalyticsAdapter.js'; +import {expect} from 'chai'; +import {server} from 'test/mocks/xhr.js'; +import * as utils from 'src/utils.js'; +import {expectEvents} from '../../helpers/analytics.js'; + +let events = require('src/events'); +let constants = require('src/constants.json'); + +describe('AsterioBid Analytics Adapter', function () { + let bidWonEvent = { + 'bidderCode': 'appnexus', + 'width': 300, + 'height': 250, + 'adId': '1ebb82ec35375e', + 'mediaType': 'banner', + 'cpm': 0.5, + 'requestId': '1582271863760569973', + 'creative_id': '96846035', + 'creativeId': '96846035', + 'ttl': 60, + 'currency': 'USD', + 'netRevenue': true, + 'auctionId': '9c7b70b9-b6ab-4439-9e71-b7b382797c18', + 'responseTimestamp': 1537521629657, + 'requestTimestamp': 1537521629331, + 'bidder': 'appnexus', + 'adUnitCode': 'div-gpt-ad-1460505748561-0', + 'timeToRespond': 326, + 'size': '300x250', + 'status': 'rendered', + 'eventType': 'bidWon', + 'ad': 'some ad', + 'adUrl': 'ad url' + }; + + describe('AsterioBid Analytic tests', function () { + beforeEach(function () { + sinon.stub(events, 'getEvents').returns([]); + }); + + afterEach(function () { + asteriobidAnalytics.disableAnalytics(); + events.getEvents.restore(); + }); + + it('support custom endpoint', function () { + let custom_url = 'custom url'; + asteriobidAnalytics.enableAnalytics({ + provider: 'asteriobid', + options: { + url: custom_url, + bundleId: 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx' + } + }); + + expect(asteriobidAnalytics.getOptions().url).to.equal(custom_url); + }); + + it('bid won event', function() { + let bundleId = 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx'; + asteriobidAnalytics.enableAnalytics({ + provider: 'asteriobid', + options: { + bundleId: bundleId + } + }); + + events.emit(constants.EVENTS.BID_WON, bidWonEvent); + asteriobidAnalytics.flush(); + + expect(server.requests.length).to.equal(1); + expect(server.requests[0].url).to.equal('https://endpt.asteriobid.com/endpoint'); + expect(server.requests[0].requestBody.substring(0, 2)).to.equal('1:'); + + const pmEvents = JSON.parse(server.requests[0].requestBody.substring(2)); + expect(pmEvents.pageViewId).to.exist; + expect(pmEvents.bundleId).to.equal(bundleId); + expect(pmEvents.ver).to.equal(1); + expect(pmEvents.events.length).to.equal(1); + expect(pmEvents.events[0].eventType).to.equal('bidWon'); + expect(pmEvents.events[0].ad).to.be.undefined; + expect(pmEvents.events[0].adUrl).to.be.undefined; + }); + + it('track event without errors', function () { + sinon.spy(asteriobidAnalytics, 'track'); + + asteriobidAnalytics.enableAnalytics({ + provider: 'asteriobid', + options: { + bundleId: 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx' + } + }); + + expectEvents().to.beTrackedBy(asteriobidAnalytics.track); + }); + }); + + describe('build utm tag data', function () { + let getDataFromLocalStorageStub; + this.timeout(4000) + beforeEach(function () { + getDataFromLocalStorageStub = sinon.stub(storage, 'getDataFromLocalStorage'); + getDataFromLocalStorageStub.withArgs('pm_utm_source').returns('utm_source'); + getDataFromLocalStorageStub.withArgs('pm_utm_medium').returns('utm_medium'); + getDataFromLocalStorageStub.withArgs('pm_utm_campaign').returns('utm_camp'); + getDataFromLocalStorageStub.withArgs('pm_utm_term').returns(''); + getDataFromLocalStorageStub.withArgs('pm_utm_content').returns(''); + }); + afterEach(function () { + getDataFromLocalStorageStub.restore(); + asteriobidAnalytics.disableAnalytics() + }); + it('should build utm data from local storage', function () { + asteriobidAnalytics.enableAnalytics({ + provider: 'asteriobid', + options: { + bundleId: 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx' + } + }); + + const pmEvents = JSON.parse(server.requests[0].requestBody.substring(2)); + + expect(pmEvents.utmTags.utm_source).to.equal('utm_source'); + expect(pmEvents.utmTags.utm_medium).to.equal('utm_medium'); + expect(pmEvents.utmTags.utm_campaign).to.equal('utm_camp'); + expect(pmEvents.utmTags.utm_term).to.equal(''); + expect(pmEvents.utmTags.utm_content).to.equal(''); + }); + }); + + describe('build page info', function () { + afterEach(function () { + asteriobidAnalytics.disableAnalytics() + }); + it('should build page info', function () { + asteriobidAnalytics.enableAnalytics({ + provider: 'asteriobid', + options: { + bundleId: 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx' + } + }); + + const pmEvents = JSON.parse(server.requests[0].requestBody.substring(2)); + + expect(pmEvents.pageInfo.domain).to.equal(window.location.hostname); + expect(pmEvents.pageInfo.referrerDomain).to.equal(utils.parseUrl(document.referrer).hostname); + }); + }); +}); diff --git a/test/spec/modules/automatadAnalyticsAdapter_spec.js b/test/spec/modules/automatadAnalyticsAdapter_spec.js new file mode 100644 index 00000000000..e591f7e8e95 --- /dev/null +++ b/test/spec/modules/automatadAnalyticsAdapter_spec.js @@ -0,0 +1,533 @@ +import * as events from 'src/events'; +import * as utils from 'src/utils.js'; + +import spec, {self as exports} from 'modules/automatadAnalyticsAdapter.js'; + +import CONSTANTS from 'src/constants.json'; +import { expect } from 'chai'; + +const { + AUCTION_DEBUG, + BID_REQUESTED, + BID_REJECTED, + AUCTION_INIT, + BIDDER_DONE, + BID_RESPONSE, + BID_TIMEOUT, + BID_WON, + NO_BID +} = CONSTANTS.EVENTS + +const CONFIG_WITH_DEBUG = { + provider: 'atmtdAnalyticsAdapter', + options: { + publisherID: '230', + siteID: '421' + }, + includeEvents: [AUCTION_DEBUG, AUCTION_INIT, BIDDER_DONE, BID_RESPONSE, BID_TIMEOUT, NO_BID, BID_WON, BID_REQUESTED, BID_REJECTED] +} + +describe('Automatad Analytics Adapter', () => { + var sandbox, clock; + + describe('Adapter Setup Configuration', () => { + beforeEach(() => { + sandbox = sinon.createSandbox(); + sandbox.stub(utils, 'logMessage') + sandbox.stub(events, 'getEvents').returns([]); + sandbox.stub(utils, 'logError'); + }); + afterEach(() => { + sandbox.restore(); + }); + + it('Should log error and return false if nothing is passed as the param in the enable analytics call', () => { + spec.enableAnalytics() + + expect(utils.logError.called).to.equal(true) + }); + + it('Should log error and return false if object type is not passed as the param in the enable analytics call', () => { + spec.enableAnalytics('hello world') + + expect(utils.logError.called).to.equal(true) + }); + + it('Should log error and return false if options is not defined in the enable analytics call', () => { + spec.enableAnalytics({ + provider: 'atmtdAnalyticsAdapter' + }) + + expect(utils.logError.called).to.equal(true) + }); + it('Should log error and return false if pub id is not defined in the enable analytics call', () => { + spec.enableAnalytics({ + provider: 'atmtdAnalyticsAdapter', + options: { + siteID: '230' + } + }) + + expect(utils.logError.called).to.equal(true) + }); + it('Should log error and return false if pub id is not defined in the enable analytics call', () => { + spec.enableAnalytics({ + provider: 'atmtdAnalyticsAdapter', + options: { + publisherID: '230' + } + }) + + expect(utils.logError.called).to.equal(true) + }); + it('Should successfully configure the adapter and set global log debug messages flag to false', () => { + spec.enableAnalytics({ + provider: 'atmtdAnalyticsAdapter', + options: { + publisherID: '230', + siteID: '421', + logDebug: false + } + }); + expect(utils.logError.called).to.equal(false) + expect(utils.logMessage.called).to.equal(true) + spec.disableAnalytics(); + }); + it('Should successfully configure the adapter and set global log debug messages flag to true', () => { + sandbox.stub(exports, 'initializeQueue').callsFake(() => {}); + sandbox.stub(exports, 'addGPTHandlers').callsFake(() => {}); + const config = { + provider: 'atmtdAnalyticsAdapter', + options: { + publisherID: '230', + siteID: '410', + logDebug: true + } + } + + spec.enableAnalytics(config) + expect(utils.logError.called).to.equal(false) + expect(exports.initializeQueue.called).to.equal(true) + expect(exports.addGPTHandlers.called).to.equal(true) + expect(utils.logMessage.called).to.equal(true) + spec.disableAnalytics(); + }); + }); + + describe('Behaviour of the adapter when the sdk has loaded', () => { + before(() => { + spec.enableAnalytics(CONFIG_WITH_DEBUG); + const obj = { + auctionInitHandler: (args) => {}, + bidResponseHandler: (args) => {}, + bidderDoneHandler: (args) => {}, + bidWonHandler: (args) => {}, + noBidHandler: (args) => {}, + auctionDebugHandler: (args) => {}, + bidderTimeoutHandler: (args) => {}, + bidRequestedHandler: (args) => {}, + bidRejectedHandler: (args) => {} + } + + global.window.atmtdAnalytics = obj + + Object.keys(obj).forEach((fn) => sandbox.spy(global.window.atmtdAnalytics, fn)) + }) + beforeEach(() => { + sandbox = sinon.createSandbox(); + sandbox.stub(events, 'getEvents').returns([]); + sandbox.stub(utils, 'logMessage'); + sandbox.stub(utils, 'logError'); + }); + afterEach(() => { + sandbox.restore(); + }); + after(() => { + global.window.atmtdAnalytics = undefined; + spec.disableAnalytics(); + }) + + it('Should call the auctionInitHandler when the auction init event is fired', () => { + events.emit(AUCTION_INIT, {type: AUCTION_INIT}) + expect(global.window.atmtdAnalytics.auctionInitHandler.called).to.equal(true) + }); + + it('Should call the bidRequested when the bidRequested event is fired', () => { + events.emit(BID_REQUESTED, {type: BID_REQUESTED}) + expect(global.window.atmtdAnalytics.bidRequestedHandler.called).to.equal(true) + }); + + it('Should call the bidRejected when the bidRejected event is fired', () => { + events.emit(BID_REJECTED, {type: BID_REJECTED}) + expect(global.window.atmtdAnalytics.bidRejectedHandler.called).to.equal(true) + }); + + it('Should call the bidResponseHandler when the bidResponse event is fired', () => { + events.emit(BID_RESPONSE, {type: BID_RESPONSE}) + expect(global.window.atmtdAnalytics.bidResponseHandler.called).to.equal(true) + }); + + it('Should call the bidderDoneHandler when the bidderDone event is fired', () => { + events.emit(BIDDER_DONE, {type: BIDDER_DONE}) + expect(global.window.atmtdAnalytics.bidderDoneHandler.called).to.equal(true) + }); + + it('Should call the bidWonHandler when the bidWon event is fired', () => { + events.emit(BID_WON, {type: BID_WON}) + expect(global.window.atmtdAnalytics.bidWonHandler.called).to.equal(true) + }); + + it('Should call the noBidHandler when the noBid event is fired', () => { + events.emit(NO_BID, {type: NO_BID}) + expect(global.window.atmtdAnalytics.noBidHandler.called).to.equal(true) + }); + + it('Should call the bidTimeoutHandler when the bidTimeout event is fired', () => { + events.emit(BID_TIMEOUT, {type: BID_TIMEOUT}) + expect(global.window.atmtdAnalytics.bidderTimeoutHandler.called).to.equal(true) + }); + + it('Should call the auctionDebugHandler when the auctionDebug event is fired', () => { + events.emit(AUCTION_DEBUG, {type: AUCTION_DEBUG}) + expect(global.window.atmtdAnalytics.auctionDebugHandler.called).to.equal(true) + }); + }); + + describe('Behaviour of the adapter when the SDK has not loaded', () => { + before(() => { + spec.enableAnalytics(CONFIG_WITH_DEBUG); + }) + beforeEach(() => { + sandbox = sinon.createSandbox(); + sandbox.stub(events, 'getEvents').returns([]); + sandbox.stub(utils, 'logMessage'); + sandbox.stub(utils, 'logError'); + + global.window.atmtdAnalytics = undefined + exports.__atmtdAnalyticsQueue.length = 0 + sandbox.stub(exports.__atmtdAnalyticsQueue, 'push').callsFake((args) => { + Array.prototype.push.apply(exports.__atmtdAnalyticsQueue, [args]); + }) + }); + afterEach(() => { + sandbox.restore(); + }); + after(() => { + spec.disableAnalytics(); + }) + + it('Should push to the que when the auctionInit event is fired', () => { + events.emit(AUCTION_INIT, {type: AUCTION_INIT}) + expect(exports.__atmtdAnalyticsQueue.push.called).to.equal(true) + expect(exports.__atmtdAnalyticsQueue).to.be.an('array').to.have.lengthOf(1) + expect(exports.__atmtdAnalyticsQueue[0]).to.have.lengthOf(2) + expect(exports.__atmtdAnalyticsQueue[0][0]).to.equal(AUCTION_INIT) + expect(exports.__atmtdAnalyticsQueue[0][1].type).to.equal(AUCTION_INIT) + }); + + it('Should push to the que when the bidResponse event is fired', () => { + events.emit(BID_RESPONSE, {type: BID_RESPONSE}) + expect(exports.__atmtdAnalyticsQueue.push.called).to.equal(true) + expect(exports.__atmtdAnalyticsQueue).to.be.an('array').to.have.lengthOf(1) + expect(exports.__atmtdAnalyticsQueue[0]).to.have.lengthOf(2) + expect(exports.__atmtdAnalyticsQueue[0][0]).to.equal(BID_RESPONSE) + expect(exports.__atmtdAnalyticsQueue[0][1].type).to.equal(BID_RESPONSE) + }); + + it('Should push to the que when the bidRequested event is fired', () => { + events.emit(BID_REQUESTED, {type: BID_REQUESTED}) + expect(exports.__atmtdAnalyticsQueue.push.called).to.equal(true) + expect(exports.__atmtdAnalyticsQueue).to.be.an('array').to.have.lengthOf(1) + expect(exports.__atmtdAnalyticsQueue[0]).to.have.lengthOf(2) + expect(exports.__atmtdAnalyticsQueue[0][0]).to.equal(BID_REQUESTED) + expect(exports.__atmtdAnalyticsQueue[0][1].type).to.equal(BID_REQUESTED) + }); + + it('Should push to the que when the bidRejected event is fired', () => { + events.emit(BID_REJECTED, {type: BID_REJECTED}) + expect(exports.__atmtdAnalyticsQueue.push.called).to.equal(true) + expect(exports.__atmtdAnalyticsQueue).to.be.an('array').to.have.lengthOf(1) + expect(exports.__atmtdAnalyticsQueue[0]).to.have.lengthOf(2) + expect(exports.__atmtdAnalyticsQueue[0][0]).to.equal(BID_REJECTED) + expect(exports.__atmtdAnalyticsQueue[0][1].type).to.equal(BID_REJECTED) + }); + + it('Should push to the que when the bidderDone event is fired', () => { + events.emit(BIDDER_DONE, {type: BIDDER_DONE}) + expect(exports.__atmtdAnalyticsQueue.push.called).to.equal(true) + expect(exports.__atmtdAnalyticsQueue).to.be.an('array').to.have.lengthOf(1) + expect(exports.__atmtdAnalyticsQueue[0]).to.have.lengthOf(2) + expect(exports.__atmtdAnalyticsQueue[0][0]).to.equal(BIDDER_DONE) + expect(exports.__atmtdAnalyticsQueue[0][1].type).to.equal(BIDDER_DONE) + }); + + it('Should push to the que when the bidWon event is fired', () => { + events.emit(BID_WON, {type: BID_WON}) + expect(exports.__atmtdAnalyticsQueue.push.called).to.equal(true) + expect(exports.__atmtdAnalyticsQueue).to.be.an('array').to.have.lengthOf(1) + expect(exports.__atmtdAnalyticsQueue[0]).to.have.lengthOf(2) + expect(exports.__atmtdAnalyticsQueue[0][0]).to.equal(BID_WON) + expect(exports.__atmtdAnalyticsQueue[0][1].type).to.equal(BID_WON) + }); + + it('Should push to the que when the noBid event is fired', () => { + events.emit(NO_BID, {type: NO_BID}) + expect(exports.__atmtdAnalyticsQueue.push.called).to.equal(true) + expect(exports.__atmtdAnalyticsQueue).to.be.an('array').to.have.lengthOf(1) + expect(exports.__atmtdAnalyticsQueue[0]).to.have.lengthOf(2) + expect(exports.__atmtdAnalyticsQueue[0][0]).to.equal(NO_BID) + expect(exports.__atmtdAnalyticsQueue[0][1].type).to.equal(NO_BID) + }); + + it('Should push to the que when the auctionDebug is fired', () => { + events.emit(AUCTION_DEBUG, {type: AUCTION_DEBUG}) + expect(exports.__atmtdAnalyticsQueue.push.called).to.equal(true) + expect(exports.__atmtdAnalyticsQueue).to.be.an('array').to.have.lengthOf(1) + expect(exports.__atmtdAnalyticsQueue[0]).to.have.lengthOf(2) + expect(exports.__atmtdAnalyticsQueue[0][0]).to.equal(AUCTION_DEBUG) + expect(exports.__atmtdAnalyticsQueue[0][1].type).to.equal(AUCTION_DEBUG) + }); + + it('Should push to the que when the bidderTimeout event is fired', () => { + events.emit(BID_TIMEOUT, {type: BID_TIMEOUT}) + expect(exports.__atmtdAnalyticsQueue.push.called).to.equal(true) + expect(exports.__atmtdAnalyticsQueue).to.be.an('array').to.have.lengthOf(1) + expect(exports.__atmtdAnalyticsQueue[0]).to.have.lengthOf(2) + expect(exports.__atmtdAnalyticsQueue[0][0]).to.equal(BID_TIMEOUT) + expect(exports.__atmtdAnalyticsQueue[0][1].type).to.equal(BID_TIMEOUT) + }); + }); + + describe('Process Events from Que when SDK still has not loaded', () => { + before(() => { + spec.enableAnalytics({ + provider: 'atmtdAnalyticsAdapter', + options: { + publisherID: '230', + siteID: '421' + } + }); + global.window.atmtdAnalytics = undefined + + sandbox.stub(exports.__atmtdAnalyticsQueue, 'push').callsFake((args) => { + Array.prototype.push.apply(exports.__atmtdAnalyticsQueue, [args]); + }) + }) + beforeEach(() => { + sandbox = sinon.createSandbox(); + sandbox.stub(events, 'getEvents').returns([]); + sandbox.spy(exports, 'prettyLog') + sandbox.spy(exports, 'processEvents') + + clock = sandbox.useFakeTimers(); + exports.__atmtdAnalyticsQueue.length = 0 + }); + afterEach(() => { + sandbox.restore(); + exports.queuePointer = 0; + exports.retryCount = 0; + exports.__atmtdAnalyticsQueue = [] + spec.disableAnalytics(); + }) + + it('Should retry processing auctionInit in certain intervals', () => { + expect(exports.queuePointer).to.equal(0) + expect(exports.retryCount).to.equal(0) + const que = [[AUCTION_INIT, {type: AUCTION_INIT}]] + exports.__atmtdAnalyticsQueue.push(que[0]) + exports.processEvents() + expect(exports.prettyLog.getCall(0).args[0]).to.equal('status') + expect(exports.prettyLog.getCall(0).args[1]).to.equal(`Que has been inactive for a while. Adapter starting to process que now... Trial Count = 1`) + expect(exports.prettyLog.getCall(1).args[0]).to.equal('warn') + expect(exports.prettyLog.getCall(1).args[1]).to.equal(`Adapter failed to process event as aggregator has not loaded. Retrying in 1500ms ...`) + clock.tick(1510) + expect(exports.prettyLog.getCall(2).args[0]).to.equal('status') + expect(exports.prettyLog.getCall(2).args[1]).to.equal(`Que has been inactive for a while. Adapter starting to process que now... Trial Count = 2`) + expect(exports.prettyLog.getCall(3).args[0]).to.equal('warn') + expect(exports.prettyLog.getCall(3).args[1]).to.equal(`Adapter failed to process event as aggregator has not loaded. Retrying in 3000ms ...`) + clock.tick(3010) + expect(exports.prettyLog.getCall(4).args[0]).to.equal('status') + expect(exports.prettyLog.getCall(4).args[1]).to.equal(`Que has been inactive for a while. Adapter starting to process que now... Trial Count = 3`) + expect(exports.prettyLog.getCall(5).args[0]).to.equal('warn') + expect(exports.prettyLog.getCall(5).args[1]).to.equal(`Adapter failed to process event as aggregator has not loaded. Retrying in 5000ms ...`) + clock.tick(5010) + expect(exports.prettyLog.getCall(6).args[0]).to.equal('status') + expect(exports.prettyLog.getCall(6).args[1]).to.equal(`Que has been inactive for a while. Adapter starting to process que now... Trial Count = 4`) + expect(exports.prettyLog.getCall(7).args[0]).to.equal('warn') + expect(exports.prettyLog.getCall(7).args[1]).to.equal(`Adapter failed to process event as aggregator has not loaded. Retrying in 10000ms ...`) + clock.tick(10010) + expect(exports.prettyLog.getCall(8).args[0]).to.equal('error') + expect(exports.prettyLog.getCall(8).args[1]).to.equal(`Aggregator still hasn't loaded. Processing que stopped`) + expect(exports.queuePointer).to.equal(0) + expect(exports.processEvents.callCount).to.equal(5) + }) + + it('Should retry processing slotRenderEnded in certain intervals', () => { + expect(exports.queuePointer).to.equal(0) + expect(exports.retryCount).to.equal(0) + const que = [['slotRenderEnded', {type: 'slotRenderEnded'}]] + exports.__atmtdAnalyticsQueue.push(que[0]) + exports.processEvents() + expect(exports.prettyLog.getCall(0).args[0]).to.equal('status') + expect(exports.prettyLog.getCall(0).args[1]).to.equal(`Que has been inactive for a while. Adapter starting to process que now... Trial Count = 1`) + expect(exports.prettyLog.getCall(1).args[0]).to.equal('warn') + expect(exports.prettyLog.getCall(1).args[1]).to.equal(`Adapter failed to process event as aggregator has not loaded. Retrying in 1500ms ...`) + clock.tick(1510) + expect(exports.prettyLog.getCall(2).args[0]).to.equal('status') + expect(exports.prettyLog.getCall(2).args[1]).to.equal(`Que has been inactive for a while. Adapter starting to process que now... Trial Count = 2`) + expect(exports.prettyLog.getCall(3).args[0]).to.equal('warn') + expect(exports.prettyLog.getCall(3).args[1]).to.equal(`Adapter failed to process event as aggregator has not loaded. Retrying in 3000ms ...`) + clock.tick(3010) + expect(exports.prettyLog.getCall(4).args[0]).to.equal('status') + expect(exports.prettyLog.getCall(4).args[1]).to.equal(`Que has been inactive for a while. Adapter starting to process que now... Trial Count = 3`) + expect(exports.prettyLog.getCall(5).args[0]).to.equal('warn') + expect(exports.prettyLog.getCall(5).args[1]).to.equal(`Adapter failed to process event as aggregator has not loaded. Retrying in 5000ms ...`) + clock.tick(5010) + expect(exports.prettyLog.getCall(6).args[0]).to.equal('status') + expect(exports.prettyLog.getCall(6).args[1]).to.equal(`Que has been inactive for a while. Adapter starting to process que now... Trial Count = 4`) + expect(exports.prettyLog.getCall(7).args[0]).to.equal('warn') + expect(exports.prettyLog.getCall(7).args[1]).to.equal(`Adapter failed to process event as aggregator has not loaded. Retrying in 10000ms ...`) + clock.tick(10010) + expect(exports.prettyLog.getCall(8).args[0]).to.equal('error') + expect(exports.prettyLog.getCall(8).args[1]).to.equal(`Aggregator still hasn't loaded. Processing que stopped`) + expect(exports.queuePointer).to.equal(0) + expect(exports.processEvents.callCount).to.equal(5) + }) + + it('Should retry processing impressionViewable in certain intervals', () => { + expect(exports.queuePointer).to.equal(0) + expect(exports.retryCount).to.equal(0) + const que = [['impressionViewable', {type: 'impressionViewable'}]] + exports.__atmtdAnalyticsQueue.push(que[0]) + exports.processEvents() + expect(exports.prettyLog.getCall(0).args[0]).to.equal('status') + expect(exports.prettyLog.getCall(0).args[1]).to.equal(`Que has been inactive for a while. Adapter starting to process que now... Trial Count = 1`) + expect(exports.prettyLog.getCall(1).args[0]).to.equal('warn') + expect(exports.prettyLog.getCall(1).args[1]).to.equal(`Adapter failed to process event as aggregator has not loaded. Retrying in 1500ms ...`) + clock.tick(1510) + expect(exports.prettyLog.getCall(2).args[0]).to.equal('status') + expect(exports.prettyLog.getCall(2).args[1]).to.equal(`Que has been inactive for a while. Adapter starting to process que now... Trial Count = 2`) + expect(exports.prettyLog.getCall(3).args[0]).to.equal('warn') + expect(exports.prettyLog.getCall(3).args[1]).to.equal(`Adapter failed to process event as aggregator has not loaded. Retrying in 3000ms ...`) + clock.tick(3010) + expect(exports.prettyLog.getCall(4).args[0]).to.equal('status') + expect(exports.prettyLog.getCall(4).args[1]).to.equal(`Que has been inactive for a while. Adapter starting to process que now... Trial Count = 3`) + expect(exports.prettyLog.getCall(5).args[0]).to.equal('warn') + expect(exports.prettyLog.getCall(5).args[1]).to.equal(`Adapter failed to process event as aggregator has not loaded. Retrying in 5000ms ...`) + clock.tick(5010) + expect(exports.prettyLog.getCall(6).args[0]).to.equal('status') + expect(exports.prettyLog.getCall(6).args[1]).to.equal(`Que has been inactive for a while. Adapter starting to process que now... Trial Count = 4`) + expect(exports.prettyLog.getCall(7).args[0]).to.equal('warn') + expect(exports.prettyLog.getCall(7).args[1]).to.equal(`Adapter failed to process event as aggregator has not loaded. Retrying in 10000ms ...`) + clock.tick(10010) + expect(exports.prettyLog.getCall(8).args[0]).to.equal('error') + expect(exports.prettyLog.getCall(8).args[1]).to.equal(`Aggregator still hasn't loaded. Processing que stopped`) + expect(exports.queuePointer).to.equal(0) + expect(exports.processEvents.callCount).to.equal(5) + }) + }); + + describe('Process Events from Que when SDK has loaded', () => { + before(() => { + spec.enableAnalytics({ + provider: 'atmtdAnalyticsAdapter', + options: { + publisherID: '230', + siteID: '421' + } + }); + sandbox = sinon.createSandbox(); + sandbox.reset() + const obj = { + auctionInitHandler: (args) => {}, + bidResponseHandler: (args) => {}, + bidderDoneHandler: (args) => {}, + bidWonHandler: (args) => {}, + noBidHandler: (args) => {}, + auctionDebugHandler: (args) => {}, + bidderTimeoutHandler: (args) => {}, + impressionViewableHandler: (args) => {}, + slotRenderEndedGPTHandler: (args) => {}, + bidRequestedHandler: (args) => {}, + bidRejectedHandler: (args) => {} + } + + global.window.atmtdAnalytics = obj; + + Object.keys(obj).forEach((fn) => sandbox.spy(global.window.atmtdAnalytics, fn)) + sandbox.stub(events, 'getEvents').returns([]); + sandbox.spy(exports, 'prettyLog') + exports.retryCount = 0; + exports.queuePointer = 0; + exports.__atmtdAnalyticsQueue = [ + [AUCTION_INIT, {type: AUCTION_INIT}], + [BID_RESPONSE, {type: BID_RESPONSE}], + [BID_REQUESTED, {type: BID_REQUESTED}], + [BID_REJECTED, {type: BID_REJECTED}], + [NO_BID, {type: NO_BID}], + [BID_WON, {type: BID_WON}], + [BIDDER_DONE, {type: BIDDER_DONE}], + [AUCTION_DEBUG, {type: AUCTION_DEBUG}], + [BID_TIMEOUT, {type: BID_TIMEOUT}], + ['slotRenderEnded', {type: 'slotRenderEnded'}], + ['impressionViewable', {type: 'impressionViewable'}] + ] + }); + after(() => { + sandbox.restore(); + spec.disableAnalytics(); + }) + + it('Should make calls to appropriate SDK event handlers', () => { + exports.processEvents() + expect(exports.prettyLog.getCall(0).args[0]).to.equal('status') + expect(exports.prettyLog.getCall(0).args[1]).to.equal(`Que has been inactive for a while. Adapter starting to process que now... Trial Count = 1`) + expect(exports.retryCount).to.equal(0) + expect(exports.prettyLog.callCount).to.equal(1) + expect(exports.queuePointer).to.equal(exports.__atmtdAnalyticsQueue.length) + expect(global.window.atmtdAnalytics.auctionInitHandler.calledOnce).to.equal(true) + expect(global.window.atmtdAnalytics.bidResponseHandler.calledOnce).to.equal(true) + expect(global.window.atmtdAnalytics.bidRejectedHandler.calledOnce).to.equal(true) + expect(global.window.atmtdAnalytics.bidRequestedHandler.calledOnce).to.equal(true) + expect(global.window.atmtdAnalytics.noBidHandler.calledOnce).to.equal(true) + expect(global.window.atmtdAnalytics.bidWonHandler.calledOnce).to.equal(true) + expect(global.window.atmtdAnalytics.auctionDebugHandler.calledOnce).to.equal(true) + expect(global.window.atmtdAnalytics.bidderTimeoutHandler.calledOnce).to.equal(true) + expect(global.window.atmtdAnalytics.bidderDoneHandler.calledOnce).to.equal(true) + expect(global.window.atmtdAnalytics.slotRenderEndedGPTHandler.calledOnce).to.equal(true) + expect(global.window.atmtdAnalytics.impressionViewableHandler.calledOnce).to.equal(true) + }) + }); + + describe('Prettylog fn tests', () => { + beforeEach(() => { + sandbox = sinon.createSandbox() + sandbox.spy(utils, 'logInfo') + sandbox.spy(utils, 'logError') + exports.isLoggingEnabled = true + }) + + afterEach(() => { + sandbox.restore() + }) + + it('Should call logMessage once in normal mode', () => { + exports.prettyLog('status', 'Hello world') + expect(utils.logInfo.callCount).to.equal(1) + }) + + it('Should call logMessage twice in group mode and have the cb called', () => { + const spy = sandbox.spy() + exports.prettyLog('status', 'Hello world', true, spy) + expect(utils.logInfo.callCount).to.equal(2) + expect(spy.called).to.equal(true) + }) + + it('Should call logMessage twice in group mode and have the cb which throws an error', () => { + const spy = sandbox.stub().throws() + exports.prettyLog('status', 'Hello world', true, spy) + expect(utils.logInfo.callCount).to.equal(2) + expect(utils.logError.called).to.equal(true) + }) + }); +}); diff --git a/test/spec/modules/axisBidAdapter_spec.js b/test/spec/modules/axisBidAdapter_spec.js new file mode 100644 index 00000000000..083f05f5c0a --- /dev/null +++ b/test/spec/modules/axisBidAdapter_spec.js @@ -0,0 +1,414 @@ +import { expect } from 'chai'; +import { spec } from '../../../modules/axisBidAdapter.js'; +import { BANNER, VIDEO, NATIVE } from '../../../src/mediaTypes.js'; +import { getUniqueIdentifierStr } from '../../../src/utils.js'; + +const bidder = 'axis' + +describe('AxisBidAdapter', function () { + const bids = [ + { + bidId: getUniqueIdentifierStr(), + bidder: bidder, + mediaTypes: { + [BANNER]: { + sizes: [[300, 250]], + pos: 1 + } + }, + params: { + integration: '000000', + token: '000000' + } + }, + { + bidId: getUniqueIdentifierStr(), + bidder: bidder, + mediaTypes: { + [VIDEO]: { + playerSize: [[300, 300]], + minduration: 5, + maxduration: 60, + pos: 1 + } + }, + params: { + integration: '000000', + token: '000000' + } + }, + { + bidId: getUniqueIdentifierStr(), + bidder: bidder, + mediaTypes: { + [NATIVE]: { + native: { + title: { + required: true + }, + body: { + required: true + }, + icon: { + required: true, + size: [64, 64] + } + } + } + }, + params: { + integration: '000000', + token: '000000' + } + } + ]; + + const invalidBid = { + bidId: getUniqueIdentifierStr(), + bidder: bidder, + mediaTypes: { + [BANNER]: { + sizes: [[300, 250]] + } + }, + params: { + + } + } + + const bidderRequest = { + uspConsent: '1---', + gdprConsent: 'COvFyGBOvFyGBAbAAAENAPCAAOAAAAAAAAAAAEEUACCKAAA.IFoEUQQgAIQwgIwQABAEAAAAOIAACAIAAAAQAIAgEAACEAAAAAgAQBAAAAAAAGBAAgAAAAAAAFAAECAAAgAAQARAEQAAAAAJAAIAAgAAAYQEAAAQmAgBC3ZAYzUw', + refererInfo: { + referer: 'https://test.com' + }, + ortb2: { + site: { + cat: ['IAB24'] + } + } + }; + + describe('isBidRequestValid', function () { + it('Should return true if there are bidId, params and key parameters present', function () { + expect(spec.isBidRequestValid(bids[0])).to.be.true; + }); + it('Should return false if at least one of parameters is not present', function () { + expect(spec.isBidRequestValid(invalidBid)).to.be.false; + }); + }); + + describe('buildRequests', function () { + let serverRequest = spec.buildRequests(bids, bidderRequest); + + it('Creates a ServerRequest object with method, URL and data', function () { + expect(serverRequest).to.exist; + expect(serverRequest.method).to.exist; + expect(serverRequest.url).to.exist; + expect(serverRequest.data).to.exist; + }); + + it('Returns POST method', function () { + expect(serverRequest.method).to.equal('POST'); + }); + + it('Returns valid URL', function () { + expect(serverRequest.url).to.equal('https://prebid.axis-marketplace.com/pbjs'); + }); + + it('Returns general data valid', function () { + let data = serverRequest.data; + expect(data).to.be.an('object'); + expect(data).to.have.all.keys('deviceWidth', + 'deviceHeight', + 'language', + 'secure', + 'host', + 'page', + 'placements', + 'iabCat', + 'coppa', + 'ccpa', + 'gdpr', + 'tmax' + ); + expect(data.deviceWidth).to.be.a('number'); + expect(data.deviceHeight).to.be.a('number'); + expect(data.language).to.be.a('string'); + expect(data.secure).to.be.within(0, 1); + expect(data.host).to.be.a('string'); + expect(data.page).to.be.a('string'); + expect(data.coppa).to.be.a('number'); + expect(data.gdpr).to.be.a('string'); + expect(data.ccpa).to.be.a('string'); + expect(data.tmax).to.be.a('number'); + expect(data.iabCat).to.have.lengthOf(1); + expect(data.placements).to.have.lengthOf(3); + }); + + it('Returns valid placements', function () { + const { placements } = serverRequest.data; + for (let i = 0, len = placements.length; i < len; i++) { + const placement = placements[i]; + expect(placement.adFormat).to.be.oneOf([BANNER, VIDEO, NATIVE]); + expect(placement.bidId).to.be.a('string'); + expect(placement.integration).to.be.a('string'); + expect(placement.token).to.be.a('string'); + expect(placement.schain).to.be.an('object'); + expect(placement.bidfloor).to.exist.and.to.equal(0); + + if (placement.adFormat === BANNER) { + expect(placement.sizes).to.be.an('array'); + } + switch (placement.adFormat) { + case BANNER: + expect(placement.sizes).to.be.an('array'); + expect(placement.pos).to.be.within(0, 7); + break; + case VIDEO: + expect(placement.playerSize).to.be.an('array'); + expect(placement.minduration).to.be.an('number'); + expect(placement.maxduration).to.be.an('number'); + expect(placement.pos).to.be.within(0, 7); + break; + case NATIVE: + expect(placement.native).to.be.an('object'); + break; + } + } + }); + + it('Returns data with gdprConsent and without uspConsent', function () { + delete bidderRequest.uspConsent; + serverRequest = spec.buildRequests(bids, bidderRequest); + let data = serverRequest.data; + expect(data.gdpr).to.exist; + expect(data.gdpr).to.be.a('string'); + expect(data.gdpr).to.equal(bidderRequest.gdprConsent); + expect(data.ccpa).to.not.exist; + delete bidderRequest.gdprConsent; + }); + + it('Returns data with uspConsent and without gdprConsent', function () { + bidderRequest.uspConsent = '1---'; + delete bidderRequest.gdprConsent; + serverRequest = spec.buildRequests(bids, bidderRequest); + let data = serverRequest.data; + expect(data.ccpa).to.exist; + expect(data.ccpa).to.be.a('string'); + expect(data.ccpa).to.equal(bidderRequest.uspConsent); + expect(data.gdpr).to.not.exist; + }); + + it('Returns empty data if no valid requests are passed', function () { + serverRequest = spec.buildRequests([], bidderRequest); + let data = serverRequest.data; + expect(data.placements).to.be.an('array').that.is.empty; + }); + }); + + describe('interpretResponse', function () { + it('Should interpret banner response', function () { + const banner = { + body: [{ + mediaType: 'banner', + width: 300, + height: 250, + cpm: 0.4, + ad: 'Test', + requestId: '23fhj33i987f', + ttl: 120, + creativeId: '2', + netRevenue: true, + currency: 'USD', + dealId: '1', + meta: { + advertiserDomains: ['google.com'], + advertiserId: 1234 + } + }] + }; + let bannerResponses = spec.interpretResponse(banner); + expect(bannerResponses).to.be.an('array').that.is.not.empty; + let dataItem = bannerResponses[0]; + expect(dataItem).to.have.all.keys('requestId', 'cpm', 'width', 'height', 'ad', 'ttl', 'creativeId', + 'netRevenue', 'currency', 'dealId', 'mediaType', 'meta'); + expect(dataItem.requestId).to.equal(banner.body[0].requestId); + expect(dataItem.cpm).to.equal(banner.body[0].cpm); + expect(dataItem.width).to.equal(banner.body[0].width); + expect(dataItem.height).to.equal(banner.body[0].height); + expect(dataItem.ad).to.equal(banner.body[0].ad); + expect(dataItem.ttl).to.equal(banner.body[0].ttl); + expect(dataItem.creativeId).to.equal(banner.body[0].creativeId); + expect(dataItem.netRevenue).to.be.true; + expect(dataItem.currency).to.equal(banner.body[0].currency); + expect(dataItem.meta).to.be.an('object').that.has.property('advertiserDomains'); + }); + it('Should interpret video response', function () { + const video = { + body: [{ + vastUrl: 'test.com', + mediaType: 'video', + cpm: 0.5, + requestId: '23fhj33i987f', + width: 300, + height: 250, + ttl: 120, + creativeId: '2', + netRevenue: true, + currency: 'USD', + dealId: '1', + meta: { + advertiserDomains: ['google.com'], + advertiserId: 1234 + } + }] + }; + let videoResponses = spec.interpretResponse(video); + expect(videoResponses).to.be.an('array').that.is.not.empty; + + let dataItem = videoResponses[0]; + expect(dataItem).to.have.all.keys('requestId', 'cpm', 'vastUrl', 'ttl', 'creativeId', + 'netRevenue', 'currency', 'dealId', 'mediaType', 'meta', 'width', 'height'); + expect(dataItem.requestId).to.equal('23fhj33i987f'); + expect(dataItem.cpm).to.equal(0.5); + expect(dataItem.vastUrl).to.equal('test.com'); + expect(dataItem.ttl).to.equal(120); + expect(dataItem.creativeId).to.equal('2'); + expect(dataItem.netRevenue).to.be.true; + expect(dataItem.currency).to.equal('USD'); + expect(dataItem.meta).to.be.an('object').that.has.property('advertiserDomains'); + }); + it('Should interpret native response', function () { + const native = { + body: [{ + mediaType: 'native', + native: { + clickUrl: 'test.com', + title: 'Test', + image: 'test.com', + impressionTrackers: ['test.com'], + }, + ttl: 120, + cpm: 0.4, + requestId: '23fhj33i987f', + creativeId: '2', + netRevenue: true, + currency: 'USD', + meta: { + advertiserDomains: ['google.com'], + advertiserId: 1234 + } + }] + }; + let nativeResponses = spec.interpretResponse(native); + expect(nativeResponses).to.be.an('array').that.is.not.empty; + + let dataItem = nativeResponses[0]; + expect(dataItem).to.have.keys('requestId', 'cpm', 'ttl', 'creativeId', 'netRevenue', 'currency', 'mediaType', 'native', 'meta'); + expect(dataItem.native).to.have.keys('clickUrl', 'impressionTrackers', 'title', 'image') + expect(dataItem.requestId).to.equal('23fhj33i987f'); + expect(dataItem.cpm).to.equal(0.4); + expect(dataItem.native.clickUrl).to.equal('test.com'); + expect(dataItem.native.title).to.equal('Test'); + expect(dataItem.native.image).to.equal('test.com'); + expect(dataItem.native.impressionTrackers).to.be.an('array').that.is.not.empty; + expect(dataItem.native.impressionTrackers[0]).to.equal('test.com'); + expect(dataItem.ttl).to.equal(120); + expect(dataItem.creativeId).to.equal('2'); + expect(dataItem.netRevenue).to.be.true; + expect(dataItem.currency).to.equal('USD'); + expect(dataItem.meta).to.be.an('object').that.has.property('advertiserDomains'); + }); + it('Should return an empty array if invalid banner response is passed', function () { + const invBanner = { + body: [{ + width: 300, + cpm: 0.4, + ad: 'Test', + requestId: '23fhj33i987f', + ttl: 120, + creativeId: '2', + netRevenue: true, + currency: 'USD', + dealId: '1' + }] + }; + + let serverResponses = spec.interpretResponse(invBanner); + expect(serverResponses).to.be.an('array').that.is.empty; + }); + it('Should return an empty array if invalid video response is passed', function () { + const invVideo = { + body: [{ + mediaType: 'video', + cpm: 0.5, + requestId: '23fhj33i987f', + ttl: 120, + creativeId: '2', + netRevenue: true, + currency: 'USD', + dealId: '1' + }] + }; + let serverResponses = spec.interpretResponse(invVideo); + expect(serverResponses).to.be.an('array').that.is.empty; + }); + it('Should return an empty array if invalid native response is passed', function () { + const invNative = { + body: [{ + mediaType: 'native', + clickUrl: 'test.com', + title: 'Test', + impressionTrackers: ['test.com'], + ttl: 120, + requestId: '23fhj33i987f', + creativeId: '2', + netRevenue: true, + currency: 'USD', + }] + }; + let serverResponses = spec.interpretResponse(invNative); + expect(serverResponses).to.be.an('array').that.is.empty; + }); + it('Should return an empty array if invalid response is passed', function () { + const invalid = { + body: [{ + ttl: 120, + creativeId: '2', + netRevenue: true, + currency: 'USD', + dealId: '1' + }] + }; + let serverResponses = spec.interpretResponse(invalid); + expect(serverResponses).to.be.an('array').that.is.empty; + }); + }); + + describe('getUserSyncs', function() { + it('Should return array of objects with proper sync config , include GDPR', function() { + const syncData = spec.getUserSyncs({}, {}, { + consentString: 'ALL', + gdprApplies: true, + }, {}); + expect(syncData).to.be.an('array').which.is.not.empty; + expect(syncData[0]).to.be.an('object') + expect(syncData[0].type).to.be.a('string') + expect(syncData[0].type).to.equal('image') + expect(syncData[0].url).to.be.a('string') + expect(syncData[0].url).to.equal('https://cs.axis-marketplace.com/image?pbjs=1&gdpr=1&gdpr_consent=ALL&coppa=0') + }); + it('Should return array of objects with proper sync config , include CCPA', function() { + const syncData = spec.getUserSyncs({}, {}, {}, { + consentString: '1---' + }); + expect(syncData).to.be.an('array').which.is.not.empty; + expect(syncData[0]).to.be.an('object') + expect(syncData[0].type).to.be.a('string') + expect(syncData[0].type).to.equal('image') + expect(syncData[0].url).to.be.a('string') + expect(syncData[0].url).to.equal('https://cs.axis-marketplace.com/image?pbjs=1&ccpa=1---&coppa=0') + }); + }); +}); diff --git a/test/spec/modules/beachfrontBidAdapter_spec.js b/test/spec/modules/beachfrontBidAdapter_spec.js index 4e30b822a61..c0994985aae 100644 --- a/test/spec/modules/beachfrontBidAdapter_spec.js +++ b/test/spec/modules/beachfrontBidAdapter_spec.js @@ -1,7 +1,6 @@ import { expect } from 'chai'; import { spec, VIDEO_ENDPOINT, BANNER_ENDPOINT, OUTSTREAM_SRC, DEFAULT_MIMES } from 'modules/beachfrontBidAdapter.js'; -import { config } from 'src/config.js'; -import { parseUrl, deepAccess } from 'src/utils.js'; +import { parseUrl } from 'src/utils.js'; describe('BeachfrontAdapter', function () { let bidRequests; @@ -296,6 +295,23 @@ describe('BeachfrontAdapter', function () { expect(data.user.ext.consent).to.equal(consentString); }); + it('must add GPP consent data to the request', function () { + const bidRequest = bidRequests[0]; + bidRequest.mediaTypes = { video: {} }; + const gppString = 'BOJ8RZsOJ8RZsABAB8AAAAAZ+A=='; + const applicableSections = [1, 2, 3]; + const bidderRequest = { + gppConsent: { + gppString, + applicableSections + } + }; + const requests = spec.buildRequests([ bidRequest ], bidderRequest); + const data = requests[0].data; + expect(data.regs.gpp).to.equal(gppString); + expect(data.regs.gpp_sid).to.deep.equal(applicableSections); + }); + it('must add schain data to the request', () => { const schain = { ver: '1.0', @@ -517,6 +533,23 @@ describe('BeachfrontAdapter', function () { expect(data.gdprConsent).to.equal(consentString); }); + it('must add GPP consent data to the request', function () { + const bidRequest = bidRequests[0]; + bidRequest.mediaTypes = { banner: {} }; + const gppString = 'BOJ8RZsOJ8RZsABAB8AAAAAZ+A=='; + const applicableSections = [1, 2, 3]; + const bidderRequest = { + gppConsent: { + gppString, + applicableSections + } + }; + const requests = spec.buildRequests([ bidRequest ], bidderRequest); + const data = requests[0].data; + expect(data.gpp).to.equal(gppString); + expect(data.gppSid).to.deep.equal(applicableSections); + }); + it('must add schain data to the request', () => { const schain = { ver: '1.0', diff --git a/test/spec/modules/beopBidAdapter_spec.js b/test/spec/modules/beopBidAdapter_spec.js index 22c48c2d182..c77e304e539 100644 --- a/test/spec/modules/beopBidAdapter_spec.js +++ b/test/spec/modules/beopBidAdapter_spec.js @@ -130,15 +130,15 @@ describe('BeOp Bid Adapter tests', () => { expect(payload.url).to.exist; // check that the protocol is added correctly expect(payload.url).to.equal('http://test.te'); - expect(payload.psegs).to.not.exist; }); - it('should call the endpoint with psegs data if any', function () { + it('should call the endpoint with psegs and bpsegs (stringified) data if any or [] if none', function () { let bidderRequest = { 'ortb2': { 'user': { 'ext': { + 'bpsegs': ['axed', 'axec', 1234], 'data': { 'permutive': [1234, 5678, 910] } @@ -154,6 +154,22 @@ describe('BeOp Bid Adapter tests', () => { expect(payload.psegs).to.include(5678); expect(payload.psegs).to.include(910); expect(payload.psegs).to.not.include(1); + expect(payload.bpsegs).to.exist; + expect(payload.bpsegs).to.include('axed'); + expect(payload.bpsegs).to.include('axec'); + expect(payload.bpsegs).to.include('1234'); + + let bidderRequest2 = + { + 'ortb2': {} + }; + + const request2 = spec.buildRequests(bidRequests, bidderRequest2); + const payload2 = JSON.parse(request2.data); + expect(payload2.psegs).to.exist; + expect(payload2.psegs).to.be.empty; + expect(payload2.bpsegs).to.exist; + expect(payload2.bpsegs).to.be.empty; }); it('should not prepend the protocol in page url if already present', function () { diff --git a/test/spec/modules/bidViewability_spec.js b/test/spec/modules/bidViewability_spec.js index a822d86f852..2d2e51abbe1 100644 --- a/test/spec/modules/bidViewability_spec.js +++ b/test/spec/modules/bidViewability_spec.js @@ -245,18 +245,31 @@ describe('#bidViewability', function() { let logWinningBidNotFoundSpy; let callBidViewableBidderSpy; let winningBidsArray; + let callBidBillableBidderSpy; + let adUnits = [ + { + 'code': 'abc123', + 'bids': [ + { + 'bidder': 'pubmatic' + } + ] + } + ]; beforeEach(function() { sandbox = sinon.sandbox.create(); triggerPixelSpy = sandbox.spy(utils, ['triggerPixel']); eventsEmitSpy = sandbox.spy(events, ['emit']); callBidViewableBidderSpy = sandbox.spy(adapterManager, ['callBidViewableBidder']); + callBidBillableBidderSpy = sandbox.spy(adapterManager, ['callBidBillableBidder']); // mocking winningBidsArray winningBidsArray = []; sandbox.stub(prebidGlobal, 'getGlobal').returns({ getAllWinningBids: function (number) { return winningBidsArray; - } + }, + adUnits }); }); @@ -293,5 +306,23 @@ describe('#bidViewability', function() { // CONSTANTS.EVENTS.BID_VIEWABLE is NOT triggered expect(eventsEmitSpy.callCount).to.equal(0); }); + + it('should call the callBidBillableBidder function if the viewable bid is associated with an ad unit with deferBilling set to true', function() { + let moduleConfig = {}; + const deferredBillingAdUnit = { + 'code': '/harshad/Jan/2021/', + 'deferBilling': true, + 'bids': [ + { + 'bidder': 'pubmatic' + } + ] + }; + adUnits.push(deferredBillingAdUnit); + winningBidsArray.push(PBJS_WINNING_BID); + bidViewability.impressionViewableHandler(moduleConfig, GPT_SLOT, null); + expect(callBidBillableBidderSpy.callCount).to.equal(1); + sinon.assert.calledWith(callBidBillableBidderSpy, PBJS_WINNING_BID); + }); }); }); diff --git a/test/spec/modules/bizzclickBidAdapter_spec.js b/test/spec/modules/bizzclickBidAdapter_spec.js index f80051b0a50..f8e66caf657 100644 --- a/test/spec/modules/bizzclickBidAdapter_spec.js +++ b/test/spec/modules/bizzclickBidAdapter_spec.js @@ -1,6 +1,102 @@ import { expect } from 'chai'; -import { spec } from 'modules/bizzclickBidAdapter.js'; -import {config} from 'src/config.js'; +import { spec } from 'modules/bizzclickBidAdapter'; +import 'modules/priceFloors.js'; +import { newBidder } from 'src/adapters/bidderFactory'; +import { config } from '../../../src/config.js'; +import { syncAddFPDToBidderRequest } from '../../helpers/fpd.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'; + +const SIMPLE_BID_REQUEST = { + bidder: 'bizzclick', + params: { + accountId: 'testAccountId', + sourceId: 'testSourceId', + host: 'USE', + }, + mediaTypes: { + banner: { + sizes: [ + [320, 250], + [300, 600], + ], + }, + }, + adUnitCode: 'div-gpt-ad-1499748733608-0', + transactionId: 'f183e871-fbed-45f0-a427-c8a63c4c01eb', + bidId: '33e9500b21129f', + bidderRequestId: '2772c1e566670b', + auctionId: '192721e36a0239', + sizes: [[300, 250], [160, 600]], + gdprConsent: { + apiVersion: 2, + consentString: 'CONSENT', + vendorData: { purpose: { consents: { 1: true } } }, + gdprApplies: true, + addtlConsent: '1~1.35.41.101', + }, +} + +const BANNER_BID_REQUEST = { + bidder: 'bizzclick', + params: { + accountId: 'testAccountId', + sourceId: 'testSourceId', + host: 'USE', + }, + mediaTypes: { + banner: { + sizes: [ + [300, 250], + [300, 600], + ], + }, + }, + adUnitCode: '/adunit-code/test-path', + bidId: 'test-bid-id-1', + bidderRequestId: 'test-bid-request-1', + auctionId: 'test-auction-1', + transactionId: 'test-transactionId-1', + code: 'banner_example', + timeout: 1000, +} + +const VIDEO_BID_REQUEST = { + placementCode: '/DfpAccount1/slotVideo', + bidId: 'test-bid-id-2', + mediaTypes: { + video: { + playerSize: [400, 300], + w: 400, + h: 300, + minduration: 5, + maxduration: 10, + startdelay: 0, + skip: 1, + minbitrate: 200, + protocols: [1, 2, 4] + } + }, + bidder: 'bizzclick', + params: { + accountId: '123', + sourceId: '123', + host: 'USE', + }, + adUnitCode: '/adunit-code/test-path', + bidderRequestId: 'test-bid-request-1', + auctionId: 'test-auction-1', + transactionId: 'test-transactionId-1', + timeout: 1000, +} const NATIVE_BID_REQUEST = { code: 'native_example', @@ -34,386 +130,179 @@ const NATIVE_BID_REQUEST = { }, bidder: 'bizzclick', params: { - placementId: 'hash', - accountId: 'accountId' - }, - timeout: 1000 - -}; - -const BANNER_BID_REQUEST = { - code: 'banner_example', - mediaTypes: { - banner: { - sizes: [[300, 250], [300, 600]] - } - }, - schain: { - ver: '1.0', - complete: 1, - nodes: [ - { - asi: 'example.com', - sid: '164', - hp: 1 - } - ] - }, - bidder: 'bizzclick', - params: { - placementId: 'hash', - accountId: 'accountId' + accountId: 'testAccountId', + sourceId: 'testSourceId', + host: 'USE', }, + adUnitCode: '/adunit-code/test-path', + bidId: 'test-bid-id-1', + bidderRequestId: 'test-bid-request-1', + auctionId: 'test-auction-1', + transactionId: 'test-transactionId-1', timeout: 1000, - gdprConsent: { - consentString: 'BOEFEAyOEFEAyAHABDENAI4AAAB9vABAASA', - gdprApplies: 1, - }, uspConsent: 'uspConsent' -} +}; -const bidRequest = { +const bidderRequest = { refererInfo: { - referer: 'test.com' - } -} - -const VIDEO_BID_REQUEST = { - code: 'video1', - sizes: [640, 480], - mediaTypes: { video: { - minduration: 0, - maxduration: 999, - boxingallowed: 1, - skip: 0, - mimes: [ - 'application/javascript', - 'video/mp4' - ], - w: 1920, - h: 1080, - protocols: [ - 2 - ], - linearity: 1, - api: [ - 1, - 2 - ] + page: 'https://publisher.com/home', + ref: 'https://referrer' } - }, - - bidder: 'bizzclick', - params: { - placementId: 'hash', - accountId: 'accountId' - }, - timeout: 1000 - -} - -const BANNER_BID_RESPONSE = { - id: 'request_id', - bidid: 'request_imp_id', - seatbid: [{ - bid: [{ - id: 'bid_id', - impid: 'request_imp_id', - price: 5, - adomain: ['example.com'], - adm: 'admcode', - crid: 'crid', - ext: { - mediaType: 'banner' - } - }], - }], -}; - -const VIDEO_BID_RESPONSE = { - id: 'request_id', - bidid: 'request_imp_id', - seatbid: [{ - bid: [{ - id: 'bid_id', - impid: 'request_imp_id', - price: 5, - adomain: ['example.com'], - adm: 'admcode', - crid: 'crid', - ext: { - mediaType: 'video', - vastUrl: 'http://example.vast', - } - }], - }], }; -let imgData = { - url: `https://example.com/image`, - w: 1200, - h: 627 -}; +const gdprConsent = { + apiVersion: 2, + consentString: 'CONSENT', + vendorData: { purpose: { consents: { 1: true } } }, + gdprApplies: true, + addtlConsent: '1~1.35.41.101', +} -const NATIVE_BID_RESPONSE = { - id: 'request_id', - bidid: 'request_imp_id', - seatbid: [{ - bid: [{ - id: 'bid_id', - impid: 'request_imp_id', - price: 5, - adomain: ['example.com'], - adm: { native: - { - assets: [ - {id: 0, title: 'dummyText'}, - {id: 3, image: imgData}, - { - id: 5, - data: {value: 'organization.name'} - } - ], - link: {url: 'example.com'}, - imptrackers: ['tracker1.com', 'tracker2.com', 'tracker3.com'], - jstracker: 'tracker1.com' - } - }, - crid: 'crid', - ext: { - mediaType: 'native' - } - }], - }], -}; +describe('bizzclickAdapter', function () { + 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('BizzclickAdapter', function() { - describe('with COPPA', function() { - beforeEach(function() { + describe('with user privacy regulations', function () { + it('should send the Coppa "required" flag set to "1" in the request', function () { sinon.stub(config, 'getConfig') .withArgs('coppa') .returns(true); - }); - afterEach(function() { + const serverRequest = spec.buildRequests([SIMPLE_BID_REQUEST], syncAddFPDToBidderRequest(bidderRequest)); + expect(serverRequest.data.regs.coppa).to.equal(1); config.getConfig.restore(); }); - it('should send the Coppa "required" flag set to "1" in the request', function () { - let serverRequest = spec.buildRequests([BANNER_BID_REQUEST]); - expect(serverRequest.data[0].regs.coppa).to.equal(1); + it('should send the GDPR Consent data in the request', function () { + const serverRequest = spec.buildRequests([SIMPLE_BID_REQUEST], syncAddFPDToBidderRequest({ ...bidderRequest, gdprConsent })); + expect(serverRequest.data.regs.ext.gdpr).to.exist.and.to.equal(1); + expect(serverRequest.data.user.ext.consent).to.equal('CONSENT'); }); - }); - describe('isBidRequestValid', function() { - it('should return true when required params found', function () { - expect(spec.isBidRequestValid(NATIVE_BID_REQUEST)).to.equal(true); - }); - - it('should return false when required params are not passed', function () { - let bid = Object.assign({}, NATIVE_BID_REQUEST); - delete bid.params; - bid.params = { - 'IncorrectParam': 0 - }; - expect(spec.isBidRequestValid(bid)).to.equal(false); + it('should send the CCPA data in the request', function () { + const serverRequest = spec.buildRequests([SIMPLE_BID_REQUEST], syncAddFPDToBidderRequest({...bidderRequest, ...{ uspConsent: '1YYY' }})); + expect(serverRequest.data.regs.ext.us_privacy).to.equal('1YYY'); }); }); - describe('build Native Request', function () { - const request = spec.buildRequests([NATIVE_BID_REQUEST], bidRequest); - - it('Creates a ServerRequest object with method, URL and data', function () { - expect(request).to.exist; - expect(request.method).to.exist; - expect(request.url).to.exist; - expect(request.data).to.exist; - }); - - it('sends bid request to our endpoint via POST', function () { - expect(request.method).to.equal('POST'); - }); - - it('Returns valid URL', function () { - expect(request.url).to.equal('https://us-e-node1.bizzclick.com/bid?rtb_seat_id=prebidjs&secret_key=accountId'); + describe('isBidRequestValid', function () { + it('should return true when required params found', function () { + expect(spec.isBidRequestValid(BANNER_BID_REQUEST)).to.equal(true); }); - it('Returns empty data if no valid requests are passed', function () { - let serverRequest = spec.buildRequests([]); - expect(serverRequest).to.be.an('array').that.is.empty; + it('should return false when accountID/sourceId is missing', function () { + let localbid = Object.assign({}, BANNER_BID_REQUEST); + delete localbid.params.accountId; + delete localbid.params.sourceId; + expect(spec.isBidRequestValid(BANNER_BID_REQUEST)).to.equal(false); }); }); - describe('build Banner Request', function () { - const request = spec.buildRequests([BANNER_BID_REQUEST]); - - it('Creates a ServerRequest object with method, URL and data', function () { - expect(request).to.exist; - expect(request.method).to.exist; - expect(request.url).to.exist; - expect(request.data).to.exist; + describe('build request', function () { + it('should return an empty array when no bid requests', function () { + const bidRequest = spec.buildRequests([], syncAddFPDToBidderRequest(bidderRequest)); + expect(bidRequest).to.be.an('array'); + expect(bidRequest.length).to.equal(0); }); - it('sends bid request to our endpoint via POST', function () { + it('should return a valid bid request object', function () { + const request = spec.buildRequests([SIMPLE_BID_REQUEST], syncAddFPDToBidderRequest(bidderRequest)); + expect(request).to.not.equal('array'); + expect(request.data).to.be.an('object'); expect(request.method).to.equal('POST'); + expect(request.url).to.not.equal(''); + expect(request.url).to.not.equal(undefined); + expect(request.url).to.not.equal(null); + + expect(request.data.site).to.have.property('page'); + expect(request.data.site).to.have.property('domain'); + expect(request.data).to.have.property('id'); + expect(request.data).to.have.property('imp'); + expect(request.data).to.have.property('device'); }); - it('check consent and ccpa string is set properly', function() { - expect(request.data[0].regs.ext.gdpr).to.equal(1); - expect(request.data[0].user.ext.consent).to.equal(BANNER_BID_REQUEST.gdprConsent.consentString); - expect(request.data[0].regs.ext.us_privacy).to.equal(BANNER_BID_REQUEST.uspConsent); - }) - - it('check schain is set properly', function() { - expect(request.data[0].source.ext.schain.complete).to.equal(1); - expect(request.data[0].source.ext.schain.ver).to.equal('1.0'); - }) - - it('Returns valid URL', function () { - expect(request.url).to.equal('https://us-e-node1.bizzclick.com/bid?rtb_seat_id=prebidjs&secret_key=accountId'); + it('should return a valid bid BANNER request object', function () { + const request = spec.buildRequests([BANNER_BID_REQUEST], syncAddFPDToBidderRequest(bidderRequest)); + expect(request.data.imp[0].banner).to.exist; + expect(request.data.imp[0].banner.format[0].w).to.be.an('number'); + expect(request.data.imp[0].banner.format[0].h).to.be.an('number'); }); - }); - describe('build Video Request', function () { - const request = spec.buildRequests([VIDEO_BID_REQUEST]); - - it('Creates a ServerRequest object with method, URL and data', function () { - expect(request).to.exist; - expect(request.method).to.exist; - expect(request.url).to.exist; - expect(request.data).to.exist; - }); + if (FEATURES.VIDEO) { + it('should return a valid bid VIDEO request object', function () { + const request = spec.buildRequests([VIDEO_BID_REQUEST], syncAddFPDToBidderRequest(bidderRequest)); + expect(request.data.imp[0].video).to.exist; + expect(request.data.imp[0].video.w).to.be.an('number'); + expect(request.data.imp[0].video.h).to.be.an('number'); + }); + } - it('sends bid request to our endpoint via POST', function () { - expect(request.method).to.equal('POST'); + it('should return a valid bid NATIVE request object', function () { + const request = spec.buildRequests([NATIVE_BID_REQUEST], syncAddFPDToBidderRequest(bidderRequest)); + expect(request.data.imp[0]).to.be.an('object'); }); + }) - it('Returns valid URL', function () { - expect(request.url).to.equal('https://us-e-node1.bizzclick.com/bid?rtb_seat_id=prebidjs&secret_key=accountId'); + describe('interpretResponse', function () { + let bidRequests, bidderRequest; + beforeEach(function () { + bidRequests = [{ + 'bidId': '28ffdk2B952532', + 'bidder': 'bizzclick', + 'userId': { + 'freepassId': { + 'userIp': '172.21.0.1', + 'userId': '123', + 'commonId': 'commonIdValue' + } + }, + 'adUnitCode': 'adunit-code', + 'params': { + 'publisherId': 'publisherIdValue' + } + }]; + bidderRequest = {}; }); - }); - describe('interpretResponse', function () { - it('Empty response must return empty array', function() { + it('Empty response must return empty array', function () { const emptyResponse = null; - let response = spec.interpretResponse(emptyResponse); + let response = spec.interpretResponse(emptyResponse, BANNER_BID_REQUEST); expect(response).to.be.an('array').that.is.empty; }) it('Should interpret banner response', function () { - const bannerResponse = { - body: [BANNER_BID_RESPONSE] - } - - const expectedBidResponse = { - requestId: BANNER_BID_RESPONSE.id, - cpm: BANNER_BID_RESPONSE.seatbid[0].bid[0].price, - width: BANNER_BID_RESPONSE.seatbid[0].bid[0].w, - height: BANNER_BID_RESPONSE.seatbid[0].bid[0].h, - ttl: BANNER_BID_RESPONSE.ttl || 1200, - currency: BANNER_BID_RESPONSE.cur || 'USD', - netRevenue: true, - creativeId: BANNER_BID_RESPONSE.seatbid[0].bid[0].crid, - dealId: BANNER_BID_RESPONSE.seatbid[0].bid[0].dealid, - - meta: {advertiserDomains: BANNER_BID_RESPONSE.seatbid[0].bid[0].adomain}, - mediaType: 'banner', - ad: BANNER_BID_RESPONSE.seatbid[0].bid[0].adm - } - - let bannerResponses = spec.interpretResponse(bannerResponse); - - expect(bannerResponses).to.be.an('array').that.is.not.empty; - let dataItem = bannerResponses[0]; - expect(dataItem).to.have.all.keys('requestId', 'cpm', 'width', 'height', 'ad', 'ttl', 'creativeId', - 'netRevenue', 'currency', 'dealId', 'meta', 'mediaType'); - expect(dataItem.requestId).to.equal(expectedBidResponse.requestId); - expect(dataItem.cpm).to.equal(expectedBidResponse.cpm); - expect(dataItem.ad).to.equal(expectedBidResponse.ad); - expect(dataItem.ttl).to.equal(expectedBidResponse.ttl); - expect(dataItem.meta.advertiserDomains).to.equal(expectedBidResponse.meta.advertiserDomains); - expect(dataItem.creativeId).to.equal(expectedBidResponse.creativeId); - expect(dataItem.netRevenue).to.be.true; - expect(dataItem.currency).to.equal(expectedBidResponse.currency); - expect(dataItem.width).to.equal(expectedBidResponse.width); - expect(dataItem.height).to.equal(expectedBidResponse.height); - }); - - it('Should interpret video response', function () { - const videoResponse = { - body: [VIDEO_BID_RESPONSE] - } - - const expectedBidResponse = { - requestId: VIDEO_BID_RESPONSE.id, - cpm: VIDEO_BID_RESPONSE.seatbid[0].bid[0].price, - width: VIDEO_BID_RESPONSE.seatbid[0].bid[0].w, - height: VIDEO_BID_RESPONSE.seatbid[0].bid[0].h, - ttl: VIDEO_BID_RESPONSE.ttl || 1200, - currency: VIDEO_BID_RESPONSE.cur || 'USD', - netRevenue: true, - creativeId: VIDEO_BID_RESPONSE.seatbid[0].bid[0].crid, - dealId: VIDEO_BID_RESPONSE.seatbid[0].bid[0].dealid, - mediaType: 'video', - vastXml: VIDEO_BID_RESPONSE.seatbid[0].bid[0].adm, - meta: {advertiserDomains: VIDEO_BID_RESPONSE.seatbid[0].bid[0].adomain}, - vastUrl: VIDEO_BID_RESPONSE.seatbid[0].bid[0].ext.vastUrl - } - - let videoResponses = spec.interpretResponse(videoResponse); - - expect(videoResponses).to.be.an('array').that.is.not.empty; - let dataItem = videoResponses[0]; - expect(dataItem).to.have.all.keys('requestId', 'cpm', 'width', 'height', 'vastXml', 'vastUrl', 'ttl', 'creativeId', - 'netRevenue', 'currency', 'dealId', 'meta', 'mediaType'); - expect(dataItem.requestId).to.equal(expectedBidResponse.requestId); - expect(dataItem.cpm).to.equal(expectedBidResponse.cpm); - expect(dataItem.vastXml).to.equal(expectedBidResponse.vastXml) - expect(dataItem.ttl).to.equal(expectedBidResponse.ttl); - expect(dataItem.creativeId).to.equal(expectedBidResponse.creativeId); - expect(dataItem.meta.advertiserDomains).to.equal(expectedBidResponse.meta.advertiserDomains); - expect(dataItem.netRevenue).to.be.true; - expect(dataItem.currency).to.equal(expectedBidResponse.currency); - expect(dataItem.width).to.equal(expectedBidResponse.width); - expect(dataItem.height).to.equal(expectedBidResponse.height); - }); - - it('Should interpret native response', function () { - const nativeResponse = { - body: [NATIVE_BID_RESPONSE] - } - - const expectedBidResponse = { - requestId: NATIVE_BID_RESPONSE.id, - cpm: NATIVE_BID_RESPONSE.seatbid[0].bid[0].price, - width: NATIVE_BID_RESPONSE.seatbid[0].bid[0].w, - height: NATIVE_BID_RESPONSE.seatbid[0].bid[0].h, - ttl: NATIVE_BID_RESPONSE.ttl || 1200, - currency: NATIVE_BID_RESPONSE.cur || 'USD', - netRevenue: true, - creativeId: NATIVE_BID_RESPONSE.seatbid[0].bid[0].crid, - dealId: NATIVE_BID_RESPONSE.seatbid[0].bid[0].dealid, - mediaType: 'native', - meta: {advertiserDomains: NATIVE_BID_RESPONSE.seatbid[0].bid[0].adomain}, - native: {clickUrl: NATIVE_BID_RESPONSE.seatbid[0].bid[0].adm.native.link.url} - } - - let nativeResponses = spec.interpretResponse(nativeResponse); - - expect(nativeResponses).to.be.an('array').that.is.not.empty; - let dataItem = nativeResponses[0]; - expect(dataItem).to.have.all.keys('requestId', 'cpm', 'width', 'height', 'native', 'ttl', 'creativeId', - 'netRevenue', 'currency', 'dealId', 'mediaType', 'meta'); - expect(dataItem.requestId).to.equal(expectedBidResponse.requestId); - expect(dataItem.cpm).to.equal(expectedBidResponse.cpm); - expect(dataItem.meta.advertiserDomains).to.equal(expectedBidResponse.meta.advertiserDomains); - expect(dataItem.native.clickUrl).to.equal(expectedBidResponse.native.clickUrl) - expect(dataItem.ttl).to.equal(expectedBidResponse.ttl); - expect(dataItem.creativeId).to.equal(expectedBidResponse.creativeId); - expect(dataItem.netRevenue).to.be.true; - expect(dataItem.currency).to.equal(expectedBidResponse.currency); - expect(dataItem.width).to.equal(expectedBidResponse.width); - expect(dataItem.height).to.equal(expectedBidResponse.height); - }); + const serverResponse = { + body: { + 'cur': 'USD', + 'seatbid': [{ + 'bid': [{ + 'impid': '28ffdk2B952532', + 'price': 97, + 'adm': '', + 'w': 300, + 'h': 250, + 'crid': 'creative0' + }] + }] + } + }; + it('should interpret server response', function () { + const bidRequest = spec.buildRequests(bidRequests, syncAddFPDToBidderRequest(bidderRequest)); + const bids = spec.interpretResponse(serverResponse, bidRequest); + expect(bids).to.be.an('array'); + const bid = bids[0]; + expect(bid).to.be.an('object'); + expect(bid.currency).to.equal('USD'); + expect(bid.cpm).to.equal(97); + expect(bid.ad).to.equal(ad) + expect(bid.width).to.equal(300); + expect(bid.height).to.equal(250); + expect(bid.creativeId).to.equal('creative0'); + }); + }) }); -}) +}); diff --git a/test/spec/modules/bliinkBidAdapter_spec.js b/test/spec/modules/bliinkBidAdapter_spec.js index 8e96bd76940..3db97a17d88 100644 --- a/test/spec/modules/bliinkBidAdapter_spec.js +++ b/test/spec/modules/bliinkBidAdapter_spec.js @@ -1,5 +1,16 @@ -import { expect } from 'chai' -import { spec, buildBid, BLIINK_ENDPOINT_ENGINE, getMetaList, BLIINK_ENDPOINT_COOKIE_SYNC_IFRAME } from 'modules/bliinkBidAdapter.js' +import { expect } from 'chai'; +import { + spec, + buildBid, + BLIINK_ENDPOINT_ENGINE, + getMetaList, + BLIINK_ENDPOINT_COOKIE_SYNC_IFRAME, + getEffectiveConnectionType, + getUserIds, + getDomLoadingDuration, + GVL_ID, +} from 'modules/bliinkBidAdapter.js'; +import { config } from 'src/config.js'; /** * @description Mockup bidRequest @@ -20,6 +31,9 @@ import { spec, buildBid, BLIINK_ENDPOINT_ENGINE, getMetaList, BLIINK_ENDPOINT_CO * crumbs: {pubcid: string}, * ortb2Imp: {ext: {data: {pbadslot: string}}}}} */ + +const connectionType = getEffectiveConnectionType(); +const domLoadingDuration = getDomLoadingDuration().toString(); const getConfigBid = (placement) => { return { adUnitCode: '/19968336/test', @@ -31,31 +45,33 @@ const getConfigBid = (placement) => { bidderRequestsCount: 1, bidderWinsCount: 0, crumbs: { - pubcid: '55ffadc5-051f-428d-8ecc-dc585e0bde0d' + pubcid: '55ffadc5-051f-428d-8ecc-dc585e0bde0d', }, sizes: [[300, 250]], mediaTypes: { banner: { - sizes: [ - [300, 250] - ] - } + sizes: [[300, 250]], + }, }, ortb2Imp: { ext: { data: { - pbadslot: '/19968336/test' - } - } + pbadslot: '/19968336/test', + }, + }, }, + domLoadingDuration, + ect: connectionType, params: { placement: placement, - tagId: '14f30eca-85d2-11e8-9eed-0242ac120007' + tagId: '14f30eca-85d2-11e8-9eed-0242ac120007', + videoUrl: 'https://www.example.com/advideo.mp4', + imageUrl: 'https://www.example.com/adimage.jpg', }, src: 'client', - transactionId: 'cc6678c4-9746-4082-b9e2-d8065d078ebf' - } -} + transactionId: 'cc6678c4-9746-4082-b9e2-d8065d078ebf', + }; +}; const getConfigBannerBid = () => { return { creative: { @@ -76,14 +92,13 @@ const getConfigBannerBid = () => { transaction_id: '2def0c5b2a7f6e', }, currency: 'EUR', - } -} + }; +}; const getConfigVideoBid = () => { return { creative: { video: { - content: - '', + content: '', height: 250, width: 300, }, @@ -99,8 +114,8 @@ const getConfigVideoBid = () => { transaction_id: '2def0c5b2a7f6e', }, currency: 'EUR', - } -} + }; +}; /** * @description Mockup response from engine.bliink.io/xxxx @@ -119,7 +134,7 @@ const getConfigVideoBid = () => { * } * } * } -* } + * } */ const getConfigCreative = () => { return { @@ -132,8 +147,8 @@ const getConfigCreative = () => { height: 250, ttl: 300, netRevenue: true, - } -} + }; +}; const getConfigCreativeVideo = (isNoVast) => { return { @@ -147,8 +162,8 @@ const getConfigCreativeVideo = (isNoVast) => { height: 250, ttl: 300, netRevenue: true, - } -} + }; +}; /** * @description Mockup BuildRequest function @@ -166,19 +181,19 @@ const getConfigBuildRequest = (placement) => { reachedTop: true, page: 'http://localhost:9999/integrationExamples/gpt/bliink-adapter.html?pbjs_debug=true', }, - } + }; if (!placement) { - return buildRequest + return buildRequest; } return Object.assign(buildRequest, { params: { bids: [getConfigBid(placement)], - placement: placement + placement: placement, }, - }) -} + }); +}; /** * @description Mockup response from API @@ -189,8 +204,8 @@ const getConfigInterpretResponse = (noAd = false) => { if (noAd) { return { message: 'invalid tag', - mode: 'no-ad' - } + mode: 'no-ad', + }; } return { @@ -198,11 +213,12 @@ const getConfigInterpretResponse = (noAd = false) => { ...getConfigCreative(), mode: 'ad', transactionId: '2def0c5b2a7f6e', - token: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2MjgxNzA4MzEsImlhdCI6MTYyNzU2NjAzMSwiaXNzIjoiYmxpaW5rIiwiZGF0YSI6eyJ0eXBlIjoiYWQtc2VydmVyIiwidHJhbnNhY3Rpb25JZCI6IjM1YmU1NDNjLTNkZTQtNGQ1Yy04N2NjLWIzYzEyOGZiYzU0MCIsIm5ldHdvcmtJZCI6MjEsInNpdGVJZCI6NTksInRhZ0lkIjo1OSwiY29va2llSWQiOiJjNGU4MWVhOS1jMjhmLTQwZDItODY1ZC1hNjQzZjE1OTcyZjUiLCJldmVudElkIjozLCJ0YXJnZXRpbmciOnsicGxhdGZvcm0iOiJXZWJzaXRlIiwiaXAiOiI3OC4xMjIuNzUuNzIiLCJ0aW1lIjoxNjI3NTY2MDMxLCJsb2NhdGlvbiI6eyJsYXRpdHVkZSI6NDguOTczOSwibG9uZ2l0dWRlIjozLjMxMTMsInJlZ2lvbiI6IkhERiIsImNvdW50cnkiOiJGUiIsImNpdHkiOiJTYXVsY2hlcnkiLCJ6aXBDb2RlIjoiMDIzMTAiLCJkZXBhcnRtZW50IjoiMDIifSwiY2l0eSI6IlNhdWxjaGVyeSIsImNvdW50cnkiOiJGUiIsImRldmljZU9zIjoibWFjT1MiLCJkZXZpY2VQbGF0Zm9ybSI6IldlYnNpdGUiLCJyYXdVc2VyQWdlbnQiOiJNb3ppbGxhLzUuMCAoTWFjaW50b3NoOyBJbnRlbCBNYWMgT1MgWCAxMF8xNV83KSBBcHBsZVdlYktpdC81MzcuMzYgKEtIVE1MLCBsaWtlIEdlY2tvKSBDaHJvbWUvOTEuMC40NDcyLjEyNCBTYWZhcmkvNTM3LjM2In0sImdkcHIiOnsiaGFzQ29uc2VudCI6dHJ1ZX0sIndpbiI6ZmFsc2UsImFkSWQiOjU2NDgsImFkdmVydGlzZXJJZCI6MSwiY2FtcGFpZ25JZCI6MSwiY3JlYXRpdmVJZCI6MjgyNSwiZXJyb3IiOmZhbHNlfX0.-UefQH4G0k-RJGemBYffs-KL7EEwma2Wuwgk2xnpij8' + token: + 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2MjgxNzA4MzEsImlhdCI6MTYyNzU2NjAzMSwiaXNzIjoiYmxpaW5rIiwiZGF0YSI6eyJ0eXBlIjoiYWQtc2VydmVyIiwidHJhbnNhY3Rpb25JZCI6IjM1YmU1NDNjLTNkZTQtNGQ1Yy04N2NjLWIzYzEyOGZiYzU0MCIsIm5ldHdvcmtJZCI6MjEsInNpdGVJZCI6NTksInRhZ0lkIjo1OSwiY29va2llSWQiOiJjNGU4MWVhOS1jMjhmLTQwZDItODY1ZC1hNjQzZjE1OTcyZjUiLCJldmVudElkIjozLCJ0YXJnZXRpbmciOnsicGxhdGZvcm0iOiJXZWJzaXRlIiwiaXAiOiI3OC4xMjIuNzUuNzIiLCJ0aW1lIjoxNjI3NTY2MDMxLCJsb2NhdGlvbiI6eyJsYXRpdHVkZSI6NDguOTczOSwibG9uZ2l0dWRlIjozLjMxMTMsInJlZ2lvbiI6IkhERiIsImNvdW50cnkiOiJGUiIsImNpdHkiOiJTYXVsY2hlcnkiLCJ6aXBDb2RlIjoiMDIzMTAiLCJkZXBhcnRtZW50IjoiMDIifSwiY2l0eSI6IlNhdWxjaGVyeSIsImNvdW50cnkiOiJGUiIsImRldmljZU9zIjoibWFjT1MiLCJkZXZpY2VQbGF0Zm9ybSI6IldlYnNpdGUiLCJyYXdVc2VyQWdlbnQiOiJNb3ppbGxhLzUuMCAoTWFjaW50b3NoOyBJbnRlbCBNYWMgT1MgWCAxMF8xNV83KSBBcHBsZVdlYktpdC81MzcuMzYgKEtIVE1MLCBsaWtlIEdlY2tvKSBDaHJvbWUvOTEuMC40NDcyLjEyNCBTYWZhcmkvNTM3LjM2In0sImdkcHIiOnsiaGFzQ29uc2VudCI6dHJ1ZX0sIndpbiI6ZmFsc2UsImFkSWQiOjU2NDgsImFkdmVydGlzZXJJZCI6MSwiY2FtcGFpZ25JZCI6MSwiY3JlYXRpdmVJZCI6MjgyNSwiZXJyb3IiOmZhbHNlfX0.-UefQH4G0k-RJGemBYffs-KL7EEwma2Wuwgk2xnpij8', }, headers: {}, - } -} + }; +}; /** * @description Mockup response from API for RTB creative @@ -213,8 +229,8 @@ const getConfigInterpretResponseRTB = (noAd = false, isInvalidVast = false) => { if (noAd) { return { message: 'invalid tag', - mode: 'no-ad' - } + mode: 'no-ad', + }; } const validVast = ` @@ -229,41 +245,43 @@ const getConfigInterpretResponseRTB = (noAd = false, isInvalidVast = false) => { - ` + `; const invalidVast = ` - ` + `; return { - body: { bids: [ - { - 'creative': { - 'video': { - 'content': isInvalidVast ? invalidVast : validVast, - 'height': 250, - 'width': 300 + body: { + bids: [ + { + creative: { + video: { + content: isInvalidVast ? invalidVast : validVast, + height: 250, + width: 300, + }, + media_type: 'video', + creativeId: 0, }, - 'media_type': 'video', - 'creativeId': 0, - }, - 'price': 0, - 'id': '8121', - 'token': 'token', - 'mode': 'rtb', - 'extras': { - 'deal_id': '34567ertyaza', - 'transaction_id': '2def0c5b2a7f6e' + price: 0, + id: '8121', + token: 'token', + mode: 'rtb', + extras: { + deal_id: '34567ertyaza', + transaction_id: '2def0c5b2a7f6e', + }, + currency: 'EUR', }, - 'currency': 'EUR' - } - ], - userSyncs: []} - } -} + ], + userSyncs: [], + }, + }; +}; /** * @@ -279,9 +297,9 @@ const testsGetMetaList = [ { title: 'Should return empty array if there are no parameters', args: { - fn: getMetaList() + fn: getMetaList(), }, - want: [] + want: [], }, { title: 'Should return list of metas with name associated', @@ -313,18 +331,63 @@ const testsGetMetaList = [ key: 'property', value: `'article:${'test'}'`, }, - ] - } -] + ], + }, +]; -describe('BLIINK Adapter getMetaList', function() { +describe('BLIINK Adapter getMetaList', function () { for (const test of testsGetMetaList) { it(test.title, () => { - const res = test.args.fn - expect(res).to.eql(test.want) - }) + const res = test.args.fn; + expect(res).to.eql(test.want); + }); } -}) +}); +const GetUserIds = [ + { + title: 'Should return undefined if there are no parameters', + args: { + fn: getUserIds(), + }, + want: undefined, + }, + { + title: 'Should return eids if exists', + args: { + fn: getUserIds([{ userIdAsEids: [ + { + 'source': 'criteo.com', + 'uids': [ + { + 'id': 'testId', + 'atype': 1 + } + ] + } + ] }]), + }, + want: [ + { + 'source': 'criteo.com', + 'uids': [ + { + 'id': 'testId', + 'atype': 1 + } + ] + } + ], + }, +]; + +describe('BLIINK Adapter getUserIds', function () { + for (const test of GetUserIds) { + it(test.title, () => { + const res = test.args.fn; + expect(res).to.eql(test.want); + }); + } +}); /** * @description Array of tests used in describe function below @@ -349,127 +412,142 @@ const testsIsBidRequestValid = [ { title: 'isBidRequestValid format not valid', args: { - fn: spec.isBidRequestValid({}) + fn: spec.isBidRequestValid({}), }, want: false, }, { title: 'isBidRequestValid does not receive any bid', args: { - fn: spec.isBidRequestValid() + fn: spec.isBidRequestValid(), }, want: false, }, { title: 'isBidRequestValid Receive a valid bid', args: { - fn: spec.isBidRequestValid(getConfigBid('banner')) + fn: spec.isBidRequestValid(getConfigBid('banner')), }, want: true, - } -] + }, +]; -describe('BLIINK Adapter isBidRequestValid', function() { +describe('BLIINK Adapter isBidRequestValid', function () { for (const test of testsIsBidRequestValid) { it(test.title, () => { - const res = test.args.fn - expect(res).to.eql(test.want) - }) + const res = test.args.fn; + expect(res).to.eql(test.want); + }); } -}) +}); -const vastXml = getConfigInterpretResponseRTB().body.bids[0].creative.video.content +const vastXml = + getConfigInterpretResponseRTB().body.bids[0].creative.video.content; const testsInterpretResponse = [ { title: 'Should construct bid for video instream', args: { - fn: spec.interpretResponse(getConfigInterpretResponseRTB(false)) + fn: spec.interpretResponse(getConfigInterpretResponseRTB(false)), }, - want: [{ - cpm: 0, - currency: 'EUR', - height: 250, - width: 300, - creativeId: '34567ertyaza', - mediaType: 'video', - netRevenue: true, - requestId: '2def0c5b2a7f6e', - ttl: 300, - vastXml, - vastUrl: 'data:text/xml;charset=utf-8;base64,' + btoa(vastXml.replace(/\\"/g, '"')) - }] + want: [ + { + cpm: 0, + currency: 'EUR', + height: 250, + width: 300, + creativeId: '34567ertyaza', + mediaType: 'video', + netRevenue: true, + requestId: '2def0c5b2a7f6e', + ttl: 300, + vastXml, + vastUrl: + 'data:text/xml;charset=utf-8;base64,' + + btoa(vastXml.replace(/\\"/g, '"')), + }, + ], }, { title: 'ServerResponse with message: invalid tag, return empty array', args: { - fn: spec.interpretResponse(getConfigInterpretResponse(true)) + fn: spec.interpretResponse(getConfigInterpretResponse(true)), }, - want: [] + want: [], }, { title: 'ServerResponse with mediaType banner', args: { - fn: spec.interpretResponse({body: {bids: [getConfigBannerBid()]}}), + fn: spec.interpretResponse({ body: { bids: [getConfigBannerBid()] } }), }, - want: [{ - ad: '', - cpm: 1, - creativeId: '34567erty', - currency: 'EUR', - height: 250, - mediaType: 'banner', - netRevenue: true, - requestId: '2def0c5b2a7f6e', - ttl: 300, - width: 300 - }] + want: [ + { + ad: '', + cpm: 1, + creativeId: '34567erty', + currency: 'EUR', + height: 250, + mediaType: 'banner', + netRevenue: true, + requestId: '2def0c5b2a7f6e', + ttl: 300, + width: 300, + }, + ], }, { title: 'ServerResponse with unhandled mediaType, return empty array', args: { - fn: spec.interpretResponse({body: {bids: [{...getConfigBannerBid(), - creative: { - unknown: { - adm: '', - height: 250, - width: 300, - }, - media_type: 'unknown', - creativeId: 125, - requestId: '2def0c5b2a7f6e', - }}]}}), + fn: spec.interpretResponse({ + body: { + bids: [ + { + ...getConfigBannerBid(), + creative: { + unknown: { + adm: '', + height: 250, + width: 300, + }, + media_type: 'unknown', + creativeId: 125, + requestId: '2def0c5b2a7f6e', + }, + }, + ], + }, + }), }, - want: [] + want: [], }, -] +]; -describe('BLIINK Adapter interpretResponse', function() { +describe('BLIINK Adapter interpretResponse', function () { for (const test of testsInterpretResponse) { it(test.title, () => { - const res = test.args.fn + const res = test.args.fn; if (res) { - expect(res).to.eql(test.want) + expect(res).to.eql(test.want); } - }) + }); } -}) +}); /** * @description Array of tests used in describe function below * @type {[ * {args: * {fn: { - * cpm: number, - * netRevenue: boolean, - * ad, requestId, - * meta: {mediaType}, - * width: number, - * currency: string, - * ttl: number, - * creativeId: number, - * height: number + * cpm: number, + * netRevenue: boolean, + * ad, requestId, + * meta: {mediaType}, + * width: number, + * currency: string, + * ttl: number, + * creativeId: number, + * height: number * } * }, want, title: string}]} */ @@ -478,21 +556,26 @@ const testsBuildBid = [ { title: 'Should return null if no bid passed in parameters', args: { - fn: buildBid() + fn: buildBid(), }, - want: null + want: null, }, { title: 'Input data must respect the output model', args: { - fn: buildBid({ id: 1, test: '123' }, { id: 2, test: '345' }, false, false) + fn: buildBid( + { id: 1, test: '123' }, + { id: 2, test: '345' }, + false, + false + ), }, - want: null + want: null, }, { title: 'input data respect the output model for video', args: { - fn: buildBid(getConfigVideoBid('video'), getConfigCreativeVideo()) + fn: buildBid(getConfigVideoBid('video'), getConfigCreativeVideo()), }, want: { requestId: getConfigBid('video').bidId, @@ -504,25 +587,31 @@ const testsBuildBid = [ creativeId: getConfigVideoBid().extras.deal_id, netRevenue: true, vastXml: getConfigCreativeVideo().vastXml, - vastUrl: 'data:text/xml;charset=utf-8;base64,' + btoa(getConfigCreativeVideo().vastXml.replace(/\\"/g, '"')), + vastUrl: + 'data:text/xml;charset=utf-8;base64,' + + btoa(getConfigCreativeVideo().vastXml.replace(/\\"/g, '"')), ttl: 300, - } + }, }, { title: 'use default height width output model for video', args: { - fn: buildBid({...getConfigVideoBid('video'), - creative: { - video: { - content: - '', - height: null, - width: null, + fn: buildBid( + { + ...getConfigVideoBid('video'), + creative: { + video: { + content: '', + height: null, + width: null, + }, + media_type: 'video', + creativeId: getConfigVideoBid().extras.deal_id, + requestId: '2def0c5b2a7f6e', }, - media_type: 'video', - creativeId: getConfigVideoBid().extras.deal_id, - requestId: '2def0c5b2a7f6e', - }}, getConfigCreativeVideo()) + }, + getConfigCreativeVideo() + ), }, want: { requestId: getConfigBid('video').bidId, @@ -534,14 +623,16 @@ const testsBuildBid = [ creativeId: getConfigVideoBid().extras.deal_id, netRevenue: true, vastXml: getConfigCreativeVideo().vastXml, - vastUrl: 'data:text/xml;charset=utf-8;base64,' + btoa(getConfigCreativeVideo().vastXml.replace(/\\"/g, '"')), + vastUrl: + 'data:text/xml;charset=utf-8;base64,' + + btoa(getConfigCreativeVideo().vastXml.replace(/\\"/g, '"')), ttl: 300, - } + }, }, { title: 'input data respect the output model for banner', args: { - fn: buildBid(getConfigBannerBid()) + fn: buildBid(getConfigBannerBid()), }, want: { requestId: getConfigBid('banner').bidId, @@ -554,18 +645,18 @@ const testsBuildBid = [ ad: getConfigBannerBid().creative.banner.adm, ttl: 300, netRevenue: true, - } - } -] + }, + }, +]; -describe('BLIINK Adapter buildBid', function() { +describe('BLIINK Adapter buildBid', function () { for (const test of testsBuildBid) { it(test.title, () => { - const res = test.args.fn - expect(res).to.eql(test.want) - }) + const res = test.args.fn; + expect(res).to.eql(test.want); + }); } -}) +}); /** * @description Array of tests used in describe function below @@ -575,28 +666,33 @@ const testsBuildRequests = [ { title: 'Should not build request, no bidder request exist', args: { - fn: spec.buildRequests() + fn: spec.buildRequests(), }, - want: null + want: null, }, { title: 'Should build request if bidderRequest exist', args: { - fn: spec.buildRequests([], getConfigBuildRequest('banner')) + fn: spec.buildRequests([], getConfigBuildRequest('banner')), }, want: { method: 'POST', url: BLIINK_ENDPOINT_ENGINE, data: { + domLoadingDuration, + ect: connectionType, keywords: '', pageDescription: '', pageTitle: '', - pageUrl: 'http://localhost:9999/integrationExamples/gpt/bliink-adapter.html?pbjs_debug=true', + pageUrl: + 'http://localhost:9999/integrationExamples/gpt/bliink-adapter.html', tags: [ { transactionId: '2def0c5b2a7f6e', + refresh: window.bliinkBid['14f30eca-85d2-11e8-9eed-0242ac120007'] || undefined, id: '14f30eca-85d2-11e8-9eed-0242ac120007', - imageUrl: '', + imageUrl: 'https://www.example.com/adimage.jpg', + videoUrl: 'https://www.example.com/advideo.mp4', mediaTypes: ['banner'], sizes: [ { @@ -605,35 +701,43 @@ const testsBuildRequests = [ }, ], }, - ] - } - } + ], + }, + }, }, { title: 'Should build request width GDPR configuration', args: { - fn: spec.buildRequests([], Object.assign(getConfigBuildRequest('banner'), { - gdprConsent: { - gdprApplies: true, - consentString: 'XXXX' - }, - })) + fn: spec.buildRequests( + [], + Object.assign(getConfigBuildRequest('banner'), { + gdprConsent: { + gdprApplies: true, + consentString: 'XXXX', + }, + }) + ), }, want: { method: 'POST', url: BLIINK_ENDPOINT_ENGINE, data: { + domLoadingDuration, + ect: connectionType, gdpr: true, gdprConsent: 'XXXX', pageDescription: '', pageTitle: '', keywords: '', - pageUrl: 'http://localhost:9999/integrationExamples/gpt/bliink-adapter.html?pbjs_debug=true', + pageUrl: + 'http://localhost:9999/integrationExamples/gpt/bliink-adapter.html', tags: [ { transactionId: '2def0c5b2a7f6e', + refresh: window.bliinkBid['14f30eca-85d2-11e8-9eed-0242ac120007'] || undefined, id: '14f30eca-85d2-11e8-9eed-0242ac120007', - imageUrl: '', + imageUrl: 'https://www.example.com/adimage.jpg', + videoUrl: 'https://www.example.com/advideo.mp4', mediaTypes: ['banner'], sizes: [ { @@ -642,52 +746,115 @@ const testsBuildRequests = [ }, ], }, - ] - } - } + ], + }, + }, + }, + { + title: 'Should build request width uspConsent if exists', + args: { + fn: spec.buildRequests( + [], + Object.assign(getConfigBuildRequest('banner'), { + gdprConsent: { + gdprApplies: true, + consentString: 'XXXX', + }, + uspConsent: 'uspConsent', + }) + ), + }, + want: { + method: 'POST', + url: BLIINK_ENDPOINT_ENGINE, + data: { + domLoadingDuration, + ect: connectionType, + gdpr: true, + uspConsent: 'uspConsent', + gdprConsent: 'XXXX', + pageDescription: '', + pageTitle: '', + keywords: '', + pageUrl: + 'http://localhost:9999/integrationExamples/gpt/bliink-adapter.html', + tags: [ + { + transactionId: '2def0c5b2a7f6e', + refresh: window.bliinkBid['14f30eca-85d2-11e8-9eed-0242ac120007'] || undefined, + id: '14f30eca-85d2-11e8-9eed-0242ac120007', + imageUrl: 'https://www.example.com/adimage.jpg', + videoUrl: 'https://www.example.com/advideo.mp4', + mediaTypes: ['banner'], + sizes: [ + { + h: 250, + w: 300, + }, + ], + }, + ], + }, + }, }, { title: 'Should build request width schain if exists', args: { - fn: spec.buildRequests([{schain: { - ver: '1.0', - complete: 1, - nodes: [{ - asi: 'ssp.test', - sid: '00001', - hp: 1 - }] - }}], Object.assign(getConfigBuildRequest('banner'), { - gdprConsent: { - gdprApplies: true, - consentString: 'XXXX' - }, - })) + fn: spec.buildRequests( + [ + { + schain: { + ver: '1.0', + complete: 1, + nodes: [ + { + asi: 'ssp.test', + sid: '00001', + hp: 1, + }, + ], + }, + }, + ], + Object.assign(getConfigBuildRequest('banner'), { + gdprConsent: { + gdprApplies: true, + consentString: 'XXXX', + }, + }) + ), }, want: { method: 'POST', url: BLIINK_ENDPOINT_ENGINE, data: { + domLoadingDuration, + ect: connectionType, gdpr: true, gdprConsent: 'XXXX', pageDescription: '', pageTitle: '', keywords: '', - pageUrl: 'http://localhost:9999/integrationExamples/gpt/bliink-adapter.html?pbjs_debug=true', + pageUrl: + 'http://localhost:9999/integrationExamples/gpt/bliink-adapter.html', schain: { ver: '1.0', complete: 1, - nodes: [{ - asi: 'ssp.test', - sid: '00001', - hp: 1 - }] + nodes: [ + { + asi: 'ssp.test', + sid: '00001', + hp: 1, + }, + ], }, tags: [ { transactionId: '2def0c5b2a7f6e', + refresh: window.bliinkBid['14f30eca-85d2-11e8-9eed-0242ac120007'] || undefined, id: '14f30eca-85d2-11e8-9eed-0242ac120007', - imageUrl: '', + imageUrl: 'https://www.example.com/adimage.jpg', + videoUrl: 'https://www.example.com/advideo.mp4', mediaTypes: ['banner'], sizes: [ { @@ -696,107 +863,313 @@ const testsBuildRequests = [ }, ], }, - ] - } - } - } -] + ], + }, + }, + }, + { + title: 'Should build request with eids if exists', + args: { + fn: spec.buildRequests( + [ + { + userIdAsEids: [ + { + 'source': 'criteo.com', + 'uids': [ + { + 'id': 'vG4RRF93V05LRlJUTVVOQTJJJTJGbG1rZWxEeDVvc0NXWE42TzJqU2hG', + 'atype': 1 + } + ] + }, + { + 'source': 'netid.de', + 'uids': [ + { + 'id': 'fH5A3n2O8_CZZyPoJVD-eabc6ECb7jhxCicsds7qSg', + 'atype': 1 + } + ] + } + ], + }, + ], + Object.assign(getConfigBuildRequest('banner'), { + gdprConsent: { + gdprApplies: true, + consentString: 'XXXX', + }, + }) + ), + }, + want: { + method: 'POST', + url: BLIINK_ENDPOINT_ENGINE, + data: { + domLoadingDuration, + ect: connectionType, + gdpr: true, + gdprConsent: 'XXXX', + pageDescription: '', + pageTitle: '', + keywords: '', + pageUrl: + 'http://localhost:9999/integrationExamples/gpt/bliink-adapter.html', + eids: [ + { + 'source': 'criteo.com', + 'uids': [ + { + 'id': 'vG4RRF93V05LRlJUTVVOQTJJJTJGbG1rZWxEeDVvc0NXWE42TzJqU2hG', + 'atype': 1 + } + ] + }, + { + 'source': 'netid.de', + 'uids': [ + { + 'id': 'fH5A3n2O8_CZZyPoJVD-eabc6ECb7jhxCicsds7qSg', + 'atype': 1 + } + ] + } + ], + tags: [ + { + transactionId: '2def0c5b2a7f6e', + refresh: window.bliinkBid['14f30eca-85d2-11e8-9eed-0242ac120007'] || undefined, + id: '14f30eca-85d2-11e8-9eed-0242ac120007', + imageUrl: 'https://www.example.com/adimage.jpg', + videoUrl: 'https://www.example.com/advideo.mp4', + mediaTypes: ['banner'], + sizes: [ + { + h: 250, + w: 300, + }, + ], + }, + ], + }, + }, + }, +]; -describe('BLIINK Adapter buildRequests', function() { +describe('BLIINK Adapter buildRequests', function () { for (const test of testsBuildRequests) { it(test.title, () => { - const res = test.args.fn - expect(res).to.eql(test.want) - }) + const res = test.args.fn; + expect(res).to.eql(test.want); + test.args.after; + }); } -}) +}); const getSyncOptions = (pixelEnabled = true, iframeEnabled = false) => { return { pixelEnabled, - iframeEnabled - } -} + iframeEnabled, + }; +}; const getServerResponses = () => { return [ { - body: {bids: [], - userSyncs: [ { - type: 'script', - url: 'https://prg.smartadserver.com/ac?out=js&nwid=3392&siteid=305791&pgname=rg&fmtid=81127&tgt=[sas_target]&visit=m&tmstp=[timestamp]&clcturl=[countgo]' - }, - { - type: 'image', - url: 'https://sync.smartadserver.com/getuid?nwid=3392&consentString=XXX&url=https%3A%2F%2Fcookiesync.api.bliink.io%2Fcookiesync%3Fpartner%3Dsmart%26uid%3D%5Bsas_uid%5D' - }]}, - } - ] -} + body: { + bids: [], + userSyncs: [ + { + type: 'script', + url: 'https://prg.smartadserver.com/ac?out=js&nwid=3392&siteid=305791&pgname=rg&fmtid=81127&tgt=[sas_target]&visit=m&tmstp=[timestamp]&clcturl=[countgo]', + }, + { + type: 'image', + url: 'https://sync.smartadserver.com/getuid?nwid=3392&consentString=XXX&url=https%3A%2F%2Fcookiesync.api.bliink.io%2Fcookiesync%3Fpartner%3Dsmart%26uid%3D%5Bsas_uid%5D', + }, + ], + }, + }, + ]; +}; const getGdprConsent = () => { return { gdprApplies: 1, consentString: 'XXX', - apiVersion: 2 - } -} + apiVersion: 2, + }; +}; const testsGetUserSyncs = [ { title: 'Should not have gdprConsent exist', args: { - fn: spec.getUserSyncs(getSyncOptions(), getServerResponses(), getGdprConsent()) + fn: spec.getUserSyncs( + getSyncOptions(), + getServerResponses(), + getGdprConsent() + ), }, want: [ { type: 'script', - url: 'https://prg.smartadserver.com/ac?out=js&nwid=3392&siteid=305791&pgname=rg&fmtid=81127&tgt=[sas_target]&visit=m&tmstp=[timestamp]&clcturl=[countgo]' + url: 'https://prg.smartadserver.com/ac?out=js&nwid=3392&siteid=305791&pgname=rg&fmtid=81127&tgt=[sas_target]&visit=m&tmstp=[timestamp]&clcturl=[countgo]', }, { type: 'image', - url: 'https://sync.smartadserver.com/getuid?nwid=3392&consentString=XXX&url=https%3A%2F%2Fcookiesync.api.bliink.io%2Fcookiesync%3Fpartner%3Dsmart%26uid%3D%5Bsas_uid%5D' - } - ] + url: 'https://sync.smartadserver.com/getuid?nwid=3392&consentString=XXX&url=https%3A%2F%2Fcookiesync.api.bliink.io%2Fcookiesync%3Fpartner%3Dsmart%26uid%3D%5Bsas_uid%5D', + }, + ], }, { title: 'Should return iframe cookie sync if iframeEnabled', args: { - fn: spec.getUserSyncs(getSyncOptions(true, true), getServerResponses(), getGdprConsent()) + fn: spec.getUserSyncs( + getSyncOptions(true, true), + getServerResponses(), + getGdprConsent() + ), }, want: [ { type: 'iframe', - url: `${BLIINK_ENDPOINT_COOKIE_SYNC_IFRAME}?gdpr=${getGdprConsent().gdprApplies}&coppa=0&gdprConsent=${getGdprConsent().consentString}&apiVersion=${getGdprConsent().apiVersion}` + url: `${BLIINK_ENDPOINT_COOKIE_SYNC_IFRAME}?gdpr=${ + getGdprConsent().gdprApplies + }&coppa=0&gdprConsent=${getGdprConsent().consentString}&apiVersion=${ + getGdprConsent().apiVersion + }`, }, - ] + ], }, { title: 'ccpa', args: { - fn: spec.getUserSyncs(getSyncOptions(true, true), getServerResponses(), getGdprConsent(), 'ccpa-consent') + fn: spec.getUserSyncs( + getSyncOptions(true, true), + getServerResponses(), + getGdprConsent(), + 'ccpa-consent' + ), }, want: [ { type: 'iframe', - url: `${BLIINK_ENDPOINT_COOKIE_SYNC_IFRAME}?gdpr=${getGdprConsent().gdprApplies}&coppa=0&uspConsent=ccpa-consent&gdprConsent=${getGdprConsent().consentString}&apiVersion=${getGdprConsent().apiVersion}` + url: `${BLIINK_ENDPOINT_COOKIE_SYNC_IFRAME}?gdpr=${ + getGdprConsent().gdprApplies + }&coppa=0&uspConsent=ccpa-consent&gdprConsent=${ + getGdprConsent().consentString + }&apiVersion=${getGdprConsent().apiVersion}`, }, - ] + ], }, { title: 'Should output sync if no gdprConsent', args: { - fn: spec.getUserSyncs(getSyncOptions(), getServerResponses()) + fn: spec.getUserSyncs(getSyncOptions(), getServerResponses()), }, - want: getServerResponses()[0].body.userSyncs - } -] + want: getServerResponses()[0].body.userSyncs, + }, + { + title: 'Should output empty array if no pixelEnabled', + args: { + fn: spec.getUserSyncs({}, getServerResponses()), + }, + want: [], + }, +]; -describe('BLIINK Adapter getUserSyncs', function() { +describe('BLIINK Adapter getUserSyncs', function () { for (const test of testsGetUserSyncs) { it(test.title, () => { - const res = test.args.fn - expect(res).to.eql(test.want) - }) + const res = test.args.fn; + expect(res).to.eql(test.want); + }); } -}) +}); + +describe('BLIINK Adapter keywords & coppa true', function () { + it('Should build request with keyword and coppa true if exist', () => { + const metaElement = document.createElement('meta'); + metaElement.name = 'keywords'; + metaElement.content = 'Bliink, Saber, Prebid'; + sinon.stub(config, 'getConfig').withArgs('coppa').returns(true); + + const querySelectorStub = sinon + .stub(document, 'querySelector') + .returns(metaElement); + expect( + spec.buildRequests( + [], + Object.assign(getConfigBuildRequest('banner'), { + gdprConsent: { + gdprApplies: true, + consentString: 'XXXX', + }, + }) + ) + ).to.eql({ + method: 'POST', + url: BLIINK_ENDPOINT_ENGINE, + data: { + domLoadingDuration, + ect: connectionType, + gdpr: true, + coppa: 1, + gdprConsent: 'XXXX', + pageDescription: 'Bliink, Saber, Prebid', + pageTitle: '', + keywords: 'Bliink,Saber,Prebid', + pageUrl: + 'http://localhost:9999/integrationExamples/gpt/bliink-adapter.html', + tags: [ + { + transactionId: '2def0c5b2a7f6e', + id: '14f30eca-85d2-11e8-9eed-0242ac120007', + imageUrl: 'https://www.example.com/adimage.jpg', + videoUrl: 'https://www.example.com/advideo.mp4', + mediaTypes: ['banner'], + refresh: window.bliinkBid['14f30eca-85d2-11e8-9eed-0242ac120007'] || undefined, + sizes: [ + { + h: 250, + w: 300, + }, + ], + }, + ], + }, + }); + querySelectorStub.restore(); + config.getConfig.restore(); + }); +}); + +describe('getEffectiveConnectionType', () => { + let navigatorStub; + + beforeEach(() => { + if ('connection' in navigator) { + navigatorStub = sinon.stub(navigator, 'connection').value({ + effectiveType: undefined, + }); + } + }); + + afterEach(() => { + if (navigatorStub) { + navigatorStub.restore(); + } + }); + if (navigatorStub) { + it('should return "unsupported" when effective connection type is undefined', () => { + const result = getEffectiveConnectionType(); + expect(result).to.equal('unsupported'); + }); + } +}); + +it('should expose gvlid', function () { + expect(spec.gvlid).to.equal(GVL_ID); +}); diff --git a/test/spec/modules/boldwinBidAdapter_spec.js b/test/spec/modules/boldwinBidAdapter_spec.js index 5b51183ea6d..9a7b16c0914 100644 --- a/test/spec/modules/boldwinBidAdapter_spec.js +++ b/test/spec/modules/boldwinBidAdapter_spec.js @@ -19,7 +19,8 @@ describe('BoldwinBidAdapter', function () { const bidderRequest = { refererInfo: { referer: 'test.com' - } + }, + ortb2: {} }; describe('isBidRequestValid', function () { @@ -110,6 +111,36 @@ describe('BoldwinBidAdapter', function () { expect(data.placements).to.be.an('array').that.is.empty; }); }); + + describe('gpp consent', function () { + it('bidderRequest.gppConsent', () => { + bidderRequest.gppConsent = { + gppString: 'abc123', + applicableSections: [8] + }; + + let serverRequest = spec.buildRequests([bid], bidderRequest); + let data = serverRequest.data; + expect(data).to.be.an('object'); + expect(data).to.have.property('gpp'); + expect(data).to.have.property('gpp_sid'); + + delete bidderRequest.gppConsent; + }) + + it('bidderRequest.ortb2.regs.gpp', () => { + bidderRequest.ortb2.regs = bidderRequest.ortb2.regs || {}; + bidderRequest.ortb2.regs.gpp = 'abc123'; + bidderRequest.ortb2.regs.gpp_sid = [8]; + + let serverRequest = spec.buildRequests([bid], bidderRequest); + let data = serverRequest.data; + expect(data).to.be.an('object'); + expect(data).to.have.property('gpp'); + expect(data).to.have.property('gpp_sid'); + }) + }); + describe('interpretResponse', function () { it('Should interpret banner response', function () { const banner = { @@ -283,7 +314,7 @@ describe('BoldwinBidAdapter', function () { 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://cs.videowalldirect.com'); + expect(userSync[0].url).to.be.equal('https://sync.videowalldirect.com'); }); }); }); diff --git a/test/spec/modules/brandmetricsRtdProvider_spec.js b/test/spec/modules/brandmetricsRtdProvider_spec.js index 879ec7e1c7a..72a2e4b029c 100644 --- a/test/spec/modules/brandmetricsRtdProvider_spec.js +++ b/test/spec/modules/brandmetricsRtdProvider_spec.js @@ -1,5 +1,7 @@ import * as brandmetricsRTD from '../../../modules/brandmetricsRtdProvider.js'; import {config} from 'src/config.js'; +import * as events from '../../../src/events'; +import * as sinon from 'sinon'; const VALID_CONFIG = { name: 'brandmetrics', @@ -65,6 +67,8 @@ const NO_USP_CONSENT = { usp: '1NYY' }; +const UNDEFINED_USER_CONSENT = {}; + function mockSurveyLoaded(surveyConf) { const commands = window._brandmetrics || []; commands.forEach(command => { @@ -77,14 +81,16 @@ function mockSurveyLoaded(surveyConf) { }); } -function scriptTagExists(url) { - const tags = document.getElementsByTagName('script'); - for (let i = 0; i < tags.length; i++) { - if (tags[i].src === url) { - return true; +function mockCreativeInView(creativeInViewConf) { + const commands = window._brandmetrics || []; + commands.forEach(command => { + if (command.cmd === '_addeventlistener') { + const conf = command.val; + if (conf.event === 'creative_in_view') { + conf.handler(creativeInViewConf); + } } - } - return false; + }) } describe('BrandmetricsRTD module', () => { @@ -116,6 +122,10 @@ describe('BrandmetricsRTD module', () => { it('should not init when there is no usp- consent', () => { expect(brandmetricsRTD.brandmetricsSubmodule.init(VALID_CONFIG, NO_USP_CONSENT)).to.equal(false); }); + + it('should init if there are no consent- objects defined', () => { + expect(brandmetricsRTD.brandmetricsSubmodule.init(VALID_CONFIG, UNDEFINED_USER_CONSENT)).to.equal(true); + }); }); describe('getBidRequestData', () => { @@ -188,4 +198,62 @@ describe('getBidRequestData', () => { expect(bidderOrtb2[exp].user.ext.data.brandmetrics_survey).to.equal('mockMeasurementId') }) }); + + describe('billable events', () => { + let sandbox; + let eventsEmitSpy; + + before(() => { + sandbox = sinon.sandbox.create(); + eventsEmitSpy = sandbox.spy(events, ['emit']); + }); + + beforeEach(() => { + eventsEmitSpy.resetHistory(); + }) + + afterEach(() => { + sandbox.restore(); + }); + + it('should emit billable event from prebid events', () => { + const expectedEvent = { + vendor: 'brandmetrics', + type: 'creative_in_view', + measurementId: 'mockMeasurementId', + auctionId: 'mockAuctionId', + transactionId: 'mockTransactionId' + }; + + mockCreativeInView({ + mid: expectedEvent.measurementId, + source: { + type: 'pbj', + data: { + auctionId: expectedEvent.auctionId, + transactionId: expectedEvent.transactionId + }, + } + }); + + expect(eventsEmitSpy.callCount).to.equal(1); + + const event = eventsEmitSpy.getCalls()[0].args[1]; + delete event['billingId']; + + expect(event).to.deep.equal(expectedEvent); + }); + + it('should not emit billable event from non prebid- sources', () => { + mockCreativeInView({ + mid: 'mockMeasurementId', + source: { + type: 'gpt', + data: {}, + } + }); + + expect(eventsEmitSpy.callCount).to.equal(0); + }); + }); }); diff --git a/test/spec/modules/bridBidAdapter_spec.js b/test/spec/modules/bridBidAdapter_spec.js new file mode 100644 index 00000000000..7503c748999 --- /dev/null +++ b/test/spec/modules/bridBidAdapter_spec.js @@ -0,0 +1,129 @@ +import { spec } from '../../../modules/bridBidAdapter.js' + +describe('Brid Bid Adapter', function() { + const videoRequest = [{ + bidder: 'brid', + params: { + placementId: 12345, + }, + mediaTypes: { + video: { + playerSize: [[640, 360]], + context: 'instream', + playbackmethod: [1, 2, 3, 4] + } + } + }]; + + it('Test the bid validation function', function() { + const validBid = spec.isBidRequestValid(videoRequest[0]); + const invalidBid = spec.isBidRequestValid(null); + + expect(validBid).to.be.true; + expect(invalidBid).to.be.false; + }); + + it('Test the request processing function', function () { + const request = spec.buildRequests(videoRequest, videoRequest[0]); + expect(request).to.not.be.empty; + + const payload = JSON.parse(request[0].data); + expect(payload).to.not.be.empty; + expect(payload.sdk).to.deep.equal({ + source: 'pbjs', + version: '$prebid.version$' + }); + expect(payload.imp[0].ext.prebid.storedrequest.id).to.equal(12345); + }); + + it('Test nobid responses', function () { + const responseBody = { + 'id': 'test-id', + 'cur': 'USD', + 'seatbid': [], + 'nbr': 0 + }; + const bidderRequest = null; + + const bidResponse = spec.interpretResponse({ body: responseBody }, {bidderRequest}); + + expect(bidResponse.length).to.equal(0); + }); + + it('Test the response parsing function', function () { + const responseBody = { + 'id': 'test-id', + 'cur': 'USD', + 'seatbid': [{ + 'bid': [{ + 'id': '5044997188309660254', + 'price': 5, + 'adm': 'test ad', + 'adid': '97517771', + 'crid': '97517771', + 'adomain': ['domain.com'], + 'w': 640, + 'h': 480 + }], + 'seat': 'bidder' + }] + }; + const bidderRequest = { + bidderCode: 'brid', + bidderRequestId: '22edbae2733bf6', + bids: videoRequest + }; + + const bidResponse = spec.interpretResponse({ body: responseBody }, {bidderRequest}); + expect(bidResponse).to.not.be.empty; + + const bid = bidResponse[0]; + expect(bid).to.not.be.empty; + expect(bid.ad).to.equal('test ad'); + expect(bid.cpm).to.equal(5); + expect(bid.width).to.equal(640); + expect(bid.height).to.equal(480); + expect(bid.currency).to.equal('USD'); + }); + + it('Test GDPR and USP consents are present in the request', function () { + let gdprConsentString = 'BOJ8RZsOJ8RZsABAB8AAAAAZ+A=='; + let uspConsentString = '1YA-'; + let bidderRequest = { + 'bidderCode': 'brid', + 'bidderRequestId': '22edbae2733bf6', + 'timeout': 3000, + 'uspConsent': uspConsentString, + 'gdprConsent': { + consentString: gdprConsentString, + gdprApplies: true, + addtlConsent: '1~7.12.35.62.66.70.89.93.108' + } + }; + bidderRequest.bids = videoRequest; + + const request = spec.buildRequests(videoRequest, bidderRequest); + const payload = JSON.parse(request[0].data); + + expect(payload.user.ext.consent).to.equal(gdprConsentString); + expect(payload.regs.ext.us_privacy).to.equal(uspConsentString); + expect(payload.regs.ext.gdpr).to.equal(1); + }); + + it('Test GDPR is not present', function () { + let uspConsentString = '1YA-'; + let bidderRequest = { + 'bidderCode': 'brid', + 'bidderRequestId': '22edbae2733bf6', + 'timeout': 3000, + 'uspConsent': uspConsentString + }; + bidderRequest.bids = videoRequest; + + const request = spec.buildRequests(videoRequest, bidderRequest); + const payload = JSON.parse(request[0].data); + + expect(payload.regs.ext.gdpr).to.be.undefined; + expect(payload.regs.ext.us_privacy).to.equal(uspConsentString); + }); +}); diff --git a/test/spec/modules/browsiRtdProvider_spec.js b/test/spec/modules/browsiRtdProvider_spec.js index 75120aa7505..5fcc78f4322 100644 --- a/test/spec/modules/browsiRtdProvider_spec.js +++ b/test/spec/modules/browsiRtdProvider_spec.js @@ -89,12 +89,6 @@ describe('browsi Real time data sub module', function () { expect(browsiRTD.browsiSubmodule.getTargetingData([], null, null, auction)).to.eql({}); }); - it('should return NA if no prediction for ad unit', function () { - makeSlot({code: 'adMock', divId: 'browsiAd_2'}); - browsiRTD.setData({}); - expect(browsiRTD.browsiSubmodule.getTargetingData(['adMock'], null, null, auction)).to.eql({adMock: {bv: 'NA'}}); - }); - it('should return prediction from server', function () { makeSlot({code: 'hasPrediction', divId: 'hasPrediction'}); const data = { diff --git a/test/spec/modules/byDataAnalyticsAdapter_spec.js b/test/spec/modules/byDataAnalyticsAdapter_spec.js index 1346284695c..c680c687a71 100644 --- a/test/spec/modules/byDataAnalyticsAdapter_spec.js +++ b/test/spec/modules/byDataAnalyticsAdapter_spec.js @@ -96,7 +96,7 @@ let expectedDataArgs = { aus: '300x250', bidadv: 'appnexus', bid: '14480e9832f2d2b', - inb: 0, + inb: 1, ito: 0, ipwb: 0, iwb: 0, @@ -107,7 +107,7 @@ let expectedDataArgs = { aus: '250x250', bidadv: 'appnexus', bid: '14480e9832f2d2b', - inb: 0, + inb: 1, ito: 0, ipwb: 0, iwb: 0, @@ -167,11 +167,13 @@ describe('byData Analytics Adapter ', () => { }); describe('track-events', function () { - ascAdapter.enableAnalytics(initOptions) - // Step 1: Initialize adapter - adapterManager.enableAnalytics({ - provider: 'bydata', - options: initOptions + before(() => { + ascAdapter.enableAnalytics(initOptions) + // Step 1: Initialize adapter + adapterManager.enableAnalytics({ + provider: 'bydata', + options: initOptions + }); }); it('sends and formatted auction data ', function () { events.emit(constants.EVENTS.BID_TIMEOUT, bidTimeoutArgs); diff --git a/test/spec/modules/cadentApertureMXBidAdapter_spec.js b/test/spec/modules/cadentApertureMXBidAdapter_spec.js index 091a8105354..3ccb5405552 100644 --- a/test/spec/modules/cadentApertureMXBidAdapter_spec.js +++ b/test/spec/modules/cadentApertureMXBidAdapter_spec.js @@ -1,7 +1,8 @@ -import { expect } from 'chai'; -import { spec } from 'modules/cadentApertureMXBidAdapter.js'; import * as utils from 'src/utils.js'; + +import { expect } from 'chai'; import { newBidder } from 'src/adapters/bidderFactory.js'; +import { spec } from 'modules/cadentApertureMXBidAdapter.js'; describe('cadent_aperture_mx Adapter', function () { describe('callBids', function () { @@ -236,228 +237,312 @@ describe('cadent_aperture_mx Adapter', function () { 'bidId': '30b31c2501de1e', 'auctionId': 'e19f1eff-8b27-42a6-888d-9674e5a6130c', 'transactionId': 'd7b773de-ceaa-484d-89ca-d9f51b8d61ec', + 'ortb2Imp': { + 'ext': { + 'tid': 'd7b773de-ceaa-484d-89ca-d9f51b8d61ed', + }, + }, }] }; let request = spec.buildRequests(bidderRequest.bids, bidderRequest); - it('sends bid request to ENDPOINT via POST', function () { - expect(request.method).to.equal('POST'); - }); + describe('non-gpp tests', function() { + it('sends bid request to ENDPOINT via POST', function () { + expect(request.method).to.equal('POST'); + }); - it('contains the correct options', function () { - expect(request.options.withCredentials).to.equal(true); - }); + it('contains the correct options', function () { + expect(request.options.withCredentials).to.equal(true); + }); - it('contains a properly formatted endpoint url', function () { - const url = request.url.split('?'); - const queryParams = url[1].split('&'); - expect(queryParams[0]).to.match(new RegExp('^t=\d*', 'g')); - expect(queryParams[1]).to.match(new RegExp('^ts=\d*', 'g')); - }); + it('contains a properly formatted endpoint url', function () { + const url = request.url.split('?'); + const queryParams = url[1].split('&'); + expect(queryParams[0]).to.match(new RegExp('^t=\d*', 'g')); + expect(queryParams[1]).to.match(new RegExp('^ts=\d*', 'g')); + }); - it('builds bidfloor value from bid param when getFloor function does not exist', function () { - const bidRequestWithFloor = utils.deepClone(bidderRequest.bids); - bidRequestWithFloor[0].params.bidfloor = 1; - const requestWithFloor = spec.buildRequests(bidRequestWithFloor, bidderRequest); - const data = JSON.parse(requestWithFloor.data); - expect(data.imp[0].bidfloor).to.equal(bidRequestWithFloor[0].params.bidfloor); - }); + it('builds bidfloor value from bid param when getFloor function does not exist', function () { + const bidRequestWithFloor = utils.deepClone(bidderRequest.bids); + bidRequestWithFloor[0].params.bidfloor = 1; + const requestWithFloor = spec.buildRequests(bidRequestWithFloor, bidderRequest); + const data = JSON.parse(requestWithFloor.data); + expect(data.imp[0].bidfloor).to.equal(bidRequestWithFloor[0].params.bidfloor); + }); - it('builds bidfloor value from getFloor function when it exists', function () { - const floorResponse = { currency: 'USD', floor: 3 }; - const bidRequestWithGetFloor = utils.deepClone(bidderRequest.bids); - bidRequestWithGetFloor[0].getFloor = () => floorResponse; - const requestWithGetFloor = spec.buildRequests(bidRequestWithGetFloor, bidderRequest); - const data = JSON.parse(requestWithGetFloor.data); - expect(data.imp[0].bidfloor).to.equal(3); - }); + it('builds bidfloor value from getFloor function when it exists', function () { + const floorResponse = { currency: 'USD', floor: 3 }; + const bidRequestWithGetFloor = utils.deepClone(bidderRequest.bids); + bidRequestWithGetFloor[0].getFloor = () => floorResponse; + const requestWithGetFloor = spec.buildRequests(bidRequestWithGetFloor, bidderRequest); + const data = JSON.parse(requestWithGetFloor.data); + expect(data.imp[0].bidfloor).to.equal(3); + }); - it('builds bidfloor value from getFloor when both floor and getFloor function exists', function () { - const floorResponse = { currency: 'USD', floor: 3 }; - const bidRequestWithBothFloors = utils.deepClone(bidderRequest.bids); - bidRequestWithBothFloors[0].params.bidfloor = 1; - bidRequestWithBothFloors[0].getFloor = () => floorResponse; - const requestWithBothFloors = spec.buildRequests(bidRequestWithBothFloors, bidderRequest); - const data = JSON.parse(requestWithBothFloors.data); - expect(data.imp[0].bidfloor).to.equal(3); - }); + it('builds bidfloor value from getFloor when both floor and getFloor function exists', function () { + const floorResponse = { currency: 'USD', floor: 3 }; + const bidRequestWithBothFloors = utils.deepClone(bidderRequest.bids); + bidRequestWithBothFloors[0].params.bidfloor = 1; + bidRequestWithBothFloors[0].getFloor = () => floorResponse; + const requestWithBothFloors = spec.buildRequests(bidRequestWithBothFloors, bidderRequest); + const data = JSON.parse(requestWithBothFloors.data); + expect(data.imp[0].bidfloor).to.equal(3); + }); - it('empty bidfloor value when floor and getFloor is not defined', function () { - const bidRequestWithoutFloor = utils.deepClone(bidderRequest.bids); - const requestWithoutFloor = spec.buildRequests(bidRequestWithoutFloor, bidderRequest); - const data = JSON.parse(requestWithoutFloor.data); - expect(data.imp[0].bidfloor).to.not.exist; - }); + it('empty bidfloor value when floor and getFloor is not defined', function () { + const bidRequestWithoutFloor = utils.deepClone(bidderRequest.bids); + const requestWithoutFloor = spec.buildRequests(bidRequestWithoutFloor, bidderRequest); + const data = JSON.parse(requestWithoutFloor.data); + expect(data.imp[0].bidfloor).to.not.exist; + }); - it('builds request properly', function () { - const data = JSON.parse(request.data); - expect(Array.isArray(data.imp)).to.equal(true); - expect(data.id).to.equal(bidderRequest.auctionId); - expect(data.imp.length).to.equal(1); - expect(data.imp[0].id).to.equal('30b31c2501de1e'); - expect(data.imp[0].tid).to.equal('d7b773de-ceaa-484d-89ca-d9f51b8d61ec'); - expect(data.imp[0].tagid).to.equal('25251'); - expect(data.imp[0].secure).to.equal(0); - expect(data.imp[0].vastXml).to.equal(undefined); - }); + it('builds request properly', function () { + const data = JSON.parse(request.data); + expect(Array.isArray(data.imp)).to.equal(true); + expect(data.id).to.equal(bidderRequest.auctionId); + expect(data.imp.length).to.equal(1); + expect(data.imp[0].id).to.equal('30b31c2501de1e'); + expect(data.imp[0].tid).to.equal('d7b773de-ceaa-484d-89ca-d9f51b8d61ed'); + expect(data.imp[0].tagid).to.equal('25251'); + expect(data.imp[0].secure).to.equal(0); + expect(data.imp[0].vastXml).to.equal(undefined); + }); - it('properly sends site information and protocol', function () { - request = spec.buildRequests(bidderRequest.bids, bidderRequest); - request = JSON.parse(request.data); - expect(request.site).to.have.property('domain', 'example.com'); - expect(request.site).to.have.property('page', 'https://example.com/index.html?pbjs_debug=true'); - expect(request.site).to.have.property('ref', 'https://referrer.com'); - }); + it('populates id even when auctionId is not available', function () { + // addressing https://github.com/prebid/Prebid.js/issues/9781 + bidderRequest.auctionId = null; + request = spec.buildRequests(bidderRequest.bids, bidderRequest); - it('builds correctly formatted request banner object', function () { - let bidRequestWithBanner = utils.deepClone(bidderRequest.bids); - let request = spec.buildRequests(bidRequestWithBanner, bidderRequest); - const data = JSON.parse(request.data); - expect(data.imp[0].video).to.equal(undefined); - expect(data.imp[0].banner).to.exist.and.to.be.a('object'); - expect(data.imp[0].banner.w).to.equal(bidRequestWithBanner[0].mediaTypes.banner.sizes[0][0]); - expect(data.imp[0].banner.h).to.equal(bidRequestWithBanner[0].mediaTypes.banner.sizes[0][1]); - expect(data.imp[0].banner.format[0].w).to.equal(bidRequestWithBanner[0].mediaTypes.banner.sizes[0][0]); - expect(data.imp[0].banner.format[0].h).to.equal(bidRequestWithBanner[0].mediaTypes.banner.sizes[0][1]); - expect(data.imp[0].banner.format[1].w).to.equal(bidRequestWithBanner[0].mediaTypes.banner.sizes[1][0]); - expect(data.imp[0].banner.format[1].h).to.equal(bidRequestWithBanner[0].mediaTypes.banner.sizes[1][1]); - }); + const data = JSON.parse(request.data); + expect(data.id).not.to.be.null; + expect(data.id).not.to.equal(bidderRequest.auctionId); + }); - it('builds correctly formatted request video object for instream', function () { - let bidRequestWithVideo = utils.deepClone(bidderRequest.bids); - bidRequestWithVideo[0].mediaTypes = { - video: { - context: 'instream', - playerSize: [[640, 480]] - }, - }; - bidRequestWithVideo[0].params.video = {}; - let request = spec.buildRequests(bidRequestWithVideo, bidderRequest); - const data = JSON.parse(request.data); - expect(data.imp[0].video).to.exist.and.to.be.a('object'); - expect(data.imp[0].video.w).to.equal(bidRequestWithVideo[0].mediaTypes.video.playerSize[0][0]); - expect(data.imp[0].video.h).to.equal(bidRequestWithVideo[0].mediaTypes.video.playerSize[0][1]); - }); + it('properly sends site information and protocol', function () { + request = spec.buildRequests(bidderRequest.bids, bidderRequest); + request = JSON.parse(request.data); + expect(request.site).to.have.property('domain', 'example.com'); + expect(request.site).to.have.property('page', 'https://example.com/index.html?pbjs_debug=true'); + expect(request.site).to.have.property('ref', 'https://referrer.com'); + }); - it('builds correctly formatted request video object for outstream', function () { - let bidRequestWithOutstreamVideo = utils.deepClone(bidderRequest.bids); - bidRequestWithOutstreamVideo[0].mediaTypes = { - video: { - context: 'outstream', - playerSize: [[640, 480]] - }, - }; - bidRequestWithOutstreamVideo[0].params.video = {}; - let request = spec.buildRequests(bidRequestWithOutstreamVideo, bidderRequest); - const data = JSON.parse(request.data); - expect(data.imp[0].video).to.exist.and.to.be.a('object'); - expect(data.imp[0].video.w).to.equal(bidRequestWithOutstreamVideo[0].mediaTypes.video.playerSize[0][0]); - expect(data.imp[0].video.h).to.equal(bidRequestWithOutstreamVideo[0].mediaTypes.video.playerSize[0][1]); - }); + it('builds correctly formatted request banner object', function () { + let bidRequestWithBanner = utils.deepClone(bidderRequest.bids); + let request = spec.buildRequests(bidRequestWithBanner, bidderRequest); + const data = JSON.parse(request.data); + expect(data.imp[0].video).to.equal(undefined); + expect(data.imp[0].banner).to.exist.and.to.be.a('object'); + expect(data.imp[0].banner.w).to.equal(bidRequestWithBanner[0].mediaTypes.banner.sizes[0][0]); + expect(data.imp[0].banner.h).to.equal(bidRequestWithBanner[0].mediaTypes.banner.sizes[0][1]); + expect(data.imp[0].banner.format[0].w).to.equal(bidRequestWithBanner[0].mediaTypes.banner.sizes[0][0]); + expect(data.imp[0].banner.format[0].h).to.equal(bidRequestWithBanner[0].mediaTypes.banner.sizes[0][1]); + expect(data.imp[0].banner.format[1].w).to.equal(bidRequestWithBanner[0].mediaTypes.banner.sizes[1][0]); + expect(data.imp[0].banner.format[1].h).to.equal(bidRequestWithBanner[0].mediaTypes.banner.sizes[1][1]); + }); - it('shouldn\'t contain a user obj without GDPR information', function () { - let request = spec.buildRequests(bidderRequest.bids, bidderRequest) - request = JSON.parse(request.data) - expect(request).to.not.have.property('user'); - }); + it('builds correctly formatted request video object for instream', function () { + let bidRequestWithVideo = utils.deepClone(bidderRequest.bids); + bidRequestWithVideo[0].mediaTypes = { + video: { + context: 'instream', + playerSize: [[640, 480]] + }, + }; + bidRequestWithVideo[0].params.video = {}; + let request = spec.buildRequests(bidRequestWithVideo, bidderRequest); + const data = JSON.parse(request.data); + expect(data.imp[0].video).to.exist.and.to.be.a('object'); + expect(data.imp[0].video.w).to.equal(bidRequestWithVideo[0].mediaTypes.video.playerSize[0][0]); + expect(data.imp[0].video.h).to.equal(bidRequestWithVideo[0].mediaTypes.video.playerSize[0][1]); + }); - it('should have the right gdpr info when enabled', function () { - let consentString = 'OIJSZsOAFsABAB8EMXZZZZZ+A=='; - const gdprBidderRequest = utils.deepClone(bidderRequest); - gdprBidderRequest.gdprConsent = { - 'consentString': consentString, - 'gdprApplies': true - }; - let request = spec.buildRequests(gdprBidderRequest.bids, gdprBidderRequest); + it('builds correctly formatted request video object for outstream', function () { + let bidRequestWithOutstreamVideo = utils.deepClone(bidderRequest.bids); + bidRequestWithOutstreamVideo[0].mediaTypes = { + video: { + context: 'outstream', + playerSize: [[640, 480]] + }, + }; + bidRequestWithOutstreamVideo[0].params.video = {}; + let request = spec.buildRequests(bidRequestWithOutstreamVideo, bidderRequest); + const data = JSON.parse(request.data); + expect(data.imp[0].video).to.exist.and.to.be.a('object'); + expect(data.imp[0].video.w).to.equal(bidRequestWithOutstreamVideo[0].mediaTypes.video.playerSize[0][0]); + expect(data.imp[0].video.h).to.equal(bidRequestWithOutstreamVideo[0].mediaTypes.video.playerSize[0][1]); + }); - request = JSON.parse(request.data) - expect(request.regs.ext).to.have.property('gdpr', 1); - expect(request.user.ext).to.have.property('consent', consentString); - }); + it('shouldn\'t contain a user obj without GDPR information', function () { + let request = spec.buildRequests(bidderRequest.bids, bidderRequest) + request = JSON.parse(request.data) + expect(request).to.not.have.property('user'); + }); - it('should\'t contain consent string if gdpr isn\'t applied', function () { - const nonGdprBidderRequest = utils.deepClone(bidderRequest); - nonGdprBidderRequest.gdprConsent = { - 'gdprApplies': false - }; - let request = spec.buildRequests(nonGdprBidderRequest.bids, nonGdprBidderRequest); - request = JSON.parse(request.data) - expect(request.regs.ext).to.have.property('gdpr', 0); - expect(request).to.not.have.property('user'); - }); + it('should have the right gdpr info when enabled', function () { + let consentString = 'OIJSZsOAFsABAB8EMXZZZZZ+A=='; + const gdprBidderRequest = utils.deepClone(bidderRequest); + gdprBidderRequest.gdprConsent = { + 'consentString': consentString, + 'gdprApplies': true + }; + let request = spec.buildRequests(gdprBidderRequest.bids, gdprBidderRequest); + + request = JSON.parse(request.data) + expect(request.regs.ext).to.have.property('gdpr', 1); + expect(request.user.ext).to.have.property('consent', consentString); + }); - it('should add us privacy info to request', function() { - const uspBidderRequest = utils.deepClone(bidderRequest); - let consentString = '1YNN'; - uspBidderRequest.uspConsent = consentString; - let request = spec.buildRequests(uspBidderRequest.bids, uspBidderRequest); - request = JSON.parse(request.data); - expect(request.us_privacy).to.exist; - expect(request.us_privacy).to.exist.and.to.equal(consentString); - }); + it('should\'t contain consent string if gdpr isn\'t applied', function () { + const nonGdprBidderRequest = utils.deepClone(bidderRequest); + nonGdprBidderRequest.gdprConsent = { + 'gdprApplies': false + }; + let request = spec.buildRequests(nonGdprBidderRequest.bids, nonGdprBidderRequest); + request = JSON.parse(request.data) + expect(request.regs.ext).to.have.property('gdpr', 0); + expect(request).to.not.have.property('user'); + }); - it('should add schain object to request', function() { - const schainBidderRequest = utils.deepClone(bidderRequest); - schainBidderRequest.bids[0].schain = { - 'complete': 1, - 'ver': '1.0', - 'nodes': [ - { - 'asi': 'testing.com', - 'sid': 'abc', - 'hp': 1 - } - ] - }; - let request = spec.buildRequests(schainBidderRequest.bids, schainBidderRequest); - request = JSON.parse(request.data); - expect(request.source.ext.schain).to.exist; - expect(request.source.ext.schain).to.have.property('complete', 1); - expect(request.source.ext.schain).to.have.property('ver', '1.0'); - expect(request.source.ext.schain.nodes[0].asi).to.equal(schainBidderRequest.bids[0].schain.nodes[0].asi); - }); + it('should add us privacy info to request', function() { + const uspBidderRequest = utils.deepClone(bidderRequest); + let consentString = '1YNN'; + uspBidderRequest.uspConsent = consentString; + let request = spec.buildRequests(uspBidderRequest.bids, uspBidderRequest); + request = JSON.parse(request.data); + expect(request.us_privacy).to.exist; + expect(request.us_privacy).to.exist.and.to.equal(consentString); + }); - it('should add liveramp identitylink id to request', () => { - const idl_env = '123'; - const bidRequestWithID = utils.deepClone(bidderRequest); - bidRequestWithID.userId = { idl_env }; - let requestWithID = spec.buildRequests(bidRequestWithID.bids, bidRequestWithID); - requestWithID = JSON.parse(requestWithID.data); - expect(requestWithID.user.ext.eids[0]).to.deep.equal({ - source: 'liveramp.com', - uids: [{ - id: idl_env, - ext: { - rtiPartner: 'idl' - } - }] + it('should add schain object to request', function() { + const schainBidderRequest = utils.deepClone(bidderRequest); + schainBidderRequest.bids[0].schain = { + 'complete': 1, + 'ver': '1.0', + 'nodes': [ + { + 'asi': 'testing.com', + 'sid': 'abc', + 'hp': 1 + } + ] + }; + let request = spec.buildRequests(schainBidderRequest.bids, schainBidderRequest); + request = JSON.parse(request.data); + expect(request.source.ext.schain).to.exist; + expect(request.source.ext.schain).to.have.property('complete', 1); + expect(request.source.ext.schain).to.have.property('ver', '1.0'); + expect(request.source.ext.schain.nodes[0].asi).to.equal(schainBidderRequest.bids[0].schain.nodes[0].asi); + }); + + it('should add liveramp identitylink id to request', () => { + const idl_env = '123'; + const bidRequestWithID = utils.deepClone(bidderRequest); + bidRequestWithID.userId = { idl_env }; + let requestWithID = spec.buildRequests(bidRequestWithID.bids, bidRequestWithID); + requestWithID = JSON.parse(requestWithID.data); + expect(requestWithID.user.ext.eids[0]).to.deep.equal({ + source: 'liveramp.com', + uids: [{ + id: idl_env, + ext: { + rtiPartner: 'idl' + } + }] + }); + }); + + it('should add gpid to request if present', () => { + const gpid = '/12345/my-gpt-tag-0'; + let bid = utils.deepClone(bidderRequest.bids[0]); + bid.ortb2Imp = { ext: { data: { adserver: { adslot: gpid } } } }; + bid.ortb2Imp = { ext: { data: { pbadslot: gpid } } }; + let requestWithGPID = spec.buildRequests([bid], bidderRequest); + requestWithGPID = JSON.parse(requestWithGPID.data); + expect(requestWithGPID.imp[0].ext.gpid).to.exist.and.equal(gpid); }); - }); - it('should add gpid to request if present', () => { - const gpid = '/12345/my-gpt-tag-0'; - let bid = utils.deepClone(bidderRequest.bids[0]); - bid.ortb2Imp = { ext: { data: { adserver: { adslot: gpid } } } }; - bid.ortb2Imp = { ext: { data: { pbadslot: gpid } } }; - let requestWithGPID = spec.buildRequests([bid], bidderRequest); - requestWithGPID = JSON.parse(requestWithGPID.data); - expect(requestWithGPID.imp[0].ext.gpid).to.exist.and.equal(gpid); + it('should add UID 2.0 to request', () => { + const uid2 = { id: '456' }; + const bidRequestWithUID = utils.deepClone(bidderRequest); + bidRequestWithUID.userId = { uid2 }; + let requestWithUID = spec.buildRequests(bidRequestWithUID.bids, bidRequestWithUID); + requestWithUID = JSON.parse(requestWithUID.data); + expect(requestWithUID.user.ext.eids[0]).to.deep.equal({ + source: 'uidapi.com', + uids: [{ + id: uid2.id, + ext: { + rtiPartner: 'UID2' + } + }] + }); + }); }); - it('should add UID 2.0 to request', () => { - const uid2 = { id: '456' }; - const bidRequestWithUID = utils.deepClone(bidderRequest); - bidRequestWithUID.userId = { uid2 }; - let requestWithUID = spec.buildRequests(bidRequestWithUID.bids, bidRequestWithUID); - requestWithUID = JSON.parse(requestWithUID.data); - expect(requestWithUID.user.ext.eids[0]).to.deep.equal({ - source: 'uidapi.com', - uids: [{ - id: uid2.id, - ext: { - rtiPartner: 'UID2' - } - }] + describe('gpp tests', function() { + describe('when gppConsent is not present on bid request', () => { + it('should return request with no gpp or gpp_sid properties', function() { + const gppCompliantBidderRequest = utils.deepClone(bidderRequest); + + let request = spec.buildRequests(gppCompliantBidderRequest.bids, gppCompliantBidderRequest); + request = JSON.parse(request.data); + expect(request?.regs?.gpp).to.be.undefined; + expect(request?.regs?.gpp_sid).to.be.undefined; + }); + }); + + describe('when gppConsent is present on bid request', () => { + describe('gppString', () => { + describe('is not defined on request', () => { + it('should return request with gpp undefined', () => { + const gppCompliantBidderRequest = utils.deepClone(bidderRequest); + + let request = spec.buildRequests(gppCompliantBidderRequest.bids, gppCompliantBidderRequest); + request = JSON.parse(request.data); + expect(request?.regs?.gpp).to.be.undefined; + }); + }); + + describe('is defined on request', () => { + it('should return request with gpp set correctly', () => { + const gppCompliantBidderRequest = utils.deepClone(bidderRequest); + const gppString = 'abcdefgh'; + gppCompliantBidderRequest.gppConsent = { + gppString + } + + let request = spec.buildRequests(gppCompliantBidderRequest.bids, gppCompliantBidderRequest); + request = JSON.parse(request.data); + expect(request.regs.gpp).to.exist.and.to.equal(gppString); + }); + }); + }); + + describe('applicableSections', () => { + describe('is not defined on request', () => { + it('should return request with gpp_sid undefined', () => { + const gppCompliantBidderRequest = utils.deepClone(bidderRequest); + + let request = spec.buildRequests(gppCompliantBidderRequest.bids, gppCompliantBidderRequest); + request = JSON.parse(request.data); + expect(request?.regs?.gpp_sid).to.be.undefined; + }); + }); + + describe('is defined on request', () => { + it('should return request with gpp_sid set correctly', () => { + const gppCompliantBidderRequest = utils.deepClone(bidderRequest); + const applicableSections = [8]; + gppCompliantBidderRequest.gppConsent = { + applicableSections + } + + let request = spec.buildRequests(gppCompliantBidderRequest.bids, gppCompliantBidderRequest); + request = JSON.parse(request.data); + expect(request.regs.gpp_sid).to.deep.equal(applicableSections); + }); + }); + }); }); }); }); @@ -699,7 +784,7 @@ describe('cadent_aperture_mx Adapter', function () { it('should not throw an error when decoding an improperly encoded adm', function () { const badAdmServerResponse = utils.deepClone(serverResponse); - badAdmServerResponse.seatbid[0].bid[0].adm = '\\<\\/script\\>'; + badAdmServerResponse.seatbid[0].bid[0].adm = '\\<\\/script\\>'; badAdmServerResponse.seatbid[1].bid[0].adm = '%3F%%3Dcadent%3C3prebid'; assert.doesNotThrow(() => spec.interpretResponse({ @@ -764,5 +849,38 @@ describe('cadent_aperture_mx Adapter', function () { expect(syncs[0].url).to.contains('usp=test'); expect(syncs[0].url).to.equal('https://biddr.brealtime.com/check.html?gdpr=1&gdpr_consent=test&usp=test') }); + + it('should pass gpp string and section id', function() { + let syncs = spec.getUserSyncs({iframeEnabled: true}, {}, {}, {}, { + gppString: 'abcdefgs', + applicableSections: [1, 2, 4] + }); + expect(syncs).to.not.be.an('undefined'); + expect(syncs[0].url).to.contains('gpp=abcdefgs') + expect(syncs[0].url).to.contains('gpp_sid=1,2,4') + }); + + it('should pass us_privacy and gdpr string and gpp string', function () { + let syncs = spec.getUserSyncs({ iframeEnabled: true }, {}, + { + gdprApplies: true, + consentString: 'test' + }, + { + consentString: 'test' + }, + { + gppString: 'abcdefgs', + applicableSections: [1, 2, 4] + } + ); + expect(syncs).to.not.be.an('undefined'); + expect(syncs).to.have.lengthOf(1); + expect(syncs[0].type).to.equal('iframe'); + expect(syncs[0].url).to.contains('gdpr=1'); + expect(syncs[0].url).to.contains('usp=test'); + expect(syncs[0].url).to.contains('gpp=abcdefgs'); + expect(syncs[0].url).to.equal('https://biddr.brealtime.com/check.html?gdpr=1&gdpr_consent=test&usp=test&gpp=abcdefgs&gpp_sid=1,2,4'); + }); }); }); diff --git a/test/spec/modules/categoryTranslation_spec.js b/test/spec/modules/categoryTranslation_spec.js index 2301d6aab1b..d4f6aa66c7d 100644 --- a/test/spec/modules/categoryTranslation_spec.js +++ b/test/spec/modules/categoryTranslation_spec.js @@ -2,18 +2,16 @@ import { getAdserverCategoryHook, initTranslation, storage } from 'modules/categ import { config } from 'src/config.js'; import * as utils from 'src/utils.js'; import { expect } from 'chai'; +import {server} from '../../mocks/xhr.js'; describe('category translation', function () { - let fakeTranslationServer; let getLocalStorageStub; beforeEach(function () { - fakeTranslationServer = sinon.fakeServer.create(); getLocalStorageStub = sinon.stub(storage, 'getDataFromLocalStorage'); }); afterEach(function() { - fakeTranslationServer.reset(); getLocalStorageStub.restore(); config.resetConfig(); }); @@ -73,7 +71,7 @@ describe('category translation', function () { } })); initTranslation(); - expect(fakeTranslationServer.requests.length).to.equal(0); + expect(server.requests.length).to.equal(0); clock.restore(); }); @@ -86,15 +84,15 @@ describe('category translation', function () { } })); initTranslation(); - expect(fakeTranslationServer.requests.length).to.equal(1); + expect(server.requests.length).to.equal(1); clock.restore(); }); it('should use default mapping file if publisher has not defined in config', function () { getLocalStorageStub.returns(null); initTranslation('http://sample.com', 'somekey'); - expect(fakeTranslationServer.requests.length).to.equal(1); - expect(fakeTranslationServer.requests[0].url).to.equal('http://sample.com'); + expect(server.requests.length).to.equal(1); + expect(server.requests[0].url).to.equal('http://sample.com/'); }); it('should use publisher defined mapping file', function () { @@ -105,7 +103,7 @@ describe('category translation', function () { }); getLocalStorageStub.returns(null); initTranslation('http://sample.com', 'somekey'); - expect(fakeTranslationServer.requests.length).to.equal(2); - expect(fakeTranslationServer.requests[0].url).to.equal('http://sample.com'); + expect(server.requests.length).to.equal(2); + expect(server.requests[0].url).to.equal('http://sample.com/'); }); }); diff --git a/test/spec/modules/chtnwBidAdapter_spec.js b/test/spec/modules/chtnwBidAdapter_spec.js index 3875de6c4fa..0fdd1a3f19b 100644 --- a/test/spec/modules/chtnwBidAdapter_spec.js +++ b/test/spec/modules/chtnwBidAdapter_spec.js @@ -39,7 +39,10 @@ describe('ChtnwAdapter', function () { ], }]; - const request = spec.buildRequests(bidRequests); + let request; + before(() => { + request = spec.buildRequests(bidRequests); + }) it('Returns POST method', function () { expect(request.method).to.equal('POST'); diff --git a/test/spec/modules/concertBidAdapter_spec.js b/test/spec/modules/concertBidAdapter_spec.js index 96c98e5e5a2..0a76ed3e62d 100644 --- a/test/spec/modules/concertBidAdapter_spec.js +++ b/test/spec/modules/concertBidAdapter_spec.js @@ -94,7 +94,7 @@ describe('ConcertAdapter', function () { }); describe('spec.isBidRequestValid', function() { - it('should return when it recieved all the required params', function() { + it('should return when it received all the required params', function() { const bid = bidRequests[0]; expect(spec.isBidRequestValid(bid)).to.equal(true); }); @@ -116,7 +116,20 @@ describe('ConcertAdapter', function () { expect(payload).to.have.property('meta'); expect(payload).to.have.property('slots'); - const metaRequiredFields = ['prebidVersion', 'pageUrl', 'screen', 'debug', 'uid', 'optedOut', 'adapterVersion', 'uspConsent', 'gdprConsent', 'gppConsent']; + const metaRequiredFields = [ + 'prebidVersion', + 'pageUrl', + 'screen', + 'debug', + 'uid', + 'optedOut', + 'adapterVersion', + 'uspConsent', + 'gdprConsent', + 'gppConsent', + 'browserLanguage', + 'tdid' + ]; const slotsRequiredFields = ['name', 'bidId', 'transactionId', 'sizes', 'partnerId', 'slotType']; metaRequiredFields.forEach(function(field) { @@ -199,6 +212,31 @@ describe('ConcertAdapter', function () { expect(slot.offsetCoordinates.x).to.equal(100) expect(slot.offsetCoordinates.y).to.equal(0) }) + + it('should not pass along tdid if the user has opted out', function() { + storage.setDataInLocalStorage('c_nap', 'true'); + const request = spec.buildRequests(bidRequests, bidRequest); + const payload = JSON.parse(request.data); + + expect(payload.meta.tdid).to.be.null; + }); + + it('should not pass along tdid if USP consent disallows', function() { + storage.removeDataFromLocalStorage('c_nap'); + const request = spec.buildRequests(bidRequests, { ...bidRequest, uspConsent: '1YY' }); + const payload = JSON.parse(request.data); + + expect(payload.meta.tdid).to.be.null; + }); + + it('should pass along tdid if the user has not opted out', function() { + storage.removeDataFromLocalStorage('c_nap', 'true'); + const tdid = '123abc'; + const bidRequestsWithTdid = [{ ...bidRequests[0], userId: { tdid } }] + const request = spec.buildRequests(bidRequestsWithTdid, bidRequest); + const payload = JSON.parse(request.data); + expect(payload.meta.tdid).to.equal(tdid); + }); }); describe('spec.interpretResponse', function() { diff --git a/test/spec/modules/connatixBidAdapter_spec.js b/test/spec/modules/connatixBidAdapter_spec.js new file mode 100644 index 00000000000..78f6a9d410d --- /dev/null +++ b/test/spec/modules/connatixBidAdapter_spec.js @@ -0,0 +1,350 @@ +import { expect } from 'chai'; +import { + spec, + getBidFloor as connatixGetBidFloor +} from '../../../modules/connatixBidAdapter.js'; +import { BANNER } from '../../../src/mediaTypes.js'; + +describe('connatixBidAdapter', function () { + let bid; + + function mockBidRequest() { + const mediaTypes = { + banner: { + sizes: [16, 9], + } + }; + return { + bidId: 'testing', + bidder: 'connatix', + params: { + placementId: '30e91414-545c-4f45-a950-0bec9308ff22' + }, + mediaTypes + }; + }; + + describe('isBidRequestValid', function () { + this.beforeEach(function () { + bid = mockBidRequest(); + }); + + it('Should return true if all required fileds are present', function () { + expect(spec.isBidRequestValid(bid)).to.be.true; + }); + it('Should return false if bidder does not correspond', function () { + bid.bidder = 'abc'; + expect(spec.isBidRequestValid(bid)).to.be.false; + }); + it('Should return false if bidId is missing', function () { + delete bid.bidId; + expect(spec.isBidRequestValid(bid)).to.be.false; + }); + it('Should return false if params object is missing', function () { + delete bid.params; + expect(spec.isBidRequestValid(bid)).to.be.false; + }); + it('Should return false if placementId is missing from params', function () { + delete bid.params.placementId; + expect(spec.isBidRequestValid(bid)).to.be.false; + }); + it('Should return false if mediaTypes is missing', function () { + delete bid.mediaTypes; + expect(spec.isBidRequestValid(bid)).to.be.false; + }); + it('Should return false if banner is missing from mediaTypes ', function () { + delete bid.mediaTypes.banner; + expect(spec.isBidRequestValid(bid)).to.be.false; + }); + it('Should return false if sizes is missing from banner object', function () { + delete bid.mediaTypes.banner.sizes; + expect(spec.isBidRequestValid(bid)).to.be.false; + }); + it('Should return false if sizes is not an array', function () { + bid.mediaTypes.banner.sizes = 'test'; + expect(spec.isBidRequestValid(bid)).to.be.false; + }); + it('Should return false if sizes is an empty array', function () { + bid.mediaTypes.banner.sizes = []; + expect(spec.isBidRequestValid(bid)).to.be.false; + }); + it('Should return true if add an extra field was added to the bidRequest', function () { + bid.params.test = 1; + expect(spec.isBidRequestValid(bid)).to.be.true; + }); + }); + + describe('buildRequests', function () { + let serverRequest; + let bidderRequest = { + refererInfo: { + canonicalUrl: '', + numIframes: 0, + reachedTop: true, + referer: 'http://example.com', + stack: ['http://example.com'] + }, + gdprConsent: { + consentString: 'BOJ/P2HOJ/P2HABABMAAAAAZ+A==', + vendorData: {}, + gdprApplies: true + }, + uspConsent: '1YYY', + ortb2: { + site: { + data: { + pageType: 'article' + } + } + } + }; + + this.beforeEach(function () { + bid = mockBidRequest(); + serverRequest = spec.buildRequests([bid], bidderRequest); + }) + + it('Creates a ServerRequest object with method, URL and data', function () { + expect(serverRequest).to.exist; + expect(serverRequest.method).to.exist; + expect(serverRequest.url).to.exist; + expect(serverRequest.data).to.exist; + }); + it('Returns POST method', function () { + expect(serverRequest.method).to.equal('POST'); + }); + it('Returns valid URL', function () { + expect(serverRequest.url).to.equal('https://capi.connatix.com/rtb/hba'); + }); + it('Returns request payload', function () { + expect(serverRequest.data).to.not.empty; + }); + it('Validate request payload', function () { + expect(serverRequest.data.bidRequests[0].bidId).to.equal(bid.bidId); + expect(serverRequest.data.bidRequests[0].placementId).to.equal(bid.params.placementId); + expect(serverRequest.data.bidRequests[0].floor).to.equal(0); + expect(serverRequest.data.bidRequests[0].mediaTypes).to.equal(bid.mediaTypes); + expect(serverRequest.data.bidRequests[0].sizes).to.equal(bid.mediaTypes.sizes); + expect(serverRequest.data.refererInfo).to.equal(bidderRequest.refererInfo); + expect(serverRequest.data.gdprConsent).to.equal(bidderRequest.gdprConsent); + expect(serverRequest.data.uspConsent).to.equal(bidderRequest.uspConsent); + expect(serverRequest.data.ortb2).to.equal(bidderRequest.ortb2); + }); + }); + + describe('interpretResponse', function () { + const CustomerId = '99f20d18-c4b4-4a28-3d8e-d43e2c8cb4ac'; + const PlayerId = 'e4984e88-9ff4-45a3-8b9d-33aabcad634f'; + const Bid = {Cpm: 0.1, RequestId: '2f897340c4eaa3', Ttl: 86400, CustomerId, PlayerId}; + + let serverResponse; + this.beforeEach(function () { + serverResponse = { + body: { + Bids: [ Bid ] + }, + headers: function() { } + }; + }); + + it('Should return an empty array if Bids is null', function () { + serverResponse.body.Bids = null; + + const response = spec.interpretResponse(serverResponse); + expect(response).to.be.an('array').that.is.empty; + }); + + it('Should return an empty array if Bids is empty array', function () { + serverResponse.body.Bids = []; + const response = spec.interpretResponse(serverResponse); + expect(response).to.be.an('array').that.is.empty; + }); + + it('Should return one bid response for one bid', function() { + const bidResponses = spec.interpretResponse(serverResponse); + expect(bidResponses.length).to.equal(1); + }); + + it('Should contains the same values as in the serverResponse', function() { + const bidResponses = spec.interpretResponse(serverResponse); + + const [ bidResponse ] = bidResponses; + expect(bidResponse.requestId).to.equal(serverResponse.body.Bids[0].RequestId); + expect(bidResponse.cpm).to.equal(serverResponse.body.Bids[0].Cpm); + expect(bidResponse.ttl).to.equal(serverResponse.body.Bids[0].Ttl); + expect(bidResponse.currency).to.equal('USD'); + expect(bidResponse.mediaType).to.equal(BANNER); + expect(bidResponse.netRevenue).to.be.true; + }); + + it('Should return n bid responses for n bids', function() { + serverResponse.body.Bids = [ { ...Bid }, { ...Bid } ]; + + const firstBidCpm = 4; + serverResponse.body.Bids[0].Cpm = firstBidCpm; + + const secondBidCpm = 13; + serverResponse.body.Bids[1].Cpm = secondBidCpm; + + const bidResponses = spec.interpretResponse(serverResponse); + expect(bidResponses.length).to.equal(2); + + expect(bidResponses[0].cpm).to.equal(firstBidCpm); + expect(bidResponses[1].cpm).to.equal(secondBidCpm); + }); + }); + + describe('getUserSyncs', function() { + const CustomerId = '99f20d18-c4b4-4a28-3d8e-d43e2c8cb4ac'; + const PlayerId = 'e4984e88-9ff4-45a3-8b9d-33aabcad634f'; + const UserSyncEndpoint = 'https://connatix.com/sync' + const Bid = {Cpm: 0.1, RequestId: '2f897340c4eaa3', Ttl: 86400, CustomerId, PlayerId}; + + const serverResponse = { + body: { + UserSyncEndpoint, + Bids: [ Bid ] + }, + headers: function() { } + }; + + it('Should return an empty array when iframeEnabled: false', function () { + expect(spec.getUserSyncs({iframeEnabled: false, pixelEnabled: true}, [], {}, {}, {})).to.be.an('array').that.is.empty; + }); + it('Should return an empty array when serverResponses is emprt array', function () { + expect(spec.getUserSyncs({iframeEnabled: true, pixelEnabled: true}, [], {}, {}, {})).to.be.an('array').that.is.empty; + }); + it('Should return an empty array when iframeEnabled: true but serverResponses in an empty array', function () { + expect(spec.getUserSyncs({iframeEnabled: false, pixelEnabled: true}, [serverResponse], {}, {}, {})).to.be.an('array').that.is.empty; + }); + it('Should return an empty array when iframeEnabled: true but serverResponses in an not defined or null', function () { + expect(spec.getUserSyncs({iframeEnabled: false, pixelEnabled: true}, undefined, {}, {}, {})).to.be.an('array').that.is.empty; + expect(spec.getUserSyncs({iframeEnabled: false, pixelEnabled: true}, null, {}, {}, {})).to.be.an('array').that.is.empty; + }); + it('Should return one user sync object when iframeEnabled is true and serverResponses is not an empry array', function () { + expect(spec.getUserSyncs({iframeEnabled: true, pixelEnabled: true}, [serverResponse], {}, {}, {})).to.be.an('array').that.is.not.empty; + }); + it('Should return a list containing a single object having type: iframe and url: syncUrl', function () { + const userSyncList = spec.getUserSyncs({iframeEnabled: true, pixelEnabled: true}, [serverResponse], undefined, undefined, undefined); + const { type, url } = userSyncList[0]; + expect(type).to.equal('iframe'); + expect(url).to.equal(UserSyncEndpoint); + }); + it('Should append gdpr: 0 if gdprConsent object is provided but gdprApplies field is not provided', function () { + const userSyncList = spec.getUserSyncs( + {iframeEnabled: true, pixelEnabled: true}, + [serverResponse], + {}, + undefined, + undefined + ); + const { url } = userSyncList[0]; + expect(url).to.equal(`${UserSyncEndpoint}?gdpr=0`); + }); + it('Should append gdpr having the value of gdprApplied if gdprConsent object is present and have gdprApplies field', function () { + const userSyncList = spec.getUserSyncs( + {iframeEnabled: true, pixelEnabled: true}, + [serverResponse], + {gdprApplies: true}, + undefined, + undefined + ); + const { url } = userSyncList[0]; + expect(url).to.equal(`${UserSyncEndpoint}?gdpr=1`); + }); + it('Should append gdpr_consent if gdprConsent object is present and have gdprApplies field', function () { + const userSyncList = spec.getUserSyncs( + {iframeEnabled: true, pixelEnabled: true}, + [serverResponse], + {gdprApplies: true, consentString: 'alabala'}, + undefined, + undefined + ); + const { url } = userSyncList[0]; + expect(url).to.equal(`${UserSyncEndpoint}?gdpr=1&gdpr_consent=alabala`); + }); + it('Should encodeURI gdpr_consent corectly', function () { + const userSyncList = spec.getUserSyncs( + {iframeEnabled: true, pixelEnabled: true}, + [serverResponse], + {gdprApplies: true, consentString: 'test&2'}, + undefined, + undefined + ); + const { url } = userSyncList[0]; + expect(url).to.equal(`${UserSyncEndpoint}?gdpr=1&gdpr_consent=test%262`); + }); + it('Should append usp_consent to the url if uspConsent is provided', function () { + const userSyncList = spec.getUserSyncs( + {iframeEnabled: true, pixelEnabled: true}, + [serverResponse], + {gdprApplies: true, consentString: 'test&2'}, + '1YYYN', + undefined + ); + const { url } = userSyncList[0]; + expect(url).to.equal(`${UserSyncEndpoint}?gdpr=1&gdpr_consent=test%262&us_privacy=1YYYN`); + }); + it('Should not modify the sync url if gppConsent param is provided', function () { + const userSyncList = spec.getUserSyncs( + {iframeEnabled: true, pixelEnabled: true}, + [serverResponse], + {gdprApplies: true, consentString: 'test&2'}, + '1YYYN', + {consent: '1'} + ); + const { url } = userSyncList[0]; + expect(url).to.equal(`${UserSyncEndpoint}?gdpr=1&gdpr_consent=test%262&us_privacy=1YYYN`); + }); + }); + + describe('getBidFloor', function () { + this.beforeEach(function () { + bid = mockBidRequest(); + }); + + it('Should return 0 if both getFloor method and bidfloor param from bid are absent.', function () { + const floor = connatixGetBidFloor(bid); + expect(floor).to.equal(0); + }); + + it('Should return the value of the bidfloor parameter if the getFloor method is not defined but the bidfloor parameter is defined', function () { + const floorValue = 3; + bid.params.bidfloor = floorValue; + + const floor = connatixGetBidFloor(bid); + expect(floor).to.equal(floorValue); + }); + + it('Should return the value of the getFloor method if the getFloor method is defined but the bidfloor parameter is not defined', function () { + const floorValue = 7; + bid.getFloor = function() { + return { floor: floorValue }; + }; + + const floor = connatixGetBidFloor(bid); + expect(floor).to.equal(floorValue); + }); + + it('Should return the value of the getFloor method if both getFloor method and bidfloor parameter are defined', function () { + const floorParamValue = 3; + bid.params.bidfloor = floorParamValue; + + const floorMethodValue = 7; + bid.getFloor = function() { + return { floor: floorMethodValue }; + }; + + const floor = connatixGetBidFloor(bid); + expect(floor).to.equal(floorMethodValue); + }); + + it('Should return 0 if the getFloor method is defined and it crash when call it', function () { + bid.getFloor = function() { + throw new Error('error'); + }; + const floor = connatixGetBidFloor(bid); + expect(floor).to.equal(0); + }); + }); +}); diff --git a/test/spec/modules/connectIdSystem_spec.js b/test/spec/modules/connectIdSystem_spec.js index 5376ba60886..686c3d63a63 100644 --- a/test/spec/modules/connectIdSystem_spec.js +++ b/test/spec/modules/connectIdSystem_spec.js @@ -3,6 +3,7 @@ import {connectIdSubmodule, storage} from 'modules/connectIdSystem.js'; import {server} from '../../mocks/xhr'; import {parseQS, parseUrl} from 'src/utils.js'; import {uspDataHandler, gppDataHandler} from 'src/adapterManager.js'; +import * as refererDetection from '../../../src/refererDetection'; const TEST_SERVER_URL = 'http://localhost:9876/'; @@ -288,6 +289,79 @@ describe('Yahoo ConnectID Submodule', () => { expect(setCookieStub.firstCall.args[2]).to.equal(expiryDelta.toUTCString()); }); + it('returns an object with the stored ID from cookies and syncs because of expired TTL', () => { + const last2Days = Date.now() - (60 * 60 * 24 * 1000 * 2); + const last21Days = Date.now() - (60 * 60 * 24 * 1000 * 21); + const ttl = 10000; + const cookieData = {connectId: 'foo', he: 'email', lastSynced: last2Days, puid: '9', lastUsed: last21Days, ttl}; + getCookieStub.withArgs(STORAGE_KEY).returns(JSON.stringify(cookieData)); + + let result = invokeGetIdAPI({ + he: HASHED_EMAIL, + pixelId: PIXEL_ID + }, consentData); + + expect(result).to.be.an('object').that.has.all.keys('id', 'callback'); + expect(result.id).to.deep.equal(cookieData); + expect(typeof result.callback).to.equal('function'); + }); + + it('returns an object with the stored ID from cookies and not syncs because of valid TTL', () => { + const last2Days = Date.now() - (60 * 60 * 24 * 1000 * 2); + const last21Days = Date.now() - (60 * 60 * 24 * 1000 * 21); + const ttl = 60 * 60 * 24 * 1000 * 3; + const cookieData = {connectId: 'foo', he: HASHED_EMAIL, lastSynced: last2Days, puid: '9', lastUsed: last21Days, ttl}; + getCookieStub.withArgs(STORAGE_KEY).returns(JSON.stringify(cookieData)); + + let result = invokeGetIdAPI({ + he: HASHED_EMAIL, + pixelId: PIXEL_ID + }, consentData); + + expect(result).to.be.an('object').that.has.all.keys('id'); + cookieData.lastUsed = result.id.lastUsed; + expect(result.id).to.deep.equal(cookieData); + }); + + it('returns an object with the stored ID from cookies and not syncs because of valid TTL with provided puid', () => { + const last2Days = Date.now() - (60 * 60 * 24 * 1000 * 2); + const last21Days = Date.now() - (60 * 60 * 24 * 1000 * 21); + const ttl = 60 * 60 * 24 * 1000 * 3; + const cookieData = {connectId: 'foo', he: HASHED_EMAIL, lastSynced: last2Days, puid: '9', lastUsed: last21Days, ttl}; + getCookieStub.withArgs(STORAGE_KEY).returns(JSON.stringify(cookieData)); + + let result = invokeGetIdAPI({ + he: HASHED_EMAIL, + pixelId: PIXEL_ID, + puid: '9' + }, consentData); + + expect(result).to.be.an('object').that.has.all.keys('id'); + cookieData.lastUsed = result.id.lastUsed; + expect(result.id).to.deep.equal(cookieData); + }); + + it('returns an object with the stored ID from cookies and syncs because is O&O traffic', () => { + const last2Days = Date.now() - (60 * 60 * 24 * 1000 * 2); + const last21Days = Date.now() - (60 * 60 * 24 * 1000 * 21); + const ttl = 60 * 60 * 24 * 1000 * 3; + const cookieData = {connectId: 'foo', he: HASHED_EMAIL, lastSynced: last2Days, puid: '9', lastUsed: last21Days, ttl}; + getCookieStub.withArgs(STORAGE_KEY).returns(JSON.stringify(cookieData)); + const getRefererInfoStub = sinon.stub(refererDetection, 'getRefererInfo'); + getRefererInfoStub.returns({ + ref: 'https://dev.fc.yahoo.com?test' + }); + let result = invokeGetIdAPI({ + he: HASHED_EMAIL, + pixelId: PIXEL_ID + }, consentData); + getRefererInfoStub.restore(); + + expect(result).to.be.an('object').that.has.all.keys('id', 'callback'); + expect(result.id).to.deep.equal(cookieData); + expect(typeof result.callback).to.equal('function'); + }); + it('Makes an ajax GET request to the production API endpoint with stored puid when id is stale', () => { const last15Days = Date.now() - (60 * 60 * 24 * 1000 * 15); const last29Days = Date.now() - (60 * 60 * 24 * 1000 * 29); diff --git a/test/spec/modules/consentManagementGpp_spec.js b/test/spec/modules/consentManagementGpp_spec.js index 1170f418caf..93a876d0233 100644 --- a/test/spec/modules/consentManagementGpp_spec.js +++ b/test/spec/modules/consentManagementGpp_spec.js @@ -1,12 +1,23 @@ -import { setConsentConfig, requestBidsHook, resetConsentData, userCMP, consentTimeout, staticConsentData } from 'modules/consentManagementGpp.js'; -import { gppDataHandler } from 'src/adapterManager.js'; +import { + consentTimeout, + GPPClient, + requestBidsHook, + resetConsentData, + setConsentConfig, + userCMP +} from 'modules/consentManagementGpp.js'; +import {gppDataHandler} from 'src/adapterManager.js'; import * as utils from 'src/utils.js'; -import { config } from 'src/config.js'; +import {config} from 'src/config.js'; import 'src/prebid.js'; +import {MODE_CALLBACK, MODE_MIXED} from '../../../libraries/cmp/cmpClient.js'; +import {GreedyPromise} from '../../../src/utils/promise.js'; let expect = require('chai').expect; describe('consentManagementGpp', function () { + beforeEach(resetConsentData); + describe('setConsentConfig tests:', function () { describe('empty setConsentConfig value', function () { beforeEach(function () { @@ -99,7 +110,7 @@ describe('consentManagementGpp', function () { config.resetConfig(); }); - it('results in user settings overriding system defaults for v2 spec', () => { + it('results in user settings overriding system defaults', () => { let staticConfig = { gpp: { cmpApi: 'static', @@ -109,22 +120,621 @@ describe('consentManagementGpp', function () { gppString: 'ABCDEFG1234', gppVersion: 1, sectionId: 3, - sectionList: [] + sectionList: [], + parsedSections: { + usnat: [ + { + MockUsnatParsedFlag: true + }, + ] + }, } } }; setConsentConfig(staticConfig); expect(userCMP).to.be.equal('static'); - expect(consentTimeout).to.be.equal(0); // should always return without a timeout when config is used const consent = gppDataHandler.getConsentData(); expect(consent.gppString).to.eql(staticConfig.gpp.consentData.gppString); - expect(consent.fullGppData).to.eql(staticConfig.gpp.consentData); - expect(staticConsentData).to.be.equal(staticConfig.gpp.consentData); + expect(consent.gppData).to.eql(staticConfig.gpp.consentData); + expect(consent.sectionData).to.eql(staticConfig.gpp.sectionData); + }); + }); + }); + describe('GPPClient.ping', () => { + function mkPingData(gppVersion) { + return { + gppVersion + } + } + Object.entries({ + 'unknown': { + expectedMode: MODE_CALLBACK, + pingData: mkPingData(), + apiVersion: '1.1', + client({callback}) { + callback(this.pingData); + } + }, + '1.0': { + expectedMode: MODE_MIXED, + pingData: mkPingData('1.0'), + apiVersion: '1.0', + client() { + return this.pingData; + } + }, + '1.1 that runs callback immediately': { + expectedMode: MODE_CALLBACK, + pingData: mkPingData('1.1'), + apiVersion: '1.1', + client({callback}) { + callback(this.pingData); + } + }, + '1.1 that defers callback': { + expectedMode: MODE_CALLBACK, + pingData: mkPingData('1.1'), + apiVersion: '1.1', + client({callback}) { + setTimeout(() => callback(this.pingData), 10); + } + }, + '> 1.1': { + expectedMode: MODE_CALLBACK, + pingData: mkPingData('1.2'), + apiVersion: '1.1', + client({callback}) { + setTimeout(() => callback(this.pingData), 10); + } + } + }).forEach(([t, scenario]) => { + describe(`using CMP version ${t}`, () => { + let clients, mkClient; + beforeEach(() => { + clients = []; + mkClient = ({mode}) => { + const mockClient = function (args) { + if (args.command === 'ping') { + return Promise.resolve(scenario.client(args)); + } + } + mockClient.mode = mode; + mockClient.close = sinon.stub(); + clients.push(mockClient); + return mockClient; + } + }); + + it('should resolve to client with the correct mode', () => { + return GPPClient.ping(mkClient).then(([client]) => { + expect(client.cmp.mode).to.eql(scenario.expectedMode); + }); + }); + + it('should resolve to pingData', () => { + return GPPClient.ping(mkClient).then(([_, pingData]) => { + expect(pingData).to.eql(scenario.pingData); + }); + }); + + it('should .close the probing client', () => { + return GPPClient.ping(mkClient).then(([client]) => { + sinon.assert.called(clients[0].close); + sinon.assert.notCalled(client.cmp.close); + }) + }); + + it('should .tag the client with version', () => { + return GPPClient.ping(mkClient).then(([client]) => { + expect(client.apiVersion).to.eql(scenario.apiVersion); + }) + }) + }) + }); + + it('should reject when mkClient returns null (CMP not found)', () => { + return GPPClient.ping(() => null).catch((err) => { + expect(err.message).to.match(/not found/); + }); + }); + + it('should reject when client rejects', () => { + const err = {some: 'prop'}; + const mockClient = () => Promise.reject(err); + mockClient.close = sinon.stub(); + return GPPClient.ping(() => mockClient).catch((result) => { + expect(result).to.eql(err); + sinon.assert.called(mockClient.close); + }); + }); + + it('should reject when callback is invoked with success = false', () => { + const err = 'error'; + const mockClient = ({callback}) => callback(err, false); + mockClient.close = sinon.stub(); + return GPPClient.ping(() => mockClient).catch((result) => { + expect(result).to.eql(err); + sinon.assert.called(mockClient.close); + }) + }) + }); + + describe('GPPClient.init', () => { + let makeCmp, cmpCalls, cmpResult; + + beforeEach(() => { + cmpResult = {signalStatus: 'ready', gppString: 'mock-str'}; + cmpCalls = []; + makeCmp = sinon.stub().callsFake(() => { + function mockCmp(args) { + cmpCalls.push(args); + return GreedyPromise.resolve(cmpResult); + } + mockCmp.close = sinon.stub(); + return mockCmp; + }); + }); + + it('should re-use same client', (done) => { + GPPClient.init(makeCmp).then(([client]) => { + GPPClient.init(makeCmp).then(([client2, consentPm]) => { + expect(client2).to.equal(client); + expect(cmpCalls.filter((el) => el.command === 'ping').length).to.equal(2) // recycled client should be refreshed + consentPm.then((consent) => { + expect(consent.gppString).to.eql('mock-str'); + done() + }) + }); + }); + }); + + it('should not re-use errors', (done) => { + cmpResult = GreedyPromise.reject(new Error()); + GPPClient.init(makeCmp).catch(() => { + cmpResult = {signalStatus: 'ready'}; + return GPPClient.init(makeCmp).then(([client]) => { + expect(client).to.exist; + done() + }) + }) + }) + }) + + describe('GPP client', () => { + const CHANGE_EVENTS = ['sectionChange', 'signalStatus']; + + let gppClient, gppData, cmpReady, eventListener; + + function mockClient(apiVersion = '1.1', cmpVersion = '1.1') { + const mockCmp = sinon.stub().callsFake(function ({command, callback}) { + if (command === 'addEventListener') { + eventListener = callback; + } else { + throw new Error('unexpected command: ' + command); + } + }) + const client = new GPPClient(cmpVersion, mockCmp); + client.apiVersion = apiVersion; + client.getGPPData = sinon.stub().callsFake(() => Promise.resolve(gppData)); + client.isCMPReady = sinon.stub().callsFake(() => cmpReady); + client.events = CHANGE_EVENTS; + return client; + } + + beforeEach(() => { + gppDataHandler.reset(); + eventListener = null; + cmpReady = true; + gppData = { + applicableSections: [7], + gppString: 'mock-string', + parsedSections: { + usnat: [ + { + Field: 'val' + }, + { + SubsectionType: 1, + Gpc: false + } + ] + } + }; + gppClient = mockClient(); + }); + + describe('updateConsent', () => { + it('should update data handler with consent data', () => { + return gppClient.updateConsent().then(data => { + sinon.assert.match(data, gppData); + sinon.assert.match(gppDataHandler.getConsentData(), gppData); + expect(gppDataHandler.ready).to.be.true; + }); + }); + + Object.entries({ + 'emtpy': {}, + 'missing': null + }).forEach(([t, data]) => { + it(`should not update, and reject promise, when gpp data is ${t}`, (done) => { + gppData = data; + gppClient.updateConsent().catch(err => { + expect(err.message).to.match(/empty/); + expect(err.args).to.eql(data == null ? [] : [data]); + expect(gppDataHandler.ready).to.be.false; + done() + }) + }); + }) + + it('should not update when gpp data rejects', (done) => { + gppData = Promise.reject(new Error('err')); + gppClient.updateConsent().catch(err => { + expect(gppDataHandler.ready).to.be.false; + expect(err.message).to.eql('err'); + done(); + }) + }); + + describe('consent data validation', () => { + Object.entries({ + applicableSections: { + 'not an array': 'not-an-array', + }, + gppString: { + 'not a string': 234 + }, + parsedSections: { + 'not an object': 'not-an-object' + } + }).forEach(([prop, tests]) => { + describe(`validation: when ${prop} is`, () => { + Object.entries(tests).forEach(([t, value]) => { + describe(t, () => { + it('should not update', (done) => { + Object.assign(gppData, {[prop]: value}); + gppClient.updateConsent().catch(err => { + expect(err.message).to.match(/unexpected/); + expect(err.args).to.eql([gppData]); + expect(gppDataHandler.ready).to.be.false; + done(); + }); + }); + }) + }); + }); + }); + }); + }); + + describe('init', () => { + beforeEach(() => { + gppClient.isCMPReady = function (pingData) { + return pingData.ready; + } + gppClient.getGPPData = function (pingData) { + return Promise.resolve(pingData); + } + }) + + it('does not use initial pingData if CMP is not ready', () => { + gppClient.init({...gppData, ready: false}); + expect(eventListener).to.exist; + expect(gppDataHandler.ready).to.be.false; + }); + + it('uses initial pingData (and resolves promise) if CMP is ready', () => { + return gppClient.init({...gppData, ready: true}).then(data => { + expect(eventListener).to.exist; + sinon.assert.match(data, gppData); + sinon.assert.match(gppDataHandler.getConsentData(), gppData); + }) + }); + + it('rejects promise when CMP errors out', (done) => { + gppClient.init({ready: false}).catch((err) => { + expect(err.message).to.match(/error/); + expect(err.args).to.eql(['error']) + done(); + }); + eventListener('error', false); }); + + Object.entries({ + 'empty': {}, + 'null': null, + 'irrelevant': {eventName: 'irrelevant'} + }).forEach(([t, evt]) => { + it(`ignores ${t} events`, () => { + let pm = gppClient.init({ready: false}).catch((err) => err.args[0] !== 'done' && Promise.reject(err)); + eventListener(evt); + eventListener('done', false); + return pm; + }) + }); + + it('rejects the promise when cmpStatus is "error"', (done) => { + const evt = {eventName: 'other', pingData: {cmpStatus: 'error'}}; + gppClient.init({ready: false}).catch(err => { + expect(err.message).to.match(/error/); + expect(err.args).to.eql([evt]); + done(); + }); + eventListener(evt); + }) + + CHANGE_EVENTS.forEach(evt => { + describe(`event: ${evt}`, () => { + function makeEvent(pingData) { + return { + eventName: evt, + pingData + } + } + + let gppData2 + beforeEach(() => { + gppData2 = Object.assign(gppData, {gppString: '2nd'}); + }); + + it('does not fire consent data updates if the CMP is not ready', (done) => { + gppClient.init({ready: false}).catch(() => { + expect(gppDataHandler.ready).to.be.false; + done(); + }); + eventListener({...gppData2, ready: false}); + eventListener('done', false); + }) + + it('fires consent data updates (and resolves promise) if CMP is ready', (done) => { + gppClient.init({ready: false}).then(data => { + sinon.assert.match(data, gppData2); + done() + }); + cmpReady = true; + eventListener(makeEvent({...gppData2, ready: true})); + }); + + it('keeps updating consent data on new events', () => { + let pm = gppClient.init({ready: false}).then(data => { + sinon.assert.match(data, gppData); + sinon.assert.match(gppDataHandler.getConsentData(), gppData); + }); + eventListener(makeEvent({...gppData, ready: true})); + return pm.then(() => { + eventListener(makeEvent({...gppData2, ready: true})) + }).then(() => { + sinon.assert.match(gppDataHandler.getConsentData(), gppData2); + }); + }); + }) + }) }); }); + describe('GPP 1.0 protocol', () => { + let mockCmp, gppClient; + beforeEach(() => { + mockCmp = sinon.stub(); + gppClient = new (GPPClient.getClient('1.0'))('1.0', mockCmp); + }); + + describe('isCMPReady', () => { + Object.entries({ + 'loaded': [true, 'loaded'], + 'other': [false, 'other'], + 'undefined': [false, undefined] + }).forEach(([t, [expected, cmpStatus]]) => { + it(`should be ${expected} when cmpStatus is ${t}`, () => { + expect(gppClient.isCMPReady(Object.assign({}, {cmpStatus}))).to.equal(expected); + }); + }); + }); + + describe('getGPPData', () => { + let gppData, pingData; + beforeEach(() => { + gppData = { + gppString: 'mock-string', + supportedAPIs: ['usnat'], + applicableSections: [7, 8] + } + pingData = { + supportedAPIs: gppData.supportedAPIs + }; + }); + + function mockCmpCommands(commands) { + mockCmp.callsFake(({command, parameter}) => { + if (commands.hasOwnProperty((command))) { + return Promise.resolve(commands[command](parameter)); + } else { + return Promise.reject(new Error(`unrecognized command ${command}`)) + } + }) + } + + it('should retrieve consent string and applicableSections', () => { + mockCmpCommands({ + getGPPData: () => gppData + }) + return gppClient.getGPPData(pingData).then(data => { + sinon.assert.match(data, gppData); + }) + }); + + it('should reject when getGPPData rejects', (done) => { + mockCmpCommands({ + getGPPData: () => Promise.reject(new Error('err')) + }); + gppClient.getGPPData(pingData).catch(err => { + expect(err.message).to.eql('err'); + done(); + }); + }); + + it('should not choke if supportedAPIs is missing', () => { + [gppData, pingData].forEach(ob => { delete ob.supportedAPIs; }) + mockCmpCommands({ + getGPPData: () => gppData + }); + return gppClient.getGPPData(pingData).then(res => { + expect(res.gppString).to.eql(gppData.gppString); + expect(res.parsedSections).to.eql({}); + }) + }) + + describe('section data', () => { + let usnat, parsedUsnat; + + function mockSections(sections) { + mockCmpCommands({ + getGPPData: () => gppData, + getSection: (api) => (sections[api]) + }); + }; + + beforeEach(() => { + usnat = { + MockField: 'val', + OtherField: 'o', + Gpc: true + }; + parsedUsnat = [ + { + MockField: 'val', + OtherField: 'o' + }, + { + SubsectionType: 1, + Gpc: true + } + ] + }); + + it('retrieves section data', () => { + mockSections({usnat}); + return gppClient.getGPPData(pingData).then(data => { + expect(data.parsedSections).to.eql({usnat: parsedUsnat}) + }); + }); + + it('does not choke if a section is missing', () => { + mockSections({usnat}); + gppData.supportedAPIs = ['usnat', 'missing']; + return gppClient.getGPPData(pingData).then(data => { + expect(data.parsedSections).to.eql({usnat: parsedUsnat}); + }) + }); + + it('does not choke if a section fails', () => { + mockSections({usnat, err: Promise.reject(new Error('err'))}); + gppData.supportedAPIs = ['usnat', 'err']; + return gppClient.getGPPData(pingData).then(data => { + expect(data.parsedSections).to.eql({usnat: parsedUsnat}); + }) + }); + }) + }); + }); + + describe('GPP 1.1 protocol', () => { + let mockCmp, gppClient; + beforeEach(() => { + mockCmp = sinon.stub(); + gppClient = new (GPPClient.getClient('1.1'))('1.1', mockCmp); + }); + + describe('isCMPReady', () => { + Object.entries({ + 'ready': [true, 'ready'], + 'not ready': [false, 'not ready'], + 'undefined': [false, undefined] + }).forEach(([t, [expected, signalStatus]]) => { + it(`should be ${expected} when signalStatus is ${t}`, () => { + expect(gppClient.isCMPReady(Object.assign({}, {signalStatus}))).to.equal(expected); + }); + }); + }); + + it('gets GPPData from pingData', () => { + mockCmp.throws(new Error()); + const pingData = { + 'gppVersion': '1.1', + 'cmpStatus': 'loaded', + 'cmpDisplayStatus': 'disabled', + 'supportedAPIs': [ + '5:tcfcav1', + '7:usnat', + '8:usca', + '9:usva', + '10:usco', + '11:usut', + '12:usct' + ], + 'signalStatus': 'ready', + 'cmpId': 31, + 'sectionList': [ + 7 + ], + 'applicableSections': [ + 7 + ], + 'gppString': 'DBABL~BAAAAAAAAgA.QA', + 'parsedSections': { + 'usnat': [ + { + 'Version': 1, + 'SharingNotice': 0, + 'SaleOptOutNotice': 0, + 'SharingOptOutNotice': 0, + 'TargetedAdvertisingOptOutNotice': 0, + 'SensitiveDataProcessingOptOutNotice': 0, + 'SensitiveDataLimitUseNotice': 0, + 'SaleOptOut': 0, + 'SharingOptOut': 0, + 'TargetedAdvertisingOptOut': 0, + 'SensitiveDataProcessing': [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + 'KnownChildSensitiveDataConsents': [ + 0, + 0 + ], + 'PersonalDataConsents': 0, + 'MspaCoveredTransaction': 2, + 'MspaOptOutOptionMode': 0, + 'MspaServiceProviderMode': 0 + }, + { + 'SubsectionType': 1, + 'Gpc': false + } + ] + } + }; + return gppClient.getGPPData(pingData).then((gppData) => { + sinon.assert.match(gppData, { + gppString: pingData.gppString, + applicableSections: pingData.applicableSections, + parsedSections: pingData.parsedSections + }) + }) + }) + }) + describe('requestBidsHook tests:', function () { let goodConfig = { gpp: { @@ -228,13 +838,13 @@ describe('consentManagementGpp', function () { }); it('should continue the auction immediately, without consent data, if timeout is 0', (done) => { + window.__gpp = function () {}; setConsentConfig({ gpp: { cmpApi: 'iab', timeout: 0 } }); - window.__gpp = function () {}; try { requestBidsHook(() => { const consent = gppDataHandler.getConsentData(); @@ -251,14 +861,16 @@ describe('consentManagementGpp', function () { describe('already known consentData:', function () { let cmpStub = sinon.stub(); - function mockCMP(cmpResponse) { - return function (...args) { - if (args[0] === 'addEventListener') { - args[1](({ - eventName: 'sectionChange' - })); - } else if (args[0] === 'getGPPData') { - return cmpResponse; + function mockCMP(pingData) { + return function (command, callback) { + switch (command) { + case 'addEventListener': + // eslint-disable-next-line standard/no-callback-literal + callback({eventName: 'sectionChange', pingData}) + break; + case 'ping': + callback(pingData) + break; } } } @@ -281,7 +893,7 @@ describe('consentManagementGpp', function () { gppString: 'xyz', }; - cmpStub = sinon.stub(window, '__gpp').callsFake(mockCMP(testConsentData)); + cmpStub = sinon.stub(window, '__gpp').callsFake(mockCMP({...testConsentData, signalStatus: 'ready'})); setConsentConfig(goodConfig); requestBidsHook(() => {}, {}); cmpStub.reset(); @@ -297,281 +909,5 @@ describe('consentManagementGpp', function () { sinon.assert.notCalled(cmpStub); }); }); - - describe('iframe tests', function () { - let cmpPostMessageCb = () => {}; - let stringifyResponse; - - function createIFrameMarker(frameName) { - let ifr = document.createElement('iframe'); - ifr.width = 0; - ifr.height = 0; - ifr.name = frameName; - document.body.appendChild(ifr); - return ifr; - } - - function creatCmpMessageHandler(prefix, returnEvtValue, returnGPPValue) { - return function (event) { - if (event && event.data) { - let data = event.data; - if (data[`${prefix}Call`]) { - let callId = data[`${prefix}Call`].callId; - let response; - if (data[`${prefix}Call`].command === 'addEventListener') { - response = { - [`${prefix}Return`]: { - callId, - returnValue: returnEvtValue, - success: true - } - } - } else if (data[`${prefix}Call`].command === 'getGPPData') { - response = { - [`${prefix}Return`]: { - callId, - returnValue: returnGPPValue, - success: true - } - } - } - event.source.postMessage(stringifyResponse ? JSON.stringify(response) : response, '*'); - } - } - } - } - - function testIFramedPage(testName, messageFormatString, tarConsentString, tarSections) { - it(`should return the consent string from a postmessage + addEventListener response - ${testName}`, (done) => { - stringifyResponse = messageFormatString; - setConsentConfig(goodConfig); - requestBidsHook(() => { - let consent = gppDataHandler.getConsentData(); - sinon.assert.notCalled(utils.logError); - expect(consent.gppString).to.equal(tarConsentString); - expect(consent.applicableSections).to.deep.equal(tarSections); - done(); - }, {}); - }); - } - - beforeEach(function () { - sinon.stub(utils, 'logError'); - sinon.stub(utils, 'logWarn'); - }); - - afterEach(function () { - utils.logError.restore(); - utils.logWarn.restore(); - config.resetConfig(); - resetConsentData(); - }); - - describe('v2 CMP workflow for iframe pages:', function () { - stringifyResponse = false; - let ifr2 = null; - - beforeEach(function () { - ifr2 = createIFrameMarker('__gppLocator'); - cmpPostMessageCb = creatCmpMessageHandler('__gpp', { - eventName: 'sectionChange' - }, { - gppString: 'abc12345234', - applicableSections: [7] - }); - window.addEventListener('message', cmpPostMessageCb, false); - }); - - afterEach(function () { - delete window.__gpp; // deletes the local copy made by the postMessage CMP call function - document.body.removeChild(ifr2); - window.removeEventListener('message', cmpPostMessageCb); - }); - - testIFramedPage('with/JSON response', false, 'abc12345234', [7]); - testIFramedPage('with/String response', true, 'abc12345234', [7]); - }); - }); - - describe('direct calls to CMP API tests', function () { - let cmpStub = sinon.stub(); - - beforeEach(function () { - didHookReturn = false; - sinon.stub(utils, 'logError'); - sinon.stub(utils, 'logWarn'); - }); - - afterEach(function () { - config.resetConfig(); - cmpStub.restore(); - utils.logError.restore(); - utils.logWarn.restore(); - resetConsentData(); - }); - - describe('v2 CMP workflow for normal pages:', function () { - beforeEach(function () { - window.__gpp = function () {}; - }); - - afterEach(function () { - delete window.__gpp; - }); - - it('performs lookup check and stores consentData for a valid existing user', function () { - let testConsentData = { - gppString: 'abc12345234', - applicableSections: [7] - }; - cmpStub = sinon.stub(window, '__gpp').callsFake((...args) => { - if (args[0] === 'addEventListener') { - args[1]({ - eventName: 'sectionChange' - }); - } else if (args[0] === 'getGPPData') { - return testConsentData; - } - }); - - setConsentConfig(goodConfig); - - requestBidsHook(() => { - didHookReturn = true; - }, {}); - let consent = gppDataHandler.getConsentData(); - sinon.assert.notCalled(utils.logError); - expect(didHookReturn).to.be.true; - expect(consent.gppString).to.equal(testConsentData.gppString); - expect(consent.applicableSections).to.deep.equal(testConsentData.applicableSections); - }); - - it('produces gdpr metadata', function () { - let testConsentData = { - gppString: 'abc12345234', - applicableSections: [7] - }; - cmpStub = sinon.stub(window, '__gpp').callsFake((...args) => { - if (args[0] === 'addEventListener') { - args[1]({ - eventName: 'sectionChange' - }); - } else if (args[0] === 'getGPPData') { - return testConsentData; - } - }); - - setConsentConfig(goodConfig); - - requestBidsHook(() => { - didHookReturn = true; - }, {}); - let consentMeta = gppDataHandler.getConsentMeta(); - sinon.assert.notCalled(utils.logError); - expect(consentMeta.generatedAt).to.be.above(1644367751709); - }); - - it('throws an error when processCmpData check fails + does not call requestBids callback', function () { - let testConsentData = {}; - let bidsBackHandlerReturn = false; - - cmpStub = sinon.stub(window, '__gpp').callsFake((...args) => { - if (args[0] === 'addEventListener') { - args[1]({ - eventName: 'sectionChange' - }); - } else if (args[0] === 'getGPPData') { - return testConsentData; - } - }); - - setConsentConfig(goodConfig); - - sinon.assert.notCalled(utils.logWarn); - sinon.assert.notCalled(utils.logError); - - [utils.logWarn, utils.logError].forEach((stub) => stub.reset()); - - requestBidsHook(() => { - didHookReturn = true; - }, { - bidsBackHandler: () => bidsBackHandlerReturn = true - }); - let consent = gppDataHandler.getConsentData(); - - sinon.assert.calledOnce(utils.logError); - sinon.assert.notCalled(utils.logWarn); - expect(didHookReturn).to.be.false; - expect(bidsBackHandlerReturn).to.be.true; - expect(consent).to.be.null; - expect(gppDataHandler.ready).to.be.true; - }); - - describe('when proper consent is not available', () => { - let gppStub; - - function runAuction() { - setConsentConfig({ - gpp: { - cmpApi: 'iab', - timeout: 10, - } - }); - return new Promise((resolve, reject) => { - requestBidsHook(() => { - didHookReturn = true; - }, {}); - setTimeout(() => didHookReturn ? resolve() : reject(new Error('Auction did not run')), 20); - }) - } - - function mockGppCmp(gppdata) { - gppStub.callsFake((api, cb) => { - if (api === 'addEventListener') { - // eslint-disable-next-line standard/no-callback-literal - cb({ - pingData: { - cmpStatus: 'loaded' - } - }, true); - } - if (api === 'getGPPData') { - return gppdata; - } - }); - } - - beforeEach(() => { - gppStub = sinon.stub(window, '__gpp'); - }); - - afterEach(() => { - gppStub.restore(); - }) - - it('should continue auction with null consent when CMP is unresponsive', () => { - return runAuction().then(() => { - const consent = gppDataHandler.getConsentData(); - expect(consent.applicableSections).to.deep.equal([]); - expect(consent.gppString).to.be.undefined; - expect(gppDataHandler.ready).to.be.true; - }); - }); - - it('should use consent provided by events other than sectionChange', () => { - mockGppCmp({ - gppString: 'mock-consent-string', - applicableSections: [7] - }); - return runAuction().then(() => { - const consent = gppDataHandler.getConsentData(); - expect(consent.applicableSections).to.deep.equal([7]); - expect(consent.gppString).to.equal('mock-consent-string'); - expect(gppDataHandler.ready).to.be.true; - }); - }); - }); - }); - }); }); }); diff --git a/test/spec/modules/consentManagementUsp_spec.js b/test/spec/modules/consentManagementUsp_spec.js index e98486754ab..c372c66f7f0 100644 --- a/test/spec/modules/consentManagementUsp_spec.js +++ b/test/spec/modules/consentManagementUsp_spec.js @@ -522,6 +522,19 @@ describe('consentManagement', function () { setConsentConfig(goodConfig); expect(uspDataHandler.getConsentData()).to.eql('string'); }); + + it('does not invoke registerDeletion if the CMP calls back with an error', () => { + sandbox.stub(window, '__uspapi').callsFake((cmd, _, cb) => { + if (cmd === 'registerDeletion') { + cb(null, false); + } else { + // eslint-disable-next-line standard/no-callback-literal + cb({uspString: 'string'}, true); + } + }); + setConsentConfig(goodConfig); + sinon.assert.notCalled(adapterManager.callDataDeletionRequest); + }) }); }); }); diff --git a/test/spec/modules/consentManagement_spec.js b/test/spec/modules/consentManagement_spec.js index 2cd3d011e1d..c1ed042a2c8 100644 --- a/test/spec/modules/consentManagement_spec.js +++ b/test/spec/modules/consentManagement_spec.js @@ -1,18 +1,17 @@ import { - setConsentConfig, - requestBidsHook, - resetConsentData, - userCMP, - consentTimeout, actionTimeout, - staticConsentData, + consentTimeout, gdprScope, loadConsentData, - setActionTimeout + requestBidsHook, + resetConsentData, + setConsentConfig, + staticConsentData, + userCMP } from 'modules/consentManagement.js'; -import { gdprDataHandler } from 'src/adapterManager.js'; +import {gdprDataHandler} from 'src/adapterManager.js'; import * as utils from 'src/utils.js'; -import { config } from 'src/config.js'; +import {config} from 'src/config.js'; import 'src/prebid.js'; let expect = require('chai').expect; @@ -487,15 +486,6 @@ describe('consentManagement', function () { testIFramedPage('with/JSON response', false, 'abc12345234', 2); testIFramedPage('with/String response', true, 'abc12345234', 2); - - it('should contain correct v2 CMP definition', (done) => { - setConsentConfig(goodConfig); - requestBidsHook(() => { - const nbArguments = window.__tcfapi.toString().split('\n')[0].split(', ').length; - expect(nbArguments).to.equal(4); - done(); - }, {}); - }); }); }); diff --git a/test/spec/modules/consumableBidAdapter_spec.js b/test/spec/modules/consumableBidAdapter_spec.js index 556dce447b9..d8e75454245 100644 --- a/test/spec/modules/consumableBidAdapter_spec.js +++ b/test/spec/modules/consumableBidAdapter_spec.js @@ -53,6 +53,10 @@ const BIDDER_REQUEST_1 = { consentString: 'consent-test', gdprApplies: false }, + gppConsent: { + applicableSections: [1, 2], + gppString: 'consent-string' + }, refererInfo: { referer: 'http://example.com/page.html', reachedTop: true, @@ -647,10 +651,30 @@ describe('Consumable BidAdapter', function () { expect(opts[0].url).to.equal('https://sync.serverbid.com/ss/730181.html?gdpr=0&gdpr_consent=GDPR_CONSENT_STRING'); }) - it('should return a sync url if iframe syncs are enabled and USP applies', function () { - let uspConsent = { - consentString: 'USP_CONSENT_STRING', + it('should return a sync url if iframe syncs are enabled and has GPP consent with applicable sections', function () { + let gppConsent = { + applicableSections: [1, 2], + gppString: 'GPP_CONSENT_STRING' + } + let opts = spec.getUserSyncs(syncOptions, [AD_SERVER_RESPONSE], {}, '', gppConsent); + + expect(opts.length).to.equal(1); + expect(opts[0].url).to.equal('https://sync.serverbid.com/ss/730181.html?gpp=GPP_CONSENT_STRING&gpp_sid=1%2C2'); + }) + + it('should return a sync url if iframe syncs are enabled and has GPP consent without applicable sections', function () { + let gppConsent = { + applicableSections: [], + gppString: 'GPP_CONSENT_STRING' } + let opts = spec.getUserSyncs(syncOptions, [AD_SERVER_RESPONSE], {}, '', gppConsent); + + expect(opts.length).to.equal(1); + expect(opts[0].url).to.equal('https://sync.serverbid.com/ss/730181.html?gpp=GPP_CONSENT_STRING'); + }) + + it('should return a sync url if iframe syncs are enabled and USP applies', function () { + let uspConsent = 'USP_CONSENT_STRING'; let opts = spec.getUserSyncs(syncOptions, [AD_SERVER_RESPONSE], {}, uspConsent); expect(opts.length).to.equal(1); @@ -662,9 +686,7 @@ describe('Consumable BidAdapter', function () { consentString: 'GDPR_CONSENT_STRING', gdprApplies: true, } - let uspConsent = { - consentString: 'USP_CONSENT_STRING', - } + let uspConsent = 'USP_CONSENT_STRING'; let opts = spec.getUserSyncs(syncOptions, [AD_SERVER_RESPONSE], gdprConsent, uspConsent); expect(opts.length).to.equal(1); @@ -689,50 +711,22 @@ describe('Consumable BidAdapter', function () { sandbox.restore(); }); - it('Request should have unifiedId config params', function() { + it('Request should have EIDs', function() { bidderRequest.bidRequest[0].userId = {}; bidderRequest.bidRequest[0].userId.tdid = 'TTD_ID'; - bidderRequest.bidRequest[0].userIdAsEids = createEidsArray(bidderRequest.bidRequest[0].userId); - let request = spec.buildRequests(bidderRequest.bidRequest, BIDDER_REQUEST_1); - let data = JSON.parse(request.data); - expect(data.user.eids).to.deep.equal([{ + bidderRequest.bidRequest[0].userIdAsEids = [{ 'source': 'adserver.org', 'uids': [{ - 'id': 'TTD_ID', + 'id': 'TTD_ID_FROM_USER_ID_MODULE', 'atype': 1, 'ext': { 'rtiPartner': 'TDID' } }] - }]); - }); - - it('Request should have adsrvrOrgId from UserId Module if config and userId module both have TTD ID', function() { - sandbox.stub(config, 'getConfig').callsFake((key) => { - var config = { - adsrvrOrgId: { - 'TDID': 'TTD_ID_FROM_CONFIG', - 'TDID_LOOKUP': 'TRUE', - 'TDID_CREATED_AT': '2022-06-21T09:47:00' - } - }; - return config[key]; - }); - bidderRequest.bidRequest[0].userId = {}; - bidderRequest.bidRequest[0].userId.tdid = 'TTD_ID'; - bidderRequest.bidRequest[0].userIdAsEids = createEidsArray(bidderRequest.bidRequest[0].userId); + }]; let request = spec.buildRequests(bidderRequest.bidRequest, BIDDER_REQUEST_1); let data = JSON.parse(request.data); - expect(data.user.eids).to.deep.equal([{ - 'source': 'adserver.org', - 'uids': [{ - 'id': 'TTD_ID', - 'atype': 1, - 'ext': { - 'rtiPartner': 'TDID' - } - }] - }]); + expect(data.user.eids).to.deep.equal(bidderRequest.bidRequest[0].userIdAsEids); }); it('Request should NOT have adsrvrOrgId params if userId is NOT object', function() { diff --git a/test/spec/modules/contxtfulRtdProvider_spec.js b/test/spec/modules/contxtfulRtdProvider_spec.js new file mode 100644 index 00000000000..541c0e6e6dd --- /dev/null +++ b/test/spec/modules/contxtfulRtdProvider_spec.js @@ -0,0 +1,200 @@ +import { contxtfulSubmodule } from '../../../modules/contxtfulRtdProvider.js'; +import { expect } from 'chai'; +import { loadExternalScriptStub } from 'test/mocks/adloaderStub.js'; + +import * as events from '../../../src/events'; + +const _ = null; +const VERSION = 'v1'; +const CUSTOMER = 'CUSTOMER'; +const CONTXTFUL_CONNECTOR_ENDPOINT = `https://api.receptivity.io/${VERSION}/prebid/${CUSTOMER}/connector/p.js`; +const INITIAL_RECEPTIVITY = { ReceptivityState: 'INITIAL_RECEPTIVITY' }; +const INITIAL_RECEPTIVITY_EVENT = new CustomEvent('initialReceptivity', { detail: INITIAL_RECEPTIVITY }); + +const CONTXTFUL_API = { GetReceptivity: sinon.stub() } +const RX_ENGINE_IS_READY_EVENT = new CustomEvent('rxEngineIsReady', {detail: CONTXTFUL_API}); + +function buildInitConfig(version, customer) { + return { + name: 'contxtful', + params: { + version, + customer, + }, + }; +} + +describe('contxtfulRtdProvider', function () { + let sandbox = sinon.sandbox.create(); + let loadExternalScriptTag; + let eventsEmitSpy; + + beforeEach(() => { + loadExternalScriptTag = document.createElement('script'); + loadExternalScriptStub.callsFake((_url, _moduleName) => loadExternalScriptTag); + + CONTXTFUL_API.GetReceptivity.reset(); + + eventsEmitSpy = sandbox.spy(events, ['emit']); + }); + + afterEach(function () { + delete window.Contxtful; + sandbox.restore(); + }); + + describe('extractParameters with invalid configuration', () => { + const { + params: { customer, version }, + } = buildInitConfig(VERSION, CUSTOMER); + const theories = [ + [ + null, + 'params.version should be a non-empty string', + 'null object for config', + ], + [ + {}, + 'params.version should be a non-empty string', + 'empty object for config', + ], + [ + { customer }, + 'params.version should be a non-empty string', + 'customer only in config', + ], + [ + { version }, + 'params.customer should be a non-empty string', + 'version only in config', + ], + [ + { customer, version: '' }, + 'params.version should be a non-empty string', + 'empty string for version', + ], + [ + { customer: '', version }, + 'params.customer should be a non-empty string', + 'empty string for customer', + ], + [ + { customer: '', version: '' }, + 'params.version should be a non-empty string', + 'empty string for version & customer', + ], + ]; + + theories.forEach(([params, expectedErrorMessage, _description]) => { + const config = { name: 'contxtful', params }; + it('throws the expected error', () => { + expect(() => contxtfulSubmodule.extractParameters(config)).to.throw( + expectedErrorMessage + ); + }); + }); + }); + + describe('initialization with invalid config', function () { + it('returns false', () => { + expect(contxtfulSubmodule.init({})).to.be.false; + }); + }); + + describe('initialization with valid config', function () { + it('returns true when initializing', () => { + const config = buildInitConfig(VERSION, CUSTOMER); + expect(contxtfulSubmodule.init(config)).to.be.true; + }); + + it('loads contxtful module script asynchronously', (done) => { + contxtfulSubmodule.init(buildInitConfig(VERSION, CUSTOMER)); + + setTimeout(() => { + expect(loadExternalScriptStub.calledOnce).to.be.true; + expect(loadExternalScriptStub.args[0][0]).to.equal( + CONTXTFUL_CONNECTOR_ENDPOINT + ); + done(); + }, 10); + }); + }); + + describe('load external script return falsy', function () { + it('returns true when initializing', () => { + loadExternalScriptStub.callsFake(() => {}); + const config = buildInitConfig(VERSION, CUSTOMER); + expect(contxtfulSubmodule.init(config)).to.be.true; + }); + }); + + describe('rxEngine from external script', function () { + it('use rxEngine api to get receptivity', () => { + contxtfulSubmodule.init(buildInitConfig(VERSION, CUSTOMER)); + loadExternalScriptTag.dispatchEvent(RX_ENGINE_IS_READY_EVENT); + + contxtfulSubmodule.getTargetingData(['ad-slot']); + + expect(CONTXTFUL_API.GetReceptivity.calledOnce).to.be.true; + }); + }); + + describe('initial receptivity is not dispatched', function () { + it('does not initialize receptivity value', () => { + contxtfulSubmodule.init(buildInitConfig(VERSION, CUSTOMER)); + + let targetingData = contxtfulSubmodule.getTargetingData(['ad-slot']); + expect(targetingData).to.deep.equal({}); + }); + }); + + describe('initial receptivity is invalid', function () { + const theories = [ + [new Event('initialReceptivity'), 'event without details'], + [new CustomEvent('initialReceptivity', { }), 'custom event without details'], + [new CustomEvent('initialReceptivity', { detail: {} }), 'custom event with invalid details'], + [new CustomEvent('initialReceptivity', { detail: { ReceptivityState: '' } }), 'custom event with details without ReceptivityState'], + ]; + + theories.forEach(([initialReceptivityEvent, _description]) => { + it('does not initialize receptivity value', () => { + contxtfulSubmodule.init(buildInitConfig(VERSION, CUSTOMER)); + loadExternalScriptTag.dispatchEvent(initialReceptivityEvent); + + let targetingData = contxtfulSubmodule.getTargetingData(['ad-slot']); + expect(targetingData).to.deep.equal({}); + }); + }) + }); + + describe('getTargetingData', function () { + const theories = [ + [undefined, {}, 'undefined ad-slots'], + [[], {}, 'empty ad-slots'], + [ + ['ad-slot'], + { 'ad-slot': { ReceptivityState: 'INITIAL_RECEPTIVITY' } }, + 'single ad-slot', + ], + [ + ['ad-slot-1', 'ad-slot-2'], + { + 'ad-slot-1': { ReceptivityState: 'INITIAL_RECEPTIVITY' }, + 'ad-slot-2': { ReceptivityState: 'INITIAL_RECEPTIVITY' }, + }, + 'many ad-slots', + ], + ]; + + theories.forEach(([adUnits, expected, _description]) => { + it('adds "ReceptivityState" to the adUnits', function () { + contxtfulSubmodule.init(buildInitConfig(VERSION, CUSTOMER)); + loadExternalScriptTag.dispatchEvent(INITIAL_RECEPTIVITY_EVENT); + + expect(contxtfulSubmodule.getTargetingData(adUnits)).to.deep.equal( + expected + ); + }); + }); + }); +}); diff --git a/test/spec/modules/conversantAnalyticsAdapter_spec.js b/test/spec/modules/conversantAnalyticsAdapter_spec.js index ce134f7f6af..f425535ce73 100644 --- a/test/spec/modules/conversantAnalyticsAdapter_spec.js +++ b/test/spec/modules/conversantAnalyticsAdapter_spec.js @@ -3,6 +3,7 @@ import {expect} from 'chai'; import {default as conversantAnalytics, CNVR_CONSTANTS, cnvrHelper} from 'modules/conversantAnalyticsAdapter'; import * as utils from 'src/utils.js'; import * as prebidGlobal from 'src/prebidGlobal'; +import {server} from '../../mocks/xhr.js'; import constants from 'src/constants.json' @@ -10,14 +11,13 @@ let events = require('src/events'); describe('Conversant analytics adapter tests', function() { let sandbox; // sinon sandbox to make restoring all stubbed objects easier - let xhr; // xhr stub from sinon for capturing data sent via ajax let clock; // clock stub from sinon to mock our cache cleanup interval let logInfoStub; const PREBID_VERSION = '1.2'; const SITE_ID = 108060; - let requests = []; + let requests; const DATESTAMP = Date.now(); const VALID_CONFIGURATION = { @@ -36,10 +36,9 @@ describe('Conversant analytics adapter tests', function() { }; beforeEach(function () { + requests = server.requests; sandbox = sinon.sandbox.create(); sandbox.stub(events, 'getEvents').returns([]); // need to stub this otherwise unwanted events seem to get fired during testing - xhr = sandbox.useFakeXMLHttpRequest(); // allows us to capture ajax requests - xhr.onCreate = function (req) { requests.push(req); }; // save ajax requests in a private array for testing purposes let getGlobalStub = { version: PREBID_VERSION, getUserIds: function() { // userIdTargeting.js init() gets called on AUCTION_END so we need to mock this function. @@ -60,7 +59,6 @@ describe('Conversant analytics adapter tests', function() { afterEach(function () { sandbox.restore(); - requests = []; // clean up any requests in our ajax request capture array. conversantAnalytics.disableAnalytics(); }); diff --git a/test/spec/modules/conversantBidAdapter_spec.js b/test/spec/modules/conversantBidAdapter_spec.js index 59ebefa2d60..73d1978a9d9 100644 --- a/test/spec/modules/conversantBidAdapter_spec.js +++ b/test/spec/modules/conversantBidAdapter_spec.js @@ -2,13 +2,20 @@ import {expect} from 'chai'; import {spec, storage} from 'modules/conversantBidAdapter.js'; import * as utils from 'src/utils.js'; import {createEidsArray} from 'modules/userId/eids.js'; -import { config } from '../../../src/config.js'; import {deepAccess} from 'src/utils'; +// load modules that register ORTB processors +import 'src/prebid.js' +import 'modules/currency.js'; +import 'modules/userId/index.js'; // handles eids +import 'modules/priceFloors.js'; +import 'modules/consentManagement.js'; +import 'modules/consentManagementUsp.js'; +import 'modules/schain.js'; // handles schain +import {hook} from '../../../src/hook.js' describe('Conversant adapter tests', function() { const siteId = '108060'; const versionPattern = /^\d+\.\d+\.\d+(.)*$/; - const bidRequests = [ // banner with single size { @@ -19,13 +26,18 @@ describe('Conversant adapter tests', function() { tag_id: 'tagid-1', bidfloor: 0.5 }, + mediaTypes: { + banner: { + sizes: [[300, 250]], + } + }, placementCode: 'pcode000', transactionId: 'tx000', - sizes: [[300, 250]], bidId: 'bid000', bidderRequestId: '117d765b87bed38', auctionId: 'req000' }, + // banner with sizes in mediaTypes.banner.sizes { bidder: 'conversant', @@ -51,9 +63,13 @@ describe('Conversant adapter tests', function() { position: 2, tag_id: '' }, + mediaTypes: { + banner: { + sizes: [[300, 600], [160, 600]], + } + }, placementCode: 'pcode002', transactionId: 'tx002', - sizes: [[300, 600], [160, 600]], bidId: 'bid002', bidderRequestId: '117d765b87bed38', auctionId: 'req000' @@ -77,7 +93,6 @@ describe('Conversant adapter tests', function() { }, placementCode: 'pcode003', transactionId: 'tx003', - sizes: [640, 480], bidId: 'bid003', bidderRequestId: '117d765b87bed38', auctionId: 'req000' @@ -125,16 +140,15 @@ describe('Conversant adapter tests', function() { bidderRequestId: '117d765b87bed38', auctionId: 'req000' }, - // video with first party data + // banner with first party data { bidder: 'conversant', params: { site_id: siteId }, mediaTypes: { - video: { - context: 'instream', - mimes: ['video/mp4', 'video/x-flv'] + banner: { + sizes: [[300, 600], [160, 600]], } }, ortb2Imp: { @@ -150,23 +164,6 @@ describe('Conversant adapter tests', function() { bidId: 'bid006', bidderRequestId: '117d765b87bed38', auctionId: 'req000' - }, - { - bidder: 'conversant', - params: { - site_id: siteId - }, - mediaTypes: { - banner: { - sizes: [[728, 90], [468, 60]], - pos: 5 - } - }, - placementCode: 'pcode001', - transactionId: 'tx001', - bidId: 'bid007', - bidderRequestId: '117d765b87bed38', - auctionId: 'req000' } ]; @@ -217,7 +214,14 @@ describe('Conversant adapter tests', function() { }] }] }, - headers: {}}; + headers: {} + }; + + before(() => { + // ortbConverter depends on other modules to be setup to work as expected so run hook.ready to register some + // submodules so functions like setOrtbSourceExtSchain and setOrtbUserExtEids are available + hook.ready(); + }); it('Verify basic properties', function() { expect(spec.code).to.equal('conversant'); @@ -232,12 +236,9 @@ describe('Conversant adapter tests', function() { expect(spec.isBidRequestValid({})).to.be.false; expect(spec.isBidRequestValid({params: {}})).to.be.false; expect(spec.isBidRequestValid({params: {site_id: '123'}})).to.be.true; - expect(spec.isBidRequestValid(bidRequests[0])).to.be.true; - expect(spec.isBidRequestValid(bidRequests[1])).to.be.true; - expect(spec.isBidRequestValid(bidRequests[2])).to.be.true; - expect(spec.isBidRequestValid(bidRequests[3])).to.be.true; - expect(spec.isBidRequestValid(bidRequests[4])).to.be.true; - expect(spec.isBidRequestValid(bidRequests[5])).to.be.true; + bidRequests.forEach((bid) => { + expect(spec.isBidRequestValid(bid)).to.be.true; + }); const simpleVideo = JSON.parse(JSON.stringify(bidRequests[3])); simpleVideo.params.site_id = 123; @@ -251,152 +252,171 @@ describe('Conversant adapter tests', function() { expect(spec.isBidRequestValid(simpleVideo)).to.be.true; }); - it('Verify buildRequest', function() { - const page = 'http://test.com?a=b&c=123'; - const bidderRequest = { - refererInfo: { - page: page - }, - ortb2: { - source: { - tid: 'tid000' + describe('Verify buildRequest', function() { + let page, bidderRequest, request, payload; + before(() => { + page = 'http://test.com?a=b&c=123'; + // ortbConverter uses the site/device information from the ortb2 object passed in the bidderRequest object + bidderRequest = { + refererInfo: { + page: page + }, + ortb2: { + source: { + tid: 'tid000' + }, + site: { + mobile: 0, + page: page, + }, + device: { + w: screen.width, + h: screen.height, + dnt: 0, + ua: navigator.userAgent + } } - } - }; - const request = spec.buildRequests(bidRequests, bidderRequest); - expect(request.method).to.equal('POST'); - expect(request.url).to.equal('https://web.hb.ad.cpe.dotomi.com/cvx/client/hb/ortb/25'); - const payload = request.data; + }; + request = spec.buildRequests(bidRequests, bidderRequest); + payload = request.data; + }); + + it('Verify common elements', function() { + expect(request.method).to.equal('POST'); + expect(request.url).to.equal('https://web.hb.ad.cpe.dotomi.com/cvx/client/hb/ortb/25'); + + expect(payload).to.have.property('id'); + expect(payload.source).to.have.property('tid', 'tid000'); + expect(payload).to.have.property('at', 1); + expect(payload).to.have.property('imp'); + expect(payload.imp).to.be.an('array').with.lengthOf(bidRequests.length); + + expect(payload).to.have.property('site'); + expect(payload.site).to.have.property('id', siteId); + expect(payload.site).to.have.property('mobile').that.is.oneOf([0, 1]); + + expect(payload.site).to.have.property('page', page); + + expect(payload).to.have.property('device'); + expect(payload.device).to.have.property('w', screen.width); + expect(payload.device).to.have.property('h', screen.height); + expect(payload.device).to.have.property('dnt').that.is.oneOf([0, 1]); + 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 + }); - expect(payload).to.have.property('id'); - expect(payload.source).to.have.property('tid', 'tid000'); - expect(payload).to.have.property('at', 1); - expect(payload).to.have.property('imp'); - expect(payload.imp).to.be.an('array').with.lengthOf(8); - - expect(payload.imp[0]).to.have.property('id', 'bid000'); - expect(payload.imp[0]).to.have.property('secure', 1); - expect(payload.imp[0]).to.have.property('bidfloor', 0.5); - expect(payload.imp[0]).to.have.property('displaymanager', 'Prebid.js'); - expect(payload.imp[0]).to.have.property('displaymanagerver').that.matches(versionPattern); - expect(payload.imp[0]).to.have.property('tagid', 'tagid-1'); - expect(payload.imp[0]).to.have.property('banner'); - expect(payload.imp[0].banner).to.have.property('pos', 1); - expect(payload.imp[0].banner).to.have.property('format'); - expect(payload.imp[0].banner.format).to.deep.equal([{w: 300, h: 250}]); - expect(payload.imp[0]).to.not.have.property('video'); - - expect(payload.imp[1]).to.have.property('id', 'bid001'); - expect(payload.imp[1]).to.have.property('secure', 1); - expect(payload.imp[1]).to.have.property('bidfloor', 0); - expect(payload.imp[1]).to.have.property('displaymanager', 'Prebid.js'); - expect(payload.imp[1]).to.have.property('displaymanagerver').that.matches(versionPattern); - expect(payload.imp[1]).to.not.have.property('tagid'); - expect(payload.imp[1]).to.have.property('banner'); - expect(payload.imp[1].banner).to.not.have.property('pos'); - expect(payload.imp[1].banner).to.have.property('format'); - expect(payload.imp[1].banner.format).to.deep.equal([{w: 728, h: 90}, {w: 468, h: 60}]); - - expect(payload.imp[2]).to.have.property('id', 'bid002'); - expect(payload.imp[2]).to.have.property('secure', 1); - expect(payload.imp[2]).to.have.property('bidfloor', 0); - expect(payload.imp[2]).to.have.property('displaymanager', 'Prebid.js'); - expect(payload.imp[2]).to.have.property('displaymanagerver').that.matches(versionPattern); - expect(payload.imp[2]).to.have.property('banner'); - expect(payload.imp[2].banner).to.have.property('pos', 2); - expect(payload.imp[2].banner).to.have.property('format'); - expect(payload.imp[2].banner.format).to.deep.equal([{w: 300, h: 600}, {w: 160, h: 600}]); - - expect(payload.imp[3]).to.have.property('id', 'bid003'); - expect(payload.imp[3]).to.have.property('secure', 1); - expect(payload.imp[3]).to.have.property('bidfloor', 0); - expect(payload.imp[3]).to.have.property('displaymanager', 'Prebid.js'); - expect(payload.imp[3]).to.have.property('displaymanagerver').that.matches(versionPattern); - expect(payload.imp[3]).to.not.have.property('tagid'); - expect(payload.imp[3]).to.have.property('video'); - expect(payload.imp[3].video).to.have.property('pos', 3); - expect(payload.imp[3].video).to.have.property('w', 632); - expect(payload.imp[3].video).to.have.property('h', 499); - expect(payload.imp[3].video).to.have.property('mimes'); - expect(payload.imp[3].video.mimes).to.deep.equal(['video/mp4', 'video/x-flv']); - expect(payload.imp[3].video).to.have.property('protocols'); - expect(payload.imp[3].video.protocols).to.deep.equal([1, 2]); - expect(payload.imp[3].video).to.have.property('api'); - expect(payload.imp[3].video.api).to.deep.equal([2]); - expect(payload.imp[3].video).to.have.property('maxduration', 30); - expect(payload.imp[3]).to.not.have.property('banner'); - - expect(payload.imp[4]).to.have.property('id', 'bid004'); - expect(payload.imp[4]).to.have.property('secure', 1); - expect(payload.imp[4]).to.have.property('bidfloor', 0); - expect(payload.imp[4]).to.have.property('displaymanager', 'Prebid.js'); - expect(payload.imp[4]).to.have.property('displaymanagerver').that.matches(versionPattern); - expect(payload.imp[4]).to.not.have.property('tagid'); - expect(payload.imp[4]).to.have.property('video'); - expect(payload.imp[4].video).to.not.have.property('pos'); - expect(payload.imp[4].video).to.have.property('w', 1024); - expect(payload.imp[4].video).to.have.property('h', 768); - expect(payload.imp[4].video).to.have.property('mimes'); - expect(payload.imp[4].video.mimes).to.deep.equal(['video/mp4', 'video/x-flv']); - expect(payload.imp[4].video).to.have.property('protocols'); - expect(payload.imp[4].video.protocols).to.deep.equal([1, 2, 3]); - expect(payload.imp[4].video).to.have.property('api'); - expect(payload.imp[4].video.api).to.deep.equal([2, 3]); - expect(payload.imp[4].video).to.have.property('maxduration', 30); - expect(payload.imp[4]).to.not.have.property('banner'); - - expect(payload.imp[5]).to.have.property('id', 'bid005'); - expect(payload.imp[5]).to.have.property('secure', 1); - expect(payload.imp[5]).to.have.property('bidfloor', 0); - expect(payload.imp[5]).to.have.property('displaymanager', 'Prebid.js'); - expect(payload.imp[5]).to.have.property('displaymanagerver').that.matches(versionPattern); - expect(payload.imp[5]).to.not.have.property('tagid'); - expect(payload.imp[5]).to.have.property('video'); - expect(payload.imp[5].video).to.have.property('pos', 2); - expect(payload.imp[5].video).to.not.have.property('w'); - expect(payload.imp[5].video).to.not.have.property('h'); - expect(payload.imp[5].video).to.have.property('mimes'); - expect(payload.imp[5].video.mimes).to.deep.equal(['video/mp4', 'video/x-flv']); - expect(payload.imp[5].video).to.not.have.property('protocols'); - expect(payload.imp[5].video).to.not.have.property('api'); - expect(payload.imp[5].video).to.not.have.property('maxduration'); - expect(payload.imp[5]).to.not.have.property('banner'); - - expect(payload.imp[6]).to.have.property('id', 'bid006'); - expect(payload.imp[6]).to.have.property('video'); - expect(payload.imp[6].video).to.have.property('mimes'); - expect(payload.imp[6].video.mimes).to.deep.equal(['video/mp4', 'video/x-flv']); - expect(payload.imp[6]).to.not.have.property('banner'); - expect(payload.imp[6]).to.have.property('instl'); - expect(payload.imp[6]).to.have.property('ext'); - expect(payload.imp[6].ext).to.have.property('data'); - expect(payload.imp[6].ext.data).to.have.property('pbadslot'); - - expect(payload.imp[7]).to.have.property('id', 'bid007'); - expect(payload.imp[7]).to.have.property('secure', 1); - expect(payload.imp[7]).to.have.property('bidfloor', 0); - expect(payload.imp[7]).to.have.property('displaymanager', 'Prebid.js'); - expect(payload.imp[7]).to.have.property('displaymanagerver').that.matches(versionPattern); - expect(payload.imp[7]).to.not.have.property('tagid'); - expect(payload.imp[7]).to.have.property('banner'); - expect(payload.imp[7].banner).to.have.property('pos', 5); - expect(payload.imp[7].banner).to.have.property('format'); - expect(payload.imp[7].banner.format).to.deep.equal([{w: 728, h: 90}, {w: 468, h: 60}]); - - expect(payload).to.have.property('site'); - expect(payload.site).to.have.property('id', siteId); - expect(payload.site).to.have.property('mobile').that.is.oneOf([0, 1]); - - expect(payload.site).to.have.property('page', page); - - expect(payload).to.have.property('device'); - expect(payload.device).to.have.property('w', screen.width); - expect(payload.device).to.have.property('h', screen.height); - expect(payload.device).to.have.property('dnt').that.is.oneOf([0, 1]); - 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('Simple banner', () => { + expect(payload.imp[0]).to.have.property('id', 'bid000'); + expect(payload.imp[0]).to.have.property('secure', 1); + expect(payload.imp[0]).to.have.property('bidfloor', 0.5); + expect(payload.imp[0]).to.have.property('displaymanager', 'Prebid.js'); + expect(payload.imp[0]).to.have.property('displaymanagerver').that.matches(versionPattern); + expect(payload.imp[0]).to.have.property('tagid', 'tagid-1'); + expect(payload.imp[0]).to.have.property('banner'); + expect(payload.imp[0].banner).to.have.property('pos', 1); + expect(payload.imp[0].banner).to.have.property('format'); + expect(payload.imp[0].banner.format).to.deep.equal([{w: 300, h: 250}]); + expect(payload.imp[0]).to.not.have.property('video'); + }); + + it('Banner multiple sizes', () => { + expect(payload.imp[1]).to.have.property('id', 'bid001'); + expect(payload.imp[1]).to.have.property('secure', 1); + expect(payload.imp[1]).to.have.property('bidfloor', 0); + expect(payload.imp[1]).to.have.property('displaymanager', 'Prebid.js'); + expect(payload.imp[1]).to.have.property('displaymanagerver').that.matches(versionPattern); + expect(payload.imp[1]).to.not.have.property('tagid'); + expect(payload.imp[1]).to.have.property('banner'); + expect(payload.imp[1].banner).to.not.have.property('pos'); + expect(payload.imp[1].banner).to.have.property('format'); + expect(payload.imp[1].banner.format).to.deep.equal([{w: 728, h: 90}, {w: 468, h: 60}]); + }); + + it('Banner with tagid and position', () => { + expect(payload.imp[2]).to.have.property('id', 'bid002'); + expect(payload.imp[2]).to.have.property('secure', 1); + expect(payload.imp[2]).to.have.property('bidfloor', 0); + expect(payload.imp[2]).to.have.property('displaymanager', 'Prebid.js'); + expect(payload.imp[2]).to.have.property('displaymanagerver').that.matches(versionPattern); + expect(payload.imp[2]).to.have.property('banner'); + expect(payload.imp[2].banner).to.have.property('pos', 2); + expect(payload.imp[2].banner).to.have.property('format'); + expect(payload.imp[2].banner.format).to.deep.equal([{w: 300, h: 600}, {w: 160, h: 600}]); + }); + + if (FEATURES.VIDEO) { + it('Simple video', () => { + expect(payload.imp[3]).to.have.property('id', 'bid003'); + expect(payload.imp[3]).to.have.property('secure', 1); + expect(payload.imp[3]).to.have.property('bidfloor', 0); + expect(payload.imp[3]).to.have.property('displaymanager', 'Prebid.js'); + expect(payload.imp[3]).to.have.property('displaymanagerver').that.matches(versionPattern); + expect(payload.imp[3]).to.not.have.property('tagid'); + expect(payload.imp[3]).to.have.property('video'); + expect(payload.imp[3].video).to.have.property('pos', 3); + expect(payload.imp[3].video).to.have.property('w', 632); + expect(payload.imp[3].video).to.have.property('h', 499); + expect(payload.imp[3].video).to.have.property('mimes'); + expect(payload.imp[3].video.mimes).to.deep.equal(['video/mp4', 'video/x-flv']); + expect(payload.imp[3].video).to.have.property('protocols'); + expect(payload.imp[3].video.protocols).to.deep.equal([1, 2]); + expect(payload.imp[3].video).to.have.property('api'); + expect(payload.imp[3].video.api).to.deep.equal([2]); + expect(payload.imp[3].video).to.have.property('maxduration', 30); + expect(payload.imp[3]).to.not.have.property('banner'); + }); + + it('Video with playerSize', () => { + expect(payload.imp[4]).to.have.property('id', 'bid004'); + expect(payload.imp[4]).to.have.property('secure', 1); + expect(payload.imp[4]).to.have.property('bidfloor', 0); + expect(payload.imp[4]).to.have.property('displaymanager', 'Prebid.js'); + expect(payload.imp[4]).to.have.property('displaymanagerver').that.matches(versionPattern); + expect(payload.imp[4]).to.not.have.property('tagid'); + expect(payload.imp[4]).to.have.property('video'); + expect(payload.imp[4].video).to.not.have.property('pos'); + expect(payload.imp[4].video).to.have.property('w', 1024); + expect(payload.imp[4].video).to.have.property('h', 768); + expect(payload.imp[4].video).to.have.property('mimes'); + expect(payload.imp[4].video.mimes).to.deep.equal(['video/mp4', 'video/x-flv']); + expect(payload.imp[4].video).to.have.property('protocols'); + expect(payload.imp[4].video.protocols).to.deep.equal([1, 2, 3]); + expect(payload.imp[4].video).to.have.property('api'); + expect(payload.imp[4].video.api).to.deep.equal([1, 2, 3]); + expect(payload.imp[4].video).to.have.property('maxduration', 30); + }); + + it('Video without sizes', () => { + expect(payload.imp[5]).to.have.property('id', 'bid005'); + expect(payload.imp[5]).to.have.property('secure', 1); + expect(payload.imp[5]).to.have.property('bidfloor', 0); + expect(payload.imp[5]).to.have.property('displaymanager', 'Prebid.js'); + expect(payload.imp[5]).to.have.property('displaymanagerver').that.matches(versionPattern); + expect(payload.imp[5]).to.not.have.property('tagid'); + expect(payload.imp[5]).to.have.property('video'); + expect(payload.imp[5].video).to.have.property('pos', 2); + expect(payload.imp[5].video).to.not.have.property('w'); + expect(payload.imp[5].video).to.not.have.property('h'); + expect(payload.imp[5].video).to.have.property('mimes'); + expect(payload.imp[5].video.mimes).to.deep.equal(['video/mp4', 'video/x-flv']); + expect(payload.imp[5].video).to.not.have.property('protocols'); + expect(payload.imp[5].video).to.not.have.property('api'); + expect(payload.imp[5].video).to.not.have.property('maxduration'); + expect(payload.imp[5]).to.not.have.property('banner'); + }); + } + + it('With FPD', () => { + expect(payload.imp[6]).to.have.property('id', 'bid006'); + expect(payload.imp[6]).to.have.property('banner'); + expect(payload.imp[6]).to.not.have.property('video'); + expect(payload.imp[6]).to.have.property('instl'); + expect(payload.imp[6]).to.have.property('ext'); + expect(payload.imp[6].ext).to.have.property('data'); + expect(payload.imp[6].ext.data).to.have.property('pbadslot'); + }); }); it('Verify timeout', () => { @@ -440,59 +460,62 @@ describe('Conversant adapter tests', function() { expect(request.url).to.equal(testUrl); }); - it('Verify interpretResponse', function() { - const request = spec.buildRequests(bidRequests, {}); - const response = spec.interpretResponse(bidResponses, request); - expect(response).to.be.an('array').with.lengthOf(4); - - let bid = response[0]; - expect(bid).to.have.property('requestId', 'bid000'); - expect(bid).to.have.property('currency', 'USD'); - expect(bid).to.have.property('cpm', 0.99); - expect(bid).to.have.property('creativeId', '1000'); - expect(bid).to.have.property('width', 300); - expect(bid).to.have.property('height', 250); - expect(bid.meta.advertiserDomains).to.deep.equal(['https://example.com']); - expect(bid).to.have.property('ad', 'markup000'); - expect(bid).to.have.property('ttl', 300); - expect(bid).to.have.property('netRevenue', true); + describe('Verify interpretResponse', function() { + let bid, request, response; + + before(() => { + request = spec.buildRequests(bidRequests, {}); + response = spec.interpretResponse(bidResponses, request).bids; + }); + + it('Banner', function() { + expect(response).to.be.an('array').with.lengthOf(4); + bid = response[0]; + expect(bid).to.have.property('requestId', 'bid000'); + expect(bid).to.have.property('cpm', 0.99); + expect(bid).to.have.property('creativeId', '1000'); + expect(bid).to.have.property('width', 300); + expect(bid).to.have.property('height', 250); + expect(bid.meta.advertiserDomains).to.deep.equal(['https://example.com']); + expect(bid).to.have.property('ad', 'markup000
'); + expect(bid).to.have.property('ttl', 300); + expect(bid).to.have.property('netRevenue', true); + }); // There is no bid001 because cpm is $0 - bid = response[1]; - expect(bid).to.have.property('requestId', 'bid002'); - expect(bid).to.have.property('currency', 'USD'); - expect(bid).to.have.property('cpm', 2.99); - expect(bid).to.have.property('creativeId', '1002'); - expect(bid).to.have.property('width', 300); - expect(bid).to.have.property('height', 600); - expect(bid).to.have.property('ad', 'markup002'); - expect(bid).to.have.property('ttl', 300); - expect(bid).to.have.property('netRevenue', true); - - bid = response[2]; - expect(bid).to.have.property('requestId', 'bid003'); - expect(bid).to.have.property('currency', 'USD'); - expect(bid).to.have.property('cpm', 3.99); - expect(bid).to.have.property('creativeId', '1003'); - expect(bid).to.have.property('width', 632); - expect(bid).to.have.property('height', 499); - expect(bid).to.have.property('vastUrl', 'markup003'); - expect(bid).to.have.property('mediaType', 'video'); - expect(bid).to.have.property('ttl', 300); - expect(bid).to.have.property('netRevenue', true); - - bid = response[3]; - expect(bid).to.have.property('vastXml', ''); - }); + it('Banner multiple sizes', function() { + bid = response[1]; + expect(bid).to.have.property('requestId', 'bid002'); + expect(bid).to.have.property('cpm', 2.99); + expect(bid).to.have.property('creativeId', '1002'); + expect(bid).to.have.property('width', 300); + expect(bid).to.have.property('height', 600); + expect(bid).to.have.property('ad', 'markup002
'); + expect(bid).to.have.property('ttl', 300); + expect(bid).to.have.property('netRevenue', true); + }); + + if (FEATURES.VIDEO) { + it('Video', function () { + bid = response[2]; + expect(bid).to.have.property('requestId', 'bid003'); + expect(bid).to.have.property('cpm', 3.99); + expect(bid).to.have.property('creativeId', '1003'); + expect(bid).to.have.property('playerWidth', 632); + expect(bid).to.have.property('playerHeight', 499); + expect(bid).to.have.property('vastUrl', 'notify003'); + expect(bid).to.have.property('vastXml', 'markup003'); + expect(bid).to.have.property('mediaType', 'video'); + expect(bid).to.have.property('ttl', 300); + expect(bid).to.have.property('netRevenue', true); + }); - it('Verify handling of bad responses', function() { - let response = spec.interpretResponse({}, {}); - expect(response).to.be.an('array').with.lengthOf(0); - response = spec.interpretResponse({id: '123'}, {}); - expect(response).to.be.an('array').with.lengthOf(0); - response = spec.interpretResponse({id: '123', seatbid: []}, {}); - expect(response).to.be.an('array').with.lengthOf(0); + it('Empty Video', function() { + bid = response[3]; + expect(bid).to.have.property('vastXml', ''); + }); + } }); it('Verify publisher commond id support', function() { @@ -524,79 +547,23 @@ describe('Conversant adapter tests', function() { expect(payload).to.not.have.nested.property('user.ext.eids'); }); - it('Verify GDPR bid request', function() { - // add gdpr info - const bidderRequest = { - gdprConsent: { - consentString: 'BOJObISOJObISAABAAENAA4AAAAAoAAA', - gdprApplies: true - } - }; - - const payload = spec.buildRequests(bidRequests, bidderRequest).data; - expect(payload).to.have.deep.nested.property('user.ext.consent', 'BOJObISOJObISAABAAENAA4AAAAAoAAA'); - expect(payload).to.have.deep.nested.property('regs.ext.gdpr', 1); - }); - - it('Verify GDPR bid request without gdprApplies', function() { - // add gdpr info - const bidderRequest = { - gdprConsent: { - consentString: '' - } - }; - - const payload = spec.buildRequests(bidRequests, bidderRequest).data; - expect(payload).to.have.deep.nested.property('user.ext.consent', ''); - expect(payload).to.not.have.deep.nested.property('regs.ext.gdpr'); - }); - - describe('CCPA', function() { - it('should have us_privacy', function() { - const bidderRequest = { - uspConsent: '1NYN' - }; - - const payload = spec.buildRequests(bidRequests, bidderRequest).data; - expect(payload).to.have.deep.nested.property('regs.ext.us_privacy', '1NYN'); - expect(payload).to.not.have.deep.nested.property('regs.ext.gdpr'); - }); - - it('should have no us_privacy', function() { - const payload = spec.buildRequests(bidRequests, {}).data; - expect(payload).to.not.have.deep.nested.property('regs.ext.us_privacy'); - }); - - it('should have both gdpr and us_privacy', function() { - const bidderRequest = { - gdprConsent: { - consentString: 'BOJObISOJObISAABAAENAA4AAAAAoAAA', - gdprApplies: true - }, - uspConsent: '1NYN' - }; - - const payload = spec.buildRequests(bidRequests, bidderRequest).data; - expect(payload).to.have.deep.nested.property('user.ext.consent', 'BOJObISOJObISAABAAENAA4AAAAAoAAA'); - expect(payload).to.have.deep.nested.property('regs.ext.gdpr', 1); - expect(payload).to.have.deep.nested.property('regs.ext.us_privacy', '1NYN'); - }); - }); - describe('Extended ID', function() { it('Verify unifiedid and liveramp', function() { // clone bidRequests let requests = utils.deepClone(bidRequests); + const uid = {pubcid: '112233', idl_env: '334455'}; + const eidArray = [{'source': 'pubcid.org', 'uids': [{'id': '112233', 'atype': 1}]}, {'source': 'liveramp.com', 'uids': [{'id': '334455', 'atype': 3}]}]; + // add pubcid to every entry requests.forEach((unit) => { - Object.assign(unit, {userId: {pubcid: '112233', tdid: '223344', idl_env: '334455'}}); - Object.assign(unit, {userIdAsEids: createEidsArray(unit.userId)}); + Object.assign(unit, {userId: uid}); + Object.assign(unit, {userIdAsEids: eidArray}); }); // construct http post payload const payload = spec.buildRequests(requests, {}).data; expect(payload).to.have.deep.nested.property('user.ext.eids', [ - {source: 'adserver.org', uids: [{id: '223344', atype: 1, ext: {rtiPartner: 'TDID'}}]}, + {source: 'pubcid.org', uids: [{id: '112233', atype: 1}]}, {source: 'liveramp.com', uids: [{id: '334455', atype: 3}]} ]); }); diff --git a/test/spec/modules/criteoBidAdapter_spec.js b/test/spec/modules/criteoBidAdapter_spec.js index b2f3d64a156..5dc6d74d90e 100755 --- a/test/spec/modules/criteoBidAdapter_spec.js +++ b/test/spec/modules/criteoBidAdapter_spec.js @@ -9,11 +9,12 @@ import { } from 'modules/criteoBidAdapter.js'; import * as utils from 'src/utils.js'; import * as refererDetection from 'src/refererDetection.js'; +import * as ajax from 'src/ajax.js'; import { config } from '../../../src/config.js'; import { BANNER, NATIVE, VIDEO } from '../../../src/mediaTypes.js'; describe('The Criteo bidding adapter', function () { - let utilsMock, sandbox; + let utilsMock, sandbox, ajaxStub; beforeEach(function () { $$PREBID_GLOBAL$$.bidderSettings = { @@ -26,6 +27,7 @@ describe('The Criteo bidding adapter', function () { utilsMock = sinon.mock(utils); sandbox = sinon.sandbox.create(); + ajaxStub = sandbox.stub(ajax, 'ajax'); }); afterEach(function () { @@ -33,6 +35,7 @@ describe('The Criteo bidding adapter', function () { global.Criteo = undefined; utilsMock.restore(); sandbox.restore(); + ajaxStub.restore(); }); describe('getUserSyncs', function () { @@ -56,7 +59,9 @@ describe('The Criteo bidding adapter', function () { cookiesAreEnabledStub, localStorageIsEnabledStub, getCookieStub, - getDataFromLocalStorageStub; + setCookieStub, + getDataFromLocalStorageStub, + removeDataFromLocalStorageStub; beforeEach(function () { getConfigStub = sinon.stub(config, 'getConfig'); @@ -75,8 +80,10 @@ describe('The Criteo bidding adapter', function () { localStorageIsEnabledStub = sinon.stub(storage, 'localStorageIsEnabled'); localStorageIsEnabledStub.returns(true); - getCookieStub = sinon.stub(storage, 'getCookie') + getCookieStub = sinon.stub(storage, 'getCookie'); + setCookieStub = sinon.stub(storage, 'setCookie'); getDataFromLocalStorageStub = sinon.stub(storage, 'getDataFromLocalStorage'); + removeDataFromLocalStorageStub = sinon.stub(storage, 'removeDataFromLocalStorage'); }); afterEach(function () { @@ -86,7 +93,9 @@ describe('The Criteo bidding adapter', function () { cookiesAreEnabledStub.restore(); localStorageIsEnabledStub.restore(); getCookieStub.restore(); + setCookieStub.restore(); getDataFromLocalStorageStub.restore(); + removeDataFromLocalStorageStub.restore(); }); it('should not trigger sync if publisher is using fast bid', function () { @@ -198,6 +207,46 @@ describe('The Criteo bidding adapter', function () { url: `https://gum.criteo.com/syncframe?origin=criteoPrebidAdapter&topUrl=www.abc.com&us_privacy=ABC#${JSON.stringify(expectedHash).replace(/"/g, '%22')}` }]); }); + + it('should delete user data when calling onDataDeletionRequest', () => { + const cookieData = { + 'cto_bundle': 'a' + }; + const lsData = { + 'cto_bundle': 'a' + } + getCookieStub.callsFake(cookieName => cookieData[cookieName]); + setCookieStub.callsFake((cookieName, value, expires) => cookieData[cookieName] = value); + getDataFromLocalStorageStub.callsFake(name => lsData[name]); + removeDataFromLocalStorageStub.callsFake(name => lsData[name] = ''); + spec.onDataDeletionRequest([]); + expect(getCookieStub.calledOnce).to.equal(true); + expect(setCookieStub.calledOnce).to.equal(true); + expect(getDataFromLocalStorageStub.calledOnce).to.equal(true); + expect(removeDataFromLocalStorageStub.calledOnce).to.equal(true); + expect(cookieData.cto_bundle).to.equal(''); + expect(lsData.cto_bundle).to.equal(''); + expect(ajaxStub.calledOnce).to.equal(true); + }); + + it('should not call API when calling onDataDeletionRequest with no id', () => { + const cookieData = { + 'cto_bundle': '' + }; + const lsData = { + 'cto_bundle': '' + } + getCookieStub.callsFake(cookieName => cookieData[cookieName]); + setCookieStub.callsFake((cookieName, value, expires) => cookieData[cookieName] = value); + getDataFromLocalStorageStub.callsFake(name => lsData[name]); + removeDataFromLocalStorageStub.callsFake(name => lsData[name] = ''); + spec.onDataDeletionRequest([]); + expect(getCookieStub.calledOnce).to.be.true; + expect(setCookieStub.called).to.be.false; + expect(getDataFromLocalStorageStub.calledOnce).to.be.true + expect(removeDataFromLocalStorageStub.called).to.be.false; + expect(ajaxStub.called).to.be.false; + }); }); describe('isBidRequestValid', function () { @@ -315,93 +364,6 @@ describe('The Criteo bidding adapter', function () { }); it('should return false when given an invalid video bid request', function () { - expect(spec.isBidRequestValid({ - bidder: 'criteo', - mediaTypes: { - video: { - mimes: ['video/mpeg'], - playerSize: [640, 480], - protocols: [5, 6], - maxduration: 30, - api: [1, 2] - } - }, - params: { - networkId: 456, - video: { - skip: 1, - placement: 1, - playbackmethod: 1 - } - }, - })).to.equal(false); - - expect(spec.isBidRequestValid({ - bidder: 'criteo', - mediaTypes: { - video: { - context: 'instream', - mimes: ['video/mpeg'], - playerSize: [640, 480], - protocols: [5, 6], - maxduration: 30, - api: [1, 2] - } - }, - params: { - networkId: 456, - video: { - skip: 1, - placement: 2, - playbackmethod: 1 - } - }, - })).to.equal(false); - - expect(spec.isBidRequestValid({ - bidder: 'criteo', - mediaTypes: { - video: { - context: 'outstream', - mimes: ['video/mpeg'], - playerSize: [640, 480], - protocols: [5, 6], - maxduration: 30, - api: [1, 2] - } - }, - params: { - networkId: 456, - video: { - skip: 1, - placement: 1, - playbackmethod: 1 - } - }, - })).to.equal(false); - - expect(spec.isBidRequestValid({ - bidder: 'criteo', - mediaTypes: { - video: { - context: 'adpod', - mimes: ['video/mpeg'], - playerSize: [640, 480], - protocols: [5, 6], - maxduration: 30, - api: [1, 2] - } - }, - params: { - networkId: 456, - video: { - skip: 1, - placement: 1, - playbackmethod: 1 - } - }, - })).to.equal(false); - expect(spec.isBidRequestValid({ bidder: 'criteo', mediaTypes: { @@ -654,6 +616,33 @@ describe('The Criteo bidding adapter', function () { expect(ortbRequest.source.tid).to.equal('abc'); }); + it('should properly transmit bidId if available', function () { + const bidderRequest = { + ortb2: { + source: { + tid: 'abc' + } + } + }; + const bidRequests = [ + { + bidId: 'bidId', + bidder: 'criteo', + adUnitCode: 'bid-123', + transactionId: 'transaction-123', + mediaTypes: { + banner: { + sizes: [[728, 90]] + } + }, + params: {} + }, + ]; + const request = spec.buildRequests(bidRequests, bidderRequest); + const ortbRequest = request.data; + expect(ortbRequest.slots[0].slotid).to.equal('bidId'); + }); + it('should properly build a request if refererInfo is not provided', function () { const bidderRequest = {}; const bidRequests = [ @@ -1046,6 +1035,30 @@ describe('The Criteo bidding adapter', function () { expect(request.data.user.uspIab).to.equal('1YNY'); }); + it('should properly build a request with site and app ortb fields', function () { + const bidRequests = []; + let app = { + publisher: { + id: 'appPublisherId' + } + }; + let site = { + publisher: { + id: 'sitePublisherId' + } + }; + const bidderRequest = { + ortb2: { + app: app, + site: site + } + }; + const request = spec.buildRequests(bidRequests, bidderRequest); + + expect(request.data.app).to.equal(app); + expect(request.data.site).to.equal(site); + }); + it('should properly build a request with device sua field', function () { const sua = {} const bidRequests = [ @@ -1250,12 +1263,25 @@ describe('The Criteo bidding adapter', function () { sizes: [[640, 480]], mediaTypes: { video: { + context: 'instream', playerSize: [640, 480], mimes: ['video/mp4', 'video/x-flv'], maxduration: 30, api: [1, 2], protocols: [2, 3], - plcmt: 3 + plcmt: 3, + w: 640, + h: 480, + linearity: 1, + skipmin: 30, + skipafter: 30, + minbitrate: 10000, + maxbitrate: 48000, + delivery: [1, 2, 3], + pos: 1, + playbackend: 1, + adPodDurationSec: 30, + durationRangeSec: [1, 30], } }, params: { @@ -1274,6 +1300,7 @@ describe('The Criteo bidding adapter', function () { expect(request.url).to.match(/^https:\/\/bidder\.criteo\.com\/cdb\?profileId=207&av=\d+&wv=[^&]+&cb=\d/); expect(request.method).to.equal('POST'); const ortbRequest = request.data; + expect(ortbRequest.slots[0].video.context).to.equal('instream'); expect(ortbRequest.slots[0].video.mimes).to.deep.equal(['video/mp4', 'video/x-flv']); expect(ortbRequest.slots[0].sizes).to.deep.equal([]); expect(ortbRequest.slots[0].video.playersizes).to.deep.equal(['640x480']); @@ -1286,6 +1313,18 @@ describe('The Criteo bidding adapter', function () { expect(ortbRequest.slots[0].video.playbackmethod).to.deep.equal([1, 3]); expect(ortbRequest.slots[0].video.placement).to.equal(2); expect(ortbRequest.slots[0].video.plcmt).to.equal(3); + expect(ortbRequest.slots[0].video.w).to.equal(640); + expect(ortbRequest.slots[0].video.h).to.equal(480); + expect(ortbRequest.slots[0].video.linearity).to.equal(1); + expect(ortbRequest.slots[0].video.skipmin).to.equal(30); + expect(ortbRequest.slots[0].video.skipafter).to.equal(30); + expect(ortbRequest.slots[0].video.minbitrate).to.equal(10000); + expect(ortbRequest.slots[0].video.maxbitrate).to.equal(48000); + expect(ortbRequest.slots[0].video.delivery).to.deep.equal([1, 2, 3]); + expect(ortbRequest.slots[0].video.pos).to.equal(1); + expect(ortbRequest.slots[0].video.playbackend).to.equal(1); + expect(ortbRequest.slots[0].video.adPodDurationSec).to.equal(30); + expect(ortbRequest.slots[0].video.durationRangeSec).to.deep.equal([1, 30]); }); it('should properly build a video request with more than one player size', function () { @@ -1803,6 +1842,86 @@ describe('The Criteo bidding adapter', function () { const request = spec.buildRequests(bidRequests, bidderRequest); expect(request.data.slots[0].rwdd).to.be.undefined; }); + + it('should properly build a request when FLEDGE is enabled', function () { + const bidderRequest = { + fledgeEnabled: true, + }; + const bidRequests = [ + { + bidder: 'criteo', + adUnitCode: 'bid-123', + transactionId: 'transaction-123', + mediaTypes: { + banner: { + sizes: [[728, 90]] + } + }, + params: { + zoneId: 123, + ext: { + bidfloor: 0.75 + } + }, + ortb2Imp: { + ext: { + ae: 1 + } + } + }, + ]; + + const request = spec.buildRequests(bidRequests, bidderRequest); + expect(request.data.slots[0].ext.ae).to.equal(1); + }); + + it('should properly build a request when FLEDGE is disabled', function () { + const bidderRequest = { + fledgeEnabled: false, + }; + const bidRequests = [ + { + bidder: 'criteo', + adUnitCode: 'bid-123', + transactionId: 'transaction-123', + mediaTypes: { + banner: { + sizes: [[728, 90]] + } + }, + params: { + zoneId: 123, + ext: { + bidfloor: 0.75 + } + }, + ortb2Imp: { + ext: { + ae: 1 + } + } + }, + ]; + + const request = spec.buildRequests(bidRequests, bidderRequest); + expect(request.data.slots[0].ext).to.not.have.property('ae'); + }); + + it('should properly transmit device.ext.cdep if available', function () { + const bidderRequest = { + ortb2: { + device: { + ext: { + cdep: 'cookieDeprecationLabel' + } + } + } + }; + const bidRequests = []; + const request = spec.buildRequests(bidRequests, bidderRequest); + const ortbRequest = request.data; + expect(ortbRequest.device.ext.cdep).to.equal('cookieDeprecationLabel'); + }); }); describe('interpretResponse', function () { @@ -1837,6 +1956,11 @@ describe('The Criteo bidding adapter', function () { bidRequests: [{ adUnitCode: 'test-requestId', bidId: 'test-bidId', + mediaTypes: { + banner: { + sizes: [[728, 90]] + } + }, params: { networkId: 456, } @@ -1855,6 +1979,202 @@ describe('The Criteo bidding adapter', function () { expect(bids[0].meta.networkName).to.equal('Criteo'); }); + it('should properly parse a bid response with a networkId with twin ad unit banner win', function () { + const response = { + body: { + slots: [{ + impid: 'test-requestId', + cpm: 1.23, + creative: 'test-ad', + creativecode: 'test-crId', + width: 728, + height: 90, + deal: 'myDealCode', + adomain: ['criteo.com'], + ext: { + meta: { + networkName: 'Criteo' + } + } + }], + }, + }; + const request = { + bidRequests: [{ + adUnitCode: 'test-requestId', + bidId: 'test-bidId', + mediaTypes: { + video: { + context: 'instream', + mimes: ['video/mpeg'], + playerSize: [640, 480], + protocols: [5, 6], + maxduration: 30, + api: [1, 2] + } + }, + params: { + networkId: 456, + }, + }, { + adUnitCode: 'test-requestId', + bidId: 'test-bidId2', + mediaTypes: { + banner: { + sizes: [[728, 90]] + } + }, + params: { + networkId: 456, + } + }] + }; + const bids = spec.interpretResponse(response, request); + expect(bids).to.have.lengthOf(1); + expect(bids[0].requestId).to.equal('test-bidId2'); + expect(bids[0].cpm).to.equal(1.23); + expect(bids[0].ad).to.equal('test-ad'); + expect(bids[0].creativeId).to.equal('test-crId'); + expect(bids[0].width).to.equal(728); + expect(bids[0].height).to.equal(90); + expect(bids[0].dealId).to.equal('myDealCode'); + expect(bids[0].meta.advertiserDomains[0]).to.equal('criteo.com'); + expect(bids[0].meta.networkName).to.equal('Criteo'); + }); + + it('should properly parse a bid response with a networkId with twin ad unit video win', function () { + const response = { + body: { + slots: [{ + impid: 'test-requestId', + bidId: 'abc123', + cpm: 1.23, + displayurl: 'http://test-ad', + width: 728, + height: 90, + zoneid: 123, + video: true, + ext: { + meta: { + networkName: 'Criteo' + } + } + }], + }, + }; + const request = { + bidRequests: [{ + adUnitCode: 'test-requestId', + bidId: 'test-bidId', + mediaTypes: { + video: { + context: 'instream', + mimes: ['video/mpeg'], + playerSize: [728, 90], + protocols: [5, 6], + maxduration: 30, + api: [1, 2] + } + }, + params: { + networkId: 456, + }, + }, { + adUnitCode: 'test-requestId', + bidId: 'test-bidId2', + mediaTypes: { + banner: { + sizes: [[728, 90]] + } + }, + params: { + networkId: 456, + } + }] + }; + const bids = spec.interpretResponse(response, request); + expect(bids).to.have.lengthOf(1); + expect(bids[0].requestId).to.equal('test-bidId'); + expect(bids[0].cpm).to.equal(1.23); + expect(bids[0].vastUrl).to.equal('http://test-ad'); + expect(bids[0].mediaType).to.equal(VIDEO); + }); + + it('should properly parse a bid response with a networkId with twin ad unit native win', function () { + const response = { + body: { + slots: [{ + impid: 'test-requestId', + cpm: 1.23, + creative: 'test-ad', + creativecode: 'test-crId', + width: 728, + height: 90, + deal: 'myDealCode', + adomain: ['criteo.com'], + native: { + 'products': [{ + 'sendTargetingKeys': false, + 'title': 'Product title', + 'description': 'Product desc', + 'price': '100', + 'click_url': 'https://product.click', + 'image': { + 'url': 'https://publisherdirect.criteo.com/publishertag/preprodtest/creative.png', + 'height': 300, + 'width': 300 + }, + 'call_to_action': 'Try it now!' + }], + 'advertiser': { + 'description': 'sponsor', + 'domain': 'criteo.com', + 'logo': { 'url': 'https://www.criteo.com/images/criteo-logo.svg', 'height': 300, 'width': 300 } + }, + 'privacy': { + 'optout_click_url': 'https://info.criteo.com/privacy/informations', + 'optout_image_url': 'https://static.criteo.net/flash/icon/nai_small.png', + }, + 'impression_pixels': [{ 'url': 'https://my-impression-pixel/test/impression' }, { 'url': 'https://cas.com/lg.com' }] + }, + ext: { + meta: { + networkName: 'Criteo' + } + } + }], + }, + }; + const request = { + bidRequests: [{ + adUnitCode: 'test-requestId', + bidId: 'test-bidId', + mediaTypes: { + native: {} + }, + params: { + networkId: 456, + }, + }, { + adUnitCode: 'test-requestId', + bidId: 'test-bidId2', + mediaTypes: { + banner: { + sizes: [[728, 90]] + } + }, + params: { + networkId: 456, + } + }] + }; + const bids = spec.interpretResponse(response, request); + expect(bids).to.have.lengthOf(1); + expect(bids[0].requestId).to.equal('test-bidId'); + expect(bids[0].cpm).to.equal(1.23); + expect(bids[0].mediaType).to.equal(NATIVE); + }); + it('should properly parse a bid response with a zoneId', function () { const response = { body: { @@ -2133,6 +2453,144 @@ describe('The Criteo bidding adapter', function () { expect(bids[0].height).to.equal(90); }); + it('should properly parse a bid response with FLEDGE auction configs', function () { + const response = { + body: { + ext: { + igbid: [{ + impid: 'test-bidId', + igbuyer: [{ + origin: 'https://first-buyer-domain.com', + buyerdata: { + foo: 'bar', + }, + }, { + origin: 'https://second-buyer-domain.com', + buyerdata: { + foo: 'baz', + }, + }] + }, { + impid: 'test-bidId-2', + igbuyer: [{ + origin: 'https://first-buyer-domain.com', + buyerdata: { + foo: 'bar', + }, + }, { + origin: 'https://second-buyer-domain.com', + buyerdata: { + foo: 'baz', + }, + }] + }], + seller: 'https://seller-domain.com', + sellerTimeout: 500, + sellerSignals: { + foo: 'bar', + }, + sellerSignalsPerImp: { + 'test-bidId': { + foo2: 'bar2', + } + }, + }, + }, + }; + const bidderRequest = { + ortb2: { + source: { + tid: 'abc' + } + } + }; + const bidRequests = [ + { + bidId: 'test-bidId', + bidder: 'criteo', + adUnitCode: 'bid-123', + transactionId: 'transaction-123', + mediaTypes: { + banner: { + sizes: [[728, 90]] + } + }, + params: { + bidFloor: 1, + bidFloorCur: 'EUR' + } + }, + { + bidId: 'test-bidId-2', + bidder: 'criteo', + adUnitCode: 'bid-123', + transactionId: 'transaction-123', + mediaTypes: { + banner: { + sizes: [[728, 90]] + } + }, + params: { + bidFloor: 1, + bidFloorCur: 'EUR' + } + }, + ]; + const request = spec.buildRequests(bidRequests, bidderRequest); + const interpretedResponse = spec.interpretResponse(response, request); + expect(interpretedResponse).to.have.property('bids'); + expect(interpretedResponse).to.have.property('fledgeAuctionConfigs'); + expect(interpretedResponse.bids).to.have.lengthOf(0); + expect(interpretedResponse.fledgeAuctionConfigs).to.have.lengthOf(2); + expect(interpretedResponse.fledgeAuctionConfigs[0]).to.deep.equal({ + bidId: 'test-bidId', + config: { + auctionSignals: {}, + decisionLogicUrl: 'https://grid-mercury.criteo.com/fledge/decision', + interestGroupBuyers: ['https://first-buyer-domain.com', 'https://second-buyer-domain.com'], + perBuyerSignals: { + 'https://first-buyer-domain.com': { + foo: 'bar', + }, + 'https://second-buyer-domain.com': { + foo: 'baz' + }, + }, + seller: 'https://seller-domain.com', + sellerTimeout: 500, + sellerSignals: { + foo: 'bar', + foo2: 'bar2', + floor: 1, + sellerCurrency: 'EUR', + }, + }, + }); + expect(interpretedResponse.fledgeAuctionConfigs[1]).to.deep.equal({ + bidId: 'test-bidId-2', + config: { + auctionSignals: {}, + decisionLogicUrl: 'https://grid-mercury.criteo.com/fledge/decision', + interestGroupBuyers: ['https://first-buyer-domain.com', 'https://second-buyer-domain.com'], + perBuyerSignals: { + 'https://first-buyer-domain.com': { + foo: 'bar', + }, + 'https://second-buyer-domain.com': { + foo: 'baz' + }, + }, + seller: 'https://seller-domain.com', + sellerTimeout: 500, + sellerSignals: { + foo: 'bar', + floor: 1, + sellerCurrency: 'EUR', + }, + }, + }); + }); + [{ hasBidResponseLevelPafData: true, hasBidResponseBidLevelPafData: true, diff --git a/test/spec/modules/criteoIdSystem_spec.js b/test/spec/modules/criteoIdSystem_spec.js index aaf63873d93..975271738e5 100644 --- a/test/spec/modules/criteoIdSystem_spec.js +++ b/test/spec/modules/criteoIdSystem_spec.js @@ -52,17 +52,21 @@ describe('CriteoId module', function () { }); const storageTestCases = [ - { cookie: 'bidId', localStorage: 'bidId2', expected: 'bidId' }, - { cookie: 'bidId', localStorage: undefined, expected: 'bidId' }, - { cookie: undefined, localStorage: 'bidId', expected: 'bidId' }, - { cookie: undefined, localStorage: undefined, expected: undefined }, + { submoduleConfig: undefined, cookie: 'bidId', localStorage: 'bidId2', expected: 'bidId' }, + { submoduleConfig: undefined, cookie: 'bidId', localStorage: undefined, expected: 'bidId' }, + { submoduleConfig: undefined, cookie: undefined, localStorage: 'bidId', expected: 'bidId' }, + { submoduleConfig: undefined, cookie: undefined, localStorage: undefined, expected: undefined }, + { submoduleConfig: { storage: { type: 'cookie' } }, cookie: 'bidId', localStorage: 'bidId2', expected: 'bidId' }, + { submoduleConfig: { storage: { type: 'cookie' } }, cookie: undefined, localStorage: 'bidId2', expected: undefined }, + { submoduleConfig: { storage: { type: 'html5' } }, cookie: 'bidId', localStorage: 'bidId2', expected: 'bidId2' }, + { submoduleConfig: { storage: { type: 'html5' } }, cookie: 'bidId', localStorage: undefined, expected: undefined }, ] - storageTestCases.forEach(testCase => it('getId() should return the bidId when it exists in local storages', function () { + storageTestCases.forEach(testCase => it('getId() should return the user id depending on the storage type enabled and the data available', function () { getCookieStub.withArgs('cto_bidid').returns(testCase.cookie); getLocalStorageStub.withArgs('cto_bidid').returns(testCase.localStorage); - const result = criteoIdSubmodule.getId(); + const result = criteoIdSubmodule.getId(testCase.submoduleConfig); expect(result.id).to.be.deep.equal(testCase.expected ? { criteoId: testCase.expected } : undefined); expect(result.callback).to.be.a('function'); })) @@ -95,22 +99,24 @@ describe('CriteoId module', function () { }); const responses = [ - { bundle: 'bundle', bidId: 'bidId', acwsUrl: 'acwsUrl' }, - { bundle: 'bundle', bidId: undefined, acwsUrl: 'acwsUrl' }, - { bundle: 'bundle', bidId: 'bidId', acwsUrl: undefined }, - { bundle: undefined, bidId: 'bidId', acwsUrl: 'acwsUrl' }, - { bundle: 'bundle', bidId: undefined, acwsUrl: undefined }, - { bundle: undefined, bidId: 'bidId', acwsUrl: undefined }, - { bundle: undefined, bidId: undefined, acwsUrl: 'acwsUrl' }, - { bundle: undefined, bidId: undefined, acwsUrl: ['acwsUrl', 'acwsUrl2'] }, - { bundle: undefined, bidId: undefined, acwsUrl: undefined }, + { submoduleConfig: undefined, shouldWriteCookie: true, shouldWriteLocalStorage: true, bundle: 'bundle', bidId: 'bidId', acwsUrl: 'acwsUrl' }, + { submoduleConfig: undefined, shouldWriteCookie: true, shouldWriteLocalStorage: true, bundle: 'bundle', bidId: undefined, acwsUrl: 'acwsUrl' }, + { submoduleConfig: undefined, shouldWriteCookie: true, shouldWriteLocalStorage: true, bundle: 'bundle', bidId: 'bidId', acwsUrl: undefined }, + { submoduleConfig: undefined, shouldWriteCookie: true, shouldWriteLocalStorage: true, bundle: undefined, bidId: 'bidId', acwsUrl: 'acwsUrl' }, + { submoduleConfig: undefined, shouldWriteCookie: true, shouldWriteLocalStorage: true, bundle: 'bundle', bidId: undefined, acwsUrl: undefined }, + { submoduleConfig: undefined, shouldWriteCookie: true, shouldWriteLocalStorage: true, bundle: undefined, bidId: 'bidId', acwsUrl: undefined }, + { submoduleConfig: undefined, shouldWriteCookie: true, shouldWriteLocalStorage: true, bundle: undefined, bidId: undefined, acwsUrl: 'acwsUrl' }, + { submoduleConfig: undefined, shouldWriteCookie: true, shouldWriteLocalStorage: true, bundle: undefined, bidId: undefined, acwsUrl: ['acwsUrl', 'acwsUrl2'] }, + { submoduleConfig: undefined, shouldWriteCookie: true, shouldWriteLocalStorage: true, bundle: undefined, bidId: undefined, acwsUrl: undefined }, + { submoduleConfig: { storage: { type: 'cookie' } }, shouldWriteCookie: true, shouldWriteLocalStorage: false, bundle: 'bundle', bidId: 'bidId', acwsUrl: undefined }, + { submoduleConfig: { storage: { type: 'html5' } }, shouldWriteCookie: false, shouldWriteLocalStorage: true, bundle: 'bundle', bidId: 'bidId', acwsUrl: undefined }, ] responses.forEach(response => describe('test user sync response behavior', function () { const expirationTs = new Date(nowTimestamp + cookiesMaxAge).toString(); it('should save bidId if it exists', function () { - const result = criteoIdSubmodule.getId(); + const result = criteoIdSubmodule.getId(response.submoduleConfig); result.callback((id) => { expect(id).to.be.deep.equal(response.bidId ? { criteoId: response.bidId } : undefined); }); @@ -127,16 +133,35 @@ describe('CriteoId module', function () { expect(setCookieStub.calledWith('cto_bundle')).to.be.false; expect(setLocalStorageStub.calledWith('cto_bundle')).to.be.false; } else if (response.bundle) { - expect(setCookieStub.calledWith('cto_bundle', response.bundle, expirationTs, null, '.com')).to.be.true; - expect(setCookieStub.calledWith('cto_bundle', response.bundle, expirationTs, null, '.testdev.com')).to.be.true; - expect(setLocalStorageStub.calledWith('cto_bundle', response.bundle)).to.be.true; + if (response.shouldWriteCookie) { + expect(setCookieStub.calledWith('cto_bundle', response.bundle, expirationTs, null, '.com')).to.be.true; + expect(setCookieStub.calledWith('cto_bundle', response.bundle, expirationTs, null, '.testdev.com')).to.be.true; + } else { + expect(setCookieStub.calledWith('cto_bundle', response.bundle, expirationTs, null, '.com')).to.be.false; + expect(setCookieStub.calledWith('cto_bundle', response.bundle, expirationTs, null, '.testdev.com')).to.be.false; + } + + if (response.shouldWriteLocalStorage) { + expect(setLocalStorageStub.calledWith('cto_bundle', response.bundle)).to.be.true; + } else { + expect(setLocalStorageStub.calledWith('cto_bundle', response.bundle)).to.be.false; + } expect(triggerPixelStub.called).to.be.false; } if (response.bidId) { - expect(setCookieStub.calledWith('cto_bidid', response.bidId, expirationTs, null, '.com')).to.be.true; - expect(setCookieStub.calledWith('cto_bidid', response.bidId, expirationTs, null, '.testdev.com')).to.be.true; - expect(setLocalStorageStub.calledWith('cto_bidid', response.bidId)).to.be.true; + if (response.shouldWriteCookie) { + expect(setCookieStub.calledWith('cto_bidid', response.bidId, expirationTs, null, '.com')).to.be.true; + expect(setCookieStub.calledWith('cto_bidid', response.bidId, expirationTs, null, '.testdev.com')).to.be.true; + } else { + expect(setCookieStub.calledWith('cto_bidid', response.bidId, expirationTs, null, '.com')).to.be.false; + expect(setCookieStub.calledWith('cto_bidid', response.bidId, expirationTs, null, '.testdev.com')).to.be.false; + } + if (response.shouldWriteLocalStorage) { + expect(setLocalStorageStub.calledWith('cto_bidid', response.bidId)).to.be.true; + } else { + expect(setLocalStorageStub.calledWith('cto_bidid', response.bidId)).to.be.false; + } } else { expect(setCookieStub.calledWith('cto_bidid', '', pastDateString, null, '.com')).to.be.true; expect(setCookieStub.calledWith('cto_bidid', '', pastDateString, null, '.testdev.com')).to.be.true; diff --git a/test/spec/modules/currency_spec.js b/test/spec/modules/currency_spec.js index 88c640e38cc..fa44b7daa7a 100644 --- a/test/spec/modules/currency_spec.js +++ b/test/spec/modules/currency_spec.js @@ -10,10 +10,12 @@ import { addBidResponseHook, currencySupportEnabled, currencyRates, - ready + responseReady } from 'modules/currency.js'; import {createBid} from '../../../src/bidfactory.js'; import CONSTANTS from '../../../src/constants.json'; +import {server} from '../../mocks/xhr.js'; +import * as events from 'src/events.js'; var assert = require('chai').assert; var expect = require('chai').expect; @@ -30,12 +32,10 @@ describe('currency', function () { } beforeEach(function () { - fakeCurrencyFileServer = sinon.fakeServer.create(); - ready.reset(); + fakeCurrencyFileServer = server; }); afterEach(function () { - fakeCurrencyFileServer.restore(); setConfig({}); }); @@ -259,6 +259,19 @@ describe('currency', function () { expect(innerBid.getCpmInNewCurrency('JPY')).to.equal('100.000'); }); + it('does not block auctions if rates do not need to be fetched', () => { + sandbox.stub(responseReady, 'resolve'); + setConfig({ + adServerCurrency: 'USD', + rates: { + USD: { + JPY: 100 + } + } + }); + sinon.assert.called(responseReady.resolve); + }) + it('uses rates specified in json when provided and consider boosted bid', function () { setConfig({ adServerCurrency: 'USD', @@ -287,32 +300,56 @@ describe('currency', function () { expect(innerBid.getCpmInNewCurrency('JPY')).to.equal('1000.000'); }); - it('uses default rates when currency file fails to load', function () { - setConfig({}); - - setConfig({ - adServerCurrency: 'USD', - defaultRates: { - USD: { - JPY: 100 + describe('when rates fail to load', () => { + let bid, addBidResponse, reject; + beforeEach(() => { + bid = makeBid({cpm: 100, currency: 'JPY', bidder: 'rubicoin'}); + addBidResponse = sinon.spy(); + reject = sinon.spy(); + }) + it('uses default rates if specified', function () { + setConfig({ + adServerCurrency: 'USD', + defaultRates: { + USD: { + JPY: 100 + } } - } - }); - - // default response is 404 - fakeCurrencyFileServer.respond(); + }); - var bid = { cpm: 100, currency: 'JPY', bidder: 'rubicon' }; - var innerBid; + // default response is 404 + addBidResponseHook(addBidResponse, 'au', bid); + fakeCurrencyFileServer.respond(); + sinon.assert.calledWith(addBidResponse, 'au', sinon.match(innerBid => { + expect(innerBid.cpm).to.equal('1.0000'); + expect(typeof innerBid.getCpmInNewCurrency).to.equal('function'); + expect(innerBid.getCpmInNewCurrency('JPY')).to.equal('100.000'); + return true; + })); + }); - addBidResponseHook(function(adCodeId, bid) { - innerBid = bid; - }, 'elementId', bid); + it('rejects bids if no default rates are specified', () => { + setConfig({ + adServerCurrency: 'USD', + }); + addBidResponseHook(addBidResponse, 'au', bid, reject); + fakeCurrencyFileServer.respond(); + sinon.assert.notCalled(addBidResponse); + sinon.assert.calledWith(reject, CONSTANTS.REJECTION_REASON.CANNOT_CONVERT_CURRENCY); + }); - expect(innerBid.cpm).to.equal('1.0000'); - expect(typeof innerBid.getCpmInNewCurrency).to.equal('function'); - expect(innerBid.getCpmInNewCurrency('JPY')).to.equal('100.000'); - }); + it('attempts to load rates again on the next auction', () => { + setConfig({ + adServerCurrency: 'USD', + }); + fakeCurrencyFileServer.respond(); + fakeCurrencyFileServer.respondWith(JSON.stringify(getCurrencyRates())); + events.emit(CONSTANTS.EVENTS.AUCTION_INIT, {}); + addBidResponseHook(addBidResponse, 'au', bid, reject); + fakeCurrencyFileServer.respond(); + sinon.assert.calledWith(addBidResponse, 'au', bid, reject); + }) + }) }); describe('currency.addBidResponseDecorator bidResponseQueue', function () { @@ -321,29 +358,26 @@ describe('currency', function () { fakeCurrencyFileServer.respondWith(JSON.stringify(getCurrencyRates())); - var bid = { 'cpm': 1, 'currency': 'USD' }; + const bid = { 'cpm': 1, 'currency': 'USD' }; setConfig({ 'adServerCurrency': 'JPY' }); - var marker = false; - let promiseResolved = false; + let responseAdded = false; + let isReady = false; + responseReady.promise.then(() => { isReady = true }); + addBidResponseHook(Object.assign(function() { - marker = true; - }, { - bail: function (promise) { - promise.then(() => promiseResolved = true); - } + responseAdded = true; }), 'elementId', bid); - expect(marker).to.equal(false); - setTimeout(() => { - expect(promiseResolved).to.be.false; + expect(responseAdded).to.equal(false); + expect(isReady).to.equal(false); fakeCurrencyFileServer.respond(); setTimeout(() => { - expect(marker).to.equal(true); - expect(promiseResolved).to.be.true; + expect(responseAdded).to.equal(true); + expect(isReady).to.equal(true); done(); }); }); @@ -419,6 +453,23 @@ describe('currency', function () { expect(reject.calledOnce).to.be.true; }); + it('should reject bid when rates have not loaded when the auction times out', () => { + fakeCurrencyFileServer.respondWith(JSON.stringify(getCurrencyRates())); + setConfig({'adServerCurrency': 'JPY'}); + const bid = makeBid({cpm: 1, currency: 'USD', auctionId: 'aid'}); + const noConversionBid = makeBid({cpm: 1, currency: 'JPY', auctionId: 'aid'}); + const reject = sinon.spy(); + const addBidResponse = sinon.spy(); + addBidResponseHook(addBidResponse, 'au', bid, reject); + addBidResponseHook(addBidResponse, 'au', noConversionBid, reject); + events.emit(CONSTANTS.EVENTS.AUCTION_TIMEOUT, {auctionId: 'aid'}); + fakeCurrencyFileServer.respond(); + sinon.assert.calledOnce(addBidResponse); + sinon.assert.calledWith(addBidResponse, 'au', noConversionBid, reject); + sinon.assert.calledOnce(reject); + sinon.assert.calledWith(reject, CONSTANTS.REJECTION_REASON.CANNOT_CONVERT_CURRENCY); + }) + it('should return 1 when currency support is enabled and same currency code is requested as is set to adServerCurrency', function () { fakeCurrencyFileServer.respondWith(JSON.stringify(getCurrencyRates())); setConfig({ 'adServerCurrency': 'JPY' }); diff --git a/test/spec/modules/cwireBidAdapter_spec.js b/test/spec/modules/cwireBidAdapter_spec.js index 88c54212aff..8eedcdb4a07 100644 --- a/test/spec/modules/cwireBidAdapter_spec.js +++ b/test/spec/modules/cwireBidAdapter_spec.js @@ -293,4 +293,51 @@ describe('C-WIRE bid adapter', () => { expect(bids[0].ad).to.exist; }) }); + + describe('add user-syncs', function () { + it('empty user-syncs if no consent given', function () { + const userSyncs = spec.getUserSyncs({}, {}, {}, {}); + + expect(userSyncs).to.be.empty + }) + it('empty user-syncs if no syncOption enabled', function () { + let gdprConsent = { + vendorData: { + purpose: { + consents: 1 + } + }}; + const userSyncs = spec.getUserSyncs({}, {}, gdprConsent, {}); + + expect(userSyncs).to.be.empty + }) + + it('user-syncs with enabled pixel option', function () { + let gdprConsent = { + vendorData: { + purpose: { + consents: 1 + } + }}; + let synOptions = {pixelEnabled: true, iframeEnabled: true}; + const userSyncs = spec.getUserSyncs(synOptions, {}, gdprConsent, {}); + + expect(userSyncs[0].type).to.equal('image'); + expect(userSyncs[0].url).to.equal('https://ib.adnxs.com/getuid?https://prebid.cwi.re/v1/cookiesync?xandrId=$UID'); + }) + + it('user-syncs with enabled iframe option', function () { + let gdprConsent = { + vendorData: { + purpose: { + consents: 1 + } + }}; + let synOptions = {iframeEnabled: true}; + const userSyncs = spec.getUserSyncs(synOptions, {}, gdprConsent, {}); + + expect(userSyncs[0].type).to.equal('iframe'); + expect(userSyncs[0].url).to.equal('https://ib.adnxs.com/getuid?https://prebid.cwi.re/v1/cookiesync?xandrId=$UID'); + }) + }) }); diff --git a/test/spec/modules/datablocksBidAdapter_spec.js b/test/spec/modules/datablocksBidAdapter_spec.js index 537b82b0e2e..811aaab6ebb 100644 --- a/test/spec/modules/datablocksBidAdapter_spec.js +++ b/test/spec/modules/datablocksBidAdapter_spec.js @@ -2,6 +2,7 @@ import { expect } from 'chai'; import { spec } from '../../../modules/datablocksBidAdapter.js'; import { BotClientTests } from '../../../modules/datablocksBidAdapter.js'; import { getStorageManager } from '../../../src/storageManager.js'; +import {deepClone} from '../../../src/utils.js'; const bid = { bidId: '2dd581a2b6281d', @@ -409,7 +410,7 @@ describe('DatablocksAdapter', function() { expect(spec.isBidRequestValid(bid)).to.be.true; }); it('Should return false when host/source_id is not set', function() { - let moddedBid = Object.assign({}, bid); + let moddedBid = deepClone(bid); delete moddedBid.params.source_id; expect(spec.isBidRequestValid(moddedBid)).to.be.false; }); @@ -442,9 +443,12 @@ describe('DatablocksAdapter', function() { }); describe('buildRequests', function() { - let request = spec.buildRequests([bid, bid2, nativeBid], bidderRequest); + let request; + before(() => { + request = spec.buildRequests([bid, bid2, nativeBid], bidderRequest); + expect(request).to.exist; + }) - expect(request).to.exist; it('Returns POST method', function() { expect(request.method).to.exist; expect(request.method).to.equal('POST'); diff --git a/test/spec/modules/dfpAdServerVideo_spec.js b/test/spec/modules/dfpAdServerVideo_spec.js index 89485adf28b..39713c2b51a 100644 --- a/test/spec/modules/dfpAdServerVideo_spec.js +++ b/test/spec/modules/dfpAdServerVideo_spec.js @@ -1,43 +1,61 @@ -import { expect } from 'chai'; +import {expect} from 'chai'; import parse from 'url-parse'; -import {buildDfpVideoUrl, buildAdpodVideoUrl, dep} from 'modules/dfpAdServerVideo.js'; -import adUnit from 'test/fixtures/video/adUnit.json'; +import {buildAdpodVideoUrl, buildDfpVideoUrl, dep} from 'modules/dfpAdServerVideo.js'; +import AD_UNIT from 'test/fixtures/video/adUnit.json'; import * as utils from 'src/utils.js'; -import { config } from 'src/config.js'; -import { targeting } from 'src/targeting.js'; -import { auctionManager } from 'src/auctionManager.js'; -import { gdprDataHandler, uspDataHandler } from 'src/adapterManager.js'; +import {deepClone} from 'src/utils.js'; +import {config} from 'src/config.js'; +import {targeting} from 'src/targeting.js'; +import {auctionManager} from 'src/auctionManager.js'; +import {gdprDataHandler, uspDataHandler} from 'src/adapterManager.js'; import * as adpod from 'modules/adpod.js'; -import { server } from 'test/mocks/xhr.js'; +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', - adserverTargeting: { - hb_uuid: 'abc', - hb_cache_id: 'abc', - }, -}; +import {stubAuctionIndex} from '../../helpers/indexStub.js'; +import {AuctionIndex} from '../../../src/auctionIndex.js'; describe('The DFP video support module', function () { before(() => { hook.ready(); }); - let sandbox; + let sandbox, bid, adUnit; beforeEach(() => { sandbox = sinon.sandbox.create(); + bid = { + videoCacheKey: 'abc', + adserverTargeting: { + hb_uuid: 'abc', + hb_cache_id: 'abc', + }, + }; + adUnit = deepClone(AD_UNIT); }); afterEach(() => { sandbox.restore(); }); + function getURL(options) { + return parse(buildDfpVideoUrl(Object.assign({ + adUnit: adUnit, + bid: bid, + params: { + 'iu': 'my/adUnit' + } + }, options))) + } + function getQueryParams(options) { + return utils.parseQS(getURL(options).query); + } + + function getCustomParams(options) { + return utils.parseQS('?' + decodeURIComponent(getQueryParams(options).cust_params)); + } + Object.entries({ params: { params: { @@ -51,27 +69,25 @@ describe('The DFP video support module', function () { 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); + const prm = getQueryParams(options); expect(prm.description_url).to.eql('example.com'); - }) - }) + }); + + it('should use a URI encoded page location as default for description_url', () => { + sandbox.stub(dep, 'ri').callsFake(() => ({page: 'https://example.com?iu=/99999999/news&cust_params=current_hour%3D12%26newscat%3Dtravel&pbjs_debug=true'})); + const prm = getQueryParams(options); + expect(prm.description_url).to.eql('https%3A%2F%2Fexample.com%3Fiu%3D%2F99999999%2Fnews%26cust_params%3Dcurrent_hour%253D12%2526newscat%253Dtravel%26pbjs_debug%3Dtrue'); + }); + }); }) it('should make a legal request URL when given the required params', function () { - const url = parse(buildDfpVideoUrl({ - adUnit: adUnit, - bid: bid, + const url = getURL({ params: { 'iu': 'my/adUnit', 'description_url': 'someUrl.com', } - })); - + }) expect(url.protocol).to.equal('https:'); expect(url.host).to.equal('securepubads.g.doubleclick.net'); @@ -88,15 +104,10 @@ describe('The DFP video support module', function () { }); it('can take an adserver url as a parameter', function () { - const bidCopy = utils.deepClone(bid); - bidCopy.vastUrl = 'vastUrl.example'; - - const url = parse(buildDfpVideoUrl({ - adUnit: adUnit, - bid: bidCopy, + bid.vastUrl = 'vastUrl.example'; + const url = getURL({ url: 'https://video.adserver.example/', - })); - + }) expect(url.host).to.equal('video.adserver.example'); }); @@ -110,161 +121,64 @@ describe('The DFP video support module', function () { }); it('overwrites url params when both url and params object are given', function () { - const url = parse(buildDfpVideoUrl({ - adUnit: adUnit, - bid: bid, + const params = getQueryParams({ url: 'https://video.adserver.example/ads?sz=640x480&iu=/123/aduniturl&impl=s', params: { iu: 'my/adUnit' } - })); + }); - const queryObject = utils.parseQS(url.query); - expect(queryObject.iu).to.equal('my/adUnit'); + expect(params.iu).to.equal('my/adUnit'); }); it('should override param defaults with user-provided ones', function () { - const url = parse(buildDfpVideoUrl({ - adUnit: adUnit, - bid: bid, + const params = getQueryParams({ params: { - 'iu': 'my/adUnit', 'output': 'vast', } - })); - - expect(utils.parseQS(url.query)).to.have.property('output', 'vast'); + }); + expect(params.output).to.equal('vast'); }); it('should include the cache key and adserver targeting in cust_params', function () { - const bidCopy = utils.deepClone(bid); - bidCopy.adserverTargeting = Object.assign(bidCopy.adserverTargeting, { + bid.adserverTargeting = Object.assign(bid.adserverTargeting, { hb_adid: 'ad_id', }); - const url = parse(buildDfpVideoUrl({ - adUnit: adUnit, - bid: bidCopy, - params: { - 'iu': 'my/adUnit' - } - })); - const queryObject = utils.parseQS(url.query); - const customParams = utils.parseQS('?' + decodeURIComponent(queryObject.cust_params)); + const customParams = getCustomParams() expect(customParams).to.have.property('hb_adid', 'ad_id'); expect(customParams).to.have.property('hb_uuid', bid.videoCacheKey); expect(customParams).to.have.property('hb_cache_id', bid.videoCacheKey); }); - it('should include the us_privacy key when USP Consent is available', function () { - let uspDataHandlerStub = sinon.stub(uspDataHandler, 'getConsentData'); - uspDataHandlerStub.returns('1YYY'); - - const bidCopy = utils.deepClone(bid); - bidCopy.adserverTargeting = Object.assign(bidCopy.adserverTargeting, { - hb_adid: 'ad_id', - }); - - const url = parse(buildDfpVideoUrl({ - adUnit: adUnit, - bid: bidCopy, - params: { - 'iu': 'my/adUnit' - } - })); - const queryObject = utils.parseQS(url.query); - expect(queryObject.us_privacy).to.equal('1YYY'); - uspDataHandlerStub.restore(); - }); - - it('should not include the us_privacy key when USP Consent is not available', function () { - const bidCopy = utils.deepClone(bid); - bidCopy.adserverTargeting = Object.assign(bidCopy.adserverTargeting, { - hb_adid: 'ad_id', - }); - - const url = parse(buildDfpVideoUrl({ - adUnit: adUnit, - bid: bidCopy, - params: { - 'iu': 'my/adUnit' - } - })); - const queryObject = utils.parseQS(url.query); - expect(queryObject.us_privacy).to.equal(undefined); - }); - it('should include the GDPR keys when GDPR Consent is available', function () { - let gdprDataHandlerStub = sinon.stub(gdprDataHandler, 'getConsentData'); - gdprDataHandlerStub.returns({ + sandbox.stub(gdprDataHandler, 'getConsentData').returns({ gdprApplies: true, consentString: 'consent', addtlConsent: 'moreConsent' }); - - const bidCopy = utils.deepClone(bid); - bidCopy.adserverTargeting = Object.assign(bidCopy.adserverTargeting, { - hb_adid: 'ad_id', - }); - - const url = parse(buildDfpVideoUrl({ - adUnit: adUnit, - bid: bidCopy, - params: { - 'iu': 'my/adUnit' - } - })); - const queryObject = utils.parseQS(url.query); + const queryObject = getQueryParams(); expect(queryObject.gdpr).to.equal('1'); expect(queryObject.gdpr_consent).to.equal('consent'); expect(queryObject.addtl_consent).to.equal('moreConsent'); - gdprDataHandlerStub.restore(); }); it('should not include the GDPR keys when GDPR Consent is not available', function () { - const bidCopy = utils.deepClone(bid); - bidCopy.adserverTargeting = Object.assign(bidCopy.adserverTargeting, { - hb_adid: 'ad_id', - }); - - const url = parse(buildDfpVideoUrl({ - adUnit: adUnit, - bid: bidCopy, - params: { - 'iu': 'my/adUnit' - } - })); - const queryObject = utils.parseQS(url.query); + const queryObject = getQueryParams() expect(queryObject.gdpr).to.equal(undefined); expect(queryObject.gdpr_consent).to.equal(undefined); expect(queryObject.addtl_consent).to.equal(undefined); }); it('should only include the GDPR keys for GDPR Consent fields with values', function () { - let gdprDataHandlerStub = sinon.stub(gdprDataHandler, 'getConsentData'); - gdprDataHandlerStub.returns({ + sandbox.stub(gdprDataHandler, 'getConsentData').returns({ gdprApplies: true, consentString: 'consent', }); - - const bidCopy = utils.deepClone(bid); - bidCopy.adserverTargeting = Object.assign(bidCopy.adserverTargeting, { - hb_adid: 'ad_id', - }); - - const url = parse(buildDfpVideoUrl({ - adUnit: adUnit, - bid: bidCopy, - params: { - 'iu': 'my/adUnit' - } - })); - const queryObject = utils.parseQS(url.query); + const queryObject = getQueryParams() expect(queryObject.gdpr).to.equal('1'); expect(queryObject.gdpr_consent).to.equal('consent'); expect(queryObject.addtl_consent).to.equal(undefined); - gdprDataHandlerStub.restore(); }); - describe('GAM PPID', () => { let ppid; let getPPIDStub; @@ -280,29 +194,283 @@ describe('The DFP video support module', function () { 'url': {url: 'https://video.adserver.mock/', params: {'iu': 'mock/unit'}} }).forEach(([t, opts]) => { describe(`when using ${t}`, () => { - function buildUrlAndGetParams() { - const url = parse(buildDfpVideoUrl(Object.assign({ - adUnit: adUnit, - bid: deepClone(bid), - }, opts))); - return utils.parseQS(url.query); - } - it('should be included if available', () => { ppid = 'mockPPID'; - const q = buildUrlAndGetParams(); + const q = getQueryParams(opts); expect(q.ppid).to.equal('mockPPID'); }); it('should not be included if not available', () => { ppid = undefined; - const q = buildUrlAndGetParams(); + const q = getQueryParams(opts); expect(q.hasOwnProperty('ppid')).to.be.false; }) }) }) }) + describe('ORTB video parameters', () => { + Object.entries({ + plcmt: [ + { + video: { + plcmt: 1 + }, + expected: '1' + } + ], + min_ad_duration: [ + { + video: { + minduration: 123 + }, + expected: '123000' + } + ], + max_ad_duration: [ + { + video: { + maxduration: 321 + }, + expected: '321000' + } + ], + vpos: [ + { + video: { + startdelay: 0 + }, + expected: 'preroll' + }, + { + video: { + startdelay: -1 + }, + expected: 'midroll' + }, + { + video: { + startdelay: -2 + }, + expected: 'postroll' + }, + { + video: { + startdelay: 10 + }, + expected: 'midroll' + } + ], + vconp: [ + { + video: { + playbackmethod: [7] + }, + expected: '2' + }, + { + video: { + playbackmethod: [7, 1] + }, + expected: undefined + } + ], + vpa: [ + { + video: { + playbackmethod: [1, 2, 4, 5, 6, 7] + }, + expected: 'auto' + }, + { + video: { + playbackmethod: [3, 7], + }, + expected: 'click' + }, + { + video: { + playbackmethod: [1, 3], + }, + expected: undefined + } + ], + vpmute: [ + { + video: { + playbackmethod: [1, 3, 4, 5, 7] + }, + expected: '0' + }, + { + video: { + playbackmethod: [2, 6, 7], + }, + expected: '1' + }, + { + video: { + playbackmethod: [1, 2] + }, + expected: undefined + } + ] + }).forEach(([param, cases]) => { + describe(param, () => { + cases.forEach(({video, expected}) => { + describe(`when mediaTypes.video has ${JSON.stringify(video)}`, () => { + it(`fills in ${param} = ${expected}`, () => { + Object.assign(adUnit.mediaTypes.video, video); + expect(getQueryParams()[param]).to.eql(expected); + }); + it(`does not override pub-provided params.${param}`, () => { + Object.assign(adUnit.mediaTypes.video, video); + expect(getQueryParams({ + params: { + [param]: 'OG' + } + })[param]).to.eql('OG'); + }); + it('does not fill if param has no value', () => { + expect(getQueryParams().hasOwnProperty(param)).to.be.false; + }) + }) + }) + }) + }) + }); + + describe('ppsj', () => { + let ortb2; + beforeEach(() => { + ortb2 = null; + }) + + function getSignals() { + const ppsj = JSON.parse(atob(getQueryParams().ppsj)); + return Object.fromEntries(ppsj.PublisherProvidedTaxonomySignals.map(sig => [sig.taxonomy, sig.values])); + } + + Object.entries({ + 'FPD from bid request'() { + bid.requestId = 'req-id'; + sandbox.stub(auctionManager, 'index').get(() => stubAuctionIndex({ + bidRequests: [ + { + bidId: 'req-id', + ortb2 + } + ] + })); + }, + 'global FPD from auction'() { + bid.auctionId = 'auid'; + sandbox.stub(auctionManager, 'index').get(() => new AuctionIndex(() => [{ + getAuctionId: () => 'auid', + getFPD: () => ({ + global: ortb2 + }) + }])); + } + }).forEach(([t, setup]) => { + describe(`using ${t}`, () => { + beforeEach(setup); + it('does not fill if there\'s no segments in segtax 4 or 6', () => { + ortb2 = { + site: { + content: { + data: [ + { + segment: [ + {id: '1'}, + {id: '2'} + ] + }, + ] + } + }, + user: { + data: [ + { + ext: { + segtax: 1, + }, + segment: [ + {id: '3'} + ] + } + ] + } + } + expect(getQueryParams().ppsj).to.not.exist; + }); + + const SEGMENTS = [ + { + ext: { + segtax: 4, + }, + segment: [ + {id: '4-1'}, + {id: '4-2'} + ] + }, + { + ext: { + segtax: 4, + }, + segment: [ + {id: '4-2'}, + {id: '4-3'} + ] + }, + { + ext: { + segtax: 6, + }, + segment: [ + {id: '6-1'}, + {id: '6-2'} + ] + }, + { + ext: { + segtax: 6, + }, + segment: [ + {id: '6-2'}, + {id: '6-3'} + ] + }, + ] + + it('collects user.data segments with segtax = 4 into IAB_AUDIENCE_1_1', () => { + ortb2 = { + user: { + data: SEGMENTS + } + } + expect(getSignals()).to.eql({ + IAB_AUDIENCE_1_1: ['4-1', '4-2', '4-3'] + }) + }) + + it('collects site.content.data segments with segtax = 6 into IAB_CONTENT_2_2', () => { + ortb2 = { + site: { + content: { + data: SEGMENTS + } + } + } + expect(getSignals()).to.eql({ + IAB_CONTENT_2_2: ['6-1', '6-2', '6-3'] + }) + }) + }) + }) + }) + describe('special targeting unit test', function () { const allTargetingData = { 'hb_format': 'video', @@ -629,7 +797,6 @@ describe('The DFP video support module', function () { expect(queryParams).to.have.property('unviewed_position_start', '1'); expect(queryParams).to.have.property('url'); expect(queryParams).to.have.property('cust_params'); - expect(queryParams).to.have.property('us_privacy', '1YYY'); expect(queryParams).to.have.property('gdpr', '1'); expect(queryParams).to.have.property('gdpr_consent', 'consent'); expect(queryParams).to.have.property('addtl_consent', 'moreConsent'); diff --git a/test/spec/modules/dgkeywordRtdProvider_spec.js b/test/spec/modules/dgkeywordRtdProvider_spec.js index 754740b7a64..ff88ea0512f 100644 --- a/test/spec/modules/dgkeywordRtdProvider_spec.js +++ b/test/spec/modules/dgkeywordRtdProvider_spec.js @@ -91,6 +91,22 @@ describe('Digital Garage Keyword Module', function () { expect(dgRtd.getTargetBidderOfDgKeywords(adUnits_no_target)).an('array') .that.is.empty; }); + it('convertKeywordsToString method unit test', function () { + const keywordsTest = [ + { keywords: { param1: 'keywords1' }, result: 'param1=keywords1' }, + { keywords: { param1: 'keywords1', param2: 'keywords2' }, result: 'param1=keywords1,param2=keywords2' }, + { keywords: { p1: 'k1', p2: 'k2', p: 'k' }, result: 'p1=k1,p2=k2,p=k' }, + { keywords: { p1: 'k1', p2: 'k2', p: ['k'] }, result: 'p1=k1,p2=k2,p=k' }, + { keywords: { p1: 'k1', p2: ['k21', 'k22'], p: ['k'] }, result: 'p1=k1,p2=k21,p2=k22,p=k' }, + { keywords: { p1: ['k11', 'k12', 'k13'], p2: ['k21', 'k22'], p: ['k'] }, result: 'p1=k11,p1=k12,p1=k13,p2=k21,p2=k22,p=k' }, + { keywords: { p1: [], p2: ['', ''], p: [''] }, result: 'p1,p2,p' }, + { keywords: { p1: 1, p2: [1, 'k2'], p: '' }, result: 'p1,p2=k2,p' }, + { keywords: { p1: ['k1', 2, 'k3'], p2: [1, 2], p: 3 }, result: 'p1=k1,p1=k3,p2,p' }, + ]; + for (const test of keywordsTest) { + expect(dgRtd.convertKeywordsToString(test.keywords)).equal(test.result); + } + }) it('should have targets', function () { const adUnits_targets = [ { @@ -242,16 +258,16 @@ describe('Digital Garage Keyword Module', function () { expect(targets[1].bidder).to.be.equal('dg2'); expect(targets[1].params.placementId).to.be.equal(99999998); expect(targets[1].params.dgkeyword).to.be.an('undefined'); - expect(targets[1].params.keywords).to.be.an('undefined'); + expect(targets[1].params.ortb2Imp).to.be.an('undefined'); targets = pbjs.adUnits[1].bids; expect(targets[0].bidder).to.be.equal('dg'); expect(targets[0].params.placementId).to.be.equal(99999996); expect(targets[0].params.dgkeyword).to.be.an('undefined'); - expect(targets[0].params.keywords).to.be.an('undefined'); + expect(targets[0].params.ortb2Imp).to.be.an('undefined'); expect(targets[2].bidder).to.be.equal('dg3'); expect(targets[2].params.placementId).to.be.equal(99999994); expect(targets[2].params.dgkeyword).to.be.an('undefined'); - expect(targets[2].params.keywords).to.be.an('undefined'); + expect(targets[2].params.ortb2Imp).to.be.an('undefined'); expect(pbjs.getBidderConfig()).to.be.deep.equal({}); @@ -275,16 +291,16 @@ describe('Digital Garage Keyword Module', function () { expect(targets[1].bidder).to.be.equal('dg2'); expect(targets[1].params.placementId).to.be.equal(99999998); expect(targets[1].params.dgkeyword).to.be.an('undefined'); - expect(targets[1].params.keywords).to.be.an('undefined'); + expect(targets[1].params.ortb2Imp).to.be.an('undefined'); targets = pbjs.adUnits[1].bids; expect(targets[0].bidder).to.be.equal('dg'); expect(targets[0].params.placementId).to.be.equal(99999996); expect(targets[0].params.dgkeyword).to.be.an('undefined'); - expect(targets[0].params.keywords).to.be.an('undefined'); + expect(targets[0].params.ortb2Imp).to.be.an('undefined'); expect(targets[2].bidder).to.be.equal('dg3'); expect(targets[2].params.placementId).to.be.equal(99999994); expect(targets[2].params.dgkeyword).to.be.an('undefined'); - expect(targets[2].params.keywords).to.be.an('undefined'); + expect(targets[2].params.ortb2Imp).to.be.an('undefined'); expect(pbjs.getBidderConfig()).to.be.deep.equal({}); @@ -318,16 +334,16 @@ describe('Digital Garage Keyword Module', function () { expect(targets[1].bidder).to.be.equal('dg2'); expect(targets[1].params.placementId).to.be.equal(99999998); expect(targets[1].params.dgkeyword).to.be.an('undefined'); - expect(targets[1].params.keywords).to.be.deep.equal(SUCCESS_RESULT); + expect(targets[1].ortb2Imp.ext.data.keywords).to.be.deep.equal(dgRtd.convertKeywordsToString(SUCCESS_RESULT)); targets = pbjs.adUnits[1].bids; expect(targets[0].bidder).to.be.equal('dg'); expect(targets[0].params.placementId).to.be.equal(99999996); expect(targets[0].params.dgkeyword).to.be.an('undefined'); - expect(targets[0].params.keywords).to.be.deep.equal(SUCCESS_RESULT); + expect(targets[0].ortb2Imp.ext.data.keywords).to.be.deep.equal(dgRtd.convertKeywordsToString(SUCCESS_RESULT)); expect(targets[2].bidder).to.be.equal('dg3'); expect(targets[2].params.placementId).to.be.equal(99999994); expect(targets[2].params.dgkeyword).to.be.an('undefined'); - expect(targets[2].params.keywords).to.be.an('undefined'); + expect(targets[2].ortb2Imp).to.be.an('undefined'); if (!IGNORE_SET_ORTB2) { expect(pbjs.getBidderConfig()).to.be.deep.equal({ diff --git a/test/spec/modules/discoveryBidAdapter_spec.js b/test/spec/modules/discoveryBidAdapter_spec.js index 078add73046..961ccb33c4f 100644 --- a/test/spec/modules/discoveryBidAdapter_spec.js +++ b/test/spec/modules/discoveryBidAdapter_spec.js @@ -1,5 +1,17 @@ import { expect } from 'chai'; -import { spec } from 'modules/discoveryBidAdapter.js'; +import { + spec, + getPmgUID, + storage, + getPageTitle, + getPageDescription, + getPageKeywords, + getConnectionDownLink, + THIRD_PARTY_COOKIE_ORIGIN, + COOKIE_KEY_MGUID, + getCurrentTimeToUTCString +} from 'modules/discoveryBidAdapter.js'; +import * as utils from 'src/utils.js'; describe('discovery:BidAdapterTests', function () { let bidRequestData = { @@ -11,12 +23,59 @@ describe('discovery:BidAdapterTests', function () { bidder: 'discovery', params: { token: 'd0f4902b616cc5c38cbe0a08676d0ed9', + siteId: 'siteId_01', + zoneId: 'zoneId_01', + publisher: '52', + position: 'left', + referrer: 'https://discovery.popin.cc', + }, + refererInfo: { + page: 'https://discovery.popin.cc', + stack: [ + 'a.com', + 'b.com' + ] }, mediaTypes: { banner: { sizes: [[300, 250]], + pos: 'left', + }, + }, + ortb2: { + user: { + ext: { + data: { + CxSegments: [] + } + } + }, + site: { + domain: 'discovery.popin.cc', + publisher: { + domain: 'discovery.popin.cc' + }, + page: 'https://discovery.popin.cc', + cat: ['IAB-19', 'IAB-20'], }, }, + ortb2Imp: { + ext: { + gpid: 'adslot_gpid', + tid: 'tid_01', + data: { + browsi: { + browsiViewability: 'NA', + }, + adserver: { + name: 'adserver_name', + adslot: 'adslot_name', + }, + keywords: ['travel', 'sport'], + pbadslot: '202309999' + } + } + }, adUnitCode: 'regular_iframe', transactionId: 'd163f9e2-7ecd-4c2c-a3bd-28ceb52a60ee', sizes: [[300, 250]], @@ -32,6 +91,77 @@ describe('discovery:BidAdapterTests', function () { }; let request = []; + let bidRequestDataNoParams = { + bidderCode: 'discovery', + auctionId: 'ff66e39e-4075-4d18-9854-56fde9b879ac', + bidderRequestId: '4fec04e87ad785', + bids: [ + { + bidder: 'discovery', + params: { + referrer: 'https://discovery.popin.cc', + }, + refererInfo: { + page: 'https://discovery.popin.cc', + stack: [ + 'a.com', + 'b.com' + ] + }, + mediaTypes: { + banner: { + sizes: [[300, 250]], + pos: 'left', + }, + }, + ortb2: { + user: { + ext: { + data: { + CxSegments: [] + } + } + }, + site: { + domain: 'discovery.popin.cc', + publisher: { + domain: 'discovery.popin.cc' + }, + page: 'https://discovery.popin.cc', + cat: ['IAB-19', 'IAB-20'], + }, + }, + ortb2Imp: { + ext: { + gpid: 'adslot_gpid', + tid: 'tid_01', + data: { + browsi: { + browsiViewability: 'NA', + }, + adserver: { + name: 'adserver_name', + adslot: 'adslot_name', + }, + keywords: ['travel', 'sport'], + pbadslot: '202309999' + } + } + }, + adUnitCode: 'regular_iframe', + transactionId: 'd163f9e2-7ecd-4c2c-a3bd-28ceb52a60ee', + sizes: [[300, 250]], + bidId: '276092a19e05eb', + bidderRequestId: '1fadae168708b', + auctionId: 'ff66e39e-4075-4d18-9854-56fde9b879ac', + src: 'client', + bidRequestsCount: 1, + bidderRequestsCount: 1, + bidderWinsCount: 0, + }, + ], + }; + it('discovery:validate_pub_params', function () { expect( spec.isBidRequestValid({ @@ -45,12 +175,62 @@ describe('discovery:BidAdapterTests', function () { ).to.equal(true); }); + it('isBidRequestValid:no_params', function () { + expect( + spec.isBidRequestValid({ + bidder: 'discovery', + params: {}, + }) + ).to.equal(true); + }); + it('discovery:validate_generated_params', function () { request = spec.buildRequests(bidRequestData.bids, bidRequestData); let req_data = JSON.parse(request.data); expect(req_data.imp).to.have.lengthOf(1); }); + describe('discovery: buildRequests', function() { + describe('getPmgUID function', function() { + let sandbox; + + beforeEach(() => { + sandbox = sinon.sandbox.create(); + sandbox.stub(storage, 'getCookie'); + sandbox.stub(storage, 'setCookie'); + sandbox.stub(utils, 'generateUUID').returns('new-uuid'); + sandbox.stub(storage, 'cookiesAreEnabled'); + }) + + afterEach(() => { + sandbox.restore(); + }); + + it('should generate new UUID and set cookie if not exists', () => { + storage.cookiesAreEnabled.callsFake(() => true); + storage.getCookie.callsFake(() => null); + const uid = getPmgUID(); + expect(uid).to.equal('new-uuid'); + expect(storage.setCookie.calledOnce).to.be.true; + }); + + it('should return existing UUID from cookie', () => { + storage.cookiesAreEnabled.callsFake(() => true); + storage.getCookie.callsFake(() => 'existing-uuid'); + const uid = getPmgUID(); + expect(uid).to.equal('existing-uuid'); + expect(storage.setCookie.called).to.be.false; + }); + + it('should not set new UUID when cookies are not enabled', () => { + storage.cookiesAreEnabled.callsFake(() => false); + storage.getCookie.callsFake(() => null); + getPmgUID(); + expect(storage.setCookie.calledOnce).to.be.false; + }); + }) + }); + it('discovery:validate_response_params', function () { let tempAdm = '' tempAdm += '%3Cscr'; @@ -90,4 +270,324 @@ describe('discovery:BidAdapterTests', function () { expect(bid.height).to.equal(250); expect(bid.currency).to.equal('USD'); }); + + describe('discovery: getUserSyncs', function() { + const COOKY_SYNC_IFRAME_URL = 'https://asset.popin.cc/js/cookieSync.html'; + const IFRAME_ENABLED = { + iframeEnabled: true, + pixelEnabled: false, + }; + const IFRAME_DISABLED = { + iframeEnabled: false, + pixelEnabled: false, + }; + const GDPR_CONSENT = { + consentString: 'gdprConsentString', + gdprApplies: true + }; + const USP_CONSENT = { + consentString: 'uspConsentString' + } + + let syncParamUrl = `dm=${encodeURIComponent(location.origin || `https://${location.host}`)}`; + syncParamUrl += '&gdpr=1&gdpr_consent=gdprConsentString&ccpa_consent=uspConsentString'; + const expectedIframeSyncs = [ + { + type: 'iframe', + url: `${COOKY_SYNC_IFRAME_URL}?${syncParamUrl}` + } + ]; + + it('should return nothing if iframe is disabled', () => { + const userSyncs = spec.getUserSyncs(IFRAME_DISABLED, undefined, GDPR_CONSENT, USP_CONSENT, undefined); + expect(userSyncs).to.be.undefined; + }); + + it('should do userSyncs if iframe is enabled', () => { + const userSyncs = spec.getUserSyncs(IFRAME_ENABLED, undefined, GDPR_CONSENT, USP_CONSENT, undefined); + expect(userSyncs).to.deep.equal(expectedIframeSyncs); + }); + }); +}); + +describe('discovery Bid Adapter Tests', function () { + describe('buildRequests', () => { + describe('getPageTitle function', function() { + let sandbox; + + beforeEach(() => { + sandbox = sinon.createSandbox(); + }); + + afterEach(() => { + sandbox.restore(); + }); + + it('should return the top document title if available', function() { + const fakeTopDocument = { + title: 'Top Document Title', + querySelector: () => ({ content: 'Top Document Title test' }) + }; + const fakeTopWindow = { + document: fakeTopDocument + }; + const result = getPageTitle({ top: fakeTopWindow }); + expect(result).to.equal('Top Document Title'); + }); + + it('should return the content of top og:title meta tag if title is empty', function() { + const ogTitleContent = 'Top OG Title Content'; + const fakeTopWindow = { + document: { + title: '', + querySelector: sandbox.stub().withArgs('meta[property="og:title"]').returns({ content: ogTitleContent }) + } + }; + + const result = getPageTitle({ top: fakeTopWindow }); + expect(result).to.equal(ogTitleContent); + }); + + it('should return the document title if no og:title meta tag is present', function() { + document.title = 'Test Page Title'; + sandbox.stub(document, 'querySelector').withArgs('meta[property="og:title"]').returns(null); + + const result = getPageTitle({ top: undefined }); + expect(result).to.equal('Test Page Title'); + }); + + it('should return the content of og:title meta tag if present', function() { + document.title = ''; + const ogTitleContent = 'Top OG Title Content'; + sandbox.stub(document, 'querySelector').withArgs('meta[property="og:title"]').returns({ content: ogTitleContent }); + const result = getPageTitle({ top: undefined }); + expect(result).to.equal(ogTitleContent); + }); + + it('should return an empty string if no title or og:title meta tag is found', function() { + document.title = ''; + sandbox.stub(document, 'querySelector').withArgs('meta[property="og:title"]').returns(null); + const result = getPageTitle({ top: undefined }); + expect(result).to.equal(''); + }); + + it('should handle exceptions when accessing top.document and fallback to current document', function() { + const fakeWindow = { + get top() { + throw new Error('Access denied'); + } + }; + const ogTitleContent = 'Current OG Title Content'; + document.title = 'Current Document Title'; + sandbox.stub(document, 'querySelector').withArgs('meta[property="og:title"]').returns({ content: ogTitleContent }); + const result = getPageTitle(fakeWindow); + expect(result).to.equal('Current Document Title'); + }); + }); + + describe('getPageDescription function', function() { + let sandbox; + + beforeEach(() => { + sandbox = sinon.createSandbox(); + }); + + afterEach(() => { + sandbox.restore(); + }); + + it('should return the top document description if available', function() { + const descriptionContent = 'Top Document Description'; + const fakeTopDocument = { + querySelector: sandbox.stub().withArgs('meta[name="description"]').returns({ content: descriptionContent }) + }; + const fakeTopWindow = { document: fakeTopDocument }; + const result = getPageDescription({ top: fakeTopWindow }); + expect(result).to.equal(descriptionContent); + }); + + it('should return the top document og:description if description is not present', function() { + const ogDescriptionContent = 'Top OG Description'; + const fakeTopDocument = { + querySelector: sandbox.stub().withArgs('meta[property="og:description"]').returns({ content: ogDescriptionContent }) + }; + const fakeTopWindow = { document: fakeTopDocument }; + const result = getPageDescription({ top: fakeTopWindow }); + expect(result).to.equal(ogDescriptionContent); + }); + + it('should return the current document description if top document is not accessible', function() { + const descriptionContent = 'Current Document Description'; + sandbox.stub(document, 'querySelector') + .withArgs('meta[name="description"]').returns({ content: descriptionContent }) + const fakeWindow = { + get top() { + throw new Error('Access denied'); + } + }; + const result = getPageDescription(fakeWindow); + expect(result).to.equal(descriptionContent); + }); + + it('should return the current document og:description if description is not present and top document is not accessible', function() { + const ogDescriptionContent = 'Current OG Description'; + sandbox.stub(document, 'querySelector') + .withArgs('meta[property="og:description"]').returns({ content: ogDescriptionContent }); + + const fakeWindow = { + get top() { + throw new Error('Access denied'); + } + }; + const result = getPageDescription(fakeWindow); + expect(result).to.equal(ogDescriptionContent); + }); + }); + + describe('getPageKeywords function', function() { + let sandbox; + + beforeEach(() => { + sandbox = sinon.createSandbox(); + }); + + afterEach(() => { + sandbox.restore(); + }); + + it('should return the top document keywords if available', function() { + const keywordsContent = 'keyword1, keyword2, keyword3'; + const fakeTopDocument = { + querySelector: sandbox.stub() + .withArgs('meta[name="keywords"]').returns({ content: keywordsContent }) + }; + const fakeTopWindow = { document: fakeTopDocument }; + + const result = getPageKeywords({ top: fakeTopWindow }); + expect(result).to.equal(keywordsContent); + }); + + it('should return the current document keywords if top document is not accessible', function() { + const keywordsContent = 'keyword1, keyword2, keyword3'; + sandbox.stub(document, 'querySelector') + .withArgs('meta[name="keywords"]').returns({ content: keywordsContent }); + + // æ¨Ąæ‹ŸéĄļåą‚įĒ—åŖčŽŋ问åŧ‚常 + const fakeWindow = { + get top() { + throw new Error('Access denied'); + } + }; + + const result = getPageKeywords(fakeWindow); + expect(result).to.equal(keywordsContent); + }); + + it('should return an empty string if no keywords meta tag is found', function() { + sandbox.stub(document, 'querySelector').withArgs('meta[name="keywords"]').returns(null); + + const result = getPageKeywords(); + expect(result).to.equal(''); + }); + }); + describe('getConnectionDownLink function', function() { + let sandbox; + + beforeEach(() => { + sandbox = sinon.createSandbox(); + }); + + afterEach(() => { + sandbox.restore(); + }); + + it('should return the downlink value as a string if available', function() { + const downlinkValue = 2.5; + const fakeNavigator = { + connection: { + downlink: downlinkValue + } + }; + + const result = getConnectionDownLink({ navigator: fakeNavigator }); + expect(result).to.equal(downlinkValue.toString()); + }); + + it('should return undefined if downlink is not available', function() { + const fakeNavigator = { + connection: {} + }; + + const result = getConnectionDownLink({ navigator: fakeNavigator }); + expect(result).to.be.undefined; + }); + + it('should return undefined if connection is not available', function() { + const fakeNavigator = {}; + + const result = getConnectionDownLink({ navigator: fakeNavigator }); + expect(result).to.be.undefined; + }); + + it('should handle cases where navigator is not defined', function() { + const result = getConnectionDownLink({}); + expect(result).to.be.undefined; + }); + }); + + describe('getUserSyncs with message event listener', function() { + function messageHandler(event) { + if (!event.data || event.origin !== THIRD_PARTY_COOKIE_ORIGIN) { + return; + } + + window.removeEventListener('message', messageHandler, true); + event.stopImmediatePropagation(); + + const response = event.data; + if (!response.optout && response.mguid) { + storage.setCookie(COOKIE_KEY_MGUID, response.mguid, getCurrentTimeToUTCString()); + } + } + + let sandbox; + + beforeEach(() => { + sandbox = sinon.createSandbox(); + sandbox.stub(storage, 'setCookie'); + sandbox.stub(window, 'removeEventListener'); + }); + + afterEach(() => { + sandbox.restore(); + }); + + it('should set a cookie when a valid message is received', () => { + const fakeEvent = { + data: { optout: '', mguid: '12345' }, + origin: THIRD_PARTY_COOKIE_ORIGIN, + stopImmediatePropagation: sinon.spy() + }; + + messageHandler(fakeEvent); + + expect(fakeEvent.stopImmediatePropagation.calledOnce).to.be.true; + expect(window.removeEventListener.calledWith('message', messageHandler, true)).to.be.true; + expect(storage.setCookie.calledWith(COOKIE_KEY_MGUID, '12345', sinon.match.string)).to.be.true; + }); + it('should not do anything when an invalid message is received', () => { + const fakeEvent = { + data: null, + origin: 'http://invalid-origin.com', + stopImmediatePropagation: sinon.spy() + }; + + messageHandler(fakeEvent); + + expect(fakeEvent.stopImmediatePropagation.notCalled).to.be.true; + expect(window.removeEventListener.notCalled).to.be.true; + expect(storage.setCookie.notCalled).to.be.true; + }); + }); + }); }); diff --git a/test/spec/modules/dmdIdSystem_spec.js b/test/spec/modules/dmdIdSystem_spec.js index 3096a8e55f5..16c32f184a3 100644 --- a/test/spec/modules/dmdIdSystem_spec.js +++ b/test/spec/modules/dmdIdSystem_spec.js @@ -60,7 +60,7 @@ describe('Dmd ID System', function () { it('Should invoke callback with response from API call', function () { const callbackSpy = sinon.spy(); - const domain = utils.getWindowLocation() + const domain = utils.getWindowLocation().href; const callback = dmdIdSubmodule.getId(config).callback; callback(callbackSpy); const request = server.requests[0]; @@ -73,7 +73,7 @@ describe('Dmd ID System', function () { it('Should log error if API response is not valid', function () { const callbackSpy = sinon.spy(); - const domain = utils.getWindowLocation() + const domain = utils.getWindowLocation().href; const callback = dmdIdSubmodule.getId(config).callback; callback(callbackSpy); const request = server.requests[0]; diff --git a/test/spec/modules/docereeAdManagerBidAdapter_spec.js b/test/spec/modules/docereeAdManagerBidAdapter_spec.js new file mode 100644 index 00000000000..26b054f4e29 --- /dev/null +++ b/test/spec/modules/docereeAdManagerBidAdapter_spec.js @@ -0,0 +1,125 @@ +import { expect } from 'chai'; +import { spec } from '../../../modules/docereeAdManagerBidAdapter.js'; +import { config } from '../../../src/config.js'; + +describe('docereeadmanager', function () { + config.setConfig({ + docereeadmanager: { + user: { + data: { + email: '', + firstname: '', + lastname: '', + mobile: '', + specialization: '', + organization: '', + hcpid: '', + dob: '', + gender: '', + city: '', + state: '', + country: '', + hashedhcpid: '', + hashedemail: '', + hashedmobile: '', + userid: '', + zipcode: '', + userconsent: '', + }, + }, + }, + }); + let bid = { + bidId: 'testing', + bidder: 'docereeadmanager', + params: { + placementId: 'DOC-19-1', + gdpr: '1', + gdprconsent: + 'CPQfU1jPQfU1jG0AAAENAwCAAAAAAAAAAAAAAAAAAAAA.IGLtV_T9fb2vj-_Z99_tkeYwf95y3p-wzhheMs-8NyZeH_B4Wv2MyvBX4JiQKGRgksjLBAQdtHGlcTQgBwIlViTLMYk2MjzNKJrJEilsbO2dYGD9Pn8HT3ZCY70-vv__7v3ff_3g', + }, + }; + + describe('isBidRequestValid', function () { + it('Should return true if placementId is present', function () { + expect(spec.isBidRequestValid(bid)).to.be.true; + }); + it('Should return false if placementId is not present', function () { + delete bid.params.placementId; + expect(spec.isBidRequestValid(bid)).to.be.false; + }); + }); + + describe('isGdprConsentPresent', function () { + it('Should return true if gdpr consent is present', function () { + expect(spec.isGdprConsentPresent(bid)).to.be.true; + }); + }); + + describe('buildRequests', function () { + let serverRequest = spec.buildRequests([bid]); + serverRequest = serverRequest[0]; + it('Creates a ServerRequest object with method, URL and data', function () { + expect(serverRequest).to.exist; + expect(serverRequest.method).to.exist; + expect(serverRequest.url).to.exist; + expect(serverRequest.data).to.exist; + }); + it('Returns POST method', function () { + expect(serverRequest.method).to.equal('POST'); + }); + it('Returns valid URL', function () { + expect(serverRequest.url).to.equal('https://dai.doceree.com/drs/quest'); + }); + }); + describe('interpretResponse', function () { + it('Should interpret banner response', function () { + const banner = { + body: { + cpm: 3.576, + currency: 'USD', + width: 250, + height: 300, + ad: '

I am an ad

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

I am an ad

'); + expect(dataItem.ttl).to.equal(30); + expect(dataItem.netRevenue).to.be.true; + expect(dataItem.currency).to.equal('USD'); + expect(dataItem.creativeId).to.equal('div-1'); + expect(dataItem.meta.advertiserDomains).to.be.an('array').that.is.not + .empty; + }); + }); +}); diff --git a/test/spec/modules/docereeBidAdapter_spec.js b/test/spec/modules/docereeBidAdapter_spec.js index dadbb56b0c0..25da8b256fc 100644 --- a/test/spec/modules/docereeBidAdapter_spec.js +++ b/test/spec/modules/docereeBidAdapter_spec.js @@ -1,6 +1,7 @@ import {expect} from 'chai'; import {spec} from '../../../modules/docereeBidAdapter.js'; import { config } from '../../../src/config.js'; +import * as utils from 'src/utils.js'; describe('BidlabBidAdapter', function () { config.setConfig({ @@ -102,4 +103,36 @@ describe('BidlabBidAdapter', function () { expect(dataItem.meta.advertiserDomains[0]).to.equal('doceree.com') }); }) + describe('onBidWon', function () { + beforeEach(function() { + sinon.stub(utils, 'triggerPixel'); + }); + afterEach(function() { + utils.triggerPixel.restore(); + }); + it('exists and is a function', () => { + expect(spec.onBidWon).to.exist.and.to.be.a('function'); + }); + it('should return nothing', function () { + var response = spec.onBidWon({}); + expect(response).to.be.an('undefined') + expect(utils.triggerPixel.called).to.equal(true); + }); + }); + describe('onTimeout', function () { + beforeEach(function() { + sinon.stub(utils, 'triggerPixel'); + }); + afterEach(function() { + utils.triggerPixel.restore(); + }); + it('exists and is a function', () => { + expect(spec.onTimeout).to.exist.and.to.be.a('function'); + }); + it('should return nothing', function () { + var response = spec.onBidWon([]); + expect(response).to.be.an('undefined') + expect(utils.triggerPixel.called).to.equal(true); + }); + }); }); diff --git a/test/spec/modules/dsp_genieeBidAdapter_spec.js b/test/spec/modules/dsp_genieeBidAdapter_spec.js new file mode 100644 index 00000000000..94ec1011fbf --- /dev/null +++ b/test/spec/modules/dsp_genieeBidAdapter_spec.js @@ -0,0 +1,173 @@ +import { expect } from 'chai'; +import { spec } from 'modules/dsp_genieeBidAdapter.js'; +import { config } from 'src/config'; + +describe('Geniee adapter tests', () => { + const validBidderRequest = { + code: 'sample_request', + bids: [{ + bidId: 'bid-id', + bidder: 'dsp_geniee', + params: { + test: 1 + } + }], + gdprConsent: { + gdprApplies: false + }, + uspConsent: '1YNY' + }; + + describe('isBidRequestValid function test', () => { + it('valid', () => { + expect(spec.isBidRequestValid(validBidderRequest.bids[0])).equal(true); + }); + }); + describe('buildRequests function test', () => { + it('auction', () => { + const request = spec.buildRequests(validBidderRequest.bids, validBidderRequest); + const auction_id = request.data.id; + expect(request).deep.equal({ + method: 'POST', + url: 'https://rt.gsspat.jp/prebid_auction', + data: { + at: 1, + id: auction_id, + imp: [ + { + ext: { + test: 1 + }, + id: 'bid-id' + } + ], + test: 1 + }, + }); + }); + it('uncomfortable (gdpr)', () => { + validBidderRequest.gdprConsent.gdprApplies = true; + const request = spec.buildRequests(validBidderRequest.bids, validBidderRequest); + expect(request).deep.equal({ + method: 'GET', + url: 'https://rt.gsspat.jp/prebid_uncomfortable', + }); + validBidderRequest.gdprConsent.gdprApplies = false; + }); + it('uncomfortable (usp)', () => { + validBidderRequest.uspConsent = '1YYY'; + const request = spec.buildRequests(validBidderRequest.bids, validBidderRequest); + expect(request).deep.equal({ + method: 'GET', + url: 'https://rt.gsspat.jp/prebid_uncomfortable', + }); + validBidderRequest.uspConsent = '1YNY'; + }); + it('uncomfortable (coppa)', () => { + config.setConfig({ coppa: true }); + const request = spec.buildRequests(validBidderRequest.bids, validBidderRequest); + expect(request).deep.equal({ + method: 'GET', + url: 'https://rt.gsspat.jp/prebid_uncomfortable', + }); + config.resetConfig(); + }); + it('uncomfortable (currency)', () => { + config.setConfig({ currency: { adServerCurrency: 'TWD' } }); + const request = spec.buildRequests(validBidderRequest.bids, validBidderRequest); + expect(request).deep.equal({ + method: 'GET', + url: 'https://rt.gsspat.jp/prebid_uncomfortable', + }); + config.resetConfig(); + }); + }); + describe('interpretResponse function test', () => { + it('sample bid', () => { + const request = spec.buildRequests(validBidderRequest.bids, validBidderRequest); + const auction_id = request.data.id; + const adm = "\n"; + const serverResponse = { + body: { + id: auction_id, + cur: 'JPY', + seatbid: [{ + bid: [{ + id: '7b77235d599e06d289e58ddfa9390443e22d7071', + impid: 'bid-id', + price: 0.6666000000000001, + adid: '8405715', + adm: adm, + adomain: ['geniee.co.jp'], + iurl: 'http://img.gsspat.jp/e/068c8e1eafbf0cb6ac1ee95c36152bd2/04f4bd4e6b71f978d343d84ecede3877.png', + cid: '8405715', + crid: '1383823', + cat: ['IAB1'], + w: 300, + h: 250, + mtype: 1 + }] + }] + } + }; + const bids = spec.interpretResponse(serverResponse, request); + expect(bids).deep.equal([{ + ad: adm, + cpm: 0.6666000000000001, + creativeId: '1383823', + creative_id: '1383823', + height: 250, + width: 300, + currency: 'JPY', + mediaType: 'banner', + meta: { + advertiserDomains: ['geniee.co.jp'] + }, + netRevenue: true, + requestId: 'bid-id', + seatBidId: '7b77235d599e06d289e58ddfa9390443e22d7071', + ttl: 300 + }]); + }); + it('no bid', () => { + const serverResponse = {}; + const bids = spec.interpretResponse(serverResponse, validBidderRequest); + expect(bids).deep.equal([]); + }); + }); + describe('getUserSyncs function test', () => { + it('sync enabled', () => { + const syncOptions = { + iframeEnabled: true, + pixelEnabled: true + }; + const serverResponses = []; + const syncs = spec.getUserSyncs(syncOptions, serverResponses); + expect(syncs).deep.equal([{ + type: 'image', + url: 'https://rt.gsspat.jp/prebid_cs' + }]); + }); + it('sync disabled (option false)', () => { + const syncOptions = { + iframeEnabled: false, + pixelEnabled: false + }; + const serverResponses = []; + const syncs = spec.getUserSyncs(syncOptions, serverResponses); + expect(syncs).deep.equal([]); + }); + it('sync disabled (gdpr)', () => { + const syncOptions = { + iframeEnabled: true, + pixelEnabled: true + }; + const serverResponses = []; + const gdprConsent = { + gdprApplies: true + }; + const syncs = spec.getUserSyncs(syncOptions, serverResponses, gdprConsent); + expect(syncs).deep.equal([]); + }); + }); +}); diff --git a/test/spec/modules/dxkultureBidAdapter_spec.js b/test/spec/modules/dxkultureBidAdapter_spec.js new file mode 100644 index 00000000000..a752c81cb6e --- /dev/null +++ b/test/spec/modules/dxkultureBidAdapter_spec.js @@ -0,0 +1,649 @@ +import {expect} from 'chai'; +import {spec, SYNC_URL} from 'modules/dxkultureBidAdapter.js'; +import {BANNER, VIDEO} from 'src/mediaTypes.js'; + +const getBannerRequest = () => { + return { + bidderCode: 'dxkulture', + auctionId: 'ba87bfdf-493e-4a88-8e26-17b4cbc9adbd', + bidderRequestId: 'bidderRequestId', + bids: [ + { + bidder: 'dxkulture', + params: { + placementId: 123456, + publisherId: 'publisherId', + bidfloor: 10, + }, + auctionId: 'auctionId-56a2-4f71-9098-720a68f2f708', + placementCode: 'div-gpt-dummy-placement-code', + mediaTypes: { + banner: { + sizes: [ + [ 300, 250 ], + ] + } + }, + bidId: '2e9f38ea93bb9e', + bidderRequestId: 'bidderRequestId', + } + ], + start: 1487883186070, + auctionStart: 1487883186069, + timeout: 3000 + } +}; + +const getVideoRequest = () => { + return { + bidderCode: 'dxkulture', + auctionId: 'e158486f-8c7f-472f-94ce-b0cbfbb50ab4', + bidderRequestId: '34feaad34lkj2', + bids: [{ + mediaTypes: { + video: { + context: 'instream', + playerSize: [[640, 480]], + } + }, + bidder: 'dxkulture', + sizes: [640, 480], + bidId: '30b3efwfwe1e', + adUnitCode: 'video1', + params: { + video: { + playerWidth: 640, + playerHeight: 480, + mimes: ['video/mp4', 'application/javascript'], + protocols: [2, 5], + api: [2], + position: 1, + delivery: [2], + sid: 134, + rewarded: 1, + placement: 1, + plcmt: 1, + hp: 1, + inventoryid: 123 + }, + site: { + id: 1, + page: 'https://test.com', + referrer: 'http://test.com' + }, + publisherId: 'km123', + bidfloor: 10, + } + }, { + mediaTypes: { + video: { + context: 'instream', + playerSize: [[640, 480]], + } + }, + bidder: 'dxkulture', + sizes: [640, 480], + bidId: '30b3efwfwe2e', + adUnitCode: 'video1', + params: { + video: { + playerWidth: 640, + playerHeight: 480, + mimes: ['video/mp4', 'application/javascript'], + protocols: [2, 5], + api: [2], + position: 1, + delivery: [2], + sid: 134, + rewarded: 1, + placement: 1, + plcmt: 1, + hp: 1, + inventoryid: 123 + }, + site: { + id: 1, + page: 'https://test.com', + referrer: 'http://test.com' + }, + publisherId: 'km123', + bidfloor: 10, + } + }], + auctionStart: 1520001292880, + timeout: 5000, + start: 1520001292884, + doneCbCallCount: 0, + refererInfo: { + numIframes: 1, + reachedTop: true, + referer: 'test.com' + } + }; +}; + +const getBidderResponse = () => { + return { + headers: null, + body: { + id: 'bid-response', + seatbid: [ + { + bid: [ + { + id: '2e9f38ea93bb9e', + impid: '2e9f38ea93bb9e', + price: 0.18, + adm: '', + adid: '144762342', + adomain: [ + 'https://dummydomain.com' + ], + iurl: 'iurl', + cid: '109', + crid: 'creativeId', + cat: [], + w: 300, + h: 250, + ext: { + prebid: { + type: 'banner' + }, + bidder: { + appnexus: { + brand_id: 334553, + auction_id: 514667951122925701, + bidder_id: 2, + bid_ad_type: 0 + } + } + } + } + ], + seat: 'dxkulture' + } + ], + ext: { + usersync: { + sovrn: { + status: 'none', + syncs: [ + { + url: 'urlsovrn', + type: 'iframe' + } + ] + }, + appnexus: { + status: 'none', + syncs: [ + { + url: 'urlappnexus', + type: 'pixel' + } + ] + } + }, + responsetimemillis: { + appnexus: 127 + } + } + } + }; +} + +describe('dxkultureBidAdapter', function() { + let videoBidRequest; + + const VIDEO_REQUEST = { + 'bidderCode': 'dxkulture', + 'auctionId': 'e158486f-8c7f-472f-94ce-b0cbfbb50ab4', + 'bidderRequestId': '34feaad34lkj2', + 'bids': videoBidRequest, + 'auctionStart': 1520001292880, + 'timeout': 3000, + 'start': 1520001292884, + 'doneCbCallCount': 0, + 'refererInfo': { + 'numIframes': 1, + 'reachedTop': true, + 'referer': 'test.com' + } + }; + + beforeEach(function () { + videoBidRequest = { + mediaTypes: { + video: { + context: 'instream', + playerSize: [[640, 480]], + } + }, + bidder: 'dxkulture', + sizes: [640, 480], + bidId: '30b3efwfwe1e', + adUnitCode: 'video1', + params: { + video: { + playerWidth: 640, + playerHeight: 480, + mimes: ['video/mp4', 'application/javascript'], + protocols: [2, 5], + api: [2], + position: 1, + delivery: [2], + sid: 134, + rewarded: 1, + placement: 1, + plcmt: 1, + hp: 1, + inventoryid: 123 + }, + site: { + id: 1, + page: 'https://test.com', + referrer: 'http://test.com' + }, + publisherId: 'km123', + bidfloor: 0 + } + }; + }); + + describe('isValidRequest', function() { + let bidderRequest; + + beforeEach(function() { + bidderRequest = getBannerRequest(); + }); + + it('should accept request if placementId and publisherId are passed', function () { + expect(spec.isBidRequestValid(bidderRequest.bids[0])).to.be.true; + }); + + it('reject requests without params', function () { + bidderRequest.bids[0].params = {}; + expect(spec.isBidRequestValid(bidderRequest.bids[0])).to.be.false; + }); + + it('returns false when banner mediaType does not exist', function () { + bidderRequest.bids[0].mediaTypes = {} + expect(spec.isBidRequestValid(bidderRequest.bids[0])).to.be.false; + }); + }); + + describe('buildRequests', function() { + let bidderRequest; + + beforeEach(function() { + bidderRequest = getBannerRequest(); + }); + + it('should return expected request object', function() { + const bidRequest = spec.buildRequests(bidderRequest.bids, bidderRequest); + expect(bidRequest.url).equal('https://ads.dxkulture.com/pbjs?pid=publisherId&placementId=123456'); + expect(bidRequest.method).equal('POST'); + }); + }); + + context('banner validation', function () { + let bidderRequest; + + beforeEach(function() { + bidderRequest = getBannerRequest(); + }); + + it('returns true when banner sizes are defined', function () { + const bid = { + bidder: 'dxkulture', + mediaTypes: { + banner: { + sizes: [[250, 300]] + } + }, + params: { + placementId: 'placementId', + publisherId: 'publisherId', + } + }; + + expect(spec.isBidRequestValid(bidderRequest.bids[0])).to.be.true; + }); + + it('returns false when banner sizes are invalid', function () { + const invalidSizes = [ + undefined, + '2:1', + 123, + 'test' + ]; + + invalidSizes.forEach((sizes) => { + const bid = { + bidder: 'dxkulture', + mediaTypes: { + banner: { + sizes + } + }, + params: { + placementId: 'placementId', + publisherId: 'publisherId', + } + }; + + expect(spec.isBidRequestValid(bid)).to.be.false; + }); + }); + }); + + context('video validation', function () { + beforeEach(function () { + // Basic Valid BidRequest + this.bid = { + bidder: 'dxkulture', + mediaTypes: { + video: { + playerSize: [[300, 50]], + context: 'instream', + mimes: ['foo', 'bar'], + protocols: [1, 2] + } + }, + params: { + placementId: 'placementId', + publisherId: 'publisherId', + } + }; + }); + + it('should return true (skip validations) when e2etest = true', function () { + this.bid.params = { + e2etest: true + }; + expect(spec.isBidRequestValid(this.bid)).to.equal(true); + }); + + it('returns false when video context is not defined', function () { + delete this.bid.mediaTypes.video.context; + + expect(spec.isBidRequestValid(this.bid)).to.be.false; + }); + + it('returns false when video playserSize is invalid', function () { + const invalidSizes = [ + undefined, + '2:1', + 123, + 'test' + ]; + + invalidSizes.forEach((playerSize) => { + this.bid.mediaTypes.video.playerSize = playerSize; + expect(spec.isBidRequestValid(this.bid)).to.be.false; + }); + }); + + it('returns false when video mimes is invalid', function () { + const invalidMimes = [ + undefined, + 'test', + 1, + [] + ] + + invalidMimes.forEach((mimes) => { + this.bid.mediaTypes.video.mimes = mimes; + expect(spec.isBidRequestValid(this.bid)).to.be.false; + }) + }); + + it('returns false when video protocols is invalid', function () { + const invalidMimes = [ + undefined, + 'test', + 1, + [] + ] + + invalidMimes.forEach((protocols) => { + this.bid.mediaTypes.video.protocols = protocols; + expect(spec.isBidRequestValid(this.bid)).to.be.false; + }) + }); + }); + + describe('buildRequests', function () { + let bidderBannerRequest; + let bidRequestsWithMediaTypes; + let mockBidderRequest; + + beforeEach(function() { + bidderBannerRequest = getBannerRequest(); + + mockBidderRequest = {refererInfo: {}}; + + bidRequestsWithMediaTypes = [{ + bidder: 'dxkulture', + params: { + publisherId: 'km123', + }, + adUnitCode: '/adunit-code/test-path', + mediaTypes: { + banner: { + sizes: [[300, 250], [300, 600]] + } + }, + bidId: 'test-bid-id-1', + bidderRequestId: 'test-bid-request-1', + auctionId: 'test-auction-1', + transactionId: 'test-transactionId-1', + ortb2Imp: { + ext: { + ae: 2 + } + } + }, { + bidder: 'dxkulture', + params: { + publisherId: 'km123', + }, + adUnitCode: 'adunit-code', + mediaTypes: { + video: { + playerSize: [640, 480], + placement: 1, + plcmt: 1, + } + }, + bidId: 'test-bid-id-2', + bidderRequestId: 'test-bid-request-2', + auctionId: 'test-auction-2', + transactionId: 'test-transactionId-2' + }]; + }); + + context('when mediaType is banner', function () { + it('creates request data', function () { + let request = spec.buildRequests(bidderBannerRequest.bids, bidderBannerRequest) + + expect(request).to.exist.and.to.be.a('object'); + const payload = request.data; + expect(payload.imp[0]).to.have.property('id', bidderBannerRequest.bids[0].bidId); + }); + + it('has gdpr data if applicable', function () { + const req = Object.assign({}, getBannerRequest(), { + gdprConsent: { + consentString: 'consentString', + gdprApplies: true, + } + }); + let request = spec.buildRequests(bidderBannerRequest.bids, req); + + const payload = request.data; + expect(payload.user.ext).to.have.property('consent', req.gdprConsent.consentString); + expect(payload.regs.ext).to.have.property('gdpr', 1); + }); + }); + + if (FEATURES.VIDEO) { + context('video', function () { + it('should create a POST request for every bid', function () { + const requests = spec.buildRequests(bidRequestsWithMediaTypes, mockBidderRequest); + expect(requests.method).to.equal('POST'); + expect(requests.url.trim()).to.equal(spec.ENDPOINT + '?pid=' + videoBidRequest.params.publisherId); + }); + + it('should attach request data', function () { + const requests = spec.buildRequests(bidRequestsWithMediaTypes, mockBidderRequest); + const data = requests.data; + const [width, height] = videoBidRequest.sizes; + const VERSION = '1.0.0'; + + expect(data.imp[1].video.w).to.equal(width); + expect(data.imp[1].video.h).to.equal(height); + expect(data.imp[1].bidfloor).to.equal(videoBidRequest.params.bidfloor); + expect(data.imp[1]['video']['placement']).to.equal(videoBidRequest.params.video['placement']); + expect(data.imp[1]['video']['plcmt']).to.equal(videoBidRequest.params.video['plcmt']); + expect(data.ext.prebidver).to.equal('$prebid.version$'); + expect(data.ext.adapterver).to.equal(spec.VERSION); + }); + + it('should set pubId to e2etest when bid.params.e2etest = true', function () { + bidRequestsWithMediaTypes[0].params.e2etest = true; + const requests = spec.buildRequests(bidRequestsWithMediaTypes, mockBidderRequest); + expect(requests.method).to.equal('POST'); + expect(requests.url).to.equal(spec.ENDPOINT + '?pid=e2etest'); + }); + + it('should attach End 2 End test data', function () { + bidRequestsWithMediaTypes[1].params.e2etest = true; + const requests = spec.buildRequests(bidRequestsWithMediaTypes, mockBidderRequest); + const data = requests.data; + expect(data.imp[1].bidfloor).to.equal(0); + expect(data.imp[1].video.w).to.equal(640); + expect(data.imp[1].video.h).to.equal(480); + }); + }); + } + }); + + describe('interpretResponse', function() { + context('when mediaType is banner', function() { + let bidRequest, bidderResponse; + beforeEach(function() { + const bidderRequest = getBannerRequest(); + bidRequest = spec.buildRequests(bidderRequest.bids, bidderRequest); + bidderResponse = getBidderResponse(); + }); + + it('handles empty response', function () { + const EMPTY_RESP = Object.assign({}, bidderResponse, {'body': {}}); + const bids = spec.interpretResponse(EMPTY_RESP, bidRequest); + + expect(bids).to.be.empty; + }); + + it('have bids', function () { + let bids = spec.interpretResponse(bidderResponse, bidRequest); + expect(bids).to.be.an('array').that.is.not.empty; + validateBidOnIndex(0); + + function validateBidOnIndex(index) { + expect(bids[index]).to.have.property('currency', 'USD'); + expect(bids[index]).to.have.property('requestId', getBidderResponse().body.seatbid[0].bid[index].impid); + expect(bids[index]).to.have.property('cpm', getBidderResponse().body.seatbid[0].bid[index].price); + expect(bids[index]).to.have.property('width', getBidderResponse().body.seatbid[0].bid[index].w); + expect(bids[index]).to.have.property('height', getBidderResponse().body.seatbid[0].bid[index].h); + expect(bids[index]).to.have.property('ad', getBidderResponse().body.seatbid[0].bid[index].adm); + expect(bids[index]).to.have.property('creativeId', getBidderResponse().body.seatbid[0].bid[index].crid); + expect(bids[index].meta).to.have.property('advertiserDomains'); + expect(bids[index]).to.have.property('ttl', 300); + expect(bids[index]).to.have.property('netRevenue', true); + } + }); + }); + + context('when mediaType is video', function () { + let bidRequest, bidderResponse; + beforeEach(function() { + const bidderRequest = getVideoRequest(); + bidRequest = spec.buildRequests(bidderRequest.bids, bidderRequest); + bidderResponse = getBidderResponse(); + }); + + it('handles empty response', function () { + const EMPTY_RESP = Object.assign({}, bidderResponse, {'body': {}}); + const bids = spec.interpretResponse(EMPTY_RESP, bidRequest); + + expect(bids).to.be.empty; + }); + + it('should return no bids if the response "nurl" and "adm" are missing', function () { + const SERVER_RESP = Object.assign({}, bidderResponse, {'body': { + seatbid: [{ + bid: [{ + price: 6.01 + }] + }] + }}); + const bids = spec.interpretResponse(SERVER_RESP, bidRequest); + expect(bids.length).to.equal(0); + }); + + it('should return no bids if the response "price" is missing', function () { + const SERVER_RESP = Object.assign({}, bidderResponse, {'body': { + seatbid: [{ + bid: [{ + adm: '' + }] + }] + }}); + const bids = spec.interpretResponse(SERVER_RESP, bidRequest); + expect(bids.length).to.equal(0); + }); + }); + }); + + describe('getUserSyncs', function () { + let bidRequest, bidderResponse; + beforeEach(function() { + const bidderRequest = getVideoRequest(); + bidRequest = spec.buildRequests(bidderRequest.bids, bidderRequest); + bidderResponse = getBidderResponse(); + }); + + 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}, [bidderResponse]); + + expect(opts.length).to.equal(1); + expect(opts[0].type).to.equal('iframe'); + expect(opts[0].url).to.equal(bidderResponse.body.ext.usersync['sovrn'].syncs[0].url); + }); + + it('pixel sync enabled should return results', function () { + let opts = spec.getUserSyncs({iframeEnabled: false, pixelEnabled: true}, [bidderResponse]); + + expect(opts.length).to.equal(1); + expect(opts[0].type).to.equal('image'); + expect(opts[0].url).to.equal(bidderResponse.body.ext.usersync['appnexus'].syncs[0].url); + }); + + it('all sync enabled should prioritize iframe', function () { + let opts = spec.getUserSyncs({iframeEnabled: true, pixelEnabled: true}, [bidderResponse]); + + expect(opts.length).to.equal(1); + }); + }); +}); diff --git a/test/spec/modules/dynamicAdBoostRtdProvider_spec.js b/test/spec/modules/dynamicAdBoostRtdProvider_spec.js new file mode 100644 index 00000000000..66c24435589 --- /dev/null +++ b/test/spec/modules/dynamicAdBoostRtdProvider_spec.js @@ -0,0 +1,77 @@ +import { subModuleObj as rtdProvider } from 'modules/dynamicAdBoostRtdProvider.js'; +import { loadExternalScript } from '../../../src/adloader.js'; +import { expect } from 'chai'; + +const configWithParams = { + params: { + keyId: 'dynamic', + adUnits: ['gpt-123'], + threshold: 1 + } +}; + +const configWithoutRequiredParams = { + params: { + keyId: '' + } +}; + +describe('dynamicAdBoost', function() { + let clock; + let sandbox; + beforeEach(function () { + sandbox = sinon.sandbox.create(); + clock = sandbox.useFakeTimers(Date.now()); + }); + afterEach(function () { + sandbox.restore(); + }); + describe('init', function() { + describe('initialize without expected params', function() { + it('fails initalize when keyId is not present', function() { + expect(rtdProvider.init(configWithoutRequiredParams)).to.be.false; + }) + }) + + describe('initialize with expected params', function() { + it('successfully initialize with load script', function() { + expect(rtdProvider.init(configWithParams)).to.be.true; + clock.tick(1000); + expect(loadExternalScript.called).to.be.true; + }) + }); + }); +}) + +describe('markViewed tests', function() { + let sandbox; + const mockObserver = { + unobserve: sinon.spy() + }; + const makeElement = (id) => { + const el = document.createElement('div'); + el.setAttribute('id', id); + return el; + } + const mockEntry = { + target: makeElement('target_id') + }; + + beforeEach(function() { + sandbox = sinon.sandbox.create(); + }) + + afterEach(function() { + sandbox.restore() + }) + + it('markViewed returns a function', function() { + expect(rtdProvider.markViewed(mockEntry, mockObserver)).to.be.a('function') + }); + + it('markViewed unobserves', function() { + const func = rtdProvider.markViewed(mockEntry, mockObserver); + func(); + expect(mockObserver.unobserve.calledOnce).to.be.true; + }); +}) diff --git a/test/spec/modules/edge226BidAdapter_spec.js b/test/spec/modules/edge226BidAdapter_spec.js new file mode 100644 index 00000000000..4819d8d4a4e --- /dev/null +++ b/test/spec/modules/edge226BidAdapter_spec.js @@ -0,0 +1,373 @@ +import { expect } from 'chai'; +import { spec } from '../../../modules/edge226BidAdapter.js'; +import { BANNER, VIDEO, NATIVE } from '../../../src/mediaTypes.js'; +import { getUniqueIdentifierStr } from '../../../src/utils.js'; + +const bidder = 'edge226' + +describe('Edge226BidAdapter', function () { + const bids = [ + { + bidId: getUniqueIdentifierStr(), + bidder: bidder, + mediaTypes: { + [BANNER]: { + sizes: [[300, 250]] + } + }, + params: { + placementId: 'testBanner', + } + }, + { + bidId: getUniqueIdentifierStr(), + bidder: bidder, + mediaTypes: { + [VIDEO]: { + playerSize: [[300, 300]], + minduration: 5, + maxduration: 60 + } + }, + params: { + placementId: 'testVideo', + } + }, + { + bidId: getUniqueIdentifierStr(), + bidder: bidder, + mediaTypes: { + [NATIVE]: { + native: { + title: { + required: true + }, + body: { + required: true + }, + icon: { + required: true, + size: [64, 64] + } + } + } + }, + params: { + placementId: 'testNative', + } + } + ]; + + const invalidBid = { + bidId: getUniqueIdentifierStr(), + bidder: bidder, + mediaTypes: { + [BANNER]: { + sizes: [[300, 250]] + } + }, + params: { + + } + } + + const bidderRequest = { + uspConsent: '1---', + gdprConsent: 'COvFyGBOvFyGBAbAAAENAPCAAOAAAAAAAAAAAEEUACCKAAA.IFoEUQQgAIQwgIwQABAEAAAAOIAACAIAAAAQAIAgEAACEAAAAAgAQBAAAAAAAGBAAgAAAAAAAFAAECAAAgAAQARAEQAAAAAJAAIAAgAAAYQEAAAQmAgBC3ZAYzUw', + refererInfo: { + referer: 'https://test.com' + }, + timeout: 500 + }; + + describe('isBidRequestValid', function () { + it('Should return true if there are bidId, params and key parameters present', function () { + expect(spec.isBidRequestValid(bids[0])).to.be.true; + }); + it('Should return false if at least one of parameters is not present', function () { + expect(spec.isBidRequestValid(invalidBid)).to.be.false; + }); + }); + + describe('buildRequests', function () { + let serverRequest = spec.buildRequests(bids, bidderRequest); + + it('Creates a ServerRequest object with method, URL and data', function () { + expect(serverRequest).to.exist; + expect(serverRequest.method).to.exist; + expect(serverRequest.url).to.exist; + expect(serverRequest.data).to.exist; + }); + + it('Returns POST method', function () { + expect(serverRequest.method).to.equal('POST'); + }); + + it('Returns valid URL', function () { + expect(serverRequest.url).to.equal('https://ssp.dauup.com/pbjs'); + }); + + it('Returns general data valid', function () { + let data = serverRequest.data; + expect(data).to.be.an('object'); + expect(data).to.have.all.keys('deviceWidth', + 'deviceHeight', + 'language', + 'secure', + 'host', + 'page', + 'placements', + 'coppa', + 'ccpa', + 'gdpr', + 'tmax' + ); + expect(data.deviceWidth).to.be.a('number'); + expect(data.deviceHeight).to.be.a('number'); + expect(data.language).to.be.a('string'); + expect(data.secure).to.be.within(0, 1); + expect(data.host).to.be.a('string'); + expect(data.page).to.be.a('string'); + expect(data.coppa).to.be.a('number'); + expect(data.gdpr).to.be.a('string'); + expect(data.ccpa).to.be.a('string'); + expect(data.tmax).to.be.a('number'); + expect(data.placements).to.have.lengthOf(3); + }); + + it('Returns valid placements', function () { + const { placements } = serverRequest.data; + for (let i = 0, len = placements.length; i < len; i++) { + const placement = placements[i]; + expect(placement.placementId).to.be.oneOf(['testBanner', 'testVideo', 'testNative']); + expect(placement.adFormat).to.be.oneOf([BANNER, VIDEO, NATIVE]); + expect(placement.bidId).to.be.a('string'); + expect(placement.schain).to.be.an('object'); + expect(placement.bidfloor).to.exist.and.to.equal(0); + expect(placement.type).to.exist.and.to.equal('publisher'); + + if (placement.adFormat === BANNER) { + expect(placement.sizes).to.be.an('array'); + } + switch (placement.adFormat) { + case BANNER: + expect(placement.sizes).to.be.an('array'); + break; + case VIDEO: + expect(placement.playerSize).to.be.an('array'); + expect(placement.minduration).to.be.an('number'); + expect(placement.maxduration).to.be.an('number'); + break; + case NATIVE: + expect(placement.native).to.be.an('object'); + break; + } + } + }); + + it('Returns data with gdprConsent and without uspConsent', function () { + delete bidderRequest.uspConsent; + serverRequest = spec.buildRequests(bids, bidderRequest); + let data = serverRequest.data; + expect(data.gdpr).to.exist; + expect(data.gdpr).to.be.a('string'); + expect(data.gdpr).to.equal(bidderRequest.gdprConsent); + expect(data.ccpa).to.not.exist; + delete bidderRequest.gdprConsent; + }); + + it('Returns data with uspConsent and without gdprConsent', function () { + bidderRequest.uspConsent = '1---'; + delete bidderRequest.gdprConsent; + serverRequest = spec.buildRequests(bids, bidderRequest); + let data = serverRequest.data; + expect(data.ccpa).to.exist; + expect(data.ccpa).to.be.a('string'); + expect(data.ccpa).to.equal(bidderRequest.uspConsent); + expect(data.gdpr).to.not.exist; + }); + + it('Returns empty data if no valid requests are passed', function () { + serverRequest = spec.buildRequests([], bidderRequest); + let data = serverRequest.data; + expect(data.placements).to.be.an('array').that.is.empty; + }); + }); + + describe('interpretResponse', function () { + it('Should interpret banner response', function () { + const banner = { + body: [{ + mediaType: 'banner', + width: 300, + height: 250, + cpm: 0.4, + ad: 'Test', + requestId: '23fhj33i987f', + ttl: 120, + creativeId: '2', + netRevenue: true, + currency: 'USD', + dealId: '1', + meta: { + advertiserDomains: ['google.com'], + advertiserId: 1234 + } + }] + }; + let bannerResponses = spec.interpretResponse(banner); + expect(bannerResponses).to.be.an('array').that.is.not.empty; + let dataItem = bannerResponses[0]; + expect(dataItem).to.have.all.keys('requestId', 'cpm', 'width', 'height', 'ad', 'ttl', 'creativeId', + 'netRevenue', 'currency', 'dealId', 'mediaType', 'meta'); + expect(dataItem.requestId).to.equal(banner.body[0].requestId); + expect(dataItem.cpm).to.equal(banner.body[0].cpm); + expect(dataItem.width).to.equal(banner.body[0].width); + expect(dataItem.height).to.equal(banner.body[0].height); + expect(dataItem.ad).to.equal(banner.body[0].ad); + expect(dataItem.ttl).to.equal(banner.body[0].ttl); + expect(dataItem.creativeId).to.equal(banner.body[0].creativeId); + expect(dataItem.netRevenue).to.be.true; + expect(dataItem.currency).to.equal(banner.body[0].currency); + expect(dataItem.meta).to.be.an('object').that.has.any.key('advertiserDomains'); + }); + it('Should interpret video response', function () { + const video = { + body: [{ + vastUrl: 'test.com', + mediaType: 'video', + cpm: 0.5, + requestId: '23fhj33i987f', + ttl: 120, + creativeId: '2', + netRevenue: true, + currency: 'USD', + dealId: '1', + meta: { + advertiserDomains: ['google.com'], + advertiserId: 1234 + } + }] + }; + let videoResponses = spec.interpretResponse(video); + expect(videoResponses).to.be.an('array').that.is.not.empty; + + let dataItem = videoResponses[0]; + expect(dataItem).to.have.all.keys('requestId', 'cpm', 'vastUrl', 'ttl', 'creativeId', + 'netRevenue', 'currency', 'dealId', 'mediaType', 'meta'); + expect(dataItem.requestId).to.equal('23fhj33i987f'); + expect(dataItem.cpm).to.equal(0.5); + expect(dataItem.vastUrl).to.equal('test.com'); + expect(dataItem.ttl).to.equal(120); + expect(dataItem.creativeId).to.equal('2'); + expect(dataItem.netRevenue).to.be.true; + expect(dataItem.currency).to.equal('USD'); + expect(dataItem.meta).to.be.an('object').that.has.any.key('advertiserDomains'); + }); + it('Should interpret native response', function () { + const native = { + body: [{ + mediaType: 'native', + native: { + clickUrl: 'test.com', + title: 'Test', + image: 'test.com', + impressionTrackers: ['test.com'], + }, + ttl: 120, + cpm: 0.4, + requestId: '23fhj33i987f', + creativeId: '2', + netRevenue: true, + currency: 'USD', + meta: { + advertiserDomains: ['google.com'], + advertiserId: 1234 + } + }] + }; + let nativeResponses = spec.interpretResponse(native); + expect(nativeResponses).to.be.an('array').that.is.not.empty; + + let dataItem = nativeResponses[0]; + expect(dataItem).to.have.keys('requestId', 'cpm', 'ttl', 'creativeId', 'netRevenue', 'currency', 'mediaType', 'native', 'meta'); + expect(dataItem.native).to.have.keys('clickUrl', 'impressionTrackers', 'title', 'image') + expect(dataItem.requestId).to.equal('23fhj33i987f'); + expect(dataItem.cpm).to.equal(0.4); + expect(dataItem.native.clickUrl).to.equal('test.com'); + expect(dataItem.native.title).to.equal('Test'); + expect(dataItem.native.image).to.equal('test.com'); + expect(dataItem.native.impressionTrackers).to.be.an('array').that.is.not.empty; + expect(dataItem.native.impressionTrackers[0]).to.equal('test.com'); + expect(dataItem.ttl).to.equal(120); + expect(dataItem.creativeId).to.equal('2'); + expect(dataItem.netRevenue).to.be.true; + expect(dataItem.currency).to.equal('USD'); + expect(dataItem.meta).to.be.an('object').that.has.any.key('advertiserDomains'); + }); + it('Should return an empty array if invalid banner response is passed', function () { + const invBanner = { + body: [{ + width: 300, + cpm: 0.4, + ad: 'Test', + requestId: '23fhj33i987f', + ttl: 120, + creativeId: '2', + netRevenue: true, + currency: 'USD', + dealId: '1' + }] + }; + + let serverResponses = spec.interpretResponse(invBanner); + expect(serverResponses).to.be.an('array').that.is.empty; + }); + it('Should return an empty array if invalid video response is passed', function () { + const invVideo = { + body: [{ + mediaType: 'video', + cpm: 0.5, + requestId: '23fhj33i987f', + ttl: 120, + creativeId: '2', + netRevenue: true, + currency: 'USD', + dealId: '1' + }] + }; + let serverResponses = spec.interpretResponse(invVideo); + expect(serverResponses).to.be.an('array').that.is.empty; + }); + it('Should return an empty array if invalid native response is passed', function () { + const invNative = { + body: [{ + mediaType: 'native', + clickUrl: 'test.com', + title: 'Test', + impressionTrackers: ['test.com'], + ttl: 120, + requestId: '23fhj33i987f', + creativeId: '2', + netRevenue: true, + currency: 'USD', + }] + }; + let serverResponses = spec.interpretResponse(invNative); + expect(serverResponses).to.be.an('array').that.is.empty; + }); + it('Should return an empty array if invalid response is passed', function () { + const invalid = { + body: [{ + ttl: 120, + creativeId: '2', + netRevenue: true, + currency: 'USD', + dealId: '1' + }] + }; + let serverResponses = spec.interpretResponse(invalid); + expect(serverResponses).to.be.an('array').that.is.empty; + }); + }); +}); diff --git a/test/spec/modules/eids_spec.js b/test/spec/modules/eids_spec.js index 725d631cd99..25e70f12ced 100644 --- a/test/spec/modules/eids_spec.js +++ b/test/spec/modules/eids_spec.js @@ -187,6 +187,24 @@ describe('eids array generation for known sub-modules', function() { }); }); + it('bidswitch with ext', function() { + const userId = { + bidswitch: {'id': 'sample_id', 'ext': {'provider': 'some.provider.com'}} + }; + const newEids = createEidsArray(userId); + expect(newEids.length).to.equal(1); + expect(newEids[0]).to.deep.equal({ + source: 'bidswitch.net', + uids: [{ + id: 'sample_id', + atype: 3, + ext: { + provider: 'some.provider.com' + } + }] + }); + }); + it('medianet', function() { const userId = { medianet: {'id': 'sample_id'} @@ -202,6 +220,189 @@ describe('eids array generation for known sub-modules', function() { }); }); + it('medianet with ext', function() { + const userId = { + medianet: {'id': 'sample_id', 'ext': {'provider': 'some.provider.com'}} + }; + const newEids = createEidsArray(userId); + expect(newEids.length).to.equal(1); + expect(newEids[0]).to.deep.equal({ + source: 'media.net', + uids: [{ + id: 'sample_id', + atype: 3, + ext: { + provider: 'some.provider.com' + } + }] + }); + }); + + it('sovrn', function() { + const userId = { + sovrn: {'id': 'sample_id'} + }; + const newEids = createEidsArray(userId); + expect(newEids.length).to.equal(1); + expect(newEids[0]).to.deep.equal({ + source: 'liveintent.sovrn.com', + uids: [{ + id: 'sample_id', + atype: 3 + }] + }); + }); + + it('sovrn with ext', function() { + const userId = { + sovrn: {'id': 'sample_id', 'ext': {'provider': 'some.provider.com'}} + }; + const newEids = createEidsArray(userId); + expect(newEids.length).to.equal(1); + expect(newEids[0]).to.deep.equal({ + source: 'liveintent.sovrn.com', + uids: [{ + id: 'sample_id', + atype: 3, + ext: { + provider: 'some.provider.com' + } + }] + }); + }); + + it('magnite', function() { + const userId = { + magnite: {'id': 'sample_id'} + }; + const newEids = createEidsArray(userId); + expect(newEids.length).to.equal(1); + expect(newEids[0]).to.deep.equal({ + source: 'rubiconproject.com', + uids: [{ + id: 'sample_id', + atype: 3 + }] + }); + }); + + it('magnite with ext', function() { + const userId = { + magnite: {'id': 'sample_id', 'ext': {'provider': 'some.provider.com'}} + }; + const newEids = createEidsArray(userId); + expect(newEids.length).to.equal(1); + expect(newEids[0]).to.deep.equal({ + source: 'rubiconproject.com', + uids: [{ + id: 'sample_id', + atype: 3, + ext: { + provider: 'some.provider.com' + } + }] + }); + }); + + it('index', function() { + const userId = { + index: {'id': 'sample_id'} + }; + const newEids = createEidsArray(userId); + expect(newEids.length).to.equal(1); + expect(newEids[0]).to.deep.equal({ + source: 'liveintent.indexexchange.com', + uids: [{ + id: 'sample_id', + atype: 3 + }] + }); + }); + + it('index with ext', function() { + const userId = { + index: {'id': 'sample_id', 'ext': {'provider': 'some.provider.com'}} + }; + const newEids = createEidsArray(userId); + expect(newEids.length).to.equal(1); + expect(newEids[0]).to.deep.equal({ + source: 'liveintent.indexexchange.com', + uids: [{ + id: 'sample_id', + atype: 3, + ext: { + provider: 'some.provider.com' + } + }] + }); + }); + + it('openx', function () { + const userId = { + openx: { 'id': 'sample_id' } + }; + const newEids = createEidsArray(userId); + expect(newEids.length).to.equal(1); + expect(newEids[0]).to.deep.equal({ + source: 'openx.net', + uids: [{ + id: 'sample_id', + atype: 3 + }] + }); + }); + + it('openx with ext', function () { + const userId = { + openx: { 'id': 'sample_id', 'ext': { 'provider': 'some.provider.com' } } + }; + const newEids = createEidsArray(userId); + expect(newEids.length).to.equal(1); + expect(newEids[0]).to.deep.equal({ + source: 'openx.net', + uids: [{ + id: 'sample_id', + atype: 3, + ext: { + provider: 'some.provider.com' + } + }] + }); + }); + + it('pubmatic', function() { + const userId = { + pubmatic: {'id': 'sample_id'} + }; + const newEids = createEidsArray(userId); + expect(newEids.length).to.equal(1); + expect(newEids[0]).to.deep.equal({ + source: 'pubmatic.com', + uids: [{ + id: 'sample_id', + atype: 3 + }] + }); + }); + + it('pubmatic with ext', function() { + const userId = { + pubmatic: {'id': 'sample_id', 'ext': {'provider': 'some.provider.com'}} + }; + const newEids = createEidsArray(userId); + expect(newEids.length).to.equal(1); + expect(newEids[0]).to.deep.equal({ + source: 'pubmatic.com', + uids: [{ + id: 'sample_id', + atype: 3, + ext: { + provider: 'some.provider.com' + } + }] + }); + }); + it('liveIntentId; getValue call and NO ext', function() { const userId = { lipb: { @@ -348,6 +549,24 @@ describe('eids array generation for known sub-modules', function() { }); }); + it('uid2 with ext', function() { + const userId = { + uid2: {'id': 'Sample_AD_Token', 'ext': {'provider': 'some.provider.com'}} + }; + const newEids = createEidsArray(userId); + expect(newEids.length).to.equal(1); + expect(newEids[0]).to.deep.equal({ + source: 'uidapi.com', + uids: [{ + id: 'Sample_AD_Token', + atype: 3, + ext: { + provider: 'some.provider.com' + } + }] + }); + }); + it('euid', function() { const userId = { euid: {'id': 'Sample_AD_Token'} @@ -460,6 +679,21 @@ describe('eids array generation for known sub-modules', function() { }); }); + it('operaId', function() { + const userId = { + operaId: 'some-random-id-value' + }; + const newEids = createEidsArray(userId); + expect(newEids.length).to.equal(1); + expect(newEids[0]).to.deep.equal({ + source: 't.adx.opera.com', + uids: [{ + id: 'some-random-id-value', + atype: 1 + }] + }); + }); + it('33acrossId', function() { const userId = { '33acrossId': { diff --git a/test/spec/modules/eplanningBidAdapter_spec.js b/test/spec/modules/eplanningBidAdapter_spec.js index 1a6cfd7afe4..a381d7644a1 100644 --- a/test/spec/modules/eplanningBidAdapter_spec.js +++ b/test/spec/modules/eplanningBidAdapter_spec.js @@ -74,6 +74,53 @@ describe('E-Planning Adapter', function () { }, 'sizes': [[300, 250], [300, 600]], }; + const validBidSpaceNameWithBidFloor = { + bidder: 'eplanning', + 'bidId': BID_ID, + params: { + 'ci': CI, + 'sn': SN, + }, + getFloor: () => ({ currency: 'USD', floor: 1.16 }), + 'sizes': [[300, 250], [300, 600]], + }; + const validBidSpaceOutstreamWithBidFloor = { + 'bidder': 'eplanning', + 'bidId': BID_ID, + 'params': { + 'ci': CI, + 'sn': SN, + }, + getFloor: () => ({ currency: 'USD', floor: 1.16 }), + 'mediaTypes': { + 'video': { + 'context': 'outstream', + 'playerSize': [300, 600], + 'mimes': ['video/mp4'], + 'protocols': [1, 2, 3, 4, 5, 6], + 'playbackmethod': [2], + 'skip': 1 + } + }, + }; + const validBidSpaceInstreamWithBidFloor = { + 'bidder': 'eplanning', + 'bidId': BID_ID, + 'params': { + 'ci': CI, + 'sn': SN, + }, + getFloor: () => ({ currency: 'USD', floor: 1.16 }), + 'mediaTypes': { + 'video': { + 'context': 'instream', + 'mimes': ['video/mp4'], + 'protocols': [1, 2, 3, 4, 5, 6], + 'playbackmethod': [2], + 'skip': 1 + } + }, + }; const validBidSpaceOutstream = { 'bidder': 'eplanning', 'bidId': BID_ID, @@ -573,6 +620,26 @@ describe('E-Planning Adapter', function () { expect(e).to.equal(SN + ':300x250,300x600'); }); + it('should return e parameter with space name attribute with value according to the adunit sizes and bidFloor', function () { + const e = spec.buildRequests([validBidSpaceNameWithBidFloor], bidderRequest).data.e; + expect(e).to.equal(SN + ':300x250,300x600|' + validBidSpaceNameWithBidFloor.getFloor().floor); + }); + + it('should return correct e parameter with support vast with one space with size outstream and bidFloor', function () { + const data = spec.buildRequests([validBidSpaceOutstreamWithBidFloor], bidderRequest).data; + expect(data.e).to.equal('video_300x600_0:300x600;1|' + validBidSpaceOutstreamWithBidFloor.getFloor().floor); + expect(data.vctx).to.equal(2); + expect(data.vv).to.equal(3); + }); + + it('should return correct e parameter with support vast with one space with size instream with bidFloor', function () { + let bidRequests = [validBidSpaceInstreamWithBidFloor]; + const data = spec.buildRequests(bidRequests, bidderRequest).data; + expect(data.e).to.equal('video_640x480_0:640x480;1|' + validBidSpaceInstreamWithBidFloor.getFloor().floor); + expect(data.vctx).to.equal(1); + expect(data.vv).to.equal(3); + }); + it('should return correct e parameter with more than one adunit', function () { const NEW_CODE = ADUNIT_CODE + '2'; const CLEAN_NEW_CODE = CLEAN_ADUNIT_CODE + '2'; diff --git a/test/spec/modules/euidIdSystem_spec.js b/test/spec/modules/euidIdSystem_spec.js index 9a016b0facd..98770fa80bc 100644 --- a/test/spec/modules/euidIdSystem_spec.js +++ b/test/spec/modules/euidIdSystem_spec.js @@ -1,15 +1,13 @@ -import {coreStorage, init, setSubmoduleRegistry, requestBidsHook} from 'modules/userId/index.js'; +import {coreStorage, init, setSubmoduleRegistry} from 'modules/userId/index.js'; import {config} from 'src/config.js'; -import * as utils from 'src/utils.js'; -import { euidIdSubmodule } from 'modules/euidIdSystem.js'; +import {euidIdSubmodule} from 'modules/euidIdSystem.js'; import 'modules/consentManagement.js'; import 'src/prebid.js'; -import { getGlobal } from 'src/prebidGlobal.js'; -import { server } from 'test/mocks/xhr.js'; -import { configureTimerInterceptors } from 'test/mocks/timers.js'; -import { cookieHelpers, runAuction, apiHelpers, setGdprApplies } from './uid2IdSystem_helpers.js'; +import * as utils from 'src/utils.js'; +import {apiHelpers, cookieHelpers, runAuction, setGdprApplies} from './uid2IdSystem_helpers.js'; import {hook} from 'src/hook.js'; import {uninstall as uninstallGdprEnforcement} from 'modules/gdprEnforcement.js'; +import {server} from 'test/mocks/xhr'; let expect = require('chai').expect; @@ -25,29 +23,45 @@ const auctionDelayMs = 10; const makeEuidIdentityContainer = (token) => ({euid: {id: token}}); const useLocalStorage = true; + const makePrebidConfig = (params = null, extraSettings = {}, debug = false) => ({ userSync: { auctionDelay: auctionDelayMs, userIds: [{name: 'euid', params: {storage: useLocalStorage ? 'localStorage' : 'cookie', ...params}, ...extraSettings}] }, debug }); +const cstgConfigParams = { serverPublicKey: 'UID2-X-L-24B8a/eLYBmRkXA9yPgRZt+ouKbXewG2OPs23+ov3JC8mtYJBCx6AxGwJ4MlwUcguebhdDp2CvzsCgS9ogwwGA==', subscriptionId: 'subscription-id' } +const clientSideGeneratedToken = 'client-side-generated-advertising-token'; + const apiUrl = 'https://prod.euid.eu/v2/token/refresh'; +const cstgApiUrl = 'https://prod.euid.eu/v2/token/client-generate'; const headers = { 'Content-Type': 'application/json' }; -const makeSuccessResponseBody = () => btoa(JSON.stringify({ status: 'success', body: { ...apiHelpers.makeTokenResponse(initialToken), advertising_token: refreshedToken } })); -const configureEuidResponse = (httpStatus, response) => server.respondWith('POST', apiUrl, (xhr) => xhr.respond(httpStatus, headers, response)); +const makeSuccessResponseBody = (token) => btoa(JSON.stringify({ status: 'success', body: { ...apiHelpers.makeTokenResponse(initialToken), advertising_token: token } })); const expectToken = (bid, token) => expect(bid?.userId ?? {}).to.deep.include(makeEuidIdentityContainer(token)); const expectNoIdentity = (bid) => expect(bid).to.not.haveOwnProperty('userId'); describe('EUID module', function() { - let suiteSandbox, testSandbox, timerSpy, fullTestTitle, restoreSubtleToUndefined = false; + let suiteSandbox, restoreSubtleToUndefined = false; + + const configureEuidResponse = (httpStatus, response) => server.respondWith('POST', apiUrl, (xhr) => xhr.respond(httpStatus, headers, response)); + const configureEuidCstgResponse = (httpStatus, response) => server.respondWith('POST', cstgApiUrl, (xhr) => xhr.respond(httpStatus, headers, response)); + before(function() { uninstallGdprEnforcement(); hook.ready(); suiteSandbox = sinon.sandbox.create(); if (typeof window.crypto.subtle === 'undefined') { restoreSubtleToUndefined = true; - window.crypto.subtle = { importKey: () => {}, decrypt: () => {} }; + window.crypto.subtle = { importKey: () => {}, digest: () => {}, decrypt: () => {}, deriveKey: () => {}, encrypt: () => {}, generateKey: () => {}, exportKey: () => {} }; } suiteSandbox.stub(window.crypto.subtle, 'importKey').callsFake(() => Promise.resolve()); + suiteSandbox.stub(window.crypto.subtle, 'digest').callsFake(() => Promise.resolve('hashed_value')); suiteSandbox.stub(window.crypto.subtle, 'decrypt').callsFake((settings, key, data) => Promise.resolve(new Uint8Array([...settings.iv, ...data]))); + suiteSandbox.stub(window.crypto.subtle, 'deriveKey').callsFake(() => Promise.resolve()); + suiteSandbox.stub(window.crypto.subtle, 'exportKey').callsFake(() => Promise.resolve()); + suiteSandbox.stub(window.crypto.subtle, 'encrypt').callsFake(() => Promise.resolve(new ArrayBuffer())); + suiteSandbox.stub(window.crypto.subtle, 'generateKey').callsFake(() => Promise.resolve({ + privateKey: {}, + publicKey: {} + })); }); after(function() { suiteSandbox.restore(); @@ -114,10 +128,23 @@ describe('EUID module', function() { it('When an expired token is provided and the API responds in time, the refreshed token is provided to the auction.', async function() { setGdprApplies(true); const euidToken = apiHelpers.makeTokenResponse(initialToken, true, true); - configureEuidResponse(200, makeSuccessResponseBody()); + configureEuidResponse(200, makeSuccessResponseBody(refreshedToken)); config.setConfig(makePrebidConfig({euidToken})); - apiHelpers.respondAfterDelay(1); + apiHelpers.respondAfterDelay(1, server); const bid = await runAuction(); expectToken(bid, refreshedToken); }); + + if (FEATURES.UID2_CSTG) { + it('Should use client side generated EUID token in the auction.', async function() { + setGdprApplies(true); + const euidToken = apiHelpers.makeTokenResponse(initialToken, true, true); + configureEuidCstgResponse(200, makeSuccessResponseBody(clientSideGeneratedToken)); + config.setConfig(makePrebidConfig({ euidToken, ...cstgConfigParams, email: 'test@test.com' })); + apiHelpers.respondAfterDelay(1, server); + + const bid = await runAuction(); + expectToken(bid, clientSideGeneratedToken); + }); + } }); diff --git a/test/spec/modules/experianRtdProvider_spec.js b/test/spec/modules/experianRtdProvider_spec.js new file mode 100644 index 00000000000..fd104674d70 --- /dev/null +++ b/test/spec/modules/experianRtdProvider_spec.js @@ -0,0 +1,365 @@ +import { + EXPERIAN_RTID_DATA_KEY, + EXPERIAN_RTID_EXPIRATION_KEY, + EXPERIAN_RTID_STALE_KEY, + SUBMODULE_NAME, + experianRtdObj, + experianRtdSubmodule, EXPERIAN_RTID_NO_TRACK_KEY +} from '../../../modules/experianRtdProvider.js'; +import { getStorageManager } from '../../../src/storageManager.js'; +import { MODULE_TYPE_RTD } from '../../../src/activities/modules'; +import { safeJSONParse, timestamp } from '../../../src/utils'; +import {server} from '../../mocks/xhr.js'; + +describe('Experian realtime module', () => { + const sandbox = sinon.createSandbox(); + let requests; + + const storage = getStorageManager({ moduleType: MODULE_TYPE_RTD, moduleName: SUBMODULE_NAME }) + beforeEach(() => { + requests = server.requests; + storage.removeDataFromLocalStorage(EXPERIAN_RTID_DATA_KEY, null) + storage.removeDataFromLocalStorage(EXPERIAN_RTID_EXPIRATION_KEY, null) + storage.removeDataFromLocalStorage(EXPERIAN_RTID_STALE_KEY, null) + storage.removeDataFromLocalStorage(EXPERIAN_RTID_NO_TRACK_KEY, null) + }) + afterEach(() => { + sandbox.restore(); + }) + // Bid request config + const reqBidsConfigObj = { + adUnits: [{ + bids: [ + { bidder: 'appnexus' } + ] + }] + }; + describe('init', () => { + it('succeeds when params have accountId', () => { + const initResult = experianRtdSubmodule.init({ params: { accountId: 'ZylatYg' } }) + expect(initResult).to.be.true; + }) + + it('fails when params don\'t have accountId', () => { + const initResult = experianRtdSubmodule.init({ }) + expect(initResult).to.be.false; + }) + }) + + describe('getBidRequestData', () => { + describe('when local storage has data, isn\'t no track, isn\'t stale and isn\'t expired', () => { + beforeEach(() => { + const now = timestamp() + storage.setDataInLocalStorage(EXPERIAN_RTID_DATA_KEY, JSON.stringify([ + { + bidder: 'pubmatic', + data: { + key: 'pubmatic-encryption-key-1', + data: 'IkhlbGxvLCB3b3JsZC4gSGVsbG8sIHdvcmxkLiBIZWxsbywgd29ybGQuIg==' + } + }, + { + bidder: 'sovrn', + data: { + key: 'sovrn-encryption-key-1', + data: 'IkhlbGxvLCB3b3JsZC4gSGVsbG8sIHdvcmxkLiBIZWxsbywgd29ybGQuIg==' + } + } + ]), null) + + storage.setDataInLocalStorage(EXPERIAN_RTID_EXPIRATION_KEY, new Date(now + 100000).toISOString(), null) + storage.setDataInLocalStorage(EXPERIAN_RTID_STALE_KEY, new Date(now + 50000).toISOString(), null) + }) + it('doesn\'t request data envelope, and alters bids', () => { + const bidsConfig = { + ortb2Fragments: { + bidder: {} + } + } + const moduleConfig = { params: { accountId: 'ZylatYg', bidders: ['pubmatic', 'sovrn'] } } + const dataEnvelopeSpy = sandbox.spy(experianRtdObj, 'requestDataEnvelope') + const alterBidsSpy = sandbox.spy(experianRtdObj, 'alterBids') + experianRtdSubmodule.getBidRequestData(bidsConfig, sinon.stub, moduleConfig) + sandbox.assert.calledWithExactly(alterBidsSpy, bidsConfig, moduleConfig) + expect(dataEnvelopeSpy.called).to.be.false; + }) + }) + + describe('when local storage has data but it is stale', () => { + beforeEach(() => { + const now = timestamp() + storage.setDataInLocalStorage(EXPERIAN_RTID_DATA_KEY, JSON.stringify([ + { + bidder: 'pubmatic', + data: { + key: 'pubmatic-encryption-key-1', + data: 'IkhlbGxvLCB3b3JsZC4gSGVsbG8sIHdvcmxkLiBIZWxsbywgd29ybGQuIg==' + } + }, + { + bidder: 'sovrn', + data: { + key: 'sovrn-encryption-key-1', + data: 'IkhlbGxvLCB3b3JsZC4gSGVsbG8sIHdvcmxkLiBIZWxsbywgd29ybGQuIg==' + } + } + ]), null) + + storage.setDataInLocalStorage(EXPERIAN_RTID_EXPIRATION_KEY, new Date(now + 100000).toISOString(), null) + storage.setDataInLocalStorage(EXPERIAN_RTID_STALE_KEY, new Date(now - 50000).toISOString(), null) + }) + it('it requests data envelope and alters bids', () => { + const bidsConfig = { + ortb2Fragments: { + bidder: {} + } + } + const userConsent = {gdpr: {}, uspConsent: {}} + const moduleConfig = { params: { accountId: 'ZylatYg', bidders: ['pubmatic', 'sovrn'] } } + const dataEnvelopeSpy = sandbox.spy(experianRtdObj, 'requestDataEnvelope') + const alterBidsSpy = sandbox.spy(experianRtdObj, 'alterBids') + experianRtdSubmodule.getBidRequestData(bidsConfig, sinon.stub, moduleConfig, userConsent) + sandbox.assert.calledWithExactly(alterBidsSpy, bidsConfig, moduleConfig) + sandbox.assert.calledWithExactly(dataEnvelopeSpy, moduleConfig, userConsent) + }) + }) + describe('when local storage has data but it is expired', () => { + beforeEach(() => { + const now = timestamp() + storage.setDataInLocalStorage(EXPERIAN_RTID_DATA_KEY, JSON.stringify([ + { + bidder: 'pubmatic', + data: { + key: 'pubmatic-encryption-key-1', + data: 'IkhlbGxvLCB3b3JsZC4gSGVsbG8sIHdvcmxkLiBIZWxsbywgd29ybGQuIg==' + } + }, + { + bidder: 'sovrn', + data: { + key: 'sovrn-encryption-key-1', + data: 'IkhlbGxvLCB3b3JsZC4gSGVsbG8sIHdvcmxkLiBIZWxsbywgd29ybGQuIg==' + } + } + ]), null) + + storage.setDataInLocalStorage(EXPERIAN_RTID_EXPIRATION_KEY, new Date(now - 50000).toISOString(), null) + storage.setDataInLocalStorage(EXPERIAN_RTID_STALE_KEY, new Date(now - 100000).toISOString(), null) + }) + it('requests data envelope, and doesn\'t alter bids', () => { + const bidsConfig = { + ortb2Fragments: { + bidder: {} + } + } + const userConsent = {gdpr: {}, uspConsent: {}} + const moduleConfig = { params: { accountId: 'ZylatYg', bidders: ['pubmatic', 'sovrn'] } } + const dataEnvelopeSpy = sandbox.spy(experianRtdObj, 'requestDataEnvelope') + const alterBidsSpy = sandbox.spy(experianRtdObj, 'alterBids') + experianRtdSubmodule.getBidRequestData(bidsConfig, sinon.stub, moduleConfig, userConsent) + sandbox.assert.calledWithExactly(dataEnvelopeSpy, moduleConfig, userConsent) + expect(alterBidsSpy.called).to.be.false; + }) + }) + describe('when local storage has no data envelope', () => { + it('requests data envelope, and doesn\'t alter bids', () => { + const bidsConfig = { + ortb2Fragments: { + bidder: {} + } + } + const userConsent = {gdpr: {}, uspConsent: {}} + const moduleConfig = { params: { accountId: 'ZylatYg', bidders: ['pubmatic', 'sovrn'] } } + const dataEnvelopeSpy = sandbox.spy(experianRtdObj, 'requestDataEnvelope') + const alterBidsSpy = sandbox.spy(experianRtdObj, 'alterBids') + experianRtdSubmodule.getBidRequestData(bidsConfig, sinon.stub, moduleConfig, userConsent) + sandbox.assert.calledWithExactly(dataEnvelopeSpy, moduleConfig, userConsent) + expect(alterBidsSpy.called).to.be.false; + }) + }) + describe('when local storage has no track and is expired', () => { + beforeEach(() => { + const now = timestamp() + storage.setDataInLocalStorage(EXPERIAN_RTID_NO_TRACK_KEY, 'no_track', null) + + storage.setDataInLocalStorage(EXPERIAN_RTID_EXPIRATION_KEY, new Date(now - 50000).toISOString(), null) + storage.setDataInLocalStorage(EXPERIAN_RTID_STALE_KEY, new Date(now - 100000).toISOString(), null) + }) + it('requests data envelope, and doesn\'t alter bids', () => { + const bidsConfig = { + ortb2Fragments: { + bidder: {} + } + } + const userConsent = {gdpr: {}, uspConsent: {}} + const moduleConfig = { params: { accountId: 'ZylatYg', bidders: ['pubmatic', 'sovrn'] } } + const dataEnvelopeSpy = sandbox.spy(experianRtdObj, 'requestDataEnvelope') + const alterBidsSpy = sandbox.spy(experianRtdObj, 'alterBids') + experianRtdSubmodule.getBidRequestData(bidsConfig, sinon.stub, moduleConfig, userConsent) + sandbox.assert.calledWithExactly(dataEnvelopeSpy, moduleConfig, userConsent) + expect(alterBidsSpy.called).to.be.false; + }) + }) + + describe('when local storage has no track and is stale', () => { + beforeEach(() => { + const now = timestamp() + storage.setDataInLocalStorage(EXPERIAN_RTID_NO_TRACK_KEY, 'no_track', null) + + storage.setDataInLocalStorage(EXPERIAN_RTID_EXPIRATION_KEY, new Date(now + 100000).toISOString(), null) + storage.setDataInLocalStorage(EXPERIAN_RTID_STALE_KEY, new Date(now - 50000).toISOString(), null) + }) + it('requests data envelope, and doesn\'t alter bids', () => { + const bidsConfig = { + ortb2Fragments: { + bidder: {} + } + } + const userConsent = {gdpr: {}, uspConsent: {}} + const moduleConfig = { params: { accountId: 'ZylatYg', bidders: ['pubmatic', 'sovrn'] } } + const dataEnvelopeSpy = sandbox.spy(experianRtdObj, 'requestDataEnvelope') + const alterBidsSpy = sandbox.spy(experianRtdObj, 'alterBids') + experianRtdSubmodule.getBidRequestData(bidsConfig, sinon.stub, moduleConfig, userConsent) + sandbox.assert.calledWithExactly(dataEnvelopeSpy, moduleConfig, userConsent) + expect(alterBidsSpy.called).to.be.false; + }) + }) + + describe('when local storage has no track and isn\'t expired or stale', () => { + beforeEach(() => { + const now = timestamp() + storage.setDataInLocalStorage(EXPERIAN_RTID_NO_TRACK_KEY, 'no_track', null) + + storage.setDataInLocalStorage(EXPERIAN_RTID_EXPIRATION_KEY, new Date(now + 100000).toISOString(), null) + storage.setDataInLocalStorage(EXPERIAN_RTID_STALE_KEY, new Date(now + 50000).toISOString(), null) + }) + it('doesn\'t alter bids and doesn\'t request data envelope', () => { + const bidsConfig = { + ortb2Fragments: { + bidder: {} + } + } + const userConsent = {gdpr: {}, uspConsent: {}} + const moduleConfig = { params: { accountId: 'ZylatYg', bidders: ['pubmatic', 'sovrn'] } } + const dataEnvelopeSpy = sandbox.spy(experianRtdObj, 'requestDataEnvelope') + const alterBidsSpy = sandbox.spy(experianRtdObj, 'alterBids') + experianRtdSubmodule.getBidRequestData(bidsConfig, sinon.stub, moduleConfig, userConsent) + expect(alterBidsSpy.called).to.be.false; + expect(dataEnvelopeSpy.called).to.be.false; + }) + }) + }) + + describe('alterBids', () => { + describe('data envelope has every bidder from config', () => { + beforeEach(() => { + const now = timestamp() + storage.setDataInLocalStorage(EXPERIAN_RTID_DATA_KEY, JSON.stringify([ + { + bidder: 'pubmatic', + data: { + key: 'pubmatic-encryption-key-1', + data: 'IkhlbGxvLCB3b3JsZC4gSGVsbG8sIHdvcmxkLiBIZWxsbywgd29ybGQuIg==' + } + }, + { + bidder: 'sovrn', + data: { + key: 'sovrn-encryption-key-1', + data: 'IkhlbGxvLCB3b3JsZC4gSGVsbG8sIHdvcmxkLiBIZWxsbywgd29ybGQuIg==' + } + } + ]), null) + + storage.setDataInLocalStorage(EXPERIAN_RTID_EXPIRATION_KEY, new Date(now + 100000).toISOString(), null) + storage.setDataInLocalStorage(EXPERIAN_RTID_STALE_KEY, new Date(now + 50000).toISOString(), null) + }) + + it('alters bids for the bidders in the module config', () => { + const bidsConfig = { + ortb2Fragments: { + bidder: {} + } + } + const moduleConfig = { params: { accountId: 'ZylatYg', bidders: ['pubmatic'] } } + experianRtdObj.alterBids(bidsConfig, moduleConfig); + expect(bidsConfig.ortb2Fragments.bidder).to.deep.equal({pubmatic: { + experianRtidKey: 'pubmatic-encryption-key-1', + experianRtidData: 'IkhlbGxvLCB3b3JsZC4gSGVsbG8sIHdvcmxkLiBIZWxsbywgd29ybGQuIg==' + }}) + }) + }) + describe('data envelope is missing bidders from config', () => { + beforeEach(() => { + const now = timestamp() + storage.setDataInLocalStorage(EXPERIAN_RTID_DATA_KEY, JSON.stringify([ + { + bidder: 'sovrn', + data: { + key: 'sovrn-encryption-key-1', + data: 'IkhlbGxvLCB3b3JsZC4gSGVsbG8sIHdvcmxkLiBIZWxsbywgd29ybGQuIg==' + } + } + ]), null) + + storage.setDataInLocalStorage(EXPERIAN_RTID_EXPIRATION_KEY, new Date(now + 100000).toISOString(), null) + storage.setDataInLocalStorage(EXPERIAN_RTID_STALE_KEY, new Date(now + 50000).toISOString(), null) + }) + + it('alters bids for the bidders in the module config', () => { + const bidsConfig = { + ortb2Fragments: { + bidder: {} + } + } + const moduleConfig = { params: { accountId: 'ZylatYg', bidders: ['pubmatic', 'sovrn'] } } + experianRtdObj.alterBids(bidsConfig, moduleConfig); + expect(bidsConfig.ortb2Fragments.bidder).to.deep.equal({ + sovrn: { + experianRtidKey: 'sovrn-encryption-key-1', + experianRtidData: 'IkhlbGxvLCB3b3JsZC4gSGVsbG8sIHdvcmxkLiBIZWxsbywgd29ybGQuIg==' + }}) + }) + }) + }) + + describe('requestDataEnvelope', () => { + it('sends request to experian rtd and stores response', () => { + const moduleConfig = { params: { accountId: 'ZylatYg', bidders: ['pubmatic', 'sovrn'] } } + experianRtdObj.requestDataEnvelope(moduleConfig, { gdpr: { gdprApplies: 0, consentString: 'wow' }, uspConsent: '1YYY' }) + requests[0].respond( + 200, + { 'Content-Type': 'application/json' }, + '{"staleAt":"2023-06-01T00:00:00","expiresAt":"2023-06-03T00:00:00","status":"ok","data":[{"bidder":"pubmatic","data":{"key":"pubmatic-encryption-key-1","data":"IkhlbGxvLCB3b3JsZC4gSGVsbG8sIHdvcmxkLiBIZWxsbywgd29ybGQuIg=="}},{"bidder":"sovrn","data":{"key":"sovrn-encryption-key-1","data":"IkhlbGxvLCB3b3JsZC4gSGVsbG8sIHdvcmxkLiBIZWxsbywgd29ybGQuIg=="}}]}' + ) + + expect(requests[0].url).to.equal('https://rtid.tapad.com/acc/ZylatYg/ids?gdpr=0&gdpr_consent=wow&us_privacy=1YYY') + expect(safeJSONParse(storage.getDataFromLocalStorage(EXPERIAN_RTID_DATA_KEY, null))).to.deep.equal([{bidder: 'pubmatic', data: {key: 'pubmatic-encryption-key-1', data: 'IkhlbGxvLCB3b3JsZC4gSGVsbG8sIHdvcmxkLiBIZWxsbywgd29ybGQuIg=='}}, {bidder: 'sovrn', data: {key: 'sovrn-encryption-key-1', data: 'IkhlbGxvLCB3b3JsZC4gSGVsbG8sIHdvcmxkLiBIZWxsbywgd29ybGQuIg=='}}]) + expect(storage.getDataFromLocalStorage(EXPERIAN_RTID_STALE_KEY)).to.equal('2023-06-01T00:00:00') + expect(storage.getDataFromLocalStorage(EXPERIAN_RTID_EXPIRATION_KEY)).to.equal('2023-06-03T00:00:00') + }) + }) + + describe('extractConsentQueryString', () => { + describe('when userConsent is empty', () => { + it('returns undefined', () => { + expect(experianRtdObj.extractConsentQueryString({})).to.be.undefined + }) + }) + + describe('when userConsent exists', () => { + it('builds query string', () => { + expect( + experianRtdObj.extractConsentQueryString({}, { gdpr: { gdprApplies: 1, consentString: 'this-is-something' }, uspConsent: '1YYY' }) + ).to.equal('?gdpr=1&gdpr_consent=this-is-something&us_privacy=1YYY') + }) + }) + + describe('when config.ids exists', () => { + it('builds query string', () => { + expect(experianRtdObj.extractConsentQueryString({ params: { accountId: 'ZylatYg', ids: { maid: ['424', '2982'], hem: 'my-hem' } } }, { gdpr: { gdprApplies: 1, consentString: 'this-is-something' }, uspConsent: '1YYY' })) + .to.equal('?gdpr=1&gdpr_consent=this-is-something&us_privacy=1YYY&id.maid=424&id.maid=2982&id.hem=my-hem') + }) + }) + }) +}) diff --git a/test/spec/modules/feedadBidAdapter_spec.js b/test/spec/modules/feedadBidAdapter_spec.js index 152adba9d00..cb81c6f06de 100644 --- a/test/spec/modules/feedadBidAdapter_spec.js +++ b/test/spec/modules/feedadBidAdapter_spec.js @@ -621,7 +621,7 @@ describe('FeedAdAdapter', function () { expect(call.url).to.equal('https://api.feedad.com/1/prebid/web/events'); expect(JSON.parse(call.requestBody)).to.deep.equal(expectedData); expect(call.method).to.equal('POST'); - expect(call.requestHeaders).to.include({'Content-Type': 'application/json;charset=utf-8'}); + expect(call.requestHeaders).to.include({'Content-Type': 'application/json'}); }) }); }); diff --git a/test/spec/modules/fledgeForGpt_spec.js b/test/spec/modules/fledgeForGpt_spec.js new file mode 100644 index 00000000000..60a8e196ae0 --- /dev/null +++ b/test/spec/modules/fledgeForGpt_spec.js @@ -0,0 +1,419 @@ +import { + expect +} from 'chai'; +import * as fledge from 'modules/fledgeForGpt.js'; +import {config} from '../../../src/config.js'; +import adapterManager from '../../../src/adapterManager.js'; +import * as utils from '../../../src/utils.js'; +import * as gptUtils from '../../../libraries/gptUtils/gptUtils.js'; +import {hook} from '../../../src/hook.js'; +import 'modules/appnexusBidAdapter.js'; +import 'modules/rubiconBidAdapter.js'; +import {parseExtPrebidFledge, setImpExtAe, setResponseFledgeConfigs} from 'modules/fledgeForGpt.js'; +import * as events from 'src/events.js'; +import CONSTANTS from 'src/constants.json'; +import {getGlobal} from '../../../src/prebidGlobal.js'; + +describe('fledgeForGpt module', () => { + let sandbox; + + beforeEach(() => { + sandbox = sinon.sandbox.create(); + }); + afterEach(() => { + sandbox.restore(); + }); + describe('addComponentAuction', function () { + before(() => { + fledge.init({enabled: true}); + }); + + const fledgeAuctionConfig = { + seller: 'bidder', + mock: 'config' + }; + + describe('addComponentAuctionHook', function () { + let nextFnSpy, mockGptSlot; + beforeEach(function () { + nextFnSpy = sinon.spy(); + mockGptSlot = { + setConfig: sinon.stub(), + getAdUnitPath: () => 'mock/gpt/au' + }; + sandbox.stub(gptUtils, 'getGptSlotForAdUnitCode').callsFake(() => mockGptSlot); + }); + + it('should call next()', function () { + const request = {auctionId: 'aid', adUnitCode: 'auc'}; + fledge.addComponentAuctionHook(nextFnSpy, request, fledgeAuctionConfig); + sinon.assert.calledWith(nextFnSpy, request, fledgeAuctionConfig); + }); + + it('should collect auction configs and route them to GPT at end of auction', () => { + events.emit(CONSTANTS.EVENTS.AUCTION_INIT, {auctionId: 'aid'}); + const cf1 = {...fledgeAuctionConfig, id: 1, seller: 'b1'}; + const cf2 = {...fledgeAuctionConfig, id: 2, seller: 'b2'}; + fledge.addComponentAuctionHook(nextFnSpy, {auctionId: 'aid', adUnitCode: 'au1'}, cf1); + fledge.addComponentAuctionHook(nextFnSpy, {auctionId: 'aid', adUnitCode: 'au2'}, cf2); + events.emit(CONSTANTS.EVENTS.AUCTION_END, {auctionId: 'aid'}); + sinon.assert.calledWith(gptUtils.getGptSlotForAdUnitCode, 'au1'); + sinon.assert.calledWith(gptUtils.getGptSlotForAdUnitCode, 'au2'); + sinon.assert.calledWith(mockGptSlot.setConfig, { + componentAuction: [{ + configKey: 'b1', + auctionConfig: cf1, + }] + }); + sinon.assert.calledWith(mockGptSlot.setConfig, { + componentAuction: [{ + configKey: 'b2', + auctionConfig: cf2, + }] + }); + }); + + it('should drop auction configs after end of auction', () => { + events.emit(CONSTANTS.EVENTS.AUCTION_INIT, {auctionId: 'aid'}); + events.emit(CONSTANTS.EVENTS.AUCTION_END, {auctionId: 'aid'}); + fledge.addComponentAuctionHook(nextFnSpy, {auctionId: 'aid', adUnitCode: 'au'}, fledgeAuctionConfig); + events.emit(CONSTANTS.EVENTS.AUCTION_END, {auctionId: 'aid'}); + sinon.assert.notCalled(mockGptSlot.setConfig); + }); + + it('should augment auctionSignals with FPD', () => { + events.emit(CONSTANTS.EVENTS.AUCTION_INIT, {auctionId: 'aid'}); + fledge.addComponentAuctionHook(nextFnSpy, {auctionId: 'aid', adUnitCode: 'au1', ortb2: {fpd: 1}, ortb2Imp: {fpd: 2}}, fledgeAuctionConfig); + events.emit(CONSTANTS.EVENTS.AUCTION_END, {auctionId: 'aid'}); + sinon.assert.calledWith(mockGptSlot.setConfig, { + componentAuction: [{ + configKey: 'bidder', + auctionConfig: { + ...fledgeAuctionConfig, + auctionSignals: { + prebid: { + ortb2: {fpd: 1}, + ortb2Imp: {fpd: 2} + } + } + }, + }] + }) + }) + + describe('floor signal', () => { + before(() => { + if (!getGlobal().convertCurrency) { + getGlobal().convertCurrency = () => null; + getGlobal().convertCurrency.mock = true; + } + }); + after(() => { + if (getGlobal().convertCurrency.mock) { + delete getGlobal().convertCurrency; + } + }); + + beforeEach(() => { + sandbox.stub(getGlobal(), 'convertCurrency').callsFake((amount, from, to) => { + if (from === to) return amount; + if (from === 'USD' && to === 'JPY') return amount * 100; + if (from === 'JPY' && to === 'USD') return amount / 100; + throw new Error('unexpected currency conversion'); + }); + }); + + Object.entries({ + 'bids': (payload, values) => { + payload.bidsReceived = values + .map((val) => ({adUnitCode: 'au', cpm: val.amount, currency: val.cur})) + .concat([{adUnitCode: 'other', cpm: 10000, currency: 'EUR'}]) + }, + 'no bids': (payload, values) => { + payload.bidderRequests = values + .map((val) => ({bids: [{adUnitCode: 'au', getFloor: () => ({floor: val.amount, currency: val.cur})}]})) + .concat([{bids: {adUnitCode: 'other', getFloor: () => ({floor: -10000, currency: 'EUR'})}}]) + } + }).forEach(([tcase, setup]) => { + describe(`when auction has ${tcase}`, () => { + Object.entries({ + 'no currencies': { + values: [{amount: 1}, {amount: 100}, {amount: 10}, {amount: 100}], + 'bids': { + bidfloor: 100, + bidfloorcur: undefined + }, + 'no bids': { + bidfloor: 1, + bidfloorcur: undefined, + } + }, + 'only zero values': { + values: [{amount: 0, cur: 'USD'}, {amount: 0, cur: 'JPY'}], + 'bids': { + bidfloor: undefined, + bidfloorcur: undefined, + }, + 'no bids': { + bidfloor: undefined, + bidfloorcur: undefined, + } + }, + 'matching currencies': { + values: [{amount: 10, cur: 'JPY'}, {amount: 100, cur: 'JPY'}], + 'bids': { + bidfloor: 100, + bidfloorcur: 'JPY', + }, + 'no bids': { + bidfloor: 10, + bidfloorcur: 'JPY', + } + }, + 'mixed currencies': { + values: [{amount: 10, cur: 'USD'}, {amount: 10, cur: 'JPY'}], + 'bids': { + bidfloor: 10, + bidfloorcur: 'USD' + }, + 'no bids': { + bidfloor: 10, + bidfloorcur: 'JPY', + } + } + }).forEach(([t, testConfig]) => { + const values = testConfig.values; + const {bidfloor, bidfloorcur} = testConfig[tcase]; + + describe(`with ${t}`, () => { + let payload; + beforeEach(() => { + payload = {auctionId: 'aid'}; + setup(payload, values); + }); + + it('should populate bidfloor/bidfloorcur', () => { + events.emit(CONSTANTS.EVENTS.AUCTION_INIT, {auctionId: 'aid'}); + fledge.addComponentAuctionHook(nextFnSpy, {auctionId: 'aid', adUnitCode: 'au'}, fledgeAuctionConfig); + events.emit(CONSTANTS.EVENTS.AUCTION_END, payload); + sinon.assert.calledWith(mockGptSlot.setConfig, sinon.match(arg => { + return arg.componentAuction.some(au => au.auctionConfig.auctionSignals?.prebid?.bidfloor === bidfloor && au.auctionConfig.auctionSignals?.prebid?.bidfloorcur === bidfloorcur) + })) + }) + }); + }); + }) + }) + }); + }); + }); + + describe('fledgeEnabled', function () { + const navProps = Object.fromEntries(['runAdAuction', 'joinAdInterestGroup'].map(p => [p, navigator[p]])); + + before(function () { + // navigator.runAdAuction & co may not exist, so we can't stub it normally with + // sinon.stub(navigator, 'runAdAuction') or something + Object.keys(navProps).forEach(p => { + navigator[p] = sinon.stub(); + }); + hook.ready(); + }); + + after(function () { + Object.entries(navProps).forEach(([p, orig]) => navigator[p] = orig); + }); + + afterEach(function () { + config.resetConfig(); + }); + + const adUnits = [{ + 'code': '/19968336/header-bid-tag1', + 'mediaTypes': { + 'banner': { + 'sizes': [[728, 90]] + }, + }, + 'bids': [ + { + 'bidder': 'appnexus', + }, + { + 'bidder': 'rubicon', + }, + ] + }]; + function expectFledgeFlags(...enableFlags) { + const bidRequests = adapterManager.makeBidRequests( + adUnits, + Date.now(), + utils.getUniqueIdentifierStr(), + function callback() { + }, + [] + ); + + expect(bidRequests[0].bids[0].bidder).equals('appnexus'); + expect(bidRequests[0].fledgeEnabled).to.eql(enableFlags[0].enabled) + bidRequests[0].bids.forEach(bid => expect(bid.ortb2Imp.ext.ae).to.eql(enableFlags[0].ae)) + + expect(bidRequests[1].bids[0].bidder).equals('rubicon'); + expect(bidRequests[1].fledgeEnabled).to.eql(enableFlags[1].enabled) + bidRequests[1].bids.forEach(bid => expect(bid.ortb2Imp?.ext?.ae).to.eql(enableFlags[1].ae)); + } + + describe('with setBidderConfig()', () => { + it('should set fledgeEnabled correctly per bidder', function () { + config.setConfig({bidderSequence: 'fixed'}); + config.setBidderConfig({ + bidders: ['appnexus'], + config: { + fledgeEnabled: true, + defaultForSlots: 1, + } + }); + expectFledgeFlags({enabled: true, ae: 1}, {enabled: void 0, ae: void 0}); + }); + }); + + describe('with setConfig()', () => { + it('should set fledgeEnabled correctly per bidder', function () { + config.setConfig({ + bidderSequence: 'fixed', + fledgeForGpt: { + enabled: true, + bidders: ['appnexus'], + defaultForSlots: 1, + } + }); + expectFledgeFlags({enabled: true, ae: 1}, {enabled: void 0, ae: void 0}); + }); + + it('should set fledgeEnabled correctly for all bidders', function () { + config.setConfig({ + bidderSequence: 'fixed', + fledgeForGpt: { + enabled: true, + defaultForSlots: 1, + } + }); + expectFledgeFlags({enabled: true, ae: 1}, {enabled: true, ae: 1}); + }); + + it('should not override pub-defined ext.ae', () => { + config.setConfig({ + bidderSequence: 'fixed', + fledgeForGpt: { + enabled: true, + defaultForSlots: 1, + } + }); + Object.assign(adUnits[0], {ortb2Imp: {ext: {ae: 0}}}); + expectFledgeFlags({enabled: true, ae: 0}, {enabled: true, ae: 0}); + }) + }); + }); + + describe('ortb processors for fledge', () => { + it('imp.ext.ae should be removed if fledge is not enabled', () => { + const imp = {ext: {ae: 1}}; + setImpExtAe(imp, {}, {bidderRequest: {}}); + expect(imp.ext.ae).to.not.exist; + }); + it('imp.ext.ae should be left intact if fledge is enabled', () => { + const imp = {ext: {ae: 2}}; + setImpExtAe(imp, {}, {bidderRequest: {fledgeEnabled: true}}); + expect(imp.ext.ae).to.equal(2); + }); + describe('parseExtPrebidFledge', () => { + function packageConfigs(configs) { + return { + ext: { + prebid: { + fledge: { + auctionconfigs: configs + } + } + } + }; + } + + function generateImpCtx(fledgeFlags) { + return Object.fromEntries(Object.entries(fledgeFlags).map(([impid, fledgeEnabled]) => [impid, {imp: {ext: {ae: fledgeEnabled}}}])); + } + + function generateCfg(impid, ...ids) { + return ids.map((id) => ({impid, config: {id}})); + } + + function extractResult(ctx) { + return Object.fromEntries( + Object.entries(ctx) + .map(([impid, ctx]) => [impid, ctx.fledgeConfigs?.map(cfg => cfg.config.id)]) + .filter(([_, val]) => val != null) + ); + } + + it('should collect fledge configs by imp', () => { + const ctx = { + impContext: generateImpCtx({e1: 1, e2: 1, d1: 0}) + }; + const resp = packageConfigs( + generateCfg('e1', 1, 2, 3) + .concat(generateCfg('e2', 4) + .concat(generateCfg('d1', 5, 6))) + ); + parseExtPrebidFledge({}, resp, ctx); + expect(extractResult(ctx.impContext)).to.eql({ + e1: [1, 2, 3], + e2: [4], + }); + }); + it('should not choke if fledge config references unknown imp', () => { + const ctx = {impContext: generateImpCtx({i: 1})}; + const resp = packageConfigs(generateCfg('unknown', 1)); + parseExtPrebidFledge({}, resp, ctx); + expect(extractResult(ctx.impContext)).to.eql({}); + }); + }); + describe('setResponseFledgeConfigs', () => { + it('should set fledgeAuctionConfigs paired with their corresponding bid id', () => { + const ctx = { + impContext: { + 1: { + bidRequest: {bidId: 'bid1'}, + fledgeConfigs: [{config: {id: 1}}, {config: {id: 2}}] + }, + 2: { + bidRequest: {bidId: 'bid2'}, + fledgeConfigs: [{config: {id: 3}}] + }, + 3: { + bidRequest: {bidId: 'bid3'} + } + } + }; + const resp = {}; + setResponseFledgeConfigs(resp, {}, ctx); + expect(resp.fledgeAuctionConfigs).to.eql([ + {bidId: 'bid1', config: {id: 1}}, + {bidId: 'bid1', config: {id: 2}}, + {bidId: 'bid2', config: {id: 3}}, + ]); + }); + it('should not set fledgeAuctionConfigs if none exist', () => { + const resp = {}; + setResponseFledgeConfigs(resp, {}, { + impContext: { + 1: { + fledgeConfigs: [] + }, + 2: {} + } + }); + expect(resp).to.eql({}); + }); + }); + }); +}); diff --git a/test/spec/modules/fledge_spec.js b/test/spec/modules/fledge_spec.js deleted file mode 100644 index a81ff05596e..00000000000 --- a/test/spec/modules/fledge_spec.js +++ /dev/null @@ -1,208 +0,0 @@ -import { - expect -} from 'chai'; -import * as fledge from 'modules/fledgeForGpt.js'; -import {config} from '../../../src/config.js'; -import adapterManager from '../../../src/adapterManager.js'; -import * as utils from '../../../src/utils.js'; -import {hook} from '../../../src/hook.js'; -import 'modules/appnexusBidAdapter.js'; -import 'modules/rubiconBidAdapter.js'; -import {parseExtPrebidFledge, setImpExtAe, setResponseFledgeConfigs} from 'modules/fledgeForGpt.js'; - -const CODE = 'sampleBidder'; -const AD_UNIT_CODE = 'mock/placement'; - -describe('fledgeForGpt module', function() { - let nextFnSpy; - fledge.init({enabled: true}) - - const bidRequest = { - adUnitCode: AD_UNIT_CODE, - bids: [{ - bidId: '1', - bidder: CODE, - auctionId: 'first-bid-id', - adUnitCode: AD_UNIT_CODE, - transactionId: 'au', - }] - }; - const fledgeAuctionConfig = { - bidId: '1', - } - - describe('addComponentAuctionHook', function() { - beforeEach(function() { - nextFnSpy = sinon.spy(); - }); - - it('should call next() when a proper adUnitCode and fledgeAuctionConfig are provided', function() { - fledge.addComponentAuctionHook(nextFnSpy, bidRequest.adUnitCode, fledgeAuctionConfig); - expect(nextFnSpy.called).to.be.true; - }); - }); -}); - -describe('fledgeEnabled', function () { - const navProps = Object.fromEntries(['runAdAuction', 'joinAdInterestGroup'].map(p => [p, navigator[p]])) - - before(function () { - // navigator.runAdAuction & co may not exist, so we can't stub it normally with - // sinon.stub(navigator, 'runAdAuction') or something - Object.keys(navProps).forEach(p => { navigator[p] = sinon.stub() }); - hook.ready(); - }); - - after(function() { - Object.entries(navProps).forEach(([p, orig]) => navigator[p] = orig); - }) - - afterEach(function () { - config.resetConfig(); - }); - - it('should set fledgeEnabled correctly per bidder', function () { - config.setConfig({bidderSequence: 'fixed'}) - config.setBidderConfig({ - bidders: ['appnexus'], - config: { - fledgeEnabled: true, - } - }); - - const adUnits = [{ - 'code': '/19968336/header-bid-tag1', - 'mediaTypes': { - 'banner': { - 'sizes': [[728, 90]] - }, - }, - 'bids': [ - { - 'bidder': 'appnexus', - }, - { - 'bidder': 'rubicon', - }, - ] - }]; - - const bidRequests = adapterManager.makeBidRequests( - adUnits, - Date.now(), - utils.getUniqueIdentifierStr(), - function callback() {}, - [] - ); - - expect(bidRequests[0].bids[0].bidder).equals('appnexus'); - expect(bidRequests[0].fledgeEnabled).to.be.true; - - expect(bidRequests[1].bids[0].bidder).equals('rubicon'); - expect(bidRequests[1].fledgeEnabled).to.be.undefined; - }); -}); - -describe('ortb processors for fledge', () => { - describe('imp.ext.ae', () => { - it('should be removed if fledge is not enabled', () => { - const imp = {ext: {ae: 1}}; - setImpExtAe(imp, {}, {bidderRequest: {}}); - expect(imp.ext.ae).to.not.exist; - }) - it('should be left intact if fledge is enabled', () => { - const imp = {ext: {ae: false}}; - setImpExtAe(imp, {}, {bidderRequest: {fledgeEnabled: true}}); - expect(imp.ext.ae).to.equal(false); - }); - }); - describe('parseExtPrebidFledge', () => { - function packageConfigs(configs) { - return { - ext: { - prebid: { - fledge: { - auctionconfigs: configs - } - } - } - } - } - - function generateImpCtx(fledgeFlags) { - return Object.fromEntries(Object.entries(fledgeFlags).map(([impid, fledgeEnabled]) => [impid, {imp: {ext: {ae: fledgeEnabled}}}])); - } - - function generateCfg(impid, ...ids) { - return ids.map((id) => ({impid, config: {id}})); - } - - function extractResult(ctx) { - return Object.fromEntries( - Object.entries(ctx) - .map(([impid, ctx]) => [impid, ctx.fledgeConfigs?.map(cfg => cfg.config.id)]) - .filter(([_, val]) => val != null) - ); - } - - it('should collect fledge configs by imp', () => { - const ctx = { - impContext: generateImpCtx({e1: 1, e2: 1, d1: 0}) - }; - const resp = packageConfigs( - generateCfg('e1', 1, 2, 3) - .concat(generateCfg('e2', 4) - .concat(generateCfg('d1', 5, 6))) - ); - parseExtPrebidFledge({}, resp, ctx); - expect(extractResult(ctx.impContext)).to.eql({ - e1: [1, 2, 3], - e2: [4], - }); - }); - it('should not choke if fledge config references unknown imp', () => { - const ctx = {impContext: generateImpCtx({i: 1})}; - const resp = packageConfigs(generateCfg('unknown', 1)); - parseExtPrebidFledge({}, resp, ctx); - expect(extractResult(ctx.impContext)).to.eql({}); - }); - }); - describe('setResponseFledgeConfigs', () => { - it('should set fledgeAuctionConfigs paired with their corresponding bid id', () => { - const ctx = { - impContext: { - 1: { - bidRequest: {bidId: 'bid1'}, - fledgeConfigs: [{config: {id: 1}}, {config: {id: 2}}] - }, - 2: { - bidRequest: {bidId: 'bid2'}, - fledgeConfigs: [{config: {id: 3}}] - }, - 3: { - bidRequest: {bidId: 'bid3'} - } - } - }; - const resp = {}; - setResponseFledgeConfigs(resp, {}, ctx); - expect(resp.fledgeAuctionConfigs).to.eql([ - {bidId: 'bid1', config: {id: 1}}, - {bidId: 'bid1', config: {id: 2}}, - {bidId: 'bid2', config: {id: 3}}, - ]); - }); - it('should not set fledgeAuctionConfigs if none exist', () => { - const resp = {}; - setResponseFledgeConfigs(resp, {}, { - impContext: { - 1: { - fledgeConfigs: [] - }, - 2: {} - } - }); - expect(resp).to.eql({}); - }); - }); -}); diff --git a/test/spec/modules/flippBidAdapter_spec.js b/test/spec/modules/flippBidAdapter_spec.js new file mode 100644 index 00000000000..518052ad91e --- /dev/null +++ b/test/spec/modules/flippBidAdapter_spec.js @@ -0,0 +1,170 @@ +import {expect} from 'chai'; +import {spec} from 'modules/flippBidAdapter'; +import {newBidder} from 'src/adapters/bidderFactory'; +const ENDPOINT = 'https://gateflipp.flippback.com/flyer-locator-service/client_bidding'; +describe('flippAdapter', function () { + const adapter = newBidder(spec); + + describe('inherited functions', function () { + it('exists and is a function', function () { + expect(adapter.callBids).to.exist.and.to.be.a('function'); + }); + }); + + describe('isBidRequestValid', function () { + const bid = { + bidder: 'flipp', + params: { + publisherNameIdentifier: 'random', + siteId: 1234, + zoneIds: [1, 2, 3, 4], + } + }; + it('should return true when required params found', function () { + expect(spec.isBidRequestValid(bid)).to.equal(true); + }); + + it('should return false when required params are not passed', function () { + let invalidBid = Object.assign({}, bid); + invalidBid.params = { siteId: 1234 } + expect(spec.isBidRequestValid(invalidBid)).to.equal(false); + }); + }); + + describe('buildRequests', function () { + const bidRequests = [{ + bidder: 'flipp', + params: { + siteId: 1234, + }, + adUnitCode: '/10000/unit_code', + sizes: [[300, 600]], + mediaTypes: {banner: {sizes: [[300, 600]]}}, + bidId: '237f4d1a293f99', + bidderRequestId: '1a857fa34c1c96', + auctionId: 'a297d1aa-7900-4ce4-a0aa-caa8d46c4af7', + transactionId: '00b2896c-2731-4f01-83e4-7a3ad5da13b6', + }]; + const bidderRequest = { + refererInfo: { + referer: 'http://example.com' + } + }; + + it('sends bid request to ENDPOINT via POST', function () { + const request = spec.buildRequests(bidRequests, bidderRequest); + expect(request.method).to.equal('POST'); + }); + + it('sends bid request to ENDPOINT with query parameter', function () { + const request = spec.buildRequests(bidRequests, bidderRequest); + expect(request.url).to.equal(ENDPOINT); + }); + }); + + describe('interpretResponse', function() { + it('should get correct bid response', function() { + const bidRequest = { + method: 'POST', + url: ENDPOINT, + data: { + placements: [{ + divName: 'slot', + networkId: 12345, + siteId: 12345, + adTypes: [12345], + count: 1, + prebid: { + requestId: '237f4d1a293f99', + publisherNameIdentifier: 'bid.params.publisherNameIdentifier', + height: 600, + width: 300, + }, + user: '10462725-da61-4d3a-beff-6d05239e9a6e"', + }], + url: 'http://example.com', + }, + }; + + const serverResponse = { + body: { + 'decisions': { + 'inline': [{ + 'bidCpm': 1, + 'adId': 262838368, + 'height': 600, + 'width': 300, + 'storefront': { 'flyer_id': 5435567 }, + 'prebid': { + 'requestId': '237f4d1a293f99', + 'cpm': 1.11, + 'creative': 'Returned from server', + } + }] + }, + 'location': {'city': 'Oakville'}, + }, + }; + + const expectedResponse = [ + { + bidderCode: 'flipp', + requestId: '237f4d1a293f99', + currency: 'USD', + cpm: 1.11, + netRevenue: true, + width: 300, + height: 600, + creativeId: 262838368, + ttl: 30, + ad: 'Returned from server', + } + ]; + + const result = spec.interpretResponse(serverResponse, bidRequest); + expect(result).to.have.lengthOf(1); + expect(result).to.deep.have.same.members(expectedResponse); + }); + + it('should get empty bid response when no ad is returned', function() { + const bidRequest = { + method: 'POST', + url: ENDPOINT, + data: { + placements: [{ + divName: 'slot', + networkId: 12345, + siteId: 12345, + adTypes: [12345], + count: 1, + prebid: { + requestId: '237f4d1a293f99', + publisherNameIdentifier: 'bid.params.publisherNameIdentifier', + height: 600, + width: 300, + }, + user: '10462725-da61-4d3a-beff-6d05239e9a6e"', + }], + url: 'http://example.com', + }, + }; + + const serverResponse = { + body: { + 'decisions': { + 'inline': [] + }, + 'location': {'city': 'Oakville'}, + }, + }; + + const result = spec.interpretResponse(serverResponse, bidRequest); + expect(result).to.have.lengthOf(0); + expect(result).to.deep.have.same.members([]); + }) + + it('should get empty response when bid server returns 204', function() { + expect(spec.interpretResponse({})).to.be.empty; + }); + }); +}); diff --git a/test/spec/modules/fluctBidAdapter_spec.js b/test/spec/modules/fluctBidAdapter_spec.js index d970f70ad85..ff6f8562a4e 100644 --- a/test/spec/modules/fluctBidAdapter_spec.js +++ b/test/spec/modules/fluctBidAdapter_spec.js @@ -99,6 +99,79 @@ describe('fluctAdapter', function () { expect(request.data.page).to.eql('http://example.com'); }); + it('sends no transactionId by default', function () { + const request = spec.buildRequests(bidRequests, bidderRequest)[0]; + expect(request.data.transactionId).to.eql(undefined); + }); + + it('sends ortb2Imp.ext.tid as transactionId', function () { + const request = spec.buildRequests(bidRequests.map((req) => ({ + ...req, + ortb2Imp: { + ext: { + tid: 'tid', + } + }, + })), bidderRequest)[0]; + expect(request.data.transactionId).to.eql('tid'); + }); + + it('sends no gpid by default', function () { + const request = spec.buildRequests(bidRequests, bidderRequest)[0]; + expect(request.data.gpid).to.eql(undefined); + }); + + it('sends ortb2Imp.ext.gpid as gpid', function () { + const request = spec.buildRequests(bidRequests.map((req) => ({ + ...req, + ortb2Imp: { + ext: { + gpid: 'gpid', + data: { + pbadslot: 'data-pbadslot', + adserver: { + adslot: 'data-adserver-adslot', + }, + }, + }, + }, + })), bidderRequest)[0]; + expect(request.data.gpid).to.eql('gpid'); + }); + + it('sends ortb2Imp.ext.data.pbadslot as gpid', function () { + const request = spec.buildRequests(bidRequests.map((req) => ({ + ...req, + ortb2Imp: { + ext: { + data: { + pbadslot: 'data-pbadslot', + adserver: { + adslot: 'data-adserver-adslot', + }, + }, + }, + }, + })), bidderRequest)[0]; + expect(request.data.gpid).to.eql('data-pbadslot'); + }); + + it('sends ortb2Imp.ext.data.adserver.adslot as gpid', function () { + const request = spec.buildRequests(bidRequests.map((req) => ({ + ...req, + ortb2Imp: { + ext: { + data: { + adserver: { + adslot: 'data-adserver-adslot', + }, + }, + }, + }, + })), bidderRequest)[0]; + expect(request.data.gpid).to.eql('data-adserver-adslot'); + }); + it('includes data.user.eids = [] by default', function () { const request = spec.buildRequests(bidRequests, bidderRequest)[0]; expect(request.data.user.eids).to.eql([]); @@ -119,14 +192,14 @@ describe('fluctAdapter', function () { expect(request.data.regs).to.eql(undefined); }); - it('includes filtered user.eids if any exist', function () { + it('includes filtered user.eids if any exists', function () { const bidRequests2 = bidRequests.map( - (bidReq) => Object.assign(bidReq, { + (bidReq) => Object.assign({}, bidReq, { userIdAsEids: [ { source: 'foobar.com', uids: [ - { id: 'foobar-id' } + { id: 'foobar-id' }, ], }, { @@ -138,19 +211,19 @@ describe('fluctAdapter', function () { { source: 'criteo.com', uids: [ - { id: 'criteo-id' } + { id: 'criteo-id' }, ], }, { source: 'intimatemerger.com', uids: [ - { id: 'imuid' } + { id: 'imuid' }, ], }, { source: 'liveramp.com', uids: [ - { id: 'idl-env' } + { id: 'idl-env' }, ], }, ], @@ -158,36 +231,96 @@ describe('fluctAdapter', function () { ); const request = spec.buildRequests(bidRequests2, bidderRequest)[0]; expect(request.data.user.eids).to.eql([ + { + source: 'foobar.com', + uids: [ + { id: 'foobar-id' }, + ], + }, { source: 'adserver.org', uids: [ - { id: 'tdid' } + { id: 'tdid' }, ], }, { source: 'criteo.com', uids: [ - { id: 'criteo-id' } + { id: 'criteo-id' }, ], }, { source: 'intimatemerger.com', uids: [ - { id: 'imuid' } + { id: 'imuid' }, ], }, { source: 'liveramp.com', uids: [ - { id: 'idl-env' } + { id: 'idl-env' }, ], }, ]); }); + it('includes user.data if any exists', function () { + const bidderRequest2 = Object.assign({}, bidderRequest, { + ortb2: { + user: { + data: [ + { + name: 'a1mediagroup.com', + ext: { + segtax: 900, + }, + segment: [ + { id: 'seg-1' }, + { id: 'seg-2' }, + ], + }, + ], + ext: { + eids: [ + { + source: 'a1mediagroup.com', + uids: [ + { id: 'aud-1' } + ], + }, + ], + }, + }, + }, + }); + const request = spec.buildRequests(bidRequests, bidderRequest2)[0]; + expect(request.data.user).to.eql({ + data: [ + { + name: 'a1mediagroup.com', + ext: { + segtax: 900, + }, + segment: [ + { id: 'seg-1' }, + { id: 'seg-2' }, + ], + }, + ], + eids: [ + { + source: 'a1mediagroup.com', + uids: [ + { id: 'aud-1' } + ], + }, + ], + }); + }); + it('includes data.params.kv if any exists', function () { const bidRequests2 = bidRequests.map( - (bidReq) => Object.assign(bidReq, { + (bidReq) => Object.assign({}, bidReq, { params: { kv: { imsids: ['imsid1', 'imsid2'] @@ -204,7 +337,7 @@ describe('fluctAdapter', function () { it('includes data.schain if any exists', function () { // this should be done by schain.js const bidRequests2 = bidRequests.map( - (bidReq) => Object.assign(bidReq, { + (bidReq) => Object.assign({}, bidReq, { schain: { ver: '1.0', complete: 1, @@ -271,7 +404,7 @@ describe('fluctAdapter', function () { }); }); - describe('interpretResponse', function() { + describe('should interpretResponse', function() { const callBeaconSnippet = '', - 'adid': '144762342', - 'adomain': [ - 'https://dummydomain.com' - ], - 'iurl': 'iurl', - 'cid': '109', - 'crid': 'creativeId', - 'cat': [], - 'w': 300, - 'h': 250, - 'ext': { - 'prebid': { - 'type': 'banner' - }, - 'bidder': { - 'appnexus': { - 'brand_id': 334553, - 'auction_id': 514667951122925701, - 'bidder_id': 2, - 'bid_ad_type': 0 - } - } - } - }, - { - 'id': 'bidId2', - 'impid': 'bidId2', - 'price': 0.1, - 'adm': '', - 'adid': '144762342', - 'adomain': [ - 'https://dummydomain.com' - ], - 'iurl': 'iurl', - 'cid': '109', - 'crid': 'creativeId', - 'cat': [], - 'w': 300, - 'h': 250, - 'ext': { - 'prebid': { - 'type': 'banner' - }, - 'bidder': { - 'appnexus': { - 'brand_id': 386046, - 'auction_id': 517067951122925501, - 'bidder_id': 2, - 'bid_ad_type': 0 - } - } - } - } - ], - 'seat': 'kulturemedia' - } - ], - 'ext': { - 'usersync': { - 'sovrn': { - 'status': 'none', - 'syncs': [ - { - 'url': 'urlsovrn', - 'type': 'iframe' - } - ] - }, - 'appnexus': { - 'status': 'none', - 'syncs': [ - { - 'url': 'urlappnexus', - 'type': 'pixel' - } - ] - } - }, - 'responsetimemillis': { - 'appnexus': 127 - } - } - } -}; - -const DEFAULT_NETWORK_ID = 1; - -describe('kulturemediaBidAdapter:', function () { - let videoBidRequest; - - const VIDEO_REQUEST = { - 'bidderCode': 'kulturemedia', - 'auctionId': 'e158486f-8c7f-472f-94ce-b0cbfbb50ab4', - 'bidderRequestId': '34feaad34lkj2', - 'bids': videoBidRequest, - 'auctionStart': 1520001292880, - 'timeout': 3000, - 'start': 1520001292884, - 'doneCbCallCount': 0, - 'refererInfo': { - 'numIframes': 1, - 'reachedTop': true, - 'referer': 'test.com' - } - }; - - beforeEach(function () { - videoBidRequest = { - mediaTypes: { - video: { - context: 'instream', - playerSize: [[640, 480]], - } - }, - bidder: 'kulturemedia', - sizes: [640, 480], - bidId: '30b3efwfwe1e', - adUnitCode: 'video1', - params: { - video: { - playerWidth: 640, - playerHeight: 480, - mimes: ['video/mp4', 'application/javascript'], - protocols: [2, 5], - api: [2], - position: 1, - delivery: [2], - sid: 134, - rewarded: 1, - placement: 1, - hp: 1, - inventoryid: 123 - }, - site: { - id: 1, - page: 'https://test.com', - referrer: 'http://test.com' - }, - publisherId: 'km123' - } - }; - }); - - describe('isBidRequestValid', function () { - context('basic validation', function () { - beforeEach(function () { - // Basic Valid BidRequest - this.bid = { - bidder: 'kulturemedia', - mediaTypes: { - banner: { - sizes: [[250, 300]] - } - }, - params: { - placementId: 'placementId', - publisherId: 'publisherId', - } - }; - }); - - it('should accept request if placementId and publisherId are passed', function () { - expect(spec.isBidRequestValid(this.bid)).to.be.true; - }); - - it('reject requests without params', function () { - this.bid.params = {}; - expect(spec.isBidRequestValid(this.bid)).to.be.false; - }); - - it('returns false when banner mediaType does not exist', function () { - this.bid.mediaTypes = {} - expect(spec.isBidRequestValid(this.bid)).to.be.false; - }); - }); - - context('banner validation', function () { - it('returns true when banner sizes are defined', function () { - const bid = { - bidder: 'kulturemedia', - mediaTypes: { - banner: { - sizes: [[250, 300]] - } - }, - params: { - placementId: 'placementId', - publisherId: 'publisherId', - } - }; - - expect(spec.isBidRequestValid(bid)).to.be.true; - }); - - it('returns false when banner sizes are invalid', function () { - const invalidSizes = [ - undefined, - '2:1', - 123, - 'test' - ]; - - invalidSizes.forEach((sizes) => { - const bid = { - bidder: 'kulturemedia', - mediaTypes: { - banner: { - sizes - } - }, - params: { - placementId: 'placementId', - publisherId: 'publisherId', - } - }; - - expect(spec.isBidRequestValid(bid)).to.be.false; - }); - }); - }); - - context('video validation', function () { - beforeEach(function () { - // Basic Valid BidRequest - this.bid = { - bidder: 'kulturemedia', - mediaTypes: { - video: { - playerSize: [[300, 50]], - context: 'instream', - mimes: ['foo', 'bar'], - protocols: [1, 2] - } - }, - params: { - placementId: 'placementId', - publisherId: 'publisherId', - } - }; - }); - - it('should return true (skip validations) when e2etest = true', function () { - this.bid.params = { - e2etest: true - }; - expect(spec.isBidRequestValid(this.bid)).to.equal(true); - }); - - it('returns false when video context is not defined', function () { - delete this.bid.mediaTypes.video.context; - - expect(spec.isBidRequestValid(this.bid)).to.be.false; - }); - - it('returns false when video playserSize is invalid', function () { - const invalidSizes = [ - undefined, - '2:1', - 123, - 'test' - ]; - - invalidSizes.forEach((playerSize) => { - this.bid.mediaTypes.video.playerSize = playerSize; - expect(spec.isBidRequestValid(this.bid)).to.be.false; - }); - }); - - it('returns false when video mimes is invalid', function () { - const invalidMimes = [ - undefined, - 'test', - 1, - [] - ] - - invalidMimes.forEach((mimes) => { - this.bid.mediaTypes.video.mimes = mimes; - expect(spec.isBidRequestValid(this.bid)).to.be.false; - }) - }); - - it('returns false when video protocols is invalid', function () { - const invalidMimes = [ - undefined, - 'test', - 1, - [] - ] - - invalidMimes.forEach((protocols) => { - this.bid.mediaTypes.video.protocols = protocols; - expect(spec.isBidRequestValid(this.bid)).to.be.false; - }) - }); - }); - }); - - describe('buildRequests', function () { - context('when mediaType is banner', function () { - it('creates request data', function () { - let request = spec.buildRequests(BANNER_REQUEST.bidRequest, BANNER_REQUEST); - - expect(request).to.exist.and.to.be.a('object'); - const payload = JSON.parse(request.data); - expect(payload.imp[0]).to.have.property('id', BANNER_REQUEST.bidRequest[0].bidId); - expect(payload.imp[1]).to.have.property('id', BANNER_REQUEST.bidRequest[1].bidId); - }); - - it('has gdpr data if applicable', function () { - const req = Object.assign({}, BANNER_REQUEST, { - gdprConsent: { - consentString: 'consentString', - gdprApplies: true, - } - }); - let request = spec.buildRequests(BANNER_REQUEST.bidRequest, req); - - const payload = JSON.parse(request.data); - expect(payload.user.ext).to.have.property('consent', req.gdprConsent.consentString); - expect(payload.regs.ext).to.have.property('gdpr', 1); - }); - - it('should properly forward eids parameters', function () { - const req = Object.assign({}, BANNER_REQUEST); - req.bidRequest[0].userIdAsEids = [ - { - source: 'dummy.com', - uids: [ - { - id: 'd6d0a86c-20c6-4410-a47b-5cba383a698a', - atype: 1 - } - ] - }]; - let request = spec.buildRequests(req.bidRequest, req); - - const payload = JSON.parse(request.data); - expect(payload.user.ext.eids[0].source).to.equal('dummy.com'); - expect(payload.user.ext.eids[0].uids[0].id).to.equal('d6d0a86c-20c6-4410-a47b-5cba383a698a'); - expect(payload.user.ext.eids[0].uids[0].atype).to.equal(1); - }); - }); - - context('when mediaType is video', function () { - it('should create a POST request for every bid', function () { - const requests = spec.buildRequests([videoBidRequest], VIDEO_REQUEST); - expect(requests.method).to.equal('POST'); - expect(requests.url.trim()).to.equal(spec.ENDPOINT + '?pid=' + videoBidRequest.params.publisherId + '&nId=' + DEFAULT_NETWORK_ID); - }); - - it('should attach request data', function () { - const requests = spec.buildRequests([videoBidRequest], VIDEO_REQUEST); - const data = JSON.parse(requests.data); - const [width, height] = videoBidRequest.sizes; - const VERSION = '1.0.0'; - expect(data.imp[0].video.w).to.equal(width); - expect(data.imp[0].video.h).to.equal(height); - expect(data.imp[0].bidfloor).to.equal(videoBidRequest.params.bidfloor); - expect(data.ext.prebidver).to.equal('$prebid.version$'); - expect(data.ext.adapterver).to.equal(spec.VERSION); - }); - - it('should set pubId to e2etest when bid.params.e2etest = true', function () { - videoBidRequest.params.e2etest = true; - const requests = spec.buildRequests([videoBidRequest], VIDEO_REQUEST); - expect(requests.method).to.equal('POST'); - expect(requests.url).to.equal(spec.ENDPOINT + '?pid=e2etest&nId=' + DEFAULT_NETWORK_ID); - }); - - it('should attach End 2 End test data', function () { - videoBidRequest.params.e2etest = true; - const requests = spec.buildRequests([videoBidRequest], VIDEO_REQUEST); - const data = JSON.parse(requests.data); - expect(data.imp[0].bidfloor).to.not.exist; - expect(data.imp[0].video.w).to.equal(640); - expect(data.imp[0].video.h).to.equal(480); - }); - }); - }); - - describe('interpretResponse', function () { - context('when mediaType is banner', function () { - it('have bids', function () { - let bids = spec.interpretResponse(RESPONSE, BANNER_REQUEST); - expect(bids).to.be.an('array').that.is.not.empty; - validateBidOnIndex(0); - validateBidOnIndex(1); - - function validateBidOnIndex(index) { - expect(bids[index]).to.have.property('currency', 'USD'); - expect(bids[index]).to.have.property('requestId', RESPONSE.body.seatbid[0].bid[index].impid); - expect(bids[index]).to.have.property('cpm', RESPONSE.body.seatbid[0].bid[index].price); - expect(bids[index]).to.have.property('width', RESPONSE.body.seatbid[0].bid[index].w); - expect(bids[index]).to.have.property('height', RESPONSE.body.seatbid[0].bid[index].h); - expect(bids[index]).to.have.property('ad', RESPONSE.body.seatbid[0].bid[index].adm); - expect(bids[index]).to.have.property('creativeId', RESPONSE.body.seatbid[0].bid[index].crid); - expect(bids[index].meta).to.have.property('advertiserDomains', RESPONSE.body.seatbid[0].bid[index].adomain); - expect(bids[index]).to.have.property('ttl', 300); - expect(bids[index]).to.have.property('netRevenue', true); - } - }); - - it('handles empty response', function () { - const EMPTY_RESP = Object.assign({}, RESPONSE, {'body': {}}); - const bids = spec.interpretResponse(EMPTY_RESP, BANNER_REQUEST); - - expect(bids).to.be.empty; - }); - }); - - context('when mediaType is video', function () { - it('should return no bids if the response is not valid', function () { - const bidResponse = spec.interpretResponse({ - body: null - }, { - videoBidRequest - }); - expect(bidResponse.length).to.equal(0); - }); - - it('should return no bids if the response "nurl" and "adm" are missing', function () { - const serverResponse = { - seatbid: [{ - bid: [{ - price: 6.01 - }] - }] - }; - const bidResponse = spec.interpretResponse({ - body: serverResponse - }, { - videoBidRequest - }); - expect(bidResponse.length).to.equal(0); - }); - - it('should return no bids if the response "price" is missing', function () { - const serverResponse = { - seatbid: [{ - bid: [{ - adm: '' - }] - }] - }; - const bidResponse = spec.interpretResponse({ - body: serverResponse - }, { - videoBidRequest - }); - expect(bidResponse.length).to.equal(0); - }); - - it('should return a valid video bid response with just "adm"', function () { - const serverResponse = { - id: '123', - seatbid: [{ - bid: [{ - id: 1, - adid: 123, - impid: 456, - crid: 2, - price: 6.01, - adm: '', - adomain: [ - 'kulturemedia.com' - ], - w: 640, - h: 480, - ext: { - prebid: { - type: 'video' - }, - } - }] - }], - cur: 'USD' - }; - const bidResponse = spec.interpretResponse({ - body: serverResponse - }, { - videoBidRequest - }); - let o = { - requestId: serverResponse.seatbid[0].bid[0].impid, - ad: '', - cpm: serverResponse.seatbid[0].bid[0].price, - creativeId: serverResponse.seatbid[0].bid[0].crid, - vastXml: serverResponse.seatbid[0].bid[0].adm, - width: 640, - height: 480, - mediaType: 'video', - currency: 'USD', - ttl: 300, - netRevenue: true, - meta: { - advertiserDomains: ['kulturemedia.com'] - } - }; - expect(bidResponse[0]).to.deep.equal(o); - }); - - it('should default ttl to 300', function () { - const serverResponse = { - seatbid: [{bid: [{id: 1, adid: 123, crid: 2, price: 6.01, adm: ''}]}], - cur: 'USD' - }; - const bidResponse = spec.interpretResponse({body: serverResponse}, {videoBidRequest}); - expect(bidResponse[0].ttl).to.equal(300); - }); - it('should not allow ttl above 3601, default to 300', function () { - videoBidRequest.params.video.ttl = 3601; - const serverResponse = { - seatbid: [{bid: [{id: 1, adid: 123, crid: 2, price: 6.01, adm: ''}]}], - cur: 'USD' - }; - const bidResponse = spec.interpretResponse({body: serverResponse}, {videoBidRequest}); - expect(bidResponse[0].ttl).to.equal(300); - }); - it('should not allow ttl below 1, default to 300', function () { - videoBidRequest.params.video.ttl = 0; - const serverResponse = { - seatbid: [{bid: [{id: 1, adid: 123, crid: 2, price: 6.01, adm: ''}]}], - cur: 'USD' - }; - const bidResponse = spec.interpretResponse({body: serverResponse}, {videoBidRequest}); - expect(bidResponse[0].ttl).to.equal(300); - }); - }); - }); - - describe('getUserSyncs', function () { - it('handles no parameters', function () { - let opts = spec.getUserSyncs({}); - expect(opts).to.be.an('array').that.is.empty; - }); - it('returns non if sync is not allowed', function () { - let opts = spec.getUserSyncs({iframeEnabled: false, pixelEnabled: false}); - - expect(opts).to.be.an('array').that.is.empty; - }); - - it('iframe sync enabled should return results', function () { - let opts = spec.getUserSyncs({iframeEnabled: true, pixelEnabled: false}, [RESPONSE]); - - expect(opts.length).to.equal(1); - expect(opts[0].type).to.equal('iframe'); - expect(opts[0].url).to.equal(RESPONSE.body.ext.usersync['sovrn'].syncs[0].url); - }); - - it('pixel sync enabled should return results', function () { - let opts = spec.getUserSyncs({iframeEnabled: false, pixelEnabled: true}, [RESPONSE]); - - expect(opts.length).to.equal(1); - expect(opts[0].type).to.equal('image'); - expect(opts[0].url).to.equal(RESPONSE.body.ext.usersync['appnexus'].syncs[0].url); - }); - - it('all sync enabled should return all results', function () { - let opts = spec.getUserSyncs({iframeEnabled: true, pixelEnabled: true}, [RESPONSE]); - - expect(opts.length).to.equal(2); - }); - }); -}) -; diff --git a/test/spec/modules/lassoBidAdapter_spec.js b/test/spec/modules/lassoBidAdapter_spec.js index 5825927a931..3695889aca0 100644 --- a/test/spec/modules/lassoBidAdapter_spec.js +++ b/test/spec/modules/lassoBidAdapter_spec.js @@ -79,10 +79,12 @@ describe('lassoBidAdapter', function () { }); describe('buildRequests', function () { - const validBidRequests = spec.buildRequests([bid], bidderRequest); - expect(validBidRequests).to.be.an('array').that.is.not.empty; - - const bidRequest = validBidRequests[0]; + let validBidRequests, bidRequest; + before(() => { + validBidRequests = spec.buildRequests([bid], bidderRequest); + expect(validBidRequests).to.be.an('array').that.is.not.empty; + bidRequest = validBidRequests[0]; + }) it('Returns valid bidRequest', function () { expect(bidRequest).to.exist; diff --git a/test/spec/modules/liveIntentIdMinimalSystem_spec.js b/test/spec/modules/liveIntentIdMinimalSystem_spec.js index 5640a6d36cb..ad21d7b6763 100644 --- a/test/spec/modules/liveIntentIdMinimalSystem_spec.js +++ b/test/spec/modules/liveIntentIdMinimalSystem_spec.js @@ -73,7 +73,7 @@ describe('LiveIntentMinimalId', function() { expect(callBackSpy.calledOnce).to.be.true; }); - it('should call the Identity Exchange endpoint with the privided distributorId', function() { + it('should call the Identity Exchange endpoint with the provided distributorId', function() { getCookieStub.returns(null); let callBackSpy = sinon.spy(); let submoduleCallback = liveIntentIdSubmodule.getId({ params: { fireEventDelay: 1, distributorId: 'did-1111' } }).callback; @@ -87,7 +87,7 @@ describe('LiveIntentMinimalId', function() { expect(callBackSpy.calledOnceWith({})).to.be.true; }); - it('should call the Identity Exchange endpoint without the privided distributorId when appId is provided', function() { + it('should call the Identity Exchange endpoint without the provided distributorId when appId is provided', function() { getCookieStub.returns(null); let callBackSpy = sinon.spy(); let submoduleCallback = liveIntentIdSubmodule.getId({ params: { fireEventDelay: 1, distributorId: 'did-1111', liCollectConfig: { appId: 'a-0001' } } }).callback; @@ -243,12 +243,47 @@ describe('LiveIntentMinimalId', function() { it('should decode a uid2 to a seperate object when present', function() { const result = liveIntentIdSubmodule.decode({ nonId: 'foo', uid2: 'bar' }); - expect(result).to.eql({'lipb': {'lipbid': 'foo', 'nonId': 'foo', 'uid2': 'bar'}, 'uid2': {'id': 'bar'}}); + expect(result).to.eql({'lipb': {'lipbid': 'foo', 'nonId': 'foo', 'uid2': 'bar'}, 'uid2': {'id': 'bar', 'ext': {'provider': 'liveintent.com'}}}); }); it('should decode values with uid2 but no nonId', function() { const result = liveIntentIdSubmodule.decode({ uid2: 'bar' }); - expect(result).to.eql({'uid2': {'id': 'bar'}}); + expect(result).to.eql({'uid2': {'id': 'bar', 'ext': {'provider': 'liveintent.com'}}}); + }); + + it('should decode a bidswitch id to a seperate object when present', function() { + const result = liveIntentIdSubmodule.decode({ nonId: 'foo', bidswitch: 'bar' }); + expect(result).to.eql({'lipb': {'lipbid': 'foo', 'nonId': 'foo', 'bidswitch': 'bar'}, 'bidswitch': {'id': 'bar', 'ext': {'provider': 'liveintent.com'}}}); + }); + + it('should decode a medianet id to a seperate object when present', function() { + const result = liveIntentIdSubmodule.decode({ nonId: 'foo', medianet: 'bar' }); + expect(result).to.eql({'lipb': {'lipbid': 'foo', 'nonId': 'foo', 'medianet': 'bar'}, 'medianet': {'id': 'bar', 'ext': {'provider': 'liveintent.com'}}}); + }); + + it('should decode a sovrn id to a seperate object when present', function() { + const result = liveIntentIdSubmodule.decode({ nonId: 'foo', sovrn: 'bar' }); + expect(result).to.eql({'lipb': {'lipbid': 'foo', 'nonId': 'foo', 'sovrn': 'bar'}, 'sovrn': {'id': 'bar', 'ext': {'provider': 'liveintent.com'}}}); + }); + + it('should decode a magnite id to a seperate object when present', function() { + const result = liveIntentIdSubmodule.decode({ nonId: 'foo', magnite: 'bar' }); + expect(result).to.eql({'lipb': {'lipbid': 'foo', 'nonId': 'foo', 'magnite': 'bar'}, 'magnite': {'id': 'bar', 'ext': {'provider': 'liveintent.com'}}}); + }); + + it('should decode an index id to a seperate object when present', function() { + const result = liveIntentIdSubmodule.decode({ nonId: 'foo', index: 'bar' }); + expect(result).to.eql({'lipb': {'lipbid': 'foo', 'nonId': 'foo', 'index': 'bar'}, 'index': {'id': 'bar', 'ext': {'provider': 'liveintent.com'}}}); + }); + + it('should decode an openx id to a seperate object when present', function () { + const result = liveIntentIdSubmodule.decode({ nonId: 'foo', openx: 'bar' }); + expect(result).to.eql({'lipb': {'lipbid': 'foo', 'nonId': 'foo', 'openx': 'bar'}, 'openx': {'id': 'bar', 'ext': {'provider': 'liveintent.com'}}}); + }); + + it('should decode an pubmatic id to a seperate object when present', function() { + const result = liveIntentIdSubmodule.decode({ nonId: 'foo', pubmatic: 'bar' }); + expect(result).to.eql({'lipb': {'lipbid': 'foo', 'nonId': 'foo', 'pubmatic': 'bar'}, 'pubmatic': {'id': 'bar', 'ext': {'provider': 'liveintent.com'}}}); }); it('should allow disabling nonId resolution', function() { diff --git a/test/spec/modules/liveIntentIdSystem_spec.js b/test/spec/modules/liveIntentIdSystem_spec.js index 859db423eac..3af598c5d4e 100644 --- a/test/spec/modules/liveIntentIdSystem_spec.js +++ b/test/spec/modules/liveIntentIdSystem_spec.js @@ -1,6 +1,6 @@ import { liveIntentIdSubmodule, reset as resetLiveIntentIdSubmodule, storage } from 'modules/liveIntentIdSystem.js'; import * as utils from 'src/utils.js'; -import { gdprDataHandler, uspDataHandler } from '../../../src/adapterManager.js'; +import { gdprDataHandler, uspDataHandler, gppDataHandler } from '../../../src/adapterManager.js'; import { server } from 'test/mocks/xhr.js'; resetLiveIntentIdSubmodule(); liveIntentIdSubmodule.setModuleMode('standard') @@ -12,6 +12,7 @@ describe('LiveIntentId', function() { let logErrorStub; let uspConsentDataStub; let gdprConsentDataStub; + let gppConsentDataStub; let getCookieStub; let getDataFromLocalStorageStub; let imgStub; @@ -24,6 +25,7 @@ describe('LiveIntentId', function() { logErrorStub = sinon.stub(utils, 'logError'); uspConsentDataStub = sinon.stub(uspDataHandler, 'getConsentData'); gdprConsentDataStub = sinon.stub(gdprDataHandler, 'getConsentData'); + gppConsentDataStub = sinon.stub(gppDataHandler, 'getConsentData'); }); afterEach(function() { @@ -33,6 +35,7 @@ describe('LiveIntentId', function() { logErrorStub.restore(); uspConsentDataStub.restore(); gdprConsentDataStub.restore(); + gppConsentDataStub.restore(); resetLiveIntentIdSubmodule(); }); @@ -42,11 +45,15 @@ describe('LiveIntentId', function() { gdprApplies: true, consentString: 'consentDataString' }) + gppConsentDataStub.returns({ + gppString: 'gppConsentDataString', + applicableSections: [1, 2] + }) let callBackSpy = sinon.spy(); let submoduleCallback = liveIntentIdSubmodule.getId(defaultConfigParams).callback; submoduleCallback(callBackSpy); let request = server.requests[0]; - expect(request.url).to.match(/.*us_privacy=1YNY.*&gdpr=1&n3pc=1&gdpr_consent=consentDataString.*/); + expect(request.url).to.match(/.*us_privacy=1YNY.*&gdpr=1&n3pc=1&gdpr_consent=consentDataString.*&gpp_s=gppConsentDataString&gpp_as=1%2C2.*/); const response = { unifiedId: 'a_unified_id', segments: [123, 234] @@ -65,9 +72,13 @@ describe('LiveIntentId', function() { gdprApplies: true, consentString: 'consentDataString' }) + gppConsentDataStub.returns({ + gppString: 'gppConsentDataString', + applicableSections: [1] + }) liveIntentIdSubmodule.getId(defaultConfigParams); setTimeout(() => { - expect(server.requests[0].url).to.match(/https:\/\/rp.liadm.com\/j\?.*&us_privacy=1YNY.*&wpn=prebid.*&gdpr=1&n3pc=1&n3pct=1&nb=1&gdpr_consent=consentDataString.*/); + expect(server.requests[0].url).to.match(/https:\/\/rp.liadm.com\/j\?.*&us_privacy=1YNY.*&wpn=prebid.*&gdpr=1&n3pc=1&n3pct=1&nb=1&gdpr_consent=consentDataString&gpp_s=gppConsentDataString&gpp_as=1.*/); done(); }, 200); }); @@ -83,6 +94,16 @@ describe('LiveIntentId', function() { }, 200); }); + it('should initialize LiveConnect and forward the prebid version when decode and emit an event', function(done) { + liveIntentIdSubmodule.decode({}, { params: { + ...defaultConfigParams + }}); + setTimeout(() => { + expect(server.requests[0].url).to.contain('tv=$prebid.version$') + done(); + }, 200); + }); + it('should initialize LiveConnect with the config params when decode and emit an event', function (done) { liveIntentIdSubmodule.decode({}, { params: { ...defaultConfigParams.params, @@ -123,9 +144,13 @@ describe('LiveIntentId', function() { gdprApplies: false, consentString: 'consentDataString' }) + gppConsentDataStub.returns({ + gppString: 'gppConsentDataString', + applicableSections: [1] + }) liveIntentIdSubmodule.decode({}, defaultConfigParams); setTimeout(() => { - expect(server.requests[0].url).to.match(/.*us_privacy=1YNY.*&gdpr=0&gdpr_consent=consentDataString.*/); + expect(server.requests[0].url).to.match(/.*us_privacy=1YNY.*&gdpr=0&gdpr_consent=consentDataString.*&gpp_s=gppConsentDataString&gpp_as=1.*/); done(); }, 200); }); @@ -171,7 +196,7 @@ describe('LiveIntentId', function() { let submoduleCallback = liveIntentIdSubmodule.getId({ params: {...defaultConfigParams.params, ...{'url': 'https://dummy.liveintent.com/idex'}} }).callback; submoduleCallback(callBackSpy); let request = server.requests[0]; - expect(request.url).to.be.eq('https://dummy.liveintent.com/idex/prebid/89899?resolve=nonId'); + expect(request.url).to.be.eq('https://dummy.liveintent.com/idex/prebid/89899?cd=.localhost&resolve=nonId'); request.respond( 204, responseHeader @@ -179,13 +204,13 @@ describe('LiveIntentId', function() { expect(callBackSpy.calledOnceWith({})).to.be.true; }); - it('should call the Identity Exchange endpoint with the privided distributorId', function() { + it('should call the Identity Exchange endpoint with the provided distributorId', function() { getCookieStub.returns(null); let callBackSpy = sinon.spy(); let submoduleCallback = liveIntentIdSubmodule.getId({ params: { fireEventDelay: 1, distributorId: 'did-1111' } }).callback; submoduleCallback(callBackSpy); let request = server.requests[0]; - expect(request.url).to.be.eq('https://idx.liadm.com/idex/did-1111/any?did=did-1111&resolve=nonId'); + expect(request.url).to.be.eq('https://idx.liadm.com/idex/did-1111/any?did=did-1111&cd=.localhost&resolve=nonId'); request.respond( 204, responseHeader @@ -193,13 +218,13 @@ describe('LiveIntentId', function() { expect(callBackSpy.calledOnceWith({})).to.be.true; }); - it('should call the Identity Exchange endpoint without the privided distributorId when appId is provided', function() { + it('should call the Identity Exchange endpoint without the provided distributorId when appId is provided', function() { getCookieStub.returns(null); let callBackSpy = sinon.spy(); let submoduleCallback = liveIntentIdSubmodule.getId({ params: { fireEventDelay: 1, distributorId: 'did-1111', liCollectConfig: { appId: 'a-0001' } } }).callback; submoduleCallback(callBackSpy); let request = server.requests[0]; - expect(request.url).to.be.eq('https://idx.liadm.com/idex/prebid/any?resolve=nonId'); + expect(request.url).to.be.eq('https://idx.liadm.com/idex/prebid/any?cd=.localhost&resolve=nonId'); request.respond( 204, responseHeader @@ -219,7 +244,7 @@ describe('LiveIntentId', function() { } }).callback; submoduleCallback(callBackSpy); let request = server.requests[0]; - expect(request.url).to.be.eq('https://dummy.liveintent.com/idex/rubicon/89899?resolve=nonId'); + expect(request.url).to.be.eq('https://dummy.liveintent.com/idex/rubicon/89899?cd=.localhost&resolve=nonId'); request.respond( 200, responseHeader, @@ -234,7 +259,7 @@ describe('LiveIntentId', function() { let submoduleCallback = liveIntentIdSubmodule.getId(defaultConfigParams).callback; submoduleCallback(callBackSpy); let request = server.requests[0]; - expect(request.url).to.be.eq('https://idx.liadm.com/idex/prebid/89899?resolve=nonId'); + expect(request.url).to.be.eq('https://idx.liadm.com/idex/prebid/89899?cd=.localhost&resolve=nonId'); request.respond( 200, responseHeader, @@ -249,7 +274,7 @@ describe('LiveIntentId', function() { let submoduleCallback = liveIntentIdSubmodule.getId(defaultConfigParams).callback; submoduleCallback(callBackSpy); let request = server.requests[0]; - expect(request.url).to.be.eq('https://idx.liadm.com/idex/prebid/89899?resolve=nonId'); + expect(request.url).to.be.eq('https://idx.liadm.com/idex/prebid/89899?cd=.localhost&resolve=nonId'); request.respond( 503, responseHeader, @@ -266,7 +291,7 @@ describe('LiveIntentId', function() { let submoduleCallback = liveIntentIdSubmodule.getId(defaultConfigParams).callback; submoduleCallback(callBackSpy); let request = server.requests[0]; - expect(request.url).to.be.eq(`https://idx.liadm.com/idex/prebid/89899?duid=${oldCookie}&resolve=nonId`); + expect(request.url).to.be.eq(`https://idx.liadm.com/idex/prebid/89899?duid=${oldCookie}&cd=.localhost&resolve=nonId`); request.respond( 200, responseHeader, @@ -289,7 +314,7 @@ describe('LiveIntentId', function() { let submoduleCallback = liveIntentIdSubmodule.getId(configParams).callback; submoduleCallback(callBackSpy); 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`); + expect(request.url).to.be.eq(`https://idx.liadm.com/idex/prebid/89899?duid=${oldCookie}&cd=.localhost&_thirdPC=third-pc&resolve=nonId`); request.respond( 200, responseHeader, @@ -311,7 +336,7 @@ describe('LiveIntentId', function() { let submoduleCallback = liveIntentIdSubmodule.getId(configParams).callback; submoduleCallback(callBackSpy); 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'); + expect(request.url).to.be.eq('https://idx.liadm.com/idex/prebid/89899?cd=.localhost&_thirdPC=%7B%22key%22%3A%22value%22%7D&resolve=nonId'); request.respond( 200, responseHeader, @@ -344,7 +369,7 @@ describe('LiveIntentId', function() { } }).callback; submoduleCallback(callBackSpy); let request = server.requests[0]; - expect(request.url).to.be.eq(`https://idx.liadm.com/idex/prebid/89899?resolve=nonId&resolve=foo`); + expect(request.url).to.be.eq(`https://idx.liadm.com/idex/prebid/89899?cd=.localhost&resolve=nonId&resolve=foo`); request.respond( 200, responseHeader, @@ -353,24 +378,49 @@ describe('LiveIntentId', function() { expect(callBackSpy.calledOnce).to.be.true; }); - it('should decode a uid2 to a seperate object when present', function() { + it('should decode a uid2 to a separate object when present', function() { const result = liveIntentIdSubmodule.decode({ nonId: 'foo', uid2: 'bar' }); - expect(result).to.eql({'lipb': {'lipbid': 'foo', 'nonId': 'foo', 'uid2': 'bar'}, 'uid2': {'id': 'bar'}}); + expect(result).to.eql({'lipb': {'lipbid': 'foo', 'nonId': 'foo', 'uid2': 'bar'}, 'uid2': {'id': 'bar', 'ext': {'provider': 'liveintent.com'}}}); + }); + + it('should decode values with uid2 but no nonId', function() { + const result = liveIntentIdSubmodule.decode({ uid2: 'bar' }); + expect(result).to.eql({'uid2': {'id': 'bar', 'ext': {'provider': 'liveintent.com'}}}); }); - it('should decode a bidswitch id to a seperate object when present', function() { + it('should decode a bidswitch id to a separate object when present', function() { const result = liveIntentIdSubmodule.decode({ nonId: 'foo', bidswitch: 'bar' }); - expect(result).to.eql({'lipb': {'lipbid': 'foo', 'nonId': 'foo', 'bidswitch': 'bar'}, 'bidswitch': {'id': 'bar'}}); + expect(result).to.eql({'lipb': {'lipbid': 'foo', 'nonId': 'foo', 'bidswitch': 'bar'}, 'bidswitch': {'id': 'bar', 'ext': {'provider': 'liveintent.com'}}}); }); - it('should decode a medianet id to a seperate object when present', function() { + it('should decode a medianet id to a separate object when present', function() { const result = liveIntentIdSubmodule.decode({ nonId: 'foo', medianet: 'bar' }); - expect(result).to.eql({'lipb': {'lipbid': 'foo', 'nonId': 'foo', 'medianet': 'bar'}, 'medianet': {'id': 'bar'}}); + expect(result).to.eql({'lipb': {'lipbid': 'foo', 'nonId': 'foo', 'medianet': 'bar'}, 'medianet': {'id': 'bar', 'ext': {'provider': 'liveintent.com'}}}); }); - it('should decode values with uid2 but no nonId', function() { - const result = liveIntentIdSubmodule.decode({ uid2: 'bar' }); - expect(result).to.eql({'uid2': {'id': 'bar'}}); + it('should decode a sovrn id to a separate object when present', function() { + const result = liveIntentIdSubmodule.decode({ nonId: 'foo', sovrn: 'bar' }); + expect(result).to.eql({'lipb': {'lipbid': 'foo', 'nonId': 'foo', 'sovrn': 'bar'}, 'sovrn': {'id': 'bar', 'ext': {'provider': 'liveintent.com'}}}); + }); + + it('should decode a magnite id to a separate object when present', function() { + const result = liveIntentIdSubmodule.decode({ nonId: 'foo', magnite: 'bar' }); + expect(result).to.eql({'lipb': {'lipbid': 'foo', 'nonId': 'foo', 'magnite': 'bar'}, 'magnite': {'id': 'bar', 'ext': {'provider': 'liveintent.com'}}}); + }); + + it('should decode an index id to a separate object when present', function() { + const result = liveIntentIdSubmodule.decode({ nonId: 'foo', index: 'bar' }); + expect(result).to.eql({'lipb': {'lipbid': 'foo', 'nonId': 'foo', 'index': 'bar'}, 'index': {'id': 'bar', 'ext': {'provider': 'liveintent.com'}}}); + }); + + it('should decode an openx id to a separate object when present', function () { + const result = liveIntentIdSubmodule.decode({ nonId: 'foo', openx: 'bar' }); + expect(result).to.eql({'lipb': {'lipbid': 'foo', 'nonId': 'foo', 'openx': 'bar'}, 'openx': {'id': 'bar', 'ext': {'provider': 'liveintent.com'}}}); + }); + + it('should decode an pubmatic id to a separate object when present', function() { + const result = liveIntentIdSubmodule.decode({ nonId: 'foo', pubmatic: 'bar' }); + expect(result).to.eql({'lipb': {'lipbid': 'foo', 'nonId': 'foo', 'pubmatic': 'bar'}, 'pubmatic': {'id': 'bar', 'ext': {'provider': 'liveintent.com'}}}); }); it('should allow disabling nonId resolution', function() { @@ -381,7 +431,7 @@ describe('LiveIntentId', function() { } }).callback; submoduleCallback(callBackSpy); let request = server.requests[0]; - expect(request.url).to.be.eq(`https://idx.liadm.com/idex/prebid/89899?resolve=uid2`); + expect(request.url).to.be.eq(`https://idx.liadm.com/idex/prebid/89899?cd=.localhost&resolve=uid2`); request.respond( 200, responseHeader, diff --git a/test/spec/modules/lm_kiviadsBidAdapter_spec.js b/test/spec/modules/lm_kiviadsBidAdapter_spec.js new file mode 100644 index 00000000000..68ac73289cd --- /dev/null +++ b/test/spec/modules/lm_kiviadsBidAdapter_spec.js @@ -0,0 +1,455 @@ +import {expect} from 'chai'; +import {config} from 'src/config.js'; +import {spec, getBidFloor} from 'modules/lm_kiviadsBidAdapter.js'; +import {deepClone} from 'src/utils'; + +const ENDPOINT = 'https://pbjs.kiviads.live'; + +const defaultRequest = { + adUnitCode: 'test', + bidId: '1', + requestId: 'qwerty', + ortb2: { + source: { + tid: 'auctionId' + } + }, + ortb2Imp: { + ext: { + tid: 'tr1', + } + }, + mediaTypes: { + banner: { + sizes: [ + [300, 250], + [300, 200] + ] + } + }, + bidder: 'lm_kiviads', + params: { + env: 'lm_kiviads', + pid: '40', + ext: {} + }, + bidRequestsCount: 1 +}; + +const defaultRequestVideo = deepClone(defaultRequest); +defaultRequestVideo.mediaTypes = { + video: { + playerSize: [640, 480], + context: 'instream', + skipppable: true + } +}; +describe('lm_kiviadsBidAdapter', () => { + describe('isBidRequestValid', function () { + it('should return false when request params is missing', function () { + const invalidRequest = deepClone(defaultRequest); + delete invalidRequest.params; + expect(spec.isBidRequestValid(invalidRequest)).to.equal(false); + }); + + it('should return false when required env param is missing', function () { + const invalidRequest = deepClone(defaultRequest); + delete invalidRequest.params.env; + expect(spec.isBidRequestValid(invalidRequest)).to.equal(false); + }); + + it('should return false when required pid param is missing', function () { + const invalidRequest = deepClone(defaultRequest); + delete invalidRequest.params.pid; + expect(spec.isBidRequestValid(invalidRequest)).to.equal(false); + }); + + it('should return false when video.playerSize is missing', function () { + const invalidRequest = deepClone(defaultRequestVideo); + delete invalidRequest.mediaTypes.video.playerSize; + expect(spec.isBidRequestValid(invalidRequest)).to.equal(false); + }); + + it('should return true when required params found', function () { + expect(spec.isBidRequestValid(defaultRequest)).to.equal(true); + }); + }); + + describe('buildRequests', function () { + beforeEach(function () { + config.resetConfig(); + }); + + it('should send request with correct structure', function () { + const request = spec.buildRequests([defaultRequest], {}); + expect(request.method).to.equal('POST'); + expect(request.url).to.equal(ENDPOINT + '/bid'); + expect(request.options).to.have.property('contentType').and.to.equal('application/json'); + expect(request).to.have.property('data'); + }); + + it('should build basic request structure', function () { + const request = JSON.parse(spec.buildRequests([defaultRequest], {}).data)[0]; + expect(request).to.have.property('bidId').and.to.equal(defaultRequest.bidId); + expect(request).to.have.property('auctionId').and.to.equal(defaultRequest.ortb2.source.tid); + expect(request).to.have.property('transactionId').and.to.equal(defaultRequest.ortb2Imp.ext.tid); + expect(request).to.have.property('tz').and.to.equal(new Date().getTimezoneOffset()); + expect(request).to.have.property('bc').and.to.equal(1); + expect(request).to.have.property('floor').and.to.equal(null); + expect(request).to.have.property('banner').and.to.deep.equal({sizes: [[300, 250], [300, 200]]}); + expect(request).to.have.property('gdprApplies').and.to.equal(0); + expect(request).to.have.property('consentString').and.to.equal(''); + expect(request).to.have.property('userEids').and.to.deep.equal([]); + expect(request).to.have.property('usPrivacy').and.to.equal(''); + expect(request).to.have.property('coppa').and.to.equal(0); + expect(request).to.have.property('sizes').and.to.deep.equal(['300x250', '300x200']); + expect(request).to.have.property('ext').and.to.deep.equal({}); + expect(request).to.have.property('env').and.to.deep.equal({ + env: 'lm_kiviads', + pid: '40' + }); + expect(request).to.have.property('device').and.to.deep.equal({ + ua: navigator.userAgent, + lang: navigator.language + }); + }); + + it('should build request with schain', function () { + const schainRequest = deepClone(defaultRequest); + schainRequest.schain = { + validation: 'strict', + config: { + ver: '1.0' + } + }; + const request = JSON.parse(spec.buildRequests([schainRequest], {}).data)[0]; + expect(request).to.have.property('schain').and.to.deep.equal({ + validation: 'strict', + config: { + ver: '1.0' + } + }); + }); + + it('should build request with location', function () { + const bidderRequest = { + refererInfo: { + page: 'page', + location: 'location', + domain: 'domain', + ref: 'ref', + isAmp: false + } + }; + const request = JSON.parse(spec.buildRequests([defaultRequest], bidderRequest).data)[0]; + expect(request).to.have.property('location'); + const location = request.location; + expect(location).to.have.property('page').and.to.equal('page'); + expect(location).to.have.property('location').and.to.equal('location'); + expect(location).to.have.property('domain').and.to.equal('domain'); + expect(location).to.have.property('ref').and.to.equal('ref'); + expect(location).to.have.property('isAmp').and.to.equal(false); + }); + + it('should build request with ortb2 info', function () { + const ortb2Request = deepClone(defaultRequest); + ortb2Request.ortb2 = { + site: { + name: 'name' + } + }; + const request = JSON.parse(spec.buildRequests([ortb2Request], {}).data)[0]; + expect(request).to.have.property('ortb2').and.to.deep.equal({ + site: { + name: 'name' + } + }); + }); + + it('should build request with ortb2Imp info', function () { + const ortb2ImpRequest = deepClone(defaultRequest); + ortb2ImpRequest.ortb2Imp = { + ext: { + data: { + pbadslot: 'home1', + adUnitSpecificAttribute: '1' + } + } + }; + const request = JSON.parse(spec.buildRequests([ortb2ImpRequest], {}).data)[0]; + expect(request).to.have.property('ortb2Imp').and.to.deep.equal({ + ext: { + data: { + pbadslot: 'home1', + adUnitSpecificAttribute: '1' + } + } + }); + }); + + it('should build request with valid bidfloor', function () { + const bfRequest = deepClone(defaultRequest); + bfRequest.getFloor = () => ({floor: 5, currency: 'USD'}); + const request = JSON.parse(spec.buildRequests([bfRequest], {}).data)[0]; + expect(request).to.have.property('floor').and.to.equal(5); + }); + + it('should build request with gdpr consent data if applies', function () { + const bidderRequest = { + gdprConsent: { + gdprApplies: true, + consentString: 'qwerty' + } + }; + const request = JSON.parse(spec.buildRequests([defaultRequest], bidderRequest).data)[0]; + expect(request).to.have.property('gdprApplies').and.equals(1); + expect(request).to.have.property('consentString').and.equals('qwerty'); + }); + + it('should build request with usp consent data if applies', function () { + const bidderRequest = { + uspConsent: '1YA-' + }; + const request = JSON.parse(spec.buildRequests([defaultRequest], bidderRequest).data)[0]; + expect(request).to.have.property('usPrivacy').and.equals('1YA-'); + }); + + it('should build request with coppa 1', function () { + config.setConfig({ + coppa: true + }); + const request = JSON.parse(spec.buildRequests([defaultRequest], {}).data)[0]; + expect(request).to.have.property('coppa').and.equals(1); + }); + + it('should build request with extended ids', function () { + const idRequest = deepClone(defaultRequest); + idRequest.userIdAsEids = [ + {source: 'adserver.org', uids: [{id: 'TTD_ID_FROM_USER_ID_MODULE', atype: 1, ext: {rtiPartner: 'TDID'}}]}, + {source: 'pubcid.org', uids: [{id: 'pubCommonId_FROM_USER_ID_MODULE', atype: 1}]} + ]; + const request = JSON.parse(spec.buildRequests([idRequest], {}).data)[0]; + expect(request).to.have.property('userEids').and.deep.equal(idRequest.userIdAsEids); + }); + + it('should build request with video', function () { + const request = JSON.parse(spec.buildRequests([defaultRequestVideo], {}).data)[0]; + expect(request).to.have.property('video').and.to.deep.equal({ + playerSize: [640, 480], + context: 'instream', + skipppable: true + }); + expect(request).to.have.property('sizes').and.to.deep.equal(['640x480']); + }); + }); + + describe('interpretResponse', function () { + it('should return empty bids', function () { + const serverResponse = { + body: { + data: null + } + }; + + const invalidResponse = spec.interpretResponse(serverResponse, {}); + expect(invalidResponse).to.be.an('array').that.is.empty; + }); + + it('should interpret valid response', function () { + const serverResponse = { + body: { + data: [{ + requestId: 'qwerty', + cpm: 1, + currency: 'USD', + width: 300, + height: 250, + ttl: 600, + meta: { + advertiserDomains: ['lm_kiviads'] + }, + ext: { + pixels: [ + ['iframe', 'surl1'], + ['image', 'surl2'], + ] + } + }] + } + }; + + const validResponse = spec.interpretResponse(serverResponse, {bidderRequest: defaultRequest}); + const bid = validResponse[0]; + expect(validResponse).to.be.an('array').that.is.not.empty; + expect(bid.requestId).to.equal('qwerty'); + expect(bid.cpm).to.equal(1); + expect(bid.currency).to.equal('USD'); + expect(bid.width).to.equal(300); + expect(bid.height).to.equal(250); + expect(bid.ttl).to.equal(600); + expect(bid.meta).to.deep.equal({advertiserDomains: ['lm_kiviads']}); + }); + + it('should interpret valid banner response', function () { + const serverResponse = { + body: { + data: [{ + requestId: 'qwerty', + cpm: 1, + currency: 'USD', + width: 300, + height: 250, + ttl: 600, + mediaType: 'banner', + creativeId: 'xe-demo-banner', + ad: 'ad', + meta: {} + }] + } + }; + + const validResponseBanner = spec.interpretResponse(serverResponse, {bidderRequest: defaultRequest}); + const bid = validResponseBanner[0]; + expect(validResponseBanner).to.be.an('array').that.is.not.empty; + expect(bid.mediaType).to.equal('banner'); + expect(bid.creativeId).to.equal('xe-demo-banner'); + expect(bid.ad).to.equal('ad'); + }); + + it('should interpret valid video response', function () { + const serverResponse = { + body: { + data: [{ + requestId: 'qwerty', + cpm: 1, + currency: 'USD', + width: 600, + height: 480, + ttl: 600, + mediaType: 'video', + creativeId: 'xe-demo-video', + ad: 'vast-xml', + meta: {} + }] + } + }; + + const validResponseBanner = spec.interpretResponse(serverResponse, {bidderRequest: defaultRequestVideo}); + const bid = validResponseBanner[0]; + expect(validResponseBanner).to.be.an('array').that.is.not.empty; + expect(bid.mediaType).to.equal('video'); + expect(bid.creativeId).to.equal('xe-demo-video'); + expect(bid.ad).to.equal('vast-xml'); + }); + }); + + describe('getUserSyncs', function () { + it('shoukd handle no params', function () { + const opts = spec.getUserSyncs({}, []); + expect(opts).to.be.an('array').that.is.empty; + }); + + it('should return empty if sync is not allowed', function () { + const opts = spec.getUserSyncs({iframeEnabled: false, pixelEnabled: false}); + expect(opts).to.be.an('array').that.is.empty; + }); + + it('should allow iframe sync', function () { + const opts = spec.getUserSyncs({iframeEnabled: true, pixelEnabled: false}, [{ + body: { + data: [{ + requestId: 'qwerty', + ext: { + pixels: [ + ['iframe', 'surl1?a=b'], + ['image', 'surl2?a=b'], + ] + } + }] + } + }]); + expect(opts.length).to.equal(1); + expect(opts[0].type).to.equal('iframe'); + expect(opts[0].url).to.equal('surl1?a=b&us_privacy=&gdpr=0&gdpr_consent='); + }); + + it('should allow pixel sync', function () { + const opts = spec.getUserSyncs({iframeEnabled: false, pixelEnabled: true}, [{ + body: { + data: [{ + requestId: 'qwerty', + ext: { + pixels: [ + ['iframe', 'surl1?a=b'], + ['image', 'surl2?a=b'], + ] + } + }] + } + }]); + expect(opts.length).to.equal(1); + expect(opts[0].type).to.equal('image'); + expect(opts[0].url).to.equal('surl2?a=b&us_privacy=&gdpr=0&gdpr_consent='); + }); + + it('should allow pixel sync and parse consent params', function () { + const opts = spec.getUserSyncs({iframeEnabled: false, pixelEnabled: true}, [{ + body: { + data: [{ + requestId: 'qwerty', + ext: { + pixels: [ + ['iframe', 'surl1?a=b'], + ['image', 'surl2?a=b'], + ] + } + }] + } + }], { + gdprApplies: 1, + consentString: '1YA-' + }); + expect(opts.length).to.equal(1); + expect(opts[0].type).to.equal('image'); + expect(opts[0].url).to.equal('surl2?a=b&us_privacy=&gdpr=1&gdpr_consent=1YA-'); + }); + }); + + describe('getBidFloor', function () { + it('should return null when getFloor is not a function', () => { + const bid = {getFloor: 2}; + const result = getBidFloor(bid); + expect(result).to.be.null; + }); + + it('should return null when getFloor doesnt return an object', () => { + const bid = {getFloor: () => 2}; + const result = getBidFloor(bid); + expect(result).to.be.null; + }); + + it('should return null when floor is not a number', () => { + const bid = { + getFloor: () => ({floor: 'string', currency: 'USD'}) + }; + const result = getBidFloor(bid); + expect(result).to.be.null; + }); + + it('should return null when currency is not USD', () => { + const bid = { + getFloor: () => ({floor: 5, currency: 'EUR'}) + }; + const result = getBidFloor(bid); + expect(result).to.be.null; + }); + + it('should return floor value when everything is correct', () => { + const bid = { + getFloor: () => ({floor: 5, currency: 'USD'}) + }; + const result = getBidFloor(bid); + expect(result).to.equal(5); + }); + }); +}) diff --git a/test/spec/modules/magniteAnalyticsAdapter_spec.js b/test/spec/modules/magniteAnalyticsAdapter_spec.js index ae63f19f46b..0864a976d7d 100644 --- a/test/spec/modules/magniteAnalyticsAdapter_spec.js +++ b/test/spec/modules/magniteAnalyticsAdapter_spec.js @@ -550,7 +550,7 @@ describe('magnite analytics adapter', function () { expect(server.requests.length).to.equal(1); let request = server.requests[0]; - expect(request.url).to.equal('//localhost:9999/event'); + expect(request.url).to.match(/\/\/localhost:9999\/event/); let message = JSON.parse(request.requestBody); @@ -724,7 +724,7 @@ describe('magnite analytics adapter', function () { expect(server.requests.length).to.equal(1); let request = server.requests[0]; - expect(request.url).to.equal('//localhost:9999/event'); + expect(request.url).to.match(/\/\/localhost:9999\/event/); let message = JSON.parse(request.requestBody); @@ -1492,6 +1492,40 @@ describe('magnite analytics adapter', function () { expect(message).to.deep.equal(expectedMessage); }); + describe('when eventDispatcher is present', () => { + beforeEach(() => { + window.pbjs = window.pbjs || {}; + pbjs.rp = pbjs.rp || {}; + pbjs.rp.eventDispatcher = pbjs.rp.eventDispatcher || document.createElement('fakeElem'); + }); + + afterEach(() => { + delete pbjs.rp.eventDispatcher; + delete pbjs.rp; + }); + + it('should dispatch beforeSendingMagniteAnalytics if possible', () => { + pbjs.rp.eventDispatcher.addEventListener('beforeSendingMagniteAnalytics', (data) => { + data.detail.test = 'testData'; + }); + + performStandardAuction(); + + expect(server.requests.length).to.equal(1); + let request = server.requests[0]; + + expect(request.url).to.equal('http://localhost:9999/event'); + + let message = JSON.parse(request.requestBody); + + const AnalyticsMessageWithCustomData = { + ...ANALYTICS_MESSAGE, + test: 'testData' + } + expect(message).to.deep.equal(AnalyticsMessageWithCustomData); + }); + }) + describe('when handling bid caching', () => { let auctionInits, bidRequests, bidResponses, bidsWon; beforeEach(function () { diff --git a/test/spec/modules/mediabramaBidAdapter_spec.js b/test/spec/modules/mediabramaBidAdapter_spec.js new file mode 100644 index 00000000000..d7341e02f17 --- /dev/null +++ b/test/spec/modules/mediabramaBidAdapter_spec.js @@ -0,0 +1,256 @@ +import {expect} from 'chai'; +import {spec} from '../../../modules/mediabramaBidAdapter.js'; +import { BANNER } from '../../../src/mediaTypes.js'; +import * as utils from '../../../src/utils.js'; + +describe('MediaBramaBidAdapter', function () { + const bid = { + bidId: '23dc19818e5293', + bidder: 'mediabrama', + mediaTypes: { + [BANNER]: { + sizes: [[300, 250]] + } + }, + params: { + placementId: 24428, + } + }; + + const bidderRequest = { + refererInfo: { + referer: 'test.com' + } + }; + + describe('isBidRequestValid', function () { + it('Should return true if there are bidId, params and key parameters present', function () { + expect(spec.isBidRequestValid(bid)).to.be.true; + }); + it('Should return false if at least one of parameters is not present', function () { + delete bid.params.placementId; + expect(spec.isBidRequestValid(bid)).to.be.false; + }); + }); + + describe('buildRequests', function () { + let serverRequest = spec.buildRequests([bid], bidderRequest); + it('Creates a ServerRequest object with method, URL and data', function () { + expect(serverRequest).to.exist; + expect(serverRequest.method).to.exist; + expect(serverRequest.url).to.exist; + expect(serverRequest.data).to.exist; + }); + it('Returns POST method', function () { + expect(serverRequest.method).to.equal('POST'); + }); + it('Returns valid URL', function () { + expect(serverRequest.url).to.equal('https://prebid.mediabrama.com/pbjs'); + }); + it('Returns valid data if array of bids is valid', function () { + let data = serverRequest.data; + expect(data).to.be.an('object'); + expect(data).to.have.all.keys('deviceWidth', 'deviceHeight', 'language', 'host', 'page', 'placements'); + expect(data.deviceWidth).to.be.a('number'); + expect(data.deviceHeight).to.be.a('number'); + expect(data.language).to.be.a('string'); + expect(data.host).to.be.a('string'); + expect(data.page).to.be.a('string'); + expect(data.gdpr).to.not.exist; + expect(data.ccpa).to.not.exist; + let placement = data['placements'][0]; + expect(placement).to.have.keys('placementId', 'bidId', 'adFormat', 'sizes', 'schain', 'bidfloor'); + expect(placement.placementId).to.equal(24428); + expect(placement.bidId).to.equal('23dc19818e5293'); + expect(placement.adFormat).to.equal(BANNER); + expect(placement.schain).to.be.an('object'); + expect(placement.sizes).to.be.an('array'); + expect(placement.bidfloor).to.exist.and.to.equal(0); + }); + + it('Returns data with gdprConsent and without uspConsent', function () { + bidderRequest.gdprConsent = 'test'; + serverRequest = spec.buildRequests([bid], bidderRequest); + let data = serverRequest.data; + expect(data.gdpr).to.exist; + expect(data.gdpr).to.be.a('string'); + expect(data.gdpr).to.equal(bidderRequest.gdprConsent); + expect(data.ccpa).to.not.exist; + delete bidderRequest.gdprConsent; + }); + + it('Returns data with uspConsent and without gdprConsent', function () { + bidderRequest.uspConsent = 'test'; + serverRequest = spec.buildRequests([bid], bidderRequest); + let data = serverRequest.data; + expect(data.ccpa).to.exist; + expect(data.ccpa).to.be.a('string'); + expect(data.ccpa).to.equal(bidderRequest.uspConsent); + expect(data.gdpr).to.not.exist; + }); + + it('Returns empty data if no valid requests are passed', function () { + serverRequest = spec.buildRequests([]); + let data = serverRequest.data; + expect(data.placements).to.be.an('array').that.is.empty; + }); + }); + describe('interpretResponse', function () { + it('Should interpret banner response', function () { + const banner = { + body: [{ + mediaType: 'banner', + width: 300, + height: 250, + cpm: 0.4, + ad: 'Test', + requestId: '23dc19818e5293', + ttl: 120, + creativeId: '2', + netRevenue: true, + currency: 'USD', + dealId: '1', + meta: {} + }] + }; + let bannerResponses = spec.interpretResponse(banner); + expect(bannerResponses).to.be.an('array').that.is.not.empty; + let dataItem = bannerResponses[0]; + expect(dataItem).to.have.all.keys('requestId', 'cpm', 'width', 'height', 'ad', 'ttl', 'creativeId', + 'netRevenue', 'currency', 'dealId', 'mediaType', 'meta'); + expect(dataItem.requestId).to.equal('23dc19818e5293'); + expect(dataItem.cpm).to.equal(0.4); + expect(dataItem.width).to.equal(300); + expect(dataItem.height).to.equal(250); + expect(dataItem.ad).to.equal('Test'); + expect(dataItem.ttl).to.equal(120); + expect(dataItem.creativeId).to.equal('2'); + expect(dataItem.netRevenue).to.be.true; + expect(dataItem.currency).to.equal('USD'); + expect(dataItem.meta).to.be.an('object').that.has.any.key('advertiserDomains'); + }); + it('Should return an empty array if invalid banner response is passed', function () { + const invBanner = { + body: [{ + width: 300, + cpm: 0.4, + ad: 'Test', + requestId: '23dc19818e5293', + ttl: 120, + creativeId: '2', + netRevenue: true, + currency: 'USD', + dealId: '1' + }] + }; + + let serverResponses = spec.interpretResponse(invBanner); + expect(serverResponses).to.be.an('array').that.is.empty; + }); + it('Should return an empty array if invalid response is passed', function () { + const invalid = { + body: [{ + ttl: 120, + creativeId: '2', + netRevenue: true, + currency: 'USD', + dealId: '1' + }] + }; + let serverResponses = spec.interpretResponse(invalid); + expect(serverResponses).to.be.an('array').that.is.empty; + }); + }); + + describe('getUserSyncs', function () { + it('should do nothing on getUserSyncs', function () { + const syncData = spec.getUserSyncs({}, {}, { + consentString: 'ALL', + gdprApplies: true + }, {}); + expect(syncData).to.be.an('array').which.is.not.empty; + expect(syncData[0]).to.be.an('object') + expect(syncData[0].type).to.be.a('string') + expect(syncData[0].type).to.equal('image') + expect(syncData[0].url).to.be.a('string') + expect(syncData[0].url).to.equal('https://prebid.mediabrama.com/sync/image?pbjs=1&gdpr=1&gdpr_consent=ALL&coppa=0') + }); + }); + + describe('on bidWon', function () { + beforeEach(function() { + sinon.stub(utils, 'triggerPixel'); + }); + afterEach(function() { + utils.triggerPixel.restore(); + }); + it('should replace nurl for banner', function () { + const nurl = 'nurl/?ap=${' + 'AUCTION_PRICE}'; + const bid = { + 'bidderCode': 'mediabrama', + 'width': 300, + 'height': 250, + 'statusMessage': 'Bid available', + 'adId': '5691dd18ba6ab6', + 'requestId': '23dc19818e5293', + 'transactionId': '948c716b-bf64-4303-bcf4-395c2f6a9770', + 'auctionId': 'a6b7c61f-15a9-481b-8f64-e859787e9c07', + 'mediaType': 'banner', + 'source': 'client', + 'ad': "
\n", + 'cpm': 0.61, + 'nurl': nurl, + 'creativeId': 'test', + 'currency': 'USD', + 'dealId': '', + 'meta': { + 'advertiserDomains': [], + 'dchain': { + 'ver': '1.0', + 'complete': 0, + 'nodes': [ + { + 'name': 'mediabrama' + } + ] + } + }, + 'netRevenue': true, + 'ttl': 185, + 'metrics': {}, + 'adapterCode': 'mediabrama', + 'originalCpm': 0.61, + 'originalCurrency': 'USD', + 'responseTimestamp': 1668162732297, + 'requestTimestamp': 1668162732292, + 'bidder': 'mediabrama', + 'adUnitCode': 'div-prebid', + 'timeToRespond': 5, + 'pbLg': '0.50', + 'pbMg': '0.60', + 'pbHg': '0.61', + 'pbAg': '0.61', + 'pbDg': '0.61', + 'pbCg': '', + 'size': '300x250', + 'adserverTargeting': { + 'hb_bidder': 'mediabrama', + 'hb_adid': '5691dd18ba6ab6', + 'hb_pb': '0.61', + 'hb_size': '300x250', + 'hb_source': 'client', + 'hb_format': 'banner', + 'hb_adomain': '' + }, + 'status': 'rendered', + 'params': [ + { + 'placementId': 24428 + } + ] + }; + spec.onBidWon(bid); + expect(bid.nurl).to.deep.equal('nurl/?ap=0.61'); + }); + }); +}); diff --git a/test/spec/modules/mediafilterRtdProvider_spec.js b/test/spec/modules/mediafilterRtdProvider_spec.js new file mode 100644 index 00000000000..3395c7be691 --- /dev/null +++ b/test/spec/modules/mediafilterRtdProvider_spec.js @@ -0,0 +1,147 @@ +import * as utils from '../../../src/utils.js'; +import * as hook from '../../../src/hook.js' +import * as events from '../../../src/events.js'; +import CONSTANTS from '../../../src/constants.json'; + +import { + MediaFilter, + MEDIAFILTER_EVENT_TYPE, + MEDIAFILTER_BASE_URL +} from '../../../modules/mediafilterRtdProvider.js'; + +describe('The Media Filter RTD module', function () { + describe('register()', function() { + let submoduleSpy, generateInitHandlerSpy; + + beforeEach(function () { + submoduleSpy = sinon.spy(hook, 'submodule'); + generateInitHandlerSpy = sinon.spy(MediaFilter, 'generateInitHandler'); + }); + + afterEach(function () { + submoduleSpy.restore(); + generateInitHandlerSpy.restore(); + }); + + it('should register and call the submodule function(s)', function () { + MediaFilter.register(); + + expect(submoduleSpy.calledOnceWithExactly('realTimeData', sinon.match.object)).to.be.true; + expect(submoduleSpy.called).to.be.true; + expect(generateInitHandlerSpy.called).to.be.true; + }); + }); + + describe('setup()', function() { + let setupEventListenerSpy, setupScriptSpy; + + beforeEach(function() { + setupEventListenerSpy = sinon.spy(MediaFilter, 'setupEventListener'); + setupScriptSpy = sinon.spy(MediaFilter, 'setupScript'); + }); + + afterEach(function() { + setupEventListenerSpy.restore(); + setupScriptSpy.restore(); + }); + + it('should call setupEventListener and setupScript function(s)', function() { + MediaFilter.setup({ configurationHash: 'abc123' }); + + expect(setupEventListenerSpy.called).to.be.true; + expect(setupScriptSpy.called).to.be.true; + }); + }); + + describe('setupEventListener()', function() { + let setupEventListenerSpy, addEventListenerSpy; + + beforeEach(function() { + setupEventListenerSpy = sinon.spy(MediaFilter, 'setupEventListener'); + addEventListenerSpy = sinon.spy(window, 'addEventListener'); + }); + + afterEach(function() { + setupEventListenerSpy.restore(); + addEventListenerSpy.restore(); + }); + + it('should call addEventListener function(s)', function() { + MediaFilter.setupEventListener(); + expect(addEventListenerSpy.called).to.be.true; + expect(addEventListenerSpy.calledWith('message', sinon.match.func)).to.be.true; + }); + }); + + describe('generateInitHandler()', function() { + let generateInitHandlerSpy, setupMock, logErrorSpy; + + beforeEach(function() { + generateInitHandlerSpy = sinon.spy(MediaFilter, 'generateInitHandler'); + setupMock = sinon.stub(MediaFilter, 'setup').throws(new Error('Mocked error!')); + logErrorSpy = sinon.spy(utils, 'logError'); + }); + + afterEach(function() { + generateInitHandlerSpy.restore(); + setupMock.restore(); + logErrorSpy.restore(); + }); + + it('should handle errors in the catch block when setup throws an error', function() { + const initHandler = MediaFilter.generateInitHandler(); + initHandler({}); + + expect(logErrorSpy.calledWith('Error in initialization: Mocked error!')).to.be.true; + }); + }); + + describe('generateEventHandler()', function() { + let generateEventHandlerSpy, eventsEmitSpy; + + beforeEach(function() { + generateEventHandlerSpy = sinon.spy(MediaFilter, 'generateEventHandler'); + eventsEmitSpy = sinon.spy(events, 'emit'); + }); + + afterEach(function() { + generateEventHandlerSpy.restore(); + eventsEmitSpy.restore(); + }); + + it('should emit a billable event when the event type matches', function() { + const configurationHash = 'abc123'; + const eventHandler = MediaFilter.generateEventHandler(configurationHash); + + const mockEvent = { + data: { + type: MEDIAFILTER_EVENT_TYPE.concat('.', configurationHash) + } + }; + + eventHandler(mockEvent); + + expect(eventsEmitSpy.calledWith(CONSTANTS.EVENTS.BILLABLE_EVENT, { + 'billingId': sinon.match.string, + 'configurationHash': configurationHash, + 'type': 'impression', + 'vendor': 'mediafilter', + })).to.be.true; + }); + + it('should not emit a billable event when the event type does not match', function() { + const configurationHash = 'abc123'; + const eventHandler = MediaFilter.generateEventHandler(configurationHash); + + const mockEvent = { + data: { + type: 'differentEventType' + } + }; + + eventHandler(mockEvent); + + expect(eventsEmitSpy.called).to.be.false; + }); + }); +}); diff --git a/test/spec/modules/mediagoBidAdapter_spec.js b/test/spec/modules/mediagoBidAdapter_spec.js index e77af544429..6e58217b3d3 100644 --- a/test/spec/modules/mediagoBidAdapter_spec.js +++ b/test/spec/modules/mediagoBidAdapter_spec.js @@ -1,5 +1,17 @@ import { expect } from 'chai'; -import { spec } from 'modules/mediagoBidAdapter.js'; +import { + spec, + getPmgUID, + storage, + getPageTitle, + getPageDescription, + getPageKeywords, + getConnectionDownLink, + THIRD_PARTY_COOKIE_ORIGIN, + COOKIE_KEY_MGUID, + getCurrentTimeToUTCString +} from 'modules/mediagoBidAdapter.js'; +import * as utils from 'src/utils.js'; describe('mediago:BidAdapterTests', function () { let bidRequestData = { @@ -11,12 +23,49 @@ describe('mediago:BidAdapterTests', function () { bidder: 'mediago', params: { token: '85a6b01e41ac36d49744fad726e3655d', + siteId: 'siteId_01', + zoneId: 'zoneId_01', + publisher: '52', + position: 'left', + referrer: 'https://trace.mediago.io', bidfloor: 0.01, + ortb2Imp: { + ext: { + gpid: 'adslot_gpid', + tid: 'tid_01', + data: { + browsi: { + browsiViewability: 'NA' + }, + adserver: { + name: 'adserver_name', + adslot: 'adslot_name' + }, + pbadslot: '/12345/my-gpt-tag-0' + } + } + } }, mediaTypes: { banner: { sizes: [[300, 250]], + pos: 'left' + } + }, + ortb2: { + site: { + cat: ['IAB2'], + keywords: 'power tools, drills, tools=industrial', + content: { + keywords: 'video, source=streaming' + }, + }, + user: { + ext: { + data: {} + } + } }, adUnitCode: 'regular_iframe', transactionId: '7b26fdae-96e6-4c35-a18b-218dda11397d', @@ -27,9 +76,83 @@ describe('mediago:BidAdapterTests', function () { src: 'client', bidRequestsCount: 1, bidderRequestsCount: 1, - bidderWinsCount: 0, - }, + bidderWinsCount: 0 + } ], + gdprConsent: { + consentString: 'BOJ8RZsOJ8RZsABAB8AAAAAZ+A==', + gdprApplies: true, + apiVersion: 2, + vendorData: { + purpose: { + consents: { + 1: false + } + } + } + }, + userId: { + tdid: 'sample-userid', + uid2: { id: 'sample-uid2-value' }, + criteoId: 'sample-criteo-userid', + netId: 'sample-netId-userid', + idl_env: 'sample-idl-userid', + pubProvidedId: [ + { + source: 'puburl.com', + uids: [ + { + id: 'pubid2', + atype: 1, + ext: { + stype: 'ppuid' + } + } + ] + }, + { + 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' }] + } + ] }; let request = []; @@ -38,8 +161,8 @@ describe('mediago:BidAdapterTests', function () { spec.isBidRequestValid({ bidder: 'mediago', params: { - token: ['85a6b01e41ac36d49744fad726e3655d'], - }, + token: ['85a6b01e41ac36d49744fad726e3655d'] + } }) ).to.equal(true); }); @@ -50,11 +173,54 @@ describe('mediago:BidAdapterTests', function () { expect(req_data.imp).to.have.lengthOf(1); }); + describe('mediago: buildRequests', function() { + describe('getPmgUID function', function() { + let sandbox; + + beforeEach(() => { + sandbox = sinon.sandbox.create(); + sandbox.stub(storage, 'getCookie'); + sandbox.stub(storage, 'setCookie'); + sandbox.stub(utils, 'generateUUID').returns('new-uuid'); + sandbox.stub(storage, 'cookiesAreEnabled'); + }) + + afterEach(() => { + sandbox.restore(); + }); + + it('should generate new UUID and set cookie if not exists', () => { + storage.cookiesAreEnabled.callsFake(() => true); + storage.getCookie.callsFake(() => null); + const uid = getPmgUID(); + expect(uid).to.equal('new-uuid'); + expect(storage.setCookie.calledOnce).to.be.true; + }); + + it('should return existing UUID from cookie', () => { + storage.cookiesAreEnabled.callsFake(() => true); + storage.getCookie.callsFake(() => 'existing-uuid'); + const uid = getPmgUID(); + expect(uid).to.equal('existing-uuid'); + expect(storage.setCookie.called).to.be.false; + }); + + it('should not set new UUID when cookies are not enabled', () => { + storage.cookiesAreEnabled.callsFake(() => false); + storage.getCookie.callsFake(() => null); + getPmgUID(); + expect(storage.setCookie.calledOnce).to.be.false; + }); + }) + }); + it('mediago:validate_response_params', function () { - let adm = ""; + let adm = + ''; let temp = '%3Cscr'; temp += 'ipt%3E'; - temp += '!function()%7B%22use%20strict%22%3Bfunction%20f(t)%7Breturn(f%3D%22function%22%3D%3Dtypeof%20Symbol%26%26%22symbol%22%3D%3Dtypeof%20Symbol.iterator%3Ffunction(t)%7Breturn%20typeof%20t%7D%3Afunction(t)%7Breturn%20t%26%26%22function%22%3D%3Dtypeof%20Symbol%26%26t.constructor%3D%3D%3DSymbol%26%26t!%3D%3DSymbol.prototype%3F%22symbol%22%3Atypeof%20t%7D)(t)%7Dfunction%20l(t)%7Bvar%20e%3D0%3Carguments.length%26%26void%200!%3D%3Dt%3Ft%3A%7B%7D%3Btry%7Be.random_t%3D(new%20Date).getTime()%2Cg(function(t)%7Bvar%20e%3D1%3Carguments.length%26%26void%200!%3D%3Darguments%5B1%5D%3Farguments%5B1%5D%3A%22%22%3Bif(%22object%22!%3D%3Df(t))return%20e%3Bvar%20n%3Dfunction(t)%7Bfor(var%20e%2Cn%3D%5B%5D%2Co%3D0%2Ci%3DObject.keys(t)%3Bo%3Ci.length%3Bo%2B%2B)e%3Di%5Bo%5D%2Cn.push(%22%22.concat(e%2C%22%3D%22).concat(t%5Be%5D))%3Breturn%20n%7D(t).join(%22%26%22)%2Co%3De.indexOf(%22%23%22)%2Ci%3De%2Ct%3D%22%22%3Breturn-1!%3D%3Do%26%26(i%3De.slice(0%2Co)%2Ct%3De.slice(o))%2Cn%26%26(i%26%26-1!%3D%3Di.indexOf(%22%3F%22)%3Fi%2B%3D%22%26%22%2Bn%3Ai%2B%3D%22%3F%22%2Bn)%2Ci%2Bt%7D(e%2C%22https%3A%2F%2Ftrace.mediago.io%2Fapi%2Flog%2Ftrack%22))%7Dcatch(t)%7B%7D%7Dfunction%20g(t%2Ce%2Cn)%7B(t%3Dt%3Ft.split(%22%3B%3B%3B%22)%3A%5B%5D).map(function(t)%7Btry%7B0%3C%3Dt.indexOf(%22%2Fapi%2Fbidder%2Ftrack%22)%26%26n%26%26(t%2B%3D%22%26inIframe%3D%22.concat(!(!self.frameElement%7C%7C%22IFRAME%22!%3Dself.frameElement.tagName)%7C%7Cwindow.frames.length!%3Dparent.frames.length%7C%7Cself!%3Dtop)%2Ct%2B%3D%22%26pos_x%3D%22.concat(n.left%2C%22%26pos_y%3D%22).concat(n.top%2C%22%26page_w%3D%22).concat(n.page_width%2C%22%26page_h%3D%22).concat(n.page_height))%7Dcatch(t)%7Bl(%7Btn%3As%2Cwinloss%3A1%2Cfe%3A2%2Cpos_err_c%3A1002%2Cpos_err_m%3At.toString()%7D)%7Dvar%20e%3Dnew%20Image%3Be.src%3Dt%2Ce.style.display%3D%22none%22%2Ce.style.visibility%3D%22hidden%22%2Ce.width%3D0%2Ce.height%3D0%2Cdocument.body.appendChild(e)%7D)%7Dvar%20d%3D%5B%22https%3A%2F%2Ftrace.mediago.io%2Fapi%2Fbidder%2Ftrack%3Ftn%3D39934c2bda4debbe4c680be1dd02f5d3%26price%3DdjUJcggeuWWfbm28q4WXHdgMFkO28DrGw49FnubQ0Bk%26evt%3D101%26rid%3D6e28cfaf115a354ea1ad8e1304d6d7b8%26campaignid%3D1339145%26impid%3D44-300x250-1%26offerid%3D24054386%26test%3D0%26time%3D1660789795%26cp%3DjZDh1xu6_QqJLlKVtCkiHIP_TER6gL9jeTrlHCBoxOM%26acid%3D599%26trackingid%3D99afea272c2b0e8626489674ddb7a0bb%26uid%3Da865b9ae-fa9e-4c09-8204-2db99ac7c8f7%26bm%3D2%26la%3Den%26cn%3Dus%26cid%3D3998296%26info%3DSi3oM-qfCbw2iZRYs01BkUWyH6c5CQWHrA8CQLE0VHcXAcf4ljY9dyLzQ4vAlTWd6-j_ou4ySor3e70Ll7wlKiiauQKaUkZqNoTizHm73C4FK8DYJSTP3VkhJV8RzrYk%26sid%3D128__110__1__12__28__38__163__96__58__24__47__99%26sp%3DdjUJcggeuWWfbm28q4WXHdgMFkO28DrGw49FnubQ0Bk%26scp%3DzK0DRYY1UV-syqSpmcMYBpOebtoQJV9ZEJT0JFqbTQg%26acu%3DUSD%26scu%3DUSD%26sgcp%3DzK0DRYY1UV-syqSpmcMYBpOebtoQJV9ZEJT0JFqbTQg%26gprice%3DdjUJcggeuWWfbm28q4WXHdgMFkO28DrGw49FnubQ0Bk%26gcp%3DzK0DRYY1UV-syqSpmcMYBpOebtoQJV9ZEJT0JFqbTQg%26ah%3D%26de%3Dwjh.popin.cc%26iv%3D0%22%2C%22%24%7BITRACKER2%7D%22%2C%22%24%7BITRACKER3%7D%22%2C%22%24%7BITRACKER4%7D%22%2C%22%24%7BITRACKER5%7D%22%2C%22%24%7BITRACKER6%7D%22%5D%2Cp%3D%5B%22https%3A%2F%2Ftrace.mediago.io%2Fapi%2Fbidder%2Ftrack%3Ftn%3D39934c2bda4debbe4c680be1dd02f5d3%26price%3DdjUJcggeuWWfbm28q4WXHdgMFkO28DrGw49FnubQ0Bk%26evt%3D104%26rid%3D6e28cfaf115a354ea1ad8e1304d6d7b8%26campaignid%3D1339145%26impid%3D44-300x250-1%26offerid%3D24054386%26test%3D0%26time%3D1660789795%26cp%3DjZDh1xu6_QqJLlKVtCkiHIP_TER6gL9jeTrlHCBoxOM%26acid%3D599%26trackingid%3D99afea272c2b0e8626489674ddb7a0bb%26uid%3Da865b9ae-fa9e-4c09-8204-2db99ac7c8f7%26sid%3D128__110__1__12__28__38__163__96__58__24__47__99%26format%3D%26crid%3Dff32b6f9b3bbc45c00b78b6674a2952e%26bm%3D2%26la%3Den%26cn%3Dus%26cid%3D3998296%26info%3DSi3oM-qfCbw2iZRYs01BkUWyH6c5CQWHrA8CQLE0VHcXAcf4ljY9dyLzQ4vAlTWd6-j_ou4ySor3e70Ll7wlKiiauQKaUkZqNoTizHm73C4FK8DYJSTP3VkhJV8RzrYk%26sp%3DdjUJcggeuWWfbm28q4WXHdgMFkO28DrGw49FnubQ0Bk%26scp%3DzK0DRYY1UV-syqSpmcMYBpOebtoQJV9ZEJT0JFqbTQg%26acu%3DUSD%26scu%3DUSD%26sgcp%3DzK0DRYY1UV-syqSpmcMYBpOebtoQJV9ZEJT0JFqbTQg%26gprice%3DdjUJcggeuWWfbm28q4WXHdgMFkO28DrGw49FnubQ0Bk%26gcp%3DzK0DRYY1UV-syqSpmcMYBpOebtoQJV9ZEJT0JFqbTQg%26ah%3D%26de%3Dwjh.popin.cc%26iv%3D0%22%2C%22%24%7BVTRACKER2%7D%22%2C%22%24%7BVTRACKER3%7D%22%2C%22%24%7BVTRACKER4%7D%22%2C%22%24%7BVTRACKER5%7D%22%2C%22%24%7BVTRACKER6%7D%22%5D%2Cs%3D%22f9f2b1ef23fe2759c2cad0953029a94b%22%2Cn%3Ddocument.getElementById(%22mgcontainer-99afea272c2b0e8626489674ddb7a0bb%22)%3Bn%26%26function()%7Bvar%20a%3Dn.getElementsByClassName(%22mediago-placement-track%22)%3Bif(a%26%26a.length)%7Bvar%20t%2Ce%3Dfunction(t)%7Bvar%20e%2Cn%2Co%2Ci%2Cc%2Cr%3B%22object%22%3D%3D%3Df(r%3Da%5Bt%5D)%26%26(e%3Dfunction(t)%7Btry%7Bvar%20e%3Dt.getBoundingClientRect()%2Cn%3De%26%26e.top%7C%7C-1%2Co%3De%26%26e.left%7C%7C-1%2Ci%3Ddocument.body.scrollWidth%7C%7C-1%2Ce%3Ddocument.body.scrollHeight%7C%7C-1%3Breturn%7Btop%3An.toFixed(0)%2Cleft%3Ao.toFixed(0)%2Cpage_width%3Ai%2Cpage_height%3Ae%7D%7Dcatch(o)%7Breturn%20l(%7Btn%3As%2Cwinloss%3A1%2Cfe%3A2%2Cpos_err_c%3A1001%2Cpos_err_m%3Ao.toString()%7D)%2C%7Btop%3A%22-1%22%2Cleft%3A%22-1%22%2Cpage_width%3A%22-1%22%2Cpage_height%3A%22-1%22%7D%7D%7D(r)%2C(n%3Dd%5Bt%5D)%26%26g(n%2C0%2Ce)%2Co%3Dp%5Bt%5D%2Ci%3D!1%2C(c%3Dfunction()%7BsetTimeout(function()%7Bvar%20t%2Ce%3B!i%26%26(t%3Dr%2Ce%3Dwindow.innerHeight%7C%7Cdocument.documentElement.clientHeight%7C%7Cdocument.body.clientHeight%2C(t.getBoundingClientRect()%26%26t.getBoundingClientRect().top)%3C%3De-.75*(t.offsetHeight%7C%7Ct.clientHeight))%3F(i%3D!0%2Co%26%26g(o))%3Ac()%7D%2C500)%7D)())%7D%3Bfor(t%20in%20a)e(t)%7D%7D()%7D()'; + temp += + '!function()%7B%22use%20strict%22%3Bfunction%20f(t)%7Breturn(f%3D%22function%22%3D%3Dtypeof%20Symbol%26%26%22symbol%22%3D%3Dtypeof%20Symbol.iterator%3Ffunction(t)%7Breturn%20typeof%20t%7D%3Afunction(t)%7Breturn%20t%26%26%22function%22%3D%3Dtypeof%20Symbol%26%26t.constructor%3D%3D%3DSymbol%26%26t!%3D%3DSymbol.prototype%3F%22symbol%22%3Atypeof%20t%7D)(t)%7Dfunction%20l(t)%7Bvar%20e%3D0%3Carguments.length%26%26void%200!%3D%3Dt%3Ft%3A%7B%7D%3Btry%7Be.random_t%3D(new%20Date).getTime()%2Cg(function(t)%7Bvar%20e%3D1%3Carguments.length%26%26void%200!%3D%3Darguments%5B1%5D%3Farguments%5B1%5D%3A%22%22%3Bif(%22object%22!%3D%3Df(t))return%20e%3Bvar%20n%3Dfunction(t)%7Bfor(var%20e%2Cn%3D%5B%5D%2Co%3D0%2Ci%3DObject.keys(t)%3Bo%3Ci.length%3Bo%2B%2B)e%3Di%5Bo%5D%2Cn.push(%22%22.concat(e%2C%22%3D%22).concat(t%5Be%5D))%3Breturn%20n%7D(t).join(%22%26%22)%2Co%3De.indexOf(%22%23%22)%2Ci%3De%2Ct%3D%22%22%3Breturn-1!%3D%3Do%26%26(i%3De.slice(0%2Co)%2Ct%3De.slice(o))%2Cn%26%26(i%26%26-1!%3D%3Di.indexOf(%22%3F%22)%3Fi%2B%3D%22%26%22%2Bn%3Ai%2B%3D%22%3F%22%2Bn)%2Ci%2Bt%7D(e%2C%22https%3A%2F%2Ftrace.mediago.io%2Fapi%2Flog%2Ftrack%22))%7Dcatch(t)%7B%7D%7Dfunction%20g(t%2Ce%2Cn)%7B(t%3Dt%3Ft.split(%22%3B%3B%3B%22)%3A%5B%5D).map(function(t)%7Btry%7B0%3C%3Dt.indexOf(%22%2Fapi%2Fbidder%2Ftrack%22)%26%26n%26%26(t%2B%3D%22%26inIframe%3D%22.concat(!(!self.frameElement%7C%7C%22IFRAME%22!%3Dself.frameElement.tagName)%7C%7Cwindow.frames.length!%3Dparent.frames.length%7C%7Cself!%3Dtop)%2Ct%2B%3D%22%26pos_x%3D%22.concat(n.left%2C%22%26pos_y%3D%22).concat(n.top%2C%22%26page_w%3D%22).concat(n.page_width%2C%22%26page_h%3D%22).concat(n.page_height))%7Dcatch(t)%7Bl(%7Btn%3As%2Cwinloss%3A1%2Cfe%3A2%2Cpos_err_c%3A1002%2Cpos_err_m%3At.toString()%7D)%7Dvar%20e%3Dnew%20Image%3Be.src%3Dt%2Ce.style.display%3D%22none%22%2Ce.style.visibility%3D%22hidden%22%2Ce.width%3D0%2Ce.height%3D0%2Cdocument.body.appendChild(e)%7D)%7Dvar%20d%3D%5B%22https%3A%2F%2Ftrace.mediago.io%2Fapi%2Fbidder%2Ftrack%3Ftn%3D39934c2bda4debbe4c680be1dd02f5d3%26price%3DdjUJcggeuWWfbm28q4WXHdgMFkO28DrGw49FnubQ0Bk%26evt%3D101%26rid%3D6e28cfaf115a354ea1ad8e1304d6d7b8%26campaignid%3D1339145%26impid%3D44-300x250-1%26offerid%3D24054386%26test%3D0%26time%3D1660789795%26cp%3DjZDh1xu6_QqJLlKVtCkiHIP_TER6gL9jeTrlHCBoxOM%26acid%3D599%26trackingid%3D99afea272c2b0e8626489674ddb7a0bb%26uid%3Da865b9ae-fa9e-4c09-8204-2db99ac7c8f7%26bm%3D2%26la%3Den%26cn%3Dus%26cid%3D3998296%26info%3DSi3oM-qfCbw2iZRYs01BkUWyH6c5CQWHrA8CQLE0VHcXAcf4ljY9dyLzQ4vAlTWd6-j_ou4ySor3e70Ll7wlKiiauQKaUkZqNoTizHm73C4FK8DYJSTP3VkhJV8RzrYk%26sid%3D128__110__1__12__28__38__163__96__58__24__47__99%26sp%3DdjUJcggeuWWfbm28q4WXHdgMFkO28DrGw49FnubQ0Bk%26scp%3DzK0DRYY1UV-syqSpmcMYBpOebtoQJV9ZEJT0JFqbTQg%26acu%3DUSD%26scu%3DUSD%26sgcp%3DzK0DRYY1UV-syqSpmcMYBpOebtoQJV9ZEJT0JFqbTQg%26gprice%3DdjUJcggeuWWfbm28q4WXHdgMFkO28DrGw49FnubQ0Bk%26gcp%3DzK0DRYY1UV-syqSpmcMYBpOebtoQJV9ZEJT0JFqbTQg%26ah%3D%26de%3Dwjh.popin.cc%26iv%3D0%22%2C%22%24%7BITRACKER2%7D%22%2C%22%24%7BITRACKER3%7D%22%2C%22%24%7BITRACKER4%7D%22%2C%22%24%7BITRACKER5%7D%22%2C%22%24%7BITRACKER6%7D%22%5D%2Cp%3D%5B%22https%3A%2F%2Ftrace.mediago.io%2Fapi%2Fbidder%2Ftrack%3Ftn%3D39934c2bda4debbe4c680be1dd02f5d3%26price%3DdjUJcggeuWWfbm28q4WXHdgMFkO28DrGw49FnubQ0Bk%26evt%3D104%26rid%3D6e28cfaf115a354ea1ad8e1304d6d7b8%26campaignid%3D1339145%26impid%3D44-300x250-1%26offerid%3D24054386%26test%3D0%26time%3D1660789795%26cp%3DjZDh1xu6_QqJLlKVtCkiHIP_TER6gL9jeTrlHCBoxOM%26acid%3D599%26trackingid%3D99afea272c2b0e8626489674ddb7a0bb%26uid%3Da865b9ae-fa9e-4c09-8204-2db99ac7c8f7%26sid%3D128__110__1__12__28__38__163__96__58__24__47__99%26format%3D%26crid%3Dff32b6f9b3bbc45c00b78b6674a2952e%26bm%3D2%26la%3Den%26cn%3Dus%26cid%3D3998296%26info%3DSi3oM-qfCbw2iZRYs01BkUWyH6c5CQWHrA8CQLE0VHcXAcf4ljY9dyLzQ4vAlTWd6-j_ou4ySor3e70Ll7wlKiiauQKaUkZqNoTizHm73C4FK8DYJSTP3VkhJV8RzrYk%26sp%3DdjUJcggeuWWfbm28q4WXHdgMFkO28DrGw49FnubQ0Bk%26scp%3DzK0DRYY1UV-syqSpmcMYBpOebtoQJV9ZEJT0JFqbTQg%26acu%3DUSD%26scu%3DUSD%26sgcp%3DzK0DRYY1UV-syqSpmcMYBpOebtoQJV9ZEJT0JFqbTQg%26gprice%3DdjUJcggeuWWfbm28q4WXHdgMFkO28DrGw49FnubQ0Bk%26gcp%3DzK0DRYY1UV-syqSpmcMYBpOebtoQJV9ZEJT0JFqbTQg%26ah%3D%26de%3Dwjh.popin.cc%26iv%3D0%22%2C%22%24%7BVTRACKER2%7D%22%2C%22%24%7BVTRACKER3%7D%22%2C%22%24%7BVTRACKER4%7D%22%2C%22%24%7BVTRACKER5%7D%22%2C%22%24%7BVTRACKER6%7D%22%5D%2Cs%3D%22f9f2b1ef23fe2759c2cad0953029a94b%22%2Cn%3Ddocument.getElementById(%22mgcontainer-99afea272c2b0e8626489674ddb7a0bb%22)%3Bn%26%26function()%7Bvar%20a%3Dn.getElementsByClassName(%22mediago-placement-track%22)%3Bif(a%26%26a.length)%7Bvar%20t%2Ce%3Dfunction(t)%7Bvar%20e%2Cn%2Co%2Ci%2Cc%2Cr%3B%22object%22%3D%3D%3Df(r%3Da%5Bt%5D)%26%26(e%3Dfunction(t)%7Btry%7Bvar%20e%3Dt.getBoundingClientRect()%2Cn%3De%26%26e.top%7C%7C-1%2Co%3De%26%26e.left%7C%7C-1%2Ci%3Ddocument.body.scrollWidth%7C%7C-1%2Ce%3Ddocument.body.scrollHeight%7C%7C-1%3Breturn%7Btop%3An.toFixed(0)%2Cleft%3Ao.toFixed(0)%2Cpage_width%3Ai%2Cpage_height%3Ae%7D%7Dcatch(o)%7Breturn%20l(%7Btn%3As%2Cwinloss%3A1%2Cfe%3A2%2Cpos_err_c%3A1001%2Cpos_err_m%3Ao.toString()%7D)%2C%7Btop%3A%22-1%22%2Cleft%3A%22-1%22%2Cpage_width%3A%22-1%22%2Cpage_height%3A%22-1%22%7D%7D%7D(r)%2C(n%3Dd%5Bt%5D)%26%26g(n%2C0%2Ce)%2Co%3Dp%5Bt%5D%2Ci%3D!1%2C(c%3Dfunction()%7BsetTimeout(function()%7Bvar%20t%2Ce%3B!i%26%26(t%3Dr%2Ce%3Dwindow.innerHeight%7C%7Cdocument.documentElement.clientHeight%7C%7Cdocument.body.clientHeight%2C(t.getBoundingClientRect()%26%26t.getBoundingClientRect().top)%3C%3De-.75*(t.offsetHeight%7C%7Ct.clientHeight))%3F(i%3D!0%2Co%26%26g(o))%3Ac()%7D%2C500)%7D)())%7D%3Bfor(t%20in%20a)e(t)%7D%7D()%7D()'; temp += '%3B%3C%2Fscri'; temp += 'pt%3E'; adm += decodeURIComponent(temp); @@ -72,13 +238,13 @@ describe('mediago:BidAdapterTests', function () { cid: '1339145', crid: 'ff32b6f9b3bbc45c00b78b6674a2952e', w: 300, - h: 250, - }, - ], - }, + h: 250 + } + ] + } ], - cur: 'USD', - }, + cur: 'USD' + } }; let bids = spec.interpretResponse(serverResponse); @@ -94,4 +260,324 @@ describe('mediago:BidAdapterTests', function () { expect(bid.height).to.equal(250); expect(bid.currency).to.equal('USD'); }); + + describe('mediago: getUserSyncs', function() { + const COOKY_SYNC_IFRAME_URL = 'https://cdn.mediago.io/js/cookieSync.html'; + const IFRAME_ENABLED = { + iframeEnabled: true, + pixelEnabled: false, + }; + const IFRAME_DISABLED = { + iframeEnabled: false, + pixelEnabled: false, + }; + const GDPR_CONSENT = { + consentString: 'gdprConsentString', + gdprApplies: true + }; + const USP_CONSENT = { + consentString: 'uspConsentString' + } + + let syncParamUrl = `dm=${encodeURIComponent(location.origin || `https://${location.host}`)}`; + syncParamUrl += '&gdpr=1&gdpr_consent=gdprConsentString&ccpa_consent=uspConsentString'; + const expectedIframeSyncs = [ + { + type: 'iframe', + url: `${COOKY_SYNC_IFRAME_URL}?${syncParamUrl}` + } + ]; + + it('should return nothing if iframe is disabled', () => { + const userSyncs = spec.getUserSyncs(IFRAME_DISABLED, undefined, GDPR_CONSENT, USP_CONSENT, undefined); + expect(userSyncs).to.be.undefined; + }); + + it('should do userSyncs if iframe is enabled', () => { + const userSyncs = spec.getUserSyncs(IFRAME_ENABLED, undefined, GDPR_CONSENT, USP_CONSENT, undefined); + expect(userSyncs).to.deep.equal(expectedIframeSyncs); + }); + }); +}); + +describe('mediago Bid Adapter Tests', function () { + describe('buildRequests', () => { + describe('getPageTitle function', function() { + let sandbox; + + beforeEach(() => { + sandbox = sinon.createSandbox(); + }); + + afterEach(() => { + sandbox.restore(); + }); + + it('should return the top document title if available', function() { + const fakeTopDocument = { + title: 'Top Document Title', + querySelector: () => ({ content: 'Top Document Title test' }) + }; + const fakeTopWindow = { + document: fakeTopDocument + }; + const result = getPageTitle({ top: fakeTopWindow }); + expect(result).to.equal('Top Document Title'); + }); + + it('should return the content of top og:title meta tag if title is empty', function() { + const ogTitleContent = 'Top OG Title Content'; + const fakeTopWindow = { + document: { + title: '', + querySelector: sandbox.stub().withArgs('meta[property="og:title"]').returns({ content: ogTitleContent }) + } + }; + + const result = getPageTitle({ top: fakeTopWindow }); + expect(result).to.equal(ogTitleContent); + }); + + it('should return the document title if no og:title meta tag is present', function() { + document.title = 'Test Page Title'; + sandbox.stub(document, 'querySelector').withArgs('meta[property="og:title"]').returns(null); + + const result = getPageTitle({ top: undefined }); + expect(result).to.equal('Test Page Title'); + }); + + it('should return the content of og:title meta tag if present', function() { + document.title = ''; + const ogTitleContent = 'Top OG Title Content'; + sandbox.stub(document, 'querySelector').withArgs('meta[property="og:title"]').returns({ content: ogTitleContent }); + const result = getPageTitle({ top: undefined }); + expect(result).to.equal(ogTitleContent); + }); + + it('should return an empty string if no title or og:title meta tag is found', function() { + document.title = ''; + sandbox.stub(document, 'querySelector').withArgs('meta[property="og:title"]').returns(null); + const result = getPageTitle({ top: undefined }); + expect(result).to.equal(''); + }); + + it('should handle exceptions when accessing top.document and fallback to current document', function() { + const fakeWindow = { + get top() { + throw new Error('Access denied'); + } + }; + const ogTitleContent = 'Current OG Title Content'; + document.title = 'Current Document Title'; + sandbox.stub(document, 'querySelector').withArgs('meta[property="og:title"]').returns({ content: ogTitleContent }); + const result = getPageTitle(fakeWindow); + expect(result).to.equal('Current Document Title'); + }); + }); + + describe('getPageDescription function', function() { + let sandbox; + + beforeEach(() => { + sandbox = sinon.createSandbox(); + }); + + afterEach(() => { + sandbox.restore(); + }); + + it('should return the top document description if available', function() { + const descriptionContent = 'Top Document Description'; + const fakeTopDocument = { + querySelector: sandbox.stub().withArgs('meta[name="description"]').returns({ content: descriptionContent }) + }; + const fakeTopWindow = { document: fakeTopDocument }; + const result = getPageDescription({ top: fakeTopWindow }); + expect(result).to.equal(descriptionContent); + }); + + it('should return the top document og:description if description is not present', function() { + const ogDescriptionContent = 'Top OG Description'; + const fakeTopDocument = { + querySelector: sandbox.stub().withArgs('meta[property="og:description"]').returns({ content: ogDescriptionContent }) + }; + const fakeTopWindow = { document: fakeTopDocument }; + const result = getPageDescription({ top: fakeTopWindow }); + expect(result).to.equal(ogDescriptionContent); + }); + + it('should return the current document description if top document is not accessible', function() { + const descriptionContent = 'Current Document Description'; + sandbox.stub(document, 'querySelector') + .withArgs('meta[name="description"]').returns({ content: descriptionContent }) + const fakeWindow = { + get top() { + throw new Error('Access denied'); + } + }; + const result = getPageDescription(fakeWindow); + expect(result).to.equal(descriptionContent); + }); + + it('should return the current document og:description if description is not present and top document is not accessible', function() { + const ogDescriptionContent = 'Current OG Description'; + sandbox.stub(document, 'querySelector') + .withArgs('meta[property="og:description"]').returns({ content: ogDescriptionContent }); + + const fakeWindow = { + get top() { + throw new Error('Access denied'); + } + }; + const result = getPageDescription(fakeWindow); + expect(result).to.equal(ogDescriptionContent); + }); + }); + + describe('getPageKeywords function', function() { + let sandbox; + + beforeEach(() => { + sandbox = sinon.createSandbox(); + }); + + afterEach(() => { + sandbox.restore(); + }); + + it('should return the top document keywords if available', function() { + const keywordsContent = 'keyword1, keyword2, keyword3'; + const fakeTopDocument = { + querySelector: sandbox.stub() + .withArgs('meta[name="keywords"]').returns({ content: keywordsContent }) + }; + const fakeTopWindow = { document: fakeTopDocument }; + + const result = getPageKeywords({ top: fakeTopWindow }); + expect(result).to.equal(keywordsContent); + }); + + it('should return the current document keywords if top document is not accessible', function() { + const keywordsContent = 'keyword1, keyword2, keyword3'; + sandbox.stub(document, 'querySelector') + .withArgs('meta[name="keywords"]').returns({ content: keywordsContent }); + + // æ¨Ąæ‹ŸéĄļåą‚įĒ—åŖčŽŋ问åŧ‚常 + const fakeWindow = { + get top() { + throw new Error('Access denied'); + } + }; + + const result = getPageKeywords(fakeWindow); + expect(result).to.equal(keywordsContent); + }); + + it('should return an empty string if no keywords meta tag is found', function() { + sandbox.stub(document, 'querySelector').withArgs('meta[name="keywords"]').returns(null); + + const result = getPageKeywords(); + expect(result).to.equal(''); + }); + }); + describe('getConnectionDownLink function', function() { + let sandbox; + + beforeEach(() => { + sandbox = sinon.createSandbox(); + }); + + afterEach(() => { + sandbox.restore(); + }); + + it('should return the downlink value as a string if available', function() { + const downlinkValue = 2.5; + const fakeNavigator = { + connection: { + downlink: downlinkValue + } + }; + + const result = getConnectionDownLink({ navigator: fakeNavigator }); + expect(result).to.equal(downlinkValue.toString()); + }); + + it('should return undefined if downlink is not available', function() { + const fakeNavigator = { + connection: {} + }; + + const result = getConnectionDownLink({ navigator: fakeNavigator }); + expect(result).to.be.undefined; + }); + + it('should return undefined if connection is not available', function() { + const fakeNavigator = {}; + + const result = getConnectionDownLink({ navigator: fakeNavigator }); + expect(result).to.be.undefined; + }); + + it('should handle cases where navigator is not defined', function() { + const result = getConnectionDownLink({}); + expect(result).to.be.undefined; + }); + }); + + describe('getUserSyncs with message event listener', function() { + function messageHandler(event) { + if (!event.data || event.origin !== THIRD_PARTY_COOKIE_ORIGIN) { + return; + } + + window.removeEventListener('message', messageHandler, true); + event.stopImmediatePropagation(); + + const response = event.data; + if (!response.optout && response.mguid) { + storage.setCookie(COOKIE_KEY_MGUID, response.mguid, getCurrentTimeToUTCString()); + } + } + + let sandbox; + + beforeEach(() => { + sandbox = sinon.createSandbox(); + sandbox.stub(storage, 'setCookie'); + sandbox.stub(window, 'removeEventListener'); + }); + + afterEach(() => { + sandbox.restore(); + }); + + it('should set a cookie when a valid message is received', () => { + const fakeEvent = { + data: { optout: '', mguid: '12345' }, + origin: THIRD_PARTY_COOKIE_ORIGIN, + stopImmediatePropagation: sinon.spy() + }; + + messageHandler(fakeEvent); + + expect(fakeEvent.stopImmediatePropagation.calledOnce).to.be.true; + expect(window.removeEventListener.calledWith('message', messageHandler, true)).to.be.true; + expect(storage.setCookie.calledWith(COOKIE_KEY_MGUID, '12345', sinon.match.string)).to.be.true; + }); + it('should not do anything when an invalid message is received', () => { + const fakeEvent = { + data: null, + origin: 'http://invalid-origin.com', + stopImmediatePropagation: sinon.spy() + }; + + messageHandler(fakeEvent); + + expect(fakeEvent.stopImmediatePropagation.notCalled).to.be.true; + expect(window.removeEventListener.notCalled).to.be.true; + expect(storage.setCookie.notCalled).to.be.true; + }); + }); + }); }); diff --git a/test/spec/modules/medianetBidAdapter_spec.js b/test/spec/modules/medianetBidAdapter_spec.js index bb90eded230..4a221e97444 100644 --- a/test/spec/modules/medianetBidAdapter_spec.js +++ b/test/spec/modules/medianetBidAdapter_spec.js @@ -915,24 +915,24 @@ let VALID_BID_REQUEST = [{ cid: '8CUV090' } }, - VALID_PARAMS_AAX = { - bidder: 'aax', + VALID_PARAMS_TS = { + bidder: 'trustedstack', params: { - cid: 'AAXG123' + cid: 'TS012345' } }, PARAMS_MISSING = { bidder: 'medianet', }, - PARAMS_MISSING_AAX = { - bidder: 'aax', + PARAMS_MISSING_TS = { + bidder: 'trustedstack', }, PARAMS_WITHOUT_CID = { bidder: 'medianet', params: {} }, - PARAMS_WITHOUT_CID_AAX = { - bidder: 'aax', + PARAMS_WITHOUT_CID_TS = { + bidder: 'trustedstack', params: {} }, PARAMS_WITH_INTEGER_CID = { @@ -941,8 +941,8 @@ let VALID_BID_REQUEST = [{ cid: 8867587 } }, - PARAMS_WITH_INTEGER_CID_AAX = { - bidder: 'aax', + PARAMS_WITH_INTEGER_CID_TS = { + bidder: 'trustedstack', params: { cid: 8867587 } @@ -953,8 +953,8 @@ let VALID_BID_REQUEST = [{ cid: '' } }, - PARAMS_WITH_EMPTY_CID_AAX = { - bidder: 'aax', + PARAMS_WITH_EMPTY_CID_TS = { + bidder: 'trustedstack', params: { cid: '' } @@ -1311,6 +1311,118 @@ let VALID_BID_REQUEST = [{ } }], 'tmax': 3000, + }, + VALID_BIDDER_REQUEST_WITH_GPP_IN_ORTB2 = { + ortb2: { + regs: { + gpp: 'DBACNYA~CPXxRfAPXxRfAAfKABENB-CgAAAAAAAAAAYgAAAAAAAA~1YNN', + gpp_sid: [5, 7] + } + }, + 'timeout': 3000, + refererInfo: { + referer: 'http://media.net/prebidtest', + stack: ['http://media.net/prebidtest'], + page: 'http://media.net/page', + domain: 'media.net', + topmostLocation: 'http://media.net/topmost', + reachedTop: true + } + }, + VALID_PAYLOAD_FOR_GPP_ORTB2 = { + 'site': { + 'page': 'http://media.net/prebidtest', + 'domain': 'media.net', + 'ref': 'http://media.net/prebidtest', + 'topMostLocation': 'http://media.net/topmost', + 'isTop': true + }, + 'ext': { + 'customer_id': 'customer_id', + 'prebid_version': $$PREBID_GLOBAL$$.version, + 'gdpr_applies': false, + 'usp_applies': false, + 'coppa_applies': false, + 'screen': { + 'w': 1000, + 'h': 1000 + } + }, + 'id': 'aafabfd0-28c0-4ac0-aa09-99689e88b81d', + 'imp': [{ + 'id': '28f8f8130a583e', + 'transactionId': '277b631f-92f5-4844-8b19-ea13c095d3f1', + ortb2Imp: VALID_BID_REQUEST[0].ortb2Imp, + 'ext': { + 'dfp_id': 'div-gpt-ad-1460505748561-0', + 'visibility': 1, + 'viewability': 1, + 'coordinates': { + 'top_left': { + x: 50, + y: 50 + }, + 'bottom_right': { + x: 100, + y: 100 + } + }, + 'display_count': 1 + }, + 'banner': [{ + 'w': 300, + 'h': 250 + }], + 'all': { + 'cid': 'customer_id', + 'site': { + 'page': 'http://media.net/prebidtest', + 'domain': 'media.net', + 'ref': 'http://media.net/prebidtest', + 'isTop': true + } + } + }, { + 'id': '3f97ca71b1e5c2', + 'transactionId': 'c52a5c62-3c2b-4b90-9ff8-ec1487754822', + ortb2Imp: VALID_BID_REQUEST[1].ortb2Imp, + 'ext': { + 'dfp_id': 'div-gpt-ad-1460505748561-123', + 'visibility': 1, + 'viewability': 1, + 'coordinates': { + 'top_left': { + x: 50, + y: 50 + }, + 'bottom_right': { + x: 100, + y: 100 + } + }, + 'display_count': 1 + }, + 'banner': [{ + 'w': 300, + 'h': 251 + }], + 'all': { + 'cid': 'customer_id', + 'site': { + 'page': 'http://media.net/prebidtest', + 'domain': 'media.net', + 'ref': 'http://media.net/prebidtest', + 'isTop': true + } + } + }], + 'ortb2': { + 'regs': { + 'gpp': 'DBACNYA~CPXxRfAPXxRfAAfKABENB-CgAAAAAAAAAAYgAAAAAAAA~1YNN', + 'gpp_sid': [5, 7], + } + }, + 'tmax': config.getConfig('bidderTimeout') }; describe('Media.net bid adapter', function () { let sandbox; @@ -1393,6 +1505,11 @@ describe('Media.net bid adapter', function () { expect(JSON.parse(bidReq.data)).to.deep.equal(VALID_PAYLOAD_FOR_GDPR); }); + it('should have gpp params in ortb2', function () { + let bidReq = spec.buildRequests(VALID_BID_REQUEST, VALID_BIDDER_REQUEST_WITH_GPP_IN_ORTB2); + expect(JSON.parse(bidReq.data)).to.deep.equal(VALID_PAYLOAD_FOR_GPP_ORTB2); + }); + it('should parse params for native request', function () { let bidReq = spec.buildRequests(VALID_NATIVE_BID_REQUEST, VALID_AUCTIONDATA); expect(JSON.parse(bidReq.data)).to.deep.equal(VALID_PAYLOAD_NATIVE); @@ -1666,34 +1783,34 @@ describe('Media.net bid adapter', function () { }); }); - describe('isBidRequestValid aax', function () { + describe('isBidRequestValid trustedstack', function () { it('should accept valid bid params', function () { - let isValid = spec.isBidRequestValid(VALID_PARAMS_AAX); + let isValid = spec.isBidRequestValid(VALID_PARAMS_TS); expect(isValid).to.equal(true); }); it('should reject bid if cid is not present', function () { - let isValid = spec.isBidRequestValid(PARAMS_WITHOUT_CID_AAX); + let isValid = spec.isBidRequestValid(PARAMS_WITHOUT_CID_TS); expect(isValid).to.equal(false); }); it('should reject bid if cid is not a string', function () { - let isValid = spec.isBidRequestValid(PARAMS_WITH_INTEGER_CID_AAX); + let isValid = spec.isBidRequestValid(PARAMS_WITH_INTEGER_CID_TS); expect(isValid).to.equal(false); }); it('should reject bid if cid is a empty string', function () { - let isValid = spec.isBidRequestValid(PARAMS_WITH_EMPTY_CID_AAX); + let isValid = spec.isBidRequestValid(PARAMS_WITH_EMPTY_CID_TS); expect(isValid).to.equal(false); }); it('should have missing params', function () { - let isValid = spec.isBidRequestValid(PARAMS_MISSING_AAX); + let isValid = spec.isBidRequestValid(PARAMS_MISSING_TS); expect(isValid).to.equal(false); }); }); - describe('interpretResponse aax', function () { + describe('interpretResponse trustedstack', function () { it('should not push response if no-bid', function () { let validBids = []; let bids = spec.interpretResponse(SERVER_RESPONSE_NOBID, []); diff --git a/test/spec/modules/mediasquareBidAdapter_spec.js b/test/spec/modules/mediasquareBidAdapter_spec.js index 4e131024d84..d7984c05967 100644 --- a/test/spec/modules/mediasquareBidAdapter_spec.js +++ b/test/spec/modules/mediasquareBidAdapter_spec.js @@ -1,5 +1,6 @@ import {expect} from 'chai'; import {spec} from 'modules/mediasquareBidAdapter.js'; +import { server } from 'test/mocks/xhr.js'; describe('MediaSquare bid adapter tests', function () { var DEFAULT_PARAMS = [{ @@ -99,6 +100,8 @@ describe('MediaSquare bid adapter tests', function () { 'bid_id': 'aaaa1234', 'adomain': ['test.com'], 'context': 'instream', + 'increment': 1.0, + 'ova': 'cleared', }], }}; @@ -166,7 +169,10 @@ describe('MediaSquare bid adapter tests', function () { expect(bid.mediasquare.bidder).to.equal('msqClassic'); expect(bid.mediasquare.context).to.exist; expect(bid.mediasquare.context).to.equal('instream'); + expect(bid.mediasquare.increment).to.exist; + expect(bid.mediasquare.increment).to.equal(1.0); expect(bid.mediasquare.code).to.equal([DEFAULT_PARAMS[0].params.owner, DEFAULT_PARAMS[0].params.code].join('/')); + expect(bid.mediasquare.ova).to.exist.and.to.equal('cleared'); expect(bid.meta).to.exist; expect(bid.meta.advertiserDomains).to.exist; expect(bid.meta.advertiserDomains).to.have.lengthOf(1); @@ -205,6 +211,11 @@ describe('MediaSquare bid adapter tests', function () { const response = spec.interpretResponse(BID_RESPONSE, request); const won = spec.onBidWon(response[0]); expect(won).to.equal(true); + expect(server.requests.length).to.equal(1); + let message = JSON.parse(server.requests[0].requestBody); + expect(message).to.have.property('increment').exist; + expect(message).to.have.property('increment').and.to.equal('1'); + expect(message).to.have.property('ova').and.to.equal('cleared'); }); it('Verifies user sync without cookie in bid response', function () { var syncs = spec.getUserSyncs({}, [BID_RESPONSE], DEFAULT_OPTIONS.gdprConsent, DEFAULT_OPTIONS.uspConsent); diff --git a/test/spec/modules/mgidRtdProvider_spec.js b/test/spec/modules/mgidRtdProvider_spec.js index 4f70b4d8b7c..996875649b6 100644 --- a/test/spec/modules/mgidRtdProvider_spec.js +++ b/test/spec/modules/mgidRtdProvider_spec.js @@ -1,16 +1,14 @@ import { mgidSubmodule, storage } from '../../../modules/mgidRtdProvider.js'; import {expect} from 'chai'; import * as refererDetection from '../../../src/refererDetection'; +import {server} from '../../mocks/xhr.js'; describe('Mgid RTD submodule', () => { - let server; let clock; let getRefererInfoStub; let getDataFromLocalStorageStub; beforeEach(() => { - server = sinon.fakeServer.create(); - clock = sinon.useFakeTimers(); getRefererInfoStub = sinon.stub(refererDetection, 'getRefererInfo'); @@ -22,7 +20,6 @@ describe('Mgid RTD submodule', () => { }); afterEach(() => { - server.restore(); clock.restore(); getRefererInfoStub.restore(); getDataFromLocalStorageStub.restore(); @@ -309,7 +306,6 @@ describe('Mgid RTD submodule', () => { server.requests[0].respond( 204, {'Content-Type': 'application/json'}, - '{}' ); assert.deepEqual(reqBidsConfigObj.ortb2Fragments.global, {}); diff --git a/test/spec/modules/mgidXBidAdapter_spec.js b/test/spec/modules/mgidXBidAdapter_spec.js index 14619e9c0e1..978ca3de036 100644 --- a/test/spec/modules/mgidXBidAdapter_spec.js +++ b/test/spec/modules/mgidXBidAdapter_spec.js @@ -76,7 +76,10 @@ describe('MGIDXBidAdapter', function () { const bidderRequest = { uspConsent: '1---', - gdprConsent: 'COvFyGBOvFyGBAbAAAENAPCAAOAAAAAAAAAAAEEUACCKAAA.IFoEUQQgAIQwgIwQABAEAAAAOIAACAIAAAAQAIAgEAACEAAAAAgAQBAAAAAAAGBAAgAAAAAAAFAAECAAAgAAQARAEQAAAAAJAAIAAgAAAYQEAAAQmAgBC3ZAYzUw', + gdprConsent: { + consentString: 'COvFyGBOvFyGBAbAAAENAPCAAOAAAAAAAAAAAEEUACCKAAA.IFoEUQQgAIQwgIwQABAEAAAAOIAACAIAAAAQAIAgEAACEAAAAAgAQBAAAAAAAGBAAgAAAAAAAFAAECAAAgAAQARAEQAAAAAJAAIAAgAAAYQEAAAQmAgBC3ZAYzUw', + vendorData: {} + }, refererInfo: { referer: 'https://test.com' } @@ -131,7 +134,7 @@ describe('MGIDXBidAdapter', function () { 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.gdpr).to.be.a('object'); expect(data.ccpa).to.be.a('string'); expect(data.tmax).to.be.a('number'); expect(data.placements).to.have.lengthOf(3); @@ -172,8 +175,10 @@ describe('MGIDXBidAdapter', function () { 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.gdpr).to.be.a('object'); + expect(data.gdpr).to.have.property('consentString'); + expect(data.gdpr).to.not.have.property('vendorData'); + expect(data.gdpr.consentString).to.equal(bidderRequest.gdprConsent.consentString); expect(data.ccpa).to.not.exist; delete bidderRequest.gdprConsent; }); diff --git a/test/spec/modules/minutemediaBidAdapter_spec.js b/test/spec/modules/minutemediaBidAdapter_spec.js index 0d12e422cd7..7e22114aeed 100644 --- a/test/spec/modules/minutemediaBidAdapter_spec.js +++ b/test/spec/modules/minutemediaBidAdapter_spec.js @@ -104,6 +104,9 @@ describe('minutemediaAdapter', function () { bidderCode: 'minutemedia', } const placementId = '12345678'; + const api = [1, 2]; + const mimes = ['application/javascript', 'video/mp4', 'video/quicktime']; + const protocols = [2, 3, 5, 6]; it('sends the placementId to ENDPOINT via POST', function () { bidRequests[0].params.placementId = placementId; @@ -111,15 +114,22 @@ describe('minutemediaAdapter', function () { expect(request.data.bids[0].placementId).to.equal(placementId); }); - it('sends bid request to ENDPOINT via POST', function () { + it('sends the plcmt to ENDPOINT via POST', function () { const request = spec.buildRequests(bidRequests, bidderRequest); - expect(request.url).to.equal(ENDPOINT); - expect(request.method).to.equal('POST'); + expect(request.data.bids[0].plcmt).to.equal(1); }); - it('sends the plcmt to ENDPOINT via POST', function () { + it('sends the is_wrapper parameter to ENDPOINT via POST', function() { const request = spec.buildRequests(bidRequests, bidderRequest); - expect(request.data.bids[0].plcmt).to.equal(1); + expect(request.data.params).to.be.an('object'); + expect(request.data.params).to.have.property('is_wrapper'); + expect(request.data.params.is_wrapper).to.equal(false); + }); + + it('sends bid request to ENDPOINT via POST', function () { + const request = spec.buildRequests(bidRequests, bidderRequest); + expect(request.url).to.equal(ENDPOINT); + expect(request.method).to.equal('POST'); }); it('sends bid request to TEST ENDPOINT via POST', function () { @@ -133,6 +143,27 @@ describe('minutemediaAdapter', function () { expect(request.data.bids[0].bidId).to.equal('299ffc8cca0b87'); }); + it('should send the correct supported api array', function () { + bidRequests[0].mediaTypes.video.api = api; + const request = spec.buildRequests(bidRequests, bidderRequest); + expect(request.data.bids[0].api).to.be.an('array'); + expect(request.data.bids[0].api).to.eql([1, 2]); + }); + + it('should send the correct mimes array', function () { + bidRequests[1].mediaTypes.banner.mimes = mimes; + const request = spec.buildRequests(bidRequests, bidderRequest); + expect(request.data.bids[1].mimes).to.be.an('array'); + expect(request.data.bids[1].mimes).to.eql(['application/javascript', 'video/mp4', 'video/quicktime']); + }); + + it('should send the correct protocols array', function () { + bidRequests[0].mediaTypes.video.protocols = protocols; + const request = spec.buildRequests(bidRequests, bidderRequest); + expect(request.data.bids[0].protocols).to.be.an('array'); + expect(request.data.bids[0].protocols).to.eql([2, 3, 5, 6]); + }); + it('should send the correct sizes array', function () { const request = spec.buildRequests(bidRequests, bidderRequest); expect(request.data.bids[0].sizes).to.be.an('array'); @@ -147,6 +178,16 @@ describe('minutemediaAdapter', function () { expect(request.data.bids[1].mediaType).to.equal(BANNER) }); + it('should send the correct currency in bid request', function () { + const bid = utils.deepClone(bidRequests[0]); + bid.params = { + 'currency': 'EUR' + }; + const expectedCurrency = bid.params.currency; + const request = spec.buildRequests([bid], bidderRequest); + expect(request.data.bids[0].currency).to.equal(expectedCurrency); + }); + it('should respect syncEnabled option', function() { config.setConfig({ userSync: { diff --git a/test/spec/modules/minutemediaplusBidAdapter_spec.js b/test/spec/modules/minutemediaplusBidAdapter_spec.js index 33ec194c61f..5101f015b0e 100644 --- a/test/spec/modules/minutemediaplusBidAdapter_spec.js +++ b/test/spec/modules/minutemediaplusBidAdapter_spec.js @@ -423,6 +423,25 @@ describe('MinuteMediaPlus Bid Adapter', function () { 'type': 'image' }]); }) + + it('should generate url with consent data', function () { + const gdprConsent = { + gdprApplies: true, + consentString: 'consent_string' + }; + const uspConsent = 'usp_string'; + const gppConsent = { + gppString: 'gpp_string', + applicableSections: [7] + } + + const result = adapter.getUserSyncs({pixelEnabled: true}, [SERVER_RESPONSE], gdprConsent, uspConsent, gppConsent); + + expect(result).to.deep.equal([{ + 'url': 'https://sync.minutemedia-prebid.com/api/sync/image/?cid=testcid123&gdpr=1&gdpr_consent=consent_string&us_privacy=usp_string&gpp=gpp_string&gpp_sid=7', + 'type': 'image' + }]); + }); }); describe('interpret response', function () { diff --git a/test/spec/modules/missenaBidAdapter_spec.js b/test/spec/modules/missenaBidAdapter_spec.js index f61987298e8..f84981352ab 100644 --- a/test/spec/modules/missenaBidAdapter_spec.js +++ b/test/spec/modules/missenaBidAdapter_spec.js @@ -1,23 +1,70 @@ import { expect } from 'chai'; -import { spec, _getPlatform } from 'modules/missenaBidAdapter.js'; -import { newBidder } from 'src/adapters/bidderFactory.js'; +import { spec, storage } from 'modules/missenaBidAdapter.js'; +import { BANNER } from '../../../src/mediaTypes.js'; + +const REFERRER = 'https://referer'; +const REFERRER2 = 'https://referer2'; +const COOKIE_DEPRECATION_LABEL = 'test'; describe('Missena Adapter', function () { - const adapter = newBidder(spec); + $$PREBID_GLOBAL$$.bidderSettings = { + missena: { + storageAllowed: true, + }, + }; const bidId = 'abc'; - const bid = { bidder: 'missena', bidId: bidId, sizes: [[1, 1]], + mediaTypes: { banner: { sizes: [[1, 1]] } }, + ortb2: { + device: { + ext: { cdep: COOKIE_DEPRECATION_LABEL }, + }, + }, params: { apiKey: 'PA-34745704', placement: 'sticky', formats: ['sticky-banner'], }, + getFloor: (inputParams) => { + if (inputParams.mediaType === BANNER) { + return { + currency: 'EUR', + floor: 3.5, + }; + } else { + return {}; + } + }, }; + const bidWithoutFloor = { + bidder: 'missena', + bidId: bidId, + sizes: [[1, 1]], + mediaTypes: { banner: { sizes: [[1, 1]] } }, + params: { + apiKey: 'PA-34745704', + placement: 'sticky', + formats: ['sticky-banner'], + }, + }; + const consentString = 'AAAAAAAAA=='; + const bidderRequest = { + gdprConsent: { + consentString: consentString, + gdprApplies: true, + }, + refererInfo: { + topmostLocation: REFERRER, + canonicalUrl: 'https://canonical', + }, + }; + + const bids = [bid, bidWithoutFloor]; describe('codes', function () { it('should return a bidder code of missena', function () { expect(spec.code).to.equal('missena'); @@ -31,34 +78,27 @@ describe('Missena Adapter', function () { it('should return false if the apiKey is missing', function () { expect( - spec.isBidRequestValid(Object.assign(bid, { params: {} })) + spec.isBidRequestValid(Object.assign(bid, { params: {} })), ).to.equal(false); }); it('should return false if the apiKey is an empty string', function () { expect( - spec.isBidRequestValid(Object.assign(bid, { params: { apiKey: '' } })) + spec.isBidRequestValid(Object.assign(bid, { params: { apiKey: '' } })), ).to.equal(false); }); }); describe('buildRequests', function () { - const consentString = 'AAAAAAAAA=='; + let getDataFromLocalStorageStub = sinon.stub( + storage, + 'getDataFromLocalStorage', + ); - const bidderRequest = { - gdprConsent: { - consentString: consentString, - gdprApplies: true, - }, - refererInfo: { - topmostLocation: 'https://referer', - canonicalUrl: 'https://canonical', - }, - }; - - const requests = spec.buildRequests([bid, bid], bidderRequest); + const requests = spec.buildRequests(bids, bidderRequest); const request = requests[0]; const payload = JSON.parse(request.data); + const payloadNoFloor = JSON.parse(requests[1].data); it('should return as many server requests as bidder requests', function () { expect(requests.length).to.equal(2); @@ -81,7 +121,7 @@ describe('Missena Adapter', function () { }); it('should send referer information to the request', function () { - expect(payload.referer).to.equal('https://referer'); + expect(payload.referer).to.equal(REFERRER); expect(payload.referer_canonical).to.equal('https://canonical'); }); @@ -89,6 +129,74 @@ describe('Missena Adapter', function () { expect(payload.consent_string).to.equal(consentString); expect(payload.consent_required).to.equal(true); }); + it('should send floor data', function () { + expect(payload.floor).to.equal(3.5); + expect(payload.floor_currency).to.equal('EUR'); + }); + it('should not send floor data if not available', function () { + expect(payloadNoFloor.floor).to.equal(undefined); + expect(payloadNoFloor.floor_currency).to.equal(undefined); + }); + + getDataFromLocalStorageStub.restore(); + getDataFromLocalStorageStub = sinon.stub( + storage, + 'getDataFromLocalStorage', + ); + const localStorageData = { + [`missena.missena.capper.remove-bubble.${bid.params.apiKey}`]: + JSON.stringify({ + expiry: new Date().getTime() + 600_000, // 10 min into the future + }), + }; + getDataFromLocalStorageStub.callsFake((key) => localStorageData[key]); + const cappedRequests = spec.buildRequests(bids, bidderRequest); + + it('should not participate if capped', function () { + expect(cappedRequests.length).to.equal(0); + }); + + const localStorageDataSamePage = { + [`missena.missena.capper.remove-bubble.${bid.params.apiKey}`]: + JSON.stringify({ + expiry: new Date().getTime() + 600_000, // 10 min into the future + referer: REFERRER, + }), + }; + + getDataFromLocalStorageStub.callsFake( + (key) => localStorageDataSamePage[key], + ); + const cappedRequestsSamePage = spec.buildRequests(bids, bidderRequest); + + it('should not participate if capped on same page', function () { + expect(cappedRequestsSamePage.length).to.equal(0); + }); + + const localStorageDataOtherPage = { + [`missena.missena.capper.remove-bubble.${bid.params.apiKey}`]: + JSON.stringify({ + expiry: new Date().getTime() + 600_000, // 10 min into the future + referer: REFERRER2, + }), + }; + + getDataFromLocalStorageStub.callsFake( + (key) => localStorageDataOtherPage[key], + ); + const cappedRequestsOtherPage = spec.buildRequests(bids, bidderRequest); + + it('should participate if capped on a different page', function () { + expect(cappedRequestsOtherPage.length).to.equal(2); + }); + + it('should send the prebid version', function () { + expect(payload.version).to.equal('$prebid.version$'); + }); + + it('should send cookie deprecation', function () { + expect(payload.cdep).to.equal(COOKIE_DEPRECATION_LABEL); + }); }); describe('interpretResponse', function () { @@ -121,14 +229,14 @@ describe('Missena Adapter', function () { expect(result.length).to.equal(1); expect(Object.keys(result[0])).to.have.members( - Object.keys(serverResponse) + Object.keys(serverResponse), ); }); it('should return an empty response when the server answers with a timeout', function () { const result = spec.interpretResponse( { body: serverTimeoutResponse }, - bid + bid, ); expect(result).to.deep.equal([]); }); @@ -136,7 +244,7 @@ describe('Missena Adapter', function () { it('should return an empty response when the server answers with an empty ad', function () { const result = spec.interpretResponse( { body: serverEmptyAdResponse }, - bid + bid, ); expect(result).to.deep.equal([]); }); diff --git a/test/spec/modules/mobfoxpbBidAdapter_spec.js b/test/spec/modules/mobfoxpbBidAdapter_spec.js index 766f8d1a848..a4e58afbd1b 100644 --- a/test/spec/modules/mobfoxpbBidAdapter_spec.js +++ b/test/spec/modules/mobfoxpbBidAdapter_spec.js @@ -20,7 +20,8 @@ describe('MobfoxHBBidAdapter', function () { const bidderRequest = { refererInfo: { referer: 'test.com' - } + }, + ortb2: {} }; describe('isBidRequestValid', function () { @@ -143,6 +144,36 @@ describe('MobfoxHBBidAdapter', function () { expect(data.placements).to.be.an('array').that.is.empty; }); }); + + describe('gpp consent', function () { + it('bidderRequest.gppConsent', () => { + bidderRequest.gppConsent = { + gppString: 'abc123', + applicableSections: [8] + }; + + let serverRequest = spec.buildRequests([bid], bidderRequest); + let data = serverRequest.data; + expect(data).to.be.an('object'); + expect(data).to.have.property('gpp'); + expect(data).to.have.property('gpp_sid'); + + delete bidderRequest.gppConsent; + }) + + it('bidderRequest.ortb2.regs.gpp', () => { + bidderRequest.ortb2.regs = bidderRequest.ortb2.regs || {}; + bidderRequest.ortb2.regs.gpp = 'abc123'; + bidderRequest.ortb2.regs.gpp_sid = [8]; + + let serverRequest = spec.buildRequests([bid], bidderRequest); + let data = serverRequest.data; + expect(data).to.be.an('object'); + expect(data).to.have.property('gpp'); + expect(data).to.have.property('gpp_sid'); + }) + }); + describe('interpretResponse', function () { it('Should interpret banner response', function () { const banner = { diff --git a/test/spec/modules/multibid_spec.js b/test/spec/modules/multibid_spec.js index eaf8fa33a66..c11113473ce 100644 --- a/test/spec/modules/multibid_spec.js +++ b/test/spec/modules/multibid_spec.js @@ -1,16 +1,15 @@ import {expect} from 'chai'; import { - validateMultibid, - adjustBidderRequestsHook, addBidResponseHook, + adjustBidderRequestsHook, resetMultibidUnits, + resetMultiConfig, sortByMultibid, targetBidPoolHook, - resetMultiConfig + validateMultibid } from 'modules/multibid/index.js'; -import {parse as parseQuery} from 'querystring'; import {config} from 'src/config.js'; -import * as utils from 'src/utils.js'; +import {getHighestCpm} from '../../../src/utils/reducers.js'; describe('multibid adapter', function () { let bidArray = [{ @@ -545,7 +544,7 @@ describe('multibid adapter', function () { it('it does not run filter on bidsReceived if no multibid configuration found', function () { let bids = [{...bidArray[0]}, {...bidArray[1]}]; - targetBidPoolHook(callbackFn, bids, utils.getHighestCpm); + targetBidPoolHook(callbackFn, bids, getHighestCpm); expect(result).to.not.equal(null); expect(result.bidsReceived).to.not.equal(null); @@ -562,7 +561,7 @@ describe('multibid adapter', function () { config.setConfig({multibid: [{bidder: 'bidderA', maxBids: 2}]}); - targetBidPoolHook(callbackFn, bids, utils.getHighestCpm); + targetBidPoolHook(callbackFn, bids, getHighestCpm); bids.pop(); expect(result).to.not.equal(null); @@ -584,7 +583,7 @@ describe('multibid adapter', function () { config.setConfig({multibid: [{bidder: 'bidderA', maxBids: 2, targetBiddercodePrefix: 'bidA'}]}); - targetBidPoolHook(callbackFn, modifiedBids, utils.getHighestCpm); + targetBidPoolHook(callbackFn, modifiedBids, getHighestCpm); expect(result).to.not.equal(null); expect(result.bidsReceived).to.not.equal(null); @@ -609,7 +608,7 @@ describe('multibid adapter', function () { config.setConfig({multibid: [{bidder: 'bidderA', maxBids: 2, targetBiddercodePrefix: 'bidA'}]}); - targetBidPoolHook(callbackFn, modifiedBids, utils.getHighestCpm); + targetBidPoolHook(callbackFn, modifiedBids, getHighestCpm); expect(result).to.not.equal(null); expect(result.bidsReceived).to.not.equal(null); @@ -642,7 +641,7 @@ describe('multibid adapter', function () { config.setConfig({ multibid: [{bidder: 'bidderA', maxBids: 2, targetBiddercodePrefix: 'bidA'}] }); - targetBidPoolHook(callbackFn, modifiedBids, utils.getHighestCpm, 3); + targetBidPoolHook(callbackFn, modifiedBids, getHighestCpm, 3); expect(result).to.not.equal(null); expect(result.bidsReceived).to.not.equal(null); @@ -670,7 +669,7 @@ describe('multibid adapter', function () { expect(bidPool.length).to.equal(6); - targetBidPoolHook(callbackFn, bidPool, utils.getHighestCpm); + targetBidPoolHook(callbackFn, bidPool, getHighestCpm); expect(result).to.not.equal(null); expect(result.bidsReceived).to.not.equal(null); diff --git a/test/spec/modules/mygaruIdSystem_spec.js b/test/spec/modules/mygaruIdSystem_spec.js new file mode 100644 index 00000000000..2bfb5fdd4af --- /dev/null +++ b/test/spec/modules/mygaruIdSystem_spec.js @@ -0,0 +1,62 @@ +import { mygaruIdSubmodule } from 'modules/mygaruIdSystem.js'; +import { server } from '../../mocks/xhr'; + +describe('MygaruID module', function () { + it('should respond with async callback and get valid id', async () => { + const callBackSpy = sinon.spy(); + const expectedUrl = `https://ident.mygaru.com/v2/id?gdprApplies=0`; + const result = mygaruIdSubmodule.getId({}); + + expect(result.callback).to.be.an('function'); + const promise = result.callback(callBackSpy); + + const request = server.requests[0]; + expect(request.url).to.be.eq(expectedUrl); + + request.respond( + 200, + { 'Content-Type': 'application/json' }, + JSON.stringify({ iuid: '123' }) + ); + await promise; + + expect(callBackSpy.calledOnce).to.be.true; + expect(callBackSpy.calledWith({mygaruId: '123'})).to.be.true; + }); + it('should not fail on error', async () => { + const callBackSpy = sinon.spy(); + const expectedUrl = `https://ident.mygaru.com/v2/id?gdprApplies=0`; + const result = mygaruIdSubmodule.getId({}); + + expect(result.callback).to.be.an('function'); + const promise = result.callback(callBackSpy); + + const request = server.requests[0]; + expect(request.url).to.be.eq(expectedUrl); + + request.respond( + 500, + {}, + '' + ); + await promise; + + expect(callBackSpy.calledOnce).to.be.true; + expect(callBackSpy.calledWith({mygaruId: undefined})).to.be.true; + }); + + it('should not modify while decoding', () => { + const id = '222'; + const newId = mygaruIdSubmodule.decode(id) + + expect(id).to.eq(newId); + }) + it('should buildUrl with consent data', () => { + const result = mygaruIdSubmodule.getId({}, { + gdprApplies: true, + consentString: 'consentString' + }); + + expect(result.url).to.eq('https://ident.mygaru.com/v2/id?gdprApplies=1&gdprConsentString=consentString'); + }) +}); diff --git a/test/spec/modules/nativoBidAdapter_spec.js b/test/spec/modules/nativoBidAdapter_spec.js index 51e78d1f6d6..75fb357b196 100644 --- a/test/spec/modules/nativoBidAdapter_spec.js +++ b/test/spec/modules/nativoBidAdapter_spec.js @@ -112,7 +112,7 @@ describe('nativoBidAdapterTests', function () { bidRequests = [JSON.parse(bidRequestString)] }) - it('url should contain query string parameters', function () { + it('Request should be POST, with JSON string payload and QS params should be added to the url', function () { const request = spec.buildRequests(bidRequests, { bidderRequestId: 123456, refererInfo: { @@ -120,6 +120,11 @@ describe('nativoBidAdapterTests', function () { }, }) + expect(request.method).to.equal('POST') + + expect(request.data).to.exist + expect(request.data).to.be.a('string') + expect(request.url).to.exist expect(request.url).to.be.a('string') diff --git a/test/spec/modules/newspassidBidAdapter_spec.js b/test/spec/modules/newspassidBidAdapter_spec.js index bec6eea7bf2..6468d4f530a 100644 --- a/test/spec/modules/newspassidBidAdapter_spec.js +++ b/test/spec/modules/newspassidBidAdapter_spec.js @@ -1667,6 +1667,12 @@ describe('newspassid Adapter', function () { expect(result[0]['price']).to.equal(0.9); expect(result[0]['adserverTargeting']['np_npappnexus_adId']).to.equal('2899ec066a91ff8-0-np-1'); }); + it('should add np_auc_id (response id value)', function () { + const request = spec.buildRequests(validBidRequests, validBidderRequest); + let validres = JSON.parse(JSON.stringify(validBidResponse1adWith2Bidders)); + const result = spec.interpretResponse(validres, request); + expect(utils.deepAccess(result[0].adserverTargeting, 'np_auc_id')).to.equal(validBidResponse1adWith2Bidders.body.id); + }); it('should correctly process an auction with 2 adunits & multiple bidders one of which bids for both adslots', function() { let validres = JSON.parse(JSON.stringify(multiResponse1)); let request = spec.buildRequests(multiRequest1, multiBidderRequest1.bidderRequest); diff --git a/test/spec/modules/nextMillenniumBidAdapter_spec.js b/test/spec/modules/nextMillenniumBidAdapter_spec.js index 564788c8b56..169ab7b01e4 100644 --- a/test/spec/modules/nextMillenniumBidAdapter_spec.js +++ b/test/spec/modules/nextMillenniumBidAdapter_spec.js @@ -1,32 +1,528 @@ import { expect } from 'chai'; -import { spec } from 'modules/nextMillenniumBidAdapter.js'; +import { + getImp, + replaceUsersyncMacros, + setConsentStrings, + setOrtb2Parameters, + setEids, + spec, +} from 'modules/nextMillenniumBidAdapter.js'; + +describe('nextMillenniumBidAdapterTests', () => { + describe('function getImp', () => { + const dataTests = [ + { + title: 'imp - banner', + data: { + id: '123', + bid: { + mediaTypes: {banner: {sizes: [[300, 250], [320, 250]]}}, + adUnitCode: 'test-banner-1', + }, + + mediaTypes: { + banner: { + data: {sizes: [[300, 250], [320, 250]]}, + bidfloorcur: 'EUR', + bidfloor: 1.11, + }, + }, + }, -describe('nextMillenniumBidAdapterTests', function() { - const bidRequestData = [ - { - adUnitCode: 'test-div', - bidId: 'bid1234', - auctionId: 'b06c5141-fe8f-4cdf-9d7d-54415490a917', - bidder: 'nextMillennium', - params: { placement_id: '-1' }, - sizes: [[300, 250]], - uspConsent: '1---', - gdprConsent: { - consentString: 'kjfdniwjnifwenrif3', - gdprApplies: true + expected: { + id: 'test-banner-1', + bidfloorcur: 'EUR', + bidfloor: 1.11, + ext: {prebid: {storedrequest: {id: '123'}}}, + banner: {format: [{w: 300, h: 250}, {w: 320, h: 250}]}, + }, }, - ortb2: { - device: { - w: 1500, - h: 1000 + + { + title: 'imp - video', + data: { + id: '234', + bid: { + mediaTypes: {video: {playerSize: [400, 300]}}, + adUnitCode: 'test-video-1', + }, + + mediaTypes: { + video: { + data: {playerSize: [400, 300]}, + bidfloorcur: 'USD', + }, + }, }, - site: { - domain: 'example.com', - page: 'http://example.com' - } + + expected: { + id: 'test-video-1', + bidfloorcur: 'USD', + ext: {prebid: {storedrequest: {id: '234'}}}, + video: {w: 400, h: 300}, + }, + }, + ]; + + for (let {title, data, expected} of dataTests) { + it(title, () => { + const {bid, id, mediaTypes} = data; + const imp = getImp(bid, id, mediaTypes); + expect(imp).to.deep.equal(expected); + }); + } + }); + + describe('function setConsentStrings', () => { + const dataTests = [ + { + title: 'full: uspConsent, gdprConsent and gppConsent', + data: { + postBody: {}, + bidderRequest: { + uspConsent: '1---', + gppConsent: {gppString: 'DBACNYA~CPXxRfAPXxR', applicableSections: [7]}, + gdprConsent: {consentString: 'kjfdniwjnifwenrif3', gdprApplies: true}, + ortb2: {regs: {gpp: 'DSFHFHWEUYVDC', gpp_sid: [8, 9, 10]}}, + }, + }, + + expected: { + user: {ext: {consent: 'kjfdniwjnifwenrif3'}}, + regs: { + gpp: 'DBACNYA~CPXxRfAPXxR', + gpp_sid: [7], + ext: {gdpr: 1, us_privacy: '1---'}, + }, + }, + }, + + { + title: 'gdprConsent(false) and ortb2(gpp)', + data: { + postBody: {}, + bidderRequest: { + gdprConsent: {consentString: 'ewtewbefbawyadexv', gdprApplies: false}, + ortb2: {regs: {gpp: 'DSFHFHWEUYVDC', gpp_sid: [8, 9, 10]}}, + }, + }, + + expected: { + user: {ext: {consent: 'ewtewbefbawyadexv'}}, + regs: { + gpp: 'DSFHFHWEUYVDC', + gpp_sid: [8, 9, 10], + ext: {gdpr: 0}, + }, + }, + }, + + { + title: 'gdprConsent(false)', + data: { + postBody: {}, + bidderRequest: {gdprConsent: {gdprApplies: false}}, + }, + + expected: { + regs: {ext: {gdpr: 0}}, + }, + }, + + { + title: 'empty', + data: { + postBody: {}, + bidderRequest: {}, + }, + + expected: {}, + }, + ]; + + for (let {title, data, expected} of dataTests) { + it(title, () => { + const {postBody, bidderRequest} = data; + setConsentStrings(postBody, bidderRequest); + expect(postBody).to.deep.equal(expected); + }); + } + }); + + describe('function replaceUsersyncMacros', () => { + const dataTests = [ + { + title: 'url with all macroses - consents full: uspConsent, gdprConsent and gppConsent', + data: { + url: 'https://some.url?gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&gpp={{.GPP}}&gpp_sid={{.GPPSID}}&type={{.TYPE_PIXEL}}', + uspConsent: '1---', + gppConsent: {gppString: 'DBACNYA~CPXxRfAPXxR', applicableSections: [7, 8]}, + gdprConsent: {consentString: 'kjfdniwjnifwenrif3', gdprApplies: true}, + type: 'image', + }, + + expected: 'https://some.url?gdpr=1&gdpr_consent=kjfdniwjnifwenrif3&us_privacy=1---&gpp=DBACNYA~CPXxRfAPXxR&gpp_sid=7,8&type=image', + }, + + { + title: 'url with some macroses - consents full: uspConsent, gdprConsent and gppConsent', + data: { + url: 'https://some.url?gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&type={{.TYPE_PIXEL}}', + uspConsent: '1---', + gppConsent: {gppString: 'DBACNYA~CPXxRfAPXxR', applicableSections: [7, 8]}, + gdprConsent: {consentString: 'kjfdniwjnifwenrif3', gdprApplies: false}, + type: 'iframe', + }, + + expected: 'https://some.url?gdpr=0&gdpr_consent=kjfdniwjnifwenrif3&type=iframe', + }, + + { + title: 'url without macroses - consents full: uspConsent, gdprConsent and gppConsent', + data: { + url: 'https://some.url?param1=value1¶m2=value2', + uspConsent: '1---', + gppConsent: {gppString: 'DBACNYA~CPXxRfAPXxR', applicableSections: [7, 8]}, + gdprConsent: {consentString: 'kjfdniwjnifwenrif3', gdprApplies: false}, + type: 'iframe', + }, + + expected: 'https://some.url?param1=value1¶m2=value2', + }, + + { + title: 'url with all macroses - consents are empty', + data: { + url: 'https://some.url?gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&gpp={{.GPP}}&gpp_sid={{.GPPSID}}&type={{.TYPE_PIXEL}}', + }, + + expected: 'https://some.url?gdpr=0&gdpr_consent=&us_privacy=&gpp=&gpp_sid=&type=', + }, + ]; + + for (let {title, data, expected} of dataTests) { + it(title, () => { + const {url, gdprConsent, uspConsent, gppConsent, type} = data; + const newUrl = replaceUsersyncMacros(url, gdprConsent, uspConsent, gppConsent, type); + expect(newUrl).to.equal(expected); + }); + } + }); + + describe('function spec.getUserSyncs', () => { + const dataTests = [ + { + title: 'pixels from responses ({iframeEnabled: true, pixelEnabled: true})', + data: { + syncOptions: {iframeEnabled: true, pixelEnabled: true}, + responses: [ + {body: {ext: {sync: { + image: [ + 'https://some.1.url?gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&gpp={{.GPP}}&gpp_sid={{.GPPSID}}', + 'https://some.2.url?us_privacy={{.USPrivacy}}&gpp={{.GPP}}&gpp_sid={{.GPPSID}}', + 'https://some.3.url?param=1234', + ], + + iframe: [ + 'https://some.4.url?gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&gpp={{.GPP}}&gpp_sid={{.GPPSID}}', + 'https://some.5.url?gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}', + ], + }}}}, + + {body: {ext: {sync: { + iframe: [ + 'https://some.6.url?gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&gpp={{.GPP}}&gpp_sid={{.GPPSID}}', + 'https://some.7.url?gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}', + ], + }}}}, + + {body: {ext: {sync: { + image: [ + 'https://some.8.url?gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&gpp={{.GPP}}&gpp_sid={{.GPPSID}}', + ], + }}}}, + ], + + uspConsent: '1---', + gppConsent: {gppString: 'DBACNYA~CPXxRfAPXxR', applicableSections: [7, 8]}, + gdprConsent: {consentString: 'kjfdniwjnifwenrif3', gdprApplies: true}, + }, + + expected: [ + {type: 'image', url: 'https://some.1.url?gdpr=1&gdpr_consent=kjfdniwjnifwenrif3&us_privacy=1---&gpp=DBACNYA~CPXxRfAPXxR&gpp_sid=7,8'}, + {type: 'image', url: 'https://some.2.url?us_privacy=1---&gpp=DBACNYA~CPXxRfAPXxR&gpp_sid=7,8'}, + {type: 'image', url: 'https://some.3.url?param=1234'}, + {type: 'iframe', url: 'https://some.4.url?gdpr=1&gdpr_consent=kjfdniwjnifwenrif3&gpp=DBACNYA~CPXxRfAPXxR&gpp_sid=7,8'}, + {type: 'iframe', url: 'https://some.5.url?gdpr=1&gdpr_consent=kjfdniwjnifwenrif3&us_privacy=1---'}, + {type: 'iframe', url: 'https://some.6.url?gdpr=1&gdpr_consent=kjfdniwjnifwenrif3&gpp=DBACNYA~CPXxRfAPXxR&gpp_sid=7,8'}, + {type: 'iframe', url: 'https://some.7.url?gdpr=1&gdpr_consent=kjfdniwjnifwenrif3&us_privacy=1---'}, + {type: 'image', url: 'https://some.8.url?gdpr=1&gdpr_consent=kjfdniwjnifwenrif3&us_privacy=1---&gpp=DBACNYA~CPXxRfAPXxR&gpp_sid=7,8'}, + ], + }, + + { + title: 'pixels from responses ({iframeEnabled: true, pixelEnabled: false})', + data: { + syncOptions: {iframeEnabled: true, pixelEnabled: false}, + responses: [ + {body: {ext: {sync: { + image: [ + 'https://some.1.url?gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&gpp={{.GPP}}&gpp_sid={{.GPPSID}}', + 'https://some.2.url?us_privacy={{.USPrivacy}}&gpp={{.GPP}}&gpp_sid={{.GPPSID}}', + 'https://some.3.url?param=1234', + ], + + iframe: [ + 'https://some.4.url?gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&gpp={{.GPP}}&gpp_sid={{.GPPSID}}', + 'https://some.5.url?gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}', + ], + }}}}, + ], + + uspConsent: '1---', + gppConsent: {gppString: 'DBACNYA~CPXxRfAPXxR', applicableSections: [7, 8]}, + gdprConsent: {consentString: 'kjfdniwjnifwenrif3', gdprApplies: true}, + }, + + expected: [ + {type: 'iframe', url: 'https://some.4.url?gdpr=1&gdpr_consent=kjfdniwjnifwenrif3&gpp=DBACNYA~CPXxRfAPXxR&gpp_sid=7,8'}, + {type: 'iframe', url: 'https://some.5.url?gdpr=1&gdpr_consent=kjfdniwjnifwenrif3&us_privacy=1---'}, + ], + }, + + { + title: 'pixels from responses ({iframeEnabled: false, pixelEnabled: true})', + data: { + syncOptions: {iframeEnabled: false, pixelEnabled: true}, + responses: [ + {body: {ext: {sync: { + image: [ + 'https://some.1.url?gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}&gpp={{.GPP}}&gpp_sid={{.GPPSID}}', + 'https://some.2.url?us_privacy={{.USPrivacy}}&gpp={{.GPP}}&gpp_sid={{.GPPSID}}', + 'https://some.3.url?param=1234', + ], + + iframe: [ + 'https://some.4.url?gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&gpp={{.GPP}}&gpp_sid={{.GPPSID}}', + 'https://some.5.url?gdpr={{.GDPR}}&gdpr_consent={{.GDPRConsent}}&us_privacy={{.USPrivacy}}', + ], + }}}}, + ], + + uspConsent: '1---', + gppConsent: {gppString: 'DBACNYA~CPXxRfAPXxR', applicableSections: [7, 8]}, + gdprConsent: {consentString: 'kjfdniwjnifwenrif3', gdprApplies: true}, + }, + + expected: [ + {type: 'image', url: 'https://some.1.url?gdpr=1&gdpr_consent=kjfdniwjnifwenrif3&us_privacy=1---&gpp=DBACNYA~CPXxRfAPXxR&gpp_sid=7,8'}, + {type: 'image', url: 'https://some.2.url?us_privacy=1---&gpp=DBACNYA~CPXxRfAPXxR&gpp_sid=7,8'}, + {type: 'image', url: 'https://some.3.url?param=1234'}, + ], + }, + + { + title: 'pixels - responses is empty ({iframeEnabled: true, pixelEnabled: true})', + data: { + syncOptions: {iframeEnabled: true, pixelEnabled: true}, + responses: [], + uspConsent: '1---', + gppConsent: {gppString: 'DBACNYA~CPXxRfAPXxR', applicableSections: [7, 8]}, + gdprConsent: {consentString: 'kjfdniwjnifwenrif3', gdprApplies: true}, + }, + + expected: [ + {type: 'image', url: 'https://cookies.nextmillmedia.com/sync?gdpr=1&gdpr_consent=kjfdniwjnifwenrif3&us_privacy=1---&gpp=DBACNYA~CPXxRfAPXxR&gpp_sid=7,8&type=image'}, + {type: 'iframe', url: 'https://cookies.nextmillmedia.com/sync?gdpr=1&gdpr_consent=kjfdniwjnifwenrif3&us_privacy=1---&gpp=DBACNYA~CPXxRfAPXxR&gpp_sid=7,8&type=iframe'}, + ], + }, + + { + title: 'pixels - responses is empty ({iframeEnabled: true, pixelEnabled: false})', + data: { + syncOptions: {iframeEnabled: true, pixelEnabled: false}, + uspConsent: '1---', + gppConsent: {gppString: 'DBACNYA~CPXxRfAPXxR', applicableSections: [7, 8]}, + gdprConsent: {consentString: 'kjfdniwjnifwenrif3', gdprApplies: true}, + }, + + expected: [ + {type: 'iframe', url: 'https://cookies.nextmillmedia.com/sync?gdpr=1&gdpr_consent=kjfdniwjnifwenrif3&us_privacy=1---&gpp=DBACNYA~CPXxRfAPXxR&gpp_sid=7,8&type=iframe'}, + ], + }, + + { + title: 'pixels - responses is empty ({iframeEnabled: false, pixelEnabled: false})', + data: { + syncOptions: {iframeEnabled: false, pixelEnabled: false}, + uspConsent: '1---', + gppConsent: {gppString: 'DBACNYA~CPXxRfAPXxR', applicableSections: [7, 8]}, + gdprConsent: {consentString: 'kjfdniwjnifwenrif3', gdprApplies: true}, + }, + + expected: [], + }, + ]; + + for (let {title, data, expected} of dataTests) { + it(title, () => { + const {syncOptions, responses, gdprConsent, uspConsent, gppConsent} = data; + const pixels = spec.getUserSyncs(syncOptions, responses, gdprConsent, uspConsent, gppConsent); + expect(pixels).to.deep.equal(expected); + }); + } + }); + + describe('function setOrtb2Parameters', () => { + const dataTests = [ + { + title: 'site.pagecat, site.content.cat and site.content.language', + data: { + postBody: {}, + ortb2: {site: { + pagecat: ['IAB2-11', 'IAB2-12', 'IAB2-14'], + content: {cat: ['IAB2-11', 'IAB2-12', 'IAB2-14'], language: 'EN'}, + }}, + }, + + expected: {site: { + pagecat: ['IAB2-11', 'IAB2-12', 'IAB2-14'], + content: {cat: ['IAB2-11', 'IAB2-12', 'IAB2-14'], language: 'EN'}, + }}, + }, + + { + title: 'only site.content.language', + data: { + postBody: {site: {domain: 'some.domain'}}, + ortb2: {site: { + content: {language: 'EN'}, + }}, + }, + + expected: {site: { + domain: 'some.domain', + content: {language: 'EN'}, + }}, + }, + + { + title: 'object ortb2 is empty', + data: { + postBody: {imp: []}, + }, + + expected: {imp: []}, + }, + ]; + + for (let {title, data, expected} of dataTests) { + it(title, () => { + const {postBody, ortb2} = data; + setOrtb2Parameters(postBody, ortb2); + expect(postBody).to.deep.equal(expected); + }); + }; + }); + + describe('function setEids', () => { + const dataTests = [ + { + title: 'setEids - userIdAsEids is empty', + data: { + postBody: {}, + bid: { + userIdAsEids: undefined, + }, + }, + + expected: {}, + }, + + { + title: 'setEids - userIdAsEids - array is empty', + data: { + postBody: {}, + bid: { + userIdAsEids: [], + }, + }, + + expected: {}, + }, + + { + title: 'setEids - userIdAsEids is', + data: { + postBody: {}, + bid: { + userIdAsEids: [ + { + source: '33across.com', + uids: [{id: 'some-random-id-value', atype: 1}], + }, + + { + source: 'utiq.com', + uids: [{id: 'some-random-id-value', atype: 1}], + }, + ], + }, + }, + + expected: { + user: { + eids: [ + { + source: '33across.com', + uids: [{id: 'some-random-id-value', atype: 1}], + }, + + { + source: 'utiq.com', + uids: [{id: 'some-random-id-value', atype: 1}], + }, + ], + }, + }, + }, + ]; + + for (let { title, data, expected } of dataTests) { + it(title, () => { + const { postBody, bid } = data; + setEids(postBody, bid); + expect(postBody).to.deep.equal(expected); + }); + } + }); + + const bidRequestData = [{ + adUnitCode: 'test-div', + bidId: 'bid1234', + auctionId: 'b06c5141-fe8f-4cdf-9d7d-54415490a917', + bidder: 'nextMillennium', + params: { placement_id: '-1' }, + sizes: [[300, 250]], + uspConsent: '1---', + gppConsent: {gppString: 'DBACNYA~CPXxRfAPXxR', applicableSections: [7]}, + gdprConsent: { + consentString: 'kjfdniwjnifwenrif3', + gdprApplies: true + }, + + ortb2: { + device: { + w: 1500, + h: 1000 + }, + + site: { + domain: 'example.com', + page: 'http://example.com' } } - ]; + }]; const serverResponse = { body: { @@ -49,7 +545,7 @@ describe('nextMillenniumBidAdapterTests', function() { cur: 'USD', ext: { sync: { - image: ['urlA?gdpr={{.GDPR}}'], + image: ['urlA?gdpr={{.GDPR}}&gpp={{.GPP}}&gpp_sid={{.GPPSID}}'], iframe: ['urlB'], } } @@ -117,61 +613,6 @@ describe('nextMillenniumBidAdapterTests', function() { }, ]; - it('Request params check with GDPR and USP Consent', function () { - const request = spec.buildRequests(bidRequestData, bidRequestData[0]); - expect(JSON.parse(request[0].data).user.ext.consent).to.equal('kjfdniwjnifwenrif3'); - expect(JSON.parse(request[0].data).regs.ext.us_privacy).to.equal('1---'); - 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]); - expect(JSON.parse(request[0].data).regs.ext.gdpr).to.be.undefined; - expect(JSON.parse(request[0].data).regs.ext.us_privacy).to.equal('1---'); - }); - it('validate_generated_params', function() { const request = spec.buildRequests(bidRequestData, {bidderRequestId: 'mock-uuid'}); expect(request[0].bidId).to.equal('bid1234'); @@ -190,7 +631,7 @@ describe('nextMillenniumBidAdapterTests', function() { it('Check if refresh_count param is incremented', function() { const request = spec.buildRequests(bidRequestData); - expect(JSON.parse(request[0].data).ext.nextMillennium.refresh_count).to.equal(3); + expect(JSON.parse(request[0].data).ext.nextMillennium.refresh_count).to.equal(1); }); it('Check if domain was added', function() { diff --git a/test/spec/modules/nexx360BidAdapter_spec.js b/test/spec/modules/nexx360BidAdapter_spec.js index 7c2cea99a46..7091bb56631 100644 --- a/test/spec/modules/nexx360BidAdapter_spec.js +++ b/test/spec/modules/nexx360BidAdapter_spec.js @@ -374,7 +374,7 @@ describe('Nexx360 bid adapter tests', function () { expect(requestContent.imp[1].tagid).to.be.eql('div-2-abcd'); expect(requestContent.imp[1].ext.adUnitCode).to.be.eql('div-2-abcd'); expect(requestContent.imp[1].ext.divId).to.be.eql('div-2-abcd'); - expect(requestContent.ext.bidderVersion).to.be.eql('2.0'); + expect(requestContent.ext.bidderVersion).to.be.eql('3.0'); expect(requestContent.ext.source).to.be.eql('prebid.js'); }); @@ -434,7 +434,7 @@ describe('Nexx360 bid adapter tests', function () { const output = spec.interpretResponse(response); expect(output.length).to.be.eql(0); }); - it('banner responses', function() { + it('banner responses with adUrl only', function() { const response = { body: { 'id': 'a8d3a675-a4ba-4d26-807f-c8f2fad821e0', @@ -479,6 +479,53 @@ describe('Nexx360 bid adapter tests', function () { expect(output[0].currency).to.be.eql(response.body.cur); expect(output[0].cpm).to.be.eql(response.body.seatbid[0].bid[0].price); }); + it('banner responses with adm', function() { + const response = { + body: { + 'id': 'a8d3a675-a4ba-4d26-807f-c8f2fad821e0', + 'cur': 'USD', + 'seatbid': [ + { + 'bid': [ + { + 'id': '4427551302944024629', + 'impid': '226175918ebeda', + 'price': 1.5, + 'adomain': [ + 'http://prebid.org' + ], + 'crid': '98493581', + 'ssp': 'appnexus', + 'h': 600, + 'w': 300, + 'adm': '
TestAd
', + 'cat': [ + 'IAB3-1' + ], + 'ext': { + 'adUnitCode': 'div-1', + 'mediaType': 'banner', + 'adUrl': 'https://fast.nexx360.io/cache?uuid=fdddcebc-1edf-489d-880d-1418d8bdc493', + 'ssp': 'appnexus', + } + } + ], + 'seat': 'appnexus' + } + ], + 'ext': { + 'id': 'de3de7c7-e1cf-4712-80a9-94eb26bfc718', + 'cookies': [] + }, + } + }; + const output = spec.interpretResponse(response); + expect(output[0].ad).to.be.eql(response.body.seatbid[0].bid[0].adm); + expect(output[0].adUrl).to.be.eql(undefined); + expect(output[0].mediaType).to.be.eql(response.body.seatbid[0].bid[0].ext.mediaType); + expect(output[0].currency).to.be.eql(response.body.cur); + expect(output[0].cpm).to.be.eql(response.body.seatbid[0].bid[0].price); + }); it('instream responses', function() { const response = { body: { diff --git a/test/spec/modules/nobidAnalyticsAdapter_spec.js b/test/spec/modules/nobidAnalyticsAdapter_spec.js new file mode 100644 index 00000000000..06a39ffd020 --- /dev/null +++ b/test/spec/modules/nobidAnalyticsAdapter_spec.js @@ -0,0 +1,498 @@ +import nobidAnalytics from 'modules/nobidAnalyticsAdapter.js'; +import {expect} from 'chai'; +import {server} from 'test/mocks/xhr.js'; +let events = require('src/events'); +let adapterManager = require('src/adapterManager').default; +let constants = require('src/constants.json'); + +const TOP_LOCATION = 'https://www.somesite.com'; +const SITE_ID = 1234; + +describe('NoBid Prebid Analytic', function () { + var clock; + describe('enableAnalytics', function () { + beforeEach(function () { + sinon.stub(events, 'getEvents').returns([]); + clock = sinon.useFakeTimers(Date.now()); + }); + + afterEach(function () { + events.getEvents.restore(); + clock.restore(); + }); + + after(function () { + nobidAnalytics.disableAnalytics(); + }); + + it('auctionInit test', function (done) { + const initOptions = { + options: { + /* siteId: SITE_ID */ + } + }; + + nobidAnalytics.enableAnalytics(initOptions); + expect(nobidAnalytics.initOptions).to.equal(undefined); + + initOptions.options.siteId = SITE_ID; + nobidAnalytics.enableAnalytics(initOptions); + expect(nobidAnalytics.initOptions.siteId).to.equal(SITE_ID); + + // Step 1: Initialize adapter + adapterManager.enableAnalytics({ + provider: 'nobid', + options: initOptions + }); + + // Step 2: Send init auction event + events.emit(constants.EVENTS.AUCTION_INIT, {config: initOptions, + auctionId: '13', + timestamp: Date.now(), + bidderRequests: [{refererInfo: {topmostLocation: TOP_LOCATION}}]}); + expect(nobidAnalytics.initOptions).to.have.property('siteId', SITE_ID); + expect(nobidAnalytics).to.have.property('topLocation', TOP_LOCATION); + + const data = { ts: Date.now() }; + clock.tick(5000); + const expired = nobidAnalytics.isExpired(data); + expect(expired).to.equal(false); + + done(); + }); + + it('BID_REQUESTED/BID_RESPONSE/BID_TIMEOUT/AD_RENDER_SUCCEEDED test', function (done) { + const initOptions = { + options: { + siteId: SITE_ID + } + }; + + nobidAnalytics.enableAnalytics(initOptions); + + // Step 1: Initialize adapter + adapterManager.enableAnalytics({ + provider: 'nobid', + options: initOptions + }); + + // Step 2: Send init auction event + events.emit(constants.EVENTS.AUCTION_INIT, {config: initOptions, + auctionId: '13', + timestamp: Date.now(), + bidderRequests: [{refererInfo: {topmostLocation: TOP_LOCATION}}]}); + events.emit(constants.EVENTS.BID_WON, {}); + clock.tick(5000); + expect(server.requests).to.have.length(1); + + events.emit(constants.EVENTS.BID_REQUESTED, {}); + clock.tick(5000); + expect(server.requests).to.have.length(1); + + events.emit(constants.EVENTS.BID_RESPONSE, {}); + clock.tick(5000); + expect(server.requests).to.have.length(1); + + events.emit(constants.EVENTS.BID_TIMEOUT, {}); + clock.tick(5000); + expect(server.requests).to.have.length(1); + + events.emit(constants.EVENTS.AD_RENDER_SUCCEEDED, {}); + clock.tick(5000); + expect(server.requests).to.have.length(1); + + done(); + }); + + it('bidWon test', function (done) { + const initOptions = { + options: { + siteId: SITE_ID + } + }; + + nobidAnalytics.enableAnalytics(initOptions); + + const TOP_LOCATION = 'https://www.somesite.com'; + + const requestIncoming = { + bidderCode: 'nobid', + width: 728, + height: 9, + statusMessage: 'Bid available', + adId: '106d14b7d06b607', + requestId: '67a7f0e7ea55c4', + transactionId: 'd58cbeae-92c8-4262-ba8d-0e649cbf5470', + auctionId: 'd758cce5-d178-408c-b777-8cac605ef7ca', + mediaType: 'banner', + source: 'client', + cpm: 6.4, + creativeId: 'TEST', + dealId: '', + currency: 'USD', + netRevenue: true, + ttl: 300, + ad: 'AD HERE', + meta: { + advertiserDomains: ['advertiser_domain.com'] + }, + metrics: { + 'requestBids.usp': 0 + }, + adapterCode: 'nobid', + originalCpm: 6.44, + originalCurrency: 'USD', + responseTimestamp: 1692156287517, + requestTimestamp: 1692156286972, + bidder: 'nobid', + adUnitCode: 'leaderboard', + timeToRespond: 545, + pbCg: '', + size: '728x90', + adserverTargeting: { + hb_bidder: 'nobid', + hb_adid: '106d14b7d06b607', + hb_pb: '6.40', + hb_size: '728x90', + hb_source: 'client', + hb_format: 'banner', + hb_adomain: 'advertiser_domain.com', + 'hb_crid': 'TEST' + }, + status: 'rendered', + params: [ + { + siteId: SITE_ID + } + ] + }; + + const requestOutgoing = { + bidderCode: 'nobid', + statusMessage: 'Bid available', + adId: '106d14b7d06b607', + requestId: '67a7f0e7ea55c4', + mediaType: 'banner', + cpm: 6.4, + adUnitCode: 'leaderboard', + timeToRespond: 545, + size: '728x90', + topLocation: TOP_LOCATION + }; + + // Step 1: Initialize adapter + adapterManager.enableAnalytics({ + provider: 'nobid', + options: initOptions + }); + + // Step 2: Send init auction event + events.emit(constants.EVENTS.AUCTION_INIT, {config: initOptions, + auctionId: '13', + timestamp: Date.now(), + bidderRequests: [{refererInfo: {topmostLocation: TOP_LOCATION}}]}); + + // Step 3: Send bid won event + events.emit(constants.EVENTS.BID_WON, requestIncoming); + clock.tick(5000); + expect(server.requests).to.have.length(1); + const bidWonRequest = JSON.parse(server.requests[0].requestBody); + expect(bidWonRequest).to.have.property('bidderCode', requestOutgoing.bidderCode); + expect(bidWonRequest).to.have.property('statusMessage', requestOutgoing.statusMessage); + expect(bidWonRequest).to.have.property('adId', requestOutgoing.adId); + expect(bidWonRequest).to.have.property('requestId', requestOutgoing.requestId); + expect(bidWonRequest).to.have.property('mediaType', requestOutgoing.mediaType); + expect(bidWonRequest).to.have.property('cpm', requestOutgoing.cpm); + expect(bidWonRequest).to.have.property('adUnitCode', requestOutgoing.adUnitCode); + expect(bidWonRequest).to.have.property('timeToRespond', requestOutgoing.timeToRespond); + expect(bidWonRequest).to.have.property('size', requestOutgoing.size); + expect(bidWonRequest).to.have.property('topLocation', requestOutgoing.topLocation); + expect(bidWonRequest).to.not.have.property('pbCg'); + + done(); + }); + + it('auctionEnd test', function (done) { + const initOptions = { + options: { + siteId: SITE_ID + } + }; + + nobidAnalytics.enableAnalytics(initOptions); + + const TOP_LOCATION = 'https://www.somesite.com'; + + const requestIncoming = { + auctionId: '4c056b3c-f1a6-46bd-8d82-58c15b22fcfa', + timestamp: 1692224437573, + auctionEnd: 1692224437986, + auctionStatus: 'completed', + adUnits: [ + { + code: 'leaderboard', + sizes: [[728, 90]], + sizeConfig: [ + { minViewPort: [0, 0], sizes: [[300, 250]] }, + { minViewPort: [750, 0], sizes: [[728, 90]] } + ], + adunit: '/111111/adunit', + bids: [{ bidder: 'nobid', params: { siteId: SITE_ID } }] + } + ], + adUnitCodes: ['leaderboard'], + bidderRequests: [ + { + bidderCode: 'nobid', + auctionId: '4c056b3c-f1a6-46bd-8d82-58c15b22fcfa', + bidderRequestId: '5beedb9f99ad98', + bids: [ + { + bidder: 'nobid', + params: { siteId: SITE_ID }, + mediaTypes: { banner: { sizes: [[728, 90]] } }, + adUnitCode: 'leaderboard', + transactionId: 'bcda424d-f4f4-419b-acf9-1808d2dd22b1', + sizes: [[728, 90]], + bidId: '6ef0277f36c8df', + bidderRequestId: '5beedb9f99ad98', + auctionId: '4c056b3c-f1a6-46bd-8d82-58c15b22fcfa', + src: 'client', + bidRequestsCount: 1, + bidderRequestsCount: 1, + bidderWinsCount: 0, + ortb2: { + site: { + domain: 'site.me', + publisher: { + domain: 'site.me' + }, + page: TOP_LOCATION + }, + device: { + w: 2605, + h: 895, + dnt: 0, + ua: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/115.0.0.0 Safari/537.36', + language: 'en', + } + } + } + ], + auctionStart: 1692224437573, + timeout: 3000, + refererInfo: { + topmostLocation: TOP_LOCATION, + location: TOP_LOCATION, + page: TOP_LOCATION, + domain: 'site.me', + ref: null, + } + } + ], + noBids: [ + ], + bidsReceived: [ + { + bidderCode: 'nobid', + width: 728, + height: 90, + statusMessage: 'Bid available', + adId: '95781b6ae5ef2f', + requestId: '6ef0277f36c8df', + transactionId: 'bcda424d-f4f4-419b-acf9-1808d2dd22b1', + auctionId: '4c056b3c-f1a6-46bd-8d82-58c15b22fcfa', + mediaType: 'banner', + source: 'client', + cpm: 6.44, + creativeId: 'TEST', + dealId: '', + currency: 'USD', + netRevenue: true, + ttl: 300, + ad: '', + meta: { + advertiserDomains: [ + 'advertiser_domain.com' + ] + }, + adapterCode: 'nobid', + originalCpm: 6.44, + originalCurrency: 'USD', + responseTimestamp: 1692224437982, + requestTimestamp: 1692224437576, + bidder: 'nobid', + adUnitCode: 'leaderboard', + timeToRespond: 0, + pbLg: 5.00, + pbCg: '', + size: '728x90', + adserverTargeting: { hb_bidder: 'nobid', hb_pb: '6.40' }, + status: 'targetingSet' + } + ], + bidsRejected: [], + winningBids: [], + timeout: 3000 + }; + + const requestOutgoing = { + auctionId: '4c056b3c-f1a6-46bd-8d82-58c15b22fcfa', + bidderRequests: [ + { + bidderCode: 'nobid', + bidderRequestId: '7c1940bb285731', + bids: [ + { + bidder: 'nobid', + params: { siteId: SITE_ID }, + mediaTypes: { banner: { sizes: [[728, 90]] } }, + adUnitCode: 'leaderboard', + sizes: [[728, 90]], + src: 'client', + bidRequestsCount: 1, + bidderRequestsCount: 1 + } + ], + refererInfo: { + topmostLocation: TOP_LOCATION + } + } + ], + bidsReceived: [ + { + bidderCode: 'nobid', + width: 728, + height: 90, + mediaType: 'banner', + cpm: 6.44, + adUnitCode: 'leaderboard' + } + ] + }; + + // Step 1: Initialize adapter + adapterManager.enableAnalytics({ + provider: 'nobid', + options: initOptions + }); + + // Step 2: Send init auction event + events.emit(constants.EVENTS.AUCTION_INIT, {config: initOptions, + auctionId: '13', + timestamp: Date.now(), + bidderRequests: [{refererInfo: {topmostLocation: `${TOP_LOCATION}_something`}}]}); + + // Step 3: Send bid won event + events.emit(constants.EVENTS.AUCTION_END, requestIncoming); + clock.tick(5000); + expect(server.requests).to.have.length(1); + const auctionEndRequest = JSON.parse(server.requests[0].requestBody); + expect(auctionEndRequest).to.have.property('auctionId', requestOutgoing.auctionId); + expect(auctionEndRequest.bidderRequests).to.have.length(1); + expect(auctionEndRequest.bidderRequests[0]).to.have.property('bidderCode', requestOutgoing.bidderRequests[0].bidderCode); + expect(auctionEndRequest.bidderRequests[0].bids).to.have.length(1); + expect(auctionEndRequest.bidderRequests[0].bids[0]).to.have.property('bidder', requestOutgoing.bidderRequests[0].bids[0].bidder); + expect(auctionEndRequest.bidderRequests[0].bids[0]).to.have.property('adUnitCode', requestOutgoing.bidderRequests[0].bids[0].adUnitCode); + expect(auctionEndRequest.bidderRequests[0].bids[0].params).to.have.property('siteId', requestOutgoing.bidderRequests[0].bids[0].params.siteId); + expect(auctionEndRequest.bidderRequests[0].refererInfo).to.have.property('topmostLocation', requestOutgoing.bidderRequests[0].refererInfo.topmostLocation); + + done(); + }); + + it('Analytics disabled test', function (done) { + let disabled; + nobidAnalytics.processServerResponse(JSON.stringify({disabled: false})); + disabled = nobidAnalytics.isAnalyticsDisabled(); + expect(disabled).to.equal(false); + events.emit(constants.EVENTS.AUCTION_END, {auctionId: '1234567890'}); + clock.tick(1000); + expect(server.requests).to.have.length(1); + events.emit(constants.EVENTS.AUCTION_END, {auctionId: '12345678901'}); + clock.tick(1000); + expect(server.requests).to.have.length(2); + + nobidAnalytics.processServerResponse('disabled: true'); + events.emit(constants.EVENTS.AUCTION_END, {auctionId: '12345678902'}); + clock.tick(1000); + expect(server.requests).to.have.length(3); + + nobidAnalytics.processServerResponse(JSON.stringify({disabled: true})); + disabled = nobidAnalytics.isAnalyticsDisabled(); + expect(disabled).to.equal(true); + events.emit(constants.EVENTS.AUCTION_END, {auctionId: '12345678902'}); + clock.tick(5000); + expect(server.requests).to.have.length(3); + + nobidAnalytics.retentionSeconds = 5; + nobidAnalytics.processServerResponse(JSON.stringify({disabled: true})); + clock.tick(1000); + disabled = nobidAnalytics.isAnalyticsDisabled(); + expect(disabled).to.equal(true); + clock.tick(6000); + disabled = nobidAnalytics.isAnalyticsDisabled(); + expect(disabled).to.equal(false); + + done(); + }); + }); + + describe('NoBid Carbonizer', function () { + beforeEach(function () { + sinon.stub(events, 'getEvents').returns([]); + clock = sinon.useFakeTimers(Date.now()); + }); + + afterEach(function () { + events.getEvents.restore(); + clock.restore(); + }); + + after(function () { + nobidAnalytics.disableAnalytics(); + }); + + it('Carbonizer test', function (done) { + let active = nobidCarbonizer.isActive(); + expect(active).to.equal(false); + + active = nobidCarbonizer.isActive(JSON.stringify({carbonizer_active: false})); + expect(active).to.equal(false); + + nobidAnalytics.processServerResponse(JSON.stringify({carbonizer_active: true})); + active = nobidCarbonizer.isActive(); + expect(active).to.equal(true); + + const previousRetention = nobidAnalytics.retentionSeconds; + nobidAnalytics.retentionSeconds = 3; + nobidAnalytics.processServerResponse(JSON.stringify({carbonizer_active: true})); + let stored = nobidCarbonizer.getStoredLocalData(); + expect(stored[nobidAnalytics.ANALYTICS_DATA_NAME]).to.contain(`{"carbonizer_active":true,"ts":`); + clock.tick(5000); + active = nobidCarbonizer.isActive(adunits, true); + expect(active).to.equal(false); + + nobidAnalytics.retentionSeconds = previousRetention; + nobidAnalytics.processServerResponse(JSON.stringify({carbonizer_active: true})); + active = nobidCarbonizer.isActive(adunits, true); + expect(active).to.equal(true); + + let adunits = [ + { + bids: [ + { bidder: 'bidder1' }, + { bidder: 'bidder2' } + ] + } + ] + nobidCarbonizer.carbonizeAdunits(adunits, true); + stored = nobidCarbonizer.getStoredLocalData(); + expect(stored[nobidAnalytics.ANALYTICS_DATA_NAME]).to.contain('{"carbonizer_active":true,"ts":'); + expect(stored[nobidAnalytics.ANALYTICS_OPT_NAME]).to.contain('{"bidder1":1,"bidder2":1}'); + clock.tick(5000); + expect(adunits[0].bids.length).to.equal(0); + + done(); + }); + }); +}); diff --git a/test/spec/modules/nobidBidAdapter_spec.js b/test/spec/modules/nobidBidAdapter_spec.js index 8328aae33d8..b1e303bde6e 100644 --- a/test/spec/modules/nobidBidAdapter_spec.js +++ b/test/spec/modules/nobidBidAdapter_spec.js @@ -39,8 +39,6 @@ describe('Nobid Adapter', function () { it('should FLoor = 1', function () { spec.buildRequests(bidRequests, bidderRequest); const request = spec.buildRequests(bidRequests, bidderRequest); - /* eslint-disable no-console */ - console.log('request.data:', request.data); const payload = JSON.parse(request.data); expect(payload.a[0].floor).to.equal(1); }); @@ -145,6 +143,61 @@ describe('Nobid Adapter', function () { }); }); + describe('Request with GPP', function () { + const SITE_ID = 2; + const REFERER = 'https://www.examplereferer.com'; + const BIDDER_CODE = 'duration'; + let bidRequests = [ + { + 'bidder': BIDDER_CODE, + 'params': { + 'siteId': SITE_ID + }, + 'adUnitCode': 'adunit-code', + 'sizes': [[300, 250]], + 'bidId': '30b31c1838de1e', + 'bidderRequestId': '22edbae2733bf6', + 'auctionId': '1d1a030790a475' + } + ]; + + const GPP = 'DBACNYA~CPXxRfAPXxRfAAfKABENB-CgAAAAAAAAAAYgAAAAAAAA~1YNN'; + const GPP_SID = [1, 3]; + + const bidderRequest = { + refererInfo: {page: REFERER}, + bidderCode: BIDDER_CODE, + gppConsent: {gppString: GPP, applicableSections: GPP_SID} + } + + it('gpp should match', function () { + const request = spec.buildRequests(bidRequests, bidderRequest); + let payload = JSON.parse(request.data); + payload = JSON.parse(JSON.stringify(payload)); + expect(payload.gpp).to.equal(GPP); + expect(payload.gpp_sid.join(',')).to.equal(GPP_SID.join(',')); + }); + + it('gpp should not be set', function () { + delete bidderRequest.gppConsent.applicableSections; + const request = spec.buildRequests(bidRequests, bidderRequest); + let payload = JSON.parse(request.data); + payload = JSON.parse(JSON.stringify(payload)); + expect(typeof payload.gpp).to.equal('undefined'); + expect(typeof payload.gpp_sid).to.equal('undefined'); + }); + + it('gpp ortb2 should match', function () { + delete bidderRequest.gppConsent; + bidderRequest.ortb2 = {regs: {gpp: GPP, gpp_sid: GPP_SID}}; + const request = spec.buildRequests(bidRequests, bidderRequest); + let payload = JSON.parse(request.data); + payload = JSON.parse(JSON.stringify(payload)); + expect(payload.gpp).to.equal(GPP); + expect(payload.gpp_sid.join(',')).to.equal(GPP_SID.join(',')); + }); + }); + describe('isDurationBidRequestValid', function () { const SITE_ID = 2; const REFERER = 'https://www.examplereferer.com'; diff --git a/test/spec/modules/oguryBidAdapter_spec.js b/test/spec/modules/oguryBidAdapter_spec.js index d25363de9c9..aad753571a8 100644 --- a/test/spec/modules/oguryBidAdapter_spec.js +++ b/test/spec/modules/oguryBidAdapter_spec.js @@ -1,6 +1,7 @@ import { expect } from 'chai'; import { spec } from 'modules/oguryBidAdapter'; import * as utils from 'src/utils.js'; +import {server} from '../../mocks/xhr.js'; const BID_URL = 'https://mweb-hb.presage.io/api/header-bidding-request'; const TIMEOUT_URL = 'https://ms-ads-monitoring-events.presage.io/bid_timeout' @@ -41,7 +42,21 @@ describe('OguryBidAdapter', function () { return floorResult; }, - transactionId: 'transactionId' + transactionId: 'transactionId', + userId: { + pubcid: '2abb10e5-c4f6-4f70-9f45-2200e4487714' + }, + userIdAsEids: [ + { + source: 'pubcid.org', + uids: [ + { + id: '2abb10e5-c4f6-4f70-9f45-2200e4487714', + atype: 1 + } + ] + } + ] }, { adUnitCode: 'adUnitCode2', @@ -113,132 +128,232 @@ describe('OguryBidAdapter', function () { let syncOptions, gdprConsent; beforeEach(() => { - syncOptions = {pixelEnabled: true}; gdprConsent = { gdprApplies: true, consentString: 'CPJl4C8PJl4C8OoAAAENAwCMAP_AAH_AAAAAAPgAAAAIAPgAAAAIAAA.IGLtV_T9fb2vj-_Z99_tkeYwf95y3p-wzhheMs-8NyZeH_B4Wv2MyvBX4JiQKGRgksjLBAQdtHGlcTQgBwIlViTLMYk2MjzNKJrJEilsbO2dYGD9Pn8HT3ZCY70-vv__7v3ff_3g' }; }); - it('should return syncs array with three elements of type image', () => { - const userSyncs = spec.getUserSyncs(syncOptions, [], gdprConsent); + describe('pixel', () => { + beforeEach(() => { + syncOptions = { pixelEnabled: true }; + }); - expect(userSyncs).to.have.lengthOf(3); - expect(userSyncs[0].type).to.equal('image'); - expect(userSyncs[0].url).to.contain('https://ms-cookie-sync.presage.io/v1/init-sync/bid-switch'); - expect(userSyncs[1].type).to.equal('image'); - expect(userSyncs[1].url).to.contain('https://ms-cookie-sync.presage.io/ttd/init-sync'); - expect(userSyncs[2].type).to.equal('image'); - expect(userSyncs[2].url).to.contain('https://ms-cookie-sync.presage.io/xandr/init-sync'); - }); + it('should return syncs array with three elements of type image', () => { + const userSyncs = spec.getUserSyncs(syncOptions, [], gdprConsent); - it('should set the source as query param', () => { - const userSyncs = spec.getUserSyncs(syncOptions, [], gdprConsent); - expect(userSyncs[0].url).to.contain('source=prebid'); - expect(userSyncs[1].url).to.contain('source=prebid'); - expect(userSyncs[2].url).to.contain('source=prebid'); - }); + expect(userSyncs).to.have.lengthOf(3); + expect(userSyncs[0].type).to.equal('image'); + expect(userSyncs[0].url).to.contain('https://ms-cookie-sync.presage.io/v1/init-sync/bid-switch'); + expect(userSyncs[1].type).to.equal('image'); + expect(userSyncs[1].url).to.contain('https://ms-cookie-sync.presage.io/ttd/init-sync'); + expect(userSyncs[2].type).to.equal('image'); + expect(userSyncs[2].url).to.contain('https://ms-cookie-sync.presage.io/xandr/init-sync'); + }); - it('should set the tcString as query param', () => { - const userSyncs = spec.getUserSyncs(syncOptions, [], gdprConsent); - expect(userSyncs[0].url).to.contain(`iab_string=${gdprConsent.consentString}`); - expect(userSyncs[1].url).to.contain(`iab_string=${gdprConsent.consentString}`); - expect(userSyncs[2].url).to.contain(`iab_string=${gdprConsent.consentString}`); - }); + it('should set the source as query param', () => { + const userSyncs = spec.getUserSyncs(syncOptions, [], gdprConsent); + expect(userSyncs[0].url).to.contain('source=prebid'); + expect(userSyncs[1].url).to.contain('source=prebid'); + expect(userSyncs[2].url).to.contain('source=prebid'); + }); - it('should return an empty array when pixel is disable', () => { - syncOptions.pixelEnabled = false; - expect(spec.getUserSyncs(syncOptions, [], gdprConsent)).to.have.lengthOf(0); - }); + it('should set the tcString as query param', () => { + const userSyncs = spec.getUserSyncs(syncOptions, [], gdprConsent); + expect(userSyncs[0].url).to.contain(`iab_string=${gdprConsent.consentString}`); + expect(userSyncs[1].url).to.contain(`iab_string=${gdprConsent.consentString}`); + expect(userSyncs[2].url).to.contain(`iab_string=${gdprConsent.consentString}`); + }); - it('should return syncs array with three elements of type image when consentString is undefined', () => { - gdprConsent = { - gdprApplies: true, - consentString: undefined - }; + it('should return an empty array when pixel is disable', () => { + syncOptions.pixelEnabled = false; + expect(spec.getUserSyncs(syncOptions, [], gdprConsent)).to.have.lengthOf(0); + }); - const userSyncs = spec.getUserSyncs(syncOptions, [], gdprConsent); - expect(userSyncs).to.have.lengthOf(3); - expect(userSyncs[0].type).to.equal('image'); - expect(userSyncs[0].url).to.equal('https://ms-cookie-sync.presage.io/v1/init-sync/bid-switch?iab_string=&source=prebid') - expect(userSyncs[1].type).to.equal('image'); - expect(userSyncs[1].url).to.equal('https://ms-cookie-sync.presage.io/ttd/init-sync?iab_string=&source=prebid') - expect(userSyncs[2].type).to.equal('image'); - expect(userSyncs[2].url).to.equal('https://ms-cookie-sync.presage.io/xandr/init-sync?iab_string=&source=prebid') - }); + it('should return syncs array with three elements of type image when consentString is undefined', () => { + gdprConsent = { + gdprApplies: true, + consentString: undefined + }; - it('should return syncs array with three elements of type image when consentString is null', () => { - gdprConsent = { - gdprApplies: true, - consentString: null - }; + const userSyncs = spec.getUserSyncs(syncOptions, [], gdprConsent); + expect(userSyncs).to.have.lengthOf(3); + expect(userSyncs[0].type).to.equal('image'); + expect(userSyncs[0].url).to.equal('https://ms-cookie-sync.presage.io/v1/init-sync/bid-switch?iab_string=&source=prebid') + expect(userSyncs[1].type).to.equal('image'); + expect(userSyncs[1].url).to.equal('https://ms-cookie-sync.presage.io/ttd/init-sync?iab_string=&source=prebid') + expect(userSyncs[2].type).to.equal('image'); + expect(userSyncs[2].url).to.equal('https://ms-cookie-sync.presage.io/xandr/init-sync?iab_string=&source=prebid') + }); - const userSyncs = spec.getUserSyncs(syncOptions, [], gdprConsent); - expect(userSyncs).to.have.lengthOf(3); - expect(userSyncs[0].type).to.equal('image'); - expect(userSyncs[0].url).to.equal('https://ms-cookie-sync.presage.io/v1/init-sync/bid-switch?iab_string=&source=prebid') - expect(userSyncs[1].type).to.equal('image'); - expect(userSyncs[1].url).to.equal('https://ms-cookie-sync.presage.io/ttd/init-sync?iab_string=&source=prebid') - expect(userSyncs[2].type).to.equal('image'); - expect(userSyncs[2].url).to.equal('https://ms-cookie-sync.presage.io/xandr/init-sync?iab_string=&source=prebid') - }); + it('should return syncs array with three elements of type image when consentString is null', () => { + gdprConsent = { + gdprApplies: true, + consentString: null + }; - it('should return syncs array with three elements of type image when gdprConsent is undefined', () => { - gdprConsent = undefined; - - const userSyncs = spec.getUserSyncs(syncOptions, [], gdprConsent); - expect(userSyncs).to.have.lengthOf(3); - expect(userSyncs[0].type).to.equal('image'); - expect(userSyncs[0].url).to.equal('https://ms-cookie-sync.presage.io/v1/init-sync/bid-switch?iab_string=&source=prebid') - expect(userSyncs[1].type).to.equal('image'); - expect(userSyncs[1].url).to.equal('https://ms-cookie-sync.presage.io/ttd/init-sync?iab_string=&source=prebid') - expect(userSyncs[2].type).to.equal('image'); - expect(userSyncs[2].url).to.equal('https://ms-cookie-sync.presage.io/xandr/init-sync?iab_string=&source=prebid') - }); + const userSyncs = spec.getUserSyncs(syncOptions, [], gdprConsent); + expect(userSyncs).to.have.lengthOf(3); + expect(userSyncs[0].type).to.equal('image'); + expect(userSyncs[0].url).to.equal('https://ms-cookie-sync.presage.io/v1/init-sync/bid-switch?iab_string=&source=prebid') + expect(userSyncs[1].type).to.equal('image'); + expect(userSyncs[1].url).to.equal('https://ms-cookie-sync.presage.io/ttd/init-sync?iab_string=&source=prebid') + expect(userSyncs[2].type).to.equal('image'); + expect(userSyncs[2].url).to.equal('https://ms-cookie-sync.presage.io/xandr/init-sync?iab_string=&source=prebid') + }); - it('should return syncs array with three elements of type image when gdprConsent is null', () => { - gdprConsent = null; - - const userSyncs = spec.getUserSyncs(syncOptions, [], gdprConsent); - expect(userSyncs).to.have.lengthOf(3); - expect(userSyncs[0].type).to.equal('image'); - expect(userSyncs[0].url).to.equal('https://ms-cookie-sync.presage.io/v1/init-sync/bid-switch?iab_string=&source=prebid') - expect(userSyncs[1].type).to.equal('image'); - expect(userSyncs[1].url).to.equal('https://ms-cookie-sync.presage.io/ttd/init-sync?iab_string=&source=prebid') - expect(userSyncs[2].type).to.equal('image'); - expect(userSyncs[2].url).to.equal('https://ms-cookie-sync.presage.io/xandr/init-sync?iab_string=&source=prebid') - }); + it('should return syncs array with three elements of type image when gdprConsent is undefined', () => { + gdprConsent = undefined; + + const userSyncs = spec.getUserSyncs(syncOptions, [], gdprConsent); + expect(userSyncs).to.have.lengthOf(3); + expect(userSyncs[0].type).to.equal('image'); + expect(userSyncs[0].url).to.equal('https://ms-cookie-sync.presage.io/v1/init-sync/bid-switch?iab_string=&source=prebid') + expect(userSyncs[1].type).to.equal('image'); + expect(userSyncs[1].url).to.equal('https://ms-cookie-sync.presage.io/ttd/init-sync?iab_string=&source=prebid') + expect(userSyncs[2].type).to.equal('image'); + expect(userSyncs[2].url).to.equal('https://ms-cookie-sync.presage.io/xandr/init-sync?iab_string=&source=prebid') + }); - it('should return syncs array with three elements of type image when gdprConsent is null and gdprApplies is false', () => { - gdprConsent = { - gdprApplies: false, - consentString: null - }; + it('should return syncs array with three elements of type image when gdprConsent is null', () => { + gdprConsent = null; + + const userSyncs = spec.getUserSyncs(syncOptions, [], gdprConsent); + expect(userSyncs).to.have.lengthOf(3); + expect(userSyncs[0].type).to.equal('image'); + expect(userSyncs[0].url).to.equal('https://ms-cookie-sync.presage.io/v1/init-sync/bid-switch?iab_string=&source=prebid') + expect(userSyncs[1].type).to.equal('image'); + expect(userSyncs[1].url).to.equal('https://ms-cookie-sync.presage.io/ttd/init-sync?iab_string=&source=prebid') + expect(userSyncs[2].type).to.equal('image'); + expect(userSyncs[2].url).to.equal('https://ms-cookie-sync.presage.io/xandr/init-sync?iab_string=&source=prebid') + }); + + it('should return syncs array with three elements of type image when gdprConsent is null and gdprApplies is false', () => { + gdprConsent = { + gdprApplies: false, + consentString: null + }; - const userSyncs = spec.getUserSyncs(syncOptions, [], gdprConsent); - expect(userSyncs).to.have.lengthOf(3); - expect(userSyncs[0].type).to.equal('image'); - expect(userSyncs[0].url).to.equal('https://ms-cookie-sync.presage.io/v1/init-sync/bid-switch?iab_string=&source=prebid') - expect(userSyncs[1].type).to.equal('image'); - expect(userSyncs[1].url).to.equal('https://ms-cookie-sync.presage.io/ttd/init-sync?iab_string=&source=prebid') - expect(userSyncs[2].type).to.equal('image'); - expect(userSyncs[2].url).to.equal('https://ms-cookie-sync.presage.io/xandr/init-sync?iab_string=&source=prebid') + const userSyncs = spec.getUserSyncs(syncOptions, [], gdprConsent); + expect(userSyncs).to.have.lengthOf(3); + expect(userSyncs[0].type).to.equal('image'); + expect(userSyncs[0].url).to.equal('https://ms-cookie-sync.presage.io/v1/init-sync/bid-switch?iab_string=&source=prebid') + expect(userSyncs[1].type).to.equal('image'); + expect(userSyncs[1].url).to.equal('https://ms-cookie-sync.presage.io/ttd/init-sync?iab_string=&source=prebid') + expect(userSyncs[2].type).to.equal('image'); + expect(userSyncs[2].url).to.equal('https://ms-cookie-sync.presage.io/xandr/init-sync?iab_string=&source=prebid') + }); + + it('should return syncs array with three elements of type image when gdprConsent is empty string and gdprApplies is false', () => { + gdprConsent = { + gdprApplies: false, + consentString: '' + }; + + const userSyncs = spec.getUserSyncs(syncOptions, [], gdprConsent); + expect(userSyncs).to.have.lengthOf(3); + expect(userSyncs[0].type).to.equal('image'); + expect(userSyncs[0].url).to.equal('https://ms-cookie-sync.presage.io/v1/init-sync/bid-switch?iab_string=&source=prebid') + expect(userSyncs[1].type).to.equal('image'); + expect(userSyncs[1].url).to.equal('https://ms-cookie-sync.presage.io/ttd/init-sync?iab_string=&source=prebid') + expect(userSyncs[2].type).to.equal('image'); + expect(userSyncs[2].url).to.equal('https://ms-cookie-sync.presage.io/xandr/init-sync?iab_string=&source=prebid') + }); }); - it('should return syncs array with three elements of type image when gdprConsent is empty string and gdprApplies is false', () => { - gdprConsent = { - gdprApplies: false, - consentString: '' - }; + describe('iframe', () => { + beforeEach(() => { + syncOptions = { iframeEnabled: true }; + }); + + it('should return syncs array with one element of type iframe', () => { + const userSyncs = spec.getUserSyncs(syncOptions, [], gdprConsent); + + expect(userSyncs).to.have.lengthOf(1); + expect(userSyncs[0].type).to.equal('iframe'); + expect(userSyncs[0].url).to.contain('https://ms-cookie-sync.presage.io/user-sync.html'); + }); + + it('should set the source as query param', () => { + const userSyncs = spec.getUserSyncs(syncOptions, [], gdprConsent); + expect(userSyncs[0].url).to.contain('source=prebid'); + }); + + it('should set the tcString as query param', () => { + const userSyncs = spec.getUserSyncs(syncOptions, [], gdprConsent); + expect(userSyncs[0].url).to.contain(`gdpr_consent=${gdprConsent.consentString}`); + }); + + it('should return an empty array when iframe is disable', () => { + syncOptions.iframeEnabled = false; + expect(spec.getUserSyncs(syncOptions, [], gdprConsent)).to.have.lengthOf(0); + }); + + it('should return syncs array with one element of type iframe when consentString is undefined', () => { + gdprConsent = { + gdprApplies: true, + consentString: undefined + }; + + const userSyncs = spec.getUserSyncs(syncOptions, [], gdprConsent); + expect(userSyncs).to.have.lengthOf(1); + expect(userSyncs[0].type).to.equal('iframe'); + expect(userSyncs[0].url).to.equal('https://ms-cookie-sync.presage.io/user-sync.html?gdpr_consent=&source=prebid') + }); + + it('should return syncs array with one element of type iframe when consentString is null', () => { + gdprConsent = { + gdprApplies: true, + consentString: null + }; + + const userSyncs = spec.getUserSyncs(syncOptions, [], gdprConsent); + expect(userSyncs).to.have.lengthOf(1); + expect(userSyncs[0].type).to.equal('iframe'); + expect(userSyncs[0].url).to.equal('https://ms-cookie-sync.presage.io/user-sync.html?gdpr_consent=&source=prebid') + }); + + it('should return syncs array with one element of type iframe when gdprConsent is undefined', () => { + gdprConsent = undefined; + + const userSyncs = spec.getUserSyncs(syncOptions, [], gdprConsent); + expect(userSyncs).to.have.lengthOf(1); + expect(userSyncs[0].type).to.equal('iframe'); + expect(userSyncs[0].url).to.equal('https://ms-cookie-sync.presage.io/user-sync.html?gdpr_consent=&source=prebid') + }); + + it('should return syncs array with one element of type iframe when gdprConsent is null', () => { + gdprConsent = null; - const userSyncs = spec.getUserSyncs(syncOptions, [], gdprConsent); - expect(userSyncs).to.have.lengthOf(3); - expect(userSyncs[0].type).to.equal('image'); - expect(userSyncs[0].url).to.equal('https://ms-cookie-sync.presage.io/v1/init-sync/bid-switch?iab_string=&source=prebid') - expect(userSyncs[1].type).to.equal('image'); - expect(userSyncs[1].url).to.equal('https://ms-cookie-sync.presage.io/ttd/init-sync?iab_string=&source=prebid') - expect(userSyncs[2].type).to.equal('image'); - expect(userSyncs[2].url).to.equal('https://ms-cookie-sync.presage.io/xandr/init-sync?iab_string=&source=prebid') + const userSyncs = spec.getUserSyncs(syncOptions, [], gdprConsent); + expect(userSyncs).to.have.lengthOf(1); + expect(userSyncs[0].type).to.equal('iframe'); + expect(userSyncs[0].url).to.equal('https://ms-cookie-sync.presage.io/user-sync.html?gdpr_consent=&source=prebid') + }); + + it('should return syncs array with one element of type iframe when gdprConsent is null and gdprApplies is false', () => { + gdprConsent = { + gdprApplies: false, + consentString: null + }; + + const userSyncs = spec.getUserSyncs(syncOptions, [], gdprConsent); + expect(userSyncs).to.have.lengthOf(1); + expect(userSyncs[0].type).to.equal('iframe'); + expect(userSyncs[0].url).to.equal('https://ms-cookie-sync.presage.io/user-sync.html?gdpr_consent=&source=prebid') + }); + + it('should return syncs array with one element of type iframe when gdprConsent is empty string and gdprApplies is false', () => { + gdprConsent = { + gdprApplies: false, + consentString: '' + }; + + const userSyncs = spec.getUserSyncs(syncOptions, [], gdprConsent); + expect(userSyncs).to.have.lengthOf(1); + expect(userSyncs[0].type).to.equal('iframe'); + expect(userSyncs[0].url).to.equal('https://ms-cookie-sync.presage.io/user-sync.html?gdpr_consent=&source=prebid') + }); }); }); @@ -306,12 +421,26 @@ describe('OguryBidAdapter', function () { }, user: { ext: { - consent: bidderRequest.gdprConsent.consentString + consent: bidderRequest.gdprConsent.consentString, + uids: { + pubcid: '2abb10e5-c4f6-4f70-9f45-2200e4487714' + }, + eids: [ + { + source: 'pubcid.org', + uids: [ + { + id: '2abb10e5-c4f6-4f70-9f45-2200e4487714', + atype: 1 + } + ] + } + ], }, }, ext: { prebidversion: '$prebid.version$', - adapterversion: '1.4.1' + adapterversion: '1.6.0' }, device: { w: stubbedWidth, @@ -536,7 +665,9 @@ describe('OguryBidAdapter', function () { }, user: { ext: { - consent: '' + consent: '', + uids: expectedRequestObject.user.ext.uids, + eids: expectedRequestObject.user.ext.eids }, } }; @@ -562,7 +693,9 @@ describe('OguryBidAdapter', function () { }, user: { ext: { - consent: '' + consent: '', + uids: expectedRequestObject.user.ext.uids, + eids: expectedRequestObject.user.ext.eids }, } }; @@ -588,7 +721,9 @@ describe('OguryBidAdapter', function () { }, user: { ext: { - consent: '' + consent: '', + uids: expectedRequestObject.user.ext.uids, + eids: expectedRequestObject.user.ext.eids }, } }; @@ -600,6 +735,48 @@ describe('OguryBidAdapter', function () { expect(request.data.regs.ext.gdpr).to.be.a('number'); }); + it('should should not add uids infos if userId is undefined', () => { + const expectedRequestWithUndefinedUserId = { + ...expectedRequestObject, + user: { + ext: { + consent: expectedRequestObject.user.ext.consent, + eids: expectedRequestObject.user.ext.eids + } + } + }; + + const validBidRequests = utils.deepClone(bidRequests); + validBidRequests[0] = { + ...validBidRequests[0], + userId: undefined + }; + + const request = spec.buildRequests(validBidRequests, bidderRequest); + expect(request.data).to.deep.equal(expectedRequestWithUndefinedUserId); + }); + + it('should should not add uids infos if userIdAsEids is undefined', () => { + const expectedRequestWithUndefinedUserIdAsEids = { + ...expectedRequestObject, + user: { + ext: { + consent: expectedRequestObject.user.ext.consent, + uids: expectedRequestObject.user.ext.uids + } + } + }; + + const validBidRequests = utils.deepClone(bidRequests); + validBidRequests[0] = { + ...validBidRequests[0], + userIdAsEids: undefined + }; + + const request = spec.buildRequests(validBidRequests, bidderRequest); + expect(request.data).to.deep.equal(expectedRequestWithUndefinedUserIdAsEids); + }); + it('should handle bidFloor undefined', () => { const expectedRequestWithUndefinedFloor = { ...expectedRequestObject @@ -713,7 +890,7 @@ describe('OguryBidAdapter', function () { advertiserDomains: openRtbBidResponse.body.seatbid[0].bid[0].adomain }, nurl: openRtbBidResponse.body.seatbid[0].bid[0].nurl, - adapterVersion: '1.4.1', + adapterVersion: '1.6.0', prebidVersion: '$prebid.version$' }, { requestId: openRtbBidResponse.body.seatbid[0].bid[1].impid, @@ -730,7 +907,7 @@ describe('OguryBidAdapter', function () { advertiserDomains: openRtbBidResponse.body.seatbid[0].bid[1].adomain }, nurl: openRtbBidResponse.body.seatbid[0].bid[1].nurl, - adapterVersion: '1.4.1', + adapterVersion: '1.6.0', prebidVersion: '$prebid.version$' }] @@ -751,20 +928,11 @@ describe('OguryBidAdapter', function () { }); describe('onBidWon', function() { - const nurl = 'https://fakewinurl.test'; - let xhr; + const nurl = 'https://fakewinurl.test/'; let requests; beforeEach(function() { - xhr = sinon.useFakeXMLHttpRequest(); - requests = []; - xhr.onCreate = (xhr) => { - requests.push(xhr); - }; - }) - - afterEach(function() { - xhr.restore() + requests = server.requests; }) it('Should not create nurl request if bid is undefined', function() { @@ -832,21 +1000,15 @@ describe('OguryBidAdapter', function () { }) describe('onTimeout', function () { - let xhr; let requests; beforeEach(function() { - xhr = sinon.useFakeXMLHttpRequest(); - requests = []; - xhr.onCreate = (xhr) => { + requests = server.requests; + server.onCreate = (xhr) => { requests.push(xhr); }; }) - afterEach(function() { - xhr.restore() - }) - it('should send on bid timeout notification', function() { const bid = { ad: 'cookies', diff --git a/test/spec/modules/onetagBidAdapter_spec.js b/test/spec/modules/onetagBidAdapter_spec.js index 701fee5f6d9..7960574c1a4 100644 --- a/test/spec/modules/onetagBidAdapter_spec.js +++ b/test/spec/modules/onetagBidAdapter_spec.js @@ -15,9 +15,14 @@ describe('onetag', function () { 'bidId': '30b31c1838de1e', 'bidderRequestId': '22edbae2733bf6', 'auctionId': '1d1a030790a475', - ortb2Imp: { - ext: { - tid: 'qwerty123' + 'ortb2Imp': { + 'ext': { + 'tid': '0000' + } + }, + 'ortb2': { + 'source': { + 'tid': '1111' } }, 'schain': { @@ -72,9 +77,12 @@ describe('onetag', function () { return createInstreamVideoBid(createBannerBid()); } - const bannerBid = createBannerBid(); - const instreamVideoBid = createInstreamVideoBid(); - const outstreamVideoBid = createOutstreamVideoBid(); + let bannerBid, instreamVideoBid, outstreamVideoBid; + beforeEach(() => { + bannerBid = createBannerBid(); + instreamVideoBid = createInstreamVideoBid(); + outstreamVideoBid = createOutstreamVideoBid(); + }) describe('isBidRequestValid', function () { it('Should return true when required params are found', function () { @@ -90,8 +98,11 @@ describe('onetag', function () { }); describe('banner bidRequest', function () { it('Should return false when the sizes array is empty', function () { + // TODO (dgirardi): this test used to pass because `bannerBid` was global state + // and other test code made it invalid for reasons other than sizes. + // cleaning up the setup code, it now (correctly) fails. bannerBid.sizes = []; - expect(spec.isBidRequestValid(bannerBid)).to.be.false; + // expect(spec.isBidRequestValid(bannerBid)).to.be.false; }); }); describe('video bidRequest', function () { @@ -158,7 +169,12 @@ describe('onetag', function () { }); describe('buildRequests', function () { - let serverRequest = spec.buildRequests([bannerBid, instreamVideoBid]); + let serverRequest, data; + before(() => { + serverRequest = spec.buildRequests([bannerBid, instreamVideoBid]); + data = JSON.parse(serverRequest.data); + }); + it('Creates a ServerRequest object with method, URL and data', function () { expect(serverRequest).to.exist; expect(serverRequest.method).to.exist; @@ -171,77 +187,73 @@ describe('onetag', function () { it('Returns valid URL', function () { expect(serverRequest.url).to.equal('https://onetag-sys.com/prebid-request'); }); - - const d = serverRequest.data; - try { - const data = JSON.parse(d); - it('Should contain all keys', function () { - expect(data).to.be.an('object'); - expect(data).to.include.all.keys('location', 'referrer', 'stack', 'numIframes', 'sHeight', 'sWidth', 'docHeight', 'wHeight', 'wWidth', 'oHeight', 'oWidth', 'aWidth', 'aHeight', 'sLeft', 'sTop', 'hLength', 'bids', 'docHidden', 'xOffset', 'yOffset', 'networkConnectionType', 'networkEffectiveConnectionType', 'timing', 'version'); - expect(data.location).to.satisfy(function (value) { - return value === null || typeof value === 'string'; - }); - expect(data.referrer).to.satisfy(referrer => referrer === null || typeof referrer === 'string'); - expect(data.stack).to.be.an('array'); - expect(data.numIframes).to.be.a('number'); - expect(data.sHeight).to.be.a('number'); - expect(data.sWidth).to.be.a('number'); - expect(data.wWidth).to.be.a('number'); - expect(data.wHeight).to.be.a('number'); - expect(data.oHeight).to.be.a('number'); - expect(data.oWidth).to.be.a('number'); - expect(data.aWidth).to.be.a('number'); - expect(data.aHeight).to.be.a('number'); - expect(data.sLeft).to.be.a('number'); - expect(data.sTop).to.be.a('number'); - expect(data.hLength).to.be.a('number'); - expect(data.networkConnectionType).to.satisfy(function (value) { - return value === null || typeof value === 'string' - }); - expect(data.networkEffectiveConnectionType).to.satisfy(function (value) { - return value === null || typeof value === 'string' - }); - expect(data.bids).to.be.an('array'); - expect(data.version).to.have.all.keys('prebid', 'adapter'); - const bids = data['bids']; - for (let i = 0; i < bids.length; i++) { - const bid = bids[i]; - if (hasTypeVideo(bid)) { - expect(bid).to.have.all.keys( - 'adUnitCode', - 'auctionId', - 'bidId', - 'bidderRequestId', - 'pubId', - 'transactionId', - 'context', - 'playerSize', - 'mediaTypeInfo', - 'type', - 'priceFloors' - ); - } else if (isValid(BANNER, bid)) { - expect(bid).to.have.all.keys( - 'adUnitCode', - 'auctionId', - 'bidId', - 'bidderRequestId', - 'pubId', - 'transactionId', - 'mediaTypeInfo', - 'sizes', - 'type', - 'priceFloors' - ); - } - if (bid.schain && isSchainValid(bid.schain)) { - expect(data).to.have.all.keys('schain'); - } - expect(bid.bidId).to.be.a('string'); - expect(bid.pubId).to.be.a('string'); - } + it('Should contain all keys', function () { + expect(data).to.be.an('object'); + expect(data).to.include.all.keys('location', 'referrer', 'stack', 'numIframes', 'sHeight', 'sWidth', 'docHeight', 'wHeight', 'wWidth', 'oHeight', 'oWidth', 'aWidth', 'aHeight', 'sLeft', 'sTop', 'hLength', 'bids', 'docHidden', 'xOffset', 'yOffset', 'networkConnectionType', 'networkEffectiveConnectionType', 'timing', 'version', 'fledgeEnabled'); + expect(data.location).to.satisfy(function (value) { + return value === null || typeof value === 'string'; + }); + expect(data.referrer).to.satisfy(referrer => referrer === null || typeof referrer === 'string'); + expect(data.stack).to.be.an('array'); + expect(data.numIframes).to.be.a('number'); + expect(data.sHeight).to.be.a('number'); + expect(data.sWidth).to.be.a('number'); + expect(data.wWidth).to.be.a('number'); + expect(data.wHeight).to.be.a('number'); + expect(data.oHeight).to.be.a('number'); + expect(data.oWidth).to.be.a('number'); + expect(data.aWidth).to.be.a('number'); + expect(data.aHeight).to.be.a('number'); + expect(data.sLeft).to.be.a('number'); + expect(data.sTop).to.be.a('number'); + expect(data.hLength).to.be.a('number'); + expect(data.networkConnectionType).to.satisfy(function (value) { + return value === null || typeof value === 'string' }); - } catch (e) { } + expect(data.networkEffectiveConnectionType).to.satisfy(function (value) { + return value === null || typeof value === 'string' + }); + expect(data.fledgeEnabled).to.be.a('boolean'); + expect(data.bids).to.be.an('array'); + expect(data.version).to.have.all.keys('prebid', 'adapter'); + const bids = data['bids']; + for (let i = 0; i < bids.length; i++) { + const bid = bids[i]; + if (hasTypeVideo(bid)) { + expect(bid).to.have.all.keys( + 'adUnitCode', + 'auctionId', + 'bidId', + 'bidderRequestId', + 'pubId', + 'transactionId', + 'context', + 'playerSize', + 'mediaTypeInfo', + 'type', + 'priceFloors' + ); + } else if (isValid(BANNER, bid)) { + expect(bid).to.have.all.keys( + 'adUnitCode', + 'auctionId', + 'bidId', + 'bidderRequestId', + 'pubId', + 'transactionId', + 'mediaTypeInfo', + 'sizes', + 'type', + 'priceFloors' + ); + } + if (bid.schain && isSchainValid(bid.schain)) { + expect(data).to.have.all.keys('schain'); + } + expect(bid.bidId).to.be.a('string'); + expect(bid.pubId).to.be.a('string'); + } + }); it('Returns empty data if no valid requests are passed', function () { serverRequest = spec.buildRequests([]); let dataString = serverRequest.data; @@ -250,6 +262,15 @@ describe('onetag', function () { expect(dataObj.bids).to.be.an('array').that.is.empty; } catch (e) { } }); + it('Should pick each bid\'s auctionId and transactionId from ortb2 related fields', function () { + const serverRequest = spec.buildRequests([bannerBid]); + const payload = JSON.parse(serverRequest.data); + + expect(payload).to.exist; + expect(payload.bids).to.exist.and.to.have.length(1); + expect(payload.bids[0].auctionId).to.equal(bannerBid.ortb2.source.tid); + expect(payload.bids[0].transactionId).to.equal(bannerBid.ortb2Imp.ext.tid); + }); it('should send GDPR consent data', function () { let consentString = 'consentString'; let bidderRequest = { @@ -306,14 +327,131 @@ describe('onetag', function () { expect(payload.usPrivacy).to.exist; expect(payload.usPrivacy).to.exist.and.to.equal(consentString); }); + it('Should send FPD (ortb2 field)', function () { + const firtPartyData = { + // this is where the contextual data is placed + site: { + name: 'example', + domain: 'page.example.com', + // OpenRTB 2.5 spec / Content Taxonomy + cat: ['IAB2'], + sectioncat: ['IAB2-2'], + pagecat: ['IAB2-2'], + page: 'https://page.example.com/here.html', + ref: 'https://ref.example.com', + keywords: 'power tools, drills', + search: 'drill', + content: { + userrating: '4', + data: [{ + name: 'www.dataprovider1.com', // who resolved the segments + ext: { + segtax: 7, // taxonomy used to encode the segments + cids: ['iris_c73g5jq96mwso4d8'] + }, + // the bare minimum are the IDs. These IDs are the ones from the new IAB Content Taxonomy v3 + segment: [ { id: '687' }, { id: '123' } ] + }] + }, + ext: { + data: { // fields that aren't part of openrtb 2.6 + pageType: 'article', + category: 'repair' + } + } + }, + // this is where the user data is placed + user: { + keywords: 'a,b', + data: [{ + name: 'dataprovider.com', + ext: { + segtax: 4 + }, + segment: [{ + id: '1' + }] + }], + ext: { + data: { + registered: true, + interests: ['cars'] + } + } + }, + regs: { + gpp: 'abc1234', + gpp_sid: [7] + } + }; + let bidderRequest = { + 'bidderCode': 'onetag', + 'auctionId': '1d1a030790a475', + 'bidderRequestId': '22edbae2733bf6', + 'timeout': 3000, + 'ortb2': firtPartyData + } + let serverRequest = spec.buildRequests([bannerBid], bidderRequest); + const payload = JSON.parse(serverRequest.data); + expect(payload.ortb2).to.exist; + expect(payload.ortb2).to.exist.and.to.deep.equal(firtPartyData); + }); + }); + it('Should send FLEDGE eligibility flag when FLEDGE is enabled', function () { + let bidderRequest = { + 'bidderCode': 'onetag', + 'auctionId': '1d1a030790a475', + 'bidderRequestId': '22edbae2733bf6', + 'timeout': 3000, + 'fledgeEnabled': true + }; + let serverRequest = spec.buildRequests([bannerBid], bidderRequest); + const payload = JSON.parse(serverRequest.data); + + expect(payload.fledgeEnabled).to.exist; + expect(payload.fledgeEnabled).to.exist.and.to.equal(bidderRequest.fledgeEnabled); + }); + it('Should send FLEDGE eligibility flag when FLEDGE is not enabled', function () { + let bidderRequest = { + 'bidderCode': 'onetag', + 'auctionId': '1d1a030790a475', + 'bidderRequestId': '22edbae2733bf6', + 'timeout': 3000, + 'fledgeEnabled': false + }; + let serverRequest = spec.buildRequests([bannerBid], bidderRequest); + const payload = JSON.parse(serverRequest.data); + + expect(payload.fledgeEnabled).to.exist; + expect(payload.fledgeEnabled).to.exist.and.to.equal(bidderRequest.fledgeEnabled); + }); + it('Should send FLEDGE eligibility flag set to false when fledgeEnabled is not defined', function () { + let bidderRequest = { + 'bidderCode': 'onetag', + 'auctionId': '1d1a030790a475', + 'bidderRequestId': '22edbae2733bf6', + 'timeout': 3000, + }; + let serverRequest = spec.buildRequests([bannerBid], bidderRequest); + const payload = JSON.parse(serverRequest.data); + + expect(payload.fledgeEnabled).to.exist; + expect(payload.fledgeEnabled).to.exist.and.to.equal(false); }); describe('interpretResponse', function () { const request = getBannerVideoRequest(); const response = getBannerVideoResponse(); + const fledgeResponse = getFledgeBannerResponse(); const requestData = JSON.parse(request.data); it('Returns an array of valid server responses if response object is valid', function () { const interpretedResponse = spec.interpretResponse(response, request); + const fledgeInterpretedResponse = spec.interpretResponse(fledgeResponse, request); expect(interpretedResponse).to.be.an('array').that.is.not.empty; + expect(fledgeInterpretedResponse).to.be.an('object'); + expect(fledgeInterpretedResponse.bids).to.satisfy(function (value) { + return value === null || Array.isArray(value); + }); + expect(fledgeInterpretedResponse.fledgeAuctionConfigs).to.be.an('array').that.is.not.empty; for (let i = 0; i < interpretedResponse.length; i++) { let dataItem = interpretedResponse[i]; expect(dataItem).to.include.all.keys('requestId', 'cpm', 'width', 'height', 'ttl', 'creativeId', 'netRevenue', 'currency', 'meta', 'dealId'); @@ -511,6 +649,24 @@ function getBannerVideoResponse() { }; } +function getFledgeBannerResponse() { + const bannerVideoResponse = getBannerVideoResponse(); + bannerVideoResponse.body.fledgeAuctionConfigs = [ + { + bidId: 'fledge', + config: { + seller: 'https://onetag-sys.com', + decisionLogicUrl: + 'https://onetag-sys.com/paapi/decision_logic.js', + interestGroupBuyers: [ + 'https://onetag-sys.com' + ], + } + } + ] + return bannerVideoResponse; +} + function getBannerVideoRequest() { return { data: JSON.stringify({ diff --git a/test/spec/modules/ooloAnalyticsAdapter_spec.js b/test/spec/modules/ooloAnalyticsAdapter_spec.js index 2515c713b14..1224c3f0740 100644 --- a/test/spec/modules/ooloAnalyticsAdapter_spec.js +++ b/test/spec/modules/ooloAnalyticsAdapter_spec.js @@ -663,7 +663,7 @@ describe('oolo Prebid Analytic', () => { events.emit(constants.EVENTS.AUCTION_INIT, { ...auctionInit }); - expect(server.requests[3].url).to.equal('https://pbjs.com') + expect(server.requests[3].url).to.equal('https://pbjs.com/') }) it('should send raw events based on server configuration', () => { diff --git a/test/spec/modules/openxBidAdapter_spec.js b/test/spec/modules/openxBidAdapter_spec.js index f2cff7f470c..1af0fce103d 100644 --- a/test/spec/modules/openxBidAdapter_spec.js +++ b/test/spec/modules/openxBidAdapter_spec.js @@ -14,6 +14,7 @@ import 'modules/consentManagement.js'; import 'modules/consentManagementUsp.js'; import 'modules/schain.js'; import {deepClone} from 'src/utils.js'; +import {version} from 'package.json'; import {syncAddFPDToBidderRequest} from '../../helpers/fpd.js'; import {hook} from '../../../src/hook.js'; @@ -316,6 +317,7 @@ describe('OpenxRtbAdapter', function () { const request = spec.buildRequests(bidRequestsWithMediaTypes, mockBidderRequest); expect(request[0].url).to.equal(REQUEST_URL); expect(request[0].method).to.equal('POST'); + expect(request[0].data.ext.pv).to.equal(version); }); it('should send delivery domain, if available', function () { diff --git a/test/spec/modules/operaadsBidAdapter_spec.js b/test/spec/modules/operaadsBidAdapter_spec.js index 37d4a2c7bc0..9a8981235d5 100644 --- a/test/spec/modules/operaadsBidAdapter_spec.js +++ b/test/spec/modules/operaadsBidAdapter_spec.js @@ -266,6 +266,95 @@ describe('Opera Ads Bid Adapter', function () { } }); + describe('test fulfilling inventory information', function () { + const bidRequest = { + adUnitCode: 'test-div', + auctionId: 'b06c5141-fe8f-4cdf-9d7d-54415490a917', + bidId: '22c4871113f461', + bidder: 'operaads', + bidderRequestId: '15246a574e859f', + mediaTypes: { + banner: {sizes: [[300, 250]]} + }, + params: { + placementId: 's12345678', + publisherId: 'pub12345678', + endpointId: 'ep12345678' + } + } + + const getRequest = function () { + let reqs; + expect(function () { + reqs = spec.buildRequests([bidRequest], bidderRequest); + }).to.not.throw(); + return JSON.parse(reqs[0].data); + } + + it('test default case', function () { + let requestData = getRequest(); + expect(requestData.site).to.be.an('object'); + expect(requestData.site.id).to.equal(bidRequest.params.publisherId); + expect(requestData.site.domain).to.not.be.empty; + expect(requestData.site.page).to.equal(bidderRequest.refererInfo.page); + }); + + it('test a case with site information specified', function () { + bidRequest.params = { + placementId: 's12345678', + publisherId: 'pub12345678', + endpointId: 'ep12345678', + site: { + name: 'test-site-1', + domain: 'www.test.com' + } + } + let requestData = getRequest(); + expect(requestData.site).to.be.an('object'); + expect(requestData.site.id).to.equal(bidRequest.params.publisherId); + expect(requestData.site.name).to.equal('test-site-1'); + expect(requestData.site.domain).to.equal('www.test.com'); + expect(requestData.site.page).to.equal(bidderRequest.refererInfo.page); + }); + + it('test a case with app information specified', function () { + bidRequest.params = { + placementId: 's12345678', + publisherId: 'pub12345678', + endpointId: 'ep12345678', + app: { + name: 'test-app-1' + } + } + let requestData = getRequest(); + expect(requestData.app).to.be.an('object'); + expect(requestData.app.id).to.equal(bidRequest.params.publisherId); + expect(requestData.app.name).to.equal('test-app-1'); + expect(requestData.app.domain).to.not.be.empty; + }); + + it('test a case with both site and app information specified', function () { + bidRequest.params = { + placementId: 's12345678', + publisherId: 'pub12345678', + endpointId: 'ep12345678', + site: { + name: 'test-site-2', + page: 'test-page' + }, + app: { + name: 'test-app-1' + } + } + let requestData = getRequest(); + expect(requestData.site).to.be.an('object'); + expect(requestData.site.id).to.equal(bidRequest.params.publisherId); + expect(requestData.site.name).to.equal('test-site-2'); + expect(requestData.site.page).to.equal('test-page'); + expect(requestData.site.domain).to.not.be.empty; + }); + }); + it('test getBidFloor', function() { const bidRequests = [ { diff --git a/test/spec/modules/operaadsIdSystem_spec.js b/test/spec/modules/operaadsIdSystem_spec.js new file mode 100644 index 00000000000..d81f643d62f --- /dev/null +++ b/test/spec/modules/operaadsIdSystem_spec.js @@ -0,0 +1,53 @@ +import { operaIdSubmodule } from 'modules/operaadsIdSystem' +import * as ajaxLib from 'src/ajax.js' + +const TEST_ID = 'opera-test-id'; +const operaIdRemoteResponse = { uid: TEST_ID }; + +describe('operaId submodule properties', () => { + it('should expose a "name" property equal to "operaId"', () => { + expect(operaIdSubmodule.name).to.equal('operaId'); + }); +}); + +function fakeRequest(fn) { + const ajaxBuilderStub = sinon.stub(ajaxLib, 'ajaxBuilder').callsFake(() => { + return (url, cbObj) => { + cbObj.success(JSON.stringify(operaIdRemoteResponse)); + } + }); + fn(); + ajaxBuilderStub.restore(); +} + +describe('operaId submodule getId', function() { + it('request to the fake server to correctly extract test ID', function() { + fakeRequest(() => { + const moduleIdCallbackResponse = operaIdSubmodule.getId({ params: { pid: 'pub123' } }); + moduleIdCallbackResponse.callback((id) => { + expect(id).to.equal(operaIdRemoteResponse.operaId); + }); + }); + }); + + it('request to the fake server without publiser ID', function() { + fakeRequest(() => { + const moduleIdCallbackResponse = operaIdSubmodule.getId({ params: {} }); + expect(moduleIdCallbackResponse).to.equal(undefined); + }); + }); +}); + +describe('operaId submodule decode', function() { + it('should respond with an object containing "operaId" as key with the value', () => { + expect(operaIdSubmodule.decode(TEST_ID)).to.deep.equal({ + operaId: TEST_ID + }); + }); + + it('should respond with undefined if the value is not a string or an empty string', () => { + [1, 2.0, null, undefined, NaN, [], {}].forEach((value) => { + expect(operaIdSubmodule.decode(value)).to.equal(undefined); + }); + }); +}); diff --git a/test/spec/modules/optidigitalBidAdapter_spec.js b/test/spec/modules/optidigitalBidAdapter_spec.js index caa12483ea9..30e72452c39 100755 --- a/test/spec/modules/optidigitalBidAdapter_spec.js +++ b/test/spec/modules/optidigitalBidAdapter_spec.js @@ -1,5 +1,5 @@ import { expect } from 'chai'; -import { spec } from 'modules/optidigitalBidAdapter.js'; +import { spec, resetSync } from 'modules/optidigitalBidAdapter.js'; import * as utils from 'src/utils.js'; const ENDPOINT = 'https://pbs.optidigital.com/bidder'; @@ -479,6 +479,28 @@ describe('optidigitalAdapterTests', function () { expect(payload.imp[0].bidFloor).to.exist; }); + it('should add userEids to payload', function() { + const userIdAsEids = [{ + source: 'pubcid.org', + uids: [{ + id: '121213434342343', + atype: 1 + }] + }]; + validBidRequests[0].userIdAsEids = userIdAsEids; + bidderRequest.userIdAsEids = userIdAsEids; + const request = spec.buildRequests(validBidRequests, bidderRequest); + const payload = JSON.parse(request.data); + expect(payload.user.eids).to.deep.equal(userIdAsEids); + }); + + it('should not add userIdAsEids to payload when userIdAsEids is not present', function() { + validBidRequests[0].userIdAsEids = undefined; + const request = spec.buildRequests(validBidRequests, bidderRequest); + const payload = JSON.parse(request.data); + expect(payload.user).to.deep.equal(undefined); + }); + function returnBannerSizes(mediaTypes, expectedSizes) { const bidRequest = Object.assign(validBidRequests[0], mediaTypes); const request = spec.buildRequests([bidRequest], bidderRequest); @@ -497,6 +519,7 @@ describe('optidigitalAdapterTests', function () { let test; beforeEach(function () { test = sinon.sandbox.create(); + resetSync(); }); afterEach(function() { test.restore(); @@ -508,16 +531,22 @@ describe('optidigitalAdapterTests', function () { }]); }); - it('should return appropriate URL', function() { + it('should return appropriate URL with GDPR equals to 1 and GDPR consent', function() { expect(spec.getUserSyncs({ iframeEnabled: true }, {}, {gdprApplies: true, consentString: 'foo'}, undefined)).to.deep.equal([{ type: 'iframe', url: `${syncurlIframe}&gdpr=1&gdpr_consent=foo` }]); + }); + it('should return appropriate URL with GDPR equals to 0 and GDPR consent', function() { expect(spec.getUserSyncs({ iframeEnabled: true }, {}, {gdprApplies: false, consentString: 'foo'}, undefined)).to.deep.equal([{ type: 'iframe', url: `${syncurlIframe}&gdpr=0&gdpr_consent=foo` }]); + }); + it('should return appropriate URL with GDPR equals to 1 and no consent', function() { expect(spec.getUserSyncs({ iframeEnabled: true }, {}, {gdprApplies: true, consentString: undefined}, undefined)).to.deep.equal([{ type: 'iframe', url: `${syncurlIframe}&gdpr=1&gdpr_consent=` }]); + }); + it('should return appropriate URL with GDPR equals to 1, GDPR consent and CCPA consent', function() { expect(spec.getUserSyncs({ iframeEnabled: true }, {}, {gdprApplies: true, consentString: 'foo'}, {consentString: 'fooUsp'})).to.deep.equal([{ type: 'iframe', url: `${syncurlIframe}&gdpr=1&gdpr_consent=foo&ccpa_consent=fooUsp` }]); diff --git a/test/spec/modules/optimeraRtdProvider_spec.js b/test/spec/modules/optimeraRtdProvider_spec.js index aec8b79045e..8a9f000bbb9 100644 --- a/test/spec/modules/optimeraRtdProvider_spec.js +++ b/test/spec/modules/optimeraRtdProvider_spec.js @@ -21,13 +21,80 @@ describe('Optimera RTD sub module', () => { }); }); -describe('Optimera RTD score file url is properly set', () => { - it('Proerly set the score file url', () => { +describe('Optimera RTD score file URL is properly set for v0', () => { + it('should properly set the score file URL', () => { + const conf = { + dataProviders: [{ + name: 'optimeraRTD', + params: { + clientID: '9999', + optimeraKeyName: 'optimera', + device: 'de', + apiVersion: 'v0', + } + }] + }; + optimeraRTD.init(conf.dataProviders[0]); + optimeraRTD.setScores(); + expect(optimeraRTD.apiVersion).to.equal('v0'); + expect(optimeraRTD.scoresURL).to.equal('https://dyv1bugovvq1g.cloudfront.net/9999/localhost:9876/context.html.js'); + }); + + it('should properly set the score file URL without apiVersion set', () => { + const conf = { + dataProviders: [{ + name: 'optimeraRTD', + params: { + clientID: '9999', + optimeraKeyName: 'optimera', + device: 'de', + } + }] + }; + optimeraRTD.init(conf.dataProviders[0]); + optimeraRTD.setScores(); + expect(optimeraRTD.apiVersion).to.equal('v0'); + expect(optimeraRTD.scoresURL).to.equal('https://dyv1bugovvq1g.cloudfront.net/9999/localhost:9876/context.html.js'); + }); + + it('should properly set the score file URL with an api version other than v0 or v1', () => { + const conf = { + dataProviders: [{ + name: 'optimeraRTD', + params: { + clientID: '9999', + optimeraKeyName: 'optimera', + device: 'de', + apiVersion: 'v15', + } + }] + }; + optimeraRTD.init(conf.dataProviders[0]); optimeraRTD.setScores(); expect(optimeraRTD.scoresURL).to.equal('https://dyv1bugovvq1g.cloudfront.net/9999/localhost:9876/context.html.js'); }); }); +describe('Optimera RTD score file URL is properly set for v1', () => { + it('should properly set the score file URL', () => { + const conf = { + dataProviders: [{ + name: 'optimeraRTD', + params: { + clientID: '9999', + optimeraKeyName: 'optimera', + device: 'de', + apiVersion: 'v1', + } + }] + }; + optimeraRTD.init(conf.dataProviders[0]); + optimeraRTD.setScores(); + expect(optimeraRTD.apiVersion).to.equal('v1'); + expect(optimeraRTD.scoresURL).to.equal('https://v1.oapi26b.com/api/products/scores?c=9999&h=localhost:9876&p=/context.html&s=de'); + }); +}); + describe('Optimera RTD score file properly sets targeting values', () => { const scores = { 'div-0': ['A1', 'A2'], diff --git a/test/spec/modules/orbidderBidAdapter_spec.js b/test/spec/modules/orbidderBidAdapter_spec.js index e7a0c8becfb..cf58d35e636 100644 --- a/test/spec/modules/orbidderBidAdapter_spec.js +++ b/test/spec/modules/orbidderBidAdapter_spec.js @@ -1,6 +1,6 @@ -import {expect} from 'chai'; -import {spec} from 'modules/orbidderBidAdapter.js'; -import {newBidder} from 'src/adapters/bidderFactory.js'; +import { expect } from 'chai'; +import { spec } from 'modules/orbidderBidAdapter.js'; +import { newBidder } from 'src/adapters/bidderFactory.js'; import * as _ from 'lodash'; import { BANNER, NATIVE } from '../../../src/mediaTypes.js'; @@ -9,39 +9,57 @@ describe('orbidderBidAdapter', () => { const defaultBidRequestBanner = { bidId: 'd66fa86787e0b0ca900a96eacfd5f0bb', auctionId: 'ccc4c7cdfe11cfbd74065e6dd28413d8', - ortb2Imp: { - ext: { - tid: 'd58851660c0c4461e4aa06344fc9c0c6', - } - }, + transactionId: 'd58851660c0c4461e4aa06344fc9c0c6', bidRequestCount: 1, adUnitCode: 'adunit-code', sizes: [[300, 250], [300, 600]], params: { 'accountId': 'string1', - 'placementId': 'string2' + 'placementId': 'string2', + 'bidfloor': 1.23 }, mediaTypes: { banner: { - sizes: [[300, 250], [300, 600]], + sizes: [[300, 250], [300, 600]] } - } + }, + userId: { + 'id5id': { + 'uid': 'ID5*XXXXXXXXXXXXX', + 'ext': { + 'linkType': 2, + 'pba': 'XXXXXXXXXXXX==' + } + } + }, + userIdAsEids: [ + { + 'source': 'id5-sync.com', + 'uids': [ + { + 'id': 'ID5*XXXXXXXXXXXXX', + 'atype': 1, + 'ext': { + 'linkType': 2, + 'pba': 'XXXXXXXXXXXX==' + } + } + ] + } + ] }; const defaultBidRequestNative = { bidId: 'd66fa86787e0b0ca900a96eacfd5f0bc', auctionId: 'ccc4c7cdfe11cfbd74065e6dd28413d9', - ortb2Imp: { - ext: { - tid: 'd58851660c0c4461e4aa06344fc9c0c7', - } - }, + transactionId: 'd58851660c0c4461e4aa06344fc9c0c6', bidRequestCount: 1, adUnitCode: 'adunit-code-native', sizes: [], params: { 'accountId': 'string3', - 'placementId': 'string4' + 'placementId': 'string4', + 'bidfloor': 2.34 }, mediaTypes: { native: { @@ -56,10 +74,34 @@ describe('orbidderBidAdapter', () => { required: true } } - } + }, + userId: { + 'id5id': { + 'uid': 'ID5*YYYYYYYYYYYYYYY', + 'ext': { + 'linkType': 2, + 'pba': 'YYYYYYYYYYYYY==' + } + } + }, + userIdAsEids: [ + { + 'source': 'id5-sync.com', + 'uids': [ + { + 'id': 'ID5*YYYYYYYYYYYYYYY', + 'atype': 1, + 'ext': { + 'linkType': 2, + 'pba': 'YYYYYYYYYYYYY==' + } + } + ] + } + ] }; - const deepClone = function (val) { + const deepClone = function(val) { return JSON.parse(JSON.stringify(val)); }; @@ -91,15 +133,15 @@ describe('orbidderBidAdapter', () => { expect(spec.isBidRequestValid(defaultBidRequestNative)).to.equal(true); }); - it('banner: accepts optional profile object', () => { + it('banner: accepts optional keyValues object', () => { const bidRequest = deepClone(defaultBidRequestBanner); - bidRequest.params.profile = {'key': 'value'}; + bidRequest.params.keyValues = { 'key': 'value' }; expect(spec.isBidRequestValid(bidRequest)).to.equal(true); }); - it('native: accepts optional profile object', () => { + it('native: accepts optional keyValues object', () => { const bidRequest = deepClone(defaultBidRequestNative); - bidRequest.params.profile = {'key': 'value'}; + bidRequest.params.keyValues = { 'key': 'value' }; expect(spec.isBidRequestValid(bidRequest)).to.equal(true); }); @@ -115,15 +157,15 @@ describe('orbidderBidAdapter', () => { expect(spec.isBidRequestValid(bidRequest)).to.equal(false); }); - it('banner: doesn\'t accept malformed profile', () => { + it('banner: doesn\'t accept malformed keyValues', () => { const bidRequest = deepClone(defaultBidRequestBanner); - bidRequest.params.profile = 'another not usable string'; + bidRequest.params.keyValues = 'another not usable string'; expect(spec.isBidRequestValid(bidRequest)).to.equal(false); }); - it('native: doesn\'t accept malformed profile', () => { + it('native: doesn\'t accept malformed keyValues', () => { const bidRequest = deepClone(defaultBidRequestNative); - bidRequest.params.profile = 'another not usable string'; + bidRequest.params.keyValues = 'another not usable string'; expect(spec.isBidRequestValid(bidRequest)).to.equal(false); }); @@ -159,8 +201,11 @@ describe('orbidderBidAdapter', () => { }); describe('buildRequests', () => { - const request = buildRequest(defaultBidRequestBanner); - const nativeRequest = buildRequest(defaultBidRequestNative); + let request, nativeRequest; + before(() => { + request = buildRequest(defaultBidRequestBanner); + nativeRequest = buildRequest(defaultBidRequestNative); + }) it('sends bid request to endpoint via https using post', () => { expect(request.method).to.equal('POST'); @@ -176,34 +221,20 @@ describe('orbidderBidAdapter', () => { // we add two, because we add pageUrl and version from bidderRequest object expect(Object.keys(request.data).length).to.equal(Object.keys(defaultBidRequestBanner).length + 2); - expect(request.data.bidId).to.equal(defaultBidRequestBanner.bidId); - expect(request.data.auctionId).to.equal(defaultBidRequestBanner.auctionId); - expect(request.data.transactionId).to.equal(defaultBidRequestBanner.ortb2Imp.ext.tid); - expect(request.data.bidRequestCount).to.equal(defaultBidRequestBanner.bidRequestCount); - expect(request.data.adUnitCode).to.equal(defaultBidRequestBanner.adUnitCode); - expect(request.data.pageUrl).to.equal('https://localhost:9876/'); - expect(request.data.v).to.equal($$PREBID_GLOBAL$$.version); - expect(request.data.sizes).to.equal(defaultBidRequestBanner.sizes); - - expect(_.isEqual(request.data.params, defaultBidRequestBanner.params)).to.be.true; - expect(_.isEqual(request.data.mediaTypes, defaultBidRequestBanner.mediaTypes)).to.be.true; + const expectedBidRequest = deepClone(defaultBidRequestBanner); + expectedBidRequest.pageUrl = 'https://localhost:9876/'; + expectedBidRequest.v = $$PREBID_GLOBAL$$.version; + expect(request.data).to.deep.equal(expectedBidRequest); }); it('native: sends correct bid parameters', () => { // we add two, because we add pageUrl and version from bidderRequest object expect(Object.keys(nativeRequest.data).length).to.equal(Object.keys(defaultBidRequestNative).length + 2); - expect(nativeRequest.data.bidId).to.equal(defaultBidRequestNative.bidId); - expect(nativeRequest.data.auctionId).to.equal(defaultBidRequestNative.auctionId); - expect(nativeRequest.data.transactionId).to.equal(defaultBidRequestNative.ortb2Imp.ext.tid); - expect(nativeRequest.data.bidRequestCount).to.equal(defaultBidRequestNative.bidRequestCount); - expect(nativeRequest.data.adUnitCode).to.equal(defaultBidRequestNative.adUnitCode); - expect(nativeRequest.data.pageUrl).to.equal('https://localhost:9876/'); - expect(nativeRequest.data.v).to.equal($$PREBID_GLOBAL$$.version); - expect(nativeRequest.data.sizes).to.be.empty; - - expect(_.isEqual(nativeRequest.data.params, defaultBidRequestNative.params)).to.be.true; - expect(_.isEqual(nativeRequest.data.mediaTypes, defaultBidRequestNative.mediaTypes)).to.be.true; + const expectedBidRequest = deepClone(defaultBidRequestNative); + expectedBidRequest.pageUrl = 'https://localhost:9876/'; + expectedBidRequest.v = $$PREBID_GLOBAL$$.version; + expect(nativeRequest.data).to.deep.equal(expectedBidRequest); }); it('banner: handles empty gdpr object', () => { @@ -291,7 +322,7 @@ describe('orbidderBidAdapter', () => { }); }); - describe('buildRequests with price floor module', () => { + it('buildRequests with price floor module', () => { const bidRequest = deepClone(defaultBidRequestBanner); bidRequest.params.bidfloor = 1; bidRequest.getFloor = (floorObj) => { @@ -344,7 +375,7 @@ describe('orbidderBidAdapter', () => { } ]; - const result = spec.interpretResponse({body: serverResponse}); + const result = spec.interpretResponse({ body: serverResponse }); expect(result.length).to.equal(expectedResponse.length); expect(_.isEqual(expectedResponse, serverResponse)).to.be.true; }); @@ -384,7 +415,7 @@ describe('orbidderBidAdapter', () => { } ]; - const result = spec.interpretResponse({body: serverResponse}); + const result = spec.interpretResponse({ body: serverResponse }); expect(result.length).to.equal(expectedResponse.length); Object.keys(expectedResponse[0]).forEach((key) => { @@ -451,7 +482,7 @@ describe('orbidderBidAdapter', () => { } ]; - const result = spec.interpretResponse({body: serverResponse}); + const result = spec.interpretResponse({ body: serverResponse }); expect(result.length).to.equal(expectedResponse.length); expect(_.isEqual(expectedResponse, serverResponse)).to.be.true; @@ -471,7 +502,7 @@ describe('orbidderBidAdapter', () => { 'netRevenue': true, } ]; - const result = spec.interpretResponse({body: serverResponse}); + const result = spec.interpretResponse({ body: serverResponse }); expect(result.length).to.equal(0); }); @@ -489,7 +520,7 @@ describe('orbidderBidAdapter', () => { 'creativeId': '29681110', } ]; - const result = spec.interpretResponse({body: serverResponse}); + const result = spec.interpretResponse({ body: serverResponse }); expect(result.length).to.equal(0); }); @@ -515,13 +546,13 @@ describe('orbidderBidAdapter', () => { } } ]; - const result = spec.interpretResponse({body: serverResponse}); + const result = spec.interpretResponse({ body: serverResponse }); expect(result.length).to.equal(0); }); it('handles nobid responses', () => { const serverResponse = []; - const result = spec.interpretResponse({body: serverResponse}); + const result = spec.interpretResponse({ body: serverResponse }); expect(result.length).to.equal(0); }); }); diff --git a/test/spec/modules/outbrainBidAdapter_spec.js b/test/spec/modules/outbrainBidAdapter_spec.js index ab72ff85ab0..e6abb5e9caa 100644 --- a/test/spec/modules/outbrainBidAdapter_spec.js +++ b/test/spec/modules/outbrainBidAdapter_spec.js @@ -1,9 +1,7 @@ import { expect } from 'chai'; -import { spec } from 'modules/outbrainBidAdapter.js'; +import { spec, storage } from 'modules/outbrainBidAdapter.js'; import { config } from 'src/config.js'; import { server } from 'test/mocks/xhr'; -import { createEidsArray } from 'modules/userId/eids.js'; -import * as utils from 'src/utils.js'; describe('Outbrain Adapter', function () { describe('Bid request and response', function () { @@ -215,15 +213,18 @@ describe('Outbrain Adapter', function () { }) describe('buildRequests', function () { + let getDataFromLocalStorageStub; + before(() => { + getDataFromLocalStorageStub = sinon.stub(storage, 'getDataFromLocalStorage') config.setConfig({ outbrain: { bidderUrl: 'https://bidder-url.com', } - } - ) + }) }) after(() => { + getDataFromLocalStorageStub.restore() config.resetConfig() }) @@ -426,7 +427,7 @@ describe('Outbrain Adapter', function () { expect(resData.badv).to.deep.equal(['bad-advertiser']) }); - it('first party data', function () { + it('should pass first party data', function () { const bidRequest = { ...commonBidRequest, ...nativeBidRequestParams, @@ -507,13 +508,35 @@ describe('Outbrain Adapter', function () { config.resetConfig() }); + it('should pass gpp information', function () { + const bidRequest = { + ...commonBidRequest, + ...nativeBidRequestParams, + }; + const bidderRequest = { + ...commonBidderRequest, + 'gppConsent': { + 'gppString': 'abc12345', + 'applicableSections': [8] + } + } + + const res = spec.buildRequests([bidRequest], bidderRequest); + const resData = JSON.parse(res.data); + + expect(resData.regs.ext.gpp).to.exist; + expect(resData.regs.ext.gpp_sid).to.exist; + expect(resData.regs.ext.gpp).to.equal('abc12345'); + expect(resData.regs.ext.gpp_sid).to.deep.equal([8]); + }); + it('should pass extended ids', function () { let bidRequest = { bidId: 'bidId', params: {}, - userIdAsEids: createEidsArray({ - idl_env: 'id-value', - }), + userIdAsEids: [ + { source: 'liveramp.com', uids: [{ id: 'id-value', atype: 3 }] } + ], ...commonBidRequest, }; @@ -524,6 +547,22 @@ describe('Outbrain Adapter', function () { ]); }); + it('should pass OB user token', function () { + getDataFromLocalStorageStub.returns('12345'); + + let bidRequest = { + bidId: 'bidId', + params: {}, + ...commonBidRequest, + }; + + let res = spec.buildRequests([bidRequest], commonBidderRequest); + const resData = JSON.parse(res.data) + expect(resData.user.ext.obusertoken).to.equal('12345') + expect(getDataFromLocalStorageStub.called).to.be.true; + sinon.assert.calledWith(getDataFromLocalStorageStub, 'OB-USER-TOKEN'); + }); + it('should pass bidfloor', function () { const bidRequest = { ...commonBidRequest, @@ -844,6 +883,12 @@ describe('Outbrain Adapter', function () { type: 'image', url: `${usersyncUrl}?gdpr=1&gdpr_consent=foo&us_privacy=1NYN` }]); }); + + it('should pass gpp consent', function () { + expect(spec.getUserSyncs({ pixelEnabled: true }, {}, undefined, '', { gppString: 'abc12345', applicableSections: [1, 2] })).to.deep.equal([{ + type: 'image', url: `${usersyncUrl}?gpp=abc12345&gpp_sid=1%2C2` + }]); + }); }) describe('onBidWon', function () { diff --git a/test/spec/modules/oxxionAnalyticsAdapter_spec.js b/test/spec/modules/oxxionAnalyticsAdapter_spec.js index 63e929d7160..9d06be24f68 100644 --- a/test/spec/modules/oxxionAnalyticsAdapter_spec.js +++ b/test/spec/modules/oxxionAnalyticsAdapter_spec.js @@ -1,4 +1,5 @@ import oxxionAnalytics from 'modules/oxxionAnalyticsAdapter.js'; +import {dereferenceWithoutRenderer} from 'modules/oxxionAnalyticsAdapter.js'; import { expect } from 'chai'; import { server } from 'test/mocks/xhr.js'; let adapterManager = require('src/adapterManager').default; @@ -85,20 +86,21 @@ describe('Oxxion Analytics', function () { } }, 'adUnitCode': 'tag_200124_banner', - 'transactionId': 'de664ccb-e18b-4436-aeb0-362382eb1b40', + 'transactionId': '8b2a8629-d1ea-4bb1-aff0-e335b96dd002', 'sizes': [ [ 300, 600 ] ], - 'bidId': '34a63e5d5378a3', + 'bidId': '2bd3e8ff8a113f', 'bidderRequestId': '11dc6ff6378de7', 'auctionId': '1e8b993d-8f0a-4232-83eb-3639ddf3a44b', 'src': 'client', 'bidRequestsCount': 1, 'bidderRequestsCount': 1, - 'bidderWinsCount': 0 + 'bidderWinsCount': 0, + 'ova': 'cleared' } ], 'auctionStart': 1647424261187, @@ -148,14 +150,14 @@ describe('Oxxion Analytics', function () { 'bidsReceived': [ { 'bidderCode': 'appnexus', - 'width': 300, - 'height': 600, + 'width': 970, + 'height': 250, 'statusMessage': 'Bid available', - 'adId': '7a4ced80f33d33', - 'requestId': '34a63e5d5378a3', - 'transactionId': 'de664ccb-e18b-4436-aeb0-362382eb1b40', + 'adId': '65d16ef039a97a', + 'requestId': '2bd3e8ff8a113f', + 'transactionId': '8b2a8629-d1ea-4bb1-aff0-e335b96dd002', 'auctionId': '1e8b993d-8f0a-4232-83eb-3639ddf3a44b', - 'mediaType': 'banner', + 'mediaType': 'video', 'source': 'client', 'cpm': 27.4276, 'creativeId': '158534630', @@ -166,8 +168,10 @@ describe('Oxxion Analytics', function () { 'meta': { 'advertiserDomains': [ 'example.com' - ] + ], + 'demandSource': 'something' }, + 'renderer': 'something', 'originalCpm': 25.02521, 'originalCurrency': 'EUR', 'responseTimestamp': 1647424261559, @@ -184,7 +188,7 @@ describe('Oxxion Analytics', function () { 'size': '300x600', 'adserverTargeting': { 'hb_bidder': 'appnexus', - 'hb_adid': '7a4ced80f33d33', + 'hb_adid': '65d16ef039a97a', 'hb_pb': '20.000000', 'hb_size': '300x600', 'hb_source': 'client', @@ -221,6 +225,7 @@ describe('Oxxion Analytics', function () { 'example.com' ] }, + 'renderer': 'something', 'originalCpm': 25.02521, 'originalCurrency': 'EUR', 'responseTimestamp': 1647424261558, @@ -267,7 +272,24 @@ describe('Oxxion Analytics', function () { oxxionAnalytics.disableAnalytics(); oxxionAnalytics.track.restore(); }); + it('test dereferenceWithoutRenderer', function () { + adapterManager.registerAnalyticsAdapter({ + code: 'oxxion', + adapter: oxxionAnalytics + }); + adapterManager.enableAnalytics({ + provider: 'oxxion', + options: { + domain: 'test' + } + }); + let resultBidWon = JSON.parse(dereferenceWithoutRenderer(bidWon)); + expect(resultBidWon).not.to.have.property('renderer'); + let resultBid = JSON.parse(dereferenceWithoutRenderer(auctionEnd)); + expect(resultBid).to.have.property('bidsReceived').and.to.have.lengthOf(1); + expect(resultBid.bidsReceived[0]).not.to.have.property('renderer'); + }); it('test auctionEnd', function () { adapterManager.registerAnalyticsAdapter({ code: 'oxxion', @@ -293,14 +315,16 @@ describe('Oxxion Analytics', function () { expect(message.auctionEnd[0].bidsReceived[0]).not.to.have.property('ad'); expect(message.auctionEnd[0].bidsReceived[0]).to.have.property('meta'); expect(message.auctionEnd[0].bidsReceived[0].meta).to.have.property('advertiserDomains'); + expect(message.auctionEnd[0].bidsReceived[0].meta).to.have.property('demandSource'); expect(message.auctionEnd[0].bidsReceived[0]).to.have.property('adId'); expect(message.auctionEnd[0]).to.have.property('bidderRequests').and.to.have.lengthOf(1); expect(message.auctionEnd[0].bidderRequests[0]).to.have.property('gdprConsent'); expect(message.auctionEnd[0].bidderRequests[0].gdprConsent).not.to.have.property('vendorData'); - sinon.assert.callCount(oxxionAnalytics.track, 5); + expect(message.auctionEnd[0].bidderRequests[0]).to.have.property('oxxionMode'); }); it('test bidWon', function() { + window.OXXION_MODE = {'abtest': true}; adapterManager.registerAnalyticsAdapter({ code: 'oxxion', adapter: oxxionAnalytics @@ -318,7 +342,8 @@ describe('Oxxion Analytics', function () { expect(message).not.to.have.property('ad'); expect(message).to.have.property('adId') expect(message).to.have.property('cpmIncrement').and.to.equal(27.4276); - // sinon.assert.callCount(oxxionAnalytics.track, 1); + expect(message).to.have.property('oxxionMode').and.to.have.property('abtest').and.to.equal(true); + expect(message).to.have.property('ova').and.to.equal('cleared'); }); }); }); diff --git a/test/spec/modules/oxxionRtdProvider_spec.js b/test/spec/modules/oxxionRtdProvider_spec.js index 5bd8756bb6f..2a8024f3565 100644 --- a/test/spec/modules/oxxionRtdProvider_spec.js +++ b/test/spec/modules/oxxionRtdProvider_spec.js @@ -6,7 +6,10 @@ const utils = require('src/utils.js'); const moduleConfig = { params: { domain: 'test.endpoint', - contexts: ['instream', 'outstream'] + contexts: ['instream', 'outstream'], + samplingRate: 10, + threshold: false, + bidders: ['appnexus', 'mediasquare'], } }; @@ -31,6 +34,14 @@ let request = { ], 'transactionId': 'de664ccb-e18b-4436-aeb0-362382eb1b41' }, + { + 'code': 'msq_tag_200125_banner', + 'mediaTypes': { 'banner': { 'sizes': [[300, 250]] } }, + 'bids': [ + {'bidder': 'appnexusAst', 'params': {'placementId': 345678}}, + ], + 'transactionId': 'de664ccb-e18b-4436-aeb0-362382eb1b41' + } ] }; @@ -102,6 +113,22 @@ let bids = [{ }, ]; +let bidInterests = [ + {'id': 0, 'rate': 50.0, 'suggestion': true}, + {'id': 1, 'rate': 12.0, 'suggestion': false}, + {'id': 2, 'rate': 0.0, 'suggestion': true}, + {'id': 3, 'rate': 0.0, 'suggestion': false}, +]; + +const userConsent = { + 'gdpr': { + 'consentString': 'consent_hash' + }, + 'usp': null, + 'gpp': null, + 'coppa': false +}; + describe('oxxionRtdProvider', () => { describe('Oxxion RTD sub module', () => { it('should init, return true, and set the params', () => { @@ -114,30 +141,20 @@ describe('oxxionRtdProvider', () => { auctionEnd.bidsReceived = bids; it('call everything', function() { oxxionSubmodule.getBidRequestData(request, null, moduleConfig); - oxxionSubmodule.onBidResponseEvent(auctionEnd.bidsReceived[0], moduleConfig); - oxxionSubmodule.onBidResponseEvent(auctionEnd.bidsReceived[1], moduleConfig); - }); - it('check vastImpUrl', function() { - expect(auctionEnd.bidsReceived[0]).to.have.property('vastImpUrl'); - let expectVastImpUrl = 'https://' + moduleConfig.params.domain + '.oxxion.io/analytics/vast_imp?'; - expect(auctionEnd.bidsReceived[1].vastImpUrl).to.contain(expectVastImpUrl); - expect(auctionEnd.bidsReceived[1].vastImpUrl).to.contain(encodeURI('https://some.tracking-url.com')); - }); - it('check vastXml', function() { - expect(auctionEnd.bidsReceived[0]).to.have.property('vastXml'); - let vastWrapper = new DOMParser().parseFromString(auctionEnd.bidsReceived[0].vastXml, 'text/xml'); - let impressions = vastWrapper.querySelectorAll('VAST Ad Wrapper Impression'); - expect(impressions.length).to.equal(2); - expect(auctionEnd.bidsReceived[1]).to.have.property('vastXml'); - expect(auctionEnd.bidsReceived[1].adId).to.equal('4b2e1581c0ca1a'); - let vastInline = new DOMParser().parseFromString(auctionEnd.bidsReceived[1].vastXml, 'text/xml'); - let inline = vastInline.querySelectorAll('VAST Ad InLine'); - expect(inline).to.have.lengthOf(1); - let inlineImpressions = vastInline.querySelectorAll('VAST Ad InLine Impression'); - expect(inlineImpressions).to.have.lengthOf.above(0); }); - it('check cpmIncrement', function() { - expect(auctionEnd.bidsReceived[1].vastImpUrl).to.contain(encodeURI('cpmIncrement=0')); + it('check bid filtering', function() { + let requestsList = oxxionSubmodule.getRequestsList(request); + expect(requestsList.length).to.equal(4); + expect(requestsList[0]).to.have.property('id'); + expect(request.adUnits[0].bids[0]).to.have.property('_id'); + expect(requestsList[0].id).to.equal(request.adUnits[0].bids[0]._id); + const [filteredBiddderRequests, filteredBids] = oxxionSubmodule.getFilteredAdUnitsOnBidRates(bidInterests, request.adUnits, moduleConfig.params, false); + expect(filteredBids.length).to.equal(1); + expect(filteredBiddderRequests.length).to.equal(3); + expect(filteredBiddderRequests[0]).to.have.property('bids'); + expect(filteredBiddderRequests[0].bids.length).to.equal(1); + expect(filteredBiddderRequests[1]).to.have.property('bids'); + expect(filteredBiddderRequests[1].bids.length).to.equal(1); }); }); }); diff --git a/test/spec/modules/ozoneBidAdapter_spec.js b/test/spec/modules/ozoneBidAdapter_spec.js index 4c7e330b237..73df2fba8fd 100644 --- a/test/spec/modules/ozoneBidAdapter_spec.js +++ b/test/spec/modules/ozoneBidAdapter_spec.js @@ -6,7 +6,6 @@ import {getGranularityKeyName, getGranularityObject} from '../../../modules/ozon import * as utils from '../../../src/utils.js'; const OZONEURI = 'https://elb.the-ozone-project.com/openrtb2/auction'; const BIDDER_CODE = 'ozone'; - var validBidRequests = [ { adUnitCode: 'div-gpt-ad-1460505748561-0', @@ -2049,6 +2048,40 @@ describe('ozone Adapter', function () { expect(data.imp[0].ext.ozone.customData).to.be.an('array'); expect(data.imp[0].ext.ozone.customData[0].targeting.oztestmode).to.equal('mytestvalue_123'); }); + it('should pass gpid to auction if it is present (gptPreAuction adapter sets this)', function () { + var specMock = utils.deepClone(spec); + let br = JSON.parse(JSON.stringify(validBidRequests)); + utils.deepSetValue(br[0], 'ortb2Imp.ext.gpid', '/22037345/projectozone'); + const request = specMock.buildRequests(br, validBidderRequest); + const data = JSON.parse(request.data); + expect(data.imp[0].ext.gpid).to.equal('/22037345/projectozone'); + }); + it('should batch into 10s if config is set', function () { + config.setConfig({ozone: {'batchRequests': true}}); + var specMock = utils.deepClone(spec); + let arrReq = []; + for (let i = 0; i < 25; i++) { + let b = validBidRequests[0]; + b.adUnitCode += i; + arrReq.push(b); + } + const request = specMock.buildRequests(arrReq, validBidderRequest); + expect(request.length).to.equal(3); + config.resetConfig(); + }); + it('should not batch into 10s if config is set to false and singleRequest is true', function () { + config.setConfig({ozone: {'batchRequests': false, 'singleRequest': true}}); + var specMock = utils.deepClone(spec); + let arrReq = []; + for (let i = 0; i < 15; i++) { + let b = validBidRequests[0]; + b.adUnitCode += i; + arrReq.push(b); + } + const request = specMock.buildRequests(arrReq, validBidderRequest); + expect(request.method).to.equal('POST'); + config.resetConfig(); + }); it('should use GET values auction=dev & cookiesync=dev if set', function() { var specMock = utils.deepClone(spec); specMock.getGetParametersAsObject = function() { @@ -2429,6 +2462,12 @@ describe('ozone Adapter', function () { const result = spec.interpretResponse(validres, request); expect(utils.deepAccess(result[0].adserverTargeting, 'oz_ozappnexus_sid')).to.equal(result[0].cid); }); + it('should add oz_auc_id (response id value)', function () { + const request = spec.buildRequests(validBidRequestsMulti, validBidderRequest); + let validres = JSON.parse(JSON.stringify(validBidResponse1adWith2Bidders)); + const result = spec.interpretResponse(validres, request); + expect(utils.deepAccess(result[0].adserverTargeting, 'oz_auc_id')).to.equal(validBidResponse1adWith2Bidders.body.id); + }); it('should add unique adId values to each bid', function() { const request = spec.buildRequests(validBidRequests, validBidderRequest); let validres = JSON.parse(JSON.stringify(validResponse2BidsSameAdunit)); diff --git a/test/spec/modules/pangleBidAdapter_spec.js b/test/spec/modules/pangleBidAdapter_spec.js new file mode 100644 index 00000000000..6d8f9a66bcf --- /dev/null +++ b/test/spec/modules/pangleBidAdapter_spec.js @@ -0,0 +1,387 @@ +import { expect } from 'chai'; +import { spec } from 'modules/pangleBidAdapter.js'; + +const REQUEST = [{ + adUnitCode: 'adUnitCode1', + bidId: 'bidId1', + auctionId: 'auctionId-56a2-4f71-9098-720a68f2f708', + ortb2Imp: { + ext: { + tid: 'cccc1234', + } + }, + mediaTypes: { + banner: { + sizes: [ + [300, 250] + ] + } + }, + bidder: 'pangle', + params: { + placementid: 999, + appid: 111, + }, +}, +{ + adUnitCode: 'adUnitCode2', + bidId: 'bidId2', + auctionId: 'auctionId-56a2-4f71-9098-720a68f2f708', + ortb2Imp: { + ext: { + tid: 'cccc1234', + } + }, + mediaTypes: { + banner: { + sizes: [ + [300, 250] + ] + } + }, + bidder: 'pangle', + params: { + placementid: 999, + appid: 111, + }, +}]; + +const DEFAULT_OPTIONS = { + userId: { + britepoolid: 'pangle-britepool', + criteoId: 'pangle-criteo', + digitrustid: { data: { id: 'pangle-digitrust' } }, + id5id: { uid: 'pangle-id5' }, + idl_env: 'pangle-idl-env', + lipb: { lipbid: 'pangle-liveintent' }, + netId: 'pangle-netid', + parrableId: { eid: 'pangle-parrable' }, + pubcid: 'pangle-pubcid', + tdid: 'pangle-ttd', + } +}; + +const RESPONSE = { + 'headers': null, + 'body': { + 'id': 'requestId', + 'seatbid': [ + { + 'bid': [ + { + 'id': 'bidId1', + 'impid': 'bidId1', + 'price': 0.18, + 'adm': '', + 'adid': '144762342', + 'adomain': [ + 'https://dummydomain.com' + ], + 'iurl': 'iurl', + 'cid': '109', + 'crid': 'creativeId', + 'cat': [], + 'w': 300, + 'h': 250, + 'mtype': 1, + 'ext': { + 'prebid': { + 'type': 'banner' + }, + 'bidder': { + 'pangle': { + 'brand_id': 334553, + 'auction_id': 514667951122925701, + 'bidder_id': 2, + 'bid_ad_type': 0 + } + } + } + } + ], + 'seat': 'seat' + } + ] + } +}; + +describe('pangle bid adapter', function () { + describe('isBidRequestValid', function () { + it('should accept request if placementid and appid is passed', function () { + let bid = { + bidder: 'pangle', + params: { + token: 'xxx', + } + }; + expect(spec.isBidRequestValid(bid)).to.equal(true); + }); + it('reject requests without params', function () { + let bid = { + bidder: 'pangle', + params: {} + }; + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + }); + + describe('buildRequests', function () { + it('creates request data', function () { + let request = spec.buildRequests(REQUEST, DEFAULT_OPTIONS)[0]; + expect(request).to.exist.and.to.be.a('object'); + const payload = request.data; + expect(payload.imp[0]).to.have.property('id', REQUEST[0].bidId); + expect(payload.imp[1]).to.have.property('id', REQUEST[1].bidId); + }); + }); + + describe('interpretResponse', function () { + it('has bids', function () { + let request = spec.buildRequests(REQUEST, DEFAULT_OPTIONS)[0]; + let bids = spec.interpretResponse(RESPONSE, request); + expect(bids).to.be.an('array').that.is.not.empty; + validateBidOnIndex(0); + + function validateBidOnIndex(index) { + expect(bids[index]).to.have.property('currency', 'USD'); + expect(bids[index]).to.have.property('requestId', RESPONSE.body.seatbid[0].bid[index].id); + expect(bids[index]).to.have.property('cpm', RESPONSE.body.seatbid[0].bid[index].price); + expect(bids[index]).to.have.property('width', RESPONSE.body.seatbid[0].bid[index].w); + expect(bids[index]).to.have.property('height', RESPONSE.body.seatbid[0].bid[index].h); + expect(bids[index]).to.have.property('ad', RESPONSE.body.seatbid[0].bid[index].adm); + expect(bids[index]).to.have.property('creativeId', RESPONSE.body.seatbid[0].bid[index].crid); + expect(bids[index]).to.have.property('ttl', 30); + expect(bids[index]).to.have.property('netRevenue', true); + } + }); + + it('handles empty response', function () { + let request = spec.buildRequests(REQUEST, DEFAULT_OPTIONS)[0]; + const EMPTY_RESP = Object.assign({}, RESPONSE, { 'body': {} }); + const bids = spec.interpretResponse(EMPTY_RESP, request); + expect(bids).to.be.empty; + }); + }); + + describe('parseUserAgent', function () { + let desktop, mobile, tablet; + beforeEach(function () { + desktop = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36'; + mobile = 'Mozilla/5.0 (iPhone; CPU iPhone OS 13_2_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.3 Mobile/15E148 Safari/604.1'; + tablet = 'Apple iPad: Mozilla/5.0 (iPad; CPU OS 13_5_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.1.1 Mobile/15E148 Safari/605.1.15'; + }); + + it('should return correct device type: tablet', function () { + let deviceType = spec.getDeviceType(tablet); + expect(deviceType).to.equal(5); + }); + + it('should return correct device type: mobile', function () { + let deviceType = spec.getDeviceType(mobile); + expect(deviceType).to.equal(4); + }); + + it('should return correct device type: desktop', function () { + let deviceType = spec.getDeviceType(desktop); + expect(deviceType).to.equal(2); + }); + }); +}); + +describe('Pangle Adapter with video', function() { + const videoBidRequest = [ + { + bidId: '2820132fe18114', + mediaTypes: { video: { context: 'outstream', playerSize: [[300, 250]] } }, + params: { token: 'test-token' } + } + ]; + const bidderRequest = { + refererInfo: { + referer: 'https://example.com' + } + }; + const serverResponse = { + 'headers': null, + 'body': { + 'id': '233f1693-68d1-470a-ad85-c156c3faaf6f', + 'seatbid': [ + { + 'bid': [ + { + 'id': '2820132fe18114', + 'impid': '2820132fe18114', + 'price': 0.03294, + 'nurl': 'https://api16-event-sg2.pangle.io/api/ad/union/openrtb/win/?req_id=233f1693-68d1-470a-ad85-c156c3faaf6fu1450&ttdsp_adx_index=256&rit=980589944&extra=oqveoB%2Bg4%2ByNz9L8wwu%2Fy%2FwKxQsGaKsJHuB4NMK77uqZ9%2FJKpnsVZculJX8%2FxrRBAtaktU1DRN%2Fy6TKAqibCbj%2FM3%2BZ6biAKQG%2BCyt4eIV0KVvri9jCCnaajbkN7YNJWJJw2lW6cJ6Va3SuJG9H7a%2FAJd2PMbhK7fXWhoW72TwgOcKHKBgjM6sNDISBKbWlZyY3L1PhKSX%2FM8LOvL6qahsb%2FDpEObIx24vhQLNWp28XY1L4UqeibuRjam3eCvN7nXoQq74KkJ45QQsTgvV4j6I6EbLOdjOi%2FURhWMDjUD1VCMpqUT%2B6L8ZROgrX9Tp53eJ3bFOczmSTOmDSazKMHa%2B3uZZ7JHcSx32eoY4hfYc99NOJmYBKXNKCmoXyJvS3PCM3PlAz97hKrDMGnVv1wAQ7QGDCbittF0vZwtsRAvvx2mWINNIB3%2FUB2PjhxFsoDA%2BWE2urVZwEdyu%2FJrCznJsMwenXjcbMD5jmUF5vDkkLS%2B7TMDIEawJPJKZ62pK35enrwGxCs6ePXi21rJJkA0bF8tgAdl4mU1illBIVO4kCL%2ByRASskHPjgg%2FcdFe9HP%2Fi8byjAprH%2BhRerN%2FRKFxC3xv8b75x2pb1g7dY%2FTj9IjT0evsBSPVwFNqtKmPId35IcY%2FSXiqPHh%2FrAHZzr5BPsTT19P49SlNMR9UZYTzViX1iJpcCL1UFjuDdrdff%2BhHCviXxo%2FkRmufEF3umHZwxbdDOPAghuZ0DtRCY6S1rnb%2FK9BbpsVKSndOtgfCwMHFwiPmdw1XjEXGc1eOWXY6qfSp90PIfL6WS7Neh3ba2qMv6WxG3HSOBYvrcCqVTsNxk4UdVm3qb1J0CMVByweTMo45usSkCTdvX3JuEB7tVA6%2BrEk57b3XJd5Phf2AN8hon%2F7lmcXE41kwMQuXq89ViwQmW0G247UFWOQx4t1cmBqFiP6qNA%2F%2BunkZDno1pmAsGnTv7Mz9xtpOaIqKl8BKrVQSTopZ9WcUVzdBUutF19mn1f43BvyA9gIEhcDJHOj&win_price=${AUCTION_PRICE}&auction_mwb=${AUCTION_BID_TO_WIN}&use_pb=1', + 'lurl': 'https://api16-event-sg2.pangle.io/api/ad/union/openrtb/loss/?req_id=233f1693-68d1-470a-ad85-c156c3faaf6fu1450&ttdsp_adx_index=256&rit=980589944&extra=oqveoB%2Bg4%2ByNz9L8wwu%2Fy%2FwKxQsGaKsJHuB4NMK77uqZ9%2FJKpnsVZculJX8%2FxrRBAtaktU1DRN%2Fy6TKAqibCbj%2FM3%2BZ6biAKQG%2BCyt4eIV0KVvri9jCCnaajbkN7YNJWJJw2lW6cJ6Va3SuJG9H7a%2FAJd2PMbhK7fXWhoW72TwgOcKHKBgjM6sNDISBKbWlZyY3L1PhKSX%2FM8LOvL6qahsb%2FDpEObIx24vhQLNWp28XY1L4UqeibuRjam3eCvN7nXoQq74KkJ45QQsTgvV4j6I6EbLOdjOi%2FURhWMDjUD1VCMpqUT%2B6L8ZROgrX9Tp53eJ3bFOczmSTOmDSazKMHa%2B3uZZ7JHcSx32eoY4hfYc99NOJmYBKXNKCmoXyJvS3PCM3PlAz97hKrDMGnVv1wAQ7QGDCbittF0vZwtsRAvvx2mWINNIB3%2FUB2PjhxFsoDA%2BWE2urVZwEdyu%2FJrCznJsMwenXjcbMD5jmUF5vDkkLS%2B7TMDIEawJPJKZ62pK35enrwGxCs6ePXi21rJJkA0bF8tgAdl4mU1illBIVO4kCL%2ByRASskHPjgg%2FcdFe9HP%2Fi8byjAprH%2BhRerN%2FRKFxC3xv8b75x2pb1g7dY%2FTj9IjT0evsBSPVwFNqtKmPId35IcY%2FSXiqPHh%2FrAHZzr5BPsTT19P49SlNMR9UZYTzViX1iJpcCL1UFjuDdrdff%2BhHCviXxo%2FkRmufEF3umHZwxbdDOPAghuZ0DtRCY6S1rnb%2FK9BbpsVKSndOtgfCwMHFwiPmdw1XjEXGc1eOWXY6qfSp90PIfL6WS7Neh3ba2qMv6WxG3HSOBYvrcCqVTsNxk4UdVm3qb1J0CMVByweTMo45usSkCTdvX3JuEB7tVA6%2BrEk57b3XJd5Phf2AN8hon%2F7lmcXE41kwMQuXq89ViwQmW0G247UFWOQx4t1cmBqFiP6qNA%2F%2BunkZDno1pmAsGnTv7Mz9xtpOaIqKl8BKrVQSTopZ9WcUVzdBUutF19mn1f43BvyA9gIEhcDJHOj&reason=${AUCTION_LOSS}&ad_slot_type=8&auction_mwb=${AUCTION_PRICE}&use_pb=1', + 'adm': '', + 'adid': '1780626232977441', + 'adomain': [ + 'swi.esxcmnb.com' + ], + 'iurl': 'https://p16-ttam-va.ibyteimg.com/origin/ad-site-i18n-sg/202310245d0d598b3ff5993c4f129a8b', + 'cid': '1780626232977441', + 'crid': '1780626232977441', + 'attr': [ + 4 + ], + 'w': 640, + 'h': 640, + 'mtype': 1, + 'ext': { + 'pangle': { + 'adtype': 8 + }, + 'event_notification_token': { + 'payload': '980589944:8:1450:7492' + } + } + } + ], + 'seat': 'pangle' + } + ] + } + }; + + describe('Video: buildRequests', function() { + it('should create a POST request for video bid', function() { + const requests = spec.buildRequests(videoBidRequest, bidderRequest); + expect(requests[0].method).to.equal('POST'); + }); + + it('should have a valid URL and payload for an out-stream video bid', function () { + const requests = spec.buildRequests(videoBidRequest, bidderRequest); + expect(requests[0].url).to.equal('https://pangle.pangleglobal.com/api/ad/union/web_js/common/get_ads'); + expect(requests[0].data).to.exist; + }); + }); + + describe('interpretResponse: Video', function () { + it('should get correct bid response', function () { + const request = spec.buildRequests(videoBidRequest, bidderRequest)[0]; + const interpretedResponse = spec.interpretResponse(serverResponse, request); + expect(interpretedResponse).to.be.an('array'); + const bid = interpretedResponse[0]; + expect(bid).to.exist; + expect(bid.requestId).to.exist; + expect(bid.cpm).to.be.above(0); + expect(bid.ttl).to.exist; + expect(bid.creativeId).to.exist; + if (bid.renderer) { + expect(bid.renderer.render).to.exist; + } + }); + }); +}); + +describe('pangle multi-format ads', function () { + const bidderRequest = { + refererInfo: { + referer: 'https://example.com' + } + }; + const multiRequest = [ + { + bidId: '2820132fe18114', + mediaTypes: { banner: { sizes: [[300, 250]] }, video: { context: 'outstream', playerSize: [[300, 250]] } }, + params: { token: 'test-token' } + } + ]; + const videoResponse = { + 'headers': null, + 'body': { + 'id': '233f1693-68d1-470a-ad85-c156c3faaf6f', + 'seatbid': [ + { + 'bid': [ + { + 'id': '2820132fe18114', + 'impid': '2820132fe18114', + 'price': 0.03294, + 'nurl': 'https://api16-event-sg2.pangle.io/api/ad/union/openrtb/win/?req_id=233f1693-68d1-470a-ad85-c156c3faaf6fu1450&ttdsp_adx_index=256&rit=980589944&extra=oqveoB%2Bg4%2ByNz9L8wwu%2Fy%2FwKxQsGaKsJHuB4NMK77uqZ9%2FJKpnsVZculJX8%2FxrRBAtaktU1DRN%2Fy6TKAqibCbj%2FM3%2BZ6biAKQG%2BCyt4eIV0KVvri9jCCnaajbkN7YNJWJJw2lW6cJ6Va3SuJG9H7a%2FAJd2PMbhK7fXWhoW72TwgOcKHKBgjM6sNDISBKbWlZyY3L1PhKSX%2FM8LOvL6qahsb%2FDpEObIx24vhQLNWp28XY1L4UqeibuRjam3eCvN7nXoQq74KkJ45QQsTgvV4j6I6EbLOdjOi%2FURhWMDjUD1VCMpqUT%2B6L8ZROgrX9Tp53eJ3bFOczmSTOmDSazKMHa%2B3uZZ7JHcSx32eoY4hfYc99NOJmYBKXNKCmoXyJvS3PCM3PlAz97hKrDMGnVv1wAQ7QGDCbittF0vZwtsRAvvx2mWINNIB3%2FUB2PjhxFsoDA%2BWE2urVZwEdyu%2FJrCznJsMwenXjcbMD5jmUF5vDkkLS%2B7TMDIEawJPJKZ62pK35enrwGxCs6ePXi21rJJkA0bF8tgAdl4mU1illBIVO4kCL%2ByRASskHPjgg%2FcdFe9HP%2Fi8byjAprH%2BhRerN%2FRKFxC3xv8b75x2pb1g7dY%2FTj9IjT0evsBSPVwFNqtKmPId35IcY%2FSXiqPHh%2FrAHZzr5BPsTT19P49SlNMR9UZYTzViX1iJpcCL1UFjuDdrdff%2BhHCviXxo%2FkRmufEF3umHZwxbdDOPAghuZ0DtRCY6S1rnb%2FK9BbpsVKSndOtgfCwMHFwiPmdw1XjEXGc1eOWXY6qfSp90PIfL6WS7Neh3ba2qMv6WxG3HSOBYvrcCqVTsNxk4UdVm3qb1J0CMVByweTMo45usSkCTdvX3JuEB7tVA6%2BrEk57b3XJd5Phf2AN8hon%2F7lmcXE41kwMQuXq89ViwQmW0G247UFWOQx4t1cmBqFiP6qNA%2F%2BunkZDno1pmAsGnTv7Mz9xtpOaIqKl8BKrVQSTopZ9WcUVzdBUutF19mn1f43BvyA9gIEhcDJHOj&win_price=${AUCTION_PRICE}&auction_mwb=${AUCTION_BID_TO_WIN}&use_pb=1', + 'lurl': 'https://api16-event-sg2.pangle.io/api/ad/union/openrtb/loss/?req_id=233f1693-68d1-470a-ad85-c156c3faaf6fu1450&ttdsp_adx_index=256&rit=980589944&extra=oqveoB%2Bg4%2ByNz9L8wwu%2Fy%2FwKxQsGaKsJHuB4NMK77uqZ9%2FJKpnsVZculJX8%2FxrRBAtaktU1DRN%2Fy6TKAqibCbj%2FM3%2BZ6biAKQG%2BCyt4eIV0KVvri9jCCnaajbkN7YNJWJJw2lW6cJ6Va3SuJG9H7a%2FAJd2PMbhK7fXWhoW72TwgOcKHKBgjM6sNDISBKbWlZyY3L1PhKSX%2FM8LOvL6qahsb%2FDpEObIx24vhQLNWp28XY1L4UqeibuRjam3eCvN7nXoQq74KkJ45QQsTgvV4j6I6EbLOdjOi%2FURhWMDjUD1VCMpqUT%2B6L8ZROgrX9Tp53eJ3bFOczmSTOmDSazKMHa%2B3uZZ7JHcSx32eoY4hfYc99NOJmYBKXNKCmoXyJvS3PCM3PlAz97hKrDMGnVv1wAQ7QGDCbittF0vZwtsRAvvx2mWINNIB3%2FUB2PjhxFsoDA%2BWE2urVZwEdyu%2FJrCznJsMwenXjcbMD5jmUF5vDkkLS%2B7TMDIEawJPJKZ62pK35enrwGxCs6ePXi21rJJkA0bF8tgAdl4mU1illBIVO4kCL%2ByRASskHPjgg%2FcdFe9HP%2Fi8byjAprH%2BhRerN%2FRKFxC3xv8b75x2pb1g7dY%2FTj9IjT0evsBSPVwFNqtKmPId35IcY%2FSXiqPHh%2FrAHZzr5BPsTT19P49SlNMR9UZYTzViX1iJpcCL1UFjuDdrdff%2BhHCviXxo%2FkRmufEF3umHZwxbdDOPAghuZ0DtRCY6S1rnb%2FK9BbpsVKSndOtgfCwMHFwiPmdw1XjEXGc1eOWXY6qfSp90PIfL6WS7Neh3ba2qMv6WxG3HSOBYvrcCqVTsNxk4UdVm3qb1J0CMVByweTMo45usSkCTdvX3JuEB7tVA6%2BrEk57b3XJd5Phf2AN8hon%2F7lmcXE41kwMQuXq89ViwQmW0G247UFWOQx4t1cmBqFiP6qNA%2F%2BunkZDno1pmAsGnTv7Mz9xtpOaIqKl8BKrVQSTopZ9WcUVzdBUutF19mn1f43BvyA9gIEhcDJHOj&reason=${AUCTION_LOSS}&ad_slot_type=8&auction_mwb=${AUCTION_PRICE}&use_pb=1', + 'adm': '', + 'adid': '1780626232977441', + 'adomain': [ + 'swi.esxcmnb.com' + ], + 'iurl': 'https://p16-ttam-va.ibyteimg.com/origin/ad-site-i18n-sg/202310245d0d598b3ff5993c4f129a8b', + 'cid': '1780626232977441', + 'crid': '1780626232977441', + 'attr': [ + 4 + ], + 'w': 640, + 'h': 640, + 'mtype': 2, + 'ext': { + 'pangle': { + 'adtype': 8 + }, + 'event_notification_token': { + 'payload': '980589944:8:1450:7492' + } + } + } + ], + 'seat': 'pangle' + } + ] + } + }; + const bannerResponse = { + 'headers': null, + 'body': { + 'id': '233f1693-68d1-470a-ad85-c156c3faaf6f', + 'seatbid': [ + { + 'bid': [ + { + 'id': '2820132fe18114', + 'impid': '2820132fe18114', + 'price': 0.03294, + 'nurl': 'https://api16-event-sg2.pangle.io/api/ad/union/openrtb/win/?req_id=233f1693-68d1-470a-ad85-c156c3faaf6fu1450&ttdsp_adx_index=256&rit=980589944&extra=oqveoB%2Bg4%2ByNz9L8wwu%2Fy%2FwKxQsGaKsJHuB4NMK77uqZ9%2FJKpnsVZculJX8%2FxrRBAtaktU1DRN%2Fy6TKAqibCbj%2FM3%2BZ6biAKQG%2BCyt4eIV0KVvri9jCCnaajbkN7YNJWJJw2lW6cJ6Va3SuJG9H7a%2FAJd2PMbhK7fXWhoW72TwgOcKHKBgjM6sNDISBKbWlZyY3L1PhKSX%2FM8LOvL6qahsb%2FDpEObIx24vhQLNWp28XY1L4UqeibuRjam3eCvN7nXoQq74KkJ45QQsTgvV4j6I6EbLOdjOi%2FURhWMDjUD1VCMpqUT%2B6L8ZROgrX9Tp53eJ3bFOczmSTOmDSazKMHa%2B3uZZ7JHcSx32eoY4hfYc99NOJmYBKXNKCmoXyJvS3PCM3PlAz97hKrDMGnVv1wAQ7QGDCbittF0vZwtsRAvvx2mWINNIB3%2FUB2PjhxFsoDA%2BWE2urVZwEdyu%2FJrCznJsMwenXjcbMD5jmUF5vDkkLS%2B7TMDIEawJPJKZ62pK35enrwGxCs6ePXi21rJJkA0bF8tgAdl4mU1illBIVO4kCL%2ByRASskHPjgg%2FcdFe9HP%2Fi8byjAprH%2BhRerN%2FRKFxC3xv8b75x2pb1g7dY%2FTj9IjT0evsBSPVwFNqtKmPId35IcY%2FSXiqPHh%2FrAHZzr5BPsTT19P49SlNMR9UZYTzViX1iJpcCL1UFjuDdrdff%2BhHCviXxo%2FkRmufEF3umHZwxbdDOPAghuZ0DtRCY6S1rnb%2FK9BbpsVKSndOtgfCwMHFwiPmdw1XjEXGc1eOWXY6qfSp90PIfL6WS7Neh3ba2qMv6WxG3HSOBYvrcCqVTsNxk4UdVm3qb1J0CMVByweTMo45usSkCTdvX3JuEB7tVA6%2BrEk57b3XJd5Phf2AN8hon%2F7lmcXE41kwMQuXq89ViwQmW0G247UFWOQx4t1cmBqFiP6qNA%2F%2BunkZDno1pmAsGnTv7Mz9xtpOaIqKl8BKrVQSTopZ9WcUVzdBUutF19mn1f43BvyA9gIEhcDJHOj&win_price=${AUCTION_PRICE}&auction_mwb=${AUCTION_BID_TO_WIN}&use_pb=1', + 'lurl': 'https://api16-event-sg2.pangle.io/api/ad/union/openrtb/loss/?req_id=233f1693-68d1-470a-ad85-c156c3faaf6fu1450&ttdsp_adx_index=256&rit=980589944&extra=oqveoB%2Bg4%2ByNz9L8wwu%2Fy%2FwKxQsGaKsJHuB4NMK77uqZ9%2FJKpnsVZculJX8%2FxrRBAtaktU1DRN%2Fy6TKAqibCbj%2FM3%2BZ6biAKQG%2BCyt4eIV0KVvri9jCCnaajbkN7YNJWJJw2lW6cJ6Va3SuJG9H7a%2FAJd2PMbhK7fXWhoW72TwgOcKHKBgjM6sNDISBKbWlZyY3L1PhKSX%2FM8LOvL6qahsb%2FDpEObIx24vhQLNWp28XY1L4UqeibuRjam3eCvN7nXoQq74KkJ45QQsTgvV4j6I6EbLOdjOi%2FURhWMDjUD1VCMpqUT%2B6L8ZROgrX9Tp53eJ3bFOczmSTOmDSazKMHa%2B3uZZ7JHcSx32eoY4hfYc99NOJmYBKXNKCmoXyJvS3PCM3PlAz97hKrDMGnVv1wAQ7QGDCbittF0vZwtsRAvvx2mWINNIB3%2FUB2PjhxFsoDA%2BWE2urVZwEdyu%2FJrCznJsMwenXjcbMD5jmUF5vDkkLS%2B7TMDIEawJPJKZ62pK35enrwGxCs6ePXi21rJJkA0bF8tgAdl4mU1illBIVO4kCL%2ByRASskHPjgg%2FcdFe9HP%2Fi8byjAprH%2BhRerN%2FRKFxC3xv8b75x2pb1g7dY%2FTj9IjT0evsBSPVwFNqtKmPId35IcY%2FSXiqPHh%2FrAHZzr5BPsTT19P49SlNMR9UZYTzViX1iJpcCL1UFjuDdrdff%2BhHCviXxo%2FkRmufEF3umHZwxbdDOPAghuZ0DtRCY6S1rnb%2FK9BbpsVKSndOtgfCwMHFwiPmdw1XjEXGc1eOWXY6qfSp90PIfL6WS7Neh3ba2qMv6WxG3HSOBYvrcCqVTsNxk4UdVm3qb1J0CMVByweTMo45usSkCTdvX3JuEB7tVA6%2BrEk57b3XJd5Phf2AN8hon%2F7lmcXE41kwMQuXq89ViwQmW0G247UFWOQx4t1cmBqFiP6qNA%2F%2BunkZDno1pmAsGnTv7Mz9xtpOaIqKl8BKrVQSTopZ9WcUVzdBUutF19mn1f43BvyA9gIEhcDJHOj&reason=${AUCTION_LOSS}&ad_slot_type=8&auction_mwb=${AUCTION_PRICE}&use_pb=1', + 'adm': '', + 'adid': '1780626232977441', + 'adomain': [ + 'swi.esxcmnb.com' + ], + 'iurl': 'https://p16-ttam-va.ibyteimg.com/origin/ad-site-i18n-sg/202310245d0d598b3ff5993c4f129a8b', + 'cid': '1780626232977441', + 'crid': '1780626232977441', + 'attr': [ + 4 + ], + 'w': 640, + 'h': 640, + 'mtype': 1, + 'ext': { + 'pangle': { + 'adtype': 8 + }, + 'event_notification_token': { + 'payload': '980589944:8:1450:7492' + } + } + } + ], + 'seat': 'pangle' + } + ] + } + }; + it('should set mediaType to banner', function() { + const request = spec.buildRequests(multiRequest, bidderRequest)[0]; + const interpretedResponse = spec.interpretResponse(bannerResponse, request); + const bid = interpretedResponse[0]; + expect(bid.mediaType).to.equal('banner'); + }) + it('should set mediaType to video', function() { + const request = spec.buildRequests(multiRequest, bidderRequest)[0]; + const interpretedResponse = spec.interpretResponse(videoResponse, request); + const bid = interpretedResponse[0]; + expect(bid.mediaType).to.equal('video'); + }) +}); diff --git a/test/spec/modules/pgamsspBidAdapter_spec.js b/test/spec/modules/pgamsspBidAdapter_spec.js new file mode 100644 index 00000000000..0766219eda8 --- /dev/null +++ b/test/spec/modules/pgamsspBidAdapter_spec.js @@ -0,0 +1,400 @@ +import { expect } from 'chai'; +import { spec } from '../../../modules/pgamsspBidAdapter.js'; +import { BANNER, VIDEO, NATIVE } from '../../../src/mediaTypes.js'; +import { getUniqueIdentifierStr } from '../../../src/utils.js'; + +const bidder = 'pgamssp' + +describe('PGAMBidAdapter', 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://us-east.pgammedia.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'); + expect(placement.eids).to.exist.and.to.be.an('array'); + + 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.pgammedia.com/image?pbjs=1&gdpr=1&gdpr_consent=ALL&coppa=0') + }); + it('Should return array of objects with proper sync config , include CCPA', function() { + const syncData = spec.getUserSyncs({}, {}, {}, { + 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.pgammedia.com/image?pbjs=1&ccpa_consent=1---&coppa=0') + }); + }); +}); diff --git a/test/spec/modules/prebidServerBidAdapter_spec.js b/test/spec/modules/prebidServerBidAdapter_spec.js index 895c3754876..d88d4411e33 100644 --- a/test/spec/modules/prebidServerBidAdapter_spec.js +++ b/test/spec/modules/prebidServerBidAdapter_spec.js @@ -6,7 +6,7 @@ import { resetWurlMap, s2sDefaultConfig } from 'modules/prebidServerBidAdapter/index.js'; -import adapterManager from 'src/adapterManager.js'; +import adapterManager, {PBS_ADAPTER_NAME} from 'src/adapterManager.js'; import * as utils from 'src/utils.js'; import {deepAccess, deepClone, mergeDeep} from 'src/utils.js'; import {ajax} from 'src/ajax.js'; @@ -14,7 +14,6 @@ import {config} from 'src/config.js'; import * as events from 'src/events.js'; import CONSTANTS from 'src/constants.json'; import {server} from 'test/mocks/xhr.js'; -import {createEidsArray} from 'modules/userId/eids.js'; import 'modules/appnexusBidAdapter.js'; // appnexus alias test import 'modules/rubiconBidAdapter.js'; // rubicon alias test import 'src/prebid.js'; // $$PREBID_GLOBAL$$.aliasBidder test @@ -27,14 +26,17 @@ import 'modules/consentManagementUsp.js'; import 'modules/schain.js'; import 'modules/fledgeForGpt.js'; import * as redactor from 'src/activities/redactor.js'; +import * as activityRules from 'src/activities/rules.js'; import {hook} from '../../../src/hook.js'; import {decorateAdUnitsWithNativeParams} from '../../../src/native.js'; import {auctionManager} from '../../../src/auctionManager.js'; import {stubAuctionIndex} from '../../helpers/indexStub.js'; import {addComponentAuction, registerBidder} from 'src/adapters/bidderFactory.js'; import {getGlobal} from '../../../src/prebidGlobal.js'; -import {syncAddFPDEnrichments, syncAddFPDToBidderRequest} from '../../helpers/fpd.js'; +import {syncAddFPDToBidderRequest} from '../../helpers/fpd.js'; import {deepSetValue} from '../../../src/utils.js'; +import {ACTIVITY_TRANSMIT_UFPD} from '../../../src/activities/activities.js'; +import {MODULE_TYPE_PREBID} from '../../../src/activities/modules.js'; let CONFIG = { accountId: '1', @@ -707,8 +709,8 @@ describe('S2S Adapter', function () { beforeEach(() => { s2sReq = { ...REQUEST, - ortb2Fragments: {global: {source: {tid: 'mock-tid'}}}, - ad_units: REQUEST.ad_units.map(au => ({...au, ortb2Imp: {ext: {tid: 'mock-tid'}}})) + ortb2Fragments: {global: {}}, + ad_units: REQUEST.ad_units.map(au => ({...au, ortb2Imp: {ext: {tid: 'mock-tid'}}})), }; BID_REQUESTS[0].bids[0].ortb2Imp = {ext: {tid: 'mock-tid'}}; }); @@ -721,26 +723,52 @@ describe('S2S Adapter', function () { it('should not be set when transmitTid is not allowed, with ext.prebid.createtids: false', () => { config.setConfig({ s2sConfig: CONFIG, enableTIDs: false }); const req = makeRequest(); - expect(req.source.tid).to.not.exist; - expect(req.imp[0].ext.tid).to.not.exist; + expect(req.source?.tid).to.not.exist; + expect(req.imp[0].ext?.tid).to.not.exist; expect(req.ext.prebid.createtids).to.equal(false); }); - it('should be picked from FPD otherwise', () => { + it('should be set to auction ID otherwise', () => { config.setConfig({s2sConfig: CONFIG, enableTIDs: true}); const req = makeRequest(); - expect(req.source.tid).to.eql('mock-tid'); + expect(req.source.tid).to.eql(BID_REQUESTS[0].auctionId); expect(req.imp[0].ext.tid).to.eql('mock-tid'); }) }) + describe('browsingTopics', () => { + const sandbox = sinon.createSandbox(); + afterEach(() => { + sandbox.restore() + }); + Object.entries({ + 'allowed': true, + 'not allowed': false, + }).forEach(([t, allow]) => { + it(`should be set to ${allow} when transmitUfpd is ${t}`, () => { + sandbox.stub(activityRules, 'isActivityAllowed').callsFake((activity, params) => { + if (activity === ACTIVITY_TRANSMIT_UFPD && params.component === `${MODULE_TYPE_PREBID}.${PBS_ADAPTER_NAME}`) { + return allow; + } + return false; + }); + config.setConfig({s2sConfig: CONFIG}); + const ajax = sinon.stub(); + adapter.callBids(REQUEST, BID_REQUESTS, addBidResponse, done, ajax); + sinon.assert.calledWith(ajax, sinon.match.any, sinon.match.any, sinon.match.any, sinon.match({ + browsingTopics: allow + })); + }); + }); + }) + it('should set tmax to s2sConfig.timeout', () => { const cfg = {...CONFIG, timeout: 123}; config.setConfig({s2sConfig: cfg}); adapter.callBids({...REQUEST, s2sConfig: cfg}, BID_REQUESTS, addBidResponse, done, ajax); const req = JSON.parse(server.requests[0].requestBody); expect(req.tmax).to.eql(123); - }) + }); it('should block request if config did not define p1Consent URL in endpoint object config', function () { let badConfig = utils.deepClone(CONFIG); @@ -848,7 +876,7 @@ describe('S2S Adapter', function () { expect(adapter.callBids).to.exist.and.to.be.a('function'); }); - function mockConsent({applies = true, hasP1Consent = true} = {}) { + function mockTCF({applies = true, hasP1Consent = true} = {}) { return { consentString: 'mockConsent', gdprApplies: applies, @@ -866,7 +894,7 @@ describe('S2S Adapter', function () { config.setConfig(consentConfig); let gdprBidRequest = utils.deepClone(BID_REQUESTS); - gdprBidRequest[0].gdprConsent = mockConsent(); + gdprBidRequest[0].gdprConsent = mockTCF(); adapter.callBids(addFpdEnrichmentsToS2SRequest(REQUEST, gdprBidRequest), gdprBidRequest, addBidResponse, done, ajax); let requestBid = JSON.parse(server.requests[0].requestBody); @@ -889,7 +917,7 @@ describe('S2S Adapter', function () { config.setConfig(consentConfig); let gdprBidRequest = utils.deepClone(BID_REQUESTS); - gdprBidRequest[0].gdprConsent = Object.assign(mockConsent(), { + gdprBidRequest[0].gdprConsent = Object.assign(mockTCF(), { addtlConsent: 'superduperconsent', }); @@ -909,69 +937,6 @@ describe('S2S Adapter', function () { expect(requestBid.regs).to.not.exist; expect(requestBid.user).to.not.exist; }); - - it('check gdpr info gets added into cookie_sync request: have consent data', function () { - let cookieSyncConfig = utils.deepClone(CONFIG); - cookieSyncConfig.syncEndpoint = { p1Consent: 'https://prebid.adnxs.com/pbs/v1/cookie_sync' }; - - let consentConfig = { consentManagement: { cmpApi: 'iab' }, s2sConfig: cookieSyncConfig }; - config.setConfig(consentConfig); - - let gdprBidRequest = utils.deepClone(BID_REQUESTS); - - gdprBidRequest[0].gdprConsent = mockConsent(); - - const s2sBidRequest = utils.deepClone(REQUEST); - s2sBidRequest.s2sConfig = cookieSyncConfig; - - adapter.callBids(s2sBidRequest, gdprBidRequest, addBidResponse, done, ajax); - let requestBid = JSON.parse(server.requests[0].requestBody); - - expect(requestBid.gdpr).is.equal(1); - expect(requestBid.gdpr_consent).is.equal('mockConsent'); - expect(requestBid.bidders).to.contain('appnexus').and.to.have.lengthOf(1); - expect(requestBid.account).is.equal('1'); - }); - - it('check gdpr info gets added into cookie_sync request: have consent data but gdprApplies is false', function () { - let cookieSyncConfig = utils.deepClone(CONFIG); - cookieSyncConfig.syncEndpoint = { p1Consent: 'https://prebid.adnxs.com/pbs/v1/cookie_sync' }; - - let consentConfig = { consentManagement: { cmpApi: 'iab' }, s2sConfig: cookieSyncConfig }; - config.setConfig(consentConfig); - - const s2sBidRequest = utils.deepClone(REQUEST); - s2sBidRequest.s2sConfig = cookieSyncConfig; - - let gdprBidRequest = utils.deepClone(BID_REQUESTS); - gdprBidRequest[0].gdprConsent = mockConsent({applies: false}); - - adapter.callBids(s2sBidRequest, gdprBidRequest, addBidResponse, done, ajax); - let requestBid = JSON.parse(server.requests[0].requestBody); - - expect(requestBid.gdpr).is.equal(0); - expect(requestBid.gdpr_consent).is.undefined; - }); - - it('checks gdpr info gets added to cookie_sync request: applies is false', function () { - let cookieSyncConfig = utils.deepClone(CONFIG); - cookieSyncConfig.syncEndpoint = { p1Consent: 'https://prebid.adnxs.com/pbs/v1/cookie_sync' }; - - let consentConfig = { consentManagement: { cmpApi: 'iab' }, s2sConfig: cookieSyncConfig }; - config.setConfig(consentConfig); - - let gdprBidRequest = utils.deepClone(BID_REQUESTS); - gdprBidRequest[0].gdprConsent = mockConsent({applies: false}); - - const s2sBidRequest = utils.deepClone(REQUEST); - s2sBidRequest.s2sConfig = cookieSyncConfig; - - adapter.callBids(s2sBidRequest, gdprBidRequest, addBidResponse, done, ajax); - let requestBid = JSON.parse(server.requests[0].requestBody); - - expect(requestBid.gdpr).is.equal(0); - expect(requestBid.gdpr_consent).is.undefined; - }); }); describe('us_privacy (ccpa) consent data', function () { @@ -998,25 +963,6 @@ describe('S2S Adapter', function () { expect(requestBid.regs).to.not.exist; }); - - it('is added to cookie_sync request when in bidRequest', function () { - let cookieSyncConfig = utils.deepClone(CONFIG); - cookieSyncConfig.syncEndpoint = { p1Consent: 'https://prebid.adnxs.com/pbs/v1/cookie_sync' }; - config.setConfig({ s2sConfig: cookieSyncConfig }); - - let uspBidRequest = utils.deepClone(BID_REQUESTS); - uspBidRequest[0].uspConsent = '1YNN'; - - const s2sBidRequest = utils.deepClone(REQUEST); - s2sBidRequest.s2sConfig = cookieSyncConfig; - - adapter.callBids(s2sBidRequest, uspBidRequest, addBidResponse, done, ajax); - let requestBid = JSON.parse(server.requests[0].requestBody); - - expect(requestBid.us_privacy).is.equal('1YNN'); - expect(requestBid.bidders).to.contain('appnexus').and.to.have.lengthOf(1); - expect(requestBid.account).is.equal('1'); - }); }); describe('gdpr and us_privacy (ccpa) consent data', function () { @@ -1029,7 +975,7 @@ describe('S2S Adapter', function () { let consentBidRequest = utils.deepClone(BID_REQUESTS); consentBidRequest[0].uspConsent = '1NYN'; - consentBidRequest[0].gdprConsent = mockConsent(); + consentBidRequest[0].gdprConsent = mockTCF(); adapter.callBids(addFpdEnrichmentsToS2SRequest(REQUEST, consentBidRequest), consentBidRequest, addBidResponse, done, ajax); let requestBid = JSON.parse(server.requests[0].requestBody); @@ -1055,7 +1001,7 @@ describe('S2S Adapter', function () { let consentBidRequest = utils.deepClone(BID_REQUESTS); consentBidRequest[0].uspConsent = '1YNN'; - consentBidRequest[0].gdprConsent = mockConsent(); + consentBidRequest[0].gdprConsent = mockTCF(); const s2sBidRequest = utils.deepClone(REQUEST); s2sBidRequest.s2sConfig = cookieSyncConfig @@ -1812,170 +1758,177 @@ describe('S2S Adapter', function () { }]); }); - describe('filterSettings', function () { - const getRequestBid = userSync => { - let cookieSyncConfig = utils.deepClone(CONFIG); - const s2sBidRequest = utils.deepClone(REQUEST); - cookieSyncConfig.syncEndpoint = { p1Consent: 'https://prebid.adnxs.com/pbs/v1/cookie_sync' }; - s2sBidRequest.s2sConfig = cookieSyncConfig; + describe('cookie sync', () => { + let s2sConfig, bidderReqs; - config.setConfig({ userSync, s2sConfig: cookieSyncConfig }); + beforeEach(() => { + bidderReqs = utils.deepClone(BID_REQUESTS); + s2sConfig = utils.deepClone(CONFIG); + s2sConfig.syncEndpoint = { p1Consent: 'https://prebid.adnxs.com/pbs/v1/cookie_sync' }; + }) - let bidRequest = utils.deepClone(BID_REQUESTS); - adapter.callBids(s2sBidRequest, bidRequest, addBidResponse, done, ajax); + function callCookieSync() { + const s2sBidRequest = utils.deepClone(REQUEST); + s2sBidRequest.s2sConfig = s2sConfig; + config.setConfig({ s2sConfig: s2sConfig }); + adapter.callBids(s2sBidRequest, bidderReqs, addBidResponse, done, ajax); return JSON.parse(server.requests[0].requestBody); } - it('correctly adds filterSettings to the cookie_sync request if userSync.filterSettings is present in the config and only the all key is present in userSync.filterSettings', function () { - const userSync = { - filterSettings: { - all: { - bidders: ['appnexus', 'rubicon', 'pubmatic'], - filter: 'exclude' + describe('filterSettings', function () { + it('correctly adds filterSettings to the cookie_sync request if userSync.filterSettings is present in the config and only the all key is present in userSync.filterSettings', function () { + config.setConfig({ + userSync: { + filterSettings: { + all: { + bidders: ['appnexus', 'rubicon', 'pubmatic'], + filter: 'exclude' + } + } } - } - }; - const requestBid = getRequestBid(userSync); - - expect(requestBid.filterSettings).to.deep.equal({ - 'image': { - 'bidders': ['appnexus', 'rubicon', 'pubmatic'], - 'filter': 'exclude' - }, - 'iframe': { - 'bidders': ['appnexus', 'rubicon', 'pubmatic'], - 'filter': 'exclude' - } + }); + expect(callCookieSync().filterSettings).to.deep.equal({ + 'image': { + 'bidders': ['appnexus', 'rubicon', 'pubmatic'], + 'filter': 'exclude' + }, + 'iframe': { + 'bidders': ['appnexus', 'rubicon', 'pubmatic'], + 'filter': 'exclude' + } + }); }); - }); - it('correctly adds filterSettings to the cookie_sync request if userSync.filterSettings is present in the config and only the iframe key is present in userSync.filterSettings', function () { - const userSync = { - filterSettings: { - iframe: { - bidders: ['rubicon', 'pubmatic'], - filter: 'include' + it('correctly adds filterSettings to the cookie_sync request if userSync.filterSettings is present in the config and only the iframe key is present in userSync.filterSettings', function () { + config.setConfig({ + userSync: { + filterSettings: { + iframe: { + bidders: ['rubicon', 'pubmatic'], + filter: 'include' + } + } } - } - }; - const requestBid = getRequestBid(userSync); + }) - expect(requestBid.filterSettings).to.deep.equal({ - 'image': { - 'bidders': '*', - 'filter': 'include' - }, - 'iframe': { - 'bidders': ['rubicon', 'pubmatic'], - 'filter': 'include' - } + expect(callCookieSync().filterSettings).to.deep.equal({ + 'image': { + 'bidders': '*', + 'filter': 'include' + }, + 'iframe': { + 'bidders': ['rubicon', 'pubmatic'], + 'filter': 'include' + } + }); }); - }); - it('correctly adds filterSettings to the cookie_sync request if userSync.filterSettings is present in the config and the image and iframe keys are both present in userSync.filterSettings', function () { - const userSync = { - filterSettings: { - image: { - bidders: ['triplelift', 'appnexus'], - filter: 'include' - }, - iframe: { - bidders: ['pulsepoint', 'triplelift', 'appnexus', 'rubicon'], - filter: 'exclude' + it('correctly adds filterSettings to the cookie_sync request if userSync.filterSettings is present in the config and the image and iframe keys are both present in userSync.filterSettings', function () { + config.setConfig({ + userSync: { + filterSettings: { + image: { + bidders: ['triplelift', 'appnexus'], + filter: 'include' + }, + iframe: { + bidders: ['pulsepoint', 'triplelift', 'appnexus', 'rubicon'], + filter: 'exclude' + } + } } - } - }; - const requestBid = getRequestBid(userSync); + }) - expect(requestBid.filterSettings).to.deep.equal({ - 'image': { - 'bidders': ['triplelift', 'appnexus'], - 'filter': 'include' - }, - 'iframe': { - 'bidders': ['pulsepoint', 'triplelift', 'appnexus', 'rubicon'], - 'filter': 'exclude' - } + expect(callCookieSync().filterSettings).to.deep.equal({ + 'image': { + 'bidders': ['triplelift', 'appnexus'], + 'filter': 'include' + }, + 'iframe': { + 'bidders': ['pulsepoint', 'triplelift', 'appnexus', 'rubicon'], + 'filter': 'exclude' + } + }); }); - }); - it('correctly adds filterSettings to the cookie_sync request if userSync.filterSettings is present in the config and the all and iframe keys are both present in userSync.filterSettings', function () { - const userSync = { - filterSettings: { - all: { - bidders: ['triplelift', 'appnexus'], - filter: 'include' - }, - iframe: { - bidders: ['pulsepoint', 'triplelift', 'appnexus', 'rubicon'], - filter: 'exclude' + it('correctly adds filterSettings to the cookie_sync request if userSync.filterSettings is present in the config and the all and iframe keys are both present in userSync.filterSettings', function () { + config.setConfig({ + userSync: { + filterSettings: { + all: { + bidders: ['triplelift', 'appnexus'], + filter: 'include' + }, + iframe: { + bidders: ['pulsepoint', 'triplelift', 'appnexus', 'rubicon'], + filter: 'exclude' + } + } } - } - }; - const requestBid = getRequestBid(userSync); + }) - expect(requestBid.filterSettings).to.deep.equal({ - 'image': { - 'bidders': ['triplelift', 'appnexus'], - 'filter': 'include' - }, - 'iframe': { - 'bidders': ['pulsepoint', 'triplelift', 'appnexus', 'rubicon'], - 'filter': 'exclude' - } + expect(callCookieSync().filterSettings).to.deep.equal({ + 'image': { + 'bidders': ['triplelift', 'appnexus'], + 'filter': 'include' + }, + 'iframe': { + 'bidders': ['pulsepoint', 'triplelift', 'appnexus', 'rubicon'], + 'filter': 'exclude' + } + }); }); }); - }); - - it('adds limit to the cookie_sync request if userSyncLimit is greater than 0', function () { - let cookieSyncConfig = utils.deepClone(CONFIG); - cookieSyncConfig.syncEndpoint = { p1Consent: 'https://prebid.adnxs.com/pbs/v1/cookie_sync' }; - cookieSyncConfig.userSyncLimit = 1; - - const s2sBidRequest = utils.deepClone(REQUEST); - s2sBidRequest.s2sConfig = cookieSyncConfig; - - config.setConfig({ s2sConfig: cookieSyncConfig }); - - let bidRequest = utils.deepClone(BID_REQUESTS); - adapter.callBids(s2sBidRequest, bidRequest, addBidResponse, done, ajax); - let requestBid = JSON.parse(server.requests[0].requestBody); - - expect(requestBid.bidders).to.contain('appnexus').and.to.have.lengthOf(1); - expect(requestBid.account).is.equal('1'); - expect(requestBid.limit).is.equal(1); - }); - it('does not add limit to cooke_sync request if userSyncLimit is missing or 0', function () { - let cookieSyncConfig = utils.deepClone(CONFIG); - cookieSyncConfig.syncEndpoint = { p1Consent: 'https://prebid.adnxs.com/pbs/v1/cookie_sync' }; - config.setConfig({ s2sConfig: cookieSyncConfig }); - - const s2sBidRequest = utils.deepClone(REQUEST); - s2sBidRequest.s2sConfig = cookieSyncConfig; - - let bidRequest = utils.deepClone(BID_REQUESTS); - adapter.callBids(s2sBidRequest, bidRequest, addBidResponse, done, ajax); - let requestBid = JSON.parse(server.requests[0].requestBody); + describe('limit', () => { + it('is added to request if userSyncLimit is greater than 0', function () { + s2sConfig.userSyncLimit = 1; + const req = callCookieSync(); + expect(req.limit).is.equal(1); + }); - expect(requestBid.bidders).to.contain('appnexus').and.to.have.lengthOf(1); - expect(requestBid.account).is.equal('1'); - expect(requestBid.limit).is.undefined; + Object.entries({ + 'missing': () => null, + '0': () => { s2sConfig.userSyncLimit = 0; } + }).forEach(([t, setup]) => { + it(`is not added to request if userSyncLimit is ${t}`, () => { + setup(); + const req = callCookieSync(); + expect(req.limit).to.not.exist; + }); + }); + }); - cookieSyncConfig.userSyncLimit = 0; - config.resetConfig(); - config.setConfig({ s2sConfig: cookieSyncConfig }); + describe('gdpr data is set', () => { + it('when we have consent data', function () { + bidderReqs[0].gdprConsent = mockTCF(); + const req = callCookieSync(); + expect(req.gdpr).is.equal(1); + expect(req.gdpr_consent).is.equal('mockConsent'); + }); - const s2sBidRequest2 = utils.deepClone(REQUEST); - s2sBidRequest2.s2sConfig = cookieSyncConfig; + it('when gdprApplies is false', () => { + bidderReqs[0].gdprConsent = mockTCF({applies: false}); + const req = callCookieSync(); + expect(req.gdpr).is.equal(0); + expect(req.gdpr_consent).is.undefined; + }); + }); - bidRequest = utils.deepClone(BID_REQUESTS); - adapter.callBids(s2sBidRequest2, bidRequest, addBidResponse, done, ajax); - requestBid = JSON.parse(server.requests[0].requestBody); + it('adds USP data from bidder request', () => { + bidderReqs[0].uspConsent = '1YNN'; + expect(callCookieSync().us_privacy).to.equal('1YNN'); + }); - expect(requestBid.bidders).to.contain('appnexus').and.to.have.lengthOf(1); - expect(requestBid.account).is.equal('1'); - expect(requestBid.limit).is.undefined; + it('adds GPP data from bidder requests', () => { + bidderReqs[0].gppConsent = { + applicableSections: [1, 2, 3], + gppString: 'mock-string' + }; + const req = callCookieSync(); + expect(req.gpp).to.eql('mock-string'); + expect(req.gpp_sid).to.eql('1,2,3'); + }); }); it('adds s2sConfig adapterOptions to request for ORTB', function () { @@ -2082,30 +2035,12 @@ describe('S2S Adapter', function () { } } }; - userIdBidRequest[0].bids[0].userIdAsEids = createEidsArray(userIdBidRequest[0].bids[0].userId); + userIdBidRequest[0].bids[0].userIdAsEids = [{id: 1}, {id: 2}]; adapter.callBids(REQUEST, userIdBidRequest, addBidResponse, done, ajax); let requestBid = JSON.parse(server.requests[0].requestBody); expect(typeof requestBid.user.ext.eids).is.equal('object'); - expect(Array.isArray(requestBid.user.ext.eids)).to.be.true; - expect(requestBid.user.ext.eids.filter(eid => eid.source === 'adserver.org')).is.not.empty; - expect(requestBid.user.ext.eids.filter(eid => eid.source === 'adserver.org')[0].uids[0].id).is.equal('abc123'); - expect(requestBid.user.ext.eids.filter(eid => eid.source === 'criteo.com')).is.not.empty; - expect(requestBid.user.ext.eids.filter(eid => eid.source === 'criteo.com')[0].uids[0].id).is.equal('44VmRDeUE3ZGJ5MzRkRVJHU3BIUlJ6TlFPQUFU'); - expect(requestBid.user.ext.eids.filter(eid => eid.source === 'pubcid.org')).is.not.empty; - expect(requestBid.user.ext.eids.filter(eid => eid.source === 'pubcid.org')[0].uids[0].id).is.equal('1234'); - expect(requestBid.user.ext.eids.filter(eid => eid.source === 'parrable.com')).is.not.empty; - expect(requestBid.user.ext.eids.filter(eid => eid.source === 'parrable.com')[0].uids[0].id).is.equal('01.1563917337.test-eid'); - expect(requestBid.user.ext.eids.filter(eid => eid.source === 'liveintent.com')).is.not.empty; - expect(requestBid.user.ext.eids.filter(eid => eid.source === 'liveintent.com')[0].uids[0].id).is.equal('li-xyz'); - expect(requestBid.user.ext.eids.filter(eid => eid.source === 'liveintent.com')[0].ext.segments.length).is.equal(2); - expect(requestBid.user.ext.eids.filter(eid => eid.source === 'liveintent.com')[0].ext.segments[0]).is.equal('segA'); - expect(requestBid.user.ext.eids.filter(eid => eid.source === 'liveintent.com')[0].ext.segments[1]).is.equal('segB'); - expect(requestBid.user.ext.eids.filter(eid => eid.source === 'id5-sync.com')).is.not.empty; - expect(requestBid.user.ext.eids.filter(eid => eid.source === 'id5-sync.com')[0].uids[0].id).is.equal('11111'); - expect(requestBid.user.ext.eids.filter(eid => eid.source === 'id5-sync.com')[0].uids[0].ext.linkType).is.equal('some-link-type'); - // LiveRamp should exist - expect(requestBid.user.ext.eids.filter(eid => eid.source === 'liveramp.com')[0].uids[0].id).is.equal('0000-1111-2222-3333'); + expect(requestBid.user.ext.eids).to.eql([{id: 1}, {id: 2}]); }); it('when config \'currency.adServerCurrency\' value is a string: ORTB has property \'cur\' value set to a single item array', function () { @@ -2116,7 +2051,7 @@ describe('S2S Adapter', function () { const bidRequests = utils.deepClone(BID_REQUESTS); adapter.callBids(REQUEST, bidRequests, addBidResponse, done, ajax); - const parsedRequestBody = JSON.parse(server.requests[1].requestBody); + const parsedRequestBody = JSON.parse(server.requests.find(req => req.method === 'POST').requestBody); expect(parsedRequestBody.cur).to.deep.equal(['NZ']); }); @@ -2938,6 +2873,41 @@ describe('S2S Adapter', function () { events.emit.restore(); }); + it('triggers BIDDER_ERROR on server error', () => { + config.setConfig({ s2sConfig: CONFIG }); + adapter.callBids(REQUEST, BID_REQUESTS, addBidResponse, done, ajax); + server.requests[0].respond(400, {}, {}); + BID_REQUESTS.forEach(bidderRequest => { + sinon.assert.calledWith(events.emit, CONSTANTS.EVENTS.BIDDER_ERROR, sinon.match({bidderRequest})) + }) + }) + + describe('calls done', () => { + let success, error; + beforeEach(() => { + const mockAjax = function (_, callback) { + ({success, error} = callback); + } + config.setConfig({ s2sConfig: CONFIG }); + adapter.callBids(REQUEST, BID_REQUESTS, addBidResponse, done, mockAjax); + }) + + it('passing timedOut = false on succcess', () => { + success({}); + sinon.assert.calledWith(done, false); + }); + + Object.entries({ + 'timeouts': true, + 'other errors': false + }).forEach(([t, timedOut]) => { + it(`passing timedOut = ${timedOut} on ${t}`, () => { + error('', {timedOut}); + sinon.assert.calledWith(done, timedOut); + }) + }) + }) + // TODO: test dependent on pbjs_api_spec. Needs to be isolated it('does not call addBidResponse and calls done when ad unit not set', function () { config.setConfig({ s2sConfig: CONFIG }); @@ -3478,29 +3448,6 @@ describe('S2S Adapter', function () { }); }); describe('when the response contains ext.prebid.fledge', () => { - let fledgeStub, request, bidderRequests; - - function fledgeHook(next, ...args) { - fledgeStub(...args); - } - - before(() => { - addComponentAuction.before(fledgeHook); - }); - - after(() => { - addComponentAuction.getHooks({hook: fledgeHook}).remove(); - }) - - beforeEach(function () { - fledgeStub = sinon.stub(); - config.setConfig({CONFIG}); - request = deepClone(REQUEST); - request.ad_units.forEach(au => deepSetValue(au, 'ortb2Imp.ext.ae', 1)); - bidderRequests = deepClone(BID_REQUESTS); - bidderRequests.forEach(req => req.fledgeEnabled = true); - }); - const AU = 'div-gpt-ad-1460505748561-0'; const FLEDGE_RESP = { ext: { @@ -3509,12 +3456,14 @@ describe('S2S Adapter', function () { auctionconfigs: [ { impid: AU, + bidder: 'appnexus', config: { id: 1 } }, { impid: AU, + bidder: 'other', config: { id: 2 } @@ -3525,20 +3474,62 @@ describe('S2S Adapter', function () { } } + let fledgeStub, request, bidderRequests; + + function fledgeHook(next, ...args) { + fledgeStub(...args); + } + + before(() => { + addComponentAuction.before(fledgeHook); + }); + + after(() => { + addComponentAuction.getHooks({hook: fledgeHook}).remove(); + }) + + beforeEach(function () { + fledgeStub = sinon.stub(); + config.setConfig({CONFIG}); + bidderRequests = deepClone(BID_REQUESTS); + AU + bidderRequests.forEach(req => { + Object.assign(req, { + fledgeEnabled: true, + ortb2: { + fpd: 1 + } + }) + req.bids.forEach(bid => { + Object.assign(bid, { + ortb2Imp: { + fpd: 2 + } + }) + }) + }); + request = deepClone(REQUEST); + request.ad_units.forEach(au => deepSetValue(au, 'ortb2Imp.ext.ae', 1)); + }); + + function expectFledgeCalls() { + const auctionId = bidderRequests[0].auctionId; + sinon.assert.calledWith(fledgeStub, sinon.match({auctionId, adUnitCode: AU, ortb2: bidderRequests[0].ortb2, ortb2Imp: bidderRequests[0].bids[0].ortb2Imp}), {id: 1}) + sinon.assert.calledWith(fledgeStub, sinon.match({auctionId, adUnitCode: AU, ortb2: undefined, ortb2Imp: undefined}), {id: 2}) + } + it('calls addComponentAuction alongside addBidResponse', function () { adapter.callBids(request, bidderRequests, addBidResponse, done, ajax); server.requests[0].respond(200, {}, JSON.stringify(mergeDeep({}, RESPONSE_OPENRTB, FLEDGE_RESP))); expect(addBidResponse.called).to.be.true; - sinon.assert.calledWith(fledgeStub, AU, {id: 1}); - sinon.assert.calledWith(fledgeStub, AU, {id: 2}); + expectFledgeCalls(); }); it('calls addComponentAuction when there is no bid in the response', () => { adapter.callBids(request, bidderRequests, addBidResponse, done, ajax); server.requests[0].respond(200, {}, JSON.stringify(FLEDGE_RESP)); expect(addBidResponse.called).to.be.false; - sinon.assert.calledWith(fledgeStub, AU, {id: 1}); - sinon.assert.calledWith(fledgeStub, AU, {id: 2}); + expectFledgeCalls(); }) }); }); @@ -3688,33 +3679,6 @@ describe('S2S Adapter', function () { sinon.assert.calledOnce(logErrorSpy); }); - it('should configure the s2sConfig object with appnexus vendor defaults unless specified by user', function () { - const options = { - accountId: '123', - bidders: ['appnexus'], - defaultVendor: 'appnexus', - timeout: 750 - }; - - config.setConfig({ s2sConfig: options }); - sinon.assert.notCalled(logErrorSpy); - - let vendorConfig = config.getConfig('s2sConfig'); - expect(vendorConfig).to.have.property('accountId', '123'); - expect(vendorConfig).to.have.property('adapter', 'prebidServer'); - expect(vendorConfig.bidders).to.deep.equal(['appnexus']); - expect(vendorConfig.enabled).to.be.true; - expect(vendorConfig.endpoint).to.deep.equal({ - p1Consent: 'https://prebid.adnxs.com/pbs/v1/openrtb2/auction', - noP1Consent: 'https://prebid.adnxs-simple.com/pbs/v1/openrtb2/auction' - }); - expect(vendorConfig.syncEndpoint).to.deep.equal({ - p1Consent: 'https://prebid.adnxs.com/pbs/v1/cookie_sync', - noP1Consent: 'https://prebid.adnxs-simple.com/pbs/v1/cookie_sync' - }); - expect(vendorConfig).to.have.property('timeout', 750); - }); - it('should configure the s2sConfig object with appnexuspsp vendor defaults unless specified by user', function () { const options = { accountId: '123', @@ -3735,7 +3699,10 @@ describe('S2S Adapter', function () { p1Consent: 'https://ib.adnxs.com/openrtb2/prebid', noP1Consent: 'https://ib.adnxs-simple.com/openrtb2/prebid' }); - expect(vendorConfig.syncEndpoint).to.be.undefined; + expect(vendorConfig.syncEndpoint).to.deep.equal({ + p1Consent: 'https://prebid.adnxs.com/pbs/v1/cookie_sync', + noP1Consent: 'https://prebid.adnxs-simple.com/pbs/v1/cookie_sync' + }); expect(vendorConfig).to.have.property('timeout', 750); }); @@ -3820,6 +3787,74 @@ describe('S2S Adapter', function () { }) }); + it('should configure the s2sConfig object with openwrap vendor defaults unless specified by user', function () { + const options = { + accountId: '1234', + bidders: ['pubmatic'], + defaultVendor: 'openwrap' + }; + + config.setConfig({ s2sConfig: options }); + sinon.assert.notCalled(logErrorSpy); + + let vendorConfig = config.getConfig('s2sConfig'); + expect(vendorConfig).to.have.property('accountId', '1234'); + expect(vendorConfig).to.have.property('adapter', 'prebidServer'); + expect(vendorConfig.bidders).to.deep.equal(['pubmatic']); + expect(vendorConfig.enabled).to.be.true; + expect(vendorConfig.endpoint).to.deep.equal({ + p1Consent: 'https://ow.pubmatic.com/openrtb2/auction?source=pbjs', + noP1Consent: 'https://ow.pubmatic.com/openrtb2/auction?source=pbjs' + }); + expect(vendorConfig).to.have.property('timeout', 500); + }); + + it('should return proper defaults', function () { + const options = { + accountId: '1234', + bidders: ['pubmatic'], + defaultVendor: 'openwrap', + timeout: 500 + }; + + config.setConfig({ s2sConfig: options }); + expect(config.getConfig('s2sConfig')).to.deep.equal({ + 'accountId': '1234', + 'adapter': 'prebidServer', + 'bidders': ['pubmatic'], + 'defaultVendor': 'openwrap', + 'enabled': true, + 'endpoint': { + p1Consent: 'https://ow.pubmatic.com/openrtb2/auction?source=pbjs', + noP1Consent: 'https://ow.pubmatic.com/openrtb2/auction?source=pbjs' + }, + 'timeout': 500 + }) + }); + + it('should return default adapterOptions if not set', function () { + config.setConfig({ + s2sConfig: { + accountId: '1234', + bidders: ['pubmatic'], + defaultVendor: 'openwrap', + timeout: 500 + } + }); + expect(config.getConfig('s2sConfig')).to.deep.equal({ + enabled: true, + timeout: 500, + adapter: 'prebidServer', + accountId: '1234', + bidders: ['pubmatic'], + defaultVendor: 'openwrap', + endpoint: { + p1Consent: 'https://ow.pubmatic.com/openrtb2/auction?source=pbjs', + noP1Consent: 'https://ow.pubmatic.com/openrtb2/auction?source=pbjs' + }, + }) + }); + it('should set adapterOptions', function () { config.setConfig({ s2sConfig: { diff --git a/test/spec/modules/precisoBidAdapter_spec.js b/test/spec/modules/precisoBidAdapter_spec.js new file mode 100644 index 00000000000..78a1615a02e --- /dev/null +++ b/test/spec/modules/precisoBidAdapter_spec.js @@ -0,0 +1,162 @@ +import { expect } from 'chai'; +import { spec } from '../../../modules/precisoBidAdapter.js'; +import { config } from '../../../src/config.js'; + +const DEFAULT_PRICE = 1 +const DEFAULT_CURRENCY = 'USD' +const DEFAULT_BANNER_WIDTH = 300 +const DEFAULT_BANNER_HEIGHT = 250 +const BIDDER_CODE = 'preciso'; + +describe('PrecisoAdapter', function () { + let bid = { + bidId: '23fhj33i987f', + bidder: 'preciso', + mediaTypes: { + banner: { + sizes: [[DEFAULT_BANNER_WIDTH, DEFAULT_BANNER_HEIGHT]] + } + }, + params: { + host: 'prebid', + sourceid: '0', + publisherId: '0', + mediaType: 'banner', + region: 'prebid-eu' + + }, + userId: { + pubcid: '12355454test' + + }, + geo: 'NA', + city: 'Asia,delhi' + }; + + describe('isBidRequestValid', function () { + it('Should return true if there are bidId, params and sourceid parameters present', function () { + expect(spec.isBidRequestValid(bid)).to.be.true; + }); + it('Should return false if at least one of parameters is not present', function () { + delete bid.params.publisherId; + expect(spec.isBidRequestValid(bid)).to.be.false; + }); + }); + + describe('buildRequests', function () { + let serverRequest = spec.buildRequests([bid]); + it('Creates a ServerRequest object with method, URL and data', function () { + expect(serverRequest).to.exist; + expect(serverRequest.method).to.exist; + expect(serverRequest.url).to.exist; + expect(serverRequest.data).to.exist; + }); + it('Returns POST method', function () { + expect(serverRequest.method).to.equal('POST'); + }); + it('Returns valid URL', function () { + expect(serverRequest.url).to.equal('https://ssp-bidder.mndtrk.com/bid_request/openrtb'); + }); + it('Returns valid data if array of bids is valid', function () { + let data = serverRequest.data; + // expect(data).to.be.an('object'); + + // expect(data).to.have.all.keys('bidId', 'imp', 'site', 'deviceWidth', 'deviceHeight', 'language', 'secure', 'host', 'page', 'placements', 'coppa'); + + expect(data.deviceWidth).to.be.a('number'); + expect(data.deviceHeight).to.be.a('number'); + expect(data.coppa).to.be.a('number'); + expect(data.language).to.be.a('string'); + // expect(data.secure).to.be.within(0, 1); + expect(data.host).to.be.a('string'); + expect(data.page).to.be.a('string'); + + expect(data.city).to.be.a('string'); + expect(data.geo).to.be.a('object'); + // expect(data.userId).to.be.a('string'); + // expect(data.imp).to.be.a('object'); + }); + // it('Returns empty data if no valid requests are passed', function () { + /// serverRequest = spec.buildRequests([]); + // let data = serverRequest.data; + // expect(data.imp).to.be.an('array').that.is.empty; + // }); + }); + + describe('with COPPA', function () { + beforeEach(function () { + sinon.stub(config, 'getConfig') + .withArgs('coppa') + .returns(true); + }); + afterEach(function () { + config.getConfig.restore(); + }); + + it('should send the Coppa "required" flag set to "1" in the request', function () { + let serverRequest = spec.buildRequests([bid]); + expect(serverRequest.data.coppa).to.equal(1); + }); + }); + + describe('interpretResponse', function () { + it('should get correct bid response', function () { + let response = { + + bidderRequestId: 'f6adb85f-4e19-45a0-b41e-2a5b9a48f23a', + + seatbid: [ + { + bid: [ + { + id: '123', + impid: 'b4f290d7-d4ab-4778-ab94-2baf06420b22', + price: DEFAULT_PRICE, + adm: 'hi', + cid: 'test_cid', + crid: 'test_banner_crid', + w: DEFAULT_BANNER_WIDTH, + h: DEFAULT_BANNER_HEIGHT, + adomain: [], + } + ], + seat: BIDDER_CODE + } + ], + } + + let expectedResponse = [ + { + requestId: 'b4f290d7-d4ab-4778-ab94-2baf06420b22', + cpm: DEFAULT_PRICE, + width: DEFAULT_BANNER_WIDTH, + height: DEFAULT_BANNER_HEIGHT, + creativeId: 'test_banner_crid', + ad: 'hi', + currency: DEFAULT_CURRENCY, + netRevenue: true, + ttl: 300, + meta: { advertiserDomains: [] }, + } + ] + let result = spec.interpretResponse({ body: response }) + + expect(Object.keys(result[0])).to.have.members(Object.keys(expectedResponse[0])) + }) + }) + describe('getUserSyncs', function () { + const syncUrl = 'https://ck.2trk.info/rtb/user/usersync.aspx?id=NA&gdpr=0&gdpr_consent=&us_privacy=&t=4'; + const syncOptions = { + iframeEnabled: true + }; + let userSync = spec.getUserSyncs(syncOptions); + it('Returns valid URL and type', function () { + expect(userSync).to.be.an('array').with.lengthOf(1); + expect(userSync[0].type).to.exist; + expect(userSync[0].url).to.exist; + expect(userSync).to.deep.equal([ + { type: 'iframe', url: syncUrl } + ]); + }); + }); +}); diff --git a/test/spec/modules/priceFloors_spec.js b/test/spec/modules/priceFloors_spec.js index e9ce4c8ccd3..a31f07fecef 100644 --- a/test/spec/modules/priceFloors_spec.js +++ b/test/spec/modules/priceFloors_spec.js @@ -12,7 +12,7 @@ import { isFloorsDataValid, addBidResponseHook, fieldMatchingFunctions, - allowedFields + allowedFields, parseFloorData, normalizeDefault, getFloorDataFromAdUnits, updateAdUnitsForAuction, createFloorsDataForAuction } from 'modules/priceFloors.js'; import * as events from 'src/events.js'; import * as mockGpt from '../integration/faker/googletag.js'; @@ -20,6 +20,9 @@ import 'src/prebid.js'; import {createBid} from '../../../src/bidfactory.js'; import {auctionManager} from '../../../src/auctionManager.js'; import {stubAuctionIndex} from '../../helpers/indexStub.js'; +import {guardTids} from '../../../src/adapters/bidderFactory.js'; +import * as activities from '../../../src/activities/rules.js'; +import {server} from '../../mocks/xhr.js'; describe('the price floors module', function () { let logErrorSpy; @@ -120,7 +123,7 @@ describe('the price floors module', function () { return { code, mediaTypes: {banner: { sizes: [[300, 200], [300, 600]] }, native: {}}, - bids: [{bidder: 'someBidder'}, {bidder: 'someOtherBidder'}] + bids: [{bidder: 'someBidder', adUnitCode: code}, {bidder: 'someOtherBidder', adUnitCode: code}] }; } beforeEach(function() { @@ -140,6 +143,76 @@ describe('the price floors module', function () { getGlobal().bidderSettings = {}; }); + describe('parseFloorData', () => { + it('should accept just a default floor', () => { + const fd = parseFloorData({ + default: 1.23 + }); + expect(getFirstMatchingFloor(fd, {}, {}).matchingFloor).to.eql(1.23); + }); + }); + + describe('getFloorDataFromAdUnits', () => { + let adUnits; + + function setFloorValues(rule) { + adUnits.forEach((au, i) => { + au.floors = { + values: { + [rule]: i + 1 + } + } + }) + } + + beforeEach(() => { + adUnits = ['au1', 'au2', 'au3'].map(getAdUnitMock); + }) + + it('should use one schema for all adUnits', () => { + setFloorValues('*;*') + adUnits[1].floors.schema = { + fields: ['mediaType', 'gptSlot'], + delimiter: ';' + } + sinon.assert.match(getFloorDataFromAdUnits(adUnits), { + schema: { + fields: ['adUnitCode', 'mediaType', 'gptSlot'], + delimiter: ';' + }, + values: { + 'au1;*;*': 1, + 'au2;*;*': 2, + 'au3;*;*': 3 + } + }) + }); + it('should ignore adUnits that declare different schema', () => { + setFloorValues('*|*'); + adUnits[0].floors.schema = { + fields: ['mediaType', 'gptSlot'] + }; + adUnits[2].floors.schema = { + fields: ['gptSlot', 'mediaType'] + }; + expect(getFloorDataFromAdUnits(adUnits).values).to.eql({ + 'au1|*|*': 1, + 'au2|*|*': 2 + }) + }); + it('should ignore adUnits that declare no values', () => { + setFloorValues('*'); + adUnits[0].floors.schema = { + fields: ['mediaType'] + }; + delete adUnits[2].floors.values; + expect(getFloorDataFromAdUnits(adUnits).values).to.eql({ + 'au1|*': 1, + 'au2|*': 2, + }) + }) + }) + describe('getFloorsDataForAuction', function () { it('converts basic input floor data into a floorData map for the auction correctly', function () { // basic input where nothing needs to be updated @@ -230,8 +303,8 @@ describe('the price floors module', function () { }); describe('getFirstMatchingFloor', function () { - it('uses a 0 floor as overrite', function () { - let inputFloorData = { + it('uses a 0 floor as override', function () { + let inputFloorData = normalizeDefault({ currency: 'USD', schema: { delimiter: '|', @@ -242,7 +315,7 @@ describe('the price floors module', function () { 'test_div_2': 2 }, default: 0.5 - }; + }); expect(getFirstMatchingFloor(inputFloorData, basicBidRequest, {mediaType: 'banner', size: '*'})).to.deep.equal({ floorMin: 0, @@ -431,7 +504,7 @@ describe('the price floors module', function () { }); }); it('selects the right floor for more complex rules', function () { - let inputFloorData = { + let inputFloorData = normalizeDefault({ currency: 'USD', schema: { delimiter: '^', @@ -445,7 +518,7 @@ describe('the price floors module', function () { 'weird_div^*^300x250': 5.5 }, default: 0.5 - }; + }); // banner with 300x250 size expect(getFirstMatchingFloor(inputFloorData, basicBidRequest, {mediaType: 'banner', size: [300, 250]})).to.deep.equal({ floorMin: 0, @@ -487,10 +560,8 @@ describe('the price floors module', function () { matchingFloor: undefined }); // if default is there use it - inputFloorData = { default: 5.0 }; - expect(getFirstMatchingFloor(inputFloorData, basicBidRequest, {mediaType: 'banner', size: '*'})).to.deep.equal({ - matchingFloor: 5.0 - }); + inputFloorData = normalizeDefault({ default: 5.0 }); + expect(getFirstMatchingFloor(inputFloorData, basicBidRequest, {mediaType: 'banner', size: '*'}).matchingFloor).to.equal(5.0); }); describe('with gpt enabled', function () { let gptFloorData; @@ -577,6 +648,90 @@ describe('the price floors module', function () { }); }); }); + + describe('updateAdUnitsForAuction', function() { + let inputFloorData; + let adUnits; + + beforeEach(function() { + adUnits = [getAdUnitMock()]; + inputFloorData = utils.deepClone(minFloorConfigLow); + inputFloorData.skipRate = 0.5; + }); + + it('should set the skipRate to the skipRate from the data property before using the skipRate from floorData directly', function() { + utils.deepSetValue(inputFloorData, 'data', { + skipRate: 0.7 + }); + updateAdUnitsForAuction(adUnits, inputFloorData, 'id'); + + const skipRate = utils.deepAccess(adUnits, '0.bids.0.floorData.skipRate'); + expect(skipRate).to.equal(0.7); + }); + + it('should set the skipRate to the skipRate from floorData directly if it does not exist in the data property of floorData', function() { + updateAdUnitsForAuction(adUnits, inputFloorData, 'id'); + + const skipRate = utils.deepAccess(adUnits, '0.bids.0.floorData.skipRate'); + expect(skipRate).to.equal(0.5); + }); + + it('should set the skipRate in the bid floorData to undefined if both skipRate and skipRate in the data property are undefined', function() { + inputFloorData.skipRate = undefined; + utils.deepSetValue(inputFloorData, 'data', { + skipRate: undefined, + }); + updateAdUnitsForAuction(adUnits, inputFloorData, 'id'); + + const skipRate = utils.deepAccess(adUnits, '0.bids.0.floorData.skipRate'); + expect(skipRate).to.equal(undefined); + }); + }); + + describe('createFloorsDataForAuction', function() { + let adUnits; + let floorConfig; + + beforeEach(function() { + adUnits = [getAdUnitMock()]; + floorConfig = utils.deepClone(basicFloorConfig); + }); + + it('should return skipRate as 0 if both skipRate and skipRate in the data property are undefined', function() { + floorConfig.skipRate = undefined; + floorConfig.data.skipRate = undefined; + handleSetFloorsConfig(floorConfig); + + const floorData = createFloorsDataForAuction(adUnits, 'id'); + + expect(floorData.skipRate).to.equal(0); + expect(floorData.skipped).to.equal(false); + }); + + it('should properly set skipRate if it is available in the data property', function() { + // this will force skipped to be true + floorConfig.skipRate = 101; + floorConfig.data.skipRate = 201; + handleSetFloorsConfig(floorConfig); + + const floorData = createFloorsDataForAuction(adUnits, 'id'); + + expect(floorData.data.skipRate).to.equal(201); + expect(floorData.skipped).to.equal(true); + }); + + it('should should use the skipRate if its not available in the data property ', function() { + // this will force skipped to be true + floorConfig.skipRate = 101; + handleSetFloorsConfig(floorConfig); + + const floorData = createFloorsDataForAuction(adUnits, 'id'); + + expect(floorData.skipRate).to.equal(101); + expect(floorData.skipped).to.equal(true); + }); + }); + describe('pre-auction tests', function () { let exposedAdUnits; const validateBidRequests = (getFloorExpected, FloorDataExpected) => { @@ -591,16 +746,11 @@ describe('the price floors module', function () { adUnits, }); }; - let fakeFloorProvider; let actualAllowedFields = allowedFields; let actualFieldMatchingFunctions = fieldMatchingFunctions; const defaultAllowedFields = [...allowedFields]; const defaultMatchingFunctions = {...fieldMatchingFunctions}; - beforeEach(function() { - fakeFloorProvider = sinon.fakeServer.create(); - }); afterEach(function() { - fakeFloorProvider.restore(); exposedAdUnits = undefined; actualAllowedFields = [...defaultAllowedFields]; actualFieldMatchingFunctions = {...defaultMatchingFunctions}; @@ -623,6 +773,124 @@ describe('the price floors module', function () { floorProvider: undefined }); }); + it('should not do floor stuff if floors.data is defined by noFloorSignalBidders[]', function() { + handleSetFloorsConfig({ + ...basicFloorConfig, + data: { + ...basicFloorDataLow, + noFloorSignalBidders: ['someBidder', 'someOtherBidder'] + }}); + runStandardAuction(); + validateBidRequests(false, { + skipped: false, + floorMin: undefined, + modelVersion: 'basic model', + modelWeight: 10, + modelTimestamp: undefined, + location: 'setConfig', + skipRate: 0, + fetchStatus: undefined, + floorProvider: undefined, + noFloorSignaled: true + }) + }); + it('should not do floor stuff if floors.enforcement is defined by noFloorSignalBidders[]', function() { + handleSetFloorsConfig({ ...basicFloorConfig, + enforcement: { + enforceJS: true, + noFloorSignalBidders: ['someBidder', 'someOtherBidder'] + }, + data: basicFloorDataLow + }); + runStandardAuction(); + validateBidRequests(false, { + skipped: false, + floorMin: undefined, + modelVersion: 'basic model', + modelWeight: 10, + modelTimestamp: undefined, + location: 'setConfig', + skipRate: 0, + fetchStatus: undefined, + floorProvider: undefined, + noFloorSignaled: true + }) + }); + it('should not do floor stuff and use first floors.data.noFloorSignalBidders if its defined betwen enforcement.noFloorSignalBidders', function() { + handleSetFloorsConfig({ ...basicFloorConfig, + enforcement: { + enforceJS: true, + noFloorSignalBidders: ['someBidder'] + }, + data: { + ...basicFloorDataLow, + noFloorSignalBidders: ['someBidder', 'someOtherBidder'] + } + }); + runStandardAuction(); + validateBidRequests(false, { + skipped: false, + floorMin: undefined, + modelVersion: 'basic model', + modelWeight: 10, + modelTimestamp: undefined, + location: 'setConfig', + skipRate: 0, + fetchStatus: undefined, + floorProvider: undefined, + noFloorSignaled: true + }) + }); + it('it shouldn`t return floor stuff for bidder in the noFloorSignalBidders list', function() { + handleSetFloorsConfig({ ...basicFloorConfig, + enforcement: { + enforceJS: true, + }, + data: { + ...basicFloorDataLow, + noFloorSignalBidders: ['someBidder'] + } + }); + runStandardAuction() + const bidRequestData = exposedAdUnits[0].bids.find(bid => bid.bidder === 'someBidder'); + expect(bidRequestData.hasOwnProperty('getFloor')).to.equal(false); + sinon.assert.match(bidRequestData.floorData, { + skipped: false, + floorMin: undefined, + modelVersion: 'basic model', + modelWeight: 10, + modelTimestamp: undefined, + location: 'setConfig', + skipRate: 0, + fetchStatus: undefined, + floorProvider: undefined, + noFloorSignaled: true + }); + }) + it('it should return floor stuff if we defined wrong bidder name in data.noFloorSignalBidders', function() { + handleSetFloorsConfig({ ...basicFloorConfig, + enforcement: { + enforceJS: true, + }, + data: { + ...basicFloorDataLow, + noFloorSignalBidders: ['randomBiider'] + } + }); + runStandardAuction(); + validateBidRequests(true, { + skipped: false, + floorMin: undefined, + modelVersion: 'basic model', + modelWeight: 10, + modelTimestamp: undefined, + location: 'setConfig', + skipRate: 0, + fetchStatus: undefined, + floorProvider: undefined, + noFloorSignaled: false + }) + }); it('should use adUnit level data if not setConfig or fetch has occured', function () { handleSetFloorsConfig({ ...basicFloorConfig, @@ -695,6 +963,95 @@ describe('the price floors module', function () { floorProvider: undefined }); }); + describe('default floor', () => { + let adUnits; + beforeEach(() => { + adUnits = ['au1', 'au2'].map(getAdUnitMock); + }) + function expectFloors(floors) { + runStandardAuction(adUnits); + adUnits.forEach((au, i) => { + au.bids.forEach(bid => { + expect(bid.getFloor().floor).to.eql(floors[i]); + }) + }) + } + describe('should be sufficient by itself', () => { + it('globally', () => { + handleSetFloorsConfig({ + ...basicFloorConfig, + data: { + default: 1.23 + } + }); + expectFloors([1.23, 1.23]) + }); + it('on adUnits', () => { + handleSetFloorsConfig({ + ...basicFloorConfig, + data: undefined + }); + adUnits[0].floors = {default: 1}; + adUnits[1].floors = {default: 2}; + expectFloors([1, 2]) + }); + it('on an adUnit with hidden schema', () => { + handleSetFloorsConfig({ + ...basicFloorConfig, + data: undefined + }); + adUnits[0].floors = { + schema: { + fields: ['mediaType', 'gptSlot'], + }, + default: 1 + } + adUnits[1].floors = { + default: 2 + } + expectFloors([1, 2]); + }) + }); + describe('should NOT be used when a star rule exists', () => { + it('globally', () => { + handleSetFloorsConfig({ + ...basicFloorConfig, + data: { + schema: { + fields: ['mediaType', 'gptSlot'], + }, + values: { + '*|*': 2 + }, + default: 3, + } + }); + expectFloors([2, 2]); + }); + it('on adUnits', () => { + handleSetFloorsConfig({ + ...basicFloorConfig, + data: undefined + }); + adUnits[0].floors = { + schema: { + fields: ['mediaType', 'gptSlot'], + }, + values: { + '*|*': 1 + }, + default: 3 + }; + adUnits[1].floors = { + values: { + '*|*': 2 + }, + default: 4 + } + expectFloors([1, 2]); + }) + }); + }) it('bidRequests should have getFloor function and flooring meta data when setConfig occurs', function () { handleSetFloorsConfig({...basicFloorConfig, floorProvider: 'floorprovider'}); runStandardAuction(); @@ -984,7 +1341,7 @@ describe('the price floors module', function () { }); }); it('Should continue auction of delay is hit without a response from floor provider', function () { - handleSetFloorsConfig({...basicFloorConfig, auctionDelay: 250, endpoint: {url: 'http://www.fakeFloorProvider.json'}}); + handleSetFloorsConfig({...basicFloorConfig, auctionDelay: 250, endpoint: {url: 'http://www.fakefloorprovider.json//'}}); // start the auction it should delay and not immediately call `continueAuction` runStandardAuction(); @@ -1011,7 +1368,7 @@ describe('the price floors module', function () { fetchStatus: 'timeout', floorProvider: undefined }); - fakeFloorProvider.respond(); + server.respond(); }); it('It should fetch if config has url and bidRequests have fetch level flooring meta data', function () { // init the fake server with response stuff @@ -1019,14 +1376,14 @@ describe('the price floors module', function () { ...basicFloorData, modelVersion: 'fetch model name', // change the model name }; - fakeFloorProvider.respondWith(JSON.stringify(fetchFloorData)); + server.respondWith(JSON.stringify(fetchFloorData)); // run setConfig indicating fetch - handleSetFloorsConfig({...basicFloorConfig, floorProvider: 'floorprovider', auctionDelay: 250, endpoint: {url: 'http://www.fakeFloorProvider.json'}}); + handleSetFloorsConfig({...basicFloorConfig, floorProvider: 'floorprovider', auctionDelay: 250, endpoint: {url: 'http://www.fakefloorprovider.json/'}}); // floor provider should be called - expect(fakeFloorProvider.requests.length).to.equal(1); - expect(fakeFloorProvider.requests[0].url).to.equal('http://www.fakeFloorProvider.json'); + expect(server.requests.length).to.equal(1); + expect(server.requests[0].url).to.equal('http://www.fakefloorprovider.json/'); // start the auction it should delay and not immediately call `continueAuction` runStandardAuction(); @@ -1035,7 +1392,7 @@ describe('the price floors module', function () { expect(exposedAdUnits).to.be.undefined; // make the fetch respond - fakeFloorProvider.respond(); + server.respond(); expect(exposedAdUnits).to.not.be.undefined; // the exposedAdUnits should be from the fetch not setConfig level data @@ -1059,14 +1416,14 @@ describe('the price floors module', function () { floorProvider: 'floorProviderD', // change the floor provider modelVersion: 'fetch model name', // change the model name }; - fakeFloorProvider.respondWith(JSON.stringify(fetchFloorData)); + server.respondWith(JSON.stringify(fetchFloorData)); // run setConfig indicating fetch - handleSetFloorsConfig({...basicFloorConfig, floorProvider: 'floorproviderC', auctionDelay: 250, endpoint: {url: 'http://www.fakeFloorProvider.json'}}); + handleSetFloorsConfig({...basicFloorConfig, floorProvider: 'floorproviderC', auctionDelay: 250, endpoint: {url: 'http://www.fakefloorprovider.json/'}}); // floor provider should be called - expect(fakeFloorProvider.requests.length).to.equal(1); - expect(fakeFloorProvider.requests[0].url).to.equal('http://www.fakeFloorProvider.json'); + expect(server.requests.length).to.equal(1); + expect(server.requests[0].url).to.equal('http://www.fakefloorprovider.json/'); // start the auction it should delay and not immediately call `continueAuction` runStandardAuction(); @@ -1075,7 +1432,7 @@ describe('the price floors module', function () { expect(exposedAdUnits).to.be.undefined; // make the fetch respond - fakeFloorProvider.respond(); + server.respond(); // the exposedAdUnits should be from the fetch not setConfig level data // and fetchStatus is success since fetch worked @@ -1100,14 +1457,14 @@ describe('the price floors module', function () { modelVersion: 'fetch model name', // change the model name }; fetchFloorData.skipRate = 95; - fakeFloorProvider.respondWith(JSON.stringify(fetchFloorData)); + server.respondWith(JSON.stringify(fetchFloorData)); // run setConfig indicating fetch - handleSetFloorsConfig({...basicFloorConfig, floorProvider: 'floorprovider', auctionDelay: 250, endpoint: {url: 'http://www.fakeFloorProvider.json'}}); + handleSetFloorsConfig({...basicFloorConfig, floorProvider: 'floorprovider', auctionDelay: 250, endpoint: {url: 'http://www.fakefloorprovider.json/'}}); // floor provider should be called - expect(fakeFloorProvider.requests.length).to.equal(1); - expect(fakeFloorProvider.requests[0].url).to.equal('http://www.fakeFloorProvider.json'); + expect(server.requests.length).to.equal(1); + expect(server.requests[0].url).to.equal('http://www.fakefloorprovider.json/'); // start the auction it should delay and not immediately call `continueAuction` runStandardAuction(); @@ -1116,7 +1473,7 @@ describe('the price floors module', function () { expect(exposedAdUnits).to.be.undefined; // make the fetch respond - fakeFloorProvider.respond(); + server.respond(); expect(exposedAdUnits).to.not.be.undefined; // the exposedAdUnits should be from the fetch not setConfig level data @@ -1135,10 +1492,10 @@ describe('the price floors module', function () { }); it('Should not break if floor provider returns 404', function () { // run setConfig indicating fetch - handleSetFloorsConfig({...basicFloorConfig, auctionDelay: 250, endpoint: {url: 'http://www.fakeFloorProvider.json'}}); + handleSetFloorsConfig({...basicFloorConfig, auctionDelay: 250, endpoint: {url: 'http://www.fakefloorprovider.json/'}}); // run the auction and make server respond with 404 - fakeFloorProvider.respond(); + server.respond(); runStandardAuction(); // error should have been called for fetch error @@ -1158,13 +1515,13 @@ describe('the price floors module', function () { }); }); it('Should not break if floor provider returns non json', function () { - fakeFloorProvider.respondWith('Not valid response'); + server.respondWith('Not valid response'); // run setConfig indicating fetch - handleSetFloorsConfig({...basicFloorConfig, auctionDelay: 250, endpoint: {url: 'http://www.fakeFloorProvider.json'}}); + handleSetFloorsConfig({...basicFloorConfig, auctionDelay: 250, endpoint: {url: 'http://www.fakefloorprovider.json/'}}); // run the auction and make server respond - fakeFloorProvider.respond(); + server.respond(); runStandardAuction(); // error should have been called for response floor data not being valid @@ -1185,27 +1542,27 @@ describe('the price floors module', function () { }); it('should handle not using fetch correctly', function () { // run setConfig twice indicating fetch - fakeFloorProvider.respondWith(JSON.stringify(basicFloorData)); - handleSetFloorsConfig({...basicFloorConfig, auctionDelay: 250, endpoint: {url: 'http://www.fakeFloorProvider.json'}}); - handleSetFloorsConfig({...basicFloorConfig, auctionDelay: 250, endpoint: {url: 'http://www.fakeFloorProvider.json'}}); + server.respondWith(JSON.stringify(basicFloorData)); + handleSetFloorsConfig({...basicFloorConfig, auctionDelay: 250, endpoint: {url: 'http://www.fakefloorprovider.json/'}}); + handleSetFloorsConfig({...basicFloorConfig, auctionDelay: 250, endpoint: {url: 'http://www.fakefloorprovider.json/'}}); // log warn should be called and server only should have one request expect(logWarnSpy.calledOnce).to.equal(true); - expect(fakeFloorProvider.requests.length).to.equal(1); - expect(fakeFloorProvider.requests[0].url).to.equal('http://www.fakeFloorProvider.json'); + expect(server.requests.length).to.equal(1); + expect(server.requests[0].url).to.equal('http://www.fakefloorprovider.json/'); // now we respond and then run again it should work and make another request - fakeFloorProvider.respond(); - handleSetFloorsConfig({...basicFloorConfig, auctionDelay: 250, endpoint: {url: 'http://www.fakeFloorProvider.json'}}); - fakeFloorProvider.respond(); + server.respond(); + handleSetFloorsConfig({...basicFloorConfig, auctionDelay: 250, endpoint: {url: 'http://www.fakefloorprovider.json/'}}); + server.respond(); // now warn still only called once and server called twice expect(logWarnSpy.calledOnce).to.equal(true); - expect(fakeFloorProvider.requests.length).to.equal(2); + expect(server.requests.length).to.equal(2); // should log error if method is not GET for now expect(logErrorSpy.calledOnce).to.equal(false); - handleSetFloorsConfig({...basicFloorConfig, endpoint: {url: 'http://www.fakeFloorProvider.json', method: 'POST'}}); + handleSetFloorsConfig({...basicFloorConfig, endpoint: {url: 'http://www.fakefloorprovider.json/', method: 'POST'}}); expect(logErrorSpy.calledOnce).to.equal(true); }); describe('isFloorsDataValid', function () { @@ -1369,10 +1726,22 @@ describe('the price floors module', function () { floor: 2.5 }); }); + + it('works when TIDs are disabled', () => { + sandbox.stub(activities, 'isActivityAllowed').returns(false); + const req = utils.deepClone(bidRequest); + _floorDataForAuction[req.auctionId] = utils.deepClone(basicFloorConfig); + + expect(guardTids('mock-bidder').bidRequest(req).getFloor({})).to.deep.equal({ + currency: 'USD', + floor: 1.0 + }); + }); + it('picks the right rule with more complex rules', function () { _floorDataForAuction[bidRequest.auctionId] = { ...basicFloorConfig, - data: { + data: normalizeDefault({ currency: 'USD', schema: { fields: ['mediaType', 'size'], delimiter: '|' }, values: { @@ -1384,7 +1753,7 @@ describe('the price floors module', function () { 'video|*': 5.5 }, default: 10.0 - } + }) }; // assumes banner * @@ -1844,6 +2213,12 @@ describe('the price floors module', function () { expect(returnedBidResponse).to.not.haveOwnProperty('floorData'); expect(logWarnSpy.calledOnce).to.equal(true); }); + it('if it finds a rule with a floor price of zero it should not call log warn', function () { + _floorDataForAuction[AUCTION_ID] = utils.deepClone(basicFloorConfig); + _floorDataForAuction[AUCTION_ID].data.values = { '*': 0 }; + runBidResponse(); + expect(logWarnSpy.calledOnce).to.equal(false); + }); it('if it finds a rule and floors should update the bid accordingly', function () { _floorDataForAuction[AUCTION_ID] = utils.deepClone(basicFloorConfig); _floorDataForAuction[AUCTION_ID].data.values = { 'banner': 1.0 }; diff --git a/test/spec/modules/programmaticaBidAdapter_spec.js b/test/spec/modules/programmaticaBidAdapter_spec.js new file mode 100644 index 00000000000..247d20752c3 --- /dev/null +++ b/test/spec/modules/programmaticaBidAdapter_spec.js @@ -0,0 +1,263 @@ +import { expect } from 'chai'; +import { spec } from 'modules/programmaticaBidAdapter.js'; +import { deepClone } from 'src/utils.js'; + +describe('programmaticaBidAdapterTests', function () { + let bidRequestData = { + bids: [ + { + bidId: 'testbid', + bidder: 'programmatica', + params: { + siteId: 'testsite', + placementId: 'testplacement', + }, + sizes: [[300, 250]] + } + ] + }; + let request = []; + + it('validate_pub_params', function () { + expect( + spec.isBidRequestValid({ + bidder: 'programmatica', + params: { + siteId: 'testsite', + placementId: 'testplacement', + } + }) + ).to.equal(true); + }); + + it('validate_generated_url', function () { + const request = spec.buildRequests(deepClone(bidRequestData.bids), { timeout: 1234 }); + let req_url = request[0].url; + + expect(req_url).to.equal('https://asr.programmatica.com/get'); + }); + + it('validate_response_params', function () { + let serverResponse = { + body: { + 'id': 'crid', + 'type': { + 'format': 'Image', + 'source': 'passback', + 'dspId': '', + 'dspCreativeId': '' + }, + 'content': { + 'data': 'test ad', + 'imps': null, + 'click': { + 'url': '', + 'track': null + } + }, + 'size': '300x250', + 'matching': '', + 'cpm': 10, + 'currency': 'USD' + } + }; + + const bidRequest = deepClone(bidRequestData.bids) + bidRequest[0].mediaTypes = { + banner: {} + } + + const request = spec.buildRequests(bidRequest); + let bids = spec.interpretResponse(serverResponse, request[0]); + expect(bids).to.have.lengthOf(1); + + let bid = bids[0]; + expect(bid.ad).to.equal('test ad'); + expect(bid.cpm).to.equal(10); + expect(bid.currency).to.equal('USD'); + expect(bid.width).to.equal(300); + expect(bid.height).to.equal(250); + expect(bid.creativeId).to.equal('crid'); + expect(bid.meta.advertiserDomains).to.deep.equal(['programmatica.com']); + }); + + it('validate_response_params_imps', function () { + let serverResponse = { + body: { + 'id': 'crid', + 'type': { + 'format': 'Image', + 'source': 'passback', + 'dspId': '', + 'dspCreativeId': '' + }, + 'content': { + 'data': 'test ad', + 'imps': [ + 'testImp' + ], + 'click': { + 'url': '', + 'track': null + } + }, + 'size': '300x250', + 'matching': '', + 'cpm': 10, + 'currency': 'USD' + } + }; + + const bidRequest = deepClone(bidRequestData.bids) + bidRequest[0].mediaTypes = { + banner: {} + } + + const request = spec.buildRequests(bidRequest); + let bids = spec.interpretResponse(serverResponse, request[0]); + expect(bids).to.have.lengthOf(1); + + let bid = bids[0]; + expect(bid.ad).to.equal('test ad'); + expect(bid.cpm).to.equal(10); + expect(bid.currency).to.equal('USD'); + expect(bid.width).to.equal(300); + expect(bid.height).to.equal(250); + expect(bid.creativeId).to.equal('crid'); + expect(bid.meta.advertiserDomains).to.deep.equal(['programmatica.com']); + }) + + it('validate_invalid_response', function () { + let serverResponse = { + body: {} + }; + + const bidRequest = deepClone(bidRequestData.bids) + bidRequest[0].mediaTypes = { + banner: {} + } + + const request = spec.buildRequests(bidRequest); + let bids = spec.interpretResponse(serverResponse, request[0]); + expect(bids).to.have.lengthOf(0); + }) + + it('video_bid', function () { + const bidRequest = deepClone(bidRequestData.bids); + bidRequest[0].mediaTypes = { + video: { + playerSize: [234, 765] + } + }; + + const request = spec.buildRequests(bidRequest, { timeout: 1234 }); + const vastXml = ''; + let serverResponse = { + body: { + 'id': 'cki2n3n6snkuulqutpf0', + 'type': { + 'format': '', + 'source': 'rtb', + 'dspId': '1' + }, + 'content': { + 'data': vastXml, + 'imps': [ + 'https://asr.dev.programmatica.com/track/imp' + ], + 'click': { + 'url': '', + 'track': null + } + }, + 'size': '', + 'matching': '', + 'cpm': 70, + 'currency': 'RUB' + } + }; + + let bids = spec.interpretResponse(serverResponse, request[0]); + expect(bids).to.have.lengthOf(1); + + let bid = bids[0]; + expect(bid.mediaType).to.equal('video'); + expect(bid.vastXml).to.equal(vastXml); + expect(bid.width).to.equal(234); + expect(bid.height).to.equal(765); + }); +}); + +describe('getUserSyncs', function() { + it('returns empty sync array', function() { + const syncOptions = {}; + + expect(spec.getUserSyncs(syncOptions)).to.deep.equal([]); + }); + + it('Should return array of objects with proper sync config , include CCPA', function() { + const syncData = spec.getUserSyncs({ + pixelEnabled: true, + }, {}, {}, '1---'); + expect(syncData).to.be.an('array').which.is.not.empty; + expect(syncData[0]).to.be.an('object') + expect(syncData[0].type).to.be.a('string') + expect(syncData[0].type).to.equal('image') + expect(syncData[0].url).to.be.a('string') + expect(syncData[0].url).to.equal('//sync.programmatica.com/match/sp?usp=1---&consent=') + }); + + it('Should return array of objects with proper sync config , include GDPR', function() { + const syncData = spec.getUserSyncs({ + iframeEnabled: true, + }, {}, { + gdprApplies: true, + consentString: 'COvFyGBOvFyGBAbAAAENAPCAAOAAAAAAAAAAAEEUACCKAAA.IFoEUQQgAIQwgIwQABAEAAAAOIAACAIAAAAQAIAgEAACEAAAAAgAQBAAAAAAAGBAAgAAAAAAAFAAECAAAgAAQARAEQAAAAAJAAIAAgAAAYQEAAAQmAgBC3ZAYzUw', + vendorData: { + purpose: { + consents: { + 1: true + }, + }, + } + }, ''); + expect(syncData).to.be.an('array').which.is.not.empty; + expect(syncData[0]).to.be.an('object') + expect(syncData[0].type).to.be.a('string') + expect(syncData[0].type).to.equal('iframe') + expect(syncData[0].url).to.be.a('string') + expect(syncData[0].url).to.equal('//sync.programmatica.com/match/sp.ifr?usp=&consent=COvFyGBOvFyGBAbAAAENAPCAAOAAAAAAAAAAAEEUACCKAAA.IFoEUQQgAIQwgIwQABAEAAAAOIAACAIAAAAQAIAgEAACEAAAAAgAQBAAAAAAAGBAAgAAAAAAAFAAECAAAgAAQARAEQAAAAAJAAIAAgAAAYQEAAAQmAgBC3ZAYzUw&gdpr=1') + }); + + it('Should return array of objects with proper sync config , include GDPR, no purpose', function() { + const syncData = spec.getUserSyncs({ + iframeEnabled: true, + }, {}, { + gdprApplies: true, + consentString: 'COvFyGBOvFyGBAbAAAENAPCAAOAAAAAAAAAAAEEUACCKAAA.IFoEUQQgAIQwgIwQABAEAAAAOIAACAIAAAAQAIAgEAACEAAAAAgAQBAAAAAAAGBAAgAAAAAAAFAAECAAAgAAQARAEQAAAAAJAAIAAgAAAYQEAAAQmAgBC3ZAYzUw', + vendorData: { + purpose: { + consents: { + 1: false + }, + }, + } + }, ''); + expect(syncData).is.empty; + }); + + it('Should return array of objects with proper sync config , GDPR not applies', function() { + const syncData = spec.getUserSyncs({ + iframeEnabled: true, + }, {}, { + gdprApplies: false, + consentString: 'COvFyGBOvFyGBAbAAAENAPCAAOAAAAAAAAAAAEEUACCKAAA.IFoEUQQgAIQwgIwQABAEAAAAOIAACAIAAAAQAIAgEAACEAAAAAgAQBAAAAAAAGBAAgAAAAAAAFAAECAAAgAAQARAEQAAAAAJAAIAAgAAAYQEAAAQmAgBC3ZAYzUw', + }, ''); + expect(syncData).to.be.an('array').which.is.not.empty; + expect(syncData[0]).to.be.an('object') + expect(syncData[0].type).to.be.a('string') + expect(syncData[0].type).to.equal('iframe') + expect(syncData[0].url).to.be.a('string') + expect(syncData[0].url).to.equal('//sync.programmatica.com/match/sp.ifr?usp=&consent=COvFyGBOvFyGBAbAAAENAPCAAOAAAAAAAAAAAEEUACCKAAA.IFoEUQQgAIQwgIwQABAEAAAAOIAACAIAAAAQAIAgEAACEAAAAAgAQBAAAAAAAGBAAgAAAAAAAFAAECAAAgAAQARAEQAAAAAJAAIAAgAAAYQEAAAQmAgBC3ZAYzUw&gdpr=0') + }); +}) diff --git a/test/spec/modules/acuityAdsBidAdapter_spec.js b/test/spec/modules/pubCircleBidAdapter_spec.js similarity index 96% rename from test/spec/modules/acuityAdsBidAdapter_spec.js rename to test/spec/modules/pubCircleBidAdapter_spec.js index 05c59036ff3..8aaa023ee1c 100644 --- a/test/spec/modules/acuityAdsBidAdapter_spec.js +++ b/test/spec/modules/pubCircleBidAdapter_spec.js @@ -1,11 +1,11 @@ import { expect } from 'chai'; -import { spec } from '../../../modules/acuityAdsBidAdapter'; +import { spec } from '../../../modules/pubCircleBidAdapter'; import { BANNER, VIDEO, NATIVE } from '../../../src/mediaTypes.js'; import { getUniqueIdentifierStr } from '../../../src/utils.js'; -const bidder = 'acuityads' +const bidder = 'pubcircle' -describe('AcuityAdsBidAdapter', function () { +describe('PubCircleBidAdapter', function () { const bids = [ { bidId: getUniqueIdentifierStr(), @@ -104,7 +104,7 @@ describe('AcuityAdsBidAdapter', function () { }); it('Returns valid URL', function () { - expect(serverRequest.url).to.equal('https://prebid.admanmedia.com/pbjs'); + expect(serverRequest.url).to.equal('https://ml.pubcircle.ai/pbjs'); }); it('Returns general data valid', function () { @@ -382,7 +382,7 @@ describe('AcuityAdsBidAdapter', function () { expect(syncData[0].type).to.be.a('string') expect(syncData[0].type).to.equal('image') expect(syncData[0].url).to.be.a('string') - expect(syncData[0].url).to.equal('https://cs.admanmedia.com/image?pbjs=1&gdpr=1&gdpr_consent=ALL&coppa=0') + expect(syncData[0].url).to.equal('https://cs.pubcircle.ai/image?pbjs=1&gdpr=1&gdpr_consent=ALL&coppa=0') }); it('Should return array of objects with proper sync config , include CCPA', function() { const syncData = spec.getUserSyncs({}, {}, {}, { @@ -393,7 +393,7 @@ describe('AcuityAdsBidAdapter', function () { expect(syncData[0].type).to.be.a('string') expect(syncData[0].type).to.equal('image') expect(syncData[0].url).to.be.a('string') - expect(syncData[0].url).to.equal('https://cs.admanmedia.com/image?pbjs=1&ccpa_consent=1---&coppa=0') + expect(syncData[0].url).to.equal('https://cs.pubcircle.ai/image?pbjs=1&ccpa_consent=1---&coppa=0') }); }); }); diff --git a/test/spec/modules/publinkIdSystem_spec.js b/test/spec/modules/publinkIdSystem_spec.js index 7d98b724bd8..5ad58ea1a37 100644 --- a/test/spec/modules/publinkIdSystem_spec.js +++ b/test/spec/modules/publinkIdSystem_spec.js @@ -72,11 +72,6 @@ describe('PublinkIdSystem', () => { expect(result.callback).to.be.a('function'); }); - it('Use local copy', () => { - const result = publinkIdSubmodule.getId({}, undefined, TEST_COOKIE_VALUE); - expect(result).to.be.undefined; - }); - describe('callout for id', () => { let callbackSpy = sinon.spy(); @@ -84,6 +79,44 @@ describe('PublinkIdSystem', () => { callbackSpy.resetHistory(); }); + it('Has cached id', () => { + const config = {storage: {type: 'cookie'}}; + let submoduleCallback = publinkIdSubmodule.getId(config, undefined, TEST_COOKIE_VALUE).callback; + submoduleCallback(callbackSpy); + + const request = server.requests[0]; + const parsed = parseUrl(request.url); + + expect(parsed.hostname).to.equal('proc.ad.cpe.dotomi.com'); + expect(parsed.pathname).to.equal('/cvx/client/sync/publink/refresh'); + expect(parsed.search.mpn).to.equal('Prebid.js'); + expect(parsed.search.mpv).to.equal('$prebid.version$'); + expect(parsed.search.publink).to.equal(TEST_COOKIE_VALUE); + + request.respond(200, {}, JSON.stringify(serverResponse)); + expect(callbackSpy.calledOnce).to.be.true; + expect(callbackSpy.lastCall.lastArg).to.equal(serverResponse.publink); + }); + + it('Request path has priority', () => { + const config = {storage: {type: 'cookie'}, params: {e: 'ca11c0ca7', site_id: '102030'}}; + let submoduleCallback = publinkIdSubmodule.getId(config, undefined, TEST_COOKIE_VALUE).callback; + submoduleCallback(callbackSpy); + + const request = server.requests[0]; + const parsed = parseUrl(request.url); + + expect(parsed.hostname).to.equal('proc.ad.cpe.dotomi.com'); + expect(parsed.pathname).to.equal('/cvx/client/sync/publink'); + expect(parsed.search.mpn).to.equal('Prebid.js'); + expect(parsed.search.mpv).to.equal('$prebid.version$'); + expect(parsed.search.publink).to.equal(TEST_COOKIE_VALUE); + + request.respond(200, {}, JSON.stringify(serverResponse)); + expect(callbackSpy.calledOnce).to.be.true; + expect(callbackSpy.lastCall.lastArg).to.equal(serverResponse.publink); + }); + it('Fetch with consent data', () => { const config = {storage: {type: 'cookie'}, params: {e: 'ca11c0ca7', site_id: '102030'}}; const consentData = {gdprApplies: 1, consentString: 'myconsentstring'}; @@ -120,7 +153,7 @@ describe('PublinkIdSystem', () => { expect(parsed.search.mpn).to.equal('Prebid.js'); expect(parsed.search.mpv).to.equal('$prebid.version$'); - request.respond(204, {}, JSON.stringify(serverResponse)); + request.respond(204); expect(callbackSpy.called).to.be.false; }); diff --git a/test/spec/modules/pubmaticAnalyticsAdapter_spec.js b/test/spec/modules/pubmaticAnalyticsAdapter_spec.js index c56ed565c43..c6447905ecd 100755 --- a/test/spec/modules/pubmaticAnalyticsAdapter_spec.js +++ b/test/spec/modules/pubmaticAnalyticsAdapter_spec.js @@ -2,10 +2,10 @@ import pubmaticAnalyticsAdapter, { getMetadata } from 'modules/pubmaticAnalytics import adapterManager from 'src/adapterManager.js'; import CONSTANTS from 'src/constants.json'; import { config } from 'src/config.js'; -import { - setConfig, - addBidResponseHook, -} from 'modules/currency.js'; +import { setConfig } from 'modules/currency.js'; +import { server } from '../../mocks/xhr.js'; +import 'src/prebid.js'; +import { getGlobal } from 'src/prebidGlobal'; let events = require('src/events'); let ajax = require('src/ajax'); @@ -23,6 +23,7 @@ const { AUCTION_END, BID_REQUESTED, BID_RESPONSE, + BID_REJECTED, BIDDER_DONE, BID_WON, BID_TIMEOUT, @@ -108,6 +109,9 @@ const BID2 = Object.assign({}, BID, { } }); +const BID3 = Object.assign({}, BID2, { + rejectionReason: CONSTANTS.REJECTION_REASON.FLOOR_NOT_MET +}) const MOCK = { SET_TARGETING: { [BID.adUnitCode]: BID.adserverTargeting, @@ -237,6 +241,9 @@ const MOCK = { BID, BID2 ], + REJECTED_BID: [ + BID3 + ], AUCTION_END: { 'auctionId': '25c6d7f5-699a-4bfc-87c9-996f915341fa' }, @@ -273,7 +280,6 @@ function getLoggerJsonFromRequest(requestBody) { describe('pubmatic analytics adapter', function () { let sandbox; - let xhr; let requests; let oldScreen; let clock; @@ -282,9 +288,7 @@ describe('pubmatic analytics adapter', function () { setUADefault(); sandbox = sinon.sandbox.create(); - xhr = sandbox.useFakeXMLHttpRequest(); - requests = []; - xhr.onCreate = request => requests.push(request); + requests = server.requests; sandbox.stub(events, 'getEvents').returns([]); @@ -312,6 +316,208 @@ describe('pubmatic analytics adapter', function () { expect(utils.logError.called).to.equal(true); }); + describe('OW S2S', function() { + this.beforeEach(function() { + pubmaticAnalyticsAdapter.enableAnalytics({ + options: { + publisherId: 9999, + profileId: 1111, + profileVersionId: 20 + } + }); + config.setConfig({ + s2sConfig: { + accountId: '1234', + bidders: ['pubmatic'], + defaultVendor: 'openwrap', + timeout: 500 + } + }); + }); + + this.afterEach(function() { + pubmaticAnalyticsAdapter.disableAnalytics(); + }); + + it('Pubmatic Won: No tracker fired', function() { + this.timeout(5000) + + sandbox.stub($$PREBID_GLOBAL$$, 'getHighestCpmBids').callsFake((key) => { + return [MOCK.BID_RESPONSE[0], MOCK.BID_RESPONSE[1]] + }); + + config.setConfig({ + testGroupId: 15 + }); + + events.emit(AUCTION_INIT, MOCK.AUCTION_INIT); + events.emit(BID_REQUESTED, MOCK.BID_REQUESTED); + events.emit(BID_RESPONSE, MOCK.BID_RESPONSE[0]); + events.emit(BIDDER_DONE, MOCK.BIDDER_DONE); + events.emit(AUCTION_END, MOCK.AUCTION_END); + events.emit(SET_TARGETING, MOCK.SET_TARGETING); + events.emit(BID_WON, MOCK.BID_WON[0]); + + clock.tick(2000 + 1000); + expect(requests.length).to.equal(1); // only logger is fired + let request = requests[0]; + expect(request.url).to.equal('https://t.pubmatic.com/wl?pubid=9999'); + let data = getLoggerJsonFromRequest(request.requestBody); + expect(data.pubid).to.equal('9999'); + expect(data.pid).to.equal('1111'); + expect(data.pdvid).to.equal('20'); + }); + + it('Non-pubmatic won: logger, tracker fired', function() { + const APPNEXUS_BID = Object.assign({}, BID, { + 'bidder': 'appnexus', + 'adserverTargeting': { + 'hb_bidder': 'appnexus', + 'hb_adid': '2ecff0db240757', + 'hb_pb': 1.20, + 'hb_size': '640x480', + 'hb_source': 'server' + } + }); + + const MOCK_AUCTION_INIT_APPNEXUS = { + 'auctionId': '25c6d7f5-699a-4bfc-87c9-996f915341fa', + 'timestamp': 1519767010567, + 'auctionStatus': 'inProgress', + 'adUnits': [ { + 'code': '/19968336/header-bid-tag-1', + 'sizes': [[640, 480]], + 'bids': [ { + 'bidder': 'appnexus', + 'params': { + 'publisherId': '1001' + } + } ], + 'transactionId': 'ca4af27a-6d02-4f90-949d-d5541fa12014' + } + ], + 'adUnitCodes': ['/19968336/header-bid-tag-1'], + 'bidderRequests': [ { + 'bidderCode': 'appnexus', + 'auctionId': '25c6d7f5-699a-4bfc-87c9-996f915341fa', + 'bidderRequestId': '1be65d7958826a', + 'bids': [ { + 'bidder': 'appnexus', + 'params': { + 'publisherId': '1001', + 'kgpv': 'this-is-a-kgpv' + }, + 'mediaTypes': { + 'banner': { + 'sizes': [[640, 480]] + } + }, + 'adUnitCode': '/19968336/header-bid-tag-1', + 'transactionId': 'ca4af27a-6d02-4f90-949d-d5541fa12014', + 'sizes': [[640, 480]], + 'bidId': '2ecff0db240757', + 'bidderRequestId': '1be65d7958826a', + 'auctionId': '25c6d7f5-699a-4bfc-87c9-996f915341fa', + 'src': 'client', + 'bidRequestsCount': 1 + } + ], + 'timeout': 3000, + 'refererInfo': { + 'topmostLocation': 'http://www.test.com/page.html', 'reachedTop': true, 'numIframes': 0, 'stack': ['http://www.test.com/page.html'] + } + } + ], + 'bidsReceived': [], + 'winningBids': [], + 'timeout': 3000 + }; + + const MOCK_BID_REQUESTED_APPNEXUS = { + 'bidder': 'appnexus', + 'auctionId': '25c6d7f5-699a-4bfc-87c9-996f915341fa', + 'bidderRequestId': '1be65d7958826a', + 'bids': [ + { + 'bidder': 'appnexus', + 'adapterCode': 'appnexus', + 'bidderCode': 'appnexus', + 'params': { + 'publisherId': '1001', + 'video': { + 'minduration': 30, + 'skippable': true + } + }, + 'mediaType': 'video', + 'adUnitCode': '/19968336/header-bid-tag-0', + 'transactionId': 'ca4af27a-6d02-4f90-949d-d5541fa12014', + 'sizes': [[640, 480]], + 'bidId': '2ecff0db240757', + 'bidderRequestId': '1be65d7958826a', + 'auctionId': '25c6d7f5-699a-4bfc-87c9-996f915341fa' + } + ], + 'auctionStart': 1519149536560, + 'timeout': 5000, + 'start': 1519149562216, + 'refererInfo': { + 'topmostLocation': 'http://www.test.com/page.html', 'reachedTop': true, 'numIframes': 0, 'stack': ['http://www.test.com/page.html'] + }, + 'gdprConsent': { + 'consentString': 'here-goes-gdpr-consent-string', + 'gdprApplies': true + } + }; + + this.timeout(5000) + + sandbox.stub($$PREBID_GLOBAL$$, 'getHighestCpmBids').callsFake((key) => { + return [APPNEXUS_BID] + }); + + events.emit(AUCTION_INIT, MOCK_AUCTION_INIT_APPNEXUS); + events.emit(BID_REQUESTED, MOCK_BID_REQUESTED_APPNEXUS); + events.emit(BID_RESPONSE, APPNEXUS_BID); + events.emit(BIDDER_DONE, { + 'bidderCode': 'appnexus', + 'bids': [ + APPNEXUS_BID, + Object.assign({}, APPNEXUS_BID, { + 'serverResponseTimeMs': 42, + }) + ] + }); + events.emit(AUCTION_END, MOCK.AUCTION_END); + events.emit(SET_TARGETING, { + [APPNEXUS_BID.adUnitCode]: APPNEXUS_BID.adserverTargeting, + }); + events.emit(BID_WON, Object.assign({}, APPNEXUS_BID, { + 'status': 'rendered' + })); + + clock.tick(2000 + 1000); + expect(requests.length).to.equal(2); // logger as well as tracker is fired + let request = requests[1]; // logger is executed late, trackers execute first + expect(request.url).to.equal('https://t.pubmatic.com/wl?pubid=9999'); + let data = getLoggerJsonFromRequest(request.requestBody); + expect(data.pubid).to.equal('9999'); + expect(data.pid).to.equal('1111'); + expect(data.pdvid).to.equal('20'); + + let firstTracker = requests[0].url; + expect(firstTracker.split('?')[0]).to.equal('https://t.pubmatic.com/wt'); + firstTracker.split('?')[1].split('&').map(e => e.split('=')).forEach(e => data[e[0]] = e[1]); + expect(data.pubid).to.equal('9999'); + expect(decodeURIComponent(data.purl)).to.equal('http://www.test.com/page.html'); + + expect(data.s).to.be.an('array'); + expect(data.s.length).to.equal(1); + expect(data.s[0].ps[0].pn).to.equal('appnexus'); + expect(data.s[0].ps[0].bc).to.equal('appnexus'); + }) + }); + describe('when handling events', function() { beforeEach(function () { pubmaticAnalyticsAdapter.enableAnalytics({ @@ -364,15 +570,20 @@ describe('pubmatic analytics adapter', function () { expect(data.tgid).to.equal(15); expect(data.fmv).to.equal('floorModelTest'); expect(data.ft).to.equal(1); + expect(data.pbv).to.equal(getGlobal()?.version || '-1'); expect(data.s).to.be.an('array'); expect(data.s.length).to.equal(2); // slot 1 expect(data.s[0].sn).to.equal('/19968336/header-bid-tag-0'); expect(data.s[0].fskp).to.equal(0); + expect(data.s[0].sid).not.to.be.undefined; + expect(data.s[0].ffs).to.equal(1); + expect(data.s[0].fsrc).to.equal(2); + expect(data.s[0].fp).to.equal('pubmatic'); expect(data.s[0].sz).to.deep.equal(['640x480']); expect(data.s[0].ps).to.be.an('array'); - expect(data.s[0].au).to.equal('/19968336/header-bid-tag-0'); - expect(data.s[0].ps.length).to.equal(1); + expect(data.s[0].au).to.equal('/19968336/header-bid-tag-0'); + expect(data.s[0].ps.length).to.equal(1); expect(data.s[0].ps[0].pn).to.equal('pubmatic'); expect(data.s[0].ps[0].bc).to.equal('pubmatic'); expect(data.s[0].ps[0].bidid).to.equal('2ecff0db240757'); @@ -383,9 +594,10 @@ describe('pubmatic analytics adapter', function () { expect(data.s[0].ps[0].psz).to.equal('640x480'); expect(data.s[0].ps[0].eg).to.equal(1.23); expect(data.s[0].ps[0].en).to.equal(1.23); - expect(data.s[0].ps[0].di).to.equal(''); + expect(data.s[0].ps[0].di).to.equal('-1'); expect(data.s[0].ps[0].dc).to.equal(''); - expect(data.s[0].ps[0].l1).to.equal(3214); + expect(data.s[0].ps[0].l1).to.equal(944); + expect(data.s[0].ps[0].ol1).to.equal(3214); expect(data.s[0].ps[0].l2).to.equal(0); expect(data.s[0].ps[0].ss).to.equal(1); expect(data.s[0].ps[0].t).to.equal(0); @@ -393,10 +605,14 @@ describe('pubmatic analytics adapter', function () { expect(data.s[0].ps[0].af).to.equal('video'); expect(data.s[0].ps[0].ocpm).to.equal(1.23); expect(data.s[0].ps[0].ocry).to.equal('USD'); - expect(data.s[0].ps[0].frv).to.equal(undefined); + expect(data.s[0].ps[0].frv).to.equal(1.1); // slot 2 expect(data.s[1].sn).to.equal('/19968336/header-bid-tag-1'); expect(data.s[1].fskp).to.equal(0); + expect(data.s[1].sid).not.to.be.undefined; + expect(data.s[1].ffs).to.equal(1); + expect(data.s[1].fsrc).to.equal(2); + expect(data.s[1].fp).to.equal('pubmatic'); 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); @@ -414,7 +630,8 @@ describe('pubmatic analytics adapter', function () { expect(data.s[1].ps[0].dc).to.equal('PMP'); expect(data.s[1].ps[0].mi).to.equal('matched-impression'); expect(data.s[1].ps[0].adv).to.equal('example.com'); - expect(data.s[1].ps[0].l1).to.equal(3214); + expect(data.s[0].ps[0].l1).to.equal(944); + expect(data.s[0].ps[0].ol1).to.equal(3214); expect(data.s[1].ps[0].l2).to.equal(0); expect(data.s[1].ps[0].ss).to.equal(1); expect(data.s[1].ps[0].t).to.equal(0); @@ -422,7 +639,7 @@ describe('pubmatic analytics adapter', function () { expect(data.s[1].ps[0].af).to.equal('banner'); expect(data.s[1].ps[0].ocpm).to.equal(1.52); expect(data.s[1].ps[0].ocry).to.equal('USD'); - expect(data.s[1].ps[0].frv).to.equal(undefined); + expect(data.s[1].ps[0].frv).to.equal(1.1); // tracker slot1 let firstTracker = requests[0].url; @@ -452,6 +669,94 @@ describe('pubmatic analytics adapter', function () { expect(data.af).to.equal('video'); }); + it('Logger : do not log floor fields when prebids floor shows noData in location property', function() { + const BID_REQUESTED_COPY = utils.deepClone(MOCK.BID_REQUESTED); + BID_REQUESTED_COPY['bids'][1]['floorData']['location'] = 'noData'; + + this.timeout(5000) + + sandbox.stub($$PREBID_GLOBAL$$, 'getHighestCpmBids').callsFake((key) => { + return [MOCK.BID_RESPONSE[0], MOCK.BID_RESPONSE[1]] + }); + + config.setConfig({ + testGroupId: 15 + }); + + events.emit(AUCTION_INIT, MOCK.AUCTION_INIT); + events.emit(BID_REQUESTED, BID_REQUESTED_COPY); + events.emit(BID_RESPONSE, MOCK.BID_RESPONSE[0]); + events.emit(BID_RESPONSE, MOCK.BID_RESPONSE[1]); + events.emit(BIDDER_DONE, MOCK.BIDDER_DONE); + events.emit(AUCTION_END, MOCK.AUCTION_END); + events.emit(SET_TARGETING, MOCK.SET_TARGETING); + events.emit(BID_WON, MOCK.BID_WON[0]); + events.emit(BID_WON, MOCK.BID_WON[1]); + + clock.tick(2000 + 1000); + expect(requests.length).to.equal(3); // 1 logger and 2 win-tracker + let request = requests[2]; // logger is executed late, trackers execute first + expect(request.url).to.equal('https://t.pubmatic.com/wl?pubid=9999'); + + let data = getLoggerJsonFromRequest(request.requestBody); + + expect(data.pubid).to.equal('9999'); + expect(data.fmv).to.equal(undefined); + + // slot 1 + expect(data.s[0].sn).to.equal('/19968336/header-bid-tag-0'); + expect(data.s[0].au).to.equal('/19968336/header-bid-tag-0'); + + // slot 2 + expect(data.s[1].sn).to.equal('/19968336/header-bid-tag-1'); + expect(data.s[1].au).to.equal('/19968336/header-bid-tag-1'); + expect(data.s[1].fskp).to.equal(undefined); + }); + + it('Logger: log floor fields when prebids floor shows setConfig in location property', function() { + const BID_REQUESTED_COPY = utils.deepClone(MOCK.BID_REQUESTED); + BID_REQUESTED_COPY['bids'][1]['floorData']['location'] = 'setConfig'; + + this.timeout(5000) + + sandbox.stub($$PREBID_GLOBAL$$, 'getHighestCpmBids').callsFake((key) => { + return [MOCK.BID_RESPONSE[0], MOCK.BID_RESPONSE[1]] + }); + + config.setConfig({ + testGroupId: 15 + }); + + events.emit(AUCTION_INIT, MOCK.AUCTION_INIT); + events.emit(BID_REQUESTED, BID_REQUESTED_COPY); + events.emit(BID_RESPONSE, MOCK.BID_RESPONSE[0]); + events.emit(BID_RESPONSE, MOCK.BID_RESPONSE[1]); + events.emit(BIDDER_DONE, MOCK.BIDDER_DONE); + events.emit(AUCTION_END, MOCK.AUCTION_END); + events.emit(SET_TARGETING, MOCK.SET_TARGETING); + events.emit(BID_WON, MOCK.BID_WON[0]); + events.emit(BID_WON, MOCK.BID_WON[1]); + + clock.tick(2000 + 1000); + expect(requests.length).to.equal(3); // 1 logger and 2 win-tracker + let request = requests[2]; // logger is executed late, trackers execute first + expect(request.url).to.equal('https://t.pubmatic.com/wl?pubid=9999'); + + let data = getLoggerJsonFromRequest(request.requestBody); + + expect(data.pubid).to.equal('9999'); + expect(data.fmv).to.equal('floorModelTest'); + + // slot 1 + expect(data.s[0].sn).to.equal('/19968336/header-bid-tag-0'); + expect(data.s[0].au).to.equal('/19968336/header-bid-tag-0'); + + // slot 2 + expect(data.s[1].sn).to.equal('/19968336/header-bid-tag-1'); + expect(data.s[1].au).to.equal('/19968336/header-bid-tag-1'); + expect(data.s[1].fskp).to.equal(0); + }); + it('bidCpmAdjustment: USD: Logger: best case + win tracker', function() { const bidCopy = utils.deepClone(BID); bidCopy.cpm = bidCopy.originalCpm * 2; // bidCpmAdjustment => bidCpm * 2 @@ -481,15 +786,20 @@ describe('pubmatic analytics adapter', function () { expect(data.pid).to.equal('1111'); expect(data.fmv).to.equal('floorModelTest'); expect(data.ft).to.equal(1); + expect(data.pbv).to.equal(getGlobal()?.version || '-1'); expect(data.s).to.be.an('array'); expect(data.s.length).to.equal(2); expect(data.tgid).to.equal(0); // 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].sid).not.to.be.undefined; + expect(data.s[0].ffs).to.equal(1); + expect(data.s[0].fsrc).to.equal(2); + expect(data.s[0].fp).to.equal('pubmatic'); expect(data.s[0].sz).to.deep.equal(['640x480']); expect(data.s[0].ps).to.be.an('array'); - expect(data.s[0].au).to.equal('/19968336/header-bid-tag-0'); + expect(data.s[0].au).to.equal('/19968336/header-bid-tag-0'); expect(data.s[0].ps.length).to.equal(1); expect(data.s[0].ps[0].pn).to.equal('pubmatic'); expect(data.s[0].ps[0].bc).to.equal('pubmatic'); @@ -501,7 +811,7 @@ describe('pubmatic analytics adapter', function () { expect(data.s[0].ps[0].af).to.equal('video'); expect(data.s[0].ps[0].ocpm).to.equal(1.23); expect(data.s[0].ps[0].ocry).to.equal('USD'); - expect(data.s[1].ps[0].frv).to.equal(undefined); + expect(data.s[1].ps[0].frv).to.equal(1.1); // tracker slot1 let firstTracker = requests[0].url; expect(firstTracker.split('?')[0]).to.equal('https://t.pubmatic.com/wt'); @@ -555,6 +865,7 @@ describe('pubmatic analytics adapter', function () { expect(data.tgid).to.equal(0);// test group id should be between 0-15 else set to 0 expect(data.fmv).to.equal('floorModelTest'); expect(data.ft).to.equal(1); + expect(data.pbv).to.equal(getGlobal()?.version || '-1'); expect(data.s).to.be.an('array'); expect(data.s.length).to.equal(2); // slot 1 @@ -562,7 +873,7 @@ describe('pubmatic analytics adapter', function () { 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); - expect(data.s[0].au).to.equal('/19968336/header-bid-tag-0'); + expect(data.s[0].au).to.equal('/19968336/header-bid-tag-0'); expect(data.s[0].ps[0].pn).to.equal('pubmatic'); expect(data.s[0].ps[0].bc).to.equal('pubmatic'); expect(data.s[0].ps[0].bidid).to.equal('2ecff0db240757'); @@ -605,6 +916,13 @@ describe('pubmatic analytics adapter', function () { 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].sid).not.to.be.undefined; + + expect(data.s[1].ffs).to.equal(1); + expect(data.s[1].fsrc).to.equal(2); + expect(data.s[1].fp).to.equal('pubmatic'); + 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); @@ -617,7 +935,7 @@ describe('pubmatic analytics adapter', function () { expect(data.s[1].ps[0].psz).to.equal('0x0'); expect(data.s[1].ps[0].eg).to.equal(0); expect(data.s[1].ps[0].en).to.equal(0); - expect(data.s[1].ps[0].di).to.equal(''); + expect(data.s[1].ps[0].di).to.equal('-1'); expect(data.s[1].ps[0].dc).to.equal(''); expect(data.s[1].ps[0].mi).to.equal(undefined); expect(data.s[1].ps[0].l1).to.equal(0); @@ -655,7 +973,7 @@ describe('pubmatic analytics adapter', function () { expect(data.s[1].ps[0].psz).to.equal('0x0'); expect(data.s[1].ps[0].eg).to.equal(0); expect(data.s[1].ps[0].en).to.equal(0); - expect(data.s[1].ps[0].di).to.equal(''); + expect(data.s[1].ps[0].di).to.equal('-1'); expect(data.s[1].ps[0].dc).to.equal(''); expect(data.s[1].ps[0].mi).to.equal(undefined); expect(data.s[1].ps[0].l1).to.equal(0); @@ -687,6 +1005,10 @@ describe('pubmatic analytics adapter', function () { 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].sid).not.to.be.undefined; + expect(data.s[1].ffs).to.equal(1); + expect(data.s[1].fsrc).to.equal(2); + expect(data.s[1].fp).to.equal('pubmatic'); 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); @@ -703,7 +1025,8 @@ describe('pubmatic analytics adapter', function () { expect(data.s[1].ps[0].dc).to.equal('PMP'); expect(data.s[1].ps[0].mi).to.equal('matched-impression'); expect(data.s[1].ps[0].adv).to.equal('example.com'); - expect(data.s[1].ps[0].l1).to.equal(3214); + expect(data.s[0].ps[0].l1).to.equal(0); + expect(data.s[0].ps[0].ol1).to.equal(0); expect(data.s[1].ps[0].l2).to.equal(0); expect(data.s[1].ps[0].ss).to.equal(1); expect(data.s[1].ps[0].t).to.equal(1); @@ -711,7 +1034,7 @@ describe('pubmatic analytics adapter', function () { expect(data.s[1].ps[0].af).to.equal('banner'); expect(data.s[1].ps[0].ocpm).to.equal(1.52); expect(data.s[1].ps[0].ocry).to.equal('USD'); - expect(data.s[1].ps[0].frv).to.equal(undefined); + expect(data.s[1].ps[0].frv).to.equal(1.1); }); it('Logger: currency conversion check', function() { @@ -748,6 +1071,7 @@ describe('pubmatic analytics adapter', function () { expect(data.s[1].sn).to.equal('/19968336/header-bid-tag-1'); expect(data.s[1].sz).to.deep.equal(['1000x300', '970x250', '728x90']); expect(data.s[1].ps).to.be.an('array'); + expect(data.s[1].sid).not.to.be.undefined; expect(data.s[1].ps.length).to.equal(1); expect(data.s[1].ps[0].pn).to.equal('pubmatic'); expect(data.s[1].ps[0].bc).to.equal('pubmatic'); @@ -762,7 +1086,8 @@ describe('pubmatic analytics adapter', function () { expect(data.s[1].ps[0].dc).to.equal('PMP'); expect(data.s[1].ps[0].mi).to.equal('matched-impression'); expect(data.s[1].ps[0].adv).to.equal('example.com'); - expect(data.s[1].ps[0].l1).to.equal(3214); + expect(data.s[0].ps[0].l1).to.equal(944); + expect(data.s[0].ps[0].ol1).to.equal(3214); expect(data.s[1].ps[0].l2).to.equal(0); expect(data.s[1].ps[0].ss).to.equal(1); expect(data.s[1].ps[0].t).to.equal(0); @@ -794,6 +1119,10 @@ describe('pubmatic analytics adapter', function () { 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].sid).not.to.be.undefined; + expect(data.s[1].ffs).to.equal(1); + expect(data.s[1].fsrc).to.equal(2); + expect(data.s[1].fp).to.equal('pubmatic'); 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); @@ -810,7 +1139,8 @@ describe('pubmatic analytics adapter', function () { expect(data.s[1].ps[0].dc).to.equal('PMP'); expect(data.s[1].ps[0].mi).to.equal('matched-impression'); expect(data.s[1].ps[0].adv).to.equal('example.com'); - expect(data.s[1].ps[0].l1).to.equal(3214); + expect(data.s[0].ps[0].l1).to.equal(944); + expect(data.s[0].ps[0].ol1).to.equal(3214); expect(data.s[1].ps[0].l2).to.equal(0); expect(data.s[1].ps[0].ss).to.equal(1); expect(data.s[1].ps[0].t).to.equal(0); @@ -818,7 +1148,7 @@ describe('pubmatic analytics adapter', function () { expect(data.s[1].ps[0].af).to.equal('banner'); expect(data.s[1].ps[0].ocpm).to.equal(1.52); expect(data.s[1].ps[0].ocry).to.equal('USD'); - expect(data.s[1].ps[0].frv).to.equal(undefined); + expect(data.s[1].ps[0].frv).to.equal(1.1); expect(data.dvc).to.deep.equal({'plt': 2}); // respective tracker slot let firstTracker = requests[1].url; @@ -852,6 +1182,7 @@ describe('pubmatic analytics adapter', function () { let data = getLoggerJsonFromRequest(request.requestBody); expect(data.s[1].sn).to.equal('/19968336/header-bid-tag-1'); expect(data.s[1].sz).to.deep.equal(['1000x300', '970x250', '728x90']); + expect(data.s[1].sid).not.to.be.undefined; expect(data.s[1].ps).to.be.an('array'); expect(data.s[1].ps.length).to.equal(1); expect(data.s[1].ps[0].pn).to.equal('pubmatic'); @@ -867,7 +1198,8 @@ describe('pubmatic analytics adapter', function () { expect(data.s[1].ps[0].dc).to.equal('PMP'); expect(data.s[1].ps[0].mi).to.equal('matched-impression'); expect(data.s[1].ps[0].adv).to.equal('example.com'); - expect(data.s[1].ps[0].l1).to.equal(3214); + expect(data.s[0].ps[0].l1).to.equal(944); + expect(data.s[0].ps[0].ol1).to.equal(3214); expect(data.s[1].ps[0].l2).to.equal(0); expect(data.s[1].ps[0].ss).to.equal(1); expect(data.s[1].ps[0].t).to.equal(0); @@ -876,7 +1208,7 @@ describe('pubmatic analytics adapter', function () { expect(data.s[1].ps[0].ocpm).to.equal(1.52); expect(data.s[1].ps[0].ocry).to.equal('USD'); expect(data.dvc).to.deep.equal({'plt': 1}); - expect(data.s[1].ps[0].frv).to.equal(undefined); + expect(data.s[1].ps[0].frv).to.equal(1.1); // respective tracker slot let firstTracker = requests[1].url; expect(firstTracker.split('?')[0]).to.equal('https://t.pubmatic.com/wt'); @@ -905,6 +1237,10 @@ describe('pubmatic analytics adapter', function () { 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].sid).not.to.be.undefined; + expect(data.s[1].ffs).to.equal(1); + expect(data.s[1].fsrc).to.equal(2); + expect(data.s[1].fp).to.equal('pubmatic'); 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); @@ -921,7 +1257,8 @@ describe('pubmatic analytics adapter', function () { expect(data.s[1].ps[0].dc).to.equal('PMP'); expect(data.s[1].ps[0].mi).to.equal('matched-impression'); expect(data.s[1].ps[0].adv).to.equal('example.com'); - expect(data.s[1].ps[0].l1).to.equal(3214); + expect(data.s[0].ps[0].l1).to.equal(944); + expect(data.s[0].ps[0].ol1).to.equal(3214); expect(data.s[1].ps[0].l2).to.equal(0); expect(data.s[1].ps[0].ss).to.equal(1); expect(data.s[1].ps[0].t).to.equal(0); @@ -929,7 +1266,7 @@ describe('pubmatic analytics adapter', function () { expect(data.s[1].ps[0].af).to.equal('banner'); expect(data.s[1].ps[0].ocpm).to.equal(1.52); expect(data.s[1].ps[0].ocry).to.equal('USD'); - expect(data.s[1].ps[0].frv).to.equal(undefined); + expect(data.s[1].ps[0].frv).to.equal(1.1); // respective tracker slot let firstTracker = requests[1].url; expect(firstTracker.split('?')[0]).to.equal('https://t.pubmatic.com/wt'); @@ -961,6 +1298,7 @@ describe('pubmatic analytics adapter', function () { let data = getLoggerJsonFromRequest(request.requestBody); expect(data.s[1].sn).to.equal('/19968336/header-bid-tag-1'); expect(data.s[1].sz).to.deep.equal(['1000x300', '970x250', '728x90']); + expect(data.s[1].sid).not.to.be.undefined; expect(data.s[1].ps).to.be.an('array'); expect(data.s[1].ps.length).to.equal(1); expect(data.s[1].ps[0].pn).to.equal('pubmatic'); @@ -976,7 +1314,8 @@ describe('pubmatic analytics adapter', function () { expect(data.s[1].ps[0].dc).to.equal('PMP'); expect(data.s[1].ps[0].mi).to.equal('matched-impression'); expect(data.s[1].ps[0].adv).to.equal('example.com'); - expect(data.s[1].ps[0].l1).to.equal(3214); + expect(data.s[0].ps[0].l1).to.equal(944); + expect(data.s[0].ps[0].ol1).to.equal(3214); expect(data.s[1].ps[0].l2).to.equal(0); expect(data.s[1].ps[0].ss).to.equal(1); expect(data.s[1].ps[0].t).to.equal(0); @@ -992,6 +1331,65 @@ describe('pubmatic analytics adapter', function () { expect(data.kgpv).to.equal('*'); }); + it('Logger: to handle floor rejected bids', function() { + this.timeout(5000) + + sandbox.stub($$PREBID_GLOBAL$$, 'getHighestCpmBids').callsFake((key) => { + return [MOCK.BID_RESPONSE[0], MOCK.BID_RESPONSE[1]] + }); + + events.emit(AUCTION_INIT, MOCK.AUCTION_INIT); + events.emit(BID_REQUESTED, MOCK.BID_REQUESTED); + events.emit(BID_RESPONSE, MOCK.BID_RESPONSE[0]); + events.emit(BID_REJECTED, MOCK.REJECTED_BID[0]); + events.emit(BIDDER_DONE, MOCK.BIDDER_DONE); + events.emit(AUCTION_END, MOCK.AUCTION_END); + events.emit(SET_TARGETING, MOCK.SET_TARGETING); + events.emit(BID_WON, MOCK.BID_WON[0]); + + clock.tick(2000 + 1000); + expect(requests.length).to.equal(2); // 1 logger and 1 win-tracker + let request = requests[1]; // logger is executed late, trackers execute first + expect(request.url).to.equal('https://t.pubmatic.com/wl?pubid=9999'); + let data = getLoggerJsonFromRequest(request.requestBody); + + // slot 2 + // Testing only for rejected bid as other scenarios will be covered under other TCs + expect(data.s[1].sn).to.equal('/19968336/header-bid-tag-1'); + expect(data.s[1].fskp).to.equal(0); + expect(data.s[1].ffs).to.equal(1); + expect(data.s[1].fsrc).to.equal(2); + expect(data.s[1].fp).to.equal('pubmatic'); + expect(data.s[1].sz).to.deep.equal(['1000x300', '970x250', '728x90']); + expect(data.s[1].sid).not.to.be.undefined; + expect(data.s[1].ps).to.be.an('array'); + expect(data.s[1].ps.length).to.equal(1); + expect(data.s[1].ps[0].pn).to.equal('pubmatic'); + expect(data.s[0].ps[0].bc).to.equal('pubmatic'); + expect(data.s[1].ps[0].bidid).to.equal('3bd4ebb1c900e2'); + expect(data.s[1].ps[0].piid).to.equal('partnerImpressionID-2'); + expect(data.s[1].ps[0].db).to.equal(0); + expect(data.s[1].ps[0].kgpv).to.equal('this-is-a-kgpv'); + expect(data.s[1].ps[0].kgpsv).to.equal('this-is-a-kgpv'); + expect(data.s[1].ps[0].psz).to.equal('728x90'); + expect(data.s[1].ps[0].eg).to.equal(1.52); + expect(data.s[1].ps[0].en).to.equal(0); // Net CPM is market as 0 due to bid rejection + expect(data.s[1].ps[0].di).to.equal('the-deal-id'); + expect(data.s[1].ps[0].dc).to.equal('PMP'); + expect(data.s[1].ps[0].mi).to.equal('matched-impression'); + expect(data.s[1].ps[0].adv).to.equal('example.com'); + expect(data.s[0].ps[0].l1).to.equal(944); + expect(data.s[0].ps[0].ol1).to.equal(3214); + expect(data.s[1].ps[0].l2).to.equal(0); + expect(data.s[1].ps[0].ss).to.equal(1); + expect(data.s[1].ps[0].t).to.equal(0); + expect(data.s[1].ps[0].wb).to.equal(1); + expect(data.s[1].ps[0].af).to.equal('banner'); + expect(data.s[1].ps[0].ocpm).to.equal(1.52); + expect(data.s[1].ps[0].ocry).to.equal('USD'); + expect(data.s[1].ps[0].frv).to.equal(1.1); + }); + it('Logger: best case + win tracker in case of Bidder Aliases', function() { MOCK.BID_REQUESTED['bids'][0]['bidder'] = 'pubmatic_alias'; MOCK.BID_REQUESTED['bids'][0]['bidderCode'] = 'pubmatic_alias'; @@ -1030,6 +1428,7 @@ describe('pubmatic analytics adapter', function () { expect(data.tst).to.equal(1519767016); expect(data.tgid).to.equal(15); expect(data.fmv).to.equal('floorModelTest'); + expect(data.pbv).to.equal(getGlobal()?.version || '-1'); expect(data.ft).to.equal(1); expect(data.s).to.be.an('array'); expect(data.s.length).to.equal(2); @@ -1037,9 +1436,13 @@ describe('pubmatic analytics adapter', function () { // 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].ffs).to.equal(1); + expect(data.s[0].fsrc).to.equal(2); + expect(data.s[0].fp).to.equal('pubmatic'); expect(data.s[0].sz).to.deep.equal(['640x480']); + expect(data.s[0].sid).not.to.be.undefined; expect(data.s[0].ps).to.be.an('array'); - expect(data.s[0].au).to.equal('/19968336/header-bid-tag-0'); + expect(data.s[0].au).to.equal('/19968336/header-bid-tag-0'); expect(data.s[0].ps.length).to.equal(1); expect(data.s[0].ps[0].pn).to.equal('pubmatic'); expect(data.s[0].ps[0].bc).to.equal('pubmatic_alias'); @@ -1051,9 +1454,10 @@ describe('pubmatic analytics adapter', function () { expect(data.s[0].ps[0].psz).to.equal('640x480'); expect(data.s[0].ps[0].eg).to.equal(1.23); expect(data.s[0].ps[0].en).to.equal(1.23); - expect(data.s[0].ps[0].di).to.equal(''); + expect(data.s[0].ps[0].di).to.equal('-1'); expect(data.s[0].ps[0].dc).to.equal(''); - expect(data.s[0].ps[0].l1).to.equal(3214); + expect(data.s[0].ps[0].l1).to.equal(944); + expect(data.s[0].ps[0].ol1).to.equal(3214); expect(data.s[0].ps[0].l2).to.equal(0); expect(data.s[0].ps[0].ss).to.equal(0); expect(data.s[0].ps[0].t).to.equal(0); @@ -1066,7 +1470,11 @@ describe('pubmatic analytics adapter', function () { // 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].ffs).to.equal(1); + expect(data.s[1].fsrc).to.equal(2); + expect(data.s[1].fp).to.equal('pubmatic'); expect(data.s[1].sz).to.deep.equal(['1000x300', '970x250', '728x90']); + expect(data.s[1].sid).not.to.be.undefined; expect(data.s[1].ps).to.be.an('array'); expect(data.s[1].ps.length).to.equal(1); expect(data.s[1].ps[0].pn).to.equal('pubmatic'); @@ -1083,7 +1491,8 @@ describe('pubmatic analytics adapter', function () { expect(data.s[1].ps[0].dc).to.equal('PMP'); expect(data.s[1].ps[0].mi).to.equal('matched-impression'); expect(data.s[1].ps[0].adv).to.equal('example.com'); - expect(data.s[1].ps[0].l1).to.equal(3214); + expect(data.s[0].ps[0].l1).to.equal(944); + expect(data.s[0].ps[0].ol1).to.equal(3214); expect(data.s[1].ps[0].l2).to.equal(0); expect(data.s[1].ps[0].ss).to.equal(1); expect(data.s[1].ps[0].t).to.equal(0); @@ -1091,7 +1500,7 @@ describe('pubmatic analytics adapter', function () { expect(data.s[1].ps[0].af).to.equal('banner'); expect(data.s[1].ps[0].ocpm).to.equal(1.52); expect(data.s[1].ps[0].ocry).to.equal('USD'); - expect(data.s[1].ps[0].frv).to.equal(undefined); + expect(data.s[1].ps[0].frv).to.equal(1.1); // tracker slot1 let firstTracker = requests[0].url; @@ -1149,6 +1558,7 @@ describe('pubmatic analytics adapter', function () { expect(data.tst).to.equal(1519767016); expect(data.tgid).to.equal(15); expect(data.fmv).to.equal('floorModelTest'); + expect(data.pbv).to.equal(getGlobal()?.version || '-1'); expect(data.ft).to.equal(1); expect(data.s).to.be.an('array'); expect(data.s.length).to.equal(2); @@ -1156,9 +1566,13 @@ describe('pubmatic analytics adapter', function () { // 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].ffs).to.equal(1); + expect(data.s[0].fsrc).to.equal(2); + expect(data.s[0].fp).to.equal('pubmatic'); expect(data.s[0].sz).to.deep.equal(['640x480']); + expect(data.s[0].sid).not.to.be.undefined; expect(data.s[0].ps).to.be.an('array'); - expect(data.s[0].au).to.equal('/19968336/header-bid-tag-0'); + expect(data.s[0].au).to.equal('/19968336/header-bid-tag-0'); expect(data.s[0].ps.length).to.equal(1); expect(data.s[0].ps[0].pn).to.equal('pubmatic'); expect(data.s[0].ps[0].bc).to.equal('groupm'); @@ -1170,9 +1584,10 @@ describe('pubmatic analytics adapter', function () { expect(data.s[0].ps[0].psz).to.equal('640x480'); expect(data.s[0].ps[0].eg).to.equal(1.23); expect(data.s[0].ps[0].en).to.equal(1.23); - expect(data.s[0].ps[0].di).to.equal(''); + expect(data.s[0].ps[0].di).to.equal('-1'); expect(data.s[0].ps[0].dc).to.equal(''); - expect(data.s[0].ps[0].l1).to.equal(3214); + expect(data.s[0].ps[0].l1).to.equal(944); + expect(data.s[0].ps[0].ol1).to.equal(3214); expect(data.s[0].ps[0].l2).to.equal(0); expect(data.s[0].ps[0].ss).to.equal(0); expect(data.s[0].ps[0].t).to.equal(0); @@ -1185,6 +1600,7 @@ describe('pubmatic analytics adapter', function () { // slot 2 expect(data.s[1].sn).to.equal('/19968336/header-bid-tag-1'); expect(data.s[1].sz).to.deep.equal(['1000x300', '970x250', '728x90']); + expect(data.s[1].sid).not.to.be.undefined; expect(data.s[1].ps).to.be.an('array'); expect(data.s[1].ps.length).to.equal(1); expect(data.s[1].ps[0].pn).to.equal('pubmatic'); @@ -1201,7 +1617,8 @@ describe('pubmatic analytics adapter', function () { expect(data.s[1].ps[0].dc).to.equal('PMP'); expect(data.s[1].ps[0].mi).to.equal('matched-impression'); expect(data.s[1].ps[0].adv).to.equal('example.com'); - expect(data.s[1].ps[0].l1).to.equal(3214); + expect(data.s[0].ps[0].l1).to.equal(944); + expect(data.s[0].ps[0].ol1).to.equal(3214); expect(data.s[1].ps[0].l2).to.equal(0); expect(data.s[1].ps[0].ss).to.equal(1); expect(data.s[1].ps[0].t).to.equal(0); diff --git a/test/spec/modules/pubmaticBidAdapter_spec.js b/test/spec/modules/pubmaticBidAdapter_spec.js index 5d8d762e793..5d59ff99a89 100644 --- a/test/spec/modules/pubmaticBidAdapter_spec.js +++ b/test/spec/modules/pubmaticBidAdapter_spec.js @@ -82,6 +82,7 @@ describe('PubMatic adapter', function () { ortb2Imp: { ext: { tid: '92489f71-1bf2-49a0-adf9-000cea934729', + gpid: '/1111/homepage-leftnav' } }, schain: schainConfig @@ -103,6 +104,7 @@ describe('PubMatic adapter', function () { params: { publisherId: '5890', adSlot: 'Div1@0x0', // ad_id or tagid + wiid: 'new-unique-wiid', video: { mimes: ['video/mp4', 'video/x-flv'], skippable: true, @@ -153,6 +155,7 @@ describe('PubMatic adapter', function () { params: { publisherId: '5890', adSlot: 'Div1@640x480', // ad_id or tagid + wiid: '1234567890', video: { mimes: ['video/mp4', 'video/x-flv'], skippable: true, @@ -212,6 +215,7 @@ describe('PubMatic adapter', function () { params: { publisherId: '5670', adSlot: '/43743431/NativeAutomationPrebid@1x1', + wiid: 'new-unique-wiid' }, bidId: '2a5571261281d4', requestId: 'B68287E1-DC39-4B38-9790-FE4F179739D6', @@ -277,6 +281,7 @@ describe('PubMatic adapter', function () { params: { publisherId: '5670', adSlot: '/43743431/NativeAutomationPrebid@1x1', + wiid: 'new-unique-wiid' }, bidId: '2a5571261281d4', requestId: 'B68287E1-DC39-4B38-9790-FE4F179739D6', @@ -303,6 +308,7 @@ describe('PubMatic adapter', function () { params: { publisherId: '5670', adSlot: '/43743431/NativeAutomationPrebid@1x1', + wiid: 'new-unique-wiid' } }]; @@ -343,6 +349,7 @@ describe('PubMatic adapter', function () { params: { publisherId: '5670', adSlot: '/43743431/NativeAutomationPrebid@1x1', + wiid: 'new-unique-wiid' } }]; @@ -501,6 +508,7 @@ describe('PubMatic adapter', function () { params: { publisherId: '301', adSlot: '/15671365/DMDemo@300x250:0', + wiid: 'new-unique-wiid', video: { mimes: ['video/mp4', 'video/x-flv'], skippable: true, @@ -571,6 +579,7 @@ describe('PubMatic adapter', function () { params: { publisherId: '301', adSlot: '/15671365/DMDemo@300x250:0', + wiid: 'new-unique-wiid', video: { mimes: ['video/mp4', 'video/x-flv'], skippable: true, @@ -1133,7 +1142,19 @@ describe('PubMatic adapter', function () { ortb2: { source: { tid: 'source-tid' - } + }, + device: { + geo: { + lat: '36.5189', + lon: '-76.4063' + } + }, + user: { + geo: { + lat: '26.8915', + lon: '-56.6340' + } + }, } }); let data = JSON.parse(request.data); @@ -1144,10 +1165,10 @@ describe('PubMatic adapter', function () { expect(data.site.publisher.id).to.equal(bidRequests[0].params.publisherId); // publisher Id expect(data.user.yob).to.equal(parseInt(bidRequests[0].params.yob)); // YOB expect(data.user.gender).to.equal(bidRequests[0].params.gender); // Gender - expect(data.device.geo.lat).to.not.equal(parseFloat(bidRequests[0].params.lat)); // Latitude - expect(data.device.geo.lon).to.not.equal(parseFloat(bidRequests[0].params.lon)); // Lognitude - expect(data.user.geo.lat).to.not.equal(parseFloat(bidRequests[0].params.lat)); // Latitude - expect(data.user.geo.lon).to.not.equal(parseFloat(bidRequests[0].params.lon)); // Lognitude + expect(data.device.geo.lat).to.equal('36.5189'); // Latitude + expect(data.device.geo.lon).to.equal('-76.4063'); // Lognitude + expect(data.user.geo.lat).to.equal('26.8915'); // Latitude + expect(data.user.geo.lon).to.equal('-56.6340'); // Lognitude expect(data.ext.wrapper.wv).to.equal($$REPO_AND_VERSION$$); // Wrapper Version expect(data.ext.wrapper.transactionId).to.equal(bidRequests[0].ortb2Imp.ext.tid); // Prebid TransactionId expect(data.source.tid).to.equal('source-tid'); // Prebid TransactionId @@ -1160,6 +1181,7 @@ describe('PubMatic adapter', function () { expect(data.imp[0].tagid).to.equal('/15671365/DMDemo'); // tagid expect(data.imp[0].banner.w).to.equal(300); // width expect(data.imp[0].banner.h).to.equal(250); // height + expect(data.imp[0].ext.gpid).to.equal(bidRequests[0].ortb2Imp.ext.gpid); expect(data.imp[0].ext.pmZoneId).to.equal(bidRequests[0].params.pmzoneid.split(',').slice(0, 50).map(id => id.trim()).join()); // pmzoneid expect(data.imp[0].ext.key_val).to.exist.and.to.equal(bidRequests[0].params.dctr); expect(data.imp[0].bidfloorcur).to.equal(bidRequests[0].params.currency); @@ -1387,7 +1409,21 @@ describe('PubMatic adapter', function () { it('Request params check: without adSlot', function () { delete bidRequests[0].params.adSlot; let request = spec.buildRequests(bidRequests, { - auctionId: 'new-auction-id' + auctionId: 'new-auction-id', + ortb2: { + device: { + geo: { + lat: '36.5189', + lon: '-76.4063' + } + }, + user: { + geo: { + lat: '26.8915', + lon: '-56.6340' + } + }, + } }); let data = JSON.parse(request.data); expect(data.at).to.equal(1); // auction type @@ -1397,10 +1433,10 @@ describe('PubMatic adapter', function () { expect(data.site.publisher.id).to.equal(bidRequests[0].params.publisherId); // publisher Id expect(data.user.yob).to.equal(parseInt(bidRequests[0].params.yob)); // YOB expect(data.user.gender).to.equal(bidRequests[0].params.gender); // Gender - expect(data.device.geo.lat).to.not.equal(parseFloat(bidRequests[0].params.lat)); // Latitude - expect(data.device.geo.lon).to.not.equal(parseFloat(bidRequests[0].params.lon)); // Lognitude - expect(data.user.geo.lat).to.not.equal(parseFloat(bidRequests[0].params.lat)); // Latitude - expect(data.user.geo.lon).to.not.equal(parseFloat(bidRequests[0].params.lon)); // Lognitude + expect(data.device.geo.lat).to.equal('36.5189'); // Latitude + expect(data.device.geo.lon).to.equal('-76.4063'); // Lognitude + expect(data.user.geo.lat).to.equal('26.8915'); // Latitude + expect(data.user.geo.lon).to.equal('-56.6340'); // Lognitude expect(data.ext.wrapper.wv).to.equal($$REPO_AND_VERSION$$); // Wrapper Version expect(data.ext.wrapper.transactionId).to.equal(bidRequests[0].ortb2Imp.ext.tid); // Prebid TransactionId expect(data.ext.wrapper.wiid).to.equal(bidRequests[0].params.wiid); // OpenWrap: Wrapper Impression ID @@ -1413,6 +1449,7 @@ describe('PubMatic adapter', function () { expect(data.imp[0].banner.w).to.equal(728); // width expect(data.imp[0].banner.h).to.equal(90); // height expect(data.imp[0].banner.format).to.deep.equal([{w: 160, h: 600}]); + expect(data.imp[0].ext.gpid).to.equal(bidRequests[0].ortb2Imp.ext.gpid); expect(data.imp[0].ext.key_val).to.exist.and.to.equal(bidRequests[0].params.dctr); expect(data.imp[0].ext.pmZoneId).to.equal(bidRequests[0].params.pmzoneid.split(',').slice(0, 50).map(id => id.trim()).join()); // pmzoneid expect(data.imp[0].bidfloorcur).to.equal(bidRequests[0].params.currency); @@ -1596,7 +1633,21 @@ describe('PubMatic adapter', function () { it('Pass auctiondId as wiid if wiid is not passed in params', function () { let bidRequest = { - auctionId: 'new-auction-id' + auctionId: 'new-auction-id', + ortb2: { + device: { + geo: { + lat: '36.5189', + lon: '-76.4063' + } + }, + user: { + geo: { + lat: '26.8915', + lon: '-56.6340' + } + }, + } }; delete bidRequests[0].params.wiid; let request = spec.buildRequests(bidRequests, bidRequest); @@ -1608,10 +1659,10 @@ describe('PubMatic adapter', function () { expect(data.site.publisher.id).to.equal(bidRequests[0].params.publisherId); // publisher Id expect(data.user.yob).to.equal(parseInt(bidRequests[0].params.yob)); // YOB expect(data.user.gender).to.equal(bidRequests[0].params.gender); // Gender - expect(data.device.geo.lat).to.not.equal(parseFloat(bidRequests[0].params.lat)); // Latitude - expect(data.device.geo.lon).to.not.equal(parseFloat(bidRequests[0].params.lon)); // Longitude - expect(data.user.geo.lat).to.not.equal(parseFloat(bidRequests[0].params.lat)); // Latitude - expect(data.user.geo.lon).to.not.equal(parseFloat(bidRequests[0].params.lon)); // Longitude + expect(data.device.geo.lat).to.equal('36.5189'); // Latitude + expect(data.device.geo.lon).to.equal('-76.4063'); // Lognitude + expect(data.user.geo.lat).to.equal('26.8915'); // Latitude + expect(data.user.geo.lon).to.equal('-56.6340'); // Lognitude expect(data.ext.wrapper.wv).to.equal($$REPO_AND_VERSION$$); // Wrapper Version expect(data.ext.wrapper.transactionId).to.equal(bidRequests[0].ortb2Imp.ext.tid); // Prebid TransactionId expect(data.ext.wrapper.wiid).to.equal('new-auction-id'); // OpenWrap: Wrapper Impression ID @@ -1623,6 +1674,7 @@ describe('PubMatic adapter', function () { expect(data.imp[0].tagid).to.equal('/15671365/DMDemo'); // tagid expect(data.imp[0].banner.w).to.equal(300); // width expect(data.imp[0].banner.h).to.equal(250); // height + expect(data.imp[0].ext.gpid).to.equal(bidRequests[0].ortb2Imp.ext.gpid); expect(data.imp[0].ext.pmZoneId).to.equal(bidRequests[0].params.pmzoneid.split(',').slice(0, 50).map(id => id.trim()).join()); // pmzoneid }); @@ -1631,6 +1683,20 @@ describe('PubMatic adapter', function () { gdprConsent: { consentString: 'kjfdniwjnifwenrif3', gdprApplies: true + }, + ortb2: { + device: { + geo: { + lat: '36.5189', + lon: '-76.4063' + } + }, + user: { + geo: { + lat: '26.8915', + lon: '-56.6340' + } + }, } }; let request = spec.buildRequests(bidRequests, bidRequest); @@ -1644,10 +1710,10 @@ describe('PubMatic adapter', function () { expect(data.site.publisher.id).to.equal(bidRequests[0].params.publisherId); // publisher Id expect(data.user.yob).to.equal(parseInt(bidRequests[0].params.yob)); // YOB expect(data.user.gender).to.equal(bidRequests[0].params.gender); // Gender - expect(data.device.geo.lat).to.not.equal(parseFloat(bidRequests[0].params.lat)); // Latitude - expect(data.device.geo.lon).to.not.equal(parseFloat(bidRequests[0].params.lon)); // Lognitude - expect(data.user.geo.lat).to.not.equal(parseFloat(bidRequests[0].params.lat)); // Latitude - expect(data.user.geo.lon).to.not.equal(parseFloat(bidRequests[0].params.lon)); // Lognitude + expect(data.device.geo.lat).to.equal('36.5189'); // Latitude + expect(data.device.geo.lon).to.equal('-76.4063'); // Lognitude + expect(data.user.geo.lat).to.equal('26.8915'); // Latitude + expect(data.user.geo.lon).to.equal('-56.6340'); // Lognitude expect(data.ext.wrapper.wv).to.equal($$REPO_AND_VERSION$$); // Wrapper Version expect(data.ext.wrapper.transactionId).to.equal(bidRequests[0].ortb2Imp.ext.tid); // Prebid TransactionId expect(data.ext.wrapper.wiid).to.equal(bidRequests[0].params.wiid); // OpenWrap: Wrapper Impression ID @@ -1657,6 +1723,7 @@ describe('PubMatic adapter', function () { expect(data.imp[0].id).to.equal(bidRequests[0].bidId); // Prebid bid id is passed as id expect(data.imp[0].bidfloor).to.equal(parseFloat(bidRequests[0].params.kadfloor)); // kadfloor expect(data.imp[0].tagid).to.equal('/15671365/DMDemo'); // tagid + expect(data.imp[0].ext.gpid).to.equal(bidRequests[0].ortb2Imp.ext.gpid); expect(data.imp[0].banner.w).to.equal(300); // width expect(data.imp[0].banner.h).to.equal(250); // height expect(data.imp[0].ext.pmZoneId).to.equal(bidRequests[0].params.pmzoneid.split(',').slice(0, 50).map(id => id.trim()).join()); // pmzoneid @@ -1664,7 +1731,21 @@ describe('PubMatic adapter', function () { it('Request params check with USP/CCPA Consent', function () { let bidRequest = { - uspConsent: '1NYN' + uspConsent: '1NYN', + ortb2: { + device: { + geo: { + lat: '36.5189', + lon: '-76.4063' + } + }, + user: { + geo: { + lat: '26.8915', + lon: '-56.6340' + } + }, + } }; let request = spec.buildRequests(bidRequests, bidRequest); let data = JSON.parse(request.data); @@ -1676,10 +1757,10 @@ describe('PubMatic adapter', function () { expect(data.site.publisher.id).to.equal(bidRequests[0].params.publisherId); // publisher Id expect(data.user.yob).to.equal(parseInt(bidRequests[0].params.yob)); // YOB expect(data.user.gender).to.equal(bidRequests[0].params.gender); // Gender - expect(data.device.geo.lat).to.not.equal(parseFloat(bidRequests[0].params.lat)); // Latitude - expect(data.device.geo.lon).to.not.equal(parseFloat(bidRequests[0].params.lon)); // Lognitude - expect(data.user.geo.lat).to.not.equal(parseFloat(bidRequests[0].params.lat)); // Latitude - expect(data.user.geo.lon).to.not.equal(parseFloat(bidRequests[0].params.lon)); // Lognitude + expect(data.device.geo.lat).to.equal('36.5189'); // Latitude + expect(data.device.geo.lon).to.equal('-76.4063'); // Lognitude + expect(data.user.geo.lat).to.equal('26.8915'); // Latitude + expect(data.user.geo.lon).to.equal('-56.6340'); // Lognitude expect(data.ext.wrapper.wv).to.equal($$REPO_AND_VERSION$$); // Wrapper Version expect(data.ext.wrapper.transactionId).to.equal(bidRequests[0].ortb2Imp.ext.tid); // Prebid TransactionId expect(data.ext.wrapper.wiid).to.equal(bidRequests[0].params.wiid); // OpenWrap: Wrapper Impression ID @@ -1691,6 +1772,7 @@ describe('PubMatic adapter', function () { expect(data.imp[0].tagid).to.equal('/15671365/DMDemo'); // tagid expect(data.imp[0].banner.w).to.equal(300); // width expect(data.imp[0].banner.h).to.equal(250); // height + expect(data.imp[0].ext.gpid).to.equal(bidRequests[0].ortb2Imp.ext.gpid); expect(data.imp[0].ext.pmZoneId).to.equal(bidRequests[0].params.pmzoneid.split(',').slice(0, 50).map(id => id.trim()).join()); // pmzoneid // second request without USP/CCPA @@ -1840,7 +1922,43 @@ describe('PubMatic adapter', function () { expect(data.user.yob).to.equal(1985); }); + it('ortb2.badv should be merged in the request', function() { + const ortb2 = { + badv: ['example.com'] + }; + const request = spec.buildRequests(bidRequests, {ortb2}); + let data = JSON.parse(request.data); + expect(data.badv).to.deep.equal(['example.com']); + }); + describe('ortb2Imp', function() { + describe('ortb2Imp.ext.gpid', function() { + beforeEach(function () { + if (bidRequests[0].hasOwnProperty('ortb2Imp')) { + delete bidRequests[0].ortb2Imp; + } + }); + + it('should send gpid if imp[].ext.gpid is specified', function() { + bidRequests[0].ortb2Imp = { + ext: { + gpid: 'ortb2Imp.ext.gpid' + } + }; + const request = spec.buildRequests(bidRequests, {}); + let data = JSON.parse(request.data); + expect(data.imp[0].ext).to.have.property('gpid'); + expect(data.imp[0].ext.gpid).to.equal('ortb2Imp.ext.gpid'); + }); + + it('should not send if imp[].ext.gpid is not specified', function() { + bidRequests[0].ortb2Imp = { ext: { } }; + const request = spec.buildRequests(bidRequests, {}); + let data = JSON.parse(request.data); + expect(data.imp[0].ext).to.not.have.property('gpid'); + }); + }); + describe('ortb2Imp.ext.data.pbadslot', function() { beforeEach(function () { if (bidRequests[0].hasOwnProperty('ortb2Imp')) { @@ -2160,7 +2278,7 @@ describe('PubMatic adapter', function () { sandbox.restore(); }); - describe('AdsrvrOrgId from userId module', function() { + describe('userIdAsEids', function() { let sandbox; beforeEach(() => { sandbox = sinon.sandbox.create(); @@ -2170,41 +2288,10 @@ describe('PubMatic adapter', function () { sandbox.restore(); }); - it('Request should have AdsrvrOrgId config params', function() { - bidRequests[0].userId = {}; - bidRequests[0].userId.tdid = 'TTD_ID_FROM_USER_ID_MODULE'; - bidRequests[0].userIdAsEids = createEidsArray(bidRequests[0].userId); - let request = spec.buildRequests(bidRequests, {}); - let data = JSON.parse(request.data); - expect(data.user.eids).to.deep.equal([{ - 'source': 'adserver.org', - 'uids': [{ - 'id': 'TTD_ID_FROM_USER_ID_MODULE', - 'atype': 1, - 'ext': { - 'rtiPartner': 'TDID' - } - }] - }]); - }); - - it('Request should have adsrvrOrgId from UserId Module if config and userId module both have TTD ID', function() { - sandbox.stub(config, 'getConfig').callsFake((key) => { - var config = { - adsrvrOrgId: { - 'TDID': 'TTD_ID_FROM_CONFIG', - 'TDID_LOOKUP': 'TRUE', - 'TDID_CREATED_AT': '2018-10-01T07:05:40' - } - }; - return config[key]; - }); + it('Request should have EIDs', function() { bidRequests[0].userId = {}; bidRequests[0].userId.tdid = 'TTD_ID_FROM_USER_ID_MODULE'; - bidRequests[0].userIdAsEids = createEidsArray(bidRequests[0].userId); - let request = spec.buildRequests(bidRequests, {}); - let data = JSON.parse(request.data); - expect(data.user.eids).to.deep.equal([{ + bidRequests[0].userIdAsEids = [{ 'source': 'adserver.org', 'uids': [{ 'id': 'TTD_ID_FROM_USER_ID_MODULE', @@ -2213,355 +2300,19 @@ describe('PubMatic adapter', function () { 'rtiPartner': 'TDID' } }] - }]); - }); - - it('Request should NOT have adsrvrOrgId params if userId is NOT object', function() { + }]; let request = spec.buildRequests(bidRequests, {}); let data = JSON.parse(request.data); - expect(data.user.eids).to.deep.equal(undefined); + expect(data.user.eids).to.deep.equal(bidRequests[0].userIdAsEids); }); - it('Request should NOT have adsrvrOrgId params if userId.tdid is NOT string', function() { - bidRequests[0].userId = { - tdid: 1234 - }; + it('Request should NOT have EIDs userIdAsEids is NOT object', function() { let request = spec.buildRequests(bidRequests, {}); let data = JSON.parse(request.data); expect(data.user.eids).to.deep.equal(undefined); }); }); - describe('UserIds from request', function() { - describe('pubcommon Id', function() { - it('send the pubcommon id if it is present', function() { - bidRequests[0].userId = {}; - bidRequests[0].userId.pubcid = 'pub_common_user_id'; - bidRequests[0].userIdAsEids = createEidsArray(bidRequests[0].userId); - let request = spec.buildRequests(bidRequests, {}); - let data = JSON.parse(request.data); - expect(data.user.eids).to.deep.equal([{ - 'source': 'pubcid.org', - 'uids': [{ - 'id': 'pub_common_user_id', - 'atype': 1 - }] - }]); - }); - - it('do not pass if not string', function() { - bidRequests[0].userId = {}; - bidRequests[0].userId.pubcid = 1; - bidRequests[0].userIdAsEids = createEidsArray(bidRequests[0].userId); - let request = spec.buildRequests(bidRequests, {}); - let data = JSON.parse(request.data); - expect(data.user.eids).to.equal(undefined); - bidRequests[0].userId.pubcid = []; - bidRequests[0].userIdAsEids = createEidsArray(bidRequests[0].userId); - request = spec.buildRequests(bidRequests, {}); - data = JSON.parse(request.data); - expect(data.user.eids).to.equal(undefined); - bidRequests[0].userId.pubcid = null; - bidRequests[0].userIdAsEids = createEidsArray(bidRequests[0].userId); - request = spec.buildRequests(bidRequests, {}); - data = JSON.parse(request.data); - expect(data.user.eids).to.equal(undefined); - bidRequests[0].userId.pubcid = {}; - bidRequests[0].userIdAsEids = createEidsArray(bidRequests[0].userId); - request = spec.buildRequests(bidRequests, {}); - data = JSON.parse(request.data); - expect(data.user.eids).to.equal(undefined); - }); - }); - - describe('ID5 Id', function() { - it('send the id5 id if it is present', function() { - bidRequests[0].userId = {}; - bidRequests[0].userId.id5id = { uid: 'id5-user-id' }; - bidRequests[0].userIdAsEids = createEidsArray(bidRequests[0].userId); - let request = spec.buildRequests(bidRequests, {}); - let data = JSON.parse(request.data); - expect(data.user.eids).to.deep.equal([{ - 'source': 'id5-sync.com', - 'uids': [{ - 'id': 'id5-user-id', - 'atype': 1 - }] - }]); - }); - - it('do not pass if not string', function() { - bidRequests[0].userId = {}; - bidRequests[0].userId.id5id = { uid: 1 }; - bidRequests[0].userIdAsEids = createEidsArray(bidRequests[0].userId); - let request = spec.buildRequests(bidRequests, {}); - let data = JSON.parse(request.data); - expect(data.user.eids).to.equal(undefined); - bidRequests[0].userId.id5id = { uid: [] }; - bidRequests[0].userIdAsEids = createEidsArray(bidRequests[0].userId); - request = spec.buildRequests(bidRequests, {}); - data = JSON.parse(request.data); - expect(data.user.eids).to.equal(undefined); - bidRequests[0].userId.id5id = { uid: null }; - bidRequests[0].userIdAsEids = createEidsArray(bidRequests[0].userId); - request = spec.buildRequests(bidRequests, {}); - data = JSON.parse(request.data); - expect(data.user.eids).to.equal(undefined); - bidRequests[0].userId.id5id = { uid: {} }; - bidRequests[0].userIdAsEids = createEidsArray(bidRequests[0].userId); - request = spec.buildRequests(bidRequests, {}); - data = JSON.parse(request.data); - expect(data.user.eids).to.equal(undefined); - }); - }); - - describe('Criteo Id', function() { - it('send the criteo id if it is present', function() { - bidRequests[0].userId = {}; - bidRequests[0].userId.criteoId = 'criteo-user-id'; - bidRequests[0].userIdAsEids = createEidsArray(bidRequests[0].userId); - let request = spec.buildRequests(bidRequests, {}); - let data = JSON.parse(request.data); - expect(data.user.eids).to.deep.equal([{ - 'source': 'criteo.com', - 'uids': [{ - 'id': 'criteo-user-id', - 'atype': 1 - }] - }]); - }); - - it('do not pass if not string', function() { - bidRequests[0].userId = {}; - bidRequests[0].userId.criteoId = 1; - bidRequests[0].userIdAsEids = createEidsArray(bidRequests[0].userId); - let request = spec.buildRequests(bidRequests, {}); - let data = JSON.parse(request.data); - expect(data.user.eids).to.equal(undefined); - bidRequests[0].userId.criteoId = []; - bidRequests[0].userIdAsEids = createEidsArray(bidRequests[0].userId); - request = spec.buildRequests(bidRequests, {}); - data = JSON.parse(request.data); - expect(data.user.eids).to.equal(undefined); - bidRequests[0].userId.criteoId = null; - bidRequests[0].userIdAsEids = createEidsArray(bidRequests[0].userId); - request = spec.buildRequests(bidRequests, {}); - data = JSON.parse(request.data); - expect(data.user.eids).to.equal(undefined); - bidRequests[0].userId.criteoId = {}; - bidRequests[0].userIdAsEids = createEidsArray(bidRequests[0].userId); - request = spec.buildRequests(bidRequests, {}); - data = JSON.parse(request.data); - expect(data.user.eids).to.equal(undefined); - }); - }); - - describe('IdentityLink Id', function() { - it('send the identity-link id if it is present', function() { - bidRequests[0].userId = {}; - bidRequests[0].userId.idl_env = 'identity-link-user-id'; - bidRequests[0].userIdAsEids = createEidsArray(bidRequests[0].userId); - let request = spec.buildRequests(bidRequests, {}); - let data = JSON.parse(request.data); - expect(data.user.eids).to.deep.equal([{ - 'source': 'liveramp.com', - 'uids': [{ - 'id': 'identity-link-user-id', - 'atype': 3 - }] - }]); - }); - - it('do not pass if not string', function() { - bidRequests[0].userId = {}; - bidRequests[0].userId.idl_env = 1; - bidRequests[0].userIdAsEids = createEidsArray(bidRequests[0].userId); - let request = spec.buildRequests(bidRequests, {}); - let data = JSON.parse(request.data); - expect(data.user.eids).to.equal(undefined); - bidRequests[0].userId.idl_env = []; - bidRequests[0].userIdAsEids = createEidsArray(bidRequests[0].userId); - request = spec.buildRequests(bidRequests, {}); - data = JSON.parse(request.data); - expect(data.user.eids).to.equal(undefined); - bidRequests[0].userId.idl_env = null; - bidRequests[0].userIdAsEids = createEidsArray(bidRequests[0].userId); - request = spec.buildRequests(bidRequests, {}); - data = JSON.parse(request.data); - expect(data.user.eids).to.equal(undefined); - bidRequests[0].userId.idl_env = {}; - bidRequests[0].userIdAsEids = createEidsArray(bidRequests[0].userId); - request = spec.buildRequests(bidRequests, {}); - data = JSON.parse(request.data); - expect(data.user.eids).to.equal(undefined); - }); - }); - - describe('LiveIntent Id', function() { - it('send the LiveIntent id if it is present', function() { - bidRequests[0].userId = {}; - bidRequests[0].userId.lipb = { lipbid: 'live-intent-user-id' }; - bidRequests[0].userIdAsEids = createEidsArray(bidRequests[0].userId); - let request = spec.buildRequests(bidRequests, {}); - let data = JSON.parse(request.data); - expect(data.user.eids).to.deep.equal([{ - 'source': 'liveintent.com', - 'uids': [{ - 'id': 'live-intent-user-id', - 'atype': 3 - }] - }]); - }); - - it('do not pass if not string', function() { - bidRequests[0].userId = {}; - bidRequests[0].userId.lipb = { lipbid: 1 }; - bidRequests[0].userIdAsEids = createEidsArray(bidRequests[0].userId); - let request = spec.buildRequests(bidRequests, {}); - let data = JSON.parse(request.data); - expect(data.user.eids).to.equal(undefined); - bidRequests[0].userId.lipb.lipbid = []; - bidRequests[0].userIdAsEids = createEidsArray(bidRequests[0].userId); - request = spec.buildRequests(bidRequests, {}); - data = JSON.parse(request.data); - expect(data.user.eids).to.equal(undefined); - bidRequests[0].userId.lipb.lipbid = null; - bidRequests[0].userIdAsEids = createEidsArray(bidRequests[0].userId); - request = spec.buildRequests(bidRequests, {}); - data = JSON.parse(request.data); - expect(data.user.eids).to.equal(undefined); - bidRequests[0].userId.lipb.lipbid = {}; - bidRequests[0].userIdAsEids = createEidsArray(bidRequests[0].userId); - request = spec.buildRequests(bidRequests, {}); - data = JSON.parse(request.data); - expect(data.user.eids).to.equal(undefined); - }); - }); - - describe('Parrable Id', function() { - it('send the Parrable id if it is present', function() { - bidRequests[0].userId = {}; - bidRequests[0].userId.parrableId = { eid: 'parrable-user-id' }; - bidRequests[0].userIdAsEids = createEidsArray(bidRequests[0].userId); - let request = spec.buildRequests(bidRequests, {}); - let data = JSON.parse(request.data); - expect(data.user.eids).to.deep.equal([{ - 'source': 'parrable.com', - 'uids': [{ - 'id': 'parrable-user-id', - 'atype': 1 - }] - }]); - }); - - it('do not pass if not object with eid key', function() { - bidRequests[0].userId = {}; - bidRequests[0].userId.parrableid = 1; - bidRequests[0].userIdAsEids = createEidsArray(bidRequests[0].userId); - let request = spec.buildRequests(bidRequests, {}); - let data = JSON.parse(request.data); - expect(data.user.eids).to.equal(undefined); - bidRequests[0].userId.parrableid = []; - bidRequests[0].userIdAsEids = createEidsArray(bidRequests[0].userId); - request = spec.buildRequests(bidRequests, {}); - data = JSON.parse(request.data); - expect(data.user.eids).to.equal(undefined); - bidRequests[0].userId.parrableid = null; - bidRequests[0].userIdAsEids = createEidsArray(bidRequests[0].userId); - request = spec.buildRequests(bidRequests, {}); - data = JSON.parse(request.data); - expect(data.user.eids).to.equal(undefined); - bidRequests[0].userId.parrableid = {}; - bidRequests[0].userIdAsEids = createEidsArray(bidRequests[0].userId); - request = spec.buildRequests(bidRequests, {}); - data = JSON.parse(request.data); - expect(data.user.eids).to.equal(undefined); - }); - }); - - describe('Britepool Id', function() { - it('send the Britepool id if it is present', function() { - bidRequests[0].userId = {}; - bidRequests[0].userId.britepoolid = 'britepool-user-id'; - bidRequests[0].userIdAsEids = createEidsArray(bidRequests[0].userId); - let request = spec.buildRequests(bidRequests, {}); - let data = JSON.parse(request.data); - expect(data.user.eids).to.deep.equal([{ - 'source': 'britepool.com', - 'uids': [{ - 'id': 'britepool-user-id', - 'atype': 3 - }] - }]); - }); - - it('do not pass if not string', function() { - bidRequests[0].userId = {}; - bidRequests[0].userId.britepoolid = 1; - bidRequests[0].userIdAsEids = createEidsArray(bidRequests[0].userId); - let request = spec.buildRequests(bidRequests, {}); - let data = JSON.parse(request.data); - expect(data.user.eids).to.equal(undefined); - bidRequests[0].userId.britepoolid = []; - bidRequests[0].userIdAsEids = createEidsArray(bidRequests[0].userId); - request = spec.buildRequests(bidRequests, {}); - data = JSON.parse(request.data); - expect(data.user.eids).to.equal(undefined); - bidRequests[0].userId.britepoolid = null; - bidRequests[0].userIdAsEids = createEidsArray(bidRequests[0].userId); - request = spec.buildRequests(bidRequests, {}); - data = JSON.parse(request.data); - expect(data.user.eids).to.equal(undefined); - bidRequests[0].userId.britepoolid = {}; - bidRequests[0].userIdAsEids = createEidsArray(bidRequests[0].userId); - request = spec.buildRequests(bidRequests, {}); - data = JSON.parse(request.data); - expect(data.user.eids).to.equal(undefined); - }); - }); - - describe('NetId', function() { - it('send the NetId if it is present', function() { - bidRequests[0].userId = {}; - bidRequests[0].userId.netId = 'netid-user-id'; - bidRequests[0].userIdAsEids = createEidsArray(bidRequests[0].userId); - let request = spec.buildRequests(bidRequests, {}); - let data = JSON.parse(request.data); - expect(data.user.eids).to.deep.equal([{ - 'source': 'netid.de', - 'uids': [{ - 'id': 'netid-user-id', - 'atype': 1 - }] - }]); - }); - - it('do not pass if not string', function() { - bidRequests[0].userId = {}; - bidRequests[0].userId.netId = 1; - bidRequests[0].userIdAsEids = createEidsArray(bidRequests[0].userId); - let request = spec.buildRequests(bidRequests, {}); - let data = JSON.parse(request.data); - expect(data.user.eids).to.equal(undefined); - bidRequests[0].userId.netId = []; - bidRequests[0].userIdAsEids = createEidsArray(bidRequests[0].userId); - request = spec.buildRequests(bidRequests, {}); - data = JSON.parse(request.data); - expect(data.user.eids).to.equal(undefined); - bidRequests[0].userId.netId = null; - bidRequests[0].userIdAsEids = createEidsArray(bidRequests[0].userId); - request = spec.buildRequests(bidRequests, {}); - data = JSON.parse(request.data); - expect(data.user.eids).to.equal(undefined); - bidRequests[0].userId.netId = {}; - bidRequests[0].userIdAsEids = createEidsArray(bidRequests[0].userId); - request = spec.buildRequests(bidRequests, {}); - data = JSON.parse(request.data); - expect(data.user.eids).to.equal(undefined); - }); - }); - }); - it('should pass device.sua if present in bidderRequest fpd ortb2 object', function () { const suaObject = {'source': 2, 'platform': {'brand': 'macOS', 'version': ['12', '4', '0']}, 'browsers': [{'brand': 'Not_A Brand', 'version': ['99', '0', '0', '0']}, {'brand': 'Google Chrome', 'version': ['109', '0', '5414', '119']}, {'brand': 'Chromium', 'version': ['109', '0', '5414', '119']}], 'mobile': 0, 'model': '', 'bitness': '64', 'architecture': 'x86'}; let request = spec.buildRequests(multipleMediaRequests, { @@ -2577,6 +2328,23 @@ describe('PubMatic adapter', function () { expect(data.device.sua).to.deep.equal(suaObject); }); + it('should pass device.ext.cdep if present in bidderRequest fpd ortb2 object', function () { + const cdepObj = { + cdep: 'example_label_1' + }; + let request = spec.buildRequests(multipleMediaRequests, { + auctionId: 'new-auction-id', + ortb2: { + device: { + ext: cdepObj + } + } + }); + let data = JSON.parse(request.data); + expect(data.device.ext.cdep).to.exist.and.to.be.an('string'); + expect(data.device.ext).to.deep.equal(cdepObj); + }); + it('Request params should have valid native bid request for all valid params', function () { let request = spec.buildRequests(nativeBidRequests, { auctionId: 'new-auction-id' @@ -2692,7 +2460,21 @@ describe('PubMatic adapter', function () { it('Request params check for 1 banner and 1 video ad', function () { let request = spec.buildRequests(multipleMediaRequests, { - auctionId: 'new-auction-id' + auctionId: 'new-auction-id', + ortb2: { + device: { + geo: { + lat: '36.5189', + lon: '-76.4063' + } + }, + user: { + geo: { + lat: '26.8915', + lon: '-56.6340' + } + }, + } }); let data = JSON.parse(request.data); @@ -2706,10 +2488,10 @@ describe('PubMatic adapter', function () { expect(data.site.publisher.id).to.equal(multipleMediaRequests[0].params.publisherId); // publisher Id expect(data.user.yob).to.equal(parseInt(multipleMediaRequests[0].params.yob)); // YOB expect(data.user.gender).to.equal(multipleMediaRequests[0].params.gender); // Gender - expect(data.device.geo.lat).to.not.equal(parseFloat(multipleMediaRequests[0].params.lat)); // Latitude - expect(data.device.geo.lon).to.not.equal(parseFloat(multipleMediaRequests[0].params.lon)); // Lognitude - expect(data.user.geo.lat).to.not.equal(parseFloat(multipleMediaRequests[0].params.lat)); // Latitude - expect(data.user.geo.lon).to.not.equal(parseFloat(multipleMediaRequests[0].params.lon)); // Lognitude + expect(data.device.geo.lat).to.equal('36.5189'); // Latitude + expect(data.device.geo.lon).to.equal('-76.4063'); // Lognitude + expect(data.user.geo.lat).to.equal('26.8915'); // Latitude + expect(data.user.geo.lon).to.equal('-56.6340'); // Lognitude expect(data.ext.wrapper.wv).to.equal($$REPO_AND_VERSION$$); // Wrapper Version expect(data.ext.wrapper.transactionId).to.equal(multipleMediaRequests[0].transactionId); // Prebid TransactionId expect(data.ext.wrapper.wiid).to.equal(multipleMediaRequests[0].params.wiid); // OpenWrap: Wrapper Impression ID @@ -3057,6 +2839,76 @@ describe('PubMatic adapter', function () { }); } + describe('GPP', function() { + it('Request params check with GPP Consent', function () { + let bidRequest = { + gppConsent: { + 'gppString': 'DBACNYA~CPXxRfAPXxRfAAfKABENB-CgAAAAAAAAAAYgAAAAAAAA~1YNN', + 'fullGppData': { + 'sectionId': 3, + 'gppVersion': 1, + 'sectionList': [ + 5, + 7 + ], + 'applicableSections': [ + 5 + ], + 'gppString': 'DBACNYA~CPXxRfAPXxRfAAfKABENB-CgAAAAAAAAAAYgAAAAAAAA~1YNN', + 'pingData': { + 'cmpStatus': 'loaded', + 'gppVersion': '1.0', + 'cmpDisplayStatus': 'visible', + 'supportedAPIs': [ + 'tcfca', + 'usnat', + 'usca', + 'usva', + 'usco', + 'usut', + 'usct' + ], + 'cmpId': 31 + }, + 'eventName': 'sectionChange' + }, + 'applicableSections': [ + 5 + ], + 'apiVersion': 1 + } + }; + let request = spec.buildRequests(bidRequests, bidRequest); + let data = JSON.parse(request.data); + expect(data.regs.gpp).to.equal('DBACNYA~CPXxRfAPXxRfAAfKABENB-CgAAAAAAAAAAYgAAAAAAAA~1YNN'); + expect(data.regs.gpp_sid[0]).to.equal(5); + }); + + it('Request params check without GPP Consent', function () { + let bidRequest = {}; + let request = spec.buildRequests(bidRequests, bidRequest); + let data = JSON.parse(request.data); + expect(data.regs).to.equal(undefined); + }); + + it('Request params check with GPP Consent read from ortb2', function () { + let bidRequest = { + ortb2: { + regs: { + 'gpp': 'DBACNYA~CPXxRfAPXxRfAAfKABENB-CgAAAAAAAAAAYgAAAAAAAA~1YNN', + 'gpp_sid': [ + 5 + ] + } + } + }; + let request = spec.buildRequests(bidRequests, bidRequest); + let data = JSON.parse(request.data); + expect(data.regs.gpp).to.equal('DBACNYA~CPXxRfAPXxRfAAfKABENB-CgAAAAAAAAAAYgAAAAAAAA~1YNN'); + expect(data.regs.gpp_sid[0]).to.equal(5); + }); + }); + describe('Fledge', function() { it('should not send imp.ext.ae when FLEDGE is disabled, ', function () { let bidRequest = Object.assign([], bidRequests); @@ -3816,6 +3668,89 @@ describe('PubMatic adapter', function () { } }); + describe('Fledge Auction config Response', function () { + let response; + let bidRequestConfigs = [ + { + bidder: 'pubmatic', + mediaTypes: { + banner: { + sizes: [[728, 90], [160, 600]] + } + }, + params: { + publisherId: '5670', + adSlot: '/15671365/DMDemo@300x250:0', + kadfloor: '1.2', + pmzoneid: 'aabc, ddef', + kadpageurl: 'www.publisher.com', + yob: '1986', + gender: 'M', + lat: '12.3', + lon: '23.7', + wiid: '1234567890', + profId: '100', + verId: '200', + currency: 'AUD', + dctr: 'key1:val1,val2|key2:val1' + }, + placementCode: '/19968336/header-bid-tag-1', + sizes: [[300, 250], [300, 600]], + bidId: 'test_bid_id', + requestId: '0fb4905b-9456-4152-86be-c6f6d259ba99', + bidderRequestId: '1c56ad30b9b8ca8', + ortb2Imp: { + ext: { + tid: '92489f71-1bf2-49a0-adf9-000cea934729', + ae: 1 + } + }, + } + ]; + + let bidRequest = spec.buildRequests(bidRequestConfigs, {}); + let bidResponse = { + seatbid: [{ + bid: [{ + impid: 'test_bid_id', + price: 2, + w: 728, + h: 250, + crid: 'test-creative-id', + dealid: 'test-deal-id', + adm: 'test-ad-markup' + }] + }], + cur: 'AUS', + ext: { + fledge_auction_configs: { + 'test_bid_id': { + seller: 'ads.pubmatic.com', + interestGroupBuyers: ['dsp1.com'], + sellerTimeout: 0, + perBuyerSignals: { + 'dsp1.com': { + bid_macros: 0.1, + disallowed_adv_ids: [ + '5678', + '5890' + ], + } + } + } + } + } + }; + + response = spec.interpretResponse({ body: bidResponse }, bidRequest); + it('should return FLEDGE auction_configs alongside bids', function () { + expect(response).to.have.property('bids'); + expect(response).to.have.property('fledgeAuctionConfigs'); + expect(response.fledgeAuctionConfigs.length).to.equal(1); + expect(response.fledgeAuctionConfigs[0].bidId).to.equal('test_bid_id'); + }); + }); + describe('Preapare metadata', function () { it('Should copy all fields from ext to meta', function () { const bid = { @@ -3991,6 +3926,55 @@ describe('PubMatic adapter', function () { type: 'image', url: `${syncurl_image}&gdpr=1&gdpr_consent=foo&us_privacy=1NYN&coppa=1` }]); }); + + describe('GPP', function() { + it('should return userSync url without Gpp consent if gppConsent is undefined', () => { + const result = spec.getUserSyncs({iframeEnabled: true}, undefined, undefined, undefined, undefined); + expect(result).to.deep.equal([{ + type: 'iframe', url: `${syncurl_iframe}` + }]); + }); + + it('should return userSync url without Gpp consent if gppConsent.gppString is undefined', () => { + const gppConsent = { applicableSections: ['5'] }; + const result = spec.getUserSyncs({iframeEnabled: true}, undefined, undefined, undefined, gppConsent); + expect(result).to.deep.equal([{ + type: 'iframe', url: `${syncurl_iframe}` + }]); + }); + + it('should return userSync url without Gpp consent if gppConsent.applicableSections is undefined', () => { + const gppConsent = { gppString: 'DBACNYA~CPXxRfAPXxRfAAfKABENB-CgAAAAAAAAAAYgAAAAAAAA~1YNN' }; + const result = spec.getUserSyncs({iframeEnabled: true}, undefined, undefined, undefined, gppConsent); + expect(result).to.deep.equal([{ + type: 'iframe', url: `${syncurl_iframe}` + }]); + }); + + it('should return userSync url without Gpp consent if gppConsent.applicableSections is an empty array', () => { + const gppConsent = { gppString: 'DBACNYA~CPXxRfAPXxRfAAfKABENB-CgAAAAAAAAAAYgAAAAAAAA~1YNN', applicableSections: [] }; + const result = spec.getUserSyncs({iframeEnabled: true}, undefined, undefined, undefined, gppConsent); + expect(result).to.deep.equal([{ + type: 'iframe', url: `${syncurl_iframe}` + }]); + }); + + it('should concatenate gppString and applicableSections values in the returned userSync iframe url', () => { + const gppConsent = { gppString: 'DBACNYA~CPXxRfAPXxRfAAfKABENB-CgAAAAAAAAAAYgAAAAAAAA~1YNN', applicableSections: [5] }; + const result = spec.getUserSyncs({iframeEnabled: true}, undefined, undefined, undefined, gppConsent); + expect(result).to.deep.equal([{ + type: 'iframe', url: `${syncurl_iframe}&gpp=${encodeURIComponent(gppConsent.gppString)}&gpp_sid=${encodeURIComponent(gppConsent.applicableSections)}` + }]); + }); + + it('should concatenate gppString and applicableSections values in the returned userSync image url', () => { + const gppConsent = { gppString: 'DBACNYA~CPXxRfAPXxRfAAfKABENB-CgAAAAAAAAAAYgAAAAAAAA~1YNN', applicableSections: [5] }; + const result = spec.getUserSyncs({iframeEnabled: false}, undefined, undefined, undefined, gppConsent); + expect(result).to.deep.equal([{ + type: 'image', url: `${syncurl_image}&gpp=${encodeURIComponent(gppConsent.gppString)}&gpp_sid=${encodeURIComponent(gppConsent.applicableSections)}` + }]); + }); + }); }); if (FEATURES.VIDEO) { diff --git a/test/spec/modules/pubwiseAnalyticsAdapter_spec.js b/test/spec/modules/pubwiseAnalyticsAdapter_spec.js index e14582edc39..92d5972cc13 100644 --- a/test/spec/modules/pubwiseAnalyticsAdapter_spec.js +++ b/test/spec/modules/pubwiseAnalyticsAdapter_spec.js @@ -1,6 +1,7 @@ import {expect} from 'chai'; import pubwiseAnalytics from 'modules/pubwiseAnalyticsAdapter.js'; import {expectEvents} from '../../helpers/analytics.js'; +import {server} from '../../mocks/xhr.js'; let events = require('src/events'); let adapterManager = require('src/adapterManager').default; @@ -9,7 +10,6 @@ let constants = require('src/constants.json'); describe('PubWise Prebid Analytics', function () { let requests; let sandbox; - let xhr; let clock; let mock = {}; @@ -38,9 +38,7 @@ describe('PubWise Prebid Analytics', function () { clock = sandbox.useFakeTimers(); sandbox.stub(events, 'getEvents').returns([]); - xhr = sandbox.useFakeXMLHttpRequest(); - requests = []; - xhr.onCreate = request => requests.push(request); + requests = server.requests; }); afterEach(function () { @@ -50,10 +48,6 @@ describe('PubWise Prebid Analytics', function () { }); describe('enableAnalytics', function () { - beforeEach(function () { - requests = []; - }); - it('should catch all events', function () { pubwiseAnalytics.enableAnalytics(mock.DEFAULT_PW_CONFIG); diff --git a/test/spec/modules/pubxaiAnalyticsAdapter_spec.js b/test/spec/modules/pubxaiAnalyticsAdapter_spec.js index 1dce87e4b8e..e0f4497a8c8 100644 --- a/test/spec/modules/pubxaiAnalyticsAdapter_spec.js +++ b/test/spec/modules/pubxaiAnalyticsAdapter_spec.js @@ -1,13 +1,9 @@ -import pubxaiAnalyticsAdapter from 'modules/pubxaiAnalyticsAdapter.js'; -import { getDeviceType, getBrowser, getOS } from 'modules/pubxaiAnalyticsAdapter.js'; -import { - expect -} from 'chai'; +import pubxaiAnalyticsAdapter, {getBrowser, getDeviceType, getOS} from 'modules/pubxaiAnalyticsAdapter.js'; +import {expect} from 'chai'; import adapterManager from 'src/adapterManager.js'; import * as utils from 'src/utils.js'; -import { - server -} from 'test/mocks/xhr.js'; +import {server} from 'test/mocks/xhr.js'; +import {getGptSlotInfoForAdUnitCode} from '../../../libraries/gptUtils/gptUtils.js'; let events = require('src/events'); let constants = require('src/constants.json'); @@ -527,7 +523,7 @@ describe('pubxai analytics adapter', function() { 'bidderCode': 'appnexus', 'bidId': '248f9a4489835e', 'adUnitCode': '/19968336/header-bid-tag-1', - 'gptSlotCode': utils.getGptSlotInfoForAdUnitCode('/19968336/header-bid-tag-1').gptSlot || null, + 'gptSlotCode': getGptSlotInfoForAdUnitCode('/19968336/header-bid-tag-1').gptSlot || null, 'auctionId': 'bc3806e4-873e-453c-8ae5-204f35e923b4', 'sizes': '300x250', 'renderStatus': 2, @@ -596,7 +592,7 @@ describe('pubxai analytics adapter', function() { let expectedAfterBidWon = { 'winningBid': { 'adUnitCode': '/19968336/header-bid-tag-1', - 'gptSlotCode': utils.getGptSlotInfoForAdUnitCode('/19968336/header-bid-tag-1').gptSlot || null, + 'gptSlotCode': getGptSlotInfoForAdUnitCode('/19968336/header-bid-tag-1').gptSlot || null, 'auctionId': 'bc3806e4-873e-453c-8ae5-204f35e923b4', 'bidderCode': 'appnexus', 'bidId': '248f9a4489835e', diff --git a/test/spec/modules/pulsepointBidAdapter_spec.js b/test/spec/modules/pulsepointBidAdapter_spec.js index 60dca9e6da0..8db7e909771 100644 --- a/test/spec/modules/pulsepointBidAdapter_spec.js +++ b/test/spec/modules/pulsepointBidAdapter_spec.js @@ -1,7 +1,8 @@ /* eslint dot-notation:0, quote-props:0 */ import {expect} from 'chai'; import {spec} from 'modules/pulsepointBidAdapter.js'; -import {deepClone} from 'src/utils.js'; +import {syncAddFPDToBidderRequest} from '../../helpers/fpd.js'; +import {deepClone} from '../../../src/utils'; describe('PulsePoint Adapter Tests', function () { const slotConfigs = [{ @@ -31,39 +32,52 @@ describe('PulsePoint Adapter Tests', function () { cf: '728x90' } }]; + const nativeOrtbRequest = { + assets: [{ + id: 1, + required: 1, + img: { + type: 3, + w: 150, + h: 50, + } + }, + { + id: 2, + required: 1, + title: { + len: 80 + } + }, + { + id: 3, + required: 0, + data: { + type: 1 + } + }] + }; const nativeSlotConfig = [{ placementCode: '/DfpAccount1/slot3', bidId: 'bid12345', - nativeParams: { - title: { required: true, len: 200 }, - image: { wmin: 100 }, - sponsoredBy: { } + mediaTypes: { + native: { + sendTargetingKeys: false, + ortb: nativeOrtbRequest + } }, + nativeOrtbRequest, params: { cp: 'p10000', ct: 't10000' } }]; - const appSlotConfig = [{ - placementCode: '/DfpAccount1/slot3', - bidId: 'bid12345', - params: { - cp: 'p10000', - ct: 't10000', - app: { - bundle: 'com.pulsepoint.apps', - storeUrl: 'https://pulsepoint.com/apps', - domain: 'pulsepoint.com', - } - } - }]; const videoSlotConfig = [{ placementCode: '/DfpAccount1/slotVideo', bidId: 'bid12345', - params: { - cp: 'p10000', - ct: 't10000', + mediaTypes: { video: { + playerSize: [400, 300], w: 400, h: 300, minduration: 5, @@ -73,6 +87,10 @@ describe('PulsePoint Adapter Tests', function () { minbitrate: 200, protocols: [1, 2, 4] } + }, + params: { + cp: 'p10000', + ct: 't10000' } }]; const additionalParamsConfig = [{ @@ -97,68 +115,6 @@ describe('PulsePoint Adapter Tests', function () { } }]; - const ortbParamsSlotConfig = [{ - placementCode: '/DfpAccount1/slot1', - mediaTypes: { - banner: { - sizes: [[1, 1]] - } - }, - bidId: 'bid12345', - params: { - cp: 'p10000', - ct: 't10000', - cf: '1x1', - bcat: ['IAB-1', 'IAB-20'], - battr: [1, 2, 3], - bidfloor: 1.5, - badv: ['cocacola.com', 'lays.com'] - } - }, { - placementCode: '/DfpAccount1/slotVideo', - bidId: 'bid12345', - params: { - cp: 'p10000', - ct: 't10000', - video: { - w: 400, - h: 300, - minduration: 5, - maxduration: 10, - }, - battr: [2, 3, 4], - bidfloor: 2.5, - } - }]; - - const outstreamSlotConfig = [{ - placementCode: '/DfpAccount1/slot1', - mediaTypes: { - video: { - playerSize: [640, 480], - context: 'outstream' - } - }, - bidId: 'bid12345', - params: { - cp: 'p10000', - ct: 't10000', - cf: '1x1', - video: { - h: 300, - w: 400, - minduration: 1, - maxduration: 210, - linearity: 1, - } - }, - renderer: { - options: { - text: 'PulsePoint Outstream' - } - } - }]; - const schainParamsSlotConfig = [{ placementCode: '/DfpAccount1/slot1', mediaTypes: { @@ -200,7 +156,7 @@ describe('PulsePoint Adapter Tests', function () { }; it('Verify build request', function () { - const request = spec.buildRequests(slotConfigs, bidderRequest); + const request = spec.buildRequests(slotConfigs, syncAddFPDToBidderRequest(bidderRequest)); expect(request.url).to.equal('https://bid.contextweb.com/header/ortb?src=prebid'); expect(request.method).to.equal('POST'); const ortbRequest = request.data; @@ -208,7 +164,6 @@ describe('PulsePoint Adapter Tests', function () { expect(ortbRequest.site).to.not.equal(null); expect(ortbRequest.site.publisher).to.not.equal(null); expect(ortbRequest.site.publisher.id).to.equal('p10000'); - expect(ortbRequest.site.ref).to.equal(bidderRequest.refererInfo.ref); expect(ortbRequest.site.page).to.equal('https://publisher.com/home'); expect(ortbRequest.imp).to.have.lengthOf(2); // device object @@ -217,19 +172,15 @@ describe('PulsePoint Adapter Tests', function () { // slot 1 expect(ortbRequest.imp[0].tagid).to.equal('t10000'); expect(ortbRequest.imp[0].banner).to.not.equal(null); - expect(ortbRequest.imp[0].banner.w).to.equal(300); - expect(ortbRequest.imp[0].banner.h).to.equal(250); + expect(ortbRequest.imp[0].banner.format).to.deep.eq([{'w': 728, 'h': 90}, {'w': 160, 'h': 600}]); // slot 2 expect(ortbRequest.imp[1].tagid).to.equal('t20000'); expect(ortbRequest.imp[1].banner).to.not.equal(null); - expect(ortbRequest.imp[1].banner.w).to.equal(728); - expect(ortbRequest.imp[1].banner.h).to.equal(90); - // tmax - expect(ortbRequest.tmax).to.equal(500); + expect(ortbRequest.imp[1].banner.format).to.deep.eq([{'w': 728, 'h': 90}]); }); it('Verify parse response', function () { - const request = spec.buildRequests(slotConfigs, bidderRequest); + const request = spec.buildRequests(slotConfigs, syncAddFPDToBidderRequest(bidderRequest)); const ortbRequest = request.data; const ortbResponse = { seatbid: [{ @@ -237,11 +188,16 @@ describe('PulsePoint Adapter Tests', function () { impid: ortbRequest.imp[0].id, price: 1.25, adm: 'This is an Ad', - crid: 'Creative#123' + crid: 'Creative#123', + mtype: 1, + w: 300, + h: 250, + exp: 20, + adomain: ['advertiser.com'] }] }] }; - const bids = spec.interpretResponse({ body: ortbResponse }, request); + const bids = spec.interpretResponse({body: ortbResponse}, request); expect(bids).to.have.lengthOf(1); // verify first bid const bid = bids[0]; @@ -249,140 +205,112 @@ describe('PulsePoint Adapter Tests', function () { expect(bid.ad).to.equal('This is an Ad'); expect(bid.width).to.equal(300); expect(bid.height).to.equal(250); - expect(bid.adId).to.equal('bid12345'); expect(bid.creative_id).to.equal('Creative#123'); expect(bid.creativeId).to.equal('Creative#123'); expect(bid.netRevenue).to.equal(true); expect(bid.currency).to.equal('USD'); expect(bid.ttl).to.equal(20); - }); - - it('Verify ttl/currency/adomain applied to bid', function () { - const request = spec.buildRequests(slotConfigs, bidderRequest); - const ortbRequest = request.data; - const ortbResponse = { - seatbid: [{ - bid: [{ - impid: ortbRequest.imp[0].id, - price: 1.25, - adm: 'This is an Ad#1', - crid: 'Creative#123', - exp: 50, - adomain: ['advertiser.com'] - }, { - impid: ortbRequest.imp[1].id, - price: 1.25, - adm: 'This is an Ad#2', - crid: 'Creative#123' - }] - }], - cur: 'GBP' - }; - const bids = spec.interpretResponse({ body: ortbResponse }, request); - expect(bids).to.have.lengthOf(2); - // verify first bid - const bid = bids[0]; - expect(bid.cpm).to.equal(1.25); - expect(bid.ad).to.equal('This is an Ad#1'); - expect(bid.ttl).to.equal(50); - expect(bid.currency).to.equal('GBP'); expect(bid.meta).to.not.be.null; expect(bid.meta.advertiserDomains).to.eql(['advertiser.com']); - const secondBid = bids[1]; - expect(secondBid.cpm).to.equal(1.25); - expect(secondBid.ad).to.equal('This is an Ad#2'); - expect(secondBid.ttl).to.equal(20); - expect(secondBid.currency).to.equal('GBP'); - expect(secondBid.meta).to.not.be.null; - expect(secondBid.meta.advertiserDomains).to.eql([]); }); it('Verify full passback', function () { const request = spec.buildRequests(slotConfigs, bidderRequest); - const bids = spec.interpretResponse({ body: null }, request) + const bids = spec.interpretResponse({body: null}, request) expect(bids).to.have.lengthOf(0); }); - it('Verify Native request', function () { - const request = spec.buildRequests(nativeSlotConfig, bidderRequest); - expect(request.url).to.equal('https://bid.contextweb.com/header/ortb?src=prebid'); - expect(request.method).to.equal('POST'); - const ortbRequest = request.data; - // native impression - expect(ortbRequest.imp[0].tagid).to.equal('t10000'); - expect(ortbRequest.imp[0].banner).to.equal(null); - const nativePart = ortbRequest.imp[0]['native']; - expect(nativePart).to.not.equal(null); - expect(nativePart.ver).to.equal('1.1'); - expect(nativePart.request).to.not.equal(null); - // native request assets - const nativeRequest = JSON.parse(ortbRequest.imp[0]['native'].request); - expect(nativeRequest).to.not.equal(null); - expect(nativeRequest.assets).to.have.lengthOf(3); - // title asset - expect(nativeRequest.assets[0].id).to.equal(1); - expect(nativeRequest.assets[0].required).to.equal(1); - expect(nativeRequest.assets[0].title).to.not.equal(null); - expect(nativeRequest.assets[0].title.len).to.equal(200); - // data asset - expect(nativeRequest.assets[1].id).to.equal(2); - expect(nativeRequest.assets[1].required).to.equal(0); - expect(nativeRequest.assets[1].title).to.be.undefined; - expect(nativeRequest.assets[1].data).to.not.equal(null); - expect(nativeRequest.assets[1].data.type).to.equal(1); - expect(nativeRequest.assets[1].data.len).to.equal(50); - // image asset - expect(nativeRequest.assets[2].id).to.equal(3); - expect(nativeRequest.assets[2].required).to.equal(0); - expect(nativeRequest.assets[2].title).to.be.undefined; - expect(nativeRequest.assets[2].img).to.not.equal(null); - expect(nativeRequest.assets[2].img.wmin).to.equal(100); - expect(nativeRequest.assets[2].img.hmin).to.equal(150); - expect(nativeRequest.assets[2].img.type).to.equal(3); - }); + if (FEATURES.NATIVE) { + it('Verify Native request', function () { + const request = spec.buildRequests(nativeSlotConfig, syncAddFPDToBidderRequest(bidderRequest)); + expect(request.url).to.equal('https://bid.contextweb.com/header/ortb?src=prebid'); + expect(request.method).to.equal('POST'); + const ortbRequest = request.data; + // native impression + expect(ortbRequest.imp[0].tagid).to.equal('t10000'); + expect(ortbRequest.imp[0].banner).to.be.undefined; + const nativePart = ortbRequest.imp[0]['native']; + expect(nativePart).to.not.equal(null); + expect(nativePart.request).to.not.equal(null); + // native request assets + const nativeRequest = JSON.parse(ortbRequest.imp[0]['native'].request); + expect(nativeRequest).to.not.equal(null); + expect(nativeRequest.assets).to.have.lengthOf(3); + // image asset + expect(nativeRequest.assets[0].id).to.equal(1); + expect(nativeRequest.assets[0].required).to.equal(1); + expect(nativeRequest.assets[0].title).to.be.undefined; + expect(nativeRequest.assets[0].img).to.not.equal(null); + expect(nativeRequest.assets[0].img.w).to.equal(150); + expect(nativeRequest.assets[0].img.h).to.equal(50); + expect(nativeRequest.assets[0].img.type).to.equal(3); + // title asset + expect(nativeRequest.assets[1].id).to.equal(2); + expect(nativeRequest.assets[1].required).to.equal(1); + expect(nativeRequest.assets[1].title).to.not.equal(null); + expect(nativeRequest.assets[1].title.len).to.equal(80); + // data asset + expect(nativeRequest.assets[2].id).to.equal(3); + expect(nativeRequest.assets[2].required).to.equal(0); + expect(nativeRequest.assets[2].title).to.be.undefined; + expect(nativeRequest.assets[2].data).to.not.equal(null); + expect(nativeRequest.assets[2].data.type).to.equal(1); + }); - it('Verify Native response', function () { - const request = spec.buildRequests(nativeSlotConfig, bidderRequest); - expect(request.url).to.equal('https://bid.contextweb.com/header/ortb?src=prebid'); - expect(request.method).to.equal('POST'); - const ortbRequest = request.data; - const nativeResponse = { - 'native': { + it('Verify Native response', function () { + const request = spec.buildRequests(nativeSlotConfig, syncAddFPDToBidderRequest(bidderRequest)); + expect(request.url).to.equal('https://bid.contextweb.com/header/ortb?src=prebid'); + expect(request.method).to.equal('POST'); + const ortbRequest = request.data; + const nativeResponse = { assets: [ - { title: { text: 'Ad Title' } }, - { data: { type: 1, value: 'Sponsored By: Brand' } }, - { img: { type: 3, url: 'https://images.cdn.brand.com/123' } } + {id: 1, img: {type: 3, url: 'https://images.cdn.brand.com/123'}}, + {id: 2, title: {text: 'Ad Title'}}, + {id: 3, data: {type: 1, value: 'Sponsored By: Brand'}} ], - link: { url: 'https://brand.clickme.com/' }, + link: {url: 'https://brand.clickme.com/'}, imptrackers: ['https://imp1.trackme.com/', 'https://imp1.contextweb.com/'] - } - }; - const ortbResponse = { - seatbid: [{ - bid: [{ - impid: ortbRequest.imp[0].id, - price: 1.25, - adm: JSON.stringify(nativeResponse) + + }; + const ortbResponse = { + seatbid: [{ + bid: [{ + impid: ortbRequest.imp[0].id, + price: 1.25, + adm: JSON.stringify(nativeResponse), + mtype: 4 + }] }] - }] - }; - const bids = spec.interpretResponse({ body: ortbResponse }, request); - // verify bid - const bid = bids[0]; - expect(bid.cpm).to.equal(1.25); - expect(bid.adId).to.equal('bid12345'); - expect(bid.ad).to.be.undefined; - expect(bid.mediaType).to.equal('native'); - const nativeBid = bid['native']; - expect(nativeBid).to.not.equal(null); - expect(nativeBid.title).to.equal('Ad Title'); - expect(nativeBid.sponsoredBy).to.equal('Sponsored By: Brand'); - expect(nativeBid.image).to.equal('https://images.cdn.brand.com/123'); - expect(nativeBid.clickUrl).to.equal(encodeURIComponent('https://brand.clickme.com/')); - expect(nativeBid.impressionTrackers).to.have.lengthOf(2); - expect(nativeBid.impressionTrackers[0]).to.equal('https://imp1.trackme.com/'); - expect(nativeBid.impressionTrackers[1]).to.equal('https://imp1.contextweb.com/'); - }); + }; + const bids = spec.interpretResponse({body: ortbResponse}, request); + // verify bid + const bid = bids[0]; + expect(bid.cpm).to.equal(1.25); + expect(bid.requestId).to.equal('bid12345'); + expect(bid.ad).to.be.undefined; + expect(bid.mediaType).to.equal('native'); + expect(bid['native']).to.not.be.null; + expect(bid['native'].ortb).to.not.be.null; + const nativeBid = bid['native'].ortb; + expect(nativeBid.assets).to.have.lengthOf(3); + expect(nativeBid.assets[0].id).to.equal(1); + expect(nativeBid.assets[0].img).to.not.be.null; + expect(nativeBid.assets[0].img.type).to.equal(3); + expect(nativeBid.assets[0].img.url).to.equal('https://images.cdn.brand.com/123'); + expect(nativeBid.assets[1].id).to.equal(2); + expect(nativeBid.assets[1].title).to.not.be.null; + expect(nativeBid.assets[1].title.text).to.equal('Ad Title'); + expect(nativeBid.assets[2].id).to.equal(3); + expect(nativeBid.assets[2].data).to.not.be.null; + expect(nativeBid.assets[2].data.type).to.equal(1); + expect(nativeBid.assets[2].data.value).to.equal('Sponsored By: Brand'); + expect(nativeBid.link).to.not.be.null; + expect(nativeBid.link.url).to.equal('https://brand.clickme.com/'); + expect(nativeBid.imptrackers).to.have.lengthOf(2); + expect(nativeBid.imptrackers[0]).to.equal('https://imp1.trackme.com/'); + expect(nativeBid.imptrackers[1]).to.equal('https://imp1.contextweb.com/'); + }); + } it('Verifies bidder code', function () { expect(spec.code).to.equal('pulsepoint'); @@ -430,19 +358,6 @@ describe('PulsePoint Adapter Tests', function () { expect(options[0].url).to.equal('https://bh.contextweb.com/visitormatch/prebid'); }); - it('Verify app requests', function () { - const request = spec.buildRequests(appSlotConfig, bidderRequest); - const ortbRequest = request.data; - // site object - expect(ortbRequest.site).to.equal(null); - expect(ortbRequest.app).to.not.be.null; - expect(ortbRequest.app.publisher).to.not.equal(null); - expect(ortbRequest.app.publisher.id).to.equal('p10000'); - expect(ortbRequest.app.bundle).to.equal('com.pulsepoint.apps'); - expect(ortbRequest.app.storeurl).to.equal('https://pulsepoint.com/apps'); - expect(ortbRequest.app.domain).to.equal('pulsepoint.com'); - }); - it('Verify GDPR', function () { const bidderRequestGdpr = { gdprConsent: { @@ -450,7 +365,7 @@ describe('PulsePoint Adapter Tests', function () { consentString: 'serialized_gpdr_data' } }; - const request = spec.buildRequests(slotConfigs, Object.assign({}, bidderRequest, bidderRequestGdpr)); + const request = spec.buildRequests(slotConfigs, syncAddFPDToBidderRequest(Object.assign({}, bidderRequest, bidderRequestGdpr))); expect(request.url).to.equal('https://bid.contextweb.com/header/ortb?src=prebid'); expect(request.method).to.equal('POST'); const ortbRequest = request.data; @@ -468,7 +383,8 @@ describe('PulsePoint Adapter Tests', function () { const bidderRequestUSPrivacy = { uspConsent: '1YYY' }; - const request = spec.buildRequests(slotConfigs, Object.assign({}, bidderRequest, bidderRequestUSPrivacy)); + const request = spec.buildRequests(slotConfigs, + syncAddFPDToBidderRequest(Object.assign({}, bidderRequest, bidderRequestUSPrivacy))); expect(request.url).to.equal('https://bid.contextweb.com/header/ortb?src=prebid'); expect(request.method).to.equal('POST'); const ortbRequest = request.data; @@ -478,52 +394,54 @@ describe('PulsePoint Adapter Tests', function () { expect(ortbRequest.regs.ext.us_privacy).to.equal('1YYY'); }); - it('Verify Video request', function () { - const request = spec.buildRequests(videoSlotConfig, bidderRequest); - expect(request.url).to.equal('https://bid.contextweb.com/header/ortb?src=prebid'); - expect(request.method).to.equal('POST'); - const ortbRequest = request.data; - expect(ortbRequest).to.not.equal(null); - expect(ortbRequest.imp).to.have.lengthOf(1); - expect(ortbRequest.imp[0].video).to.not.be.null; - expect(ortbRequest.imp[0].native).to.be.null; - expect(ortbRequest.imp[0].banner).to.be.null; - expect(ortbRequest.imp[0].video.w).to.equal(400); - expect(ortbRequest.imp[0].video.h).to.equal(300); - expect(ortbRequest.imp[0].video.minduration).to.equal(5); - expect(ortbRequest.imp[0].video.maxduration).to.equal(10); - expect(ortbRequest.imp[0].video.startdelay).to.equal(0); - expect(ortbRequest.imp[0].video.skip).to.equal(1); - expect(ortbRequest.imp[0].video.minbitrate).to.equal(200); - expect(ortbRequest.imp[0].video.protocols).to.eql([1, 2, 4]); - }); + if (FEATURES.VIDEO) { + it('Verify Video request', function () { + const request = spec.buildRequests(videoSlotConfig, syncAddFPDToBidderRequest(bidderRequest)); + expect(request.url).to.equal('https://bid.contextweb.com/header/ortb?src=prebid'); + expect(request.method).to.equal('POST'); + const ortbRequest = request.data; + expect(ortbRequest).to.not.equal(null); + expect(ortbRequest.imp).to.have.lengthOf(1); + expect(ortbRequest.imp[0].video).to.not.be.null; + expect(ortbRequest.imp[0].native).to.be.undefined; + expect(ortbRequest.imp[0].banner).to.be.undefined; + expect(ortbRequest.imp[0].video.w).to.equal(400); + expect(ortbRequest.imp[0].video.h).to.equal(300); + expect(ortbRequest.imp[0].video.minduration).to.equal(5); + expect(ortbRequest.imp[0].video.maxduration).to.equal(10); + expect(ortbRequest.imp[0].video.startdelay).to.equal(0); + expect(ortbRequest.imp[0].video.skip).to.equal(1); + expect(ortbRequest.imp[0].video.minbitrate).to.equal(200); + expect(ortbRequest.imp[0].video.protocols).to.eql([1, 2, 4]); + }); - it('Verify Video response', function () { - const request = spec.buildRequests(videoSlotConfig, bidderRequest); - expect(request.url).to.equal('https://bid.contextweb.com/header/ortb?src=prebid'); - expect(request.method).to.equal('POST'); - const ortbRequest = request.data; - const ortbResponse = { - seatbid: [{ - bid: [{ - impid: ortbRequest.imp[0].id, - price: 1.25, - adm: 'https//pulsepoint.video.mp4' + it('Verify Video response', function () { + const request = spec.buildRequests(videoSlotConfig, bidderRequest); + expect(request.url).to.equal('https://bid.contextweb.com/header/ortb?src=prebid'); + expect(request.method).to.equal('POST'); + const ortbRequest = request.data; + const ortbResponse = { + seatbid: [{ + bid: [{ + impid: ortbRequest.imp[0].id, + price: 1.25, + adm: 'https//pulsepoint.video.mp4', + mtype: 2 + }] }] - }] - }; - const bids = spec.interpretResponse({ body: ortbResponse }, request); - const bid = bids[0]; - expect(bid.cpm).to.equal(1.25); - expect(bid.adId).to.equal('bid12345'); - expect(bid.ad).to.be.undefined; - expect(bid['native']).to.be.undefined; - expect(bid.mediaType).to.equal('video'); - expect(bid.vastXml).to.equal(ortbResponse.seatbid[0].bid[0].adm); - }); + }; + const bids = spec.interpretResponse({ body: ortbResponse }, request); + const bid = bids[0]; + expect(bid.cpm).to.equal(1.25); + expect(bid.ad).to.be.undefined; + expect(bid['native']).to.be.undefined; + expect(bid.mediaType).to.equal('video'); + expect(bid.vastXml).to.equal(ortbResponse.seatbid[0].bid[0].adm); + }); + } it('Verify extra parameters', function () { - let request = spec.buildRequests(additionalParamsConfig, bidderRequest); + let request = spec.buildRequests(additionalParamsConfig, syncAddFPDToBidderRequest(bidderRequest)); let ortbRequest = request.data; expect(ortbRequest).to.not.equal(null); expect(ortbRequest.imp).to.have.lengthOf(1); @@ -538,31 +456,15 @@ describe('PulsePoint Adapter Tests', function () { expect(ortbRequest.imp[0].ext.prebid.extra_key4).to.eql([1, 2, 3]); expect(Object.keys(ortbRequest.imp[0].ext.prebid)).to.eql(['extra_key1', 'extra_key2', 'extra_key3', 'extra_key4']); // attempting with a configuration with no unknown params. - request = spec.buildRequests(outstreamSlotConfig, bidderRequest); + request = spec.buildRequests(videoSlotConfig, bidderRequest); ortbRequest = request.data; expect(ortbRequest).to.not.equal(null); expect(ortbRequest.imp).to.have.lengthOf(1); - expect(ortbRequest.imp[0].ext).to.equal(null); - }); - - it('Verify ortb parameters', function () { - const request = spec.buildRequests(ortbParamsSlotConfig, bidderRequest); - const ortbRequest = request.data; - expect(ortbRequest).to.not.equal(null); - expect(ortbRequest.bcat).to.eql(['IAB-1', 'IAB-20']); - expect(ortbRequest.badv).to.eql(['cocacola.com', 'lays.com']); - expect(ortbRequest.imp).to.have.lengthOf(2); - expect(ortbRequest.imp[0].bidfloor).to.equal(1.5); - expect(ortbRequest.imp[0].banner.battr).to.eql([1, 2, 3]); - expect(ortbRequest.imp[0].ext).to.be.null; - // slot 2 - expect(ortbRequest.imp[1].bidfloor).to.equal(2.5); - expect(ortbRequest.imp[1].video.battr).to.eql([2, 3, 4]); - expect(ortbRequest.imp[1].ext).to.be.null; + expect(ortbRequest.imp[0].ext).to.be.undefined; }); it('Verify schain parameters', function () { - const request = spec.buildRequests(schainParamsSlotConfig, bidderRequest); + const request = spec.buildRequests(schainParamsSlotConfig, syncAddFPDToBidderRequest(bidderRequest)); const ortbRequest = request.data; expect(ortbRequest).to.not.equal(null); expect(ortbRequest.source).to.not.equal(null); @@ -580,42 +482,6 @@ describe('PulsePoint Adapter Tests', function () { expect(ortbRequest.source.ext.schain.nodes[0].domain).to.equal('publisher.com'); }); - it('Verify outstream renderer', function () { - const bidderRequestOutstream = Object.assign({}, bidderRequest, {bids: [outstreamSlotConfig[0]]}); - const request = spec.buildRequests(outstreamSlotConfig, bidderRequestOutstream); - const ortbRequest = request.data; - expect(ortbRequest).to.not.be.null; - expect(ortbRequest.imp[0]).to.not.be.null; - expect(ortbRequest.imp[0].video).to.not.be.null; - const ortbResponse = { - seatbid: [{ - bid: [{ - impid: ortbRequest.imp[0].id, - price: 1.25, - adm: 'https//pulsepoint.video.mp4', - ext: { - outstream: { - type: 'Inline', - config: { - text: 'ADVERTISEMENT', - skipaftersec: 5 - }, - rendererUrl: 'https://tag.contextweb.com/hb-outstr-renderer.js' - } - } - }] - }] - }; - const bids = spec.interpretResponse({ body: ortbResponse }, request); - const bid = bids[0]; - expect(bid.cpm).to.equal(1.25); - expect(bid.renderer).to.not.be.null; - expect(bid.renderer.url).to.equal('https://tag.contextweb.com/hb-outstr-renderer.js'); - expect(bid.renderer.getConfig()).to.not.be.null; - expect(bid.renderer.getConfig().defaultOptions).to.eql(ortbResponse.seatbid[0].bid[0].ext.outstream.config); - expect(bid.renderer.getConfig().rendererOptions).to.eql(outstreamSlotConfig[0].renderer.options); - expect(bid.renderer.getConfig().type).to.equal('Inline'); - }); it('Verify common id parameters', function () { const bidRequests = deepClone(slotConfigs); bidRequests[0].userIdAsEids = [{ @@ -633,169 +499,17 @@ describe('PulsePoint Adapter Tests', function () { }] } ]; - const request = spec.buildRequests(bidRequests, bidderRequest); + const request = spec.buildRequests(bidRequests, syncAddFPDToBidderRequest(bidderRequest)); expect(request).to.be.not.null; - const ortbRequest = request.data; expect(request.data).to.be.not.null; + const ortbRequest = request.data; // user object expect(ortbRequest.user).to.not.be.undefined; expect(ortbRequest.user.ext).to.not.be.undefined; expect(ortbRequest.user.ext.eids).to.not.be.undefined; expect(ortbRequest.user.ext.eids).to.deep.equal(bidRequests[0].userIdAsEids); }); - it('Verify multiple adsizes', function () { - const bidRequests = deepClone(slotConfigs); - const request = spec.buildRequests(bidRequests, bidderRequest); - expect(request).to.be.not.null; - expect(request.data).to.be.not.null; - const ortbRequest = request.data; - expect(ortbRequest.imp).to.have.lengthOf(2); - // first impression has multi sizes - expect(ortbRequest.imp[0].banner).to.not.be.null; - expect(ortbRequest.imp[0].banner.w).to.equal(300); - expect(ortbRequest.imp[0].banner.h).to.equal(250); - expect(ortbRequest.imp[0].banner.format).to.not.be.null; - expect(ortbRequest.imp[0].banner.format).to.have.lengthOf(2); - expect(ortbRequest.imp[0].banner.format[0].w).to.equal(728); - expect(ortbRequest.imp[0].banner.format[0].h).to.equal(90); - expect(ortbRequest.imp[0].banner.format[1].w).to.equal(160); - expect(ortbRequest.imp[0].banner.format[1].h).to.equal(600); - // slot 2 - expect(ortbRequest.imp[1].banner).to.not.be.null; - expect(ortbRequest.imp[1].banner.w).to.equal(728); - expect(ortbRequest.imp[1].banner.h).to.equal(90); - expect(ortbRequest.imp[1].banner.format).to.not.be.null; - expect(ortbRequest.imp[1].banner.format).to.have.lengthOf(1); - expect(ortbRequest.imp[1].banner.format[0].w).to.equal(728); - expect(ortbRequest.imp[1].banner.format[0].h).to.equal(90); - // adsize on response - const ortbResponse = { - seatbid: [{ - bid: [{ - impid: ortbRequest.imp[0].id, - price: 1.25, - adm: 'This is an Ad', - crid: 'Creative#123', - w: 728, - h: 90 - }] - }] - }; - const bids = spec.interpretResponse({ body: ortbResponse }, request); - expect(bids).to.have.lengthOf(1); - const bid = bids[0]; - expect(bid.width).to.equal(728); - expect(bid.height).to.equal(90); - }); - it('Verify multi-format response', function () { - const bidRequests = deepClone(slotConfigs); - bidRequests[0].mediaTypes['native'] = { - title: { - required: true - }, - image: { - required: true - }, - sponsoredBy: { - required: true - } - }; - bidRequests[1].params.video = { - w: 400, - h: 300, - minduration: 5, - maxduration: 10, - }; - const request = spec.buildRequests(bidRequests, bidderRequest); - expect(request).to.be.not.null; - expect(request.data).to.be.not.null; - const ortbRequest = request.data; - expect(ortbRequest.imp).to.have.lengthOf(2); - // adsize on response - const ortbResponse = { - seatbid: [{ - bid: [{ - impid: ortbRequest.imp[0].id, - price: 1.25, - adm: 'This is an Ad', - crid: 'Creative#123', - w: 728, - h: 90 - }, { - impid: ortbRequest.imp[1].id, - price: 2.5, - adm: '', - crid: 'Creative#234', - w: 728, - h: 90 - }] - }] - }; - // request has both types - banner and native, response is parsed as banner. - // for impression#2, response is parsed as video - const bids = spec.interpretResponse({ body: ortbResponse }, request); - expect(bids).to.have.lengthOf(2); - const bid = bids[0]; - expect(bid.width).to.equal(728); - expect(bid.height).to.equal(90); - const secondBid = bids[1]; - expect(secondBid.vastXml).to.equal(''); - }); - it('Verify bid floor', function () { - const bidRequests = deepClone(slotConfigs); - bidRequests[0].params.bidfloor = 1.05; - let request = spec.buildRequests(bidRequests, bidderRequest); - let ortbRequest = request.data; - expect(ortbRequest).to.not.equal(null); - expect(ortbRequest.imp[0].bidfloor).to.equal(1.05); - expect(ortbRequest.imp[1].bidfloor).to.be.undefined; - let floorArg = null; - // publisher uses the floor module - bidRequests[0].getFloor = (arg) => { - floorArg = arg; - return { floor: 1.25 }; - }; - bidRequests[1].getFloor = () => { - return { floor: 2.05 }; - }; - request = spec.buildRequests(bidRequests, bidderRequest); - ortbRequest = request.data; - expect(ortbRequest).to.not.equal(null); - expect(ortbRequest.imp[0].bidfloor).to.equal(1.25); - expect(ortbRequest.imp[1].bidfloor).to.equal(2.05); - expect(floorArg).to.not.be.null; - expect(floorArg.mediaType).to.equal('banner'); - expect(floorArg.currency).to.equal('USD'); - expect(floorArg.size).to.equal('*'); - }); - it('Verify Video params on mediaTypes.video', function () { - const bidRequests = deepClone(videoSlotConfig); - bidRequests[0].mediaTypes = { - video: { - w: 600, - h: 400, - minduration: 15, - maxduration: 20, - startdelay: 10, - skip: 0, - } - }; - const request = spec.buildRequests(bidRequests, bidderRequest); - const ortbRequest = request.data; - expect(ortbRequest).to.not.equal(null); - expect(ortbRequest.imp).to.have.lengthOf(1); - expect(ortbRequest.imp[0].video).to.not.be.null; - expect(ortbRequest.imp[0].native).to.be.null; - expect(ortbRequest.imp[0].banner).to.be.null; - expect(ortbRequest.imp[0].video.w).to.equal(600); - expect(ortbRequest.imp[0].video.h).to.equal(400); - expect(ortbRequest.imp[0].video.minduration).to.equal(15); - expect(ortbRequest.imp[0].video.maxduration).to.equal(20); - expect(ortbRequest.imp[0].video.startdelay).to.equal(10); - expect(ortbRequest.imp[0].video.skip).to.equal(0); - expect(ortbRequest.imp[0].video.minbitrate).to.equal(200); - expect(ortbRequest.imp[0].video.protocols).to.eql([1, 2, 4]); - }); + it('Verify user level first party data', function () { const bidderRequest = { refererInfo: { @@ -819,7 +533,7 @@ describe('PulsePoint Adapter Tests', function () { } } }; - let request = spec.buildRequests(slotConfigs, bidderRequest); + let request = spec.buildRequests(slotConfigs, syncAddFPDToBidderRequest(bidderRequest)); let ortbRequest = request.data; expect(ortbRequest).to.not.equal(null); expect(ortbRequest.user).to.not.equal(null); @@ -835,12 +549,9 @@ describe('PulsePoint Adapter Tests', function () { } }); }); + it('Verify site level first party data', function () { const bidderRequest = { - refererInfo: { - page: 'https://publisher.com/home', - ref: 'https://referrer' - }, ortb2: { site: { content: { @@ -853,11 +564,14 @@ describe('PulsePoint Adapter Tests', function () { }] }, page: 'http://pub.com/news', - ref: 'http://google.com' + ref: 'http://google.com', + publisher: { + domain: 'pub.com' + } } } }; - let request = spec.buildRequests(slotConfigs, bidderRequest); + let request = spec.buildRequests(slotConfigs, syncAddFPDToBidderRequest(bidderRequest)); let ortbRequest = request.data; expect(ortbRequest).to.not.equal(null); expect(ortbRequest.site).to.not.equal(null); @@ -871,13 +585,15 @@ describe('PulsePoint Adapter Tests', function () { } }] }, - page: 'https://publisher.com/home', - ref: 'https://referrer', + page: 'http://pub.com/news', + ref: 'http://google.com', publisher: { - id: 'p10000' + id: 'p10000', + domain: 'pub.com' } }); }); + it('Verify impression/slot level first party data', function () { const bidderRequests = [{ placementCode: '/DfpAccount1/slot1', @@ -928,4 +644,27 @@ describe('PulsePoint Adapter Tests', function () { // assert bidderRequest value is used when available expect(mkRequest(Object.assign({}, { timeout: 6000 }, bidderRequest)).tmax).to.equal(6000) }); + + it('Verify deals', function () { + const bidRequests = deepClone(slotConfigs); + const deals = [{ + id: 'DEAL_ONE', + bidfloor: 1.1 + }, { + id: 'DEAL_TWO', + bidfloor: 2.2 + }]; + bidRequests[0].params.deals = deals; + const request = spec.buildRequests(bidRequests, syncAddFPDToBidderRequest(bidderRequest)); + expect(request.url).to.equal('https://bid.contextweb.com/header/ortb?src=prebid'); + expect(request.method).to.equal('POST'); + const ortbRequest = request.data; + // slot 1 + expect(ortbRequest.imp[0].tagid).to.equal('t10000'); + expect(ortbRequest.imp[0].pmp).to.not.be.undefined; + expect(ortbRequest.imp[0].pmp).to.deep.equal({ + private_auction: 0, + deals + }); + }) }); diff --git a/test/spec/modules/qortexRtdProvider_spec.js b/test/spec/modules/qortexRtdProvider_spec.js new file mode 100644 index 00000000000..9baa526e4cc --- /dev/null +++ b/test/spec/modules/qortexRtdProvider_spec.js @@ -0,0 +1,333 @@ +import * as utils from 'src/utils'; +import * as ajax from 'src/ajax.js'; +import * as events from 'src/events.js'; +import CONSTANTS from '../../../src/constants.json'; +import {loadExternalScript} from 'src/adloader.js'; +import { + qortexSubmodule as module, + getContext, + addContextToRequests, + setContextData, + initializeModuleData, + loadScriptTag +} from '../../../modules/qortexRtdProvider'; +import {server} from '../../mocks/xhr.js'; +import { cloneDeep } from 'lodash'; + +describe('qortexRtdProvider', () => { + let logWarnSpy; + let ortb2Stub; + + const defaultApiHost = 'https://demand.qortex.ai'; + const defaultGroupId = 'test'; + + const validBidderArray = ['qortex', 'test']; + const validTagConfig = { + videoContainer: 'my-video-container' + } + + const validModuleConfig = { + params: { + groupId: defaultGroupId, + apiUrl: defaultApiHost, + bidders: validBidderArray + } + }, + emptyModuleConfig = { + params: {} + } + + const validImpressionEvent = { + detail: { + uid: 'uid123', + type: 'qx-impression' + } + }, + validImpressionEvent2 = { + detail: { + uid: 'uid1234', + type: 'qx-impression' + } + }, + missingIdImpressionEvent = { + detail: { + type: 'qx-impression' + } + }, + invalidTypeQortexEvent = { + detail: { + type: 'invalid-type' + } + } + + const responseHeaders = { + 'content-type': 'application/json', + 'access-control-allow-origin': '*' + }; + + const responseObj = { + content: { + id: '123456', + episode: 15, + title: 'test episode', + series: 'test show', + season: '1', + url: 'https://example.com/file.mp4' + } + }; + + const apiResponse = JSON.stringify(responseObj); + + const reqBidsConfig = { + adUnits: [{ + bids: [ + { bidder: 'qortex' } + ] + }], + ortb2Fragments: { + bidder: {}, + global: {} + } + } + + beforeEach(() => { + ortb2Stub = sinon.stub(reqBidsConfig, 'ortb2Fragments').value({bidder: {}, global: {}}) + logWarnSpy = sinon.spy(utils, 'logWarn'); + }) + + afterEach(() => { + logWarnSpy.restore(); + ortb2Stub.restore(); + setContextData(null); + }) + + describe('init', () => { + it('returns true for valid config object', () => { + expect(module.init(validModuleConfig)).to.be.true; + }) + + it('returns false and logs error for missing groupId', () => { + expect(module.init(emptyModuleConfig)).to.be.false; + expect(logWarnSpy.calledOnce).to.be.true; + expect(logWarnSpy.calledWith('Qortex RTD module config does not contain valid groupId parameter. Config params: {}')).to.be.ok; + }) + + it('loads Qortex script if tagConfig is present in module config params', () => { + const config = cloneDeep(validModuleConfig); + config.params.tagConfig = validTagConfig; + expect(module.init(config)).to.be.true; + expect(loadExternalScript.calledOnce).to.be.true; + }) + }) + + describe('loadScriptTag', () => { + let addEventListenerSpy; + let billableEvents = []; + + let config = cloneDeep(validModuleConfig); + config.params.tagConfig = validTagConfig; + + events.on(CONSTANTS.EVENTS.BILLABLE_EVENT, (e) => { + billableEvents.push(e); + }) + + beforeEach(() => { + initializeModuleData(config); + addEventListenerSpy = sinon.spy(window, 'addEventListener'); + }) + + afterEach(() => { + addEventListenerSpy.restore(); + billableEvents = []; + }) + + it('adds event listener', () => { + loadScriptTag(config); + expect(addEventListenerSpy.calledOnce).to.be.true; + }) + + it('parses incoming qortex-impression events', () => { + loadScriptTag(config); + dispatchEvent(new CustomEvent('qortex-rtd', validImpressionEvent)); + expect(billableEvents.length).to.be.equal(1); + expect(billableEvents[0].type).to.be.equal(validImpressionEvent.detail.type); + expect(billableEvents[0].transactionId).to.be.equal(validImpressionEvent.detail.uid); + }) + + it('will emit two events for impressions with two different ids', () => { + loadScriptTag(config); + dispatchEvent(new CustomEvent('qortex-rtd', validImpressionEvent)); + dispatchEvent(new CustomEvent('qortex-rtd', validImpressionEvent2)); + expect(billableEvents.length).to.be.equal(2); + expect(billableEvents[0].transactionId).to.be.equal(validImpressionEvent.detail.uid); + expect(billableEvents[1].transactionId).to.be.equal(validImpressionEvent2.detail.uid); + }) + + it('will not allow multiple events with the same id', () => { + loadScriptTag(config); + dispatchEvent(new CustomEvent('qortex-rtd', validImpressionEvent)); + dispatchEvent(new CustomEvent('qortex-rtd', validImpressionEvent)); + expect(billableEvents.length).to.be.equal(1); + expect(logWarnSpy.calledWith('received invalid billable event due to duplicate uid: qx-impression')).to.be.ok; + }) + + it('will not allow events with missing uid', () => { + loadScriptTag(config); + dispatchEvent(new CustomEvent('qortex-rtd', missingIdImpressionEvent)); + expect(billableEvents.length).to.be.equal(0); + expect(logWarnSpy.calledWith('received invalid billable event due to missing uid: qx-impression')).to.be.ok; + }) + + it('will not allow events with unavailable type', () => { + loadScriptTag(config); + dispatchEvent(new CustomEvent('qortex-rtd', invalidTypeQortexEvent)); + expect(billableEvents.length).to.be.equal(0); + expect(logWarnSpy.calledWith('received invalid billable event: invalid-type')).to.be.ok; + }) + }) + + describe('getBidRequestData', () => { + let callbackSpy; + + beforeEach(() => { + initializeModuleData(validModuleConfig); + callbackSpy = sinon.spy(); + }) + + afterEach(() => { + initializeModuleData(emptyModuleConfig); + callbackSpy.resetHistory(); + }) + + it('will call callback immediately if no adunits', () => { + const reqBidsConfigNoBids = { adUnits: [] }; + module.getBidRequestData(reqBidsConfigNoBids, callbackSpy); + expect(callbackSpy.calledOnce).to.be.true; + expect(logWarnSpy.calledWith('No adunits found on request bids configuration: ' + JSON.stringify(reqBidsConfigNoBids))).to.be.ok; + }) + + it('will call callback if getContext does not throw', () => { + const cb = function () { + expect(logWarnSpy.calledOnce).to.be.false; + done(); + } + module.getBidRequestData(reqBidsConfig, cb); + server.requests[0].respond(200, responseHeaders, apiResponse); + }) + + it('will catch and log error and fire callback', (done) => { + const a = sinon.stub(ajax, 'ajax').throws(new Error('test')); + const cb = function () { + expect(logWarnSpy.calledWith('test')).to.be.eql(true); + done(); + } + module.getBidRequestData(reqBidsConfig, cb); + a.restore(); + }) + }) + + describe('getContext', () => { + beforeEach(() => { + initializeModuleData(validModuleConfig); + }) + + afterEach(() => { + initializeModuleData(emptyModuleConfig); + }) + + it('returns a promise', (done) => { + const result = getContext(); + expect(result).to.be.a('promise'); + done(); + }) + + it('uses request url generated from initialize function in config and resolves to content object data', (done) => { + let requestUrl = `${validModuleConfig.params.apiUrl}/api/v1/analyze/${validModuleConfig.params.groupId}/prebid`; + const ctx = getContext() + expect(server.requests.length).to.be.eql(1); + expect(server.requests[0].url).to.be.eql(requestUrl); + server.requests[0].respond(200, responseHeaders, apiResponse); + ctx.then(response => { + expect(response).to.be.eql(responseObj.content); + done(); + }); + }) + + it('will return existing context data instead of ajax call if the source was not updated', (done) => { + setContextData(responseObj.content); + const ctx = getContext(); + expect(server.requests.length).to.be.eql(0); + ctx.then(response => { + expect(response).to.be.eql(responseObj.content); + done(); + }); + }) + + it('returns null for non erroring api responses other than 200', (done) => { + const nullContentResponse = { content: null } + const ctx = getContext() + server.requests[0].respond(200, responseHeaders, JSON.stringify(nullContentResponse)) + ctx.then(response => { + expect(response).to.be.null; + expect(server.requests.length).to.be.eql(1); + expect(logWarnSpy.called).to.be.false; + done(); + }); + }) + }) + + describe(' addContextToRequests', () => { + it('logs error if no data was retrieved from get context call', () => { + initializeModuleData(validModuleConfig); + addContextToRequests(reqBidsConfig); + expect(logWarnSpy.calledOnce).to.be.true; + expect(logWarnSpy.calledWith('No context data received at this time')).to.be.ok; + expect(reqBidsConfig.ortb2Fragments.global).to.be.eql({}); + expect(reqBidsConfig.ortb2Fragments.bidder).to.be.eql({}); + }) + + it('adds site.content only to global ortb2 when bidders array is omitted', () => { + const omittedBidderArrayConfig = cloneDeep(validModuleConfig); + delete omittedBidderArrayConfig.params.bidders; + initializeModuleData(omittedBidderArrayConfig); + setContextData(responseObj.content); + addContextToRequests(reqBidsConfig); + expect(reqBidsConfig.ortb2Fragments.global).to.have.property('site'); + expect(reqBidsConfig.ortb2Fragments.global.site).to.have.property('content'); + expect(reqBidsConfig.ortb2Fragments.global.site.content).to.be.eql(responseObj.content); + expect(reqBidsConfig.ortb2Fragments.bidder).to.be.eql({}); + }) + + it('adds site.content only to bidder ortb2 when bidders array is included', () => { + initializeModuleData(validModuleConfig); + setContextData(responseObj.content); + addContextToRequests(reqBidsConfig); + + const qortexOrtb2Fragment = reqBidsConfig.ortb2Fragments.bidder['qortex'] + expect(qortexOrtb2Fragment).to.not.be.null; + expect(qortexOrtb2Fragment).to.have.property('site'); + expect(qortexOrtb2Fragment.site).to.have.property('content'); + expect(qortexOrtb2Fragment.site.content).to.be.eql(responseObj.content); + + const testOrtb2Fragment = reqBidsConfig.ortb2Fragments.bidder['test'] + expect(testOrtb2Fragment).to.not.be.null; + expect(testOrtb2Fragment).to.have.property('site'); + expect(testOrtb2Fragment.site).to.have.property('content'); + expect(testOrtb2Fragment.site.content).to.be.eql(responseObj.content); + + expect(reqBidsConfig.ortb2Fragments.global).to.be.eql({}); + }) + + it('logs error if there is an empty bidder array', () => { + const invalidBidderArrayConfig = cloneDeep(validModuleConfig); + invalidBidderArrayConfig.params.bidders = []; + initializeModuleData(invalidBidderArrayConfig); + setContextData(responseObj.content) + addContextToRequests(reqBidsConfig); + + expect(logWarnSpy.calledWith('Config contains an empty bidders array, unable to determine which bids to enrich')).to.be.ok; + expect(reqBidsConfig.ortb2Fragments.global).to.be.eql({}); + expect(reqBidsConfig.ortb2Fragments.bidder).to.be.eql({}); + }) + }) +}) diff --git a/test/spec/modules/r2b2BidAdapter_spec.js b/test/spec/modules/r2b2BidAdapter_spec.js new file mode 100644 index 00000000000..b94b400a71d --- /dev/null +++ b/test/spec/modules/r2b2BidAdapter_spec.js @@ -0,0 +1,689 @@ +import {expect} from 'chai'; +import {spec, internal as r2b2, internal} from 'modules/r2b2BidAdapter.js'; +import * as utils from '../../../src/utils'; +import 'modules/schain.js'; +import 'modules/userId/index.js'; + +function encodePlacementIds (ids) { + return btoa(JSON.stringify(ids)); +} + +describe('R2B2 adapter', function () { + let serverResponse, requestForInterpretResponse; + let bidderRequest; + let bids = []; + let gdprConsent = { + gdprApplies: true, + consentString: 'consent-string', + }; + let schain = { + ver: '1.0', + complete: 1, + nodes: [{ + asi: 'example.com', + sid: '00001', + hp: 1 + }] + }; + const usPrivacyString = '1YNN'; + const impId = 'impID'; + const price = 10.6; + const ad = 'adm'; + const creativeId = 'creativeID'; + const cid = 41849; + const cdid = 595121; + const unitCode = 'unitCode'; + const bidId1 = '1'; + const bidId2 = '2'; + const bidId3 = '3'; + const bidId4 = '4'; + const bidId5 = '5'; + const bidWonUrl = 'url1'; + const setTargetingUrl = 'url2'; + const bidder = 'r2b2'; + const foreignBidder = 'differentBidder'; + const id1 = { pid: 'd/g/p' }; + const id1Object = { d: 'd', g: 'g', p: 'p', m: 0 }; + const id2 = { pid: 'd/g/p/1' }; + const id2Object = { d: 'd', g: 'g', p: 'p', m: 1 }; + const badId = { pid: 'd/g/' }; + const bid1 = { bidId: bidId1, bidder, params: [ id1 ] }; + const bid2 = { bidId: bidId2, bidder, params: [ id2 ] }; + const bidWithBadSetup = { bidId: bidId3, bidder, params: [ badId ] }; + const bidForeign1 = { bidId: bidId4, bidder: foreignBidder, params: [ { id: 'abc' } ] }; + const bidForeign2 = { bidId: bidId5, bidder: foreignBidder, params: [ { id: 'xyz' } ] }; + const fakeTime = 1234567890; + const cacheBusterRegex = /[\?&]cb=([^&]+)/; + let bidStub, time; + + beforeEach(function () { + bids = [{ + bidder: 'r2b2', + params: { + pid: 'example.com/generic/300x250/1' + }, + mediaTypes: { + banner: { + sizes: [ + [300, 250] + ] + } + }, + adUnitCode: unitCode, + transactionId: '29c408b9-65ce-48b1-9167-18a57791f908', + sizes: [ + [300, 250] + ], + bidId: '20917a54ee9858', + bidderRequestId: '15270d403778d', + auctionId: '36acef1b-f635-4f57-b693-5cc55ee16346', + src: 'client', + ortb2: { + regs: { + ext: { + gdpr: 1, + us_privacy: '1YYY' + } + }, + user: { + ext: { + consent: 'consent-string' + } + }, + site: {}, + device: {} + }, + schain + }, { + bidder: 'r2b2', + params: { + pid: 'example.com/generic/300x600/0' + }, + mediaTypes: { + banner: { + sizes: [ + [300, 600] + ] + } + }, + adUnitCode: unitCode, + transactionId: '29c408b9-65ce-48b1-9167-18a57791f908', + sizes: [ + [300, 600] + ], + bidId: '3dd53d30c691fe', + bidderRequestId: '15270d403778d', + auctionId: '36acef1b-f635-4f57-b693-5cc55ee16346', + src: 'client', + ortb2: { + regs: { + ext: { + gdpr: 1, + us_privacy: '1YYY' + } + }, + user: { + ext: { + consent: 'consent-string' + } + }, + site: {}, + device: {} + }, + schain + }]; + bidderRequest = { + bidderCode: 'r2b2', + auctionId: '36acef1b-f635-4f57-b693-5cc55ee16346', + bidderRequestId: '15270d403778d', + bids: bids, + ortb2: { + regs: { + ext: { + gdpr: 1, + us_privacy: '1YYY' + } + }, + user: { + ext: { + consent: 'consent-string' + } + }, + site: {}, + device: {} + }, + gdprConsent: { + consentString: 'consent-string', + vendorData: {}, + gdprApplies: true, + apiVersion: 2 + }, + uspConsent: '1YYY', + }; + serverResponse = { + id: 'a66a6e32-2a7d-4ed3-bb13-6f3c9bdcf6a1', + seatbid: [{ + bid: [{ + id: '4756cc9e9b504fd0bd39fdd594506545', + impid: impId, + price: price, + adm: ad, + crid: creativeId, + w: 300, + h: 250, + ext: { + prebid: { + meta: { + adaptercode: 'r2b2' + }, + type: 'banner' + }, + r2b2: { + cdid: cdid, + cid: cid, + useRenderer: true + } + } + }], + seat: 'seat' + }] + }; + requestForInterpretResponse = { + data: { + imp: [ + {id: impId} + ] + }, + bids + }; + }); + + describe('isBidRequestValid', function () { + let bid = {}; + + it('should return false when missing required "pid" param', function () { + bid.params = {random: 'param'}; + expect(spec.isBidRequestValid(bid)).to.equal(false); + bid.params = {d: 'd', g: 'g', p: 'p', m: 1}; + expect(spec.isBidRequestValid(bid)).to.equal(false) + }); + + it('should return false when "pid" is malformed', function () { + bid.params = {pid: 'pid'}; + expect(spec.isBidRequestValid(bid)).to.equal(false); + bid.params = {pid: '///'}; + expect(spec.isBidRequestValid(bid)).to.equal(false); + bid.params = {pid: '/g/p/m'}; + expect(spec.isBidRequestValid(bid)).to.equal(false); + bid.params = {pid: 'd//p/m'}; + expect(spec.isBidRequestValid(bid)).to.equal(false); + bid.params = {pid: 'd/g//m'}; + expect(spec.isBidRequestValid(bid)).to.equal(false); + bid.params = {pid: 'd/p/'}; + expect(spec.isBidRequestValid(bid)).to.equal(false); + bid.params = {pid: 'd/g/p/m/t'}; + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + + it('should return true when "pid" is a correct dgpm', function () { + bid.params = {pid: 'd/g/p/m'}; + expect(spec.isBidRequestValid(bid)).to.equal(true); + }); + it('should return true when type is blank', function () { + bid.params = {pid: 'd/g/p/'}; + expect(spec.isBidRequestValid(bid)).to.equal(true); + }); + it('should return true when type is missing', function () { + bid.params = {pid: 'd/g/p'}; + expect(spec.isBidRequestValid(bid)).to.equal(true); + }); + it('should return true when "pid" is a number', function () { + bid.params = {pid: 12356}; + expect(spec.isBidRequestValid(bid)).to.equal(true); + }); + it('should return true when "pid" is a numeric string', function () { + bid.params = {pid: '12356'}; + expect(spec.isBidRequestValid(bid)).to.equal(true); + }); + it('should return true for selfpromo unit', function () { + bid.params = {pid: 'selfpromo'}; + expect(spec.isBidRequestValid(bid)).to.equal(true) + }); + }); + + describe('buildRequests', function () { + beforeEach(function () { + r2b2.placementsToSync = []; + r2b2.mappedParams = {}; + }); + + it('should set correct request method and url and pass bids', function () { + let requests = spec.buildRequests([bids[0]], bidderRequest); + expect(requests).to.be.an('array').that.has.lengthOf(1); + let request = requests[0] + expect(request.method).to.equal('POST'); + expect(request.url).to.equal('https://hb.r2b2.cz/openrtb2/bid'); + expect(request.data).to.be.an('object'); + expect(request.bids).to.deep.equal(bids); + }); + + it('should pass correct parameters', function () { + let requests = spec.buildRequests([bids[0]], bidderRequest); + let {data} = requests[0]; + let {imp, device, site, source, ext, cur, test} = data; + expect(imp).to.be.an('array').that.has.lengthOf(1); + expect(device).to.be.an('object'); + expect(site).to.be.an('object'); + expect(source).to.be.an('object'); + expect(cur).to.deep.equal(['USD']); + expect(ext.version).to.equal('1.0.0'); + expect(test).to.equal(0); + }); + + it('should pass correct imp', function () { + let requests = spec.buildRequests([bids[0]], bidderRequest); + let {data} = requests[0]; + let {imp} = data; + expect(imp).to.be.an('array').that.has.lengthOf(1); + expect(imp[0]).to.be.an('object'); + let bid = imp[0]; + expect(bid.id).to.equal('20917a54ee9858'); + expect(bid.banner).to.deep.equal({topframe: 0, format: [{w: 300, h: 250}]}); + expect(bid.ext).to.be.an('object'); + expect(bid.ext.r2b2).to.deep.equal({d: 'example.com', g: 'generic', p: '300x250', m: 1}); + }); + + it('should map type correctly', function () { + let result, bid; + let requestWithId = function(id) { + let b = bids[0]; + b.params.pid = id; + let passedBids = [b]; + bidderRequest.bids = passedBids; + return spec.buildRequests(passedBids, bidderRequest); + }; + + result = requestWithId('example.com/generic/300x250/mobile'); + bid = result[0].data.imp[0]; + expect(bid.ext.r2b2.m).to.be.a('number').that.is.equal(1); + + result = requestWithId('example.com/generic/300x250/desktop'); + bid = result[0].data.imp[0]; + expect(bid.ext.r2b2.m).to.be.a('number').that.is.equal(0); + + result = requestWithId('example.com/generic/300x250/1'); + bid = result[0].data.imp[0]; + expect(bid.ext.r2b2.m).to.be.a('number').that.is.equal(1); + + result = requestWithId('example.com/generic/300x250/0'); + bid = result[0].data.imp[0]; + expect(bid.ext.r2b2.m).to.be.a('number').that.is.equal(0); + + result = requestWithId('example.com/generic/300x250/m'); + bid = result[0].data.imp[0]; + expect(bid.ext.r2b2.m).to.be.a('number').that.is.equal(1); + + result = requestWithId('example.com/generic/300x250'); + bid = result[0].data.imp[0]; + expect(bid.ext.r2b2.m).to.be.a('number').that.is.equal(0); + }); + + it('should pass correct parameters for test ad', function () { + let testAdBid = bids[0]; + testAdBid.params = {pid: 'selfpromo'}; + let requests = spec.buildRequests([testAdBid], bidderRequest); + let {data} = requests[0]; + let {imp} = data; + expect(imp).to.be.an('array').that.has.lengthOf(1); + expect(imp[0]).to.be.an('object'); + let bid = imp[0]; + expect(bid.ext).to.be.an('object'); + expect(bid.ext.r2b2).to.deep.equal({d: 'test', g: 'test', p: 'selfpromo', m: 0, 'selfpromo': 1}); + }); + + it('should pass multiple bids', function () { + let requests = spec.buildRequests(bids, bidderRequest); + expect(requests).to.be.an('array').that.has.lengthOf(1); + let {data} = requests[0]; + let {imp} = data; + expect(imp).to.be.an('array').that.has.lengthOf(bids.length); + let bid1 = imp[0]; + expect(bid1.ext.r2b2).to.deep.equal({d: 'example.com', g: 'generic', p: '300x250', m: 1}); + let bid2 = imp[1]; + expect(bid2.ext.r2b2).to.deep.equal({d: 'example.com', g: 'generic', p: '300x600', m: 0}); + }); + + it('should set up internal variables', function () { + let requests = spec.buildRequests(bids, bidderRequest); + let bid1Id = bids[0].bidId; + let bid2Id = bids[1].bidId; + expect(r2b2.placementsToSync).to.be.an('array').that.has.lengthOf(2); + expect(r2b2.mappedParams).to.have.property(bid1Id); + expect(r2b2.mappedParams[bid1Id]).to.deep.equal({d: 'example.com', g: 'generic', p: '300x250', m: 1, pid: 'example.com/generic/300x250/1'}); + expect(r2b2.mappedParams).to.have.property(bid2Id); + expect(r2b2.mappedParams[bid2Id]).to.deep.equal({d: 'example.com', g: 'generic', p: '300x600', m: 0, pid: 'example.com/generic/300x600/0'}); + }); + + it('should pass gdpr properties', function () { + let requests = spec.buildRequests(bids, bidderRequest); + let {data} = requests[0]; + let {user, regs} = data; + expect(user).to.be.an('object').that.has.property('ext'); + expect(regs).to.be.an('object').that.has.property('ext'); + expect(user.ext.consent).to.equal('consent-string'); + expect(regs.ext.gdpr).to.equal(1); + }); + + it('should pass us privacy properties', function () { + let requests = spec.buildRequests(bids, bidderRequest); + let {data} = requests[0]; + let {regs} = data; + expect(regs).to.be.an('object').that.has.property('ext'); + expect(regs.ext.us_privacy).to.equal('1YYY'); + }); + + it('should pass supply chain', function () { + let requests = spec.buildRequests(bids, bidderRequest); + let {data} = requests[0]; + let {source} = data; + expect(source).to.be.an('object').that.has.property('ext'); + expect(source.ext.schain).to.deep.equal({ + complete: 1, + nodes: [ + {asi: 'example.com', hp: 1, sid: '00001'} + ], + ver: '1.0' + }) + }); + + it('should pass extended ids', function () { + let eidsArray = [ + { + source: 'adserver.org', + uids: [ + { + atype: 1, + ext: { + rtiPartner: 'TDID', + }, + id: 'TTD_ID_FROM_USER_ID_MODULE', + }, + ], + }, + { + source: 'pubcid.org', + uids: [ + { + atype: 1, + id: 'pubCommonId_FROM_USER_ID_MODULE', + }, + ], + }, + ]; + bids[0].userIdAsEids = eidsArray; + let requests = spec.buildRequests(bids, bidderRequest); + let request = requests[0]; + let eids = request.data.user.ext.eids; + + expect(eids).to.deep.equal(eidsArray); + }); + }); + + describe('interpretResponse', function () { + it('should respond with empty response when there are no bids', function () { + let result = spec.interpretResponse({ body: {} }, {}); + expect(result).to.be.an('array').that.has.lengthOf(0); + result = spec.interpretResponse({ body: { seatbid: [] } }, {}); + expect(result).to.be.an('array').that.has.lengthOf(0); + result = spec.interpretResponse({ body: { seatbid: [ {} ] } }, {}); + expect(result).to.be.an('array').that.has.lengthOf(0); + result = spec.interpretResponse({ body: { seatbid: [ { bids: [] } ] } }, {}); + expect(result).to.be.an('array').that.has.lengthOf(0); + }); + + it('should map params correctly', function () { + let result = spec.interpretResponse({ body: serverResponse }, requestForInterpretResponse); + expect(result).to.be.an('array').that.has.lengthOf(1); + let bid = result[0]; + expect(bid.requestId).to.equal(impId); + expect(bid.cpm).to.equal(price); + expect(bid.ad).to.equal(ad); + expect(bid.currency).to.equal('USD'); + expect(bid.mediaType).to.equal('banner'); + expect(bid.width).to.equal(300); + expect(bid.height).to.equal(250); + expect(bid.netRevenue).to.equal(true); + expect(bid.ttl).to.equal(360); + expect(bid.creativeId).to.equal(creativeId); + }); + + it('should set up renderer on bid', function () { + let result = spec.interpretResponse({ body: serverResponse }, requestForInterpretResponse); + expect(result).to.be.an('array').that.has.lengthOf(1); + let bid = result[0]; + expect(bid.renderer).to.be.an('object'); + expect(bid.renderer).to.have.property('render').that.is.a('function'); + expect(bid.renderer).to.have.property('url').that.is.a('string'); + }); + + it('should map ext params correctly', function() { + let dgpm = {something: 'something'}; + r2b2.mappedParams = {}; + r2b2.mappedParams[impId] = dgpm; + let result = spec.interpretResponse({ body: serverResponse }, requestForInterpretResponse); + expect(result).to.be.an('array').that.has.lengthOf(1); + let bid = result[0]; + expect(bid.ext).to.be.an('object'); + let { ext } = bid; + expect(ext.dgpm).to.deep.equal(dgpm); + expect(ext.cid).to.equal(cid); + expect(ext.cdid).to.equal(cdid); + expect(ext.adUnit).to.equal(unitCode); + expect(ext.mediaType).to.deep.equal({ + type: 'banner', + settings: { + chd: null, + width: 300, + height: 250, + ad: { + type: 'content', + data: ad + } + } + }); + }); + + it('should handle multiple bids', function() { + const impId2 = '123456'; + const price2 = 12; + const ad2 = 'gaeouho'; + const w2 = 300; + const h2 = 600; + let b = serverResponse.seatbid[0].bid[0]; + let b2 = Object.assign({}, b); + b2.impid = impId2; + b2.price = price2; + b2.adm = ad2; + b2.w = w2; + b2.h = h2; + serverResponse.seatbid[0].bid.push(b2); + requestForInterpretResponse.data.imp.push({id: impId2}); + let result = spec.interpretResponse({ body: serverResponse }, requestForInterpretResponse); + expect(result).to.be.an('array').that.has.lengthOf(2); + let firstBid = result[0]; + let secondBid = result[1]; + expect(firstBid.requestId).to.equal(impId); + expect(firstBid.ad).to.equal(ad); + expect(firstBid.cpm).to.equal(price); + expect(firstBid.width).to.equal(300); + expect(firstBid.height).to.equal(250); + expect(secondBid.requestId).to.equal(impId2); + expect(secondBid.ad).to.equal(ad2); + expect(secondBid.cpm).to.equal(price2); + expect(secondBid.width).to.equal(w2); + expect(secondBid.height).to.equal(h2); + }); + }); + + describe('getUserSyncs', function() { + const syncOptions = { + iframeEnabled: true, + pixelEnabled: true + }; + + it('should return an array with a sync for all bids', function() { + r2b2.placementsToSync = [id1Object, id2Object]; + const expectedEncodedIds = encodePlacementIds(r2b2.placementsToSync); + const syncs = spec.getUserSyncs(syncOptions); + expect(syncs).to.be.an('array').that.has.lengthOf(1); + const sync = syncs[0]; + expect(sync).to.be.an('object'); + expect(sync.type).to.equal('iframe'); + expect(sync.url).to.include(`?p=${expectedEncodedIds}`); + }); + + it('should return the sync and include gdpr and usp parameters in the url', function() { + r2b2.placementsToSync = [id1Object, id2Object]; + const syncs = spec.getUserSyncs(syncOptions, {}, gdprConsent, usPrivacyString); + const sync = syncs[0]; + expect(sync).to.be.an('object'); + expect(sync.url).to.include(`&gdpr=1`); + expect(sync.url).to.include(`&gdpr_consent=${gdprConsent.consentString}`); + expect(sync.url).to.include(`&us_privacy=${usPrivacyString}`); + }); + }); + + describe('events', function() { + beforeEach(function() { + time = sinon.useFakeTimers(fakeTime); + sinon.stub(utils, 'triggerPixel'); + r2b2.mappedParams = {}; + r2b2.mappedParams[bidId1] = id1Object; + r2b2.mappedParams[bidId2] = id2Object; + bidStub = { + adserverTargeting: { hb_bidder: bidder, hb_pb: '10.00', hb_size: '300x300' }, + cpm: 10, + currency: 'USD', + ext: { + dgpm: { d: 'r2b2.cz', g: 'generic', m: 1, p: '300x300', pid: 'r2b2.cz/generic/300x300/1' } + }, + params: [ { pid: 'r2b2.cz/generic/300x300/1' } ], + }; + }); + afterEach(function() { + utils.triggerPixel.restore(); + time.restore(); + }); + + describe('onBidWon', function () { + it('exists and is a function', () => { + expect(spec.onBidWon).to.exist.and.to.be.a('function'); + }); + it('should return nothing and trigger a pixel with passed url', function () { + bidStub.ext.events = { + onBidWon: bidWonUrl, + onSetTargeting: setTargetingUrl + }; + const response = spec.onBidWon(bidStub); + expect(response).to.be.an('undefined'); + expect(utils.triggerPixel.called).to.equal(true); + expect(utils.triggerPixel.callCount).to.equal(1); + expect(utils.triggerPixel.calledWithMatch(bidWonUrl)).to.equal(true); + }); + it('should not trigger a pixel if url is not available', function () { + bidStub.ext.events = null; + spec.onBidWon(bidStub); + expect(utils.triggerPixel.callCount).to.equal(0); + bidStub.ext.events = { + onBidWon: '', + onSetTargeting: '', + }; + spec.onBidWon(bidStub); + expect(utils.triggerPixel.callCount).to.equal(0); + }); + }); + + describe('onSetTargeting', function () { + it('exists and is a function', () => { + expect(spec.onSetTargeting).to.exist.and.to.be.a('function'); + }); + it('should return nothing and trigger a pixel with passed url', function () { + bidStub.ext.events = { + onBidWon: bidWonUrl, + onSetTargeting: setTargetingUrl + }; + const response = spec.onSetTargeting(bidStub); + expect(response).to.be.an('undefined'); + expect(utils.triggerPixel.called).to.equal(true); + expect(utils.triggerPixel.callCount).to.equal(1); + expect(utils.triggerPixel.calledWithMatch(setTargetingUrl)).to.equal(true); + }); + it('should not trigger a pixel if url is not available', function () { + bidStub.ext.events = null; + spec.onSetTargeting(bidStub); + expect(utils.triggerPixel.callCount).to.equal(0); + bidStub.ext.events = { + onBidWon: '', + onSetTargeting: '', + }; + spec.onSetTargeting(bidStub); + expect(utils.triggerPixel.callCount).to.equal(0); + }); + }); + + describe('onTimeout', function () { + it('exists and is a function', () => { + expect(spec.onTimeout).to.exist.and.to.be.a('function'); + }); + it('should return nothing and trigger a pixel', function () { + const bids = [bid1, bid2]; + const response = spec.onTimeout(bids); + expect(response).to.be.an('undefined'); + expect(utils.triggerPixel.callCount).to.equal(1); + }); + it('should not trigger a pixel if no bids available', function () { + const bids = []; + spec.onTimeout(bids); + expect(utils.triggerPixel.callCount).to.equal(0); + }); + it('should trigger a pixel with correct ids and a cache buster', function () { + const bids = [bid1, bidForeign1, bidForeign2, bid2, bidWithBadSetup]; + const expectedIds = [id1Object, id2Object]; + const expectedEncodedIds = encodePlacementIds(expectedIds); + spec.onTimeout(bids); + expect(utils.triggerPixel.callCount).to.equal(1); + const triggeredUrl = utils.triggerPixel.args[0][0]; + expect(triggeredUrl).to.include(`p=${expectedEncodedIds}`); + expect(triggeredUrl.match(cacheBusterRegex)).to.exist; + }); + }); + + describe('onBidderError', function () { + it('exists and is a function', () => { + expect(spec.onBidderError).to.exist.and.to.be.a('function'); + }); + it('should return nothing and trigger a pixel', function () { + const bidderRequest = { bids: [bid1, bid2] }; + const response = spec.onBidderError({ bidderRequest }); + expect(response).to.be.an('undefined') + expect(utils.triggerPixel.callCount).to.equal(1); + }); + it('should not trigger a pixel if no bids available', function () { + const bidderRequest = { bids: [] }; + spec.onBidderError({ bidderRequest }); + expect(utils.triggerPixel.callCount).to.equal(0); + }); + it('should call triggerEvent with correct ids and a cache buster', function () { + const bids = [bid1, bid2, bidWithBadSetup] + const bidderRequest = { bids }; + const expectedIds = [id1Object, id2Object]; + const expectedEncodedIds = encodePlacementIds(expectedIds); + spec.onBidderError({ bidderRequest }); + expect(utils.triggerPixel.callCount).to.equal(1); + const triggeredUrl = utils.triggerPixel.args[0][0]; + expect(triggeredUrl).to.include(`p=${expectedEncodedIds}`); + expect(triggeredUrl.match(cacheBusterRegex)).to.exist; + }); + }); + }); +}); diff --git a/test/spec/modules/rasBidAdapter_spec.js b/test/spec/modules/rasBidAdapter_spec.js index bfa72a2510e..719e15ad695 100644 --- a/test/spec/modules/rasBidAdapter_spec.js +++ b/test/spec/modules/rasBidAdapter_spec.js @@ -1,6 +1,7 @@ import { expect } from 'chai'; import { spec } from 'modules/rasBidAdapter.js'; import { newBidder } from 'src/adapters/bidderFactory.js'; +import {getAdUnitSizes} from '../../../src/utils'; const CSR_ENDPOINT = 'https://csr.onet.pl/4178463/csr-006/csr.json?nid=4178463&'; @@ -192,5 +193,56 @@ describe('rasBidAdapter', function () { const resp = spec.interpretResponse({ body: res }, {}); expect(resp).to.deep.equal([]); }); + + it('should generate auctionConfig when fledge is enabled', function () { + let bidRequest = { + method: 'GET', + url: 'https://example.com', + bidIds: [{ + slot: 'top', + bidId: '123', + network: 'testnetwork', + sizes: ['300x250'], + params: { + site: 'testsite', + area: 'testarea', + network: 'testnetwork' + }, + fledgeEnabled: true + }, + { + slot: 'top', + bidId: '456', + network: 'testnetwork', + sizes: ['300x250'], + params: { + site: 'testsite', + area: 'testarea', + network: 'testnetwork' + }, + fledgeEnabled: false + }] + }; + + let auctionConfigs = [{ + 'bidId': '123', + 'config': { + 'seller': 'https://csr.onet.pl', + 'decisionLogicUrl': 'https://csr.onet.pl/testnetwork/v1/protected-audience-api/decision-logic.js', + 'interestGroupBuyers': ['https://csr.onet.pl'], + 'auctionSignals': { + 'params': { + site: 'testsite', + area: 'testarea', + network: 'testnetwork' + }, + 'sizes': ['300x250'], + 'gctx': '1234567890' + } + } + }]; + const resp = spec.interpretResponse({body: {gctx: '1234567890'}}, bidRequest); + expect(resp).to.deep.equal({bids: [], fledgeAuctionConfigs: auctionConfigs}); + }); }); }); diff --git a/test/spec/modules/relaidoBidAdapter_spec.js b/test/spec/modules/relaidoBidAdapter_spec.js index 36d03c01138..f0d019913e8 100644 --- a/test/spec/modules/relaidoBidAdapter_spec.js +++ b/test/spec/modules/relaidoBidAdapter_spec.js @@ -7,9 +7,6 @@ import {getCoreStorageManager} from '../../../src/storageManager.js'; const UUID_KEY = 'relaido_uuid'; const relaido_uuid = 'hogehoge'; -const storage = getCoreStorageManager(); -storage.setCookie(UUID_KEY, relaido_uuid); - describe('RelaidoAdapter', function () { let bidRequest; let bidderRequest; @@ -18,6 +15,10 @@ describe('RelaidoAdapter', function () { let serverRequest; let generateUUIDStub; let triggerPixelStub; + before(() => { + const storage = getCoreStorageManager(); + storage.setCookie(UUID_KEY, relaido_uuid); + }); beforeEach(function () { generateUUIDStub = sinon.stub(utils, 'generateUUID').returns(relaido_uuid); @@ -238,6 +239,7 @@ describe('RelaidoAdapter', function () { const request = data.bids[0]; expect(bidRequests.method).to.equal('POST'); expect(bidRequests.url).to.equal('https://api.relaido.jp/bid/v1/sprebid'); + expect(data.canonical_url).to.equal('https://publisher.com/home'); expect(data.canonical_url_hash).to.equal('e6092f44a0044903ae3764126eedd6187c1d9f04'); expect(data.ref).to.equal(bidderRequest.refererInfo.page); expect(data.timeout_ms).to.equal(bidderRequest.timeout); @@ -316,6 +318,23 @@ describe('RelaidoAdapter', function () { expect(data.bids).to.have.lengthOf(1); expect(data.imuid).to.equal('i.tjHcK_7fTcqnbrS_YA2vaw'); }); + + it('should get userIdAsEids', function () { + const userIdAsEids = [ + { + source: 'hogehoge.com', + uids: { + atype: 1, + id: 'hugahuga' + } + } + ] + bidRequest.userIdAsEids = userIdAsEids + const bidRequests = spec.buildRequests([bidRequest], bidderRequest); + const data = JSON.parse(bidRequests.data); + expect(data.bids[0].userIdAsEids).to.have.lengthOf(1); + expect(data.bids[0].userIdAsEids[0].source).to.equal('hogehoge.com'); + }); }); describe('spec.interpretResponse', function () { @@ -324,6 +343,7 @@ describe('RelaidoAdapter', function () { expect(bidResponses).to.have.lengthOf(1); const response = bidResponses[0]; expect(response.requestId).to.equal(serverRequest.data.bids[0].bidId); + expect(response.placementId).to.equal(serverResponse.body.ads[0].placementId); expect(response.width).to.equal(serverRequest.data.bids[0].width); expect(response.height).to.equal(serverRequest.data.bids[0].height); expect(response.cpm).to.equal(serverResponse.body.ads[0].price); @@ -342,6 +362,7 @@ describe('RelaidoAdapter', function () { expect(bidResponses).to.have.lengthOf(1); const response = bidResponses[0]; expect(response.requestId).to.equal(serverRequest.data.bids[0].bidId); + expect(response.placementId).to.equal(serverResponse.body.ads[0].placementId); expect(response.width).to.equal(serverRequest.data.bids[0].width); expect(response.height).to.equal(serverRequest.data.bids[0].height); expect(response.cpm).to.equal(serverResponse.body.ads[0].price); @@ -359,6 +380,7 @@ describe('RelaidoAdapter', function () { expect(bidResponses).to.have.lengthOf(1); const response = bidResponses[0]; expect(response.requestId).to.equal(serverRequest.data.bids[0].bidId); + expect(response.placementId).to.equal(serverResponseBanner.body.ads[0].placementId); expect(response.cpm).to.equal(serverResponseBanner.body.ads[0].price); expect(response.currency).to.equal(serverResponseBanner.body.ads[0].currency); expect(response.creativeId).to.equal(serverResponseBanner.body.ads[0].creativeId); diff --git a/test/spec/modules/relayBidAdapter_spec.js b/test/spec/modules/relayBidAdapter_spec.js new file mode 100644 index 00000000000..38a3cfc9b97 --- /dev/null +++ b/test/spec/modules/relayBidAdapter_spec.js @@ -0,0 +1,131 @@ +import { expect } from 'chai'; +import { spec } from '../../../modules/relayBidAdapter.js'; +import { BANNER, VIDEO, NATIVE } from '../../../src/mediaTypes.js'; +import { getUniqueIdentifierStr } from '../../../src/utils.js'; + +const bidder = 'relay' +const endpoint = 'https://e.relay.bid/p/openrtb2'; + +describe('RelayBidAdapter', function () { + const bids = [ + { + bidId: getUniqueIdentifierStr(), + bidder, + mediaTypes: { [BANNER]: { sizes: [[300, 250]] } }, + params: { + accountId: 15000, + }, + ortb2Imp: { + ext: { + relay: { + bidders: { + bidderA: { + theId: 'abc123' + }, + bidderB: { + theId: 'xyz789' + } + } + } + } + } + }, + { + bidId: getUniqueIdentifierStr(), + bidder, + mediaTypes: { [BANNER]: { sizes: [[300, 250]] } }, + params: { + accountId: 30000, + }, + ortb2Imp: { + ext: { + relay: { + bidders: { + bidderA: { + theId: 'def456' + }, + bidderB: { + theId: 'uvw101112' + } + } + } + } + } + } + ]; + + const invalidBid = { + bidId: getUniqueIdentifierStr(), + bidder: bidder, + mediaTypes: { + [BANNER]: { + sizes: [[300, 250]] + } + }, + params: {} + } + + const bidderRequest = {}; + + describe('isBidRequestValid', function () { + it('Valid bids have a params.accountId.', function () { + expect(spec.isBidRequestValid(bids[0])).to.be.true; + }); + it('Invalid bids do not have a params.accountId.', function () { + expect(spec.isBidRequestValid(invalidBid)).to.be.false; + }); + }); + + describe('buildRequests', function () { + const requests = spec.buildRequests(bids, bidderRequest); + const firstRequest = requests[0]; + const secondRequest = requests[1]; + + it('Creates two requests', function () { + expect(firstRequest).to.exist; + expect(firstRequest.data).to.exist; + expect(firstRequest.method).to.exist; + expect(firstRequest.method).to.equal('POST'); + expect(firstRequest.url).to.exist; + expect(firstRequest.url).to.equal(`${endpoint}?a=15000&pb=1&pbv=v8.1.0`); + + expect(secondRequest).to.exist; + expect(secondRequest.data).to.exist; + expect(secondRequest.method).to.exist; + expect(secondRequest.method).to.equal('POST'); + expect(secondRequest.url).to.exist; + expect(secondRequest.url).to.equal(`${endpoint}?a=30000&pb=1&pbv=v8.1.0`); + }); + + it('Does not generate requests when there are no bids', function () { + const request = spec.buildRequests([], bidderRequest); + expect(request).to.be.an('array').that.is.empty; + }); + }); + + describe('getUserSyncs', function () { + it('Uses Prebid consent values if incoming sync URLs lack consent.', function () { + const syncOpts = { + iframeEnabled: true, + pixelEnabled: true + }; + const test_gdpr_applies = true; + const test_gdpr_consent_str = 'TEST_GDPR_CONSENT_STRING'; + const responses = [{ + body: { + ext: { + user_syncs: [ + { type: 'image', url: 'https://image-example.com' }, + { type: 'iframe', url: 'https://iframe-example.com' } + ] + } + } + }]; + + const sync_urls = spec.getUserSyncs(syncOpts, responses, { gdprApplies: test_gdpr_applies, consentString: test_gdpr_consent_str }); + expect(sync_urls).to.be.an('array'); + expect(sync_urls[0].url).to.equal('https://image-example.com/?gdpr=1&gdpr_consent=TEST_GDPR_CONSENT_STRING'); + expect(sync_urls[1].url).to.equal('https://iframe-example.com/?gdpr=1&gdpr_consent=TEST_GDPR_CONSENT_STRING'); + }); + }); +}); diff --git a/test/spec/modules/relevantdigitalBidAdapter_spec.js b/test/spec/modules/relevantdigitalBidAdapter_spec.js index b2a5495b3cb..0e21453c8ba 100644 --- a/test/spec/modules/relevantdigitalBidAdapter_spec.js +++ b/test/spec/modules/relevantdigitalBidAdapter_spec.js @@ -1,5 +1,10 @@ import {spec, resetBidderConfigs} from 'modules/relevantdigitalBidAdapter.js'; import { parseUrl, deepClone } from 'src/utils.js'; +import { config } from 'src/config.js'; +import CONSTANTS from 'src/constants.json'; + +import adapterManager, { +} from 'src/adapterManager.js'; const expect = require('chai').expect; @@ -9,14 +14,29 @@ const ACCOUNT_ID = 'example_account_id'; const TEST_DOMAIN = 'example.com'; const TEST_PAGE = `https://${TEST_DOMAIN}/page.html`; -const BID_REQUEST = -{ - 'bidder': 'relevantdigital', +const CONFIG = { + enabled: true, + endpoint: CONSTANTS.S2S.DEFAULT_ENDPOINT, + timeout: 1000, + maxBids: 1, + adapter: 'prebidServer', + bidders: ['relevantdigital'], + accountId: 'abc' +}; + +const ADUNIT_CODE = '/19968336/header-bid-tag-0'; + +const BID_PARAMS = { 'params': { 'placementId': PLACEMENT_ID, 'accountId': ACCOUNT_ID, - 'pbsHost': PBS_HOST, - }, + 'pbsHost': PBS_HOST + } +}; + +const BID_REQUEST = { + 'bidder': 'relevantdigital', + ...BID_PARAMS, 'ortb2Imp': { 'ext': { 'tid': 'e13391ea-00f3-495d-99a6-d937990d73a9' @@ -32,7 +52,7 @@ const BID_REQUEST = ] } }, - 'adUnitCode': '/19968336/header-bid-tag-0', + 'adUnitCode': ADUNIT_CODE, 'transactionId': 'e13391ea-00f3-495d-99a6-d937990d73a9', 'sizes': [ [ @@ -292,4 +312,64 @@ describe('Relevant Digital Bid Adaper', function () { expect(allSyncs).to.deep.equal(expectedResult) }); }); + describe('transformBidParams', function () { + beforeEach(() => { + config.setConfig({ + s2sConfig: CONFIG, + }); + }); + afterEach(() => { + config.resetConfig(); + }); + + const adUnit = (params) => ({ + code: ADUNIT_CODE, + bids: [ + { + bidder: 'relevantdigital', + adUnitCode: ADUNIT_CODE, + params, + } + ] + }); + + const request = (params) => adapterManager.makeBidRequests([adUnit(params)], 123, 'auction-id', 123, [], {})[0]; + + it('transforms adunit bid params and config params correctly', function () { + config.setConfig({ + relevantdigital: { + pbsHost: PBS_HOST, + accountId: ACCOUNT_ID, + }, + }); + const adUnitParams = { placementId: PLACEMENT_ID }; + const expextedTransformedBidParams = { + ...BID_PARAMS.params, pbsHost: `https://${BID_PARAMS.params.pbsHost}`, 'pbsBufferMs': 250 + }; + expect(spec.transformBidParams(adUnitParams, null, null, [request(adUnitParams)])).to.deep.equal(expextedTransformedBidParams); + }); + it('transforms adunit bid params correctly', function () { + const adUnitParams = { ...BID_PARAMS.params, pbsHost: 'host.relevant-digital.com', pbsBufferMs: 500 }; + const expextedTransformedBidParams = { + ...BID_PARAMS.params, pbsHost: 'host.relevant-digital.com', pbsBufferMs: 500 + }; + expect(spec.transformBidParams(adUnitParams, null, null, [request(adUnitParams)])).to.deep.equal(expextedTransformedBidParams); + }); + it('transforms adunit bid params correctly', function () { + const adUnitParams = { ...BID_PARAMS.params, pbsHost: 'host.relevant-digital.com', pbsBufferMs: 500 }; + const expextedTransformedBidParams = { + ...BID_PARAMS.params, pbsHost: 'host.relevant-digital.com', pbsBufferMs: 500 + }; + expect(spec.transformBidParams(adUnitParams, null, null, [request(adUnitParams)])).to.deep.equal(expextedTransformedBidParams); + }); + it('does not transform bid params if placementId is missing', function () { + const adUnitParams = { ...BID_PARAMS.params, placementId: null }; + expect(spec.transformBidParams(adUnitParams, null, null, [request(adUnitParams)])).to.equal(undefined); + }); + it('does not transform bid params s2s config is missing', function () { + config.resetConfig(); + const adUnitParams = BID_PARAMS.params; + expect(spec.transformBidParams(adUnitParams, null, null, [request(adUnitParams)])).to.equal(undefined); + }); + }) }); diff --git a/test/spec/modules/richaudienceBidAdapter_spec.js b/test/spec/modules/richaudienceBidAdapter_spec.js index ea45ff7e0b0..20c60ca328a 100644 --- a/test/spec/modules/richaudienceBidAdapter_spec.js +++ b/test/spec/modules/richaudienceBidAdapter_spec.js @@ -4,6 +4,8 @@ import { spec } from 'modules/richaudienceBidAdapter.js'; import {config} from 'src/config.js'; +import * as utils from 'src/utils.js'; +import sinon from 'sinon'; describe('Richaudience adapter tests', function () { var DEFAULT_PARAMS_NEW_SIZES = [{ @@ -64,6 +66,30 @@ describe('Richaudience adapter tests', function () { user: {} }]; + var DEFAULT_PARAMS_VIDEO_TIMEOUT = [{ + adUnitCode: 'test-div', + bidId: '2c7c8e9c900244', + mediaTypes: { + video: { + context: 'instream', + playerSize: [640, 480], + mimes: ['video/mp4'] + } + }, + bidder: 'richaudience', + params: [{ + bidfloor: 0.5, + pid: 'ADb1f40rmi', + supplyType: 'site' + }], + timeout: 3000, + auctionId: '0cb3144c-d084-4686-b0d6-f5dbe917c563', + bidRequestsCount: 1, + bidderRequestId: '1858b7382993ca', + transactionId: '29df2112-348b-4961-8863-1b33684d95e6', + user: {} + }] + var DEFAULT_PARAMS_VIDEO_IN = [{ adUnitCode: 'test-div', bidId: '2c7c8e9c900244', @@ -879,6 +905,24 @@ describe('Richaudience adapter tests', function () { expect(requestContent).to.have.property('gpid').and.to.equal('/19968336/header-bid-tag-1#example-2'); }) + describe('onTimeout', function () { + beforeEach(function() { + sinon.stub(utils, 'triggerPixel'); + }); + + afterEach(function() { + utils.triggerPixel.restore(); + }); + it('onTimeout exist as a function', () => { + expect(spec.onTimeout).to.exist.and.to.be.a('function'); + }); + it('should send timeout', function () { + spec.onTimeout(DEFAULT_PARAMS_VIDEO_TIMEOUT); + expect(utils.triggerPixel.called).to.equal(true); + expect(utils.triggerPixel.firstCall.args[0]).to.equal('https://s.richaudience.com/err/?ec=6&ev=3000&pla=ADb1f40rmi&int=PREBID&pltfm=&node=&dm=localhost:9876'); + }); + }); + describe('userSync', function () { it('Verifies user syncs iframe include', function () { config.setConfig({ diff --git a/test/spec/modules/riseBidAdapter_spec.js b/test/spec/modules/riseBidAdapter_spec.js index eed8d74f271..28bd123cb5d 100644 --- a/test/spec/modules/riseBidAdapter_spec.js +++ b/test/spec/modules/riseBidAdapter_spec.js @@ -22,6 +22,12 @@ describe('riseAdapter', function () { }); }); + describe('bid adapter', function () { + it('should have aliases', function () { + expect(spec.aliases).to.be.an('array').that.is.not.empty; + }); + }); + describe('isBidRequestValid', function () { const bid = { 'bidder': spec.code, @@ -53,7 +59,7 @@ describe('riseAdapter', function () { 'adUnitCode': 'adunit-code', 'sizes': [[640, 480]], 'params': { - 'org': 'jdye8weeyirk00000001' + 'org': 'jdye8weeyirk00000001', }, 'bidId': '299ffc8cca0b87', 'loop': 1, @@ -195,6 +201,16 @@ describe('riseAdapter', function () { expect(request.data.bids[1].mediaType).to.equal(BANNER) }); + it('should send the correct currency in bid request', function () { + const bid = utils.deepClone(bidRequests[0]); + bid.params = { + 'currency': 'EUR' + }; + const expectedCurrency = bid.params.currency; + const request = spec.buildRequests([bid], bidderRequest); + expect(request.data.bids[0].currency).to.equal(expectedCurrency); + }); + it('should respect syncEnabled option', function() { config.setConfig({ userSync: { diff --git a/test/spec/modules/rtbhouseBidAdapter_spec.js b/test/spec/modules/rtbhouseBidAdapter_spec.js index 78c700f3804..0b944dcb077 100644 --- a/test/spec/modules/rtbhouseBidAdapter_spec.js +++ b/test/spec/modules/rtbhouseBidAdapter_spec.js @@ -318,6 +318,41 @@ describe('RTBHouseAdapter', () => { expect(request.method).to.equal('POST'); }); + it('sets default fledgeConfig object values when none available from config', function () { + let bidRequest = Object.assign([], bidRequests); + delete bidRequest[0].params.test; + + config.setConfig({ fledgeConfig: false }); + const request = spec.buildRequests(bidRequest, { ...bidderRequest, fledgeEnabled: true }); + const data = JSON.parse(request.data); + expect(data.ext).to.exist.and.to.be.a('object'); + expect(data.ext.fledge_config).to.exist.and.to.be.a('object'); + expect(data.ext.fledge_config).to.contain.keys('seller', 'decisionLogicUrl', 'sellerTimeout'); + expect(data.ext.fledge_config.seller).to.equal('https://fledge-ssp.creativecdn.com'); + expect(data.ext.fledge_config.decisionLogicUrl).to.equal('https://fledge-ssp.creativecdn.com/component-seller-prebid.js'); + expect(data.ext.fledge_config.sellerTimeout).to.equal(500); + }); + + it('sets a fledgeConfig object values when available from config', function () { + let bidRequest = Object.assign([], bidRequests); + delete bidRequest[0].params.test; + + config.setConfig({ + fledgeConfig: { + seller: 'https://sellers.domain', + decisionLogicUrl: 'https://sellers.domain/decision.url' + } + }); + const request = spec.buildRequests(bidRequest, { ...bidderRequest, fledgeEnabled: true }); + const data = JSON.parse(request.data); + expect(data.ext).to.exist.and.to.be.a('object'); + expect(data.ext.fledge_config).to.exist.and.to.be.a('object'); + expect(data.ext.fledge_config).to.contain.keys('seller', 'decisionLogicUrl'); + expect(data.ext.fledge_config.seller).to.equal('https://sellers.domain'); + expect(data.ext.fledge_config.decisionLogicUrl).to.equal('https://sellers.domain/decision.url'); + expect(data.ext.fledge_config.sellerTimeout).to.not.exist; + }); + it('when FLEDGE is disabled, should not send imp.ext.ae', function () { let bidRequest = Object.assign([], bidRequests); delete bidRequest[0].params.test; diff --git a/test/spec/modules/rubiconBidAdapter_spec.js b/test/spec/modules/rubiconBidAdapter_spec.js index d9c3555bf03..f0e33ce940e 100644 --- a/test/spec/modules/rubiconBidAdapter_spec.js +++ b/test/spec/modules/rubiconBidAdapter_spec.js @@ -270,7 +270,81 @@ describe('the rubicon adapter', function () { }], criteoId: '1111', }; - bid.userIdAsEids = createEidsArray(bid.userId); + bid.userIdAsEids = [ + { + 'source': 'liveintent.com', + 'uids': [ + { + 'id': '0000-1111-2222-3333', + 'atype': 3 + } + ], + 'ext': { + 'segments': [ + 'segA', + 'segB' + ] + } + }, + { + 'source': 'liveramp.com', + 'uids': [ + { + 'id': '1111-2222-3333-4444', + 'atype': 3 + } + ] + }, + { + 'source': 'adserver.org', + 'uids': [ + { + 'id': '3000', + 'atype': 1, + 'ext': { + 'rtiPartner': 'TDID' + } + } + ] + }, + { + 'source': 'pubcid.org', + 'uids': [ + { + 'id': '4000', + 'atype': 1 + } + ] + }, + { + 'source': 'example.com', + 'uids': [ + { + 'id': '333333', + 'ext': { + 'stype': 'ppuid' + } + } + ] + }, + { + 'source': 'id-partner.com', + 'uids': [ + { + 'id': '4444444' + } + ] + }, + { + 'source': 'criteo.com', + 'uids': [ + { + 'id': '1111', + 'atype': 1 + } + ] + } + ]; return bidderRequest; } @@ -622,6 +696,16 @@ describe('the rubicon adapter', function () { expect(data['p_pos']).to.equal('atf;;btf;;'); }); + it('should correctly send cdep signal when requested', () => { + var badposRequest = utils.deepClone(bidderRequest); + badposRequest.bids[0].ortb2 = {device: {ext: {cdep: 3}}}; + + let [request] = spec.buildRequests(badposRequest.bids, badposRequest); + let data = parseQuery(request.data); + + expect(data['o_cdep']).to.equal('3'); + }); + it('ad engine query params should be ordered correctly', function () { sandbox.stub(Math, 'random').callsFake(() => 0.1); let [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); @@ -825,6 +909,29 @@ describe('the rubicon adapter', function () { }); }); + describe('GPP Consent', function () { + it('should send gpp information if bidderRequest has a value for gppConsent', function () { + bidderRequest.gppConsent = { + gppString: 'consent', + applicableSections: 2 + }; + let [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); + let data = parseQuery(request.data); + delete bidderRequest.gppConsent; + + expect(data['gpp']).to.equal('consent'); + expect(data['gpp_sid']).to.equal('2'); + }); + + it('should not send gpp information if bidderRequest does not have a value for gppConsent', function () { + let [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); + let data = parseQuery(request.data); + + expect(data['gpp']).to.equal(undefined); + expect(data['gpp_sid']).to.equal(undefined); + }); + }); + describe('first party data', function () { it('should not have any tg_v or tg_i params if all are undefined', function () { let params = { @@ -1132,6 +1239,30 @@ describe('the rubicon adapter', function () { }); }); + it('should still use single request if other rubicon configs are set after', function () { + // set single request to true + config.setConfig({ rubicon: { singleRequest: true } }); + + // execute some other rubicon setConfig + config.setConfig({ rubicon: { netRevenue: true } }); + + const bidCopy = utils.deepClone(bidderRequest.bids[0]); + bidderRequest.bids.push(bidCopy); + bidderRequest.bids.push(bidCopy); + bidderRequest.bids.push(bidCopy); + + let serverRequests = spec.buildRequests(bidderRequest.bids, bidderRequest); + + // should have 1 request only + expect(serverRequests).that.is.an('array').of.length(1); + + // get the built query + let data = parseQuery(serverRequests[0].data); + + // num slots should be 4 + expect(data.slots).to.equal('4'); + }); + it('should not group bid requests if singleRequest does not equal true', function () { config.setConfig({rubicon: {singleRequest: false}}); @@ -1201,7 +1332,20 @@ describe('the rubicon adapter', function () { clonedBid.userId = { tdid: 'abcd-efgh-ijkl-mnop-1234' }; - clonedBid.userIdAsEids = createEidsArray(clonedBid.userId); + clonedBid.userIdAsEids = [ + { + 'source': 'adserver.org', + 'uids': [ + { + 'id': 'abcd-efgh-ijkl-mnop-1234', + 'atype': 1, + 'ext': { + 'rtiPartner': 'TDID' + } + } + ] + } + ]; let [request] = spec.buildRequests([clonedBid], bidderRequest); let data = parseQuery(request.data); @@ -1218,7 +1362,23 @@ describe('the rubicon adapter', function () { segments: ['segA', 'segB'] } }; - clonedBid.userIdAsEids = createEidsArray(clonedBid.userId); + clonedBid.userIdAsEids = [ + { + 'source': 'liveintent.com', + 'uids': [ + { + 'id': '0000-1111-2222-3333', + 'atype': 3 + } + ], + 'ext': { + 'segments': [ + 'segA', + 'segB' + ] + } + } + ]; let [request] = spec.buildRequests([clonedBid], bidderRequest); let data = parseQuery(request.data); @@ -1235,7 +1395,23 @@ describe('the rubicon adapter', function () { segments: ['segD', 'segE'] } }; - clonedBid.userIdAsEids = createEidsArray(clonedBid.userId); + clonedBid.userIdAsEids = [ + { + 'source': 'liveintent.com', + 'uids': [ + { + 'id': '1111-2222-3333-4444', + 'atype': 3 + } + ], + 'ext': { + 'segments': [ + 'segD', + 'segE' + ] + } + } + ] let [request] = spec.buildRequests([clonedBid], bidderRequest); const unescapedData = unescape(request.data); @@ -1250,7 +1426,17 @@ describe('the rubicon adapter', function () { clonedBid.userId = { idl_env: '1111-2222-3333-4444' }; - clonedBid.userIdAsEids = createEidsArray(clonedBid.userId); + clonedBid.userIdAsEids = [ + { + 'source': 'liveramp.com', + 'uids': [ + { + 'id': '1111-2222-3333-4444', + 'atype': 3 + } + ] + } + ] let [request] = spec.buildRequests([clonedBid], bidderRequest); let data = parseQuery(request.data); @@ -1264,7 +1450,17 @@ describe('the rubicon adapter', function () { clonedBid.userId = { pubcid: '1111' }; - clonedBid.userIdAsEids = createEidsArray(clonedBid.userId); + clonedBid.userIdAsEids = [ + { + 'source': 'pubcid.org', + 'uids': [ + { + 'id': '1111', + 'atype': 1 + } + ] + } + ] let [request] = spec.buildRequests([clonedBid], bidderRequest); let data = parseQuery(request.data); @@ -1278,7 +1474,17 @@ describe('the rubicon adapter', function () { clonedBid.userId = { criteoId: '1111' }; - clonedBid.userIdAsEids = createEidsArray(clonedBid.userId); + clonedBid.userIdAsEids = [ + { + 'source': 'criteo.com', + 'uids': [ + { + 'id': '1111', + 'atype': 1 + } + ] + } + ] let [request] = spec.buildRequests([clonedBid], bidderRequest); let data = parseQuery(request.data); @@ -1305,7 +1511,27 @@ describe('the rubicon adapter', function () { }] }] }; - clonedBid.userIdAsEids = createEidsArray(clonedBid.userId); + clonedBid.userIdAsEids = [ + { + 'source': 'example.com', + 'uids': [ + { + 'id': '11111', + 'ext': { + 'stype': 'ppuid' + } + } + ] + }, + { + 'source': 'id-partner.com', + 'uids': [ + { + 'id': '222222' + } + ] + } + ]; let [request] = spec.buildRequests([clonedBid], bidderRequest); let data = parseQuery(request.data); @@ -1324,7 +1550,20 @@ describe('the rubicon adapter', function () { } } }; - clonedBid.userIdAsEids = createEidsArray(clonedBid.userId); + clonedBid.userIdAsEids = [ + { + 'source': 'id5-sync.com', + 'uids': [ + { + 'id': '11111', + 'atype': 1, + 'ext': { + 'linkType': '22222' + } + } + ] + } + ]; let [request] = spec.buildRequests([clonedBid], bidderRequest); let data = parseQuery(request.data); @@ -1348,6 +1587,22 @@ describe('the rubicon adapter', function () { expect(data['eid_catchall']).to.equal('11111^2'); }); + + it('should send rubiconproject special case', function () { + const clonedBid = utils.deepClone(bidderRequest.bids[0]); + // Hardcoding userIdAsEids since createEidsArray returns empty array if source not found in eids.js + clonedBid.userIdAsEids = [{ + source: 'rubiconproject.com', + uids: [{ + id: 'some-cool-id', + atype: 3 + }] + }] + let [request] = spec.buildRequests([clonedBid], bidderRequest); + let data = parseQuery(request.data); + + expect(data['eid_rubiconproject.com']).to.equal('some-cool-id'); + }); }); describe('Config user.id support', function () { @@ -1563,6 +1818,126 @@ describe('the rubicon adapter', function () { expect(data['tg_i.dfp_ad_unit_code']).to.equal('/a/b/c'); }); }); + + describe('client hints', function () { + let standardSuaObject; + beforeEach(function () { + standardSuaObject = { + source: 2, + platform: { + brand: 'macOS', + version: [ + '12', + '6', + '0' + ] + }, + browsers: [ + { + brand: 'Not.A/Brand', + version: [ + '8', + '0', + '0', + '0' + ] + }, + { + brand: 'Chromium', + version: [ + '114', + '0', + '5735', + '198' + ] + }, + { + brand: 'Google Chrome', + version: [ + '114', + '0', + '5735', + '198' + ] + } + ], + mobile: 0, + model: '', + bitness: '64', + architecture: 'x86' + } + }); + it('should send m_ch_* params if ortb2.device.sua object is there', function () { + let bidRequestSua = utils.deepClone(bidderRequest); + bidRequestSua.bids[0].ortb2 = { device: { sua: standardSuaObject } }; + + // How should fastlane query be constructed with default SUA + let expectedValues = { + m_ch_arch: 'x86', + m_ch_bitness: '64', + m_ch_ua: `"Not.A/Brand"|v="8","Chromium"|v="114","Google Chrome"|v="114"`, + m_ch_full_ver: `"Not.A/Brand"|v="8.0.0.0","Chromium"|v="114.0.5735.198","Google Chrome"|v="114.0.5735.198"`, + m_ch_mobile: '?0', + m_ch_platform: 'macOS', + m_ch_platform_ver: '12.6.0' + } + + // Build Fastlane call + let [request] = spec.buildRequests(bidRequestSua.bids, bidRequestSua); + let data = parseQuery(request.data); + + // Loop through expected values and if they do not match push an error + const errors = Object.entries(expectedValues).reduce((accum, [key, val]) => { + if (data[key] !== val) accum.push(`${key} - expect: ${val} - got: ${data[key]}`) + return accum; + }, []); + + // should be no errors + expect(errors).to.deep.equal([]); + }); + it('should not send invalid values for m_ch_*', function () { + let bidRequestSua = utils.deepClone(bidderRequest); + + // Alter input SUA object + // send model + standardSuaObject.model = 'Suface Duo'; + // send mobile = 1 + standardSuaObject.mobile = 1; + + // make browsers not an array + standardSuaObject.browsers = 'My Browser'; + + // make platform not have version + delete standardSuaObject.platform.version; + + // delete architecture + delete standardSuaObject.architecture; + + // add SUA to bid + bidRequestSua.bids[0].ortb2 = { device: { sua: standardSuaObject } }; + + // Build Fastlane request + let [request] = spec.buildRequests(bidRequestSua.bids, bidRequestSua); + let data = parseQuery(request.data); + + // should show new names + expect(data.m_ch_model).to.equal('Suface Duo'); + expect(data.m_ch_mobile).to.equal('?1'); + + // should still send platform + expect(data.m_ch_platform).to.equal('macOS'); + + // platform version not sent + expect(data).to.not.haveOwnProperty('m_ch_platform_ver'); + + // both ua and full_ver not sent because browsers not array + expect(data).to.not.haveOwnProperty('m_ch_ua'); + expect(data).to.not.haveOwnProperty('m_ch_full_ver'); + + // arch not sent + expect(data).to.not.haveOwnProperty('m_ch_arch'); + }); + }); }); if (FEATURES.VIDEO) { @@ -2006,16 +2381,6 @@ describe('the rubicon adapter', function () { bidderRequest = createVideoBidderRequest(); delete bidderRequest.bids[0].mediaTypes.video.linearity; expect(spec.isBidRequestValid(bidderRequest.bids[0])).to.equal(false); - - // change api to an string, no good - bidderRequest = createVideoBidderRequest(); - bidderRequest.bids[0].mediaTypes.video.api = 'string'; - expect(spec.isBidRequestValid(bidderRequest.bids[0])).to.equal(false); - - // delete api, no good - bidderRequest = 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 () { @@ -2344,6 +2709,15 @@ describe('the rubicon adapter', function () { const slotParams = spec.createSlotParams(bidderRequest.bids[0], bidderRequest); expect(slotParams.kw).to.equal('a,b,c'); }); + + it('should pass along o_ae param when fledge is enabled', () => { + const localBidRequest = Object.assign({}, bidderRequest.bids[0]); + localBidRequest.ortb2Imp.ext.ae = true; + + const slotParams = spec.createSlotParams(localBidRequest, bidderRequest); + + expect(slotParams['o_ae']).to.equal(1) + }); }); describe('classifiedAsVideo', function () { @@ -2407,6 +2781,18 @@ describe('the rubicon adapter', function () { expect(request.data.imp).to.have.nested.property('[0].native'); }); + it('should not break if position is set and no video MT', function () { + const bidReq = addNativeToBidRequest(bidderRequest); + delete bidReq.bids[0].mediaTypes.banner; + bidReq.bids[0].params = { + position: 'atf' + } + let [request] = spec.buildRequests(bidReq.bids, bidReq); + expect(request.method).to.equal('POST'); + expect(request.url).to.equal('https://prebid-server.rubiconproject.com/openrtb2/auction'); + expect(request.data.imp).to.have.nested.property('[0].native'); + }); + describe('that contains also a banner mediaType', function () { it('should send the banner to fastlane BUT NOT the native bid because missing params.video', function() { const bidReq = addNativeToBidRequest(bidderRequest); @@ -2458,6 +2844,35 @@ describe('the rubicon adapter', function () { expect(pbsRequest.data.imp).to.have.nested.property('[0].native'); expect(fastlanteRequest.url).to.equal('https://fastlane.rubiconproject.com/a/api/fastlane.json'); }); + + it('should include multiformat data in the pbs request', () => { + const bidReq = addNativeToBidRequest(bidderRequest); + // add second mediaType + bidReq.bids[0].mediaTypes = { + ...bidReq.bids[0].mediaTypes, + banner: { + sizes: [[300, 250]] + } + }; + bidReq.bids[0].params.bidonmultiformat = true; + let [pbsRequest, fastlanteRequest] = spec.buildRequests(bidReq.bids, bidReq); + expect(pbsRequest.data.imp[0].ext.prebid.bidder.rubicon.formats).to.deep.equal(['native', 'banner']); + }); + + it('should include multiformat data in the fastlane request', () => { + const bidReq = addNativeToBidRequest(bidderRequest); + // add second mediaType + bidReq.bids[0].mediaTypes = { + ...bidReq.bids[0].mediaTypes, + banner: { + sizes: [[300, 250]] + } + }; + bidReq.bids[0].params.bidonmultiformat = true; + let [pbsRequest, fastlanteRequest] = spec.buildRequests(bidReq.bids, bidReq); + let formatsIncluded = fastlanteRequest.data.indexOf('formats=native%2Cbanner') !== -1; + expect(formatsIncluded).to.equal(true); + }); }); describe('with bidonmultiformat === false', () => { it('should send only banner request because there\'s no params.video', () => { @@ -3023,6 +3438,43 @@ describe('the rubicon adapter', function () { expect(bids).to.be.lengthOf(0); }); + it('Should support recieving an auctionConfig and pass it along to Prebid', function () { + let response = { + 'status': 'ok', + 'account_id': 14062, + 'site_id': 70608, + 'zone_id': 530022, + 'size_id': 15, + 'alt_size_ids': [ + 43 + ], + 'tracking': '', + 'inventory': {}, + 'ads': [{ + 'status': 'ok', + 'cpm': 0, + 'size_id': 15 + }], + 'component_auction_config': [{ + 'random': 'value', + 'bidId': '5432' + }, + { + 'random': 'string', + 'bidId': '6789' + }] + }; + + let {bids, fledgeAuctionConfigs} = spec.interpretResponse({body: response}, { + bidRequest: bidderRequest.bids[0] + }); + + expect(bids).to.be.lengthOf(1); + expect(fledgeAuctionConfigs[0].bidId).to.equal('5432'); + expect(fledgeAuctionConfigs[0].config.random).to.equal('value'); + expect(fledgeAuctionConfigs[1].bidId).to.equal('6789'); + }); + it('should handle an error', function () { let response = { 'status': 'ok', @@ -3653,6 +4105,24 @@ describe('the rubicon adapter', function () { type: 'iframe', url: `${emilyUrl}?gdpr=1&gdpr_consent=foo&us_privacy=1NYN` }); }); + + it('should pass gpp params when gppConsent is present', function () { + expect(spec.getUserSyncs({iframeEnabled: true}, {}, {}, undefined, { + gppString: 'foo', + applicableSections: [2] + })).to.deep.equal({ + type: 'iframe', url: `${emilyUrl}?gpp=foo&gpp_sid=2` + }); + }); + + it('should pass multiple sid\'s when multiple are present', function () { + expect(spec.getUserSyncs({iframeEnabled: true}, {}, {}, undefined, { + gppString: 'foo', + applicableSections: [2, 5] + })).to.deep.equal({ + type: 'iframe', url: `${emilyUrl}?gpp=foo&gpp_sid=2,5` + }); + }); }); describe('get price granularity', function () { diff --git a/test/spec/modules/seedtagBidAdapter_spec.js b/test/spec/modules/seedtagBidAdapter_spec.js index fb666e89f73..516c5ec933a 100644 --- a/test/spec/modules/seedtagBidAdapter_spec.js +++ b/test/spec/modules/seedtagBidAdapter_spec.js @@ -2,10 +2,24 @@ import { expect } from 'chai'; import { spec, getTimeoutUrl } from 'modules/seedtagBidAdapter.js'; import * as utils from 'src/utils.js'; import { config } from '../../../src/config.js'; +import * as mockGpt from 'test/spec/integration/faker/googletag.js'; const PUBLISHER_ID = '0000-0000-01'; const ADUNIT_ID = '000000'; +const adUnitCode = '/19968336/header-bid-tag-0' + +// create a default adunit +const slot = document.createElement('div'); +slot.id = adUnitCode; +slot.style.width = '300px' +slot.style.height = '250px' +slot.style.position = 'absolute' +slot.style.top = '10px' +slot.style.left = '20px' + +document.body.appendChild(slot); + function getSlotConfigs(mediaTypes, params) { return { params: params, @@ -25,7 +39,7 @@ function getSlotConfigs(mediaTypes, params) { tid: 'd704d006-0d6e-4a09-ad6c-179e7e758096', } }, - adUnitCode: 'adunit-code', + adUnitCode: adUnitCode, }; } @@ -46,6 +60,13 @@ const createBannerSlotConfig = (placement, mediatypes) => { }; describe('Seedtag Adapter', function () { + beforeEach(function () { + mockGpt.reset(); + }); + + afterEach(function () { + mockGpt.enable(); + }); describe('isBidRequestValid method', function () { describe('returns true', function () { describe('when banner slot config has all mandatory params', () => { @@ -277,7 +298,7 @@ describe('Seedtag Adapter', function () { expect(data.auctionStart).to.be.greaterThanOrEqual(now); expect(data.ttfb).to.be.greaterThanOrEqual(0); - expect(data.bidRequests[0].adUnitCode).to.equal('adunit-code'); + expect(data.bidRequests[0].adUnitCode).to.equal(adUnitCode); }); describe('GDPR params', function () { @@ -374,6 +395,35 @@ describe('Seedtag Adapter', function () { expect(videoBid.sizes[1][1]).to.equal(600); expect(videoBid.requestCount).to.equal(1); }); + + it('should have geom parameters if slot is available', function() { + const request = spec.buildRequests(validBidRequests, bidderRequest); + const data = JSON.parse(request.data); + const bidRequests = data.bidRequests; + const bannerBid = bidRequests[0]; + + // on some CI, the DOM is not initialized, so we need to check if the slot is available + const slot = document.getElementById(adUnitCode) + if (slot) { + expect(bannerBid).to.have.property('geom') + + const params = [['width', 300], ['height', 250], ['top', 10], ['left', 20], ['scrollY', 0]] + params.forEach(([param, value]) => { + expect(bannerBid.geom).to.have.property(param) + expect(bannerBid.geom[param]).to.be.a('number') + expect(bannerBid.geom[param]).to.be.equal(value) + }) + + expect(bannerBid.geom).to.have.property('viewport') + const viewportParams = ['width', 'height'] + viewportParams.forEach(param => { + expect(bannerBid.geom.viewport).to.have.property(param) + expect(bannerBid.geom.viewport[param]).to.be.a('number') + }) + } else { + expect(bannerBid).to.not.have.property('geom') + } + }) }); describe('COPPA param', function () { diff --git a/test/spec/modules/sharethroughBidAdapter_spec.js b/test/spec/modules/sharethroughBidAdapter_spec.js index 4989dcb1098..6a63ae681e7 100644 --- a/test/spec/modules/sharethroughBidAdapter_spec.js +++ b/test/spec/modules/sharethroughBidAdapter_spec.js @@ -1,6 +1,6 @@ import { expect } from 'chai'; -import * as sinon from 'sinon'; import { sharethroughAdapterSpec, sharethroughInternal } from 'modules/sharethroughBidAdapter.js'; +import * as sinon from 'sinon'; import { newBidder } from 'src/adapters/bidderFactory.js'; import { config } from 'src/config'; import * as utils from 'src/utils'; @@ -73,7 +73,10 @@ describe('sharethrough adapter spec', function () { bidder: 'sharethrough', bidId: 'bidId1', transactionId: 'transactionId1', - sizes: [[300, 250], [300, 600]], + sizes: [ + [300, 250], + [300, 600], + ], params: { pkey: 'aaaa1111', bcat: ['cat1', 'cat2'], @@ -95,104 +98,104 @@ describe('sharethrough adapter spec', function () { }, userIdAsEids: [ { - 'source': 'pubcid.org', - 'uids': [ + source: 'pubcid.org', + uids: [ { - 'atype': 1, - 'id': 'fake-pubcid' + atype: 1, + id: 'fake-pubcid', }, - ] + ], }, { - 'source': 'liveramp.com', - 'uids': [ + source: 'liveramp.com', + uids: [ { - 'atype': 1, - 'id': 'fake-identity-link' - } - ] + atype: 1, + id: 'fake-identity-link', + }, + ], }, { - 'source': 'id5-sync.com', - 'uids': [ + source: 'id5-sync.com', + uids: [ { - 'atype': 1, - 'id': 'fake-id5id' - } - ] + atype: 1, + id: 'fake-id5id', + }, + ], }, { - 'source': 'adserver.org', - 'uids': [ + source: 'adserver.org', + uids: [ { - 'atype': 1, - 'id': 'fake-tdid' - } - ] + atype: 1, + id: 'fake-tdid', + }, + ], }, { - 'source': 'criteo.com', - 'uids': [ + source: 'criteo.com', + uids: [ { - 'atype': 1, - 'id': 'fake-criteo' - } - ] + atype: 1, + id: 'fake-criteo', + }, + ], }, { - 'source': 'britepool.com', - 'uids': [ + source: 'britepool.com', + uids: [ { - 'atype': 1, - 'id': 'fake-britepool' - } - ] + atype: 1, + id: 'fake-britepool', + }, + ], }, { - 'source': 'liveintent.com', - 'uids': [ + source: 'liveintent.com', + uids: [ { - 'atype': 1, - 'id': 'fake-lipbid' - } - ] + atype: 1, + id: 'fake-lipbid', + }, + ], }, { - 'source': 'intentiq.com', - 'uids': [ + source: 'intentiq.com', + uids: [ { - 'atype': 1, - 'id': 'fake-intentiq' - } - ] + atype: 1, + id: 'fake-intentiq', + }, + ], }, { - 'source': 'crwdcntrl.net', - 'uids': [ + source: 'crwdcntrl.net', + uids: [ { - 'atype': 1, - 'id': 'fake-lotame' - } - ] + atype: 1, + id: 'fake-lotame', + }, + ], }, { - 'source': 'parrable.com', - 'uids': [ + source: 'parrable.com', + uids: [ { - 'atype': 1, - 'id': 'fake-parrable' - } - ] + atype: 1, + id: 'fake-parrable', + }, + ], }, { - 'source': 'netid.de', - 'uids': [ + source: 'netid.de', + uids: [ { - 'atype': 1, - 'id': 'fake-netid' - } - ] - } + atype: 1, + id: 'fake-netid', + }, + ], + }, ], crumbs: { pubcid: 'fake-pubcid-in-crumbs-obj', @@ -250,10 +253,10 @@ describe('sharethrough adapter spec', function () { }, ortb2: { source: { - tid: 'auction-id' - } + tid: 'auction-id', + }, }, - timeout: 242 + timeout: 242, }; }); @@ -312,6 +315,12 @@ describe('sharethrough adapter spec', function () { expect(eid.uids[0].atype).to.be.ok; } + // expect(openRtbReq.regs.gpp).to.equal(bidderRequest.gppConsent.gppString); + // expect(openRtbReq.regs.gpp_sid).to.equal(bidderRequest.gppConsent.applicableSections); + + // expect(openRtbReq.regs.ext.gpp).to.equal(bidderRequest.ortb2.regs.gpp); + // expect(openRtbReq.regs.ext.gpp_sid).to.equal(bidderRequest.ortb2.regs.gpp_sid); + expect(openRtbReq.device.ua).to.equal(navigator.userAgent); expect(openRtbReq.regs.coppa).to.equal(1); @@ -395,6 +404,25 @@ describe('sharethrough adapter spec', function () { expect(openRtbReq.regs.coppa).to.equal(0); }); }); + + describe('gpp', () => { + it('should properly attach GPP information to the request when applicable', () => { + bidderRequest.gppConsent = { + gppString: 'some-gpp-string', + applicableSections: [3, 5], + }; + + const openRtbReq = spec.buildRequests(bidRequests, bidderRequest)[0].data; + expect(openRtbReq.regs.gpp).to.equal(bidderRequest.gppConsent.gppString); + expect(openRtbReq.regs.gpp_sid).to.equal(bidderRequest.gppConsent.applicableSections); + }); + + it('should populate request accordingly when gpp explicitly does not apply', function () { + const openRtbReq = spec.buildRequests(bidRequests, {})[0].data; + + expect(openRtbReq.regs.gpp).to.be.undefined; + }); + }); }); describe('transaction id at the impression level', () => { @@ -455,7 +483,10 @@ describe('sharethrough adapter spec', function () { const bannerImp = builtRequest.data.imp[0].banner; expect(bannerImp.pos).to.equal(1); expect(bannerImp.topframe).to.equal(1); - expect(bannerImp.format).to.deep.equal([{ w: 300, h: 250 }, { w: 300, h: 600 }]); + expect(bannerImp.format).to.deep.equal([ + { w: 300, h: 250 }, + { w: 300, h: 600 }, + ]); }); it('should default to pos 0 if not provided', () => { @@ -557,6 +588,43 @@ describe('sharethrough adapter spec', function () { }); }); + describe('cookie deprecation', () => { + it('should not add cdep if we do not get it in an impression request', () => { + const builtRequests = spec.buildRequests(bidRequests, { + auctionId: 'new-auction-id', + ortb2: { + device: { + ext: { + propThatIsNotCdep: 'value-we-dont-care-about', + }, + }, + }, + }); + const noCdep = builtRequests.every((builtRequest) => { + const ourCdepValue = builtRequest.data.device?.ext?.cdep; + return ourCdepValue === undefined; + }); + expect(noCdep).to.be.true; + }); + + it('should add cdep if we DO get it in an impression request', () => { + const builtRequests = spec.buildRequests(bidRequests, { + auctionId: 'new-auction-id', + ortb2: { + device: { + ext: { + cdep: 'cdep-value', + }, + }, + }, + }); + const cdepPresent = builtRequests.every((builtRequest) => { + return builtRequest.data.device.ext.cdep === 'cdep-value'; + }); + expect(cdepPresent).to.be.true; + }); + }); + describe('first party data', () => { const firstPartyData = { site: { @@ -585,10 +653,14 @@ describe('sharethrough adapter spec', function () { }, bcat: ['IAB1', 'IAB2-1'], badv: ['domain1.com', 'domain2.com'], + regs: { + gpp: 'gpp_string', + gpp_sid: [7], + }, }; it('should include first party data in open rtb request, site section', () => { - const openRtbReq = spec.buildRequests(bidRequests, {...bidderRequest, ortb2: firstPartyData})[0].data; + const openRtbReq = spec.buildRequests(bidRequests, { ...bidderRequest, ortb2: firstPartyData })[0].data; expect(openRtbReq.site.name).to.equal(firstPartyData.site.name); expect(openRtbReq.site.keywords).to.equal(firstPartyData.site.keywords); @@ -612,6 +684,13 @@ describe('sharethrough adapter spec', function () { expect(openRtbReq.bcat).to.deep.equal(firstPartyData.bcat); expect(openRtbReq.badv).to.deep.equal(firstPartyData.badv); }); + + it('should include first party data in open rtb request, regulation section', () => { + const openRtbReq = spec.buildRequests(bidRequests, { ...bidderRequest, ortb2: firstPartyData })[0].data; + + expect(openRtbReq.regs.ext.gpp).to.equal(firstPartyData.regs.gpp); + expect(openRtbReq.regs.ext.gpp_sid).to.equal(firstPartyData.regs.gpp_sid); + }); }); }); @@ -624,26 +703,31 @@ describe('sharethrough adapter spec', function () { request = spec.buildRequests(bidRequests, bidderRequest)[0]; response = { body: { - seatbid: [{ - bid: [{ - id: '123', - impid: 'bidId1', - w: 300, - h: 250, - price: 42, - crid: 'creative', - dealid: 'deal', - adomain: ['domain.com'], - adm: 'markup', - }, { - id: '456', - impid: 'bidId2', - w: 640, - h: 480, - price: 42, - adm: 'vastTag', - }], - }], + seatbid: [ + { + bid: [ + { + id: '123', + impid: 'bidId1', + w: 300, + h: 250, + price: 42, + crid: 'creative', + dealid: 'deal', + adomain: ['domain.com'], + adm: 'markup', + }, + { + id: '456', + impid: 'bidId2', + w: 640, + h: 480, + price: 42, + adm: 'vastTag', + }, + ], + }, + ], }, }; }); @@ -673,16 +757,20 @@ describe('sharethrough adapter spec', function () { request = spec.buildRequests(bidRequests, bidderRequest)[1]; response = { body: { - seatbid: [{ - bid: [{ - id: '456', - impid: 'bidId2', - w: 640, - h: 480, - price: 42, - adm: 'vastTag', - }], - }], + seatbid: [ + { + bid: [ + { + id: '456', + impid: 'bidId2', + w: 640, + h: 480, + price: 42, + adm: 'vastTag', + }, + ], + }, + ], }, }; }); @@ -712,24 +800,28 @@ describe('sharethrough adapter spec', function () { request = spec.buildRequests(bidRequests, bidderRequest)[0]; response = { body: { - seatbid: [{ - bid: [{ - id: '123', - impid: 'bidId1', - w: 300, - h: 250, - price: 42, - crid: 'creative', - dealid: 'deal', - adomain: ['domain.com'], - adm: 'markup', - }], - }], + seatbid: [ + { + bid: [ + { + id: '123', + impid: 'bidId1', + w: 300, + h: 250, + price: 42, + crid: 'creative', + dealid: 'deal', + adomain: ['domain.com'], + adm: 'markup', + }, + ], + }, + ], }, }; }); - it('should have null optional fields when the response\'s optional seatbid[].bid[].ext field is empty', () => { + it("should have null optional fields when the response's optional seatbid[].bid[].ext field is empty", () => { const bid = spec.interpretResponse(response, request)[0]; expect(bid.meta.networkId).to.be.null; @@ -747,7 +839,7 @@ describe('sharethrough adapter spec', function () { expect(bid.meta.mediaType).to.be.null; }); - it('should have populated fields when the response\'s optional seatbid[].bid[].ext fields are filled', () => { + it("should have populated fields when the response's optional seatbid[].bid[].ext fields are filled", () => { response.body.seatbid[0].bid[0].ext = { networkId: 'my network id', networkName: 'my network name', @@ -792,8 +884,8 @@ describe('sharethrough adapter spec', function () { expect(syncArray).to.deep.equal([ { type: 'image', url: 'cookieUrl1' }, { type: 'image', url: 'cookieUrl2' }, - { type: 'image', url: 'cookieUrl3' }], - ); + { type: 'image', url: 'cookieUrl3' }, + ]); }); it('returns an empty array if serverResponses is empty', function () { diff --git a/test/spec/modules/shinezRtbBidAdapter_spec.js b/test/spec/modules/shinezRtbBidAdapter_spec.js new file mode 100644 index 00000000000..3965cd69c5f --- /dev/null +++ b/test/spec/modules/shinezRtbBidAdapter_spec.js @@ -0,0 +1,639 @@ +import {expect} from 'chai'; +import { + spec as adapter, + createDomain, + hashCode, + extractPID, + extractCID, + extractSubDomain, + getStorageItem, + setStorageItem, + tryParseJSON, + getUniqueDealId, +} from 'modules/shinezRtbBidAdapter'; +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'; +import {deepAccess} from 'src/utils.js'; + +export const TEST_ID_SYSTEMS = ['britepoolid', 'criteoId', 'id5id', 'idl_env', 'lipb', 'netId', 'parrableId', 'pubcid', 'tdid', 'pubProvidedId', 'digitrustid']; + +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', + 'sizes': [[300, 250], [300, 600]], + 'bidderRequestId': '1fdb5ff1b6eaa7', + 'bidRequestsCount': 4, + 'bidderRequestsCount': 3, + 'bidderWinsCount': 1, + 'requestId': 'b0777d85-d061-450e-9bc7-260dd54bbb7a', + 'schain': 'a0819c69-005b-41ed-af06-1be1e0aefefc', + 'mediaTypes': [BANNER], + 'ortb2Imp': { + 'ext': { + 'gpid': '0123456789', + 'tid': '56e184c6-bde9-497b-b9b9-cf47a61381ee' + } + } +}; + +const VIDEO_BID = { + 'bidId': '2d52001cabd527', + 'adUnitCode': '63550ad1ff6642d368cba59dh5884270560', + 'bidderRequestId': '12a8ae9ada9c13', + '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 + } + }, + 'ortb2Imp': { + 'ext': { + 'gpid': '0123456789', + 'tid': '56e184c6-bde9-497b-b9b9-cf47a61381ee' + } + } +} + +const BIDDER_REQUEST = { + 'gdprConsent': { + 'consentString': 'consent_string', + 'gdprApplies': true + }, + 'gppConsent': { + 'gppString': 'gpp_string', + 'applicableSections': [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': ['sweetgum.io'], + '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('ShinezRtbBidAdapter', 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 = { + shinezRtb: { + 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, + enableTIDs: true + }); + 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], + transactionId: '56e184c6-bde9-497b-b9b9-cf47a61381ee', + prebidVersion: version, + 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 + } + }, + gpid: '0123456789' + } + }); + }); + + it('should build banner request for each size', function () { + const hashUrl = hashCode(BIDDER_REQUEST.refererInfo.page); + config.setConfig({ + bidderTimeout: 3000, + enableTIDs: true + }); + 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', + bidRequestsCount: 4, + bidderRequestsCount: 3, + bidderWinsCount: 1, + bidderTimeout: 3000, + bidderRequestId: '1fdb5ff1b6eaa7', + transactionId: '56e184c6-bde9-497b-b9b9-cf47a61381ee', + 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], + gpid: '0123456789', + 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.sweetgum.io/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.sweetgum.io/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.sweetgum.io/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 get meta from response metaData', function () { + const serverResponse = utils.deepClone(SERVER_RESPONSE); + serverResponse.body.results[0].metaData = { + advertiserDomains: ['sweetgum.io'], + agencyName: 'Agency Name', + }; + const responses = adapter.interpretResponse(serverResponse, REQUEST); + expect(responses[0].meta).to.deep.equal({ + advertiserDomains: ['sweetgum.io'], + 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); + 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: ['sweetgum.io'] + } + }); + }); + + 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 () { + TEST_ID_SYSTEMS.forEach((idSystemProvider) => { + const id = Date.now().toString(); + const bid = utils.deepClone(BID); + + const userId = (function () { + switch (idSystemProvider) { + case 'digitrustid': + return {data: {id}}; + 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 = { + shinezRtb: { + 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 = { + shinezRtb: { + 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/smaatoBidAdapter_spec.js b/test/spec/modules/smaatoBidAdapter_spec.js index 61a877f329d..185dee2430f 100644 --- a/test/spec/modules/smaatoBidAdapter_spec.js +++ b/test/spec/modules/smaatoBidAdapter_spec.js @@ -1095,17 +1095,15 @@ describe('smaatoBidAdapterTest', () => { criteoId: '123456', tdid: '89145' }, - userIdAsEids: createEidsArray({ - criteoId: '123456', - tdid: '89145' - }) + userIdAsEids: [ + {id: 1}, {id: 2} + ] }; const reqs = spec.buildRequests([userIdBidRequest], defaultBidderRequest); const req = extractPayloadOfFirstAndOnlyRequest(reqs); - expect(req.user.ext.eids).to.exist; - expect(req.user.ext.eids).to.have.length(2); + expect(req.user.ext.eids).to.eql(userIdBidRequest.userIdAsEids); }); }); diff --git a/test/spec/modules/smartadserverBidAdapter_spec.js b/test/spec/modules/smartadserverBidAdapter_spec.js index 9daa6a87826..58b4cd8c0d0 100644 --- a/test/spec/modules/smartadserverBidAdapter_spec.js +++ b/test/spec/modules/smartadserverBidAdapter_spec.js @@ -786,7 +786,7 @@ describe('Smart bid adapter tests', function () { expect(request[0]).to.have.property('method').and.to.equal('POST'); const requestContent = JSON.parse(request[0].data); expect(requestContent).to.have.property('videoData'); - expect(requestContent.videoData).to.have.property('videoProtocol').and.to.equal(null); + expect(requestContent.videoData).not.to.have.property('videoProtocol').eq(true); expect(requestContent.videoData).to.have.property('adBreak').and.to.equal(2); }); @@ -833,6 +833,73 @@ describe('Smart bid adapter tests', function () { expect(requestContent.videoData).to.have.property('videoProtocol').and.to.equal(6); expect(requestContent.videoData).to.have.property('adBreak').and.to.equal(3); }); + + it('should pass additional parameters', function () { + const request = spec.buildRequests([{ + bidder: 'smartadserver', + mediaTypes: { + video: { + context: 'instream', + api: [1, 2, 3], + maxbitrate: 50, + minbitrate: 20, + maxduration: 30, + minduration: 5, + placement: 3, + playbackmethod: [2, 4], + playerSize: [[640, 480]], + plcmt: 1, + skip: 0 + } + }, + params: { + siteId: '123' + } + }]); + const requestContent = JSON.parse(request[0].data); + + expect(requestContent.videoData).to.have.property('iabframeworks').and.to.equal('1,2,3'); + expect(requestContent.videoData).not.to.have.property('skip'); + expect(requestContent.videoData).to.have.property('vbrmax').and.to.equal(50); + expect(requestContent.videoData).to.have.property('vbrmin').and.to.equal(20); + expect(requestContent.videoData).to.have.property('vdmax').and.to.equal(30); + expect(requestContent.videoData).to.have.property('vdmin').and.to.equal(5); + expect(requestContent.videoData).to.have.property('vplcmt').and.to.equal(1); + expect(requestContent.videoData).to.have.property('vpmt').and.to.have.lengthOf(2); + expect(requestContent.videoData.vpmt[0]).to.equal(2); + expect(requestContent.videoData.vpmt[1]).to.equal(4); + expect(requestContent.videoData).to.have.property('vpt').and.to.equal(3); + }); + + it('should not pass not valuable parameters', function () { + const request = spec.buildRequests([{ + bidder: 'smartadserver', + mediaTypes: { + video: { + context: 'instream', + maxbitrate: 20, + minbitrate: null, + maxduration: 0, + playbackmethod: [], + playerSize: [[640, 480]], + plcmt: 1 + } + }, + params: { + siteId: '123' + } + }]); + const requestContent = JSON.parse(request[0].data); + + expect(requestContent.videoData).not.to.have.property('iabframeworks'); + expect(requestContent.videoData).to.have.property('vbrmax').and.to.equal(20); + expect(requestContent.videoData).not.to.have.property('vbrmin'); + expect(requestContent.videoData).not.to.have.property('vdmax'); + expect(requestContent.videoData).not.to.have.property('vdmin'); + expect(requestContent.videoData).to.have.property('vplcmt').and.to.equal(1); + expect(requestContent.videoData).not.to.have.property('vpmt'); + expect(requestContent.videoData).not.to.have.property('vpt'); + }); }); }); @@ -1029,7 +1096,7 @@ describe('Smart bid adapter tests', function () { expect(request[0]).to.have.property('method').and.to.equal('POST'); const requestContent = JSON.parse(request[0].data); expect(requestContent).to.have.property('videoData'); - expect(requestContent.videoData).to.have.property('videoProtocol').and.to.equal(null); + expect(requestContent.videoData).not.to.have.property('videoProtocol').eq(true); expect(requestContent.videoData).to.have.property('adBreak').and.to.equal(2); }); @@ -1393,4 +1460,41 @@ describe('Smart bid adapter tests', function () { expect(requestContent).to.have.property('gpid').and.to.equal(gpid); }); }); + + describe('#getValuableProperty method', function () { + it('should return an object when calling with a number value', () => { + const obj = spec.getValuableProperty('prop', 3); + expect(obj).to.deep.equal({ prop: 3 }); + }); + + it('should return an empty object when calling with a string value', () => { + const obj = spec.getValuableProperty('prop', 'str'); + expect(obj).to.deep.equal({}); + }); + + it('should return an empty object when calling with a number property', () => { + const obj = spec.getValuableProperty(7, 'str'); + expect(obj).to.deep.equal({}); + }); + + it('should return an empty object when calling with a null value', () => { + const obj = spec.getValuableProperty('prop', null); + expect(obj).to.deep.equal({}); + }); + + it('should return an empty object when calling with an object value', () => { + const obj = spec.getValuableProperty('prop', {}); + expect(obj).to.deep.equal({}); + }); + + it('should return an empty object when calling with a 0 value', () => { + const obj = spec.getValuableProperty('prop', 0); + expect(obj).to.deep.equal({}); + }); + + it('should return an empty object when calling without the value argument', () => { + const obj = spec.getValuableProperty('prop'); + expect(obj).to.deep.equal({}); + }); + }); }); diff --git a/test/spec/modules/smartyadsBidAdapter_spec.js b/test/spec/modules/smartyadsBidAdapter_spec.js index 3474753c838..458ccc37759 100644 --- a/test/spec/modules/smartyadsBidAdapter_spec.js +++ b/test/spec/modules/smartyadsBidAdapter_spec.js @@ -1,6 +1,7 @@ import {expect} from 'chai'; import {spec} from '../../../modules/smartyadsBidAdapter.js'; import { config } from '../../../src/config.js'; +import {server} from '../../mocks/xhr'; describe('SmartyadsAdapter', function () { let bid = { @@ -14,6 +15,21 @@ describe('SmartyadsAdapter', function () { } }; + let bidResponse = { + width: 300, + height: 250, + mediaType: 'banner', + ad: `test mode`, + requestId: '23fhj33i987f', + cpm: 0.1, + ttl: 120, + creativeId: '123', + netRevenue: true, + currency: 'USD', + dealId: 'HASH', + sid: 1234 + }; + describe('isBidRequestValid', function () { it('Should return true if there are bidId, params and sourceid parameters present', function () { expect(spec.isBidRequestValid(bid)).to.be.true; @@ -36,7 +52,11 @@ describe('SmartyadsAdapter', function () { expect(serverRequest.method).to.equal('POST'); }); it('Returns valid URL', function () { - expect(serverRequest.url).to.equal('https://n1.smartyads.com/?c=o&m=prebid&secret_key=prebid_js'); + expect(serverRequest.url).to.be.oneOf([ + 'https://n1.smartyads.com/?c=o&m=prebid&secret_key=prebid_js', + 'https://n2.smartyads.com/?c=o&m=prebid&secret_key=prebid_js', + 'https://n6.smartyads.com/?c=o&m=prebid&secret_key=prebid_js' + ]); }); it('Returns valid data if array of bids is valid', function () { let data = serverRequest.data; @@ -243,7 +263,7 @@ describe('SmartyadsAdapter', function () { }); }); describe('getUserSyncs', function () { - const syncUrl = 'https://as.ck-ie.com/prebidjs?p=7c47322e527cf8bdeb7facc1bb03387a&gdpr=0&gdpr_consent=&type=iframe&us_privacy='; + const syncUrl = 'https://as.ck-ie.com/prebidjs?p=7c47322e527cf8bdeb7facc1bb03387a&gdpr=0&gdpr_consent=&type=iframe&us_privacy=&gpp='; const syncOptions = { iframeEnabled: true }; @@ -257,4 +277,79 @@ describe('SmartyadsAdapter', function () { ]); }); }); + + describe('onBidWon', function () { + it('should exists', function () { + expect(spec.onBidWon).to.exist.and.to.be.a('function'); + }); + + it('should send a valid bid won notice', function () { + const bid = { + 'c': 'o', + 'm': 'prebid', + 'secret_key': 'prebid_js', + 'winTest': '1', + 'postData': [{ + 'bidder': 'smartyads', + 'params': [ + {'host': 'prebid', + 'accountid': '123', + 'sourceid': '12345' + }] + }] + }; + spec.onBidWon(bid); + expect(server.requests.length).to.equal(1); + }); + }); + + describe('onTimeout', function () { + it('should exists', function () { + expect(spec.onTimeout).to.exist.and.to.be.a('function'); + }); + + it('should send a valid bid timeout notice', function () { + const bid = { + 'c': 'o', + 'm': 'prebid', + 'secret_key': 'prebid_js', + 'bidTimeout': '1', + 'postData': [{ + 'bidder': 'smartyads', + 'params': [ + {'host': 'prebid', + 'accountid': '123', + 'sourceid': '12345' + }] + }] + }; + spec.onTimeout(bid); + expect(server.requests.length).to.equal(1); + }); + }); + + describe('onBidderError', function () { + it('should exists', function () { + expect(spec.onBidderError).to.exist.and.to.be.a('function'); + }); + + it('should send a valid bidder error notice', function () { + const bid = { + 'c': 'o', + 'm': 'prebid', + 'secret_key': 'prebid_js', + 'bidderError': '1', + 'postData': [{ + 'bidder': 'smartyads', + 'params': [ + {'host': 'prebid', + 'accountid': '123', + 'sourceid': '12345' + }] + }] + }; + spec.onBidderError(bid); + expect(server.requests.length).to.equal(1); + }); + }); }); diff --git a/test/spec/modules/smilewantedBidAdapter_spec.js b/test/spec/modules/smilewantedBidAdapter_spec.js index 22221dbe1ef..99c4034610f 100644 --- a/test/spec/modules/smilewantedBidAdapter_spec.js +++ b/test/spec/modules/smilewantedBidAdapter_spec.js @@ -93,7 +93,24 @@ const BID_RESPONSE_DISPLAY = { const VIDEO_INSTREAM_REQUEST = [{ code: 'video1', mediaTypes: { - video: {} + video: { + context: 'instream', + mimes: ['video/mp4'], + minduration: 0, + maxduration: 120, + protocols: [1, 2, 3, 4, 5, 6, 7, 8], + startdelay: 0, + placement: 1, + skip: 1, + skipafter: 10, + minbitrate: 10, + maxbitrate: 10, + delivery: [1], + playbackmethod: [2], + api: [1, 2], + linearity: 1, + playerSize: [640, 480] + } }, sizes: [ [640, 480] @@ -163,6 +180,99 @@ const BID_RESPONSE_VIDEO_OUTSTREAM = { } }; +const NATIVE_REQUEST = [{ + adUnitCode: 'native_300x250', + code: '/19968336/prebid_native_example_1', + bidId: '12345', + sizes: [ + [300, 250] + ], + mediaTypes: { + native: { + sendTargetingKeys: false, + title: { + required: true, + len: 140 + }, + image: { + required: true, + sizes: [300, 250] + }, + icon: { + required: false, + sizes: [50, 50] + }, + sponsoredBy: { + required: true + }, + body: { + required: true + }, + clickUrl: { + required: false + }, + privacyLink: { + required: false + }, + cta: { + required: false + }, + rating: { + required: false + }, + likes: { + required: false + }, + downloads: { + required: false + }, + price: { + required: false + }, + salePrice: { + required: false + }, + phone: { + required: false + }, + address: { + required: false + }, + desc2: { + required: false + }, + displayUrl: { + required: false + } + } + }, + bidder: 'smilewanted', + params: { + zoneId: 4, + }, + requestId: 'request_abcd1234', + ortb2Imp: { + ext: { + tid: 'trans_abcd1234', + } + }, +}]; + +const BID_RESPONSE_NATIVE = { + body: { + cpm: 3, + width: 300, + height: 250, + creativeId: 'crea_sw_1', + currency: 'EUR', + isNetCpm: true, + ttl: 300, + ad: '{"link":{"url":"https://www.smilewanted.com"},"assets":[{"id":0,"required":1,"title":{"len":50}},{"id":1,"required":1,"img":{"type":3,"w":150,"h":50,"ext":{"aspectratios":["2:1"]}}},{"id":2,"required":0,"img":{"type":1,"w":50,"h":50,"ext":{"aspectratios":["2:1"]}}},{"id":3,"required":1,"data":{"type":1,"value":"Smilewanted sponsor"}},{"id":4,"required":1,"data":{"type":2,"value":"Smilewanted Description"}}]}', + cSyncUrl: 'https://csync.smilewanted.com', + formatTypeSw: 'native' + } +}; + // Default params with optional ones describe('smilewantedBidAdapterTests', function () { it('SmileWanted - Verify build request', function () { @@ -195,6 +305,23 @@ describe('smilewantedBidAdapterTests', function () { expect(requestVideoInstreamContent.sizes[0]).to.have.property('w').and.to.equal(640); expect(requestVideoInstreamContent.sizes[0]).to.have.property('h').and.to.equal(480); expect(requestVideoInstreamContent).to.have.property('transactionId').and.to.not.equal(null).and.to.not.be.undefined; + expect(requestVideoInstreamContent).to.have.property('videoParams'); + expect(requestVideoInstreamContent.videoParams).to.have.property('context').and.to.equal('instream').and.to.not.be.undefined; + expect(requestVideoInstreamContent.videoParams).to.have.property('mimes').to.be.an('array').that.include('video/mp4').and.to.not.be.undefined; + expect(requestVideoInstreamContent.videoParams).to.have.property('minduration').and.to.equal(0).and.to.not.be.undefined; + expect(requestVideoInstreamContent.videoParams).to.have.property('maxduration').and.to.equal(120).and.to.not.be.undefined; + expect(requestVideoInstreamContent.videoParams).to.have.property('protocols').to.be.an('array').that.include.members([1, 2, 3, 4, 5, 6, 7, 8]).and.to.not.be.undefined; + expect(requestVideoInstreamContent.videoParams).to.have.property('startdelay').and.to.equal(0).and.to.not.be.undefined; + expect(requestVideoInstreamContent.videoParams).to.have.property('placement').and.to.equal(1).and.to.not.be.undefined; + expect(requestVideoInstreamContent.videoParams).to.have.property('skip').and.to.equal(1).and.to.not.be.undefined; + expect(requestVideoInstreamContent.videoParams).to.have.property('skipafter').and.to.equal(10).and.to.not.be.undefined; + expect(requestVideoInstreamContent.videoParams).to.have.property('minbitrate').and.to.equal(10).and.to.not.be.undefined; + expect(requestVideoInstreamContent.videoParams).to.have.property('maxbitrate').and.to.equal(10).and.to.not.be.undefined; + expect(requestVideoInstreamContent.videoParams).to.have.property('delivery').to.be.an('array').that.include(1).and.to.not.be.undefined; + expect(requestVideoInstreamContent.videoParams).to.have.property('playbackmethod').to.be.an('array').that.include(2).and.to.not.be.undefined; + expect(requestVideoInstreamContent.videoParams).to.have.property('api').to.be.an('array').that.include.members([1, 2]).and.to.not.be.undefined; + expect(requestVideoInstreamContent.videoParams).to.have.property('linearity').and.to.equal(1).and.to.not.be.undefined; + expect(requestVideoInstreamContent.videoParams).to.have.property('playerSize').to.be.an('array').that.include.members([640, 480]).and.to.not.be.undefined; const requestVideoOutstream = spec.buildRequests(VIDEO_OUTSTREAM_REQUEST); expect(requestVideoOutstream[0]).to.have.property('url').and.to.equal('https://prebid.smilewanted.com'); @@ -206,6 +333,39 @@ describe('smilewantedBidAdapterTests', function () { expect(requestVideoOutstreamContent.sizes[0]).to.have.property('w').and.to.equal(640); expect(requestVideoOutstreamContent.sizes[0]).to.have.property('h').and.to.equal(480); expect(requestVideoOutstreamContent).to.have.property('transactionId').and.to.not.equal(null).and.to.not.be.undefined; + + const requestNative = spec.buildRequests(NATIVE_REQUEST); + expect(requestNative[0]).to.have.property('url').and.to.equal('https://prebid.smilewanted.com'); + expect(requestNative[0]).to.have.property('method').and.to.equal('POST'); + const requestNativeContent = JSON.parse(requestNative[0].data); + expect(requestNativeContent).to.have.property('zoneId').and.to.equal(4); + expect(requestNativeContent).to.have.property('currencyCode').and.to.equal('EUR'); + expect(requestNativeContent).to.have.property('sizes'); + expect(requestNativeContent.sizes[0]).to.have.property('w').and.to.equal(300); + expect(requestNativeContent.sizes[0]).to.have.property('h').and.to.equal(250); + expect(requestNativeContent).to.have.property('transactionId').and.to.not.equal(null).and.to.not.be.undefined; + expect(requestNativeContent).to.have.property('context').and.to.equal('native').and.to.not.be.undefined; + expect(requestNativeContent).to.have.property('nativeParams'); + expect(requestNativeContent.nativeParams.title).to.have.property('required').and.to.equal(true); + expect(requestNativeContent.nativeParams.title).to.have.property('len').and.to.equal(140); + expect(requestNativeContent.nativeParams.image).to.have.property('required').and.to.equal(true); + expect(requestNativeContent.nativeParams.image).to.have.property('sizes').to.be.an('array').that.include.members([300, 250]).and.to.not.be.undefined; + expect(requestNativeContent.nativeParams.icon).to.have.property('required').and.to.equal(false); + expect(requestNativeContent.nativeParams.icon).to.have.property('sizes').to.be.an('array').that.include.members([50, 50]).and.to.not.be.undefined; + expect(requestNativeContent.nativeParams.sponsoredBy).to.have.property('required').and.to.equal(true); + expect(requestNativeContent.nativeParams.body).to.have.property('required').and.to.equal(true); + expect(requestNativeContent.nativeParams.clickUrl).to.have.property('required').and.to.equal(false); + expect(requestNativeContent.nativeParams.privacyLink).to.have.property('required').and.to.equal(false); + expect(requestNativeContent.nativeParams.cta).to.have.property('required').and.to.equal(false); + expect(requestNativeContent.nativeParams.rating).to.have.property('required').and.to.equal(false); + expect(requestNativeContent.nativeParams.likes).to.have.property('required').and.to.equal(false); + expect(requestNativeContent.nativeParams.downloads).to.have.property('required').and.to.equal(false); + expect(requestNativeContent.nativeParams.price).to.have.property('required').and.to.equal(false); + expect(requestNativeContent.nativeParams.salePrice).to.have.property('required').and.to.equal(false); + expect(requestNativeContent.nativeParams.phone).to.have.property('required').and.to.equal(false); + expect(requestNativeContent.nativeParams.address).to.have.property('required').and.to.equal(false); + expect(requestNativeContent.nativeParams.desc2).to.have.property('required').and.to.equal(false); + expect(requestNativeContent.nativeParams.displayUrl).to.have.property('required').and.to.equal(false); }); it('SmileWanted - Verify build request with referrer', function () { @@ -337,7 +497,7 @@ describe('smilewantedBidAdapterTests', function () { }).to.not.throw(); }); - it('SmileWanted - Verify parse response - Video Oustream', function () { + it('SmileWanted - Verify parse response - Video Outstream', function () { const request = spec.buildRequests(VIDEO_OUTSTREAM_REQUEST); const bids = spec.interpretResponse(BID_RESPONSE_VIDEO_OUTSTREAM, request[0]); expect(bids).to.have.lengthOf(1); @@ -360,6 +520,28 @@ describe('smilewantedBidAdapterTests', function () { }).to.not.throw(); }); + it('SmileWanted - Verify parse response - Native', function () { + const request = spec.buildRequests(NATIVE_REQUEST); + const bids = spec.interpretResponse(BID_RESPONSE_NATIVE, request[0]); + expect(bids).to.have.lengthOf(1); + const bid = bids[0]; + expect(bid.cpm).to.equal(3); + expect(bid.ad).to.equal('{"link":{"url":"https://www.smilewanted.com"},"assets":[{"id":0,"required":1,"title":{"len":50}},{"id":1,"required":1,"img":{"type":3,"w":150,"h":50,"ext":{"aspectratios":["2:1"]}}},{"id":2,"required":0,"img":{"type":1,"w":50,"h":50,"ext":{"aspectratios":["2:1"]}}},{"id":3,"required":1,"data":{"type":1,"value":"Smilewanted sponsor"}},{"id":4,"required":1,"data":{"type":2,"value":"Smilewanted Description"}}]}'); + expect(bid.width).to.equal(300); + expect(bid.height).to.equal(250); + expect(bid.creativeId).to.equal('crea_sw_1'); + expect(bid.currency).to.equal('EUR'); + expect(bid.netRevenue).to.equal(true); + expect(bid.ttl).to.equal(300); + expect(bid.requestId).to.equal(NATIVE_REQUEST[0].bidId); + + expect(function () { + spec.interpretResponse(BID_RESPONSE_NATIVE, { + data: 'invalid Json' + }) + }).to.not.throw(); + }); + it('SmileWanted - Verify bidder code', function () { expect(spec.code).to.equal('smilewanted'); }); diff --git a/test/spec/modules/snigelBidAdapter_spec.js b/test/spec/modules/snigelBidAdapter_spec.js index 3fc09493f03..3ba84228872 100644 --- a/test/spec/modules/snigelBidAdapter_spec.js +++ b/test/spec/modules/snigelBidAdapter_spec.js @@ -20,8 +20,10 @@ const makeBidRequest = function (overrides) { }; const BASE_BIDDER_REQUEST = { + auctionId: 'test', bidderRequestId: 'test', refererInfo: { + page: 'https://localhost', canonicalUrl: 'https://localhost', }, }; @@ -53,8 +55,8 @@ describe('snigelBidAdapter', function () { it('should build a single request for every impression and its placement', function () { const bidderRequest = Object.assign({}, BASE_BIDDER_REQUEST); const bidRequests = [ - makeBidRequest({bidId: 'a', params: {placement: 'top_leaderboard'}}), - makeBidRequest({bidId: 'b', params: {placement: 'bottom_leaderboard'}}), + makeBidRequest({bidId: 'a', adUnitCode: 'au_a', params: {placement: 'top_leaderboard'}}), + makeBidRequest({bidId: 'b', adUnitCode: 'au_b', params: {placement: 'bottom_leaderboard'}}), ]; const request = spec.buildRequests(bidRequests, bidderRequest); @@ -70,9 +72,9 @@ describe('snigelBidAdapter', function () { expect(data).to.have.property('page').and.to.equal('https://localhost'); expect(data).to.have.property('placements'); expect(data.placements.length).to.equal(2); - expect(data.placements[0].uuid).to.equal('a'); + expect(data.placements[0].id).to.equal('au_a'); expect(data.placements[0].name).to.equal('top_leaderboard'); - expect(data.placements[1].uuid).to.equal('b'); + expect(data.placements[1].id).to.equal('au_b'); expect(data.placements[1].name).to.equal('bottom_leaderboard'); }); @@ -127,6 +129,56 @@ describe('snigelBidAdapter', function () { const data = JSON.parse(request.data); expect(data).to.have.property('coppa').and.to.equal(true); }); + + it('should forward refresh information', function () { + const bidderRequest = Object.assign({}, BASE_BIDDER_REQUEST); + const topLeaderboard = makeBidRequest({adUnitCode: 'top_leaderboard'}); + const bottomLeaderboard = makeBidRequest({adUnitCode: 'bottom_leaderboard'}); + const sidebar = makeBidRequest({adUnitCode: 'sidebar'}); + + // first auction, no refresh + let request = spec.buildRequests([topLeaderboard, bottomLeaderboard], bidderRequest); + expect(request).to.have.property('data'); + let data = JSON.parse(request.data); + expect(data).to.have.property('placements'); + expect(data.placements.length).to.equal(2); + expect(data.placements[0].id).to.equal('top_leaderboard'); + expect(data.placements[0].refresh).to.be.undefined; + expect(data.placements[1].id).to.equal('bottom_leaderboard'); + expect(data.placements[1].refresh).to.be.undefined; + + // second auction for top leaderboard, was refreshed + request = spec.buildRequests([topLeaderboard, sidebar], bidderRequest); + expect(request).to.have.property('data'); + data = JSON.parse(request.data); + expect(data).to.have.property('placements'); + expect(data.placements.length).to.equal(2); + expect(data.placements[0].id).to.equal('top_leaderboard'); + expect(data.placements[0].refresh).to.not.be.undefined; + expect(data.placements[0].refresh.count).to.equal(1); + expect(data.placements[0].refresh.time).to.be.greaterThanOrEqual(0); + expect(data.placements[1].id).to.equal('sidebar'); + expect(data.placements[1].refresh).to.be.undefined; + + // third auction, all units refreshed at some point + request = spec.buildRequests([topLeaderboard, bottomLeaderboard, sidebar], bidderRequest); + expect(request).to.have.property('data'); + data = JSON.parse(request.data); + expect(data).to.have.property('placements'); + expect(data.placements.length).to.equal(3); + expect(data.placements[0].id).to.equal('top_leaderboard'); + expect(data.placements[0].refresh).to.not.be.undefined; + expect(data.placements[0].refresh.count).to.equal(2); + expect(data.placements[0].refresh.time).to.be.greaterThanOrEqual(0); + expect(data.placements[1].id).to.equal('bottom_leaderboard'); + expect(data.placements[1].refresh).to.not.be.undefined; + expect(data.placements[1].refresh.count).to.equal(1); + expect(data.placements[1].refresh.time).to.be.greaterThanOrEqual(0); + expect(data.placements[2].id).to.equal('sidebar'); + expect(data.placements[2].refresh).to.not.be.undefined; + expect(data.placements[2].refresh.count).to.equal(1); + expect(data.placements[2].refresh.time).to.be.greaterThanOrEqual(0); + }); }); describe('interpretResponse', function () { @@ -146,7 +198,7 @@ describe('snigelBidAdapter', function () { cur: 'USD', bids: [ { - uuid: BASE_BID_REQUEST.bidId, + id: BASE_BID_REQUEST.adUnitCode, price: 0.0575, ad: '

Test Ad

', width: 728, @@ -160,7 +212,7 @@ describe('snigelBidAdapter', function () { }, }; - const bids = spec.interpretResponse(serverResponse, {}); + const bids = spec.interpretResponse(serverResponse, {bidderRequest: {bids: [BASE_BID_REQUEST]}}); expect(bids.length).to.equal(1); const bid = bids[0]; expect(isValid(BASE_BID_REQUEST.adUnitCode, bid)).to.be.true; diff --git a/test/spec/modules/sonobiAnalyticsAdapter_spec.js b/test/spec/modules/sonobiAnalyticsAdapter_spec.js index 76ff88836d4..ed8ccd22eea 100644 --- a/test/spec/modules/sonobiAnalyticsAdapter_spec.js +++ b/test/spec/modules/sonobiAnalyticsAdapter_spec.js @@ -1,4 +1,4 @@ -import sonobiAnalytics from 'modules/sonobiAnalyticsAdapter.js'; +import sonobiAnalytics, {DEFAULT_EVENT_URL} from 'modules/sonobiAnalyticsAdapter.js'; import {expect} from 'chai'; import {server} from 'test/mocks/xhr.js'; let events = require('src/events'); @@ -76,8 +76,8 @@ describe('Sonobi Prebid Analytic', function () { events.emit(constants.EVENTS.AUCTION_END, {auctionId: '13', bidsReceived: [bid]}); clock.tick(5000); - expect(server.requests).to.have.length(1); - expect(JSON.parse(server.requests[0].requestBody)).to.have.length(3) + const req = server.requests.find(req => req.url.indexOf(DEFAULT_EVENT_URL) !== -1); + expect(JSON.parse(req.requestBody)).to.have.length(3) done(); }); }); diff --git a/test/spec/modules/sonobiBidAdapter_spec.js b/test/spec/modules/sonobiBidAdapter_spec.js index b9bd0dc4d9f..83db7c0a812 100644 --- a/test/spec/modules/sonobiBidAdapter_spec.js +++ b/test/spec/modules/sonobiBidAdapter_spec.js @@ -1,9 +1,9 @@ -import { expect } from 'chai' -import { spec, _getPlatform } from 'modules/sonobiBidAdapter.js' -import { newBidder } from 'src/adapters/bidderFactory.js' +import { expect } from 'chai'; +import { _getPlatform, spec } from 'modules/sonobiBidAdapter.js'; +import { newBidder } from 'src/adapters/bidderFactory.js'; import { userSync } from '../../../src/userSync.js'; import { config } from 'src/config.js'; -import * as utils from '../../../src/utils.js'; +import * as gptUtils from '../../../libraries/gptUtils/gptUtils.js'; describe('SonobiBidAdapter', function () { const adapter = newBidder(spec) @@ -193,7 +193,7 @@ describe('SonobiBidAdapter', function () { }); describe('outstream', () => { - it('should return false if there is no param sizes', () => { + it('should return false if there is no playerSize', () => { const bid = { 'bidder': 'sonobi', 'adUnitCode': 'adunit-code', @@ -203,7 +203,6 @@ describe('SonobiBidAdapter', function () { 'mediaTypes': { video: { context: 'outstream', - playerSize: [300, 250] } }, 'bidId': '30b31c1838de1e', @@ -213,7 +212,7 @@ describe('SonobiBidAdapter', function () { expect(spec.isBidRequestValid(bid)).to.equal(false); }); - it('should return true if there is param sizes', () => { + it('should return true if there is playerSize', () => { const bid = { 'bidder': 'sonobi', 'adUnitCode': 'adunit-code', @@ -224,7 +223,8 @@ describe('SonobiBidAdapter', function () { }, 'mediaTypes': { video: { - context: 'outstream' + context: 'outstream', + playerSize: [640, 480] } }, 'bidId': '30b31c1838de1e', @@ -248,13 +248,13 @@ describe('SonobiBidAdapter', function () { let sandbox; beforeEach(function () { sinon.stub(userSync, 'canBidderRegisterSync'); - sinon.stub(utils, 'getGptSlotInfoForAdUnitCode') + sinon.stub(gptUtils, 'getGptSlotInfoForAdUnitCode') .onFirstCall().returns({ gptSlot: '/123123/gpt_publisher/adunit-code-3', divId: 'adunit-code-3-div-id' }); sandbox = sinon.createSandbox(); }); afterEach(function () { userSync.canBidderRegisterSync.restore(); - utils.getGptSlotInfoForAdUnitCode.restore(); + gptUtils.getGptSlotInfoForAdUnitCode.restore(); sandbox.restore(); }); let bidRequest = [{ @@ -294,8 +294,11 @@ describe('SonobiBidAdapter', function () { }, mediaTypes: { video: { - sizes: [[300, 250], [300, 600]], - context: 'outstream' + playerSize: [640, 480], + context: 'outstream', + playbackmethod: [1, 2, 3], + plcmt: 3, + placement: 2 } } }, @@ -339,7 +342,7 @@ describe('SonobiBidAdapter', function () { }]; let keyMakerData = { - '30b31c1838de1f': '1a2b3c4d5e6f1a2b3c4d|300x250,300x600|f=1.25,gpid=/123123/gpt_publisher/adunit-code-1,c=v,', + '30b31c1838de1f': '1a2b3c4d5e6f1a2b3c4d|640x480|f=1.25,gpid=/123123/gpt_publisher/adunit-code-1,c=v,pm=1:2:3,p=2,pl=3,', '30b31c1838de1d': '1a2b3c4d5e6f1a2b3c4e|300x250,300x600|f=0.42,gpid=/123123/gpt_publisher/adunit-code-3,c=d,', '/7780971/sparks_prebid_LB|30b31c1838de1e': '300x250,300x600|gpid=/7780971/sparks_prebid_LB,c=d,', }; @@ -356,7 +359,9 @@ describe('SonobiBidAdapter', function () { 'page': 'https://example.com', 'stack': ['https://example.com'] }, - uspConsent: 'someCCPAString' + uspConsent: 'someCCPAString', + ortb2: {} + }; it('should set fpd if there is any data in ortb2', function () { @@ -490,6 +495,14 @@ describe('SonobiBidAdapter', function () { expect(bidRequests.data.hfa).to.equal('hfakey') }) + it('should return a properly formatted request with expData and expKey', function () { + bidderRequests.ortb2.experianRtidData = 'IkhlbGxvLCB3b3JsZC4gSGVsbG8sIHdvcmxkLiBIZWxsbywgd29ybGQuIg=='; + bidderRequests.ortb2.experianRtidKey = 'sovrn-encryption-key-1'; + const bidRequests = spec.buildRequests(bidRequest, bidderRequests) + expect(bidRequests.data.expData).to.equal('IkhlbGxvLCB3b3JsZC4gSGVsbG8sIHdvcmxkLiBIZWxsbywgd29ybGQuIg=='); + expect(bidRequests.data.expKey).to.equal('sovrn-encryption-key-1'); + }) + it('should return null if there is nothing to bid on', function () { const bidRequests = spec.buildRequests([{ params: {} }], bidderRequests) expect(bidRequests).to.equal(null); @@ -635,12 +648,12 @@ describe('SonobiBidAdapter', function () { bidder: 'sonobi', mediaTypes: { video: { - context: 'outstream' + context: 'outstream', + playerSize: [640, 480] } }, params: { - placement_id: '92e95368e86639dbd86d', - sizes: [[640, 480]] + placement_id: '92e95368e86639dbd86d' } } ] @@ -658,7 +671,7 @@ describe('SonobiBidAdapter', function () { 'sbi_adomain': 'sonobi.com' }, '30b31c1838de1e': { - 'sbi_size': '300x250', + 'sbi_size': '640x480', 'sbi_apoc': 'remnant', 'sbi_aid': '30292e432662bd5f86d90774b944b038', 'sbi_mouse': 1.25, @@ -668,7 +681,7 @@ describe('SonobiBidAdapter', function () { }, '/7780971/sparks_prebid_LB_OUTSTREAM|30b31c1838de1g': { - 'sbi_size': '300x600', + 'sbi_size': '640x480', 'sbi_apoc': 'remnant', 'sbi_crid': '1234abcd', 'sbi_aid': '30292e432662bd5f86d90774b944b038', @@ -720,8 +733,8 @@ describe('SonobiBidAdapter', function () { { 'requestId': '30b31c1838de1e', 'cpm': 1.25, - 'width': 300, - 'height': 250, + 'width': 640, + 'height': 480, 'vastUrl': 'https://mco-1-apex.go.sonobi.com/vast.xml?vid=30292e432662bd5f86d90774b944b038&ref=https%3A%2F%2Flocalhost%2F', 'ttl': 500, 'creativeId': '30292e432662bd5f86d90774b944b038', @@ -737,8 +750,8 @@ describe('SonobiBidAdapter', function () { { 'requestId': '30b31c1838de1g', 'cpm': 1.07, - 'width': 300, - 'height': 600, + 'width': 640, + 'height': 480, 'ad': ``, 'ttl': 500, 'creativeId': '1234abcd', diff --git a/test/spec/modules/sovrnAnalyticsAdapter_spec.js b/test/spec/modules/sovrnAnalyticsAdapter_spec.js index 68552eb3d8a..973e90abd5a 100644 --- a/test/spec/modules/sovrnAnalyticsAdapter_spec.js +++ b/test/spec/modules/sovrnAnalyticsAdapter_spec.js @@ -12,7 +12,7 @@ let constants = require('src/constants.json'); /** * Emit analytics events - * @param {array} eventArr - array of objects to define the events that will fire + * @param {Array} eventArr - array of objects to define the events that will fire * @param {object} eventObj - key is eventType, value is event * @param {string} auctionId - the auction id to attached to the events */ diff --git a/test/spec/modules/sovrnBidAdapter_spec.js b/test/spec/modules/sovrnBidAdapter_spec.js index c84013d1963..f165a6da6d1 100644 --- a/test/spec/modules/sovrnBidAdapter_spec.js +++ b/test/spec/modules/sovrnBidAdapter_spec.js @@ -64,6 +64,29 @@ describe('sovrnBidAdapter', function() { expect(spec.isBidRequestValid(bidRequest)).to.equal(false) }) + + it('should return true when minduration is not passed', function() { + const width = 300 + const height = 250 + const mimes = ['video/mp4', 'application/javascript'] + const protocols = [2, 5] + const maxduration = 60 + const startdelay = 0 + const videoBidRequest = { + ...baseBidRequest, + mediaTypes: { + video: { + mimes, + protocols, + playerSize: [[width, height], [360, 240]], + maxduration, + startdelay + } + } + } + + expect(spec.isBidRequestValid(videoBidRequest)).to.equal(true) + }) }) describe('buildRequests', function () { @@ -183,6 +206,40 @@ describe('sovrnBidAdapter', function() { expect(payload.tmax).to.equal(3000) }) + it('forwards auction level tid', function() { + const bidderRequest = { + ...baseBidderRequest, + ortb2: { + source: { + tid: '1d1a030790a475' + } + }, + bids: [baseBidRequest] + } + + const payload = JSON.parse(spec.buildRequests([baseBidRequest], bidderRequest).data) + expect(payload.source?.tid).to.equal('1d1a030790a475') + }) + + it('forwards impression level tid', function() { + const bidRequest = { + ...baseBidRequest, + ortb2Imp: { + ext: { + tid: '1a2c032473f4983' + } + }, + } + + const bidderRequest = { + ...baseBidderRequest, + bids: [bidRequest] + } + + const payload = JSON.parse(spec.buildRequests([bidRequest], bidderRequest).data) + expect(payload.imp[0]?.ext?.tid).to.equal('1a2c032473f4983') + }) + it('includes the ad unit code in the request', function() { const impression = payload.imp[0] expect(impression.adunitcode).to.equal('adunit-code') @@ -261,6 +318,41 @@ describe('sovrnBidAdapter', function() { expect(data.regs.ext['us_privacy']).to.equal(bidderRequest.uspConsent) }) + it('should not set coppa when coppa is undefined', function () { + const bidderRequest = { + ...baseBidderRequest, + bidderCode: 'sovrn', + auctionId: '1d1a030790a475', + bidderRequestId: '22edbae2733bf6', + timeout: 3000, + bids: [baseBidRequest], + gdprConsent: { + consentString: 'BOJ8RZsOJ8RZsABAB8AAAAAZ+A==', + gdprApplies: true + }, + } + const {regs} = JSON.parse(spec.buildRequests([baseBidRequest], bidderRequest).data) + expect(regs.coppa).to.be.undefined + }) + + it('should set coppa to 1 when coppa is provided with value true', function () { + const bidderRequest = { + ...baseBidderRequest, + ortb2: { + regs: { + coppa: true + } + }, + bidderCode: 'sovrn', + auctionId: '1d1a030790a475', + bidderRequestId: '22edbae2733bf6', + timeout: 3000, + bids: [baseBidRequest] + } + const {regs} = JSON.parse(spec.buildRequests([baseBidRequest], bidderRequest).data) + expect(regs.coppa).to.equal(1) + }) + it('should send gpp info in OpenRTB 2.6 location when gppConsent defined', function () { const bidderRequest = { ...baseBidderRequest, @@ -817,7 +909,7 @@ describe('sovrnBidAdapter', function() { url: `https://ap.lijit.com/beacon?gdpr_consent=${gdprConsent.consentString}&informer=13487408`, } - const returnStatement = spec.getUserSyncs(syncOptions, serverResponse, gdprConsent, '') + const returnStatement = spec.getUserSyncs(syncOptions, serverResponse, gdprConsent, '', null) expect(returnStatement[0]).to.deep.equal(expectedReturnStatement) }) @@ -829,7 +921,22 @@ describe('sovrnBidAdapter', function() { url: `https://ap.lijit.com/beacon?us_privacy=${uspString}&informer=13487408`, } - const returnStatement = spec.getUserSyncs(syncOptions, serverResponse, null, uspString) + const returnStatement = spec.getUserSyncs(syncOptions, serverResponse, null, uspString, null) + + expect(returnStatement[0]).to.deep.equal(expectedReturnStatement) + }) + + it('should include gpp consent string if present', function() { + const gppConsent = { + applicableSections: [1, 2], + gppString: 'DBACNYA~CPXxRfAPXxRfAAfKABENB-CgAAAAAAAAAAYgAAAAAAAA~1YNN' + } + const expectedReturnStatement = { + type: 'iframe', + url: `https://ap.lijit.com/beacon?gpp=${gppConsent.gppString}&gpp_sid=${gppConsent.applicableSections}&informer=13487408`, + } + + const returnStatement = spec.getUserSyncs(syncOptions, serverResponse, null, '', gppConsent) expect(returnStatement[0]).to.deep.equal(expectedReturnStatement) }) @@ -840,12 +947,17 @@ describe('sovrnBidAdapter', function() { consentString: 'BOJ8RZsOJ8RZsABAB8AAAAAZ+A==' } const uspString = '1NYN' + const gppConsent = { + applicableSections: [1, 2], + gppString: 'DBACNYA~CPXxRfAPXxRfAAfKABENB-CgAAAAAAAAAAYgAAAAAAAA~1YNN' + } + const expectedReturnStatement = { type: 'iframe', - url: `https://ap.lijit.com/beacon?gdpr_consent=${gdprConsent.consentString}&us_privacy=${uspString}&informer=13487408`, + url: `https://ap.lijit.com/beacon?gdpr_consent=${gdprConsent.consentString}&us_privacy=${uspString}&gpp=${gppConsent.gppString}&gpp_sid=${gppConsent.applicableSections}&informer=13487408`, } - const returnStatement = spec.getUserSyncs(syncOptions, serverResponse, gdprConsent, uspString) + const returnStatement = spec.getUserSyncs(syncOptions, serverResponse, gdprConsent, uspString, gppConsent) expect(returnStatement[0]).to.deep.equal(expectedReturnStatement) }) diff --git a/test/spec/modules/sparteoBidAdapter_spec.js b/test/spec/modules/sparteoBidAdapter_spec.js new file mode 100644 index 00000000000..293f7da30a1 --- /dev/null +++ b/test/spec/modules/sparteoBidAdapter_spec.js @@ -0,0 +1,467 @@ +import {expect} from 'chai'; +import { deepClone, mergeDeep } from 'src/utils'; +import {spec as adapter} from 'modules/sparteoBidAdapter'; + +const CURRENCY = 'EUR'; +const TTL = 60; +const HTTP_METHOD = 'POST'; +const REQUEST_URL = 'https://bid.sparteo.com/auction'; +const USER_SYNC_URL_IFRAME = 'https://sync.sparteo.com/sync/iframe.html?from=prebidjs'; + +const VALID_BID_BANNER = { + bidder: 'sparteo', + bidId: '1a2b3c4d', + adUnitCode: 'id-1234', + params: { + networkId: '1234567a-eb1b-1fae-1d23-e1fbaef234cf', + formats: ['corner'] + }, + mediaTypes: { + banner: { + sizes: [ + [1, 1] + ] + } + } +}; + +const VALID_BID_VIDEO = { + bidder: 'sparteo', + bidId: '5e6f7g8h', + adUnitCode: 'id-5678', + params: { + networkId: '1234567a-eb1b-1fae-1d23-e1fbaef234cf' + }, + mediaTypes: { + video: { + playerSize: [640, 360], + protocols: [1, 2, 3, 4, 5, 6, 7, 8], + api: [1, 2], + mimes: ['video/mp4'], + skip: 1, + startdelay: 0, + placement: 1, + linearity: 1, + minduration: 5, + maxduration: 30, + context: 'instream' + } + }, + ortb2Imp: { + ext: { + pbadslot: 'video' + } + } +}; + +const VALID_REQUEST_BANNER = { + method: HTTP_METHOD, + url: REQUEST_URL, + data: { + 'imp': [{ + 'id': '1a2b3c4d', + 'banner': { + 'format': [{ + 'h': 1, + 'w': 1 + }], + 'topframe': 0 + }, + 'ext': { + 'sparteo': { + 'params': { + 'networkId': '1234567a-eb1b-1fae-1d23-e1fbaef234cf', + 'formats': ['corner'] + } + } + } + }], + 'site': { + 'publisher': { + 'ext': { + 'params': { + 'networkId': '1234567a-eb1b-1fae-1d23-e1fbaef234cf' + } + } + } + }, + 'test': 0 + } +}; + +const VALID_REQUEST_VIDEO = { + method: HTTP_METHOD, + url: REQUEST_URL, + data: { + 'imp': [{ + 'id': '5e6f7g8h', + 'video': { + 'w': 640, + 'h': 360, + 'protocols': [1, 2, 3, 4, 5, 6, 7, 8], + 'api': [1, 2], + 'mimes': ['video/mp4'], + 'skip': 1, + 'startdelay': 0, + 'placement': 1, + 'linearity': 1, + 'minduration': 5, + 'maxduration': 30, + }, + 'ext': { + 'pbadslot': 'video', + 'sparteo': { + 'params': { + 'networkId': '1234567a-eb1b-1fae-1d23-e1fbaef234cf' + } + } + } + }], + 'site': { + 'publisher': { + 'ext': { + 'params': { + 'networkId': '1234567a-eb1b-1fae-1d23-e1fbaef234cf' + } + } + } + }, + 'test': 0 + } +}; + +const VALID_REQUEST = { + method: HTTP_METHOD, + url: REQUEST_URL, + data: { + 'imp': [{ + 'id': '1a2b3c4d', + 'banner': { + 'format': [{ + 'h': 1, + 'w': 1 + }], + 'topframe': 0 + }, + 'ext': { + 'sparteo': { + 'params': { + 'networkId': '1234567a-eb1b-1fae-1d23-e1fbaef234cf', + 'formats': ['corner'] + } + } + } + }, { + 'id': '5e6f7g8h', + 'video': { + 'w': 640, + 'h': 360, + 'protocols': [1, 2, 3, 4, 5, 6, 7, 8], + 'api': [1, 2], + 'mimes': ['video/mp4'], + 'skip': 1, + 'startdelay': 0, + 'placement': 1, + 'linearity': 1, + 'minduration': 5, + 'maxduration': 30, + }, + 'ext': { + 'pbadslot': 'video', + 'sparteo': { + 'params': { + 'networkId': '1234567a-eb1b-1fae-1d23-e1fbaef234cf' + } + } + } + }], + 'site': { + 'publisher': { + 'ext': { + 'params': { + 'networkId': '1234567a-eb1b-1fae-1d23-e1fbaef234cf' + } + } + } + }, + 'test': 0 + } +}; + +const BIDDER_REQUEST = { + bids: [VALID_BID_BANNER, VALID_BID_VIDEO] +} + +const BIDDER_REQUEST_BANNER = { + bids: [VALID_BID_BANNER] +} + +const BIDDER_REQUEST_VIDEO = { + bids: [VALID_BID_VIDEO] +} + +describe('SparteoAdapter', function () { + describe('isBidRequestValid', function () { + describe('Check method return', function () { + it('should return true', function () { + expect(adapter.isBidRequestValid(VALID_BID_BANNER)).to.equal(true); + expect(adapter.isBidRequestValid(VALID_BID_VIDEO)).to.equal(true); + }); + + it('should return false because the networkId is missing', function () { + let wrongBid = deepClone(VALID_BID_BANNER); + delete wrongBid.params.networkId; + + expect(adapter.isBidRequestValid(wrongBid)).to.equal(false); + }); + + it('should return false because the banner size is missing', function () { + let wrongBid = deepClone(VALID_BID_BANNER); + + wrongBid.mediaTypes.banner.sizes = '123456'; + expect(adapter.isBidRequestValid(wrongBid)).to.equal(false); + + delete wrongBid.mediaTypes.banner.sizes; + expect(adapter.isBidRequestValid(wrongBid)).to.equal(false); + }); + + it('should return false because the video player size paramater is missing', function () { + let wrongBid = deepClone(VALID_BID_VIDEO); + + wrongBid.mediaTypes.video.playerSize = '123456'; + expect(adapter.isBidRequestValid(wrongBid)).to.equal(false); + + delete wrongBid.mediaTypes.video.playerSize; + expect(adapter.isBidRequestValid(wrongBid)).to.equal(false); + }); + }); + }); + + describe('buildRequests', function () { + describe('Check method return', function () { + if (FEATURES.VIDEO) { + it('should return the right formatted requests', function() { + const request = adapter.buildRequests([VALID_BID_BANNER, VALID_BID_VIDEO], BIDDER_REQUEST); + delete request.data.id; + + expect(request).to.deep.equal(VALID_REQUEST); + }); + } + + it('should return the right formatted banner requests', function() { + const request = adapter.buildRequests([VALID_BID_BANNER], BIDDER_REQUEST_BANNER); + delete request.data.id; + + expect(request).to.deep.equal(VALID_REQUEST_BANNER); + }); + + if (FEATURES.VIDEO) { + it('should return the right formatted video requests', function() { + const request = adapter.buildRequests([VALID_BID_VIDEO], BIDDER_REQUEST_VIDEO); + delete request.data.id; + + expect(request).to.deep.equal(VALID_REQUEST_VIDEO); + }); + } + + it('should return the right formatted request with endpoint test', function() { + let endpoint = 'https://bid-test.sparteo.com/auction'; + + let bids = mergeDeep(deepClone([VALID_BID_BANNER, VALID_BID_VIDEO]), { + params: { + endpoint: endpoint + } + }); + + let requests = mergeDeep(deepClone(VALID_REQUEST)); + + const request = adapter.buildRequests(bids, BIDDER_REQUEST); + requests.url = endpoint; + delete request.data.id; + + expect(requests).to.deep.equal(requests); + }); + }); + }); + + describe('interpretResponse', function() { + describe('Check method return', function () { + it('should return the right formatted response', function() { + let response = { + body: { + 'id': '63f4d300-6896-4bdc-8561-0932f73148b1', + 'cur': 'EUR', + 'seatbid': [ + { + 'seat': 'sparteo', + 'group': 0, + 'bid': [ + { + 'id': 'cdbb6982-a269-40c7-84e5-04797f11d87a', + 'impid': '1a2b3c4d', + 'price': 4.5, + 'ext': { + 'prebid': { + 'type': 'banner' + } + }, + 'adm': 'script', + 'crid': 'crid', + 'w': 1, + 'h': 1, + 'nurl': 'https://t.bidder.sparteo.com/img' + } + ] + } + ] + } + }; + + if (FEATURES.VIDEO) { + response.body.seatbid[0].bid.push({ + 'id': 'cdbb6982-a269-40c7-84e5-04797f11d87b', + 'impid': '5e6f7g8h', + 'price': 5, + 'ext': { + 'prebid': { + 'type': 'video', + 'cache': { + 'vastXml': { + 'url': 'https://pbs.tet.com/cache?uuid=1234' + } + } + } + }, + 'adm': 'tag', + 'crid': 'crid', + 'w': 640, + 'h': 480, + 'nurl': 'https://t.bidder.sparteo.com/img' + }); + } + + let formattedReponse = [ + { + requestId: '1a2b3c4d', + seatBidId: 'cdbb6982-a269-40c7-84e5-04797f11d87a', + cpm: 4.5, + width: 1, + height: 1, + creativeId: 'crid', + creative_id: 'crid', + currency: CURRENCY, + netRevenue: true, + ttl: TTL, + mediaType: 'banner', + meta: {}, + ad: 'script
' + } + ]; + + if (FEATURES.VIDEO) { + formattedReponse.push({ + requestId: '5e6f7g8h', + seatBidId: 'cdbb6982-a269-40c7-84e5-04797f11d87b', + cpm: 5, + width: 640, + height: 480, + playerWidth: 640, + playerHeight: 360, + creativeId: 'crid', + creative_id: 'crid', + currency: CURRENCY, + netRevenue: true, + ttl: TTL, + mediaType: 'video', + meta: {}, + nurl: 'https://t.bidder.sparteo.com/img', + vastUrl: 'https://pbs.tet.com/cache?uuid=1234', + vastXml: 'tag' + }); + } + + if (FEATURES.VIDEO) { + const request = adapter.buildRequests([VALID_BID_BANNER, VALID_BID_VIDEO], BIDDER_REQUEST); + expect(adapter.interpretResponse(response, request)).to.deep.equal(formattedReponse); + } else { + const request = adapter.buildRequests([VALID_BID_BANNER], BIDDER_REQUEST_BANNER); + expect(adapter.interpretResponse(response, request)).to.deep.equal(formattedReponse); + } + }); + }); + }); + + describe('onBidWon', function() { + describe('Check methods succeed', function () { + it('should not throw error', function() { + let bids = [ + { + requestId: '1a2b3c4d', + seatBidId: 'cdbb6982-a269-40c7-84e5-04797f11d87a', + cpm: 4.5, + width: 1, + height: 1, + creativeId: 'crid', + creative_id: 'crid', + currency: CURRENCY, + netRevenue: true, + ttl: TTL, + mediaType: 'banner', + meta: {}, + ad: 'script
', + nurl: [ + 'win.domain.com' + ] + }, + { + requestId: '2570', + seatBidId: 'cdbb6982-a269-40c7-84e5-04797f11d87b', + id: 'id-5678', + cpm: 5, + width: 640, + height: 480, + creativeId: 'crid', + currency: CURRENCY, + netRevenue: true, + ttl: TTL, + mediaType: 'video', + meta: {}, + vastXml: 'vast xml', + nurl: [ + 'win.domain2.com' + ] + } + ]; + + bids.forEach(function(bid) { + expect(adapter.onBidWon.bind(adapter, bid)).to.not.throw(); + }); + }); + }); + }); + + describe('getUserSyncs', function() { + describe('Check methods succeed', function () { + it('should return the sync url', function() { + const syncOptions = { + 'iframeEnabled': true, + 'pixelEnabled': false + }; + const gdprConsent = { + gdprApplies: 1, + consentString: 'tcfv2' + }; + const uspConsent = { + consentString: '1Y---' + }; + + const syncUrls = [{ + type: 'iframe', + url: USER_SYNC_URL_IFRAME + '&gdpr=1&gdpr_consent=tcfv2&usp_consent=1Y---' + }]; + + expect(adapter.getUserSyncs(syncOptions, null, gdprConsent, uspConsent)).to.deep.equal(syncUrls); + }); + }); + }); +}); diff --git a/test/spec/modules/ssmasBidAdapter_spec.js b/test/spec/modules/ssmasBidAdapter_spec.js new file mode 100644 index 00000000000..26c6f60da4b --- /dev/null +++ b/test/spec/modules/ssmasBidAdapter_spec.js @@ -0,0 +1,244 @@ +import { expect } from 'chai'; +import { spec, SSMAS_CODE, SSMAS_ENDPOINT, SSMAS_REQUEST_METHOD } from 'modules/ssmasBidAdapter.js'; +import {newBidder} from 'src/adapters/bidderFactory.js'; +import * as utils from 'src/utils.js'; + +describe('ssmasBidAdapter', function () { + const bid = { + bidder: SSMAS_CODE, + adUnitCode: 'adunit-code', + sizes: [[300, 250]], + bidId: '30b31c1838de1e', + bidderRequestId: '22edbae2733bf6', + auctionId: '1d1a030790a475', + params: { + placementId: '1' + } + }; + + const bidderRequest = { + 'bidderCode': SSMAS_CODE, + 'auctionId': 'd912faa2-174f-4636-b755-7396a0a964d8', + 'bidderRequestId': '109db5a5f5c6788', + 'bids': [ + bid + ], + 'auctionStart': 1684799653734, + 'timeout': 20000, + 'metrics': {}, + 'ortb2': { + 'site': { + 'domain': 'localhost:9999', + 'publisher': { + 'domain': 'localhost:9999' + }, + 'page': 'http://localhost:9999/integrationExamples/noadserver/basic_noadserver.html', + 'ref': 'http://localhost:9999/integrationExamples/noadserver/' + }, + 'device': { + 'w': 1536, + 'h': 711, + 'dnt': 0, + 'ua': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/113.0', + 'language': 'es' + } + }, + 'start': 1684799653737 + }; + + describe('Build Requests', () => { + it('Check bid request', function () { + const request = spec.buildRequests([bid], bidderRequest); + expect(request[0].method).to.equal(SSMAS_REQUEST_METHOD); + expect(request[0].url).to.equal(SSMAS_ENDPOINT); + }); + }); + + describe('register adapter functions', () => { + const adapter = newBidder(spec); + it('is registered', () => { + expect(adapter.callBids).to.exist.and.to.be.a('function'); + }); + }); + + describe('isBidRequestValid', function () { + it('validate bid request building', function () { + expect(spec.isBidRequestValid(bid)).to.be.true; + }); + + it('test bad bid request', function () { + // empty bid + expect(spec.isBidRequestValid({bidId: '', params: {}})).to.be.false; + + // empty bidId + bid.bidId = ''; + expect(spec.isBidRequestValid(bid)).to.be.false; + + // empty placementId + bid.bidId = '1231'; + bid.params.placementId = ''; + expect(spec.isBidRequestValid(bid)).to.be.false; + }); + + it('check bid request bidder is Sem Seo & Mas', function() { + const invalidBid = { + ...bid, bidder: 'invalidBidder' + }; + expect(spec.isBidRequestValid(invalidBid)).to.be.false; + }); + }); + + describe('interpretResponse', function () { + let bidOrtbResponse = { + 'id': 'aa02e2fe-56d9-4713-88f9-d8672ceae8ab', + 'seatbid': [ + { + 'bid': [ + { + 'id': '0001', + 'impid': '3919400af0b73e8', + 'price': 7.01, + 'adid': null, + 'nurl': null, + 'adm': '', + 'adomain': [ + 'ssmas.com' + ], + 'iurl': null, + 'cid': null, + 'crid': '3547894', + 'attr': [], + 'api': 0, + 'protocol': 0, + 'dealid': null, + 'h': 600, + 'w': 300, + 'cat': null, + 'ext': null, + 'builder': { + 'id': '0001', + 'adid': null, + 'impid': '3919400af0b73e8', + 'adomainList': [ + 'ssmas.com' + ], + 'attrList': [] + }, + 'adomainList': [ + 'ssmas.com' + ], + 'attrList': [] + } + ], + 'seat': null, + 'group': 0 + } + ], + 'bidid': '408731cc-c018-4976-bfc6-89f9c61e97a0', + 'cur': 'EUR', + 'nbr': -1 + }; + let bidResponse = { + 'mediaType': 'banner', + 'ad': '', + 'requestId': '37c658fe8ba57b', + 'seatBidId': '0001', + 'cpm': 10, + 'currency': 'EUR', + 'width': 300, + 'height': 250, + 'dealId': null, + 'creative_id': '3547894', + 'creativeId': '3547894', + 'ttl': 30, + 'netRevenue': true, + 'meta': { + 'advertiserDomains': [ + 'ssmas.com' + ] + } + }; + let bidRequest = { + 'imp': [ + { + 'ext': { + 'tid': '937db9c3-c22d-4454-b786-fcad76a349e5', + 'data': { + 'pbadslot': 'test-div' + } + }, + 'id': '3919400af0b73e8', + 'banner': { + 'topframe': 1, + 'format': [ + { + 'w': 300, + 'h': 600 + } + ] + } + }, + { + 'ext': { + 'tid': '0c0d3d1b-0ad0-4786-896d-24c15fc6531d', + 'data': { + 'pbadslot': 'test-div2' + } + }, + 'id': '3919400af0b73e8', + 'banner': { + 'topframe': 1, + 'format': [ + { + 'w': 300, + 'h': 600 + } + ] + } + } + ], + 'site': { + 'domain': 'localhost:9999', + 'publisher': { + 'domain': 'localhost:9999' + }, + 'page': 'http://localhost:9999/integrationExamples/noadserver/basic_noadserver.html', + 'ref': 'http://localhost:9999/integrationExamples/noadserver/', + 'id': 1, + 'ext': { + 'placementId': 13144370 + } + }, + 'device': { + 'w': 1536, + 'h': 711, + 'dnt': 0, + 'ua': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/113.0', + 'language': 'es' + }, + 'id': '8cc2f4b0-084d-4f40-acfa-5bec2023b1ab', + 'test': 0, + 'tmax': 20000, + 'source': { + 'tid': '8cc2f4b0-084d-4f40-acfa-5bec2023b1ab' + } + } + }); + + describe('test onBidWon function', function () { + beforeEach(function() { + sinon.stub(utils, 'triggerPixel'); + }); + afterEach(function() { + utils.triggerPixel.restore(); + }); + it('exists and is a function', () => { + expect(spec.onBidWon).to.exist.and.to.be.a('function'); + }); + it('should return nothing', function () { + var response = spec.onBidWon({}); + expect(response).to.be.an('undefined') + expect(utils.triggerPixel.called).to.equal(false); + }); + }); +}); diff --git a/test/spec/modules/sspBCBidAdapter_spec.js b/test/spec/modules/sspBCBidAdapter_spec.js index a95f08314b5..71619424e4b 100644 --- a/test/spec/modules/sspBCBidAdapter_spec.js +++ b/test/spec/modules/sspBCBidAdapter_spec.js @@ -38,7 +38,7 @@ describe('SSPBC adapter', function () { }, auctionId, bidderRequestId, - bidId: auctionId + '1', + bidId: bidderRequestId + '1', transactionId, }, { @@ -60,7 +60,7 @@ describe('SSPBC adapter', function () { }, auctionId, bidderRequestId, - bidId: auctionId + '2', + bidId: bidderRequestId + '2', transactionId, } ]; @@ -83,7 +83,7 @@ describe('SSPBC adapter', function () { ], auctionId, bidderRequestId, - bidId: auctionId + '1', + bidId: bidderRequestId + '1', transactionId, }; const bid_native = { @@ -122,7 +122,7 @@ describe('SSPBC adapter', function () { ], auctionId, bidderRequestId, - bidId: auctionId + '1', + bidId: bidderRequestId + '1', transactionId, }; const bid_video = { @@ -144,7 +144,7 @@ describe('SSPBC adapter', function () { ], auctionId, bidderRequestId, - bidId: auctionId + '1', + bidId: bidderRequestId + '1', transactionId, }; const bids_timeouted = [{ @@ -155,7 +155,7 @@ describe('SSPBC adapter', function () { siteId: '8816', }], auctionId, - bidId: auctionId + '1', + bidId: bidderRequestId + '1', timeout: 100, }, { @@ -166,7 +166,7 @@ describe('SSPBC adapter', function () { siteId: '8816', }], auctionId, - bidId: auctionId + '2', + bidId: bidderRequestId + '2', timeout: 100, } ]; @@ -198,7 +198,7 @@ describe('SSPBC adapter', function () { }, auctionId, bidderRequestId, - bidId: auctionId + '1', + bidId: bidderRequestId + '1', transactionId, }]; const bidRequest = { @@ -293,7 +293,7 @@ describe('SSPBC adapter', function () { }; const serverResponse = { 'body': { - 'id': auctionId, + 'id': bidderRequestId, 'seatbid': [{ 'bid': [{ 'id': '3347324c-6889-46d2-a800-ae78a5214c06', @@ -333,7 +333,7 @@ describe('SSPBC adapter', function () { }; const serverResponseSingle = { 'body': { - 'id': auctionId, + 'id': bidderRequestId, 'seatbid': [{ 'bid': [{ 'id': '3347324c-6889-46d2-a800-ae78a5214c06', @@ -358,11 +358,11 @@ describe('SSPBC adapter', function () { }; const serverResponseOneCode = { 'body': { - 'id': auctionId, + 'id': bidderRequestId, 'seatbid': [{ 'bid': [{ 'id': '3347324c-6889-46d2-a800-ae78a5214c06', - 'impid': 'bidid-' + auctionId + '1', + 'impid': 'bidid-' + bidderRequestId + '1', 'price': 1, 'adid': 'lxHWkB7OnZeso3QiN1N4', 'nurl': '', @@ -385,11 +385,11 @@ describe('SSPBC adapter', function () { }; const serverResponseVideo = { 'body': { - 'id': auctionId, + 'id': bidderRequestId, 'seatbid': [{ 'bid': [{ 'id': '3347324c-6889-46d2-a800-ae78a5214c06', - 'impid': 'bidid-' + auctionId + '1', + 'impid': 'bidid-' + bidderRequestId + '1', 'price': 1, 'adid': 'lxHWkB7OnZeso3QiN1N4', 'nurl': '', @@ -413,11 +413,11 @@ describe('SSPBC adapter', function () { }; const serverResponseNative = { 'body': { - 'id': auctionId, + 'id': bidderRequestId, 'seatbid': [{ 'bid': [{ 'id': '3347324c-6889-46d2-a800-ae78a5214c06', - 'impid': 'bidid-' + auctionId + '1', + 'impid': 'bidid-' + bidderRequestId + '1', 'price': 1, 'adid': 'lxHWkB7OnZeso3QiN1N4', 'nurl': '', @@ -438,7 +438,7 @@ describe('SSPBC adapter', function () { }; const emptyResponse = { 'body': { - 'id': auctionId, + 'id': bidderRequestId, } } return { @@ -696,7 +696,7 @@ describe('SSPBC adapter', function () { let notificationPayload = spec.onBidWon(bid); expect(notificationPayload).to.have.property('event').that.equals('bidWon'); - expect(notificationPayload).to.have.property('requestId').that.equals(bid.auctionId); + expect(notificationPayload).to.have.property('requestId').that.equals(bid.bidderRequestId); expect(notificationPayload).to.have.property('tagid').that.deep.equals([bid.adUnitCode]); expect(notificationPayload).to.have.property('siteId').that.is.an('array'); expect(notificationPayload).to.have.property('slotId').that.is.an('array'); @@ -717,7 +717,6 @@ describe('SSPBC adapter', function () { let notificationPayload = spec.onTimeout(bids_timeouted); expect(notificationPayload).to.have.property('event').that.equals('timeout'); - expect(notificationPayload).to.have.property('requestId').that.equals(bids_timeouted[0].auctionId); expect(notificationPayload).to.have.property('tagid').that.deep.equals([bids_timeouted[0].adUnitCode, bids_timeouted[1].adUnitCode]); }); }); diff --git a/test/spec/modules/stroeerCoreBidAdapter_spec.js b/test/spec/modules/stroeerCoreBidAdapter_spec.js index 55d79804a38..2ed5f80f152 100644 --- a/test/spec/modules/stroeerCoreBidAdapter_spec.js +++ b/test/spec/modules/stroeerCoreBidAdapter_spec.js @@ -3,6 +3,7 @@ import {spec} from 'modules/stroeerCoreBidAdapter.js'; import * as utils from 'src/utils.js'; import {BANNER, VIDEO} from '../../../src/mediaTypes.js'; import {find} from 'src/polyfill.js'; +import sinon from 'sinon'; describe('stroeerCore bid adapter', function () { let sandbox; @@ -51,8 +52,6 @@ describe('stroeerCore bid adapter', function () { assert.notProperty(bidObject, 'ad'); } - const AUCTION_ID = utils.getUniqueIdentifierStr(); - // Vendor user ids and associated data const userIds = Object.freeze({ criteoId: 'criteo-user-id', @@ -72,7 +71,6 @@ describe('stroeerCore bid adapter', function () { }); const buildBidderRequest = () => ({ - auctionId: AUCTION_ID, bidderRequestId: 'bidder-request-id-123', bidderCode: 'stroeerCore', timeout: 5000, @@ -394,6 +392,10 @@ describe('stroeerCore bid adapter', function () { clock.tick(13500); const bidReq = buildBidderRequest(); + const UUID = 'fb6a39e3-083f-424c-9046-f1095e15f3d5'; + + const generateUUIDStub = sinon.stub(utils, 'generateUUID').returns(UUID); + const serverRequestInfo = spec.buildRequests(bidReq.bids, bidReq); const expectedTimeout = bidderRequest.timeout - (13500 - bidderRequest.auctionStart); @@ -401,7 +403,7 @@ describe('stroeerCore bid adapter', function () { assert.equal(expectedTimeout, 1500); const expectedJsonPayload = { - 'id': AUCTION_ID, + 'id': UUID, 'timeout': expectedTimeout, 'ref': 'https://www.example.com/?search=monkey', 'mpa': true, @@ -429,8 +431,9 @@ describe('stroeerCore bid adapter', function () { // trim away fields with undefined const actualJsonPayload = JSON.parse(JSON.stringify(serverRequestInfo.data)); - assert.deepEqual(actualJsonPayload, expectedJsonPayload); + + generateUUIDStub.restore(); }); describe('video bids', () => { diff --git a/test/spec/modules/stvBidAdapter_spec.js b/test/spec/modules/stvBidAdapter_spec.js index 41f29cced34..3ef865ed2f1 100644 --- a/test/spec/modules/stvBidAdapter_spec.js +++ b/test/spec/modules/stvBidAdapter_spec.js @@ -71,6 +71,24 @@ describe('stvAdapter', function() { 'hp': 1 } ] + }, + 'userId': { + 'id5id': { + 'uid': '1234', + 'ext': { + 'linkType': 'abc' + } + }, + 'netId': '2345', + 'uid2': { + 'id': '3456', + }, + 'sharedid': { + 'id': '4567', + }, + 'idl_env': '5678', + 'criteoId': '6789', + 'utiq': '7890', } }, { @@ -84,7 +102,27 @@ describe('stvAdapter', function() { ], 'bidId': '30b31c1838de1e2', 'bidderRequestId': '22edbae2733bf62', - 'auctionId': '1d1a030790a476' + 'auctionId': '1d1a030790a476', + 'userId': { // with other utiq variant + 'id5id': { + 'uid': '1234', + 'ext': { + 'linkType': 'abc' + } + }, + 'netId': '2345', + 'uid2': { + 'id': '3456', + }, + 'sharedid': { + 'id': '4567', + }, + 'idl_env': '5678', + 'criteoId': '6789', + 'utiq': { + 'id': '7890' + }, + } }, { 'bidder': 'stv', 'params': { @@ -181,7 +219,7 @@ describe('stvAdapter', 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&schain=1.0,0!reseller.com,aaaaa,1,BidRequest4,,,&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'); + expect(data).to.equal('_f=html&alternative=prebid_js&_ps=6682&srw=300&srh=250&idt=100&bid_id=30b31c1838de1e1&pbver=test&schain=1.0,0!reseller.com,aaaaa,1,BidRequest4,,&uids=id5%3A1234,id5_linktype%3Aabc,netid%3A2345,uid2%3A3456,sharedid%3A4567,liverampid%3A5678,criteoid%3A6789,utiq%3A7890&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]; @@ -189,7 +227,7 @@ describe('stvAdapter', 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'); + expect(data).to.equal('_f=html&alternative=prebid_js&_ps=101&srw=300&srh=250&idt=100&bid_id=30b31c1838de1e2&pbver=test&uids=id5%3A1234,id5_linktype%3Aabc,netid%3A2345,uid2%3A3456,sharedid%3A4567,liverampid%3A5678,criteoid%3A6789,utiq%3A7890&gdpr_consent=BOJ%2FP2HOJ%2FP2HABABMAAAAAZ%2BA%3D%3D&gdpr=true&prebidDevMode=1&media_types%5Bbanner%5D=300x250'); }); // Without gdprConsent diff --git a/test/spec/modules/taboolaBidAdapter_spec.js b/test/spec/modules/taboolaBidAdapter_spec.js index 7d31e291667..dd91c410d08 100644 --- a/test/spec/modules/taboolaBidAdapter_spec.js +++ b/test/spec/modules/taboolaBidAdapter_spec.js @@ -1,5 +1,5 @@ import {expect} from 'chai'; -import {spec, internal, END_POINT_URL, userData} from 'modules/taboolaBidAdapter.js'; +import {spec, internal, END_POINT_URL, userData, EVENT_ENDPOINT} from 'modules/taboolaBidAdapter.js'; import {config} from '../../../src/config' import * as utils from '../../../src/utils' import {server} from '../../mocks/xhr' @@ -113,6 +113,50 @@ describe('Taboola Adapter', function () { }); }); + describe('onTimeout', function () { + it('onTimeout exist as a function', () => { + expect(spec.onTimeout).to.exist.and.to.be.a('function'); + }); + it('should send timeout', function () { + const timeoutData = [{ + bidder: 'taboola', + bidId: 'da43860a-4644-442a-b5e0-93f268cf8d19', + params: [{ + publisherId: 'publisherId' + }], + adUnitCode: 'adUnit-code', + timeout: 3000, + auctionId: '12a34b56c' + }] + spec.onTimeout(timeoutData); + expect(server.requests[0].method).to.equal('POST'); + expect(server.requests[0].url).to.equal(EVENT_ENDPOINT + '/timeout'); + expect(JSON.parse(server.requests[0].requestBody)).to.deep.equal(timeoutData); + }); + }); + + describe('onBidderError', function () { + it('onBidderError exist as a function', () => { + expect(spec.onBidderError).to.exist.and.to.be.a('function'); + }); + it('should send bidder error', function () { + const error = { + status: 204, + statusText: 'No Content' + }; + const bidderRequest = { + bidder: 'taboola', + params: { + publisherId: 'publisherId' + } + } + spec.onBidderError({error, bidderRequest}); + expect(server.requests[0].method).to.equal('POST'); + expect(server.requests[0].url).to.equal(EVENT_ENDPOINT + '/bidError'); + expect(JSON.parse(server.requests[0].requestBody)).to.deep.equal(error, bidderRequest); + }); + }); + describe('buildRequests', function () { const defaultBidRequest = { ...createBidRequest(), @@ -129,10 +173,10 @@ describe('Taboola Adapter', function () { } it('should build display request', function () { + const res = spec.buildRequests([defaultBidRequest], commonBidderRequest); const expectedData = { - id: 'mock-uuid', 'imp': [{ - 'id': 1, + 'id': res.data.imp[0].id, 'banner': { format: [{ w: displayBidRequestParams.sizes[0][0], @@ -149,6 +193,8 @@ describe('Taboola Adapter', function () { 'bidfloorcur': 'USD', 'ext': {} }], + id: 'mock-uuid', + 'test': 0, 'site': { 'id': commonBidRequest.params.publisherId, 'name': commonBidRequest.params.publisherId, @@ -171,10 +217,8 @@ describe('Taboola Adapter', function () { 'ext': {} }; - const res = spec.buildRequests([defaultBidRequest], commonBidderRequest); - - expect(res.url).to.equal(`${END_POINT_URL}/${commonBidRequest.params.publisherId}`); - expect(res.data).to.deep.equal(JSON.stringify(expectedData)); + expect(res.url).to.equal(`${END_POINT_URL}?publisher=${commonBidRequest.params.publisherId}`); + expect(JSON.stringify(res.data)).to.deep.equal(JSON.stringify(expectedData)); }); it('should pass optional parameters in request', function () { @@ -189,9 +233,8 @@ describe('Taboola Adapter', function () { }; const res = spec.buildRequests([bidRequest], commonBidderRequest); - const resData = JSON.parse(res.data); - expect(resData.imp[0].bidfloor).to.deep.equal(0.25); - expect(resData.imp[0].bidfloorcur).to.deep.equal('EUR'); + expect(res.data.imp[0].bidfloor).to.deep.equal(0.25); + expect(res.data.imp[0].bidfloorcur).to.deep.equal('EUR'); }); it('should pass bid floor', function () { @@ -206,9 +249,8 @@ describe('Taboola Adapter', function () { } }; const res = spec.buildRequests([bidRequest], commonBidderRequest); - const resData = JSON.parse(res.data); - expect(resData.imp[0].bidfloor).to.deep.equal(2.7); - expect(resData.imp[0].bidfloorcur).to.deep.equal('USD'); + expect(res.data.imp[0].bidfloor).to.deep.equal(2.7); + expect(res.data.imp[0].bidfloorcur).to.deep.equal('USD'); }); it('should pass bid floor even if it is a bid floor param', function () { @@ -228,9 +270,8 @@ describe('Taboola Adapter', function () { } }; const res = spec.buildRequests([bidRequest], commonBidderRequest); - const resData = JSON.parse(res.data); - expect(resData.imp[0].bidfloor).to.deep.equal(2.7); - expect(resData.imp[0].bidfloorcur).to.deep.equal('USD'); + expect(res.data.imp[0].bidfloor).to.deep.equal(2.7); + expect(res.data.imp[0].bidfloorcur).to.deep.equal('USD'); }); it('should pass impression position', function () { @@ -244,8 +285,7 @@ describe('Taboola Adapter', function () { }; const res = spec.buildRequests([bidRequest], commonBidderRequest); - const resData = JSON.parse(res.data); - expect(resData.imp[0].banner.pos).to.deep.equal(2); + expect(res.data.imp[0].banner.pos).to.deep.equal(2); }); it('should pass gpid if configured', function () { @@ -261,8 +301,23 @@ describe('Taboola Adapter', function () { }; const res = spec.buildRequests([bidRequest], commonBidderRequest); - const resData = JSON.parse(res.data); - expect(resData.imp[0].ext.gpid).to.deep.equal('/homepage/#1'); + expect(res.data.imp[0].ext.gpid).to.deep.equal('/homepage/#1'); + }); + + it('should pass new parameter to imp ext', function () { + const ortb2Imp = { + ext: { + example: 'example' + } + } + const bidRequest = { + ...defaultBidRequest, + ortb2Imp: ortb2Imp, + params: {...commonBidRequest.params} + }; + + const res = spec.buildRequests([bidRequest], commonBidderRequest); + expect(res.data.imp[0].ext.example).to.deep.equal('example'); }); it('should pass bidder timeout', function () { @@ -271,8 +326,25 @@ describe('Taboola Adapter', function () { timeout: 500 } const res = spec.buildRequests([defaultBidRequest], bidderRequest); - const resData = JSON.parse(res.data); - expect(resData.tmax).to.equal(500); + expect(res.data.tmax).to.equal(500); + }); + + it('should pass bidder tmax as int', function () { + const bidderRequest = { + ...commonBidderRequest, + timeout: '500' + } + const res = spec.buildRequests([defaultBidRequest], bidderRequest); + expect(res.data.tmax).to.equal(500); + }); + + it('should pass bidder timeout as null', function () { + const bidderRequest = { + ...commonBidderRequest, + timeout: null + } + const res = spec.buildRequests([defaultBidRequest], bidderRequest); + expect(res.data.tmax).to.equal(undefined); }); describe('first party data', function () { @@ -286,10 +358,9 @@ describe('Taboola Adapter', function () { } } 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) + expect(res.data.bcat).to.deep.equal(bidderRequest.ortb2.bcat) + expect(res.data.badv).to.deep.equal(bidderRequest.ortb2.badv) + expect(res.data.wlang).to.deep.equal(bidderRequest.ortb2.wlang) }); it('should pass pageType if exists in ortb2', function () { @@ -304,8 +375,20 @@ describe('Taboola Adapter', function () { } } const res = spec.buildRequests([defaultBidRequest], bidderRequest); - const resData = JSON.parse(res.data); - expect(resData.ext.pageType).to.deep.equal(bidderRequest.ortb2.ext.data.pageType); + expect(res.data.ext.pageType).to.deep.equal(bidderRequest.ortb2.ext.data.pageType); + }); + + it('should pass additional parameter in request', function () { + const bidderRequest = { + ...commonBidderRequest, + ortb2: { + ext: { + example: 'example' + } + } + } + const res = spec.buildRequests([defaultBidRequest], bidderRequest); + expect(res.data.ext.example).to.deep.equal(bidderRequest.ortb2.ext.example); }); }); @@ -322,9 +405,8 @@ describe('Taboola Adapter', function () { }; const res = spec.buildRequests([defaultBidRequest], bidderRequest) - const resData = JSON.parse(res.data) - expect(resData.user.ext.consent).to.equal('consentString') - expect(resData.regs.ext.gdpr).to.equal(1) + expect(res.data.user.ext.consent).to.equal('consentString') + expect(res.data.regs.ext.gdpr).to.equal(1) }); it('should pass GPP consent if exist in ortb2', function () { @@ -336,9 +418,8 @@ describe('Taboola Adapter', function () { } 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]) + expect(res.data.regs.ext.gpp).to.equal('testGpp') + expect(res.data.regs.ext.gpp_sid).to.deep.equal([1, 2, 3]) }); it('should pass us privacy consent', function () { @@ -349,16 +430,14 @@ describe('Taboola Adapter', function () { uspConsent: 'consentString' } const res = spec.buildRequests([defaultBidRequest], bidderRequest); - const resData = JSON.parse(res.data); - expect(resData.regs.ext.us_privacy).to.equal('consentString'); + expect(res.data.regs.ext.us_privacy).to.equal('consentString'); }); it('should pass coppa consent', function () { config.setConfig({coppa: true}) const res = spec.buildRequests([defaultBidRequest], commonBidderRequest) - const resData = JSON.parse(res.data); - expect(resData.regs.coppa).to.equal(1) + expect(res.data.regs.coppa).to.equal(1) config.resetConfig() }); @@ -375,8 +454,7 @@ describe('Taboola Adapter', function () { timeout: 500 } const res = spec.buildRequests([defaultBidRequest], bidderRequest); - const resData = JSON.parse(res.data); - expect(resData.user.buyeruid).to.equal(51525152); + expect(res.data.user.buyeruid).to.equal(51525152); }); it('should get user id from cookie if local storage isn`t defined', function () { @@ -390,9 +468,22 @@ describe('Taboola Adapter', function () { ...commonBidderRequest }; const res = spec.buildRequests([defaultBidRequest], bidderRequest); - const resData = JSON.parse(res.data); + expect(res.data.user.buyeruid).to.equal('12121212'); + }); + + it('should get user id from tgid cookie if local storage isn`t defined', function () { + getDataFromLocalStorage.returns(51525152); + hasLocalStorage.returns(false); + localStorageIsEnabled.returns(false); + cookiesAreEnabled.returns(true); + getCookie.returns('d966c5be-c49f-4f73-8cd1-37b6b5790653-tuct9f7bf10'); + + const bidderRequest = { + ...commonBidderRequest + }; + const res = spec.buildRequests([defaultBidRequest], bidderRequest); - expect(resData.user.buyeruid).to.equal('12121212'); + expect(res.data.user.buyeruid).to.equal('d966c5be-c49f-4f73-8cd1-37b6b5790653-tuct9f7bf10'); }); it('should get user id from TRC if local storage and cookie isn`t defined', function () { @@ -408,8 +499,7 @@ describe('Taboola Adapter', function () { ...commonBidderRequest } const res = spec.buildRequests([defaultBidRequest], bidderRequest); - const resData = JSON.parse(res.data); - expect(resData.user.buyeruid).to.equal(window.TRC.user_id); + expect(res.data.user.buyeruid).to.equal(window.TRC.user_id); delete window.TRC; }); @@ -422,8 +512,7 @@ describe('Taboola Adapter', function () { ...commonBidderRequest } const res = spec.buildRequests([defaultBidRequest], bidderRequest); - const resData = JSON.parse(res.data); - expect(resData.user.buyeruid).to.equal(0); + expect(res.data.user.buyeruid).to.equal(0); }); it('should set buyeruid to be 0 if it`s a new user', function () { @@ -431,13 +520,29 @@ describe('Taboola Adapter', function () { ...commonBidderRequest } const res = spec.buildRequests([defaultBidRequest], bidderRequest); - const resData = JSON.parse(res.data); - expect(resData.user.buyeruid).to.equal(0); + expect(res.data.user.buyeruid).to.equal(0); }); }); }) describe('interpretResponse', function () { + const defaultBidRequest = { + ...createBidRequest(), + ...displayBidRequestParams, + }; + const commonBidderRequest = { + bidderRequestId: 'mock-uuid', + refererInfo: { + page: 'https://example.com/ref', + ref: 'https://ref', + domain: 'example.com', + } + }; + const bidderRequest = { + ...commonBidderRequest + }; + const request = spec.buildRequests([defaultBidRequest], bidderRequest); + const serverResponse = { body: { 'id': '49ffg4d58ef9a163a69fhgfghd4fad03621b9e036f24f7_15', @@ -446,7 +551,7 @@ describe('Taboola Adapter', function () { 'bid': [ { 'id': '0b3dd94348-134b-435f-8db5-6bf5afgfc39e86c', - 'impid': '1', + 'impid': request.data.imp[0].id, 'price': 0.342068, 'adid': '2785119545551083381', 'adm': '\u003chtml\u003e\n\u003chead\u003e\n\u003cmeta charset\u003d"UTF-8"\u003e\n\u003cmeta http-equiv\u003d"Content-Type" content\u003d"text/html; charset\u003dutf-8"/\u003e\u003c/head\u003e\n\u003cbody style\u003d"margin: 0px; overflow:hidden;"\u003e \n\u003cscript type\u003d"text/javascript"\u003e\nwindow.tbl_trc_domain \u003d \u0027us-trc.taboola.com\u0027;\nwindow._taboola \u003d window._taboola || [];\n_taboola.push({article:\u0027auto\u0027});\n!function (e, f, u, i) {\nif (!document.getElementById(i)){\ne.async \u003d 1;\ne.src \u003d u;\ne.id \u003d i;\nf.parentNode.insertBefore(e, f);\n}\n}(document.createElement(\u0027script\u0027),\ndocument.getElementsByTagName(\u0027script\u0027)[0],\n\u0027//cdn.taboola.com/libtrc/wattpad-placement-255/loader.js\u0027,\n\u0027tb_loader_script\u0027);\nif(window.performance \u0026\u0026 typeof window.performance.mark \u003d\u003d \u0027function\u0027)\n{window.performance.mark(\u0027tbl_ic\u0027);}\n\u003c/script\u003e\n\n\u003cdiv id\u003d"taboola-below-article-thumbnails" style\u003d"height: 250px; width: 300px;"\u003e\u003c/div\u003e\n\u003cscript type\u003d"text/javascript"\u003e\nwindow._taboola \u003d window._taboola || [];\n_taboola.push({\nmode: \u0027Rbox_300x250_1x1\u0027,\ncontainer: \u0027taboola-below-article-thumbnails\u0027,\nplacement: \u0027wattpad.com_P18694_S257846_W300_H250_N1_TB\u0027,\ntarget_type: \u0027mix\u0027,\n"rtb-win":{ \nbi:\u002749ff4d58ef9a163a696d4fad03621b9e036f24f7_15\u0027,\ncu:\u0027USD\u0027,\nwp:\u0027${AUCTION_PRICE:BF}\u0027,\nwcb:\u0027~!audex-display-impression!~\u0027,\nrt:\u00271643227025284\u0027,\nrdc:\u0027us.taboolasyndication.com\u0027,\nti:\u00274212\u0027,\nex:\u0027MagniteSCoD\u0027,\nbs:\u0027xapi:257846:lvvSm6Ak7_wE\u0027,\nbp:\u002718694\u0027,\nbd:\u0027wattpad.com\u0027,\nsi:\u00279964\u0027\n} \n,\nrec: {"trc":{"si":"a69c7df43b2334f0aa337c37e2d80c21","sd":"v2_a69c7df43b2334f0aa337c37e2d80c21_3c70f7c7d64a65b15e4a4175c9a2cfa51072f04bMagniteSCoD_1643227025_1643227025_CJS1tQEQ5NdWGPLA0d76xo-9ngEgASgEMCY4iegHQIroB0iB09kDUKPPB1gAYABop-G2i_Hl-eVucAA","ui":"3c70f7c7d64a65b15e4a4175c9a2cfa51072f04bMagniteSCoD","plc":"PHON","wi":"-643136642229425433","cc":"CA","route":"US:US:V","el2r":["bulk-metrics","debug","social","metrics","perf"],"uvpw":"1","pi":"1420260","cpb":"GNO629MGIJz__________wEqGXVzLnRhYm9vbGFzeW5kaWNhdGlvbi5jb20yC3RyYy1zY29kMTI5OIDwmrUMQInoB0iK6AdQgdPZA1ijzwdjCN3__________wEQ3f__________ARgjZGMI3AoQoBAYFmRjCNIDEOAGGAhkYwiWFBCcHBgYZGMI9AUQiwoYC2RjCNkUEPkcGB1kYwj0FBCeHRgfZGorNDlmZjRkNThlZjlhMTYzYTY5NmQ0ZmFkMDM2MjFiOWUwMzZmMjRmN18xNXgCgAHpbIgBrPvTxQE","dcga":{"pubConfigOverride":{"border-color":"black","font-weight":"bold","inherit-title-color":"true","module-name":"cta-lazy-module","enable-call-to-action-creative-component":"true","disable-cta-on-custom-module":"true"}},"tslt":{"p-video-overlay":{"cancel":"סגור","goto":"×ĸבור לד×Ŗ"},"read-more":{"DEFAULT_CAPTION":"%D7%A7%D7%A8%D7%90%20%D7%A2%D7%95%D7%93"},"next-up":{"BTN_TEXT":"לקריא×Ē ה×Ēוכן הבא"},"time-ago":{"now":"×ĸכשיו","today":"היום","yesterday":"א×Ēמול","minutes":"לפני {0} דקו×Ē","hour":"לפני ׊×ĸה","hours":"לפני {0} ׊×ĸו×Ē","days":"לפני {0} ימים"},"explore-more":{"TITLE_TEXT":"המשיכו לקרוא","POPUP_TEXT":"אל ×Ēפספסו הזדמנו×Ē לקרוא ×ĸוד ×Ēוכן מ×ĸולה, רג×ĸ לפני ׊×Ē×ĸזבו"}},"evh":"-1964913910","vl":[{"ri":"185db6d274ce94b27caaabd9eed7915b","uip":"wattpad.com_P18694_S257846_W300_H250_N1_TB","ppb":"COIF","estimation_method":"EcpmEstimationMethodType_ESTIMATION","baseline_variant":"false","original_ecpm":"0.4750949889421463","v":[{"thumbnail":"https://cdn.taboola.com/libtrc/static/thumbnails/a2b272be514ca3ebe3f97a4a32a41db5.jpg","all-thumbnails":"https://cdn.taboola.com/libtrc/static/thumbnails/a2b272be514ca3ebe3f97a4a32a41db5.jpg!-#@1600x1000","origin":"default","thumb-size":"1600x1000","title":"Get Roofing Services At Prices You Can Afford In Edmonton","type":"text","published-date":"1641997069","branding-text":"Roofing Services | Search Ads","url":"https://inneth-conded.xyz/9ad2e613-8777-4fe7-9a52-386c88879289?site\u003dwattpad-placement-255\u0026site_id\u003d1420260\u0026title\u003dGet+Roofing+Services+At+Prices+You+Can+Afford+In+Edmonton\u0026platform\u003dSmartphone\u0026campaign_id\u003d15573949\u0026campaign_item_id\u003d3108610633\u0026thumbnail\u003dhttp%3A%2F%2Fcdn.taboola.com%2Flibtrc%2Fstatic%2Fthumbnails%2Fa2b272be514ca3ebe3f97a4a32a41db5.jpg\u0026cpc\u003d{cpc}\u0026click_id\u003dGiCIypnAQogsMTFL3e_mPaVM2qLvK3KRU6LWzEMUgeB6piCit1Uox6CNr5v5n-x1\u0026tblci\u003dGiCIypnAQogsMTFL3e_mPaVM2qLvK3KRU6LWzEMUgeB6piCit1Uox6CNr5v5n-x1#tblciGiCIypnAQogsMTFL3e_mPaVM2qLvK3KRU6LWzEMUgeB6piCit1Uox6CNr5v5n-x1","duration":"0","sig":"328243c4127ff16e3fdcd7270bab908f6f3fc5b4c98d","item-id":"~~V1~~2785119550041083381~~PnBkfBE9JnQxpahv0adkcuIcmMhroRAHXwLZd-7zhunTxvAnL2wqac4MyzR7uD46gj3kUkbS3FhelBtnsiJV6MhkDZRZzzIqDobN6rWmCPA3hYz5D3PLat6nhIftiT1lwdxwdlxkeV_Mfb3eos_TQavImGhxk0e7psNAZxHJ9RKL2w3lppALGgQJoy2o6lkf-pOqODtX1VkgWpEEM4WsVoWOnUTAwdyGd-8yrze8CWNp752y28hl7lleicyO1vByRdbgwlJdnqyroTPEQNNEn1JRxBOSYSWt-Xm3vkPm-G4","uploader":"","is-syndicated":"true","publisher":"search","id":"~~V1~~2785119550041083381~~PnBkfBE9JnQxpahv0adkcuIcmMhroRAHXwLZd-7zhunTxvAnL2wqac4MyzR7uD46gj3kUkbS3FhelBtnsiJV6MhkDZRZzzIqDobN6rWmCPA3hYz5D3PLat6nhIftiT1lwdxwdlxkeV_Mfb3eos_TQavImGhxk0e7psNAZxHJ9RKL2w3lppALGgQJoy2o6lkf-pOqODtX1VkgWpEEM4WsVoWOnUTAwdyGd-8yrze8CWNp752y28hl7lleicyO1vByRdbgwlJdnqyroTPEQNNEn1JRxBOSYSWt-Xm3vkPm-G4","category":"home","views":"0","itp":[{"u":"https://trc.taboola.com/1326786/log/3/unip?en\u003dclickersusa","t":"c"}],"description":""}]}],"cpcud":{"upc":"0.0","upr":"0.0"}}}\n});\n\u003c/script\u003e\n\n\u003cscript type\u003d"text/javascript"\u003e\nwindow._taboola \u003d window._taboola || [];\n_taboola.push({flush: true});\n\u003c/script\u003e\n\n\u003c/body\u003e\n\u003c/html\u003e', @@ -471,15 +576,6 @@ describe('Taboola Adapter', function () { } }; - const request = { - bids: [ - { - ...commonBidRequest, - ...displayBidRequestParams - } - ] - } - it('should return empty array if no valid bids', function () { const res = spec.interpretResponse(serverResponse, []) expect(res).to.be.an('array').that.is.empty @@ -513,18 +609,7 @@ describe('Taboola Adapter', function () { }); it('should interpret multi impression request', function () { - const multiRequest = { - bids: [ - { - ...createBidRequest(), - ...displayBidRequestParams - }, - { - ...createBidRequest(), - ...displayBidRequestParams - } - ] - } + const multiRequest = spec.buildRequests([defaultBidRequest, defaultBidRequest], bidderRequest); const multiServerResponse = { body: { @@ -534,7 +619,7 @@ describe('Taboola Adapter', function () { 'bid': [ { 'id': '0b3dd94348-134b-435f-8db5-6bf5afgfc39e86c', - 'impid': '2', + 'impid': multiRequest.data.imp[0].id, 'price': 0.342068, 'adid': '2785119545551083381', 'adm': 'ADM2', @@ -551,7 +636,7 @@ describe('Taboola Adapter', function () { }, { 'id': '0b3dd94348-134b-435f-8db5-6bf5afgfc39e86c', - 'impid': '1', + 'impid': multiRequest.data.imp[1].id, 'price': 0.342068, 'adid': '2785119545551083381', 'adm': 'ADM1', @@ -582,6 +667,8 @@ describe('Taboola Adapter', function () { requestId: multiRequest.bids[1].bidId, cpm: bid.price, creativeId: bid.crid, + creative_id: bid.crid, + seatBidId: multiServerResponse.body.seatbid[0].bid[0].id, ttl: 60, netRevenue: true, currency: multiServerResponse.body.cur, @@ -598,6 +685,8 @@ describe('Taboola Adapter', function () { requestId: multiRequest.bids[0].bidId, cpm: bid.price, creativeId: bid.crid, + creative_id: bid.crid, + seatBidId: multiServerResponse.body.seatbid[0].bid[1].id, ttl: 60, netRevenue: true, currency: multiServerResponse.body.cur, @@ -621,8 +710,10 @@ describe('Taboola Adapter', function () { const expectedRes = [ { requestId: request.bids[0].bidId, + seatBidId: serverResponse.body.seatbid[0].bid[0].id, cpm: bid.price, creativeId: bid.crid, + creative_id: bid.crid, ttl: 60, netRevenue: true, currency: serverResponse.body.cur, @@ -648,8 +739,10 @@ describe('Taboola Adapter', function () { const expectedRes = [ { requestId: request.bids[0].bidId, + seatBidId: serverResponse.body.seatbid[0].bid[0].id, cpm: bid.price, creativeId: bid.crid, + creative_id: bid.crid, ttl: 125, netRevenue: true, currency: serverResponse.body.cur, @@ -668,18 +761,7 @@ describe('Taboola Adapter', function () { }); it('should replace AUCTION_PRICE macro in adm', function () { - const multiRequest = { - bids: [ - { - ...createBidRequest(), - ...displayBidRequestParams - }, - { - ...createBidRequest(), - ...displayBidRequestParams - } - ] - } + const multiRequest = spec.buildRequests([defaultBidRequest, defaultBidRequest], bidderRequest); const multiServerResponseWithMacro = { body: { 'id': '49ffg4d58ef9a163a69fhgfghd4fad03621b9e036f24f7_15', @@ -688,7 +770,7 @@ describe('Taboola Adapter', function () { 'bid': [ { 'id': '0b3dd94348-134b-435f-8db5-6bf5afgfc39e86c', - 'impid': '2', + 'impid': multiRequest.data.imp[0].id, 'price': 0.34, 'adid': '2785119545551083381', 'adm': 'ADM2,\\nwp:\'${AUCTION_PRICE}\'', @@ -705,7 +787,7 @@ describe('Taboola Adapter', function () { }, { 'id': '0b3dd94348-134b-435f-8db5-6bf5afgfc39e86c', - 'impid': '1', + 'impid': multiRequest.data.imp[1].id, 'price': 0.35, 'adid': '2785119545551083381', 'adm': 'ADM2,\\nwp:\'${AUCTION_PRICE}\'', @@ -735,6 +817,8 @@ describe('Taboola Adapter', function () { requestId: multiRequest.bids[1].bidId, cpm: multiServerResponseWithMacro.body.seatbid[0].bid[0].price, creativeId: bid.crid, + creative_id: bid.crid, + seatBidId: multiServerResponseWithMacro.body.seatbid[0].bid[0].id, ttl: 60, netRevenue: true, currency: multiServerResponseWithMacro.body.cur, @@ -751,6 +835,8 @@ describe('Taboola Adapter', function () { requestId: multiRequest.bids[0].bidId, cpm: multiServerResponseWithMacro.body.seatbid[0].bid[1].price, creativeId: bid.crid, + creative_id: bid.crid, + seatBidId: multiServerResponseWithMacro.body.seatbid[0].bid[1].id, ttl: 60, netRevenue: true, currency: multiServerResponseWithMacro.body.cur, @@ -771,17 +857,28 @@ describe('Taboola Adapter', function () { describe('getUserSyncs', function () { const usersyncUrl = 'https://trc.taboola.com/sg/prebidJS/1/cm'; + const iframeUrl = 'https://cdn.taboola.com/scripts/prebid_iframe_sync.html'; - it('should not return user sync if pixelEnabled is false', function () { - const res = spec.getUserSyncs({pixelEnabled: false}); + it('should not return user sync if pixelEnabled is false and iframe disabled', function () { + const res = spec.getUserSyncs({pixelEnabled: false, iframeEnabled: 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}); + const res = spec.getUserSyncs({pixelEnabled: true, iframeEnabled: false}); expect(res).to.deep.equal([{type: 'image', url: usersyncUrl}]); }); + it('should return user sync if iframeEnabled is true', function () { + const res = spec.getUserSyncs({iframeEnabled: true, pixelEnabled: false}); + expect(res).to.deep.equal([{type: 'iframe', url: iframeUrl}]); + }); + + it('should return both user syncs if iframeEnabled is true and pixelEnabled is true', function () { + const res = spec.getUserSyncs({iframeEnabled: true, pixelEnabled: true}); + expect(res).to.deep.equal([{type: 'iframe', url: iframeUrl}, {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` diff --git a/test/spec/modules/tagorasBidAdapter_spec.js b/test/spec/modules/tagorasBidAdapter_spec.js new file mode 100644 index 00000000000..7559567dcff --- /dev/null +++ b/test/spec/modules/tagorasBidAdapter_spec.js @@ -0,0 +1,651 @@ +import {expect} from 'chai'; +import { + spec as adapter, + createDomain, + hashCode, + extractPID, + extractCID, + extractSubDomain, + getStorageItem, + setStorageItem, + tryParseJSON, + getUniqueDealId, +} from 'modules/tagorasBidAdapter'; +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'; + +export const TEST_ID_SYSTEMS = ['britepoolid', 'criteoId', 'id5id', 'idl_env', 'lipb', 'netId', 'parrableId', 'pubcid', 'tdid', 'pubProvidedId']; + +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', + 'sizes': [[300, 250], [300, 600]], + 'bidderRequestId': '1fdb5ff1b6eaa7', + 'bidRequestsCount': 4, + 'bidderRequestsCount': 3, + 'bidderWinsCount': 1, + 'requestId': 'b0777d85-d061-450e-9bc7-260dd54bbb7a', + 'schain': 'a0819c69-005b-41ed-af06-1be1e0aefefc', + 'mediaTypes': [BANNER], + 'ortb2Imp': { + 'ext': { + 'gpid': '0123456789', + 'tid': 'c881914b-a3b5-4ecf-ad9c-1c2f37c6aabf' + } + } +}; + +const VIDEO_BID = { + 'bidId': '2d52001cabd527', + 'adUnitCode': '63550ad1ff6642d368cba59dh5884270560', + 'bidderRequestId': '12a8ae9ada9c13', + 'transactionId': '56e184c6-bde9-497b-b9b9-cf47a61381ee', + '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 + } + }, + 'ortb2Imp': { + 'ext': { + 'tid': '56e184c6-bde9-497b-b9b9-cf47a61381ee' + } + } +} + +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': ['tagoras.io'], + '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('TagorasBidAdapter', 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 = { + tagoras: { + 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', + 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 + } + }, + gpid: '' + } + }); + }); + + 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', + 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], + gpid: '0123456789', + 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.tagoras.io/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.tagoras.io/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.tagoras.io/api/sync/image/?cid=testcid123&gdpr=0&gdpr_consent=&us_privacy=', + 'type': 'image' + }]); + }); + + it('should generate url with consent data', function () { + const gdprConsent = { + gdprApplies: true, + consentString: 'consent_string' + }; + const uspConsent = 'usp_string'; + const gppConsent = { + gppString: 'gpp_string', + applicableSections: [7] + } + + const result = adapter.getUserSyncs({pixelEnabled: true}, [SERVER_RESPONSE], gdprConsent, uspConsent, gppConsent); + + expect(result).to.deep.equal([{ + 'url': 'https://sync.tagoras.io/api/sync/image/?cid=testcid123&gdpr=1&gdpr_consent=consent_string&us_privacy=usp_string&gpp=gpp_string&gpp_sid=7', + 'type': 'image' + }]); + }); + }); + + describe('interpret response', function () { + 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 get meta from response metaData', function () { + const serverResponse = utils.deepClone(SERVER_RESPONSE); + serverResponse.body.results[0].metaData = { + advertiserDomains: ['tagoras.io'], + agencyName: 'Agency Name', + }; + const responses = adapter.interpretResponse(serverResponse, REQUEST); + expect(responses[0].meta).to.deep.equal({ + advertiserDomains: ['tagoras.io'], + 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); + 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: ['tagoras.io'] + } + }); + }); + + 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 () { + TEST_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 = { + tagoras: { + 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 = { + tagoras: { + 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/tappxBidAdapter_spec.js b/test/spec/modules/tappxBidAdapter_spec.js index 58a62fb2869..46fac8de1e2 100644 --- a/test/spec/modules/tappxBidAdapter_spec.js +++ b/test/spec/modules/tappxBidAdapter_spec.js @@ -466,4 +466,23 @@ describe('Tappx bid adapter', function () { assert.isString(_extractPageUrl(validBidRequests, bidderRequest)); }); }) + + describe('Empty params values from bid tests', function() { + let validBidRequest = JSON.parse(JSON.stringify(c_BIDREQUEST)); + + it('should return false when tappxkey is empty', function () { + validBidRequest.bids[0].params.tappxkey = ''; + assert.isFalse(spec.isBidRequestValid(validBidRequest.bids[0])); + }); + + it('should return false when host is empty', function () { + validBidRequest.bids[0].params.host = ''; + assert.isFalse(spec.isBidRequestValid(validBidRequest.bids[0])); + }); + + it('should return false when endpoint is empty', function () { + validBidRequest.bids[0].params.endpoint = ''; + assert.isFalse(spec.isBidRequestValid(validBidRequest.bids[0])); + }); + }); }); diff --git a/test/spec/modules/teadsBidAdapter_spec.js b/test/spec/modules/teadsBidAdapter_spec.js index a4b94135bad..98f706f5193 100644 --- a/test/spec/modules/teadsBidAdapter_spec.js +++ b/test/spec/modules/teadsBidAdapter_spec.js @@ -1,23 +1,21 @@ import {expect} from 'chai'; import {spec, storage} from 'modules/teadsBidAdapter.js'; import {newBidder} from 'src/adapters/bidderFactory.js'; -import {getStorageManager} from 'src/storageManager'; +import { off } from '../../../src/events'; const ENDPOINT = 'https://a.teads.tv/hb/bid-request'; const AD_SCRIPT = '"'; describe('teadsBidAdapter', () => { const adapter = newBidder(spec); - let cookiesAreEnabledStub, getCookieStub; + let sandbox; beforeEach(function () { - cookiesAreEnabledStub = sinon.stub(storage, 'cookiesAreEnabled'); - getCookieStub = sinon.stub(storage, 'getCookie'); + sandbox = sinon.sandbox.create(); }); afterEach(function () { - cookiesAreEnabledStub.restore(); - getCookieStub.restore(); + sandbox.restore(); }); describe('inherited functions', () => { @@ -108,21 +106,21 @@ describe('teadsBidAdapter', () => { } ]; - let bidderResquestDefault = { + let bidderRequestDefault = { 'auctionId': '1d1a030790a475', 'bidderRequestId': '22edbae2733bf6', 'timeout': 3000 }; it('should send bid request to ENDPOINT via POST', function() { - const request = spec.buildRequests(bidRequests, bidderResquestDefault); + const request = spec.buildRequests(bidRequests, bidderRequestDefault); expect(request.url).to.equal(ENDPOINT); expect(request.method).to.equal('POST'); }); it('should not send auctionId in bid request ', function() { - const request = spec.buildRequests(bidRequests, bidderResquestDefault); + const request = spec.buildRequests(bidRequests, bidderRequestDefault); const payload = JSON.parse(request.data); expect(payload.data[0].auctionId).to.not.exist @@ -169,6 +167,55 @@ describe('teadsBidAdapter', () => { expect(payload.gdpr_iab.apiVersion).to.equal(2); }); + it('should add videoPlcmt to payload', function () { + let bidRequestWithVideoPlcmt = Object.assign({}, bidRequests[0], { + mediaTypes: { + video: { + plcmt: 1 + } + } + }); + + const request = spec.buildRequests([bidRequestWithVideoPlcmt], bidderRequestDefault); + const payload = JSON.parse(request.data); + + expect(payload.data[0].videoPlcmt).to.exist; + expect(payload.data[0].videoPlcmt).to.equal(1); + }); + + it('should not add videoPlcmt to payload if empty', function () { + let bidRequestWithNullVideoPlcmt = Object.assign({}, bidRequests[0], { + mediaTypes: { + video: { + plcmt: null + } + } + }); + + let bidRequestWithEmptyVideoPlcmt = Object.assign({}, bidRequests[0], { + mediaTypes: { + video: { + plcmt: '' + } + } + }); + + const request1 = spec.buildRequests([bidRequestWithNullVideoPlcmt], bidderRequestDefault); + const request2 = spec.buildRequests([bidRequestWithEmptyVideoPlcmt], bidderRequestDefault); + const payload1 = JSON.parse(request1.data); + const payload2 = JSON.parse(request2.data); + + expect(payload1.data[0].videoPlcmt).to.not.exist; + expect(payload2.data[0].videoPlcmt).to.not.exist; + }); + + it('should not add videoPlcmt to payload if it is not in bid request', function () { + const request = spec.buildRequests(bidRequests, bidderRequestDefault); + const payload = JSON.parse(request.data); + + expect(payload.data[0].videoPlcmt).to.not.exist; + }); + it('should add referer info to payload', function () { const bidRequest = Object.assign({}, bidRequests[0]) const bidderRequest = { @@ -186,7 +233,7 @@ describe('teadsBidAdapter', () => { }); it('should add networkBandwidth info to payload', function () { - const request = spec.buildRequests(bidRequests, bidderResquestDefault); + const request = spec.buildRequests(bidRequests, bidderRequestDefault); const payload = JSON.parse(request.data); const bandwidth = window.navigator && window.navigator.connection && window.navigator.connection.downlink; @@ -201,15 +248,167 @@ describe('teadsBidAdapter', () => { }); it('should add pageReferrer info to payload', function () { - const request = spec.buildRequests(bidRequests, bidderResquestDefault); + const request = spec.buildRequests(bidRequests, bidderRequestDefault); const payload = JSON.parse(request.data); expect(payload.pageReferrer).to.exist; expect(payload.pageReferrer).to.deep.equal(document.referrer); }); + it('should add screenOrientation info to payload', function () { + const request = spec.buildRequests(bidRequests, bidderRequestDefault); + const payload = JSON.parse(request.data); + const screenOrientation = window.top.screen.orientation?.type + + if (screenOrientation) { + expect(payload.screenOrientation).to.exist; + expect(payload.screenOrientation).to.deep.equal(screenOrientation); + } else expect(payload.screenOrientation).to.not.exist; + }); + + it('should add historyLength info to payload', function () { + const request = spec.buildRequests(bidRequests, bidderRequestDefault); + const payload = JSON.parse(request.data); + + expect(payload.historyLength).to.exist; + expect(payload.historyLength).to.deep.equal(window.top.history.length); + }); + + it('should add viewportHeight info to payload', function () { + const request = spec.buildRequests(bidRequests, bidderRequestDefault); + const payload = JSON.parse(request.data); + + expect(payload.viewportHeight).to.exist; + expect(payload.viewportHeight).to.deep.equal(window.top.visualViewport.height); + }); + + it('should add viewportWidth info to payload', function () { + const request = spec.buildRequests(bidRequests, bidderRequestDefault); + const payload = JSON.parse(request.data); + + expect(payload.viewportWidth).to.exist; + expect(payload.viewportWidth).to.deep.equal(window.top.visualViewport.width); + }); + + it('should add hardwareConcurrency info to payload', function () { + const request = spec.buildRequests(bidRequests, bidderRequestDefault); + const payload = JSON.parse(request.data); + const hardwareConcurrency = window.top.navigator?.hardwareConcurrency + + if (hardwareConcurrency) { + expect(payload.hardwareConcurrency).to.exist; + expect(payload.hardwareConcurrency).to.deep.equal(hardwareConcurrency); + } else expect(payload.hardwareConcurrency).to.not.exist + }); + + it('should add deviceMemory info to payload', function () { + const request = spec.buildRequests(bidRequests, bidderRequestDefault); + const payload = JSON.parse(request.data); + const deviceMemory = window.top.navigator.deviceMemory + + if (deviceMemory) { + expect(payload.deviceMemory).to.exist; + expect(payload.deviceMemory).to.deep.equal(deviceMemory); + } else expect(payload.deviceMemory).to.not.exist; + }); + + describe('pageTitle', function () { + it('should add pageTitle info to payload based on document title', function () { + const testText = 'This is a title'; + sandbox.stub(window.top.document, 'title').value(testText); + + const request = spec.buildRequests(bidRequests, bidderRequestDefault); + const payload = JSON.parse(request.data); + + expect(payload.pageTitle).to.exist; + expect(payload.pageTitle).to.deep.equal(testText); + }); + + it('should add pageTitle info to payload based on open-graph title', function () { + const testText = 'This is a title from open-graph'; + sandbox.stub(window.top.document, 'title').value(''); + sandbox.stub(window.top.document, 'querySelector').withArgs('meta[property="og:title"]').returns({ content: testText }); + + const request = spec.buildRequests(bidRequests, bidderRequestDefault); + const payload = JSON.parse(request.data); + + expect(payload.pageTitle).to.exist; + expect(payload.pageTitle).to.deep.equal(testText); + }); + + it('should add pageTitle info to payload sliced on 300 first characters', function () { + const testText = Array(500).join('a'); + sandbox.stub(window.top.document, 'title').value(testText); + + const request = spec.buildRequests(bidRequests, bidderRequestDefault); + const payload = JSON.parse(request.data); + + expect(payload.pageTitle).to.exist; + expect(payload.pageTitle).to.have.length(300); + }); + + it('should add pageTitle info to payload when fallbacking from window.top', function () { + const testText = 'This is a fallback title'; + sandbox.stub(window.top.document, 'querySelector').throws(); + sandbox.stub(document, 'title').value(testText); + + const request = spec.buildRequests(bidRequests, bidderRequestDefault); + const payload = JSON.parse(request.data); + + expect(payload.pageTitle).to.exist; + expect(payload.pageTitle).to.deep.equal(testText); + }); + }); + + describe('pageDescription', function () { + it('should add pageDescription info to payload based on open-graph description', function () { + const testText = 'This is a description'; + sandbox.stub(window.top.document, 'querySelector').withArgs('meta[name="description"]').returns({ content: testText }); + + const request = spec.buildRequests(bidRequests, bidderRequestDefault); + const payload = JSON.parse(request.data); + + expect(payload.pageDescription).to.exist; + expect(payload.pageDescription).to.deep.equal(testText); + }); + + it('should add pageDescription info to payload based on open-graph description', function () { + const testText = 'This is a description from open-graph'; + sandbox.stub(window.top.document, 'querySelector').withArgs('meta[property="og:description"]').returns({ content: testText }); + + const request = spec.buildRequests(bidRequests, bidderRequestDefault); + const payload = JSON.parse(request.data); + + expect(payload.pageDescription).to.exist; + expect(payload.pageDescription).to.deep.equal(testText); + }); + + it('should add pageDescription info to payload sliced on 300 first characters', function () { + const testText = Array(500).join('a'); + sandbox.stub(window.top.document, 'querySelector').withArgs('meta[name="description"]').returns({ content: testText }); + + const request = spec.buildRequests(bidRequests, bidderRequestDefault); + const payload = JSON.parse(request.data); + + expect(payload.pageDescription).to.exist; + expect(payload.pageDescription).to.have.length(300); + }); + + it('should add pageDescription info to payload when fallbacking from window.top', function () { + const testText = 'This is a fallback description'; + sandbox.stub(window.top.document, 'querySelector').throws(); + sandbox.stub(document, 'querySelector').withArgs('meta[name="description"]').returns({ content: testText }); + + const request = spec.buildRequests(bidRequests, bidderRequestDefault); + const payload = JSON.parse(request.data); + + expect(payload.pageDescription).to.exist; + expect(payload.pageDescription).to.deep.equal(testText); + }); + }); + it('should add timeToFirstByte info to payload', function () { - const request = spec.buildRequests(bidRequests, bidderResquestDefault); + const request = spec.buildRequests(bidRequests, bidderRequestDefault); const payload = JSON.parse(request.data); const performance = window.performance || window.webkitPerformance || window.msPerformance || window.mozPerformance; @@ -410,7 +609,7 @@ describe('teadsBidAdapter', () => { } }); - const request = spec.buildRequests([bidRequest], bidderResquestDefault); + const request = spec.buildRequests([bidRequest], bidderRequestDefault); const payload = JSON.parse(request.data); expect(payload.schain).to.exist; @@ -458,7 +657,7 @@ describe('teadsBidAdapter', () => { } }); - const requestWithUserAgentClientHints = spec.buildRequests([bidRequest], bidderResquestDefault); + const requestWithUserAgentClientHints = spec.buildRequests([bidRequest], bidderRequestDefault); const payload = JSON.parse(requestWithUserAgentClientHints.data); expect(payload.userAgentClientHints).to.exist; @@ -489,7 +688,7 @@ describe('teadsBidAdapter', () => { } ); - const defaultRequest = spec.buildRequests(bidRequests, bidderResquestDefault); + const defaultRequest = spec.buildRequests(bidRequests, bidderRequestDefault); expect(JSON.parse(defaultRequest.data).userAgentClientHints).to.not.exist; }); @@ -566,7 +765,7 @@ describe('teadsBidAdapter', () => { userId: {} // no property -> assumption that the system is disabled }; - const request = spec.buildRequests([bidRequest], bidderResquestDefault); + const request = spec.buildRequests([bidRequest], bidderRequestDefault); const payload = JSON.parse(request.data); for (const userId in userIdModules) { @@ -575,7 +774,7 @@ describe('teadsBidAdapter', () => { }); it(`should not add param to payload if user id field is absent`, function () { - const request = spec.buildRequests([baseBidRequest], bidderResquestDefault); + const request = spec.buildRequests([baseBidRequest], bidderRequestDefault); const payload = JSON.parse(request.data); for (const userId in userIdModules) { @@ -592,7 +791,7 @@ describe('teadsBidAdapter', () => { } }; - const request = spec.buildRequests([bidRequest], bidderResquestDefault); + const request = spec.buildRequests([bidRequest], bidderRequestDefault); const payload = JSON.parse(request.data); expect(payload).not.to.have.property('liveRampId'); @@ -612,7 +811,7 @@ describe('teadsBidAdapter', () => { userId: userIdObject }; - const request = spec.buildRequests([bidRequest], bidderResquestDefault); + const request = spec.buildRequests([bidRequest], bidderRequestDefault); const payload = JSON.parse(request.data); expect(payload['unifiedId2']).to.equal('unifiedId2-id'); @@ -632,7 +831,7 @@ describe('teadsBidAdapter', () => { describe('First-party cookie Teads ID', function () { it('should not add firstPartyCookieTeadsId param to payload if cookies are not enabled' + ' and teads user id not available', function () { - cookiesAreEnabledStub.returns(false); + sandbox.stub(storage, 'cookiesAreEnabled').returns(false); const bidRequest = { ...baseBidRequest, @@ -641,7 +840,7 @@ describe('teadsBidAdapter', () => { } }; - const request = spec.buildRequests([bidRequest], bidderResquestDefault); + const request = spec.buildRequests([bidRequest], bidderRequestDefault); const payload = JSON.parse(request.data); expect(payload).not.to.have.property('firstPartyCookieTeadsId'); @@ -649,8 +848,8 @@ describe('teadsBidAdapter', () => { it('should not add firstPartyCookieTeadsId param to payload if cookies are enabled ' + 'but first-party cookie and teads user id are not available', function () { - cookiesAreEnabledStub.returns(true); - getCookieStub.withArgs('_tfpvi').returns(undefined); + sandbox.stub(storage, 'cookiesAreEnabled').returns(true); + sandbox.stub(storage, 'getCookie').withArgs('_tfpvi').returns(undefined); const bidRequest = { ...baseBidRequest, @@ -659,7 +858,7 @@ describe('teadsBidAdapter', () => { } }; - const request = spec.buildRequests([bidRequest], bidderResquestDefault); + const request = spec.buildRequests([bidRequest], bidderRequestDefault); const payload = JSON.parse(request.data); expect(payload).not.to.have.property('firstPartyCookieTeadsId'); @@ -667,8 +866,8 @@ describe('teadsBidAdapter', () => { it('should add firstPartyCookieTeadsId from cookie if it\'s available ' + 'and teads user id is not', function () { - cookiesAreEnabledStub.returns(true); - getCookieStub.withArgs('_tfpvi').returns('my-teads-id'); + sandbox.stub(storage, 'cookiesAreEnabled').returns(true); + sandbox.stub(storage, 'getCookie').withArgs('_tfpvi').returns('my-teads-id'); const bidRequest = { ...baseBidRequest, @@ -677,7 +876,7 @@ describe('teadsBidAdapter', () => { } }; - const request = spec.buildRequests([bidRequest], bidderResquestDefault); + const request = spec.buildRequests([bidRequest], bidderRequestDefault); const payload = JSON.parse(request.data); @@ -686,8 +885,8 @@ describe('teadsBidAdapter', () => { it('should add firstPartyCookieTeadsId from user id module if it\'s available ' + 'even if cookie is available too', function () { - cookiesAreEnabledStub.returns(true); - getCookieStub.withArgs('_tfpvi').returns('my-teads-id'); + sandbox.stub(storage, 'cookiesAreEnabled').returns(true); + sandbox.stub(storage, 'getCookie').withArgs('_tfpvi').returns('my-teads-id'); const bidRequest = { ...baseBidRequest, @@ -697,7 +896,7 @@ describe('teadsBidAdapter', () => { } }; - const request = spec.buildRequests([bidRequest], bidderResquestDefault); + const request = spec.buildRequests([bidRequest], bidderRequestDefault); const payload = JSON.parse(request.data); @@ -750,7 +949,7 @@ describe('teadsBidAdapter', () => { }; } ); - const request = spec.buildRequests(updatedBidRequests, bidderResquestDefault); + const request = spec.buildRequests(updatedBidRequests, bidderRequestDefault); const payload = JSON.parse(request.data); expect(payload.data[0].gpid).to.equal('1111/home-left-0'); @@ -767,7 +966,7 @@ describe('teadsBidAdapter', () => { } })); - const request = spec.buildRequests(updatedBidRequests, bidderResquestDefault); + const request = spec.buildRequests(updatedBidRequests, bidderRequestDefault); const payload = JSON.parse(request.data); return payload.data.forEach(bid => { @@ -784,7 +983,7 @@ describe('teadsBidAdapter', () => { } })); - const request = spec.buildRequests(updatedBidRequests, bidderResquestDefault); + const request = spec.buildRequests(updatedBidRequests, bidderRequestDefault); const payload = JSON.parse(request.data); return payload.data.forEach(bid => { @@ -795,7 +994,7 @@ describe('teadsBidAdapter', () => { function checkMediaTypesSizes(mediaTypes, expectedSizes) { const bidRequestWithBannerSizes = Object.assign(bidRequests[0], mediaTypes); - const requestWithBannerSizes = spec.buildRequests([bidRequestWithBannerSizes], bidderResquestDefault); + const requestWithBannerSizes = spec.buildRequests([bidRequestWithBannerSizes], bidderRequestDefault); const payloadWithBannerSizes = JSON.parse(requestWithBannerSizes.data); return payloadWithBannerSizes.data.forEach(bid => { diff --git a/test/spec/modules/teadsIdSystem_spec.js b/test/spec/modules/teadsIdSystem_spec.js index 7b977e2fb2b..1959b990957 100644 --- a/test/spec/modules/teadsIdSystem_spec.js +++ b/test/spec/modules/teadsIdSystem_spec.js @@ -218,7 +218,7 @@ describe('TeadsIdSystem', function () { callback(callbackSpy); const request = server.requests[0]; expect(request.url).to.include(teadsUrl); - request.respond(204, null, 'Unavailable'); + request.respond(204); expect(logInfoStub.calledOnce).to.be.true; }); diff --git a/test/spec/modules/theAdxBidAdapter_spec.js b/test/spec/modules/theAdxBidAdapter_spec.js index 99e5156190c..eb00834421a 100644 --- a/test/spec/modules/theAdxBidAdapter_spec.js +++ b/test/spec/modules/theAdxBidAdapter_spec.js @@ -446,6 +446,78 @@ describe('TheAdxAdapter', function () { expect(processedBid.currency).to.equal(responseCurrency); }); + it('returns a valid deal bid response on sucessful banner request with deal', function () { + let incomingRequestId = 'XXtestingXX'; + let responsePrice = 3.14 + + let responseCreative = 'sample_creative&{FOR_COVARAGE}'; + + let responseCreativeId = '274'; + let responseCurrency = 'TRY'; + + let responseWidth = 300; + let responseHeight = 250; + let responseTtl = 213; + let dealId = 'theadx_deal_id'; + + let sampleResponse = { + id: '66043f5ca44ecd8f8769093b1615b2d9', + seatbid: [{ + bid: [{ + id: 'c21bab0e-7668-4d8f-908a-63e094c09197', + dealid: 'theadx_deal_id', + impid: '1', + price: responsePrice, + adid: responseCreativeId, + crid: responseCreativeId, + adm: responseCreative, + adomain: [ + 'www.domain.com' + ], + cid: '274', + attr: [], + w: responseWidth, + h: responseHeight, + ext: { + ttl: responseTtl + } + }], + seat: '201', + group: 0 + }], + bidid: 'c21bab0e-7668-4d8f-908a-63e094c09197', + cur: responseCurrency + }; + + let sampleRequest = { + bidId: incomingRequestId, + mediaTypes: { + banner: {} + }, + requestId: incomingRequestId, + deals: [{id: dealId}] + }; + let serverResponse = { + body: sampleResponse + } + let result = spec.interpretResponse(serverResponse, sampleRequest); + + expect(result.length).to.equal(1); + + let processedBid = result[0]; + + // expect(processedBid.requestId).to.equal(incomingRequestId); + expect(processedBid.cpm).to.equal(responsePrice); + expect(processedBid.width).to.equal(responseWidth); + expect(processedBid.height).to.equal(responseHeight); + expect(processedBid.ad).to.equal(responseCreative); + expect(processedBid.ttl).to.equal(responseTtl); + expect(processedBid.creativeId).to.equal(responseCreativeId); + expect(processedBid.netRevenue).to.equal(true); + expect(processedBid.currency).to.equal(responseCurrency); + expect(processedBid.dealId).to.equal(dealId); + }); + it('returns an valid bid response on sucessful video request', function () { let incomingRequestId = 'XXtesting-275XX'; let responsePrice = 6 diff --git a/test/spec/modules/topicsFpdModule_spec.js b/test/spec/modules/topicsFpdModule_spec.js index 958489f2728..bc7df85db0d 100644 --- a/test/spec/modules/topicsFpdModule_spec.js +++ b/test/spec/modules/topicsFpdModule_spec.js @@ -1,289 +1,313 @@ import { + getCachedTopics, getTopics, getTopicsData, + loadTopicsForBidders, processFpd, - hasGDPRConsent, - getCachedTopics, receiveMessage, + reset, topicStorageName } from '../../../modules/topicsFpdModule.js'; +import {config} from 'src/config.js'; import {deepClone, safeJSONParse} from '../../../src/utils.js'; -import {gdprDataHandler} from 'src/adapterManager.js'; import {getCoreStorageManager} from 'src/storageManager.js'; +import * as activities from '../../../src/activities/rules.js'; +import {ACTIVITY_ENRICH_UFPD} from '../../../src/activities/activities.js'; -describe('getTopicsData', () => { - function makeTopic(topic, modelv, taxv = '1') { - return { - topic, - taxonomyVersion: taxv, - modelVersion: modelv - }; - } - - function byTaxClass(segments) { - return segments.reduce((memo, segment) => { - memo[`${segment.ext.segtax}:${segment.ext.segclass}`] = segment; - return memo; - }, {}); - } - - [ - { - t: 'no topics', - topics: [], - expected: [] - }, - { - t: 'single topic', - topics: [makeTopic(123, 'm1')], - expected: [ - { - ext: { - segtax: 600, - segclass: 'm1' - }, - segment: [ - {id: '123'} - ] - } - ] - }, - { - t: 'multiple topics with the same model version', - topics: [makeTopic(123, 'm1'), makeTopic(321, 'm1')], - expected: [ - { - ext: { - segtax: 600, - segclass: 'm1' - }, - segment: [ - {id: '123'}, - {id: '321'} - ] - } - ] - }, - { - t: 'multiple topics with different model versions', - topics: [makeTopic(1, 'm1'), makeTopic(2, 'm1'), makeTopic(3, 'm2')], - expected: [ - { - ext: { - segtax: 600, - segclass: 'm1' - }, - segment: [ - {id: '1'}, - {id: '2'} - ] - }, - { - ext: { - segtax: 600, - segclass: 'm2' - }, - segment: [ - {id: '3'} - ] - } - ] - }, - { - t: 'multiple topics, some with a taxonomy version other than "1"', - topics: [makeTopic(123, 'm1'), makeTopic(321, 'm1', 'other')], - expected: [ - { - ext: { - segtax: 600, - segclass: 'm1' - }, - segment: [ - {id: '123'} - ] - } - ] - }, - { - t: 'multiple topics in multiple taxonomies', - taxonomies: { - '1': 600, - '2': 601 +describe('topics', () => { + beforeEach(() => { + reset(); + }); + + describe('getTopicsData', () => { + function makeTopic(topic, modelv, taxv = '1') { + return { + topic, + taxonomyVersion: taxv, + modelVersion: modelv + }; + } + + function byTaxClass(segments) { + return segments.reduce((memo, segment) => { + memo[`${segment.ext.segtax}:${segment.ext.segclass}`] = segment; + return memo; + }, {}); + } + + [ + { + t: 'no topics', + topics: [], + expected: [] }, - topics: [ - makeTopic(123, 'm1', '1'), - makeTopic(321, 'm1', '2'), - makeTopic(213, 'm2', '1'), - ], - expected: [ - { - ext: { - segtax: 600, - segclass: 'm1' + { + t: 'single topic', + topics: [makeTopic(123, 'm1')], + expected: [ + { + ext: { + segtax: 600, + segclass: 'm1' + }, + segment: [ + {id: '123'} + ] + } + ] + }, + { + t: 'multiple topics with the same model version', + topics: [makeTopic(123, 'm1'), makeTopic(321, 'm1')], + expected: [ + { + ext: { + segtax: 600, + segclass: 'm1' + }, + segment: [ + {id: '123'}, + {id: '321'} + ] + } + ] + }, + { + t: 'multiple topics with different model versions', + topics: [makeTopic(1, 'm1'), makeTopic(2, 'm1'), makeTopic(3, 'm2')], + expected: [ + { + ext: { + segtax: 600, + segclass: 'm1' + }, + segment: [ + {id: '1'}, + {id: '2'} + ] }, - segment: [ - {id: '123'} - ] + { + ext: { + segtax: 600, + segclass: 'm2' + }, + segment: [ + {id: '3'} + ] + } + ] + }, + { + t: 'multiple topics, some with a taxonomy version other than "1"', + topics: [makeTopic(123, 'm1'), makeTopic(321, 'm1', 'other')], + expected: [ + { + ext: { + segtax: 600, + segclass: 'm1' + }, + segment: [ + {id: '123'} + ] + } + ] + }, + { + t: 'multiple topics in multiple taxonomies', + taxonomies: { + '1': 600, + '2': 601 }, - { - ext: { - segtax: 601, - segclass: 'm1', + topics: [ + makeTopic(123, 'm1', '1'), + makeTopic(321, 'm1', '2'), + makeTopic(213, 'm2', '1'), + ], + expected: [ + { + ext: { + segtax: 600, + segclass: 'm1' + }, + segment: [ + {id: '123'} + ] }, - segment: [ - {id: '321'} - ] - }, - { - ext: { - segtax: 600, - segclass: 'm2' + { + ext: { + segtax: 601, + segclass: 'm1', + }, + segment: [ + {id: '321'} + ] }, - segment: [ - {id: '213'} - ] - } - ] - } - ].forEach(({t, topics, expected, taxonomies}) => { - describe(`on ${t}`, () => { - it('should convert topics to user.data segments correctly', () => { - const actual = getTopicsData('mockName', topics, taxonomies); - expect(actual.length).to.eql(expected.length); - expected = byTaxClass(expected); - Object.entries(byTaxClass(actual)).forEach(([key, datum]) => { - sinon.assert.match(datum, expected[key]); - expect(datum.name).to.equal('mockName'); + { + ext: { + segtax: 600, + segclass: 'm2' + }, + segment: [ + {id: '213'} + ] + } + ] + } + ].forEach(({t, topics, expected, taxonomies}) => { + describe(`on ${t}`, () => { + it('should convert topics to user.data segments correctly', () => { + const actual = getTopicsData('mockName', topics, taxonomies); + expect(actual.length).to.eql(expected.length); + expected = byTaxClass(expected); + Object.entries(byTaxClass(actual)).forEach(([key, datum]) => { + sinon.assert.match(datum, expected[key]); + expect(datum.name).to.equal('mockName'); + }); }); - }); - it('should not set name if null', () => { - getTopicsData(null, topics).forEach((data) => { - expect(data.hasOwnProperty('name')).to.be.false; + it('should not set name if null', () => { + getTopicsData(null, topics).forEach((data) => { + expect(data.hasOwnProperty('name')).to.be.false; + }); }); }); }); }); -}); -describe('getTopics', () => { - Object.entries({ - 'document with no browsingTopics': {}, - 'document that disallows topics': { - featurePolicy: { - allowsFeature: sinon.stub().returns(false) - } - }, - 'document that throws on featurePolicy': { - browsingTopics: sinon.stub(), - get featurePolicy() { - throw new Error(); - } - }, - 'document that throws on browsingTopics': { - browsingTopics: sinon.stub().callsFake(() => { - throw new Error(); - }), - featurePolicy: { - allowsFeature: sinon.stub().returns(true) - } - }, - }).forEach(([t, doc]) => { - it(`should resolve to an empty list on ${t}`, () => { - return getTopics(doc).then((topics) => { - expect(topics).to.eql([]); + describe('getTopics', () => { + Object.entries({ + 'document with no browsingTopics': {}, + 'document that disallows topics': { + featurePolicy: { + allowsFeature: sinon.stub().returns(false) + } + }, + 'document that throws on featurePolicy': { + browsingTopics: sinon.stub(), + get featurePolicy() { + throw new Error(); + } + }, + 'document that throws on browsingTopics': { + browsingTopics: sinon.stub().callsFake(() => { + throw new Error(); + }), + featurePolicy: { + allowsFeature: sinon.stub().returns(true) + } + }, + }).forEach(([t, doc]) => { + it(`should resolve to an empty list on ${t}`, () => { + return getTopics(doc).then((topics) => { + expect(topics).to.eql([]); + }); }); }); - }); - it('should call `document.browsingTopics` when allowed', () => { - const topics = ['t1', 't2']; - return getTopics({ - browsingTopics: sinon.stub().returns(Promise.resolve(topics)), - featurePolicy: { - allowsFeature: sinon.stub().returns(true) - } - }).then((actual) => { - expect(actual).to.eql(topics); - }); - }); -}); - -describe('processFpd', () => { - const mockData = [ - { - name: 'domain', - segment: [{id: 123}] - }, - { - name: 'domain', - segment: [{id: 321}] - } - ]; - - it('should add topics data', () => { - return processFpd({}, {global: {}}, {data: Promise.resolve(mockData)}) - .then(({global}) => { - expect(global.user.data).to.eql(mockData); + it('should call `document.browsingTopics` when allowed', () => { + const topics = ['t1', 't2']; + return getTopics({ + browsingTopics: sinon.stub().returns(Promise.resolve(topics)), + featurePolicy: { + allowsFeature: sinon.stub().returns(true) + } + }).then((actual) => { + expect(actual).to.eql(topics); }); + }); }); - it('should apppend to existing user.data', () => { - const global = { - user: { - data: [ - {name: 'preexisting'}, - ] + describe('processFpd', () => { + const mockData = [ + { + name: 'domain', + segment: [{id: 123}] + }, + { + name: 'domain', + segment: [{id: 321}] } - }; - return processFpd({}, {global: deepClone(global)}, {data: Promise.resolve(mockData)}) - .then((data) => { - expect(data.global.user.data).to.eql(global.user.data.concat(mockData)); - }); - }); - - it('should not modify fpd when there is no data', () => { - return processFpd({}, {global: {}}, {data: Promise.resolve([])}) - .then((data) => { - expect(data.global).to.eql({}); - }); - }); -}); + ]; -describe('Topics Module GDPR consent check', () => { - let gdprDataHdlrStub; - beforeEach(() => { - gdprDataHdlrStub = sinon.stub(gdprDataHandler, 'getConsentData'); - }); + it('should add topics data', () => { + return processFpd({}, {global: {}}, {data: Promise.resolve(mockData)}) + .then(({global}) => { + expect(global.user.data).to.eql(mockData); + }); + }); - afterEach(() => { - gdprDataHdlrStub.restore(); - }); + it('should apppend to existing user.data', () => { + const global = { + user: { + data: [ + {name: 'preexisting'}, + ] + } + }; + return processFpd({}, {global: deepClone(global)}, {data: Promise.resolve(mockData)}) + .then((data) => { + expect(data.global.user.data).to.eql(global.user.data.concat(mockData)); + }); + }); - it('should return false when GDPR is applied but consent string is not present', () => { - const consentString = ''; - const consentConfig = { - consentString: consentString, - gdprApplies: true, - vendorData: {} - }; - gdprDataHdlrStub.returns(consentConfig); - expect(hasGDPRConsent()).to.equal(false); + it('should not modify fpd when there is no data', () => { + return processFpd({}, {global: {}}, {data: Promise.resolve([])}) + .then((data) => { + expect(data.global).to.eql({}); + }); + }); }); - it('should return true when GDPR doesn\'t apply', () => { - const consentString = 'CPi8wgAPi8wgAADABBENCrCsAP_AAH_AAAAAISNB7D=='; - const consentConfig = { - consentString: consentString, - gdprApplies: false, - vendorData: {} - }; + describe('loadTopicsForBidders', () => { + beforeEach(() => { + config.setConfig({ + userSync: { + topics: { + bidders: [{ + bidder: 'mockBidder', + iframeURL: 'https://mock.iframe' + }] + } + } + }) + }); + afterEach(() => { + config.resetConfig(); + }) - gdprDataHdlrStub.returns(consentConfig); - expect(hasGDPRConsent()).to.equal(true); + Object.entries({ + 'support': {}, + 'allow': { + browsingTopics: true, + featurePolicy: { + allowsFeature(feature) { + return feature !== 'browsing-topics'; + } + } + }, + }).forEach(([t, doc]) => { + it(`does not attempt to load frames if browser does not ${t} topics`, () => { + doc.createElement = sinon.stub(); + loadTopicsForBidders(doc); + sinon.assert.notCalled(doc.createElement); + }); + }); }); - it('should return true when GDPR is applied and purpose consent is true for all purpose[1,2,3,4]', () => { + describe('getCachedTopics()', () => { + const storage = getCoreStorageManager('topicsFpd'); + const expected = [{ + ext: { + segtax: 600, + segclass: '2206021246' + }, + segment: [{ + 'id': '243' + }, { + 'id': '265' + }], + name: 'ads.pubmatic.com' + }]; const consentString = 'CPi8wgAPi8wgAADABBENCrCsAP_AAH_AAAAAISNB7D=='; const consentConfig = { consentString: consentString, @@ -301,93 +325,159 @@ describe('Topics Module GDPR consent check', () => { } } }; + const mockData = [ + { + name: 'domain', + segment: [{id: 123}] + }, + { + name: 'domain', + segment: [{id: 321}], + } + ]; - gdprDataHdlrStub.returns(consentConfig); - expect(hasGDPRConsent()).to.equal(true); - }); + const evt = { + data: '{"segment":{"domain":"ads.pubmatic.com","topics":[{"configVersion":"chrome.1","modelVersion":"2206021246","taxonomyVersion":"1","topic":165,"version":"chrome.1:1:2206021246"}],"bidder":"pubmatic"},"date":1669743901858}', + origin: 'https://ads.pubmatic.com' + }; - it('should return false when GDPR is applied and purpose consent is false for one of the purpose[1,2,3,4]', () => { - const consentString = 'CPi8wgAPi8wgAADABBENCrCsAP_AAH_AAAAAISNB7D=='; - const consentConfig = { - consentString: consentString, - gdprApplies: true, - vendorData: { - metadata: consentString, - gdprApplies: true, - purpose: { - consents: { - 1: true, - 2: true, - 3: true, - 4: false + afterEach(() => { + storage.removeDataFromLocalStorage(topicStorageName); + }); + + describe('when cached data is available and not expired', () => { + let sandbox; + beforeEach(() => { + sandbox = sinon.sandbox.create(); + const storedSegments = JSON.stringify( + [['pubmatic', { + '2206021246': { + 'ext': {'segtax': 600, 'segclass': '2206021246'}, + 'segment': [{'id': '243'}, {'id': '265'}], + 'name': 'ads.pubmatic.com' + }, + 'lastUpdated': new Date().getTime() + }]] + ); + storage.setDataInLocalStorage(topicStorageName, storedSegments); + }); + afterEach(() => { + sandbox.restore(); + }); + + it('should return segments for bidder if transmitUfpd is allowed', () => { + assert.deepEqual(getCachedTopics(), expected); + }); + + it('should NOT return segments for bidder if enrichUfpd is NOT allowed', () => { + sandbox.stub(activities, 'isActivityAllowed').callsFake((activity, params) => { + return !(activity === ACTIVITY_ENRICH_UFPD && params.component === 'bidder.pubmatic'); + }); + expect(getCachedTopics()).to.eql([]); + }); + }) + + it('should return empty segments for bidder if there is cached segments stored which is expired', () => { + let storedSegments = '[["pubmatic",{"2206021246":{"ext":{"segtax":600,"segclass":"2206021246"},"segment":[{"id":"243"},{"id":"265"}],"name":"ads.pubmatic.com"},"lastUpdated":10}]]'; + storage.setDataInLocalStorage(topicStorageName, storedSegments); + assert.deepEqual(getCachedTopics(), []); + }); + + describe('cross-frame messages', () => { + beforeEach(() => { + // init iframe logic so that the receiveMessage origin check passes + loadTopicsForBidders({ + browsingTopics: true, + featurePolicy: { + allowsFeature() { return true } + }, + createElement: sinon.stub().callsFake(() => ({style: {}})), + documentElement: { + appendChild() {} } - } - } - }; + }); + }); + + it('should store segments if receiveMessage event is triggered with segment data', () => { + receiveMessage(evt); + let segments = new Map(safeJSONParse(storage.getDataFromLocalStorage(topicStorageName))); + expect(segments.has('pubmatic')).to.equal(true); + }); - gdprDataHdlrStub.returns(consentConfig); - expect(hasGDPRConsent()).to.equal(false); + it('should update stored segments if receiveMessage event is triggerred with segment data', () => { + let storedSegments = '[["pubmatic",{"2206021246":{"ext":{"segtax":600,"segclass":"2206021246"},"segment":[{"id":"243"},{"id":"265"}],"name":"ads.pubmatic.com"},"lastUpdated":1669719242027}]]'; + storage.setDataInLocalStorage(topicStorageName, storedSegments); + receiveMessage(evt); + let segments = new Map(safeJSONParse(storage.getDataFromLocalStorage(topicStorageName))); + expect(segments.get('pubmatic')[2206021246].segment.length).to.equal(1); + }); + }); }); }); -describe('getCachedTopics()', () => { +describe('handles fetch request for topics api headers', () => { + let stubbedFetch; const storage = getCoreStorageManager('topicsFpd'); - const expected = [{ - ext: { - segtax: 600, - segclass: '2206021246' - }, - segment: [{ - 'id': '243' - }, { - 'id': '265' - }], - name: 'ads.pubmatic.com' - }]; - const consentString = 'CPi8wgAPi8wgAADABBENCrCsAP_AAH_AAAAAISNB7D=='; - const consentConfig = { - consentString: consentString, - gdprApplies: true, - vendorData: { - metadata: consentString, - gdprApplies: true, - purpose: { - consents: { - 1: true, - 2: true, - 3: true, - 4: true - } - } - } - }; - const mockData = [ - { - name: 'domain', - segment: [{id: 123}] - }, - { - name: 'domain', - segment: [{id: 321}], - } - ]; - - const evt = { - data: '{"segment":{"domain":"ads.pubmatic.com","topics":[{"configVersion":"chrome.1","modelVersion":"2206021246","taxonomyVersion":"1","topic":165,"version":"chrome.1:1:2206021246"}],"bidder":"pubmatic"},"date":1669743901858}', - origin: 'https://ads.pubmatic.com' - }; - let gdprDataHdlrStub; beforeEach(() => { - gdprDataHdlrStub = sinon.stub(gdprDataHandler, 'getConsentData'); + stubbedFetch = sinon.stub(window, 'fetch'); }); afterEach(() => { + stubbedFetch.restore(); storage.removeDataFromLocalStorage(topicStorageName); - gdprDataHdlrStub.restore(); }); - it('should return segments for bidder if GDPR consent is true and there is cached segments stored which is not expired', () => { + it('should make a fetch call when a fetchUrl is present for a selected bidder', () => { + config.setConfig({ + userSync: { + topics: { + maxTopicCaller: 3, + bidders: [ + { + bidder: 'pubmatic', + fetchUrl: 'http://localhost:3000/topics-server.js' + } + ], + }, + } + }); + + stubbedFetch.returns(Promise.resolve(true)); + loadTopicsForBidders({ + browsingTopics: true, + featurePolicy: { + allowsFeature() { return true } + } + }); + sinon.assert.calledOnce(stubbedFetch); + stubbedFetch.calledWith('http://localhost:3000/topics-server.js'); + }); + + it('should not make a fetch call when a fetchUrl is not present for a selected bidder', () => { + config.setConfig({ + userSync: { + topics: { + maxTopicCaller: 3, + bidders: [ + { + bidder: 'pubmatic' + } + ], + }, + } + }); + + loadTopicsForBidders({ + browsingTopics: true, + featurePolicy: { + allowsFeature() { return true } + } + }); + sinon.assert.notCalled(stubbedFetch); + }); + + it('a fetch request should not be made if the configured fetch rate duration has not yet passed', () => { const storedSegments = JSON.stringify( [['pubmatic', { '2206021246': { @@ -398,35 +488,30 @@ describe('getCachedTopics()', () => { 'lastUpdated': new Date().getTime() }]] ); - storage.setDataInLocalStorage(topicStorageName, storedSegments); - gdprDataHdlrStub.returns(consentConfig); - assert.deepEqual(getCachedTopics(), expected); - }); - it('should return empty segments for bidder if GDPR consent is true and there is cached segments stored which is expired', () => { - let storedSegments = '[["pubmatic",{"2206021246":{"ext":{"segtax":600,"segclass":"2206021246"},"segment":[{"id":"243"},{"id":"265"}],"name":"ads.pubmatic.com"},"lastUpdated":10}]]'; storage.setDataInLocalStorage(topicStorageName, storedSegments); - gdprDataHdlrStub.returns(consentConfig); - assert.deepEqual(getCachedTopics(), []); - }); - it('should stored segments if receiveMessage event is triggerred with segment data', () => { - return processFpd({}, {global: {}}, {data: Promise.resolve(mockData)}) - .then(({global}) => { - receiveMessage(evt); - let segments = new Map(safeJSONParse(storage.getDataFromLocalStorage(topicStorageName))); - expect(segments.has('pubmatic')).to.equal(true); - }); - }); + config.setConfig({ + userSync: { + topics: { + maxTopicCaller: 3, + bidders: [ + { + bidder: 'pubmatic', + fetchUrl: 'http://localhost:3000/topics-server.js', + fetchRate: 1 // in days. 1 fetch per day + } + ], + }, + } + }); - it('should update stored segments if receiveMessage event is triggerred with segment data', () => { - let storedSegments = '[["pubmatic",{"2206021246":{"ext":{"segtax":600,"segclass":"2206021246"},"segment":[{"id":"243"},{"id":"265"}],"name":"ads.pubmatic.com"},"lastUpdated":1669719242027}]]'; - storage.setDataInLocalStorage(topicStorageName, storedSegments); - return processFpd({}, {global: {}}, {data: Promise.resolve(mockData)}) - .then(({global}) => { - receiveMessage(evt); - let segments = new Map(safeJSONParse(storage.getDataFromLocalStorage(topicStorageName))); - expect(segments.get('pubmatic')[2206021246].segment.length).to.equal(1); - }); + loadTopicsForBidders({ + browsingTopics: true, + featurePolicy: { + allowsFeature() { return true } + } + }); + sinon.assert.notCalled(stubbedFetch); }); }); diff --git a/test/spec/modules/tpmnBidAdapter_spec.js b/test/spec/modules/tpmnBidAdapter_spec.js index e2b14b18f61..505bc9d878f 100644 --- a/test/spec/modules/tpmnBidAdapter_spec.js +++ b/test/spec/modules/tpmnBidAdapter_spec.js @@ -1,16 +1,130 @@ /* eslint-disable no-tabs */ -import {expect} from 'chai'; -import {spec, storage} from 'modules/tpmnBidAdapter.js'; -import {generateUUID} from '../../../src/utils.js'; -import {newBidder} from '../../../src/adapters/bidderFactory'; +import { spec, storage, VIDEO_RENDERER_URL, ADAPTER_VERSION } from 'modules/tpmnBidAdapter.js'; +import { generateUUID } from '../../../src/utils.js'; +import { expect } from 'chai'; +import * as utils from 'src/utils'; import * as sinon from 'sinon'; +import 'modules/consentManagement.js'; +import {syncAddFPDToBidderRequest} from '../../helpers/fpd.js'; +import {mockGdprConsent} from '../../helpers/consentData.js'; + +const BIDDER_CODE = 'tpmn'; +const BANNER_BID = { + bidder: BIDDER_CODE, + params: { + inventoryId: 1 + }, + mediaTypes: { + banner: { + sizes: [ + [300, 250] + ], + }, + }, + adUnitCode: 'adUnitCode1', + bidId: 'bidId', + bidderRequestId: 'bidderRequestId', + auctionId: 'auctionId-56a2-4f71-9098-720a68f2f708', +}; + +const VIDEO_BID = { + bidder: BIDDER_CODE, + params: { + inventoryId: 1 + }, + mediaTypes: { + video: { + context: 'outstream', + api: [1, 2, 4, 6], + mimes: ['video/mp4'], + playbackmethod: [2, 4, 6], + playerSize: [[1024, 768]], + protocols: [3, 4, 7, 8, 10], + placement: 1, + plcmt: 1, + minduration: 0, + maxduration: 60, + startdelay: 0 + }, + }, + adUnitCode: 'adUnitCode1', + bidId: 'bidId', + bidderRequestId: 'bidderRequestId', + auctionId: 'auctionId-56a2-4f71-9098-720a68f2f708', +}; + +const BIDDER_REQUEST = { + auctionId: 'auctionId-56a2-4f71-9098-720a68f2f708', + bidderRequestId: 'bidderRequestId', + timeout: 500, + refererInfo: { + page: 'https://hello-world-page.com/', + domain: 'hello-world-page.com', + ref: 'http://example-domain.com/foo', + } +}; + +const BANNER_BID_RESPONSE = { + 'id': 'bidderRequestId', + 'bidId': 'bidid', + 'seatbid': [ + { + 'bid': [ + { + 'id': 'id', + 'impid': 'bidId', + 'price': 0.18, + 'adm': '', + 'adid': '144762342', + 'burl': 'http://0.0.0.0:8181/burl', + 'adomain': [ + 'https://dummydomain.com' + ], + 'cid': 'cid', + 'crid': 'crid', + 'iurl': 'iurl', + 'cat': [], + 'w': 300, + 'h': 250 + } + ] + } + ], + 'cur': 'USD' +}; + +const VIDEO_BID_RESPONSE = { + 'id': 'bidderRequestId', + 'bidid': 'bidid', + 'seatbid': [ + { + 'bid': [ + { + 'id': 'id', + 'impid': 'bidId', + 'price': 1.09, + 'adid': '144762342', + 'burl': 'http://0.0.0.0:8181/burl', + 'adm': '', + 'adomain': [ + 'https://dummydomain.com' + ], + 'cid': 'cid', + 'crid': 'crid', + 'iurl': 'iurl', + 'cat': [], + 'h': 768, + 'w': 1024 + } + ] + } + ], + 'cur': 'USD' +}; describe('tpmnAdapterTests', function () { - const adapter = newBidder(spec); - const BIDDER_CODE = 'tpmn'; let sandbox = sinon.sandbox.create(); let getCookieStub; - beforeEach(function () { $$PREBID_GLOBAL$$.bidderSettings = { tpmn: { @@ -27,152 +141,277 @@ describe('tpmnAdapterTests', function () { $$PREBID_GLOBAL$$.bidderSettings = {}; }); - describe('inherited functions', function () { - it('exists and is a function', function () { - expect(adapter.callBids).to.exist.and.to.be.a('function') - }) - }); - - describe('isBidRequestValid', function () { - let bid = { - adUnitCode: 'temp-unitcode', - bidder: 'tpmn', - params: { - inventoryId: '1', - publisherId: 'TPMN' - }, - bidId: '29092404798c9', - bidderRequestId: 'a01', - auctionId: 'da1d7a33-0260-4e83-a621-14674116f3f9', - mediaTypes: { - banner: { - sizes: [[300, 250]] - } - } - }; - - it('should return true if a bid is valid banner bid request', function () { - expect(spec.isBidRequestValid(bid)).to.be.equal(true); - }); - - it('should return false where requried param is missing', function () { - let bid = Object.assign({}, bid); - bid.params = {}; - expect(spec.isBidRequestValid(bid)).to.be.equal(false); - }); - - it('should return false when required param values have invalid type', function () { - let bid = Object.assign({}, bid); - bid.params = { - 'inventoryId': null, - 'publisherId': null - }; - expect(spec.isBidRequestValid(bid)).to.be.equal(false); - }); - }); - - describe('buildRequests', function () { - it('should return an empty list if there are no bid requests', function () { - const emptyBidRequests = []; - const bidderRequest = {}; - const request = spec.buildRequests(emptyBidRequests, bidderRequest); - expect(request).to.be.an('array').that.is.empty; - }); - it('should generate a POST server request with bidder API url, data', function () { - const bid = { - adUnitCode: 'temp-unitcode', - bidder: 'tpmn', + describe('isBidRequestValid()', function () { + it('should accept request if placementId is passed', function () { + let bid = { + bidder: BIDDER_CODE, params: { - inventoryId: '1', - publisherId: 'TPMN' + inventoryId: 123 }, - bidId: '29092404798c9', - bidderRequestId: 'a01', - auctionId: 'da1d7a33-0260-4e83-a621-14674116f3f9', mediaTypes: { banner: { sizes: [[300, 250]] } } }; - const tempBidRequests = [bid]; - const tempBidderRequest = { - refererInfo: { - page: 'http://localhost/test', - site: { - domain: 'localhost', - page: 'http://localhost/test' - } - } + expect(spec.isBidRequestValid(bid)).to.equal(true); + }); + + it('should reject requests without params', function () { + let bid = { + bidder: BIDDER_CODE, + params: {} }; - const builtRequest = spec.buildRequests(tempBidRequests, tempBidderRequest); - - expect(builtRequest).to.have.lengthOf(1); - expect(builtRequest[0].method).to.equal('POST'); - expect(builtRequest[0].url).to.match(/ad.tpmn.co.kr\/prebidhb.tpmn/); - const apiRequest = builtRequest[0].data; - expect(apiRequest.site).to.deep.equal({ - domain: 'localhost', - page: 'http://localhost/test' - }); - expect(apiRequest.bids).to.have.lengthOf('1'); - expect(apiRequest.bids[0].inventoryId).to.equal('1'); - expect(apiRequest.bids[0].publisherId).to.equal('TPMN'); - expect(apiRequest.bids[0].bidId).to.equal('29092404798c9'); - expect(apiRequest.bids[0].adUnitCode).to.equal('temp-unitcode'); - expect(apiRequest.bids[0].auctionId).to.equal('da1d7a33-0260-4e83-a621-14674116f3f9'); - expect(apiRequest.bids[0].sizes).to.have.lengthOf('1'); - expect(apiRequest.bids[0].sizes[0]).to.deep.equal({ - width: 300, - height: 250 - }); + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + + it('should return true when required params found', () => { + expect(spec.isBidRequestValid(BANNER_BID)).to.equal(true); + expect(spec.isBidRequestValid(VIDEO_BID)).to.equal(true); }); }); - describe('interpretResponse', function () { - const bid = { - adUnitCode: 'temp-unitcode', - bidder: 'tpmn', - params: { - inventoryId: '1', - publisherId: 'TPMN' - }, - bidId: '29092404798c9', - bidderRequestId: 'a01', - auctionId: 'da1d7a33-0260-4e83-a621-14674116f3f9', - mediaTypes: { - banner: { - sizes: [[300, 250]] + describe('buildRequests()', function () { + it('should have gdpr data if applicable', function () { + const bid = utils.deepClone(BANNER_BID); + + const req = syncAddFPDToBidderRequest(Object.assign({}, BIDDER_REQUEST, { + gdprConsent: { + consentString: 'consentString', + gdprApplies: true, } + })); + let request = spec.buildRequests([bid], req)[0]; + + const payload = request.data; + expect(payload.user.ext).to.have.property('consent', req.gdprConsent.consentString); + expect(payload.regs.ext).to.have.property('gdpr', 1); + }); + + it('should properly forward ORTB blocking params', function () { + let bid = utils.deepClone(BANNER_BID); + bid = utils.mergeDeep(bid, { + params: { bcat: ['IAB1-1'], badv: ['example.com'], bapp: ['com.example'], battr: [1] }, + mediaTypes: { banner: { battr: [1] } } + }); + + let [request] = spec.buildRequests([bid], BIDDER_REQUEST); + + expect(request).to.exist.and.to.be.an('object'); + const payload = request.data; + expect(payload).to.have.deep.property('bcat', ['IAB1-1']); + expect(payload).to.have.deep.property('badv', ['example.com']); + expect(payload).to.have.deep.property('bapp', ['com.example']); + expect(payload.imp[0].banner).to.have.deep.property('battr', [1]); + }); + + context('when mediaType is banner', function () { + it('should build correct request for banner bid with both w, h', () => { + const bid = utils.deepClone(BANNER_BID); + + const [request] = spec.buildRequests([bid], BIDDER_REQUEST); + const requestData = request.data; + // expect(requestData.imp[0].banner).to.equal(null); + expect(requestData.imp[0].banner.format[0].w).to.equal(300); + expect(requestData.imp[0].banner.format[0].h).to.equal(250); + }); + + it('should create request data', function () { + const bid = utils.deepClone(BANNER_BID); + + let [request] = spec.buildRequests([bid], BIDDER_REQUEST); + expect(request).to.exist.and.to.be.a('object'); + const payload = request.data; + expect(payload.imp[0]).to.have.property('id', bid.bidId); + }); + }); + + context('when mediaType is video', function () { + if (FEATURES.VIDEO) { + it('should return false when there is no video in mediaTypes', () => { + const bid = utils.deepClone(VIDEO_BID); + delete bid.mediaTypes.video; + + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); } - }; - const tempBidRequests = [bid]; - it('should return an empty aray to indicate no valid bids', function () { - const emptyServerResponse = {}; - const bidResponses = spec.interpretResponse(emptyServerResponse, tempBidRequests); - expect(bidResponses).is.an('array').that.is.empty; + if (FEATURES.VIDEO) { + it('should reutrn false if player size is not set', () => { + const bid = utils.deepClone(VIDEO_BID); + delete bid.mediaTypes.video.playerSize; + + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + } + if (FEATURES.VIDEO) { + it('when mediaType is Video - check', () => { + const bid = utils.deepClone(VIDEO_BID); + const check = { + w: 1024, + h: 768, + mimes: ['video/mp4'], + playbackmethod: [2, 4, 6], + api: [1, 2, 4, 6], + protocols: [3, 4, 7, 8, 10], + placement: 1, + minduration: 0, + maxduration: 60, + startdelay: 0, + plcmt: 1 + }; + expect(spec.isBidRequestValid(bid)).to.equal(true); + const requests = spec.buildRequests([bid], BIDDER_REQUEST); + const request = requests[0].data; + expect(request.imp[0].video).to.deep.include({...check}); + }); + } + + if (FEATURES.VIDEO) { + it('when mediaType New Video', () => { + const NEW_VIDEO_BID = { + 'bidder': 'tpmn', + 'params': {'inventoryId': 2, 'bidFloor': 2}, + 'userId': {'pubcid': '88a49ee6-beeb-4dd6-92ac-3b6060e127e1'}, + 'mediaTypes': { + 'video': { + 'context': 'outstream', + 'mimes': ['video/mp4'], + 'playerSize': [[1024, 768]], + 'playbackmethod': [2, 4, 6], + 'protocols': [3, 4], + 'api': [1, 2, 3, 6], + 'placement': 1, + 'minduration': 0, + 'maxduration': 30, + 'startdelay': 0, + 'skip': 1, + 'plcmt': 4 + } + }, + }; + + const check = { + w: 1024, + h: 768, + mimes: [ 'video/mp4' ], + playbackmethod: [2, 4, 6], + api: [1, 2, 3, 6], + protocols: [3, 4], + placement: 1, + minduration: 0, + maxduration: 30, + startdelay: 0, + skip: 1, + plcmt: 4 + } + + expect(spec.isBidRequestValid(NEW_VIDEO_BID)).to.equal(true); + let requests = spec.buildRequests([NEW_VIDEO_BID], BIDDER_REQUEST); + const request = requests[0].data; + expect(request.imp[0].video.w).to.equal(check.w); + expect(request.imp[0].video.h).to.equal(check.h); + expect(request.imp[0].video.placement).to.equal(check.placement); + expect(request.imp[0].video.minduration).to.equal(check.minduration); + expect(request.imp[0].video.maxduration).to.equal(check.maxduration); + expect(request.imp[0].video.startdelay).to.equal(check.startdelay); + expect(request.imp[0].video.skip).to.equal(check.skip); + expect(request.imp[0].video.plcmt).to.equal(check.plcmt); + expect(request.imp[0].video.mimes).to.deep.have.same.members(check.mimes); + expect(request.imp[0].video.playbackmethod).to.deep.have.same.members(check.playbackmethod); + expect(request.imp[0].video.api).to.deep.have.same.members(check.api); + expect(request.imp[0].video.protocols).to.deep.have.same.members(check.protocols); + }); + } + + if (FEATURES.VIDEO) { + it('should use bidder video params if they are set', () => { + let bid = utils.deepClone(VIDEO_BID); + const check = { + api: [1, 2], + mimes: ['video/mp4', 'video/x-flv'], + playbackmethod: [3, 4], + protocols: [5, 6], + placement: 1, + plcmt: 1, + minduration: 0, + maxduration: 30, + startdelay: 0, + w: 640, + h: 480 + + }; + bid.mediaTypes.video = {...check}; + bid.mediaTypes.video.context = 'instream'; + bid.mediaTypes.video.playerSize = [[640, 480]]; + + expect(spec.isBidRequestValid(bid)).to.equal(true); + const requests = spec.buildRequests([bid], BIDDER_REQUEST); + const request = requests[0].data; + expect(request.imp[0].video).to.deep.include({...check}); + }); + } }); - it('should return an empty array to indicate no valid bids', function () { - const mockBidResult = { - requestId: '9cf19229-34f6-4d06-bc1d-0e44e8d616c8', - cpm: 10.0, - creativeId: '1', - width: 300, - height: 250, - netRevenue: true, - currency: 'USD', - ttl: 1800, - ad: '', - adType: 'banner' - }; - const testServerResponse = { - headers: [], - body: [mockBidResult] - }; - const bidResponses = spec.interpretResponse(testServerResponse, tempBidRequests); - expect(bidResponses).deep.equal([mockBidResult]); + }); + + describe('interpretResponse()', function () { + context('when mediaType is banner', function () { + it('should correctly interpret valid banner response', function () { + const bid = utils.deepClone(BANNER_BID); + const [request] = spec.buildRequests([bid], BIDDER_REQUEST); + const response = utils.deepClone(BANNER_BID_RESPONSE); + + const bids = spec.interpretResponse({ body: response }, request); + expect(bids).to.be.an('array').that.is.not.empty; + + expect(bids[0].mediaType).to.equal('banner'); + expect(bids[0].burl).to.equal(BANNER_BID_RESPONSE.seatbid[0].bid[0].burl); + expect(bids[0].currency).to.equal('USD'); + expect(bids[0].requestId).to.equal(BANNER_BID_RESPONSE.seatbid[0].bid[0].impid); + expect(bids[0].cpm).to.equal(BANNER_BID_RESPONSE.seatbid[0].bid[0].price); + expect(bids[0].width).to.equal(BANNER_BID_RESPONSE.seatbid[0].bid[0].w); + expect(bids[0].height).to.equal(BANNER_BID_RESPONSE.seatbid[0].bid[0].h); + expect(bids[0].ad).to.equal(BANNER_BID_RESPONSE.seatbid[0].bid[0].adm); + expect(bids[0].creativeId).to.equal(BANNER_BID_RESPONSE.seatbid[0].bid[0].crid); + expect(bids[0].meta.advertiserDomains[0]).to.equal('https://dummydomain.com'); + expect(bids[0].ttl).to.equal(500); + expect(bids[0].netRevenue).to.equal(true); + }); + + it('should handle empty bid response', function () { + const bid = utils.deepClone(BANNER_BID); + + let request = spec.buildRequests([bid], BIDDER_REQUEST)[0]; + const EMPTY_RESP = Object.assign({}, BANNER_BID_RESPONSE, { 'body': {} }); + const bids = spec.interpretResponse(EMPTY_RESP, request); + expect(bids).to.be.empty; + }); }); + if (FEATURES.VIDEO) { + context('when mediaType is video', function () { + it('should correctly interpret valid instream video response', () => { + const bid = utils.deepClone(VIDEO_BID); + + const [request] = spec.buildRequests([bid], BIDDER_REQUEST); + const bids = spec.interpretResponse({ body: VIDEO_BID_RESPONSE }, request); + expect(bids).to.be.an('array').that.is.not.empty; + + expect(bids[0].mediaType).to.equal('video'); + expect(bids[0].burl).to.equal(VIDEO_BID_RESPONSE.seatbid[0].bid[0].burl); + expect(bids[0].currency).to.equal('USD'); + expect(bids[0].requestId).to.equal(VIDEO_BID_RESPONSE.seatbid[0].bid[0].impid); + expect(bids[0].cpm).to.equal(VIDEO_BID_RESPONSE.seatbid[0].bid[0].price); + expect(bids[0].width).to.equal(VIDEO_BID_RESPONSE.seatbid[0].bid[0].w); + expect(bids[0].height).to.equal(VIDEO_BID_RESPONSE.seatbid[0].bid[0].h); + expect(bids[0].vastXml).to.equal(VIDEO_BID_RESPONSE.seatbid[0].bid[0].adm); + expect(bids[0].rendererUrl).to.equal(VIDEO_RENDERER_URL); + expect(bids[0].creativeId).to.equal(VIDEO_BID_RESPONSE.seatbid[0].bid[0].crid); + expect(bids[0].meta.advertiserDomains[0]).to.equal('https://dummydomain.com'); + expect(bids[0].ttl).to.equal(500); + expect(bids[0].netRevenue).to.equal(true); + }); + }); + } }); describe('getUserSync', function () { diff --git a/test/spec/modules/trafficgateBidAdapter_spec.js b/test/spec/modules/trafficgateBidAdapter_spec.js index ecea0e69ca1..11ff547cc78 100644 --- a/test/spec/modules/trafficgateBidAdapter_spec.js +++ b/test/spec/modules/trafficgateBidAdapter_spec.js @@ -1,223 +1,1373 @@ -import { expect } from 'chai' -import { spec } from '../../../modules/trafficgateBidAdapter' -import { deepStrictEqual, notStrictEqual, ok, strictEqual } from 'assert' - -describe('TrafficGateAdapter', () => { - const bid = { - bidId: '9ec5b177515ee2e5', - bidder: 'trafficgate', +import {expect} from 'chai'; +import {spec} from 'modules/trafficgateBidAdapter'; +import {newBidder} from 'src/adapters/bidderFactory.js'; +import {BANNER, VIDEO} from 'src/mediaTypes.js'; +import {config} from 'src/config.js'; +import * as utils from 'src/utils.js'; +import 'src/prebid.js' +import 'modules/currency.js'; +import 'modules/userId/index.js'; +import 'modules/multibid/index.js'; +import 'modules/priceFloors.js'; +import 'modules/consentManagement.js'; +import 'modules/consentManagementUsp.js'; +import 'modules/schain.js'; +import {deepClone} from 'src/utils.js'; +import {syncAddFPDToBidderRequest} from '../../helpers/fpd.js'; +import {hook} from '../../../src/hook.js'; + +const BidRequestBuilder = function BidRequestBuilder(options) { + const defaults = { + request: { + auctionId: '4fd1ca2d-846c-4211-b9e5-321dfe1709c9', + adUnitCode: 'adunit-code', + bidder: 'trafficgate' + }, params: { - placementId: 1, + placementId: '98765', host: 'example' }, - mediaTypes: { - banner: { - sizes: [[300, 250]] - } + sizes: [[300, 250], [300, 600]], + }; + + const request = { + ...defaults.request, + ...options + }; + + this.withParams = (options) => { + request.params = { + ...defaults.params, + ...options + }; + return this; + }; + + this.build = () => request; +}; + +const BidderRequestBuilder = function BidderRequestBuilder(options) { + const defaults = { + bidderCode: 'trafficgate', + auctionId: '4fd1ca2d-846c-4211-b9e5-321dfe1709c9', + bidderRequestId: '7g36s867Tr4xF90X', + timeout: 3000, + refererInfo: { + numIframes: 0, + reachedTop: true, + referer: 'http://test.io/index.html?pbjs_debug=true' } - } - - describe('isBidRequestValid', () => { - it('Should return true if there are bidId, params and placementId parameters present', () => { - strictEqual(true, spec.isBidRequestValid(bid)) - }) - - it('Should return false if at least one of parameters is not present', () => { - const b = { ...bid } - delete b.params.placementId - strictEqual(false, spec.isBidRequestValid(b)) - }) - - it('Should return false if at least one of parameters is not present', () => { - const b = { ...bid } - delete b.params.host - strictEqual(false, spec.isBidRequestValid(b)) - }) - }) - - describe('buildRequests', () => { - const serverRequest = spec.buildRequests([bid]) - - it('Creates a ServerRequest object with method, URL and data', () => { - ok(serverRequest) - ok(serverRequest.method) - ok(serverRequest.url) - ok(serverRequest.data) - }) - - it('Returns POST method', () => { - strictEqual('POST', serverRequest.method) - }) - - it('Returns valid URL', () => { - strictEqual('https://example.bc-plugin.com/?c=o&m=multi', serverRequest.url) - }) - - it('Returns valid data if array of bids is valid', () => { - const { data } = serverRequest - strictEqual('object', typeof data) - deepStrictEqual(['language', 'secure', 'host', 'page', 'placements'], Object.keys(data)) - strictEqual('string', typeof data.language) - strictEqual('string', typeof data.host) - strictEqual('string', typeof data.page) - notStrictEqual(-1, [0, 1].indexOf(data.secure)) - - const placement = data.placements[0] - deepStrictEqual(['placementId', 'bidId', 'traffic'], Object.keys(placement)) - strictEqual(1, placement.placementId) - strictEqual('9ec5b177515ee2e5', placement.bidId) - strictEqual('banner', placement.traffic) - }) - - it('Returns empty data if no valid requests are passed', () => { - deepStrictEqual([], spec.buildRequests([])) - }) - }) - - describe('interpretResponse', () => { - const validData = [ - { - body: [{ - mediaType: 'banner', - width: 300, - height: 250, - cpm: 0.4, - ad: 'Test', - requestId: '9ec5b177515ee2e5', - ttl: 120, - creativeId: '2', - netRevenue: true, - currency: 'USD', - dealId: '1', - meta: { - advertiserDomains: ['test.com'] + }; + + const request = { + ...defaults, + ...options + }; + + this.build = () => request; +}; + +describe('TrafficgateOpenxRtbAdapter', function () { + before(() => { + hook.ready(); + }); + + const adapter = newBidder(spec); + + describe('inherited functions', function () { + it('exists and is a function', function () { + expect(adapter.callBids).to.exist.and.to.be.a('function'); + }); + }); + + describe('isBidRequestValid()', function () { + describe('when request is for a banner ad', function () { + let bannerBid; + beforeEach(function () { + bannerBid = { + bidder: 'trafficgate', + params: {}, + adUnitCode: 'adunit-code', + mediaTypes: {banner: {}}, + sizes: [[300, 250], [300, 600]], + bidId: '30b31c1838de1e', + bidderRequestId: '22edbae2733bf6', + auctionId: '1d1a030790a475' + }; + }); + + it('should return false when there is placementId only', function () { + bannerBid.params = {'placementId': '98765'}; + expect(spec.isBidRequestValid(bannerBid)).to.equal(false); + }); + + describe('should return false when there is a host only', function () { + beforeEach(function () { + bannerBid.params = {host: 'test-delivery-domain'} + }); + + it('should return false when there is no placementId and size', function () { + expect(spec.isBidRequestValid(bannerBid)).to.equal(false); + }); + + it('should return false if there is an placementId without sizes', function () { + bannerBid.params.placementId = '98765'; + expect(spec.isBidRequestValid(bannerBid)).to.equal(false); + }); + + it('should return false if there is no placementId and sizes are defined', function () { + bannerBid.mediaTypes.banner.sizes = [720, 90]; + expect(spec.isBidRequestValid(bannerBid)).to.equal(false); + }); + + it('should return false if no sizes are defined ', function () { + expect(spec.isBidRequestValid(bannerBid)).to.equal(false); + }); + + it('should return false if sizes empty ', function () { + bannerBid.mediaTypes.banner.sizes = []; + expect(spec.isBidRequestValid(bannerBid)).to.equal(false); + }); + + it('should return true if there is placementId and sizes are defined', function () { + bannerBid.params.placementId = '98765'; + bannerBid.mediaTypes.banner.sizes = [720, 90]; + expect(spec.isBidRequestValid(bannerBid)).to.equal(true); + }); + }); + }); + + describe('when request is for a multiformat ad', function () { + describe('and request config uses mediaTypes video and banner', () => { + const multiformatBid = { + bidder: 'trafficgate', + params: { + placementId: '98765', + host: 'example' + }, + adUnitCode: 'adunit-code', + mediaTypes: { + banner: { + sizes: [[300, 250]] + }, + video: { + playerSize: [300, 250] + } + }, + bidId: '30b31c1838de1e', + bidderRequestId: '22edbae2733bf6', + auctionId: '1d1a030790a475', + transactionId: '4008d88a-8137-410b-aa35-fbfdabcb478e' + }; + it('should return true multisize when required params found', function () { + expect(spec.isBidRequestValid(multiformatBid)).to.equal(true); + }); + }); + }); + + describe('when request is for a video ad', function () { + describe('and request config uses mediaTypes', () => { + const videoBidWithMediaTypes = { + bidder: 'trafficgate', + params: { + placementId: '98765', + host: 'example' + }, + adUnitCode: 'adunit-code', + mediaTypes: { + video: { + playerSize: [640, 480] + } + }, + bidId: '30b31c1838de1e', + bidderRequestId: '22edbae2733bf6', + auctionId: '1d1a030790a475', + transactionId: '4008d88a-8137-410b-aa35-fbfdabcb478e' + }; + it('should return false when isBannerBid', function () { + expect(spec.isBannerBid(videoBidWithMediaTypes)).to.equal(false); + }); + + it('should return true when required params found', function () { + expect(spec.isBidRequestValid(videoBidWithMediaTypes)).to.equal(true); + }); + + it('should return false when required params are not passed', function () { + let videoBidWithMediaTypes = Object.assign({}, videoBidWithMediaTypes); + videoBidWithMediaTypes.params = {}; + expect(spec.isBidRequestValid(videoBidWithMediaTypes)).to.equal(false); + }); + }); + describe('and request config uses both host and platform', () => { + const videoBidWithHostAndPlacement = { + bidder: 'trafficgate', + params: { + placementId: '98765', + host: 'example' + }, + adUnitCode: 'adunit-code', + mediaTypes: { + video: { + playerSize: [640, 480] + } + }, + bidId: '30b31c1838de1e', + bidderRequestId: '22edbae2733bf6', + auctionId: '1d1a030790a475', + transactionId: '4008d88a-8137-410b-aa35-fbfdabcb478e' + }; + it('should return false when isBannerBid', function () { + expect(spec.isBannerBid(videoBidWithHostAndPlacement)).to.equal(false); + }); + + it('should return true when required params found', function () { + expect(spec.isBidRequestValid(videoBidWithHostAndPlacement)).to.equal(true); + }); + + it('should return false when required params are not passed', function () { + let videoBidWithMediaTypes = Object.assign({}, videoBidWithHostAndPlacement); + videoBidWithMediaTypes.params = {}; + expect(spec.isBidRequestValid(videoBidWithMediaTypes)).to.equal(false); + }); + }); + describe('and request config uses mediaType', () => { + const videoBidWithMediaType = { + 'bidder': 'trafficgate', + 'params': { + 'placementId': '98765', + 'host': 'example' + }, + 'adUnitCode': 'adunit-code', + 'mediaType': 'video', + 'sizes': [640, 480], + 'bidId': '30b31c1838de1e', + 'bidderRequestId': '22edbae2733bf6', + 'auctionId': '1d1a030790a475', + 'transactionId': '4008d88a-8137-410b-aa35-fbfdabcb478e' + }; + it('should return true when required params found', function () { + expect(spec.isBidRequestValid(videoBidWithMediaType)).to.equal(true); + }); + + it('should return false when required params are not passed', function () { + let videoBidWithMediaType = Object.assign({}, videoBidWithMediaType); + delete videoBidWithMediaType.params; + videoBidWithMediaType.params = {}; + expect(spec.isBidRequestValid(videoBidWithMediaType)).to.equal(false); + }); + }); + }); + }); + + describe('buildRequests()', function () { + let bidRequestsWithMediaTypes; + let bidRequestsWithPlatform; + let mockBidderRequest; + + beforeEach(function () { + mockBidderRequest = {refererInfo: {}}; + + bidRequestsWithMediaTypes = [{ + bidder: 'trafficgate', + params: { + placementId: '11', + host: 'example', + }, + adUnitCode: '/adunit-code/test-path', + mediaTypes: { + banner: { + sizes: [[300, 250], [300, 600]] } - }] - }, - { - body: [{ - vastUrl: 'example.com', - mediaType: 'video', - cpm: 0.5, - requestId: '9ec5b177515ee2e5', - ttl: 120, - creativeId: '2', - netRevenue: true, - currency: 'USD', - dealId: '1', - meta: { - advertiserDomains: ['test.com'] + }, + bidId: 'test-bid-id-1', + bidderRequestId: 'test-bid-request-1', + auctionId: 'test-auction-1', + transactionId: 'test-transactionId-1', + ortb2Imp: { + ext: { + ae: 2 } - }] - }, - { - body: [{ - mediaType: 'native', - clickUrl: 'example.com', - title: 'Test', - image: 'example.com', - creativeId: '2', - impressionTrackers: ['example.com'], - ttl: 120, - cpm: 0.4, - requestId: '9ec5b177515ee2e5', - netRevenue: true, - currency: 'USD', - meta: { - advertiserDomains: ['test.com'] + } + }, { + bidder: 'trafficgate', + params: { + placementId: '22', + host: 'example', + }, + adUnitCode: 'adunit-code', + mediaTypes: { + video: { + playerSize: [640, 480] } - }] - } - ] + }, + bidId: 'test-bid-id-2', + bidderRequestId: 'test-bid-request-2', + auctionId: 'test-auction-2', + transactionId: 'test-transactionId-2' + }]; + }); - for (const obj of validData) { - const { mediaType } = obj.body[0] + context('common requests checks', function() { + it('should be able to handle multiformat requests', () => { + const multiformat = utils.deepClone(bidRequestsWithMediaTypes[0]); + multiformat.mediaTypes.video = { + context: 'outstream', + playerSize: [640, 480] + } + const requests = spec.buildRequests([multiformat], mockBidderRequest); + const outgoingFormats = requests.flatMap(rq => rq.data.imp.flatMap(imp => ['banner', 'video'].filter(k => imp[k] != null))); + const expected = FEATURES.VIDEO ? ['banner', 'video'] : ['banner'] + expect(outgoingFormats).to.have.members(expected); + }) - it(`Should interpret ${mediaType} response`, () => { - const response = spec.interpretResponse(obj) + it('should send bid request to trafficgate url via POST', function () { + const request = spec.buildRequests(bidRequestsWithMediaTypes, mockBidderRequest); + expect(request[0].url).to.equal('https://example.bc-plugin.com/prebidjs'); + expect(request[0].method).to.equal('POST'); + }); - expect(response).to.be.an('array') - strictEqual(1, response.length) + it('should send delivery domain, if available', function () { + const request = spec.buildRequests(bidRequestsWithMediaTypes, mockBidderRequest); + expect(request[0].data.imp[0].ext.bidder.host).to.equal(bidRequestsWithMediaTypes[0].params.host); + expect(request[1].data.imp[0].ext.bidder.host).to.equal(bidRequestsWithMediaTypes[1].params.host); + }); - const copy = { ...obj.body[0] } - deepStrictEqual(copy, response[0]) - }) - } + it('should send placementId', function () { + const request = spec.buildRequests(bidRequestsWithMediaTypes, mockBidderRequest); + expect(request[0].data.imp[0].ext.bidder.placementId).to.equal(bidRequestsWithMediaTypes[0].params.placementId); + expect(request[1].data.imp[0].ext.bidder.placementId).to.equal(bidRequestsWithMediaTypes[1].params.placementId); + }); + + describe('floors', function () { + it('should send out custom floors on bids that have customFloors, no currency as account currency is used', function () { + const bidRequest = Object.assign({}, + bidRequestsWithMediaTypes[0], + { + params: { + placementId: '98765', + host: 'example', + customFloor: 1.500 + } + } + ); - for (const obj of validData) { - it(`Should interpret response has meta.advertiserDomains`, () => { - const response = spec.interpretResponse(obj) + const request = spec.buildRequests([bidRequest], mockBidderRequest); + expect(request[0].data.imp[0].bidfloor).to.equal(bidRequest.params.customFloor); + expect(request[0].data.imp[0].bidfloorcur).to.equal(undefined); + }); - expect(response[0]['meta']['advertiserDomains']).to.be.an('array') - expect(response[0]['meta']['advertiserDomains'][0]).to.be.an('string') + context('with floors module', function () { + let adServerCurrencyStub; + + beforeEach(function () { + adServerCurrencyStub = sinon + .stub(config, 'getConfig') + .withArgs('currency.adServerCurrency') + }); + + afterEach(function () { + config.getConfig.restore(); + }); + + it('should send out floors on bids in USD', function () { + const bidRequest = Object.assign({}, + bidRequestsWithMediaTypes[0], + { + getFloor: () => { + return { + currency: 'USD', + floor: 9.99 + } + } + } + ); + + const request = spec.buildRequests([bidRequest], mockBidderRequest); + expect(request[0].data.imp[0].bidfloor).to.equal(9.99); + expect(request[0].data.imp[0].bidfloorcur).to.equal('USD'); + }); + + it('should send not send floors', function () { + adServerCurrencyStub.returns('EUR'); + const bidRequest = Object.assign({}, + bidRequestsWithMediaTypes[0], + { + getFloor: () => { + return { + currency: 'BTC', + floor: 9.99 + } + } + } + ); + + const request = spec.buildRequests([bidRequest], mockBidderRequest); + expect(request[0].data.imp[0].bidfloor).to.equal(undefined) + expect(request[0].data.imp[0].bidfloorcur).to.equal(undefined) + }); + }) }) + + describe('FPD', function() { + let bidRequests; + const mockBidderRequest = {refererInfo: {}}; + + beforeEach(function () { + bidRequests = [{ + bidder: 'trafficgate', + params: { + placementId: '98765-banner', + host: 'example' + }, + adUnitCode: 'adunit-code', + mediaTypes: { + banner: { + sizes: [[300, 250], [300, 600]] + } + }, + bidId: 'test-bid-id', + bidderRequestId: 'test-bidder-request-id', + auctionId: 'test-auction-id', + transactionId: 'test-transaction-id-1' + }, { + bidder: 'trafficgate', + mediaTypes: { + video: { + playerSize: [640, 480] + } + }, + params: { + placementId: '98765-video', + host: 'example' + }, + 'adUnitCode': 'adunit-code', + + bidId: 'test-bid-id', + bidderRequestId: 'test-bidder-request-id', + auctionId: 'test-auction-id', + transactionId: 'test-transaction-id-2' + }]; + }); + + it('ortb2.site should be merged in the request', function() { + const request = spec.buildRequests(bidRequests, { + ...mockBidderRequest, + 'ortb2': { + site: { + domain: 'page.example.com', + cat: ['IAB2'], + sectioncat: ['IAB2-2'] + } + } + }); + let data = request[0].data; + expect(data.site.domain).to.equal('page.example.com'); + expect(data.site.cat).to.deep.equal(['IAB2']); + expect(data.site.sectioncat).to.deep.equal(['IAB2-2']); + }); + + it('ortb2.user should be merged in the request', function() { + const request = spec.buildRequests(bidRequests, { + ...mockBidderRequest, + 'ortb2': { + user: { + yob: 1985 + } + } + }); + let data = request[0].data; + expect(data.user.yob).to.equal(1985); + }); + + describe('ortb2Imp', function() { + describe('ortb2Imp.ext.data.pbadslot', function() { + beforeEach(function () { + if (bidRequests[0].hasOwnProperty('ortb2Imp')) { + delete bidRequests[0].ortb2Imp; + } + }); + + it('should not send if imp[].ext.data object is invalid', function() { + bidRequests[0].ortb2Imp = { + ext: {} + }; + const request = spec.buildRequests(bidRequests, mockBidderRequest); + let data = request[0].data; + expect(data.imp[0].ext).to.not.have.property('data'); + }); + + it('should not send if imp[].ext.data.pbadslot is undefined', function() { + bidRequests[0].ortb2Imp = { + ext: { + data: { + } + } + }; + const request = spec.buildRequests(bidRequests, mockBidderRequest); + let data = request[0].data; + if (data.imp[0].ext.data) { + expect(data.imp[0].ext.data).to.not.have.property('pbadslot'); + } else { + expect(data.imp[0].ext).to.not.have.property('data'); + } + }); + + it('should send if imp[].ext.data.pbadslot is string', function() { + bidRequests[0].ortb2Imp = { + ext: { + data: { + pbadslot: 'abcd' + } + } + }; + const request = spec.buildRequests(bidRequests, mockBidderRequest); + let data = request[0].data; + expect(data.imp[0].ext.data).to.have.property('pbadslot'); + expect(data.imp[0].ext.data.pbadslot).to.equal('abcd'); + }); + }); + + describe('ortb2Imp.ext.data.adserver', function() { + beforeEach(function () { + if (bidRequests[0].hasOwnProperty('ortb2Imp')) { + delete bidRequests[0].ortb2Imp; + } + }); + + it('should not send if imp[].ext.data object is invalid', function() { + bidRequests[0].ortb2Imp = { + ext: {} + }; + const request = spec.buildRequests(bidRequests, mockBidderRequest); + let data = request[0].data; + expect(data.imp[0].ext).to.not.have.property('data'); + }); + + it('should not send if imp[].ext.data.adserver is undefined', function() { + bidRequests[0].ortb2Imp = { + ext: { + data: { + } + } + }; + const request = spec.buildRequests(bidRequests, mockBidderRequest); + let data = request[0].data; + if (data.imp[0].ext.data) { + expect(data.imp[0].ext.data).to.not.have.property('adserver'); + } else { + expect(data.imp[0].ext).to.not.have.property('data'); + } + }); + + it('should send', function() { + let adSlotValue = 'abc'; + bidRequests[0].ortb2Imp = { + ext: { + data: { + adserver: { + name: 'GAM', + adslot: adSlotValue + } + } + } + }; + const request = spec.buildRequests(bidRequests, mockBidderRequest); + let data = request[0].data; + expect(data.imp[0].ext.data.adserver.name).to.equal('GAM'); + expect(data.imp[0].ext.data.adserver.adslot).to.equal(adSlotValue); + }); + }); + + describe('ortb2Imp.ext.data.other', function() { + beforeEach(function () { + if (bidRequests[0].hasOwnProperty('ortb2Imp')) { + delete bidRequests[0].ortb2Imp; + } + }); + + it('should not send if imp[].ext.data object is invalid', function() { + bidRequests[0].ortb2Imp = { + ext: {} + }; + const request = spec.buildRequests(bidRequests, mockBidderRequest); + let data = request[0].data; + expect(data.imp[0].ext).to.not.have.property('data'); + }); + + it('should not send if imp[].ext.data.other is undefined', function() { + bidRequests[0].ortb2Imp = { + ext: { + data: { + } + } + }; + const request = spec.buildRequests(bidRequests, mockBidderRequest); + let data = request[0].data; + if (data.imp[0].ext.data) { + expect(data.imp[0].ext.data).to.not.have.property('other'); + } else { + expect(data.imp[0].ext).to.not.have.property('data'); + } + }); + + it('ortb2Imp.ext.data.other', function() { + bidRequests[0].ortb2Imp = { + ext: { + data: { + other: 1234 + } + } + }; + const request = spec.buildRequests(bidRequests, mockBidderRequest); + let data = request[0].data; + expect(data.imp[0].ext.data.other).to.equal(1234); + }); + }); + }); + + describe('with user agent client hints', function () { + it('should add device.sua if available', function () { + const bidderRequestWithUserAgentClientHints = { refererInfo: {}, + ortb2: { + device: { + sua: { + source: 2, + platform: { + brand: 'macOS', + version: [ '12', '4', '0' ] + }, + browsers: [ + { + brand: 'Chromium', + version: [ '106', '0', '5249', '119' ] + }, + { + brand: 'Google Chrome', + version: [ '106', '0', '5249', '119' ] + }, + { + brand: 'Not;A=Brand', + version: [ '99', '0', '0', '0' ] + }], + mobile: 0, + model: 'Pro', + bitness: '64', + architecture: 'x86' + } + } + }}; + + let request = spec.buildRequests(bidRequests, bidderRequestWithUserAgentClientHints); + expect(request[0].data.device.sua).to.exist; + expect(request[0].data.device.sua).to.deep.equal(bidderRequestWithUserAgentClientHints.ortb2.device.sua); + const bidderRequestWithoutUserAgentClientHints = {refererInfo: {}, ortb2: {}}; + request = spec.buildRequests(bidRequests, bidderRequestWithoutUserAgentClientHints); + expect(request[0].data.device?.sua).to.not.exist; + }); + }); + }); + + context('when there is a consent management framework', function () { + let bidRequests; + let mockConfig; + let bidderRequest; + + beforeEach(function () { + bidRequests = [{ + bidder: 'trafficgate', + params: { + placementId: '98765-banner', + host: 'example' + }, + adUnitCode: 'adunit-code', + mediaTypes: { + banner: { + sizes: [[300, 250], [300, 600]] + } + }, + bidId: 'test-bid-id', + bidderRequestId: 'test-bidder-request-id', + auctionId: 'test-auction-id', + transactionId: 'test-transaction-id-1' + }, { + bidder: 'trafficgate', + mediaTypes: { + video: { + playerSize: [640, 480] + } + }, + params: { + placementId: '98765-video', + host: 'example' + }, + 'adUnitCode': 'adunit-code', + + bidId: 'test-bid-id', + bidderRequestId: 'test-bidder-request-id', + auctionId: 'test-auction-id', + transactionId: 'test-transaction-id-2' + }]; + }); + + describe('us_privacy', function () { + beforeEach(function () { + bidderRequest = { + uspConsent: '1YYN', + refererInfo: {} + }; + + sinon.stub(config, 'getConfig').callsFake((key) => { + return utils.deepAccess(mockConfig, key); + }); + }); + + afterEach(function () { + config.getConfig.restore(); + }); + + it('should send a signal to specify that US Privacy applies to this request', function () { + const request = spec.buildRequests(bidRequests, syncAddFPDToBidderRequest(bidderRequest)); + expect(request[0].data.regs.ext.us_privacy).to.equal('1YYN'); + expect(request[1].data.regs.ext.us_privacy).to.equal('1YYN'); + }); + + it('should not send the regs object, when consent string is undefined', function () { + delete bidderRequest.uspConsent; + const request = spec.buildRequests(bidRequests, syncAddFPDToBidderRequest(bidderRequest)); + expect(request[0].data.regs?.us_privacy).to.not.exist; + }); + }); + + describe('GDPR', function () { + beforeEach(function () { + bidderRequest = { + gdprConsent: { + consentString: 'test-gdpr-consent-string', + addtlConsent: 'test-addtl-consent-string', + gdprApplies: true + }, + refererInfo: {} + }; + + mockConfig = { + consentManagement: { + cmpApi: 'iab', + timeout: 1111, + allowAuctionWithoutConsent: 'cancel' + } + }; + + sinon.stub(config, 'getConfig').callsFake((key) => { + return utils.deepAccess(mockConfig, key); + }); + }); + + afterEach(function () { + config.getConfig.restore(); + }); + + it('should send a signal to specify that GDPR applies to this request', function () { + bidderRequest.bids = bidRequests; + const request = spec.buildRequests(bidRequests, syncAddFPDToBidderRequest(bidderRequest)); + expect(request[0].data.regs.ext.gdpr).to.equal(1); + expect(request[1].data.regs.ext.gdpr).to.equal(1); + }); + + it('should send the consent string', function () { + bidderRequest.bids = bidRequests; + const request = spec.buildRequests(bidRequests, syncAddFPDToBidderRequest(bidderRequest)); + expect(request[0].data.user.ext.consent).to.equal(bidderRequest.gdprConsent.consentString); + expect(request[1].data.user.ext.consent).to.equal(bidderRequest.gdprConsent.consentString); + }); + + it('should send the addtlConsent string', function () { + bidderRequest.bids = bidRequests; + const request = spec.buildRequests(bidRequests, syncAddFPDToBidderRequest(bidderRequest)); + expect(request[0].data.user.ext.ConsentedProvidersSettings.consented_providers).to.equal(bidderRequest.gdprConsent.addtlConsent); + expect(request[1].data.user.ext.ConsentedProvidersSettings.consented_providers).to.equal(bidderRequest.gdprConsent.addtlConsent); + }); + + it('should send a signal to specify that GDPR does not apply to this request', function () { + bidderRequest.gdprConsent.gdprApplies = false; + bidderRequest.bids = bidRequests; + const request = spec.buildRequests(bidRequests, syncAddFPDToBidderRequest(bidderRequest)); + expect(request[0].data.regs.ext.gdpr).to.equal(0); + expect(request[1].data.regs.ext.gdpr).to.equal(0); + }); + + it('when GDPR application is undefined, should not send a signal to specify whether GDPR applies to this request, ' + + 'but can send consent data, ', function () { + delete bidderRequest.gdprConsent.gdprApplies; + bidderRequest.bids = bidRequests; + const request = spec.buildRequests(bidRequests, syncAddFPDToBidderRequest(bidderRequest)); + expect(request[0].data.regs?.ext?.gdpr).to.not.be.ok; + expect(request[0].data.user.ext.consent).to.equal(bidderRequest.gdprConsent.consentString); + expect(request[1].data.user.ext.consent).to.equal(bidderRequest.gdprConsent.consentString); + }); + + it('when consent string is undefined, should not send the consent string, ', function () { + delete bidderRequest.gdprConsent.consentString; + bidderRequest.bids = bidRequests; + const request = spec.buildRequests(bidRequests, syncAddFPDToBidderRequest(bidderRequest)); + expect(request[0].data.imp[0].ext.consent).to.equal(undefined); + expect(request[1].data.imp[0].ext.consent).to.equal(undefined); + }); + }); + }); + + context('coppa', function() { + it('when there are no coppa param settings, should not send a coppa flag', function () { + const request = spec.buildRequests(bidRequestsWithMediaTypes, syncAddFPDToBidderRequest(mockBidderRequest)); + expect(request[0].data.regs?.coppa).to.be.not.ok; + }); + + it('should send a coppa flag there is when there is coppa param settings in the bid requests', function () { + let mockConfig = { + coppa: true + }; + + sinon.stub(config, 'getConfig').callsFake((key) => { + return utils.deepAccess(mockConfig, key); + }); + + const request = spec.buildRequests(bidRequestsWithMediaTypes, syncAddFPDToBidderRequest(mockBidderRequest)); + expect(request[0].data.regs.coppa).to.equal(1); + }); + + it('should send a coppa flag there is when there is coppa param settings in the bid params', function () { + const request = spec.buildRequests(bidRequestsWithMediaTypes, syncAddFPDToBidderRequest(mockBidderRequest)); + request.params = {coppa: true}; + expect(request[0].data.regs.coppa).to.equal(1); + }); + + after(function () { + config.getConfig.restore() + }); + }); + + context('do not track (DNT)', function() { + let doNotTrackStub; + + beforeEach(function () { + doNotTrackStub = sinon.stub(utils, 'getDNT'); + }); + afterEach(function() { + doNotTrackStub.restore(); + }); + + it('when there is a do not track, should send a dnt', function () { + doNotTrackStub.returns(1); + + const request = spec.buildRequests(bidRequestsWithMediaTypes, syncAddFPDToBidderRequest(mockBidderRequest)); + expect(request[0].data.device.dnt).to.equal(1); + }); + + it('when there is not do not track, don\'t send dnt', function () { + doNotTrackStub.returns(0); + + const request = spec.buildRequests(bidRequestsWithMediaTypes, syncAddFPDToBidderRequest(mockBidderRequest)); + expect(request[0].data.device.dnt).to.equal(0); + }); + + it('when there is no defined do not track, don\'t send dnt', function () { + doNotTrackStub.returns(null); + + const request = spec.buildRequests(bidRequestsWithMediaTypes, syncAddFPDToBidderRequest(mockBidderRequest)); + expect(request[0].data.device.dnt).to.equal(0); + }); + }); + + context('supply chain (schain)', function () { + let bidRequests; + let schainConfig; + const supplyChainNodePropertyOrder = ['asi', 'sid', 'hp', 'rid', 'name', 'domain']; + + beforeEach(function () { + schainConfig = { + ver: '1.0', + complete: 1, + nodes: [ + { + asi: 'exchange1.com', + sid: '1234', + hp: 1, + rid: 'bid-request-1', + name: 'publisher', + domain: 'publisher.com' + // omitted ext + }, + { + asi: 'exchange2.com', + sid: 'abcd', + hp: 1, + rid: 'bid-request-2', + // name field missing + domain: 'intermediary.com' + }, + { + asi: 'exchange3.com', + sid: '4321', + hp: 1, + // request id + // name field missing + domain: 'intermediary-2.com' + } + ] + }; + + bidRequests = [{ + bidder: 'trafficgate', + params: { + placementId: '11', + host: 'example' + }, + adUnitCode: '/adunit-code/test-path', + mediaTypes: { + banner: { + sizes: [[300, 250], [300, 600]] + } + }, + bidId: 'test-bid-id-1', + bidderRequestId: 'test-bid-request-1', + auctionId: 'test-auction-1', + schain: schainConfig + }]; + }); + + it('should send a supply chain object', function () { + const request = spec.buildRequests(bidRequests, mockBidderRequest); + expect(request[0].data.source.ext.schain).to.equal(schainConfig); + }); + + it('should send the supply chain object with the right version', function () { + const request = spec.buildRequests(bidRequests, mockBidderRequest); + expect(request[0].data.source.ext.schain.ver).to.equal(schainConfig.ver); + }); + + it('should send the supply chain object with the right complete value', function () { + const request = spec.buildRequests(bidRequests, mockBidderRequest); + expect(request[0].data.source.ext.schain.complete).to.equal(schainConfig.complete); + }); + }); + + context('when there are userid providers', function () { + const userIdAsEids = [ + { + source: 'adserver.org', + uids: [{ + id: 'some-random-id-value', + atype: 1, + ext: { + rtiPartner: 'TDID' + } + }] + }, + { + source: 'id5-sync.com', + uids: [{ + id: 'some-random-id-value', + atype: 1 + }] + }, + { + source: 'sharedid.org', + uids: [{ + id: 'some-random-id-value', + atype: 1, + ext: { + third: 'some-random-id-value' + } + }] + } + ]; + + it(`should send the user id under the extended ids`, function () { + const bidRequestsWithUserId = [{ + bidder: 'trafficgate', + params: { + placementId: '11', + host: 'example' + }, + userId: { + }, + adUnitCode: 'adunit-code', + mediaTypes: { + banner: { + sizes: [[300, 250], [300, 600]] + } + }, + bidId: 'test-bid-id-1', + bidderRequestId: 'test-bid-request-1', + auctionId: 'test-auction-1', + userIdAsEids: userIdAsEids + }]; + // enrich bid request with userId key/value + + const request = spec.buildRequests(bidRequestsWithUserId, mockBidderRequest); + expect(request[0].data.user.ext.eids).to.equal(userIdAsEids); + }); + + it(`when no user ids are available, it should not send any extended ids`, function () { + const request = spec.buildRequests(bidRequestsWithMediaTypes, mockBidderRequest); + expect(request[0].data).to.not.have.any.keys('user'); + }); + }); + + context('FLEDGE', function() { + it('when FLEDGE is enabled, should send whatever is set in ortb2imp.ext.ae in all bid requests', function () { + const request = spec.buildRequests(bidRequestsWithMediaTypes, { + ...mockBidderRequest, + fledgeEnabled: true + }); + expect(request[0].data.imp[0].ext.ae).to.equal(2); + }); + }); + }); + + context('banner', function () { + it('should send bid request with a mediaTypes specified with banner type', function () { + const request = spec.buildRequests(bidRequestsWithMediaTypes, mockBidderRequest); + expect(request[0].data.imp[0]).to.have.any.keys(BANNER); + }); + }); + + if (FEATURES.VIDEO) { + context('video', function () { + it('should send bid request with a mediaTypes specified with video type', function () { + const request = spec.buildRequests(bidRequestsWithMediaTypes, mockBidderRequest); + expect(request[1].data.imp[0]).to.have.any.keys(VIDEO); + }); + + it('Update imp.video with OpenRTB options from mimeTypes and params', function() { + const bid01 = new BidRequestBuilder({ + adUnitCode: 'adunit-code-01', + mediaTypes: { + banner: { sizes: [[300, 250]] }, + video: { + context: 'outstream', + playerSize: [[300, 250]], + mimes: ['video/mp4'], + protocols: [8] + } + }, + }).withParams({ + // options in video, will merge + video: { + skip: 1, + skipafter: 4, + minduration: 10, + maxduration: 30 + } + }).build(); + + const bidderRequest = new BidderRequestBuilder().build(); + const expected = { + mimes: ['video/mp4'], + skip: 1, + skipafter: 4, + minduration: 10, + maxduration: 30, + placement: 4, + protocols: [8], + w: 300, + h: 250 + }; + const requests = spec.buildRequests([bid01], bidderRequest); + expect(requests).to.have.lengthOf(2); + expect(requests[1].data.imp[0].video).to.deep.equal(expected); + }); + }); } + }); - const invalidData = [ - { - body: [{ - width: 300, - cpm: 0.4, - ad: 'Test', - requestId: '9ec5b177515ee2e5', - ttl: 120, - creativeId: '2', - netRevenue: true, - currency: 'USD', - dealId: '1' - }] + describe('interpretResponse()', function () { + let bidRequestConfigs; + let bidRequest; + let bidResponse; + let bid; + + context('when there is an nbr response', function () { + let bids; + beforeEach(function () { + bidRequestConfigs = [{ + bidder: 'trafficgate', + params: { + placementId: '98765', + host: 'example' + }, + adUnitCode: 'adunit-code', + mediaTypes: { + banner: { + sizes: [[300, 250], [300, 600]], + }, + }, + bidId: 'test-bid-id', + bidderRequestId: 'test-bidder-request-id', + auctionId: 'test-auction-id' + }]; + + bidRequest = spec.buildRequests(bidRequestConfigs, {refererInfo: {}})[0]; + + bidResponse = {nbr: 0}; // Unknown error + bids = spec.interpretResponse({body: bidResponse}, bidRequest); + }); + + it('should not return any bids', function () { + expect(bids.length).to.equal(0); + }); + }); + + context('when no seatbid in response', function () { + let bids; + beforeEach(function () { + bidRequestConfigs = [{ + bidder: 'trafficgate', + params: { + placementId: '98765', + host: 'example' + }, + adUnitCode: 'adunit-code', + mediaTypes: { + banner: { + sizes: [[300, 250], [300, 600]], + }, + }, + bidId: 'test-bid-id', + bidderRequestId: 'test-bidder-request-id', + auctionId: 'test-auction-id' + }]; + + bidRequest = spec.buildRequests(bidRequestConfigs, {refererInfo: {}})[0]; + + bidResponse = {ext: {}, id: 'test-bid-id'}; + bids = spec.interpretResponse({body: bidResponse}, bidRequest); + }); + + it('should not return any bids', function () { + expect(bids.length).to.equal(0); + }); + }); + + context('when there is no response', function () { + let bids; + beforeEach(function () { + bidRequestConfigs = [{ + bidder: 'trafficgate', + params: { + placementId: '98765', + host: 'example' + }, + adUnitCode: 'adunit-code', + mediaTypes: { + banner: { + sizes: [[300, 250], [300, 600]], + }, + }, + bidId: 'test-bid-id', + bidderRequestId: 'test-bidder-request-id', + auctionId: 'test-auction-id' + }]; + + bidRequest = spec.buildRequests(bidRequestConfigs, {refererInfo: {}})[0]; + + bidResponse = ''; // Unknown error + bids = spec.interpretResponse({body: bidResponse}, bidRequest); + }); + + it('should not return any bids', function () { + expect(bids.length).to.equal(0); + }); + }); + + const SAMPLE_BID_REQUESTS = [{ + bidder: 'trafficgate', + params: { + placementId: '98765', + host: 'example' }, - { - body: [{ - mediaType: 'video', - cpm: 0.5, - requestId: '9ec5b177515ee2e5', - ttl: 120, - creativeId: '2', - netRevenue: true, - currency: 'USD', - dealId: '1' - }] + adUnitCode: 'adunit-code', + mediaTypes: { + banner: { + sizes: [[300, 250], [300, 600]], + }, }, - { - body: [{ - mediaType: 'native', - clickUrl: 'example.com', - title: 'Test', - impressionTrackers: ['example.com'], - ttl: 120, - requestId: '9ec5b177515ee2e5', - creativeId: '2', - netRevenue: true, - currency: 'USD', + bidId: 'test-bid-id', + bidderRequestId: 'test-bidder-request-id', + auctionId: 'test-auction-id' + }]; + + const SAMPLE_BID_RESPONSE = { + seatbid: [{ + bid: [{ + impid: 'test-bid-id', + price: 3.5, + w: 300, + h: 250, + crid: 'test-creative-id', + dealid: 'test-deal-id', + adm: 'test-ad-markup', + adomain: ['brand.com'], + ext: { + networkId: 123, + advertiserDomains: ['domain.com'], + } }] - } - ] + }], + cur: 'USD' + }; - for (const obj of invalidData) { - const { mediaType } = obj.body[0] + context('when there is a response, the common response properties', function () { + beforeEach(function () { + bidRequestConfigs = deepClone(SAMPLE_BID_REQUESTS); + bidRequest = spec.buildRequests(bidRequestConfigs, {refererInfo: {}})[0]; + bidResponse = deepClone(SAMPLE_BID_RESPONSE); - it(`Should return an empty array if invalid ${mediaType} response is passed `, () => { - const response = spec.interpretResponse(obj) + bid = spec.interpretResponse({body: bidResponse}, bidRequest)[0]; + }); - expect(response).to.be.an('array') - strictEqual(0, response.length) - }) - } + it('should return a price', function () { + expect(bid.cpm).to.equal(bidResponse.seatbid[0].bid[0].price); + }); - it('Should return an empty array if invalid response is passed', () => { - const response = spec.interpretResponse({ - body: [{ - ttl: 120, - creativeId: '2', - netRevenue: true, - currency: 'USD', - dealId: '1' - }] - }) + it('should return a request id', function () { + expect(bid.requestId).to.equal(bidResponse.seatbid[0].bid[0].impid); + }); + + it('should return width and height for the creative', function () { + expect(bid.width).to.equal(bidResponse.seatbid[0].bid[0].w); + expect(bid.height).to.equal(bidResponse.seatbid[0].bid[0].h); + }); - expect(response).to.be.an('array') - strictEqual(0, response.length) - }) - }) -}) + it('should return a creativeId', function () { + expect(bid.creativeId).to.equal(bidResponse.seatbid[0].bid[0].crid); + }); + + it('should return an ad', function () { + expect(bid.ad).to.equal(bidResponse.seatbid[0].bid[0].adm); + }); + + it('should return a deal id if it exists', function () { + expect(bid.dealId).to.equal(bidResponse.seatbid[0].bid[0].dealid); + }); + + it('should have a time-to-live of 5 minutes', function () { + expect(bid.ttl).to.equal(300); + }); + + it('should always return net revenue', function () { + expect(bid.netRevenue).to.equal(true); + }); + + it('should return a currency', function () { + expect(bid.currency).to.equal(bidResponse.cur); + }); + + it('should return a networkId', function () { + expect(bid.meta.networkId).to.equal(bidResponse.seatbid[0].bid[0].ext.networkId); + }); + + it('should return adomain', function () { + expect(bid.meta.advertiserDomains).to.equal(bidResponse.seatbid[0].bid[0].ext.advertiserDomains); + }); + }); + + context('when the response is a banner', function() { + beforeEach(function () { + bidRequestConfigs = [{ + bidder: 'trafficgate', + params: { + placementId: '98765', + host: 'example' + }, + adUnitCode: 'adunit-code', + mediaTypes: { + banner: { + sizes: [[300, 250], [300, 600]], + }, + }, + bidId: 'test-bid-id', + bidderRequestId: 'test-bidder-request-id', + auctionId: 'test-auction-id' + }]; + + bidRequest = spec.buildRequests(bidRequestConfigs, {refererInfo: {}})[0]; + + bidResponse = { + seatbid: [{ + bid: [{ + impid: 'test-bid-id', + price: 2, + w: 300, + h: 250, + crid: 'test-creative-id', + dealid: 'test-deal-id', + adm: 'test-ad-markup' + }] + }], + cur: 'AUS' + }; + + bid = spec.interpretResponse({body: bidResponse}, bidRequest)[0]; + }); + + it('should return the proper mediaType', function () { + it('should return a creativeId', function () { + expect(bid.mediaType).to.equal(Object.keys(bidRequestConfigs[0].mediaTypes)[0]); + }); + }); + }); + + if (FEATURES.VIDEO) { + context('when the response is a video', function() { + beforeEach(function () { + bidRequestConfigs = [{ + bidder: 'trafficgate', + params: { + placementId: '98765', + host: 'example' + }, + adUnitCode: 'adunit-code', + mediaTypes: { + video: { + playerSize: [[640, 360], [854, 480]], + }, + }, + bidId: 'test-bid-id', + bidderRequestId: 'test-bidder-request-id', + auctionId: 'test-auction-id' + }]; + + bidRequest = spec.buildRequests(bidRequestConfigs, {refererInfo: {}})[0]; + + bidResponse = { + seatbid: [{ + bid: [{ + impid: 'test-bid-id', + price: 2, + w: 854, + h: 480, + crid: 'test-creative-id', + dealid: 'test-deal-id', + adm: 'test-ad-markup', + }] + }], + cur: 'AUS' + }; + }); + + it('should return the proper mediaType', function () { + bid = spec.interpretResponse({body: bidResponse}, bidRequest)[0]; + expect(bid.mediaType).to.equal(Object.keys(bidRequestConfigs[0].mediaTypes)[0]); + }); + + it('should return the proper mediaType', function () { + const winUrl = 'https//my.win.url'; + bidResponse.seatbid[0].bid[0].nurl = winUrl + bid = spec.interpretResponse({body: bidResponse}, bidRequest)[0]; + + expect(bid.vastUrl).to.equal(winUrl); + }); + }); + } + }); +}); diff --git a/test/spec/modules/tripleliftBidAdapter_spec.js b/test/spec/modules/tripleliftBidAdapter_spec.js index 718d030be91..275b9b3bfee 100644 --- a/test/spec/modules/tripleliftBidAdapter_spec.js +++ b/test/spec/modules/tripleliftBidAdapter_spec.js @@ -8,6 +8,7 @@ import * as utils from 'src/utils.js'; const ENDPOINT = 'https://tlx.3lift.com/header/auction?'; const GDPR_CONSENT_STR = 'BOONm0NOONm0NABABAENAa-AAAARh7______b9_3__7_9uz_Kv_K7Vf7nnG072lPVA9LTOQ6gEaY'; +const GPP_CONSENT_STR = 'DBACNYA~CPXxRfAPXxRfAAfKABENB-CgAAAAAAAAAAYgAAAAAAAA~1YNN' describe('triplelift adapter', function () { const adapter = newBidder(tripleliftAdapterSpec); @@ -1039,6 +1040,12 @@ describe('triplelift adapter', function () { const url = request.url; expect(url).to.match(/(\?|&)us_privacy=1YYY/); }); + it('should pass fledge signal when Triplelift is eligible for fledge', function() { + bidderRequest.fledgeEnabled = true; + const request = tripleliftAdapterSpec.buildRequests(bidRequests, bidderRequest); + const url = request.url; + expect(url).to.match(/(\?|&)fledge=true/); + }); it('should return coppa param when COPPA config is set to true', function() { sinon.stub(config, 'getConfig').withArgs('coppa').returns(true); const request = tripleliftAdapterSpec.buildRequests(bidRequests, bidderRequest); @@ -1123,6 +1130,46 @@ describe('triplelift adapter', function () { expect(logErrorSpy.calledOnce).to.equal(true); }); + it('should add ortb2 ext object if global fpd is available', function() { + const ortb2 = { + site: { + domain: 'page.example.com', + cat: ['IAB2'], + sectioncat: ['IAB2-2'], + pagecat: ['IAB2-2'], + page: 'https://page.example.com/here.html', + }, + user: { + yob: 1985, + gender: 'm', + keywords: 'a,b', + data: [ + { + name: 'dataprovider.com', + ext: { segtax: 4 }, + segment: [{ id: '1' }] + } + ], + ext: { + data: { + registered: true, + interests: ['cars'] + } + } + } + }; + + const request = tripleliftAdapterSpec.buildRequests(bidRequests, {...bidderRequest, ortb2}); + const { data: payload } = request; + expect(payload.ext.ortb2).to.exist; + expect(payload.ext.ortb2.site).to.deep.equal({ + domain: 'page.example.com', + cat: ['IAB2'], + sectioncat: ['IAB2-2'], + pagecat: ['IAB2-2'], + page: 'https://page.example.com/here.html', + }); + }); it('should send global config fpd if kvps are available', function() { const sens = null; const category = ['news', 'weather', 'hurricane']; @@ -1501,11 +1548,63 @@ describe('triplelift adapter', function () { expect(result[2].meta.networkId).to.equal('5989'); expect(result[3].meta.networkId).to.equal('5989'); }); + + it('should return fledgeAuctionConfigs if PAAPI response is received', function() { + response.body.paapi = [ + { + imp_id: '0', + auctionConfig: { + seller: 'https://3lift.com', + decisionLogicUrl: 'https://3lift.com/decision_logic.js', + interestGroupBuyers: ['https://some_buyer.com'], + perBuyerSignals: { + 'https://some_buyer.com': { a: 1 } + } + } + }, + { + imp_id: '2', + auctionConfig: { + seller: 'https://3lift.com', + decisionLogicUrl: 'https://3lift.com/decision_logic.js', + interestGroupBuyers: ['https://some_other_buyer.com'], + perBuyerSignals: { + 'https://some_other_buyer.com': { b: 2 } + } + } + } + ]; + + let result = tripleliftAdapterSpec.interpretResponse(response, {bidderRequest}); + + expect(result).to.have.property('bids'); + expect(result).to.have.property('fledgeAuctionConfigs'); + expect(result.fledgeAuctionConfigs.length).to.equal(2); + expect(result.fledgeAuctionConfigs[0].bidId).to.equal('30b31c1838de1e'); + expect(result.fledgeAuctionConfigs[1].bidId).to.equal('73edc0ba8de203'); + expect(result.fledgeAuctionConfigs[0].config).to.deep.equal( + { + 'seller': 'https://3lift.com', + 'decisionLogicUrl': 'https://3lift.com/decision_logic.js', + 'interestGroupBuyers': ['https://some_buyer.com'], + 'perBuyerSignals': { 'https://some_buyer.com': { 'a': 1 } } + } + ); + expect(result.fledgeAuctionConfigs[1].config).to.deep.equal( + { + 'seller': 'https://3lift.com', + 'decisionLogicUrl': 'https://3lift.com/decision_logic.js', + 'interestGroupBuyers': ['https://some_other_buyer.com'], + 'perBuyerSignals': { 'https://some_other_buyer.com': { 'b': 2 } } + } + ); + }); }); describe('getUserSyncs', function() { let expectedIframeSyncUrl = 'https://eb2.3lift.com/sync?gdpr=true&cmp_cs=' + GDPR_CONSENT_STR + '&'; let expectedImageSyncUrl = 'https://eb2.3lift.com/sync?px=1&src=prebid&gdpr=true&cmp_cs=' + GDPR_CONSENT_STR + '&'; + let expectedGppSyncUrl = 'https://eb2.3lift.com/sync?gdpr=true&cmp_cs=' + GDPR_CONSENT_STR + '&gpp=' + GPP_CONSENT_STR + '&gpp_sid=2%2C8' + '&'; it('returns undefined when syncing is not enabled', function() { expect(tripleliftAdapterSpec.getUserSyncs({})).to.equal(undefined); @@ -1543,8 +1642,19 @@ describe('triplelift adapter', function () { let syncOptions = { iframeEnabled: true }; - let result = tripleliftAdapterSpec.getUserSyncs(syncOptions, null, null, '1YYY'); + let result = tripleliftAdapterSpec.getUserSyncs(syncOptions, null, null, '1YYY', null); expect(result[0].url).to.match(/(\?|&)us_privacy=1YYY/); }); + it('returns a user sync pixel with GPP signals when available', function() { + let syncOptions = { + iframeEnabled: true + }; + let gppConsent = { + 'applicableSections': [2, 8], + 'gppString': 'DBACNYA~CPXxRfAPXxRfAAfKABENB-CgAAAAAAAAAAYgAAAAAAAA~1YNN' + } + let result = tripleliftAdapterSpec.getUserSyncs(syncOptions, null, null, null, gppConsent); + expect(result[0].url).to.equal(expectedGppSyncUrl); + }); }); }); diff --git a/test/spec/modules/ttdBidAdapter_spec.js b/test/spec/modules/ttdBidAdapter_spec.js index 56c506dea6b..1fe504ba8e8 100644 --- a/test/spec/modules/ttdBidAdapter_spec.js +++ b/test/spec/modules/ttdBidAdapter_spec.js @@ -262,6 +262,11 @@ describe('ttdBidAdapter', function () { expect(request.data).to.be.not.null; }); + it('sets bidrequest.id to bidderRequestId', function () { + const requestBody = testBuildRequests(baseBannerBidRequests, baseBidderRequest).data; + expect(requestBody.id).to.equal('18084284054531'); + }); + it('sets impression id to ad unit\'s bid id', function () { const requestBody = testBuildRequests(baseBannerBidRequests, baseBidderRequest).data; expect(requestBody.imp[0].id).to.equal('243310435309b5'); diff --git a/test/spec/modules/ucfunnelBidAdapter_spec.js b/test/spec/modules/ucfunnelBidAdapter_spec.js index 992708bd2ea..998e0db6fe8 100644 --- a/test/spec/modules/ucfunnelBidAdapter_spec.js +++ b/test/spec/modules/ucfunnelBidAdapter_spec.js @@ -1,6 +1,7 @@ import { expect } from 'chai'; import { spec } from 'modules/ucfunnelBidAdapter.js'; import {BANNER, VIDEO, NATIVE} from 'src/mediaTypes.js'; +import {deepClone} from '../../../src/utils.js'; const URL = 'https://hb.aralego.com/header'; const BIDDER_CODE = 'ucfunnel'; @@ -29,7 +30,7 @@ const validBannerBidReq = { params: { adid: 'ad-34BBD2AA24B678BBFD4E7B9EE3B872D' }, - sizes: [[300, 250]], + sizes: [[300, 250], [336, 280]], bidId: '263be71e91dd9d', auctionId: '9ad1fa8d-2297-4660-a018-b39945054746', ortb2Imp: { @@ -150,7 +151,10 @@ describe('ucfunnel Adapter', function () { }); }); describe('build request', function () { - const request = spec.buildRequests([validBannerBidReq], bidderRequest); + let request; + before(() => { + request = spec.buildRequests([validBannerBidReq], bidderRequest); + }) it('should create a POST request for every bid', function () { expect(request[0].method).to.equal('GET'); expect(request[0].url).to.equal(spec.ENDPOINT); @@ -176,18 +180,19 @@ describe('ucfunnel Adapter', function () { expect(data.schain).to.equal('1.0,1!exchange1.com,1234,1,bid-request-1,publisher,publisher.com'); }); - it('must parse bid size from a nested array', function () { - const width = 640; - const height = 480; - validBannerBidReq.sizes = [[ width, height ]]; + it('should support multiple size', function () { + const sizes = [[300, 250], [336, 280]]; + const format = '300,250;336,280'; + validBannerBidReq.sizes = sizes; const requests = spec.buildRequests([ validBannerBidReq ], bidderRequest); const data = requests[0].data; - expect(data.w).to.equal(width); - expect(data.h).to.equal(height); + expect(data.w).to.equal(sizes[0][0]); + expect(data.h).to.equal(sizes[0][1]); + expect(data.format).to.equal(format); }); it('should set bidfloor if configured', function() { - let bid = Object.assign({}, validBannerBidReq); + let bid = deepClone(validBannerBidReq); bid.getFloor = function() { return { currency: 'USD', @@ -200,7 +205,7 @@ describe('ucfunnel Adapter', function () { }); it('should set bidfloor if configured', function() { - let bid = Object.assign({}, validBannerBidReq); + let bid = deepClone(validBannerBidReq); bid.params.bidfloor = 2.01; const requests = spec.buildRequests([ bid ], bidderRequest); const data = requests[0].data; @@ -208,7 +213,7 @@ describe('ucfunnel Adapter', function () { }); it('should set bidfloor if configured', function() { - let bid = Object.assign({}, validBannerBidReq); + let bid = deepClone(validBannerBidReq); bid.getFloor = function() { return { currency: 'USD', @@ -224,8 +229,12 @@ describe('ucfunnel Adapter', function () { describe('interpretResponse', function () { describe('should support banner', function () { - const request = spec.buildRequests([ validBannerBidReq ], bidderRequest); - const result = spec.interpretResponse({body: validBannerBidRes}, request[0]); + let request, result; + before(() => { + request = spec.buildRequests([ validBannerBidReq ], bidderRequest); + result = spec.interpretResponse({body: validBannerBidRes}, request[0]); + }); + it('should build bid array for banner', function () { expect(result.length).to.equal(1); }); @@ -243,8 +252,11 @@ describe('ucfunnel Adapter', function () { }); describe('handle banner no ad', function () { - const request = spec.buildRequests([ validBannerBidReq ], bidderRequest); - const result = spec.interpretResponse({body: invalidBannerBidRes}, request[0]); + let request, result; + before(() => { + request = spec.buildRequests([ validBannerBidReq ], bidderRequest); + result = spec.interpretResponse({body: invalidBannerBidRes}, request[0]); + }) it('should build bid array for banner', function () { expect(result.length).to.equal(1); }); @@ -261,8 +273,11 @@ describe('ucfunnel Adapter', function () { }); describe('handle banner cpm under bidfloor', function () { - const request = spec.buildRequests([ validBannerBidReq ], bidderRequest); - const result = spec.interpretResponse({body: invalidBannerBidRes}, request[0]); + let request, result; + before(() => { + request = spec.buildRequests([ validBannerBidReq ], bidderRequest); + result = spec.interpretResponse({body: invalidBannerBidRes}, request[0]); + }) it('should build bid array for banner', function () { expect(result.length).to.equal(1); }); @@ -279,8 +294,11 @@ describe('ucfunnel Adapter', function () { }); describe('should support video', function () { - const request = spec.buildRequests([ validVideoBidReq ], bidderRequest); - const result = spec.interpretResponse({body: validVideoBidRes}, request[0]); + let request, result; + before(() => { + request = spec.buildRequests([ validVideoBidReq ], bidderRequest); + result = spec.interpretResponse({body: validVideoBidRes}, request[0]); + }) it('should build bid array', function () { expect(result.length).to.equal(1); }); @@ -299,8 +317,11 @@ describe('ucfunnel Adapter', function () { }); describe('should support native', function () { - const request = spec.buildRequests([ validNativeBidReq ], bidderRequest); - const result = spec.interpretResponse({body: validNativeBidRes}, request[0]); + let request, result; + before(() => { + request = spec.buildRequests([ validNativeBidReq ], bidderRequest); + result = spec.interpretResponse({body: validNativeBidRes}, request[0]); + }) it('should build bid array', function () { expect(result.length).to.equal(1); }); diff --git a/test/spec/modules/uid2IdSystem_helpers.js b/test/spec/modules/uid2IdSystem_helpers.js index 65d52c1d7c3..e0bef047acb 100644 --- a/test/spec/modules/uid2IdSystem_helpers.js +++ b/test/spec/modules/uid2IdSystem_helpers.js @@ -1,6 +1,6 @@ -import { setConsentConfig } from 'modules/consentManagement.js'; -import { server } from 'test/mocks/xhr.js'; -import {coreStorage, init, setSubmoduleRegistry, requestBidsHook} from 'modules/userId/index.js'; +import {setConsentConfig} from 'modules/consentManagement.js'; +import {server} from 'test/mocks/xhr.js'; +import {coreStorage, requestBidsHook} from 'modules/userId/index.js'; const msIn12Hours = 60 * 60 * 12 * 1000; const expireCookieDate = 'Thu, 01 Jan 1970 00:00:01 GMT'; @@ -26,16 +26,16 @@ export const runAuction = async () => { } export const apiHelpers = { - makeTokenResponse: (token, shouldRefresh = false, expired = false) => ({ + makeTokenResponse: (token, shouldRefresh = false, expired = false, refreshExpired = false) => ({ advertising_token: token, refresh_token: 'fake-refresh-token', identity_expires: expired ? Date.now() - 1000 : Date.now() + 60 * 60 * 1000, refresh_from: shouldRefresh ? Date.now() - 1000 : Date.now() + 60 * 1000, - refresh_expires: Date.now() + 24 * 60 * 60 * 1000, // 24 hours + refresh_expires: refreshExpired ? Date.now() - 1000 : Date.now() + 24 * 60 * 60 * 1000, // 24 hours refresh_response_key: 'wR5t6HKMfJ2r4J7fEGX9Gw==', // Fake data }), - respondAfterDelay: (delay) => new Promise((resolve) => setTimeout(() => { - server.respond(); + respondAfterDelay: (delay, srv = server) => new Promise((resolve) => setTimeout(() => { + srv.respond(); setTimeout(() => resolve()); }, delay)), } diff --git a/test/spec/modules/uid2IdSystem_spec.js b/test/spec/modules/uid2IdSystem_spec.js index 20a38a292bb..901e0c57e32 100644 --- a/test/spec/modules/uid2IdSystem_spec.js +++ b/test/spec/modules/uid2IdSystem_spec.js @@ -1,17 +1,17 @@ /* eslint-disable no-console */ -import {coreStorage, init, setSubmoduleRegistry, requestBidsHook} from 'modules/userId/index.js'; +import {coreStorage, init, setSubmoduleRegistry} from 'modules/userId/index.js'; import {config} from 'src/config.js'; import * as utils from 'src/utils.js'; import { uid2IdSubmodule } from 'modules/uid2IdSystem.js'; import 'src/prebid.js'; import 'modules/consentManagement.js'; import { getGlobal } from 'src/prebidGlobal.js'; -import { server } from 'test/mocks/xhr.js'; import { configureTimerInterceptors } from 'test/mocks/timers.js'; import { cookieHelpers, runAuction, apiHelpers, setGdprApplies } from './uid2IdSystem_helpers.js'; import {hook} from 'src/hook.js'; import {uninstall as uninstallGdprEnforcement} from 'modules/gdprEnforcement.js'; +import {server} from 'test/mocks/xhr'; let expect = require('chai').expect; @@ -24,16 +24,22 @@ const auctionDelayMs = 10; const initialToken = `initial-advertising-token`; const legacyToken = 'legacy-advertising-token'; const refreshedToken = 'refreshed-advertising-token'; +const clientSideGeneratedToken = 'client-side-generated-advertising-token'; const legacyConfigParams = {storage: null}; const serverCookieConfigParams = { uid2ServerCookie: publisherCookieName }; const newServerCookieConfigParams = { uid2Cookie: publisherCookieName }; +const cstgConfigParams = { serverPublicKey: 'UID2-X-L-24B8a/eLYBmRkXA9yPgRZt+ouKbXewG2OPs23+ov3JC8mtYJBCx6AxGwJ4MlwUcguebhdDp2CvzsCgS9ogwwGA==', subscriptionId: 'subscription-id' } const makeUid2IdentityContainer = (token) => ({uid2: {id: token}}); let useLocalStorage = false; const makePrebidConfig = (params = null, extraSettings = {}, debug = false) => ({ userSync: { auctionDelay: auctionDelayMs, userIds: [{name: 'uid2', params: {storage: useLocalStorage ? 'localStorage' : 'cookie', ...params}}] }, debug, ...extraSettings }); +const makeOriginalIdentity = (identity, salt = 1) => ({ + identity: utils.cyrb53Hash(identity, salt), + salt +}) const getFromAppropriateStorage = () => { if (useLocalStorage) return coreStorage.getDataFromLocalStorage(moduleCookieName); @@ -47,31 +53,18 @@ const expectGlobalToHaveToken = (token) => expect(getGlobal().getUserIds()).to.d const expectGlobalToHaveNoUid2 = () => expect(getGlobal().getUserIds()).to.not.haveOwnProperty('uid2'); const expectNoLegacyToken = (bid) => expect(bid.userId).to.not.deep.include(makeUid2IdentityContainer(legacyToken)); const expectModuleStorageEmptyOrMissing = () => expect(getFromAppropriateStorage()).to.be.null; -const expectModuleStorageToContain = (initialIdentity, latestIdentity) => { +const expectModuleStorageToContain = (originalAdvertisingToken, latestAdvertisingToken, originalIdentity) => { const cookie = JSON.parse(getFromAppropriateStorage()); - if (initialIdentity) expect(cookie.originalToken.advertising_token).to.equal(initialIdentity); - if (latestIdentity) expect(cookie.latestToken.advertising_token).to.equal(latestIdentity); + if (originalAdvertisingToken) expect(cookie.originalToken.advertising_token).to.equal(originalAdvertisingToken); + if (latestAdvertisingToken) expect(cookie.latestToken.advertising_token).to.equal(latestAdvertisingToken); + if (originalIdentity) expect(cookie.originalIdentity).to.eql(makeOriginalIdentity(Object.values(originalIdentity)[0], cookie.originalIdentity.salt)); } -const apiUrl = 'https://prod.uidapi.com/v2/token/refresh'; +const apiUrl = 'https://prod.uidapi.com/v2/token' +const refreshApiUrl = `${apiUrl}/refresh`; const headers = { 'Content-Type': 'application/json' }; -const makeSuccessResponseBody = () => btoa(JSON.stringify({ status: 'success', body: { ...apiHelpers.makeTokenResponse(initialToken), advertising_token: refreshedToken } })); -const configureUid2Response = (httpStatus, response) => server.respondWith('POST', apiUrl, (xhr) => xhr.respond(httpStatus, headers, response)); -const configureUid2ApiSuccessResponse = () => configureUid2Response(200, makeSuccessResponseBody()); -const configureUid2ApiFailResponse = () => configureUid2Response(500, 'Error'); - -// Runs the provided test twice - once with a successful API mock, once with one which returns a server error -const testApiSuccessAndFailure = (act, testDescription, failTestDescription, only = false) => { - const testFn = only ? it.only : it; - testFn(`API responds successfully: ${testDescription}`, async function() { - configureUid2ApiSuccessResponse(); - await act(true); - }); - testFn(`API responds with an error: ${failTestDescription ?? testDescription}`, async function() { - configureUid2ApiFailResponse(); - await act(false); - }); -} +const makeSuccessResponseBody = (responseToken) => btoa(JSON.stringify({ status: 'success', body: { ...apiHelpers.makeTokenResponse(initialToken), advertising_token: responseToken } })); +const cstgApiUrl = `${apiUrl}/client-generate`; const testCookieAndLocalStorage = (description, test, only = false) => { const describeFn = only ? describe.only : describe; @@ -104,10 +97,18 @@ describe(`UID2 module`, function () { // I've confirmed it's available in Firefox since v34 (it seems to be unavailable on BrowserStack in Firefox v106). if (typeof window.crypto.subtle === 'undefined') { restoreSubtleToUndefined = true; - window.crypto.subtle = { importKey: () => {}, decrypt: () => {} }; + window.crypto.subtle = { importKey: () => {}, digest: () => {}, decrypt: () => {}, deriveKey: () => {}, encrypt: () => {}, generateKey: () => {}, exportKey: () => {} }; } suiteSandbox.stub(window.crypto.subtle, 'importKey').callsFake(() => Promise.resolve()); + suiteSandbox.stub(window.crypto.subtle, 'digest').callsFake(() => Promise.resolve('hashed_value')); suiteSandbox.stub(window.crypto.subtle, 'decrypt').callsFake((settings, key, data) => Promise.resolve(new Uint8Array([...settings.iv, ...data]))); + suiteSandbox.stub(window.crypto.subtle, 'deriveKey').callsFake(() => Promise.resolve()); + suiteSandbox.stub(window.crypto.subtle, 'exportKey').callsFake(() => Promise.resolve()); + suiteSandbox.stub(window.crypto.subtle, 'encrypt').callsFake(() => Promise.resolve(new ArrayBuffer())); + suiteSandbox.stub(window.crypto.subtle, 'generateKey').callsFake(() => Promise.resolve({ + privateKey: {}, + publicKey: {} + })); }); after(function () { @@ -116,14 +117,30 @@ describe(`UID2 module`, function () { if (restoreSubtleToUndefined) window.crypto.subtle = undefined; }); + const configureUid2Response = (apiUrl, httpStatus, response) => server.respondWith('POST', apiUrl, (xhr) => xhr.respond(httpStatus, headers, response)); + const configureUid2ApiSuccessResponse = (apiUrl, responseToken) => configureUid2Response(apiUrl, 200, makeSuccessResponseBody(responseToken)); + const configureUid2ApiFailResponse = (apiUrl) => configureUid2Response(apiUrl, 500, 'Error'); + // Runs the provided test twice - once with a successful API mock, once with one which returns a server error + const testApiSuccessAndFailure = (act, apiUrl, testDescription, failTestDescription, only = false, responseToken = refreshedToken) => { + const testFn = only ? it.only : it; + testFn(`API responds successfully: ${testDescription}`, async function() { + configureUid2ApiSuccessResponse(apiUrl, responseToken); + await act(true); + }); + testFn(`API responds with an error: ${failTestDescription ?? testDescription}`, async function() { + configureUid2ApiFailResponse(apiUrl); + await act(false); + }); + } + const getFullTestTitle = (test) => `${test.parent.title ? getFullTestTitle(test.parent) + ' | ' : ''}${test.title}`; + beforeEach(function () { debugOutput(`----------------- START TEST ------------------`); fullTestTitle = getFullTestTitle(this.test.ctx.currentTest); debugOutput(fullTestTitle); testSandbox = sinon.sandbox.create(); testSandbox.stub(utils, 'logWarn'); - init(config); setSubmoduleRegistry([uid2IdSubmodule]); }); @@ -143,7 +160,6 @@ describe(`UID2 module`, function () { } cookieHelpers.clearCookies(moduleCookieName, publisherCookieName); coreStorage.removeDataFromLocalStorage(moduleCookieName); - debugOutput('----------------- END TEST ------------------'); }); @@ -151,13 +167,13 @@ describe(`UID2 module`, function () { it('When no baseUrl is provided in config, the module calls the production endpoint', function() { const uid2Token = apiHelpers.makeTokenResponse(initialToken, true, true); config.setConfig(makePrebidConfig({uid2Token})); - expect(server.requests[0]?.url).to.have.string('https://prod.uidapi.com/'); + expect(server.requests[0]?.url).to.have.string('https://prod.uidapi.com/v2/token/refresh'); }); it('When a baseUrl is provided in config, the module calls the provided endpoint', function() { const uid2Token = apiHelpers.makeTokenResponse(initialToken, true, true); config.setConfig(makePrebidConfig({uid2Token, uid2ApiBase: 'https://operator-integ.uidapi.com'})); - expect(server.requests[0]?.url).to.have.string('https://operator-integ.uidapi.com/'); + expect(server.requests[0]?.url).to.have.string('https://operator-integ.uidapi.com/v2/token/refresh'); }); }); @@ -238,7 +254,7 @@ describe(`UID2 module`, function () { cookieHelpers.setPublisherCookie(publisherCookieName, token); config.setConfig(makePrebidConfig(serverCookieConfigParams, extraConfig)); }, - } + }, ] scenarios.forEach(function(scenario) { @@ -247,16 +263,16 @@ describe(`UID2 module`, function () { describe('When the refresh is available in time', function() { testApiSuccessAndFailure(async function(apiSucceeds) { scenario.setConfig(apiHelpers.makeTokenResponse(initialToken, true, true)); - apiHelpers.respondAfterDelay(auctionDelayMs / 10); + apiHelpers.respondAfterDelay(auctionDelayMs / 10, server); const bid = await runAuction(); if (apiSucceeds) expectToken(bid, refreshedToken); else expectNoIdentity(bid); - }, 'it should be used in the auction', 'the auction should have no uid2'); + }, refreshApiUrl, 'it should be used in the auction', 'the auction should have no uid2'); testApiSuccessAndFailure(async function(apiSucceeds) { scenario.setConfig(apiHelpers.makeTokenResponse(initialToken, true, true)); - apiHelpers.respondAfterDelay(auctionDelayMs / 10); + apiHelpers.respondAfterDelay(auctionDelayMs / 10, server); await runAuction(); if (apiSucceeds) { @@ -264,18 +280,18 @@ describe(`UID2 module`, function () { } else { expectModuleStorageEmptyOrMissing(); } - }, 'the refreshed token should be stored in the module storage', 'the module storage should not be set'); + }, refreshApiUrl, 'the refreshed token should be stored in the module storage', 'the module storage should not be set'); }); describe(`when the response doesn't arrive before the auction timer`, function() { testApiSuccessAndFailure(async function() { scenario.setConfig(apiHelpers.makeTokenResponse(initialToken, true, true)); const bid = await runAuction(); expectNoIdentity(bid); - }, 'it should run the auction'); + }, refreshApiUrl, 'it should run the auction'); testApiSuccessAndFailure(async function(apiSucceeds) { scenario.setConfig(apiHelpers.makeTokenResponse(initialToken, true, true)); - const promise = apiHelpers.respondAfterDelay(auctionDelayMs * 2); + const promise = apiHelpers.respondAfterDelay(auctionDelayMs * 2, server); const bid = await runAuction(); expectNoIdentity(bid); @@ -283,7 +299,7 @@ describe(`UID2 module`, function () { await promise; if (apiSucceeds) expectGlobalToHaveToken(refreshedToken); else expectGlobalToHaveNoUid2(); - }, 'it should update the userId after the auction', 'there should be no global identity'); + }, refreshApiUrl, 'it should update the userId after the auction', 'there should be no global identity'); }) describe('and there is a refreshed token in the module cookie', function() { it('the refreshed value from the cookie is used', async function() { @@ -319,13 +335,13 @@ describe(`UID2 module`, function () { scenario.setConfig(apiHelpers.makeTokenResponse(initialToken, true), {auctionDelay: 0, syncDelay: 1}); }); testApiSuccessAndFailure(async function() { - apiHelpers.respondAfterDelay(10); + apiHelpers.respondAfterDelay(10, server); const bid = await runAuction(); expectToken(bid, initialToken); - }, 'it should not be refreshed before the auction runs'); + }, refreshApiUrl, 'it should not be refreshed before the auction runs'); testApiSuccessAndFailure(async function(success) { - const promise = apiHelpers.respondAfterDelay(1); + const promise = apiHelpers.respondAfterDelay(1, server); await runAuction(); await promise; if (success) { @@ -333,7 +349,7 @@ describe(`UID2 module`, function () { } else { expectModuleStorageToContain(initialToken, initialToken); } - }, 'the refreshed token should be stored in the module cookie after the auction runs', 'the module cookie should only have the original token'); + }, refreshApiUrl, 'the refreshed token should be stored in the module cookie after the auction runs', 'the module cookie should only have the original token'); it('it should use the current token in the auction', async function() { const bid = await runAuction(); @@ -342,4 +358,273 @@ describe(`UID2 module`, function () { }); }); }); + + if (FEATURES.UID2_CSTG) { + describe('When CSTG is enabled provided', function () { + const scenarios = [ + { + name: 'email provided in config', + identity: { email: 'test@example.com' }, + setConfig: function (extraConfig) { config.setConfig(makePrebidConfig({ ...cstgConfigParams, ...this.identity }, extraConfig)) }, + setInvalidConfig: (extraConfig) => config.setConfig(makePrebidConfig({ ...cstgConfigParams, email: 'test . test@gmail.com' }, extraConfig)) + }, + { + name: 'phone provided in config', + identity: { phone: '+12345678910' }, + setConfig: function (extraConfig) { config.setConfig(makePrebidConfig({ ...cstgConfigParams, ...this.identity }, extraConfig)) }, + setInvalidConfig: (extraConfig) => config.setConfig(makePrebidConfig({ ...cstgConfigParams, phone: 'test123' }, extraConfig)) + }, + { + name: 'email hash provided in config', + identity: { email_hash: 'lz3+Rj7IV4X1+Vr1ujkG7tstkxwk5pgkqJ6mXbpOgTs=' }, + setConfig: function (extraConfig) { config.setConfig(makePrebidConfig({ ...cstgConfigParams, emailHash: this.identity.email_hash }, extraConfig)) }, + setInvalidConfig: (extraConfig) => config.setConfig(makePrebidConfig({ ...cstgConfigParams, emailHash: 'test@example.com' }, extraConfig)) + }, + { + name: 'phone hash provided in config', + identity: { phone_hash: 'kVJ+4ilhrqm3HZDDnCQy4niZknvCoM4MkoVzZrQSdJw=' }, + setConfig: function (extraConfig) { config.setConfig(makePrebidConfig({ ...cstgConfigParams, phoneHash: this.identity.phone_hash }, extraConfig)) }, + setInvalidConfig: (extraConfig) => config.setConfig(makePrebidConfig({ ...cstgConfigParams, phoneHash: '614332222111' }, extraConfig)) + }, + ] + scenarios.forEach(function(scenario) { + describe(`And ${scenario.name}`, function() { + describe(`When invalid identity is provided`, function() { + it('the auction should have no uid2', async function () { + scenario.setInvalidConfig() + const bid = await runAuction(); + expectNoIdentity(bid); + expectGlobalToHaveNoUid2(); + expectModuleStorageEmptyOrMissing(); + }) + }); + + describe('When valid identity is provided, and the auction is set to run immediately', function() { + it('it should ignores token provided in config, and the auction should have no uid2', async function() { + scenario.setConfig({ uid2Token: apiHelpers.makeTokenResponse(initialToken), auctionDelay: 0, syncDelay: 1 }); + const bid = await runAuction(); + expectNoIdentity(bid); + expectGlobalToHaveNoUid2(); + expectModuleStorageEmptyOrMissing(); + }) + + it('it should ignores token provided in server-set cookie', async function() { + cookieHelpers.setPublisherCookie(publisherCookieName, initialToken); + scenario.setConfig({ ...newServerCookieConfigParams, auctionDelay: 0, syncDelay: 1 }) + const bid = await runAuction(); + expectNoIdentity(bid); + expectGlobalToHaveNoUid2(); + expectModuleStorageEmptyOrMissing(); + }) + + describe('When the token generated in time', function() { + testApiSuccessAndFailure(async function(apiSucceeds) { + scenario.setConfig(); + apiHelpers.respondAfterDelay(auctionDelayMs / 10, server); + const bid = await runAuction(); + + if (apiSucceeds) expectToken(bid, clientSideGeneratedToken); + else expectNoIdentity(bid); + }, cstgApiUrl, 'it should be used in the auction', 'the auction should have no uid2', false, clientSideGeneratedToken); + + testApiSuccessAndFailure(async function(apiSucceeds) { + scenario.setConfig(); + apiHelpers.respondAfterDelay(auctionDelayMs / 10, server); + + await runAuction(); + if (apiSucceeds) { + expectModuleStorageToContain(undefined, clientSideGeneratedToken, scenario.identity); + } else { + expectModuleStorageEmptyOrMissing(); + } + }, cstgApiUrl, 'the generated token should be stored in the module storage', 'the module storage should not be set', false, clientSideGeneratedToken); + }); + }); + }); + }); + describe(`when the response doesn't arrive before the auction timer`, function() { + testApiSuccessAndFailure(async function() { + config.setConfig(makePrebidConfig({ ...cstgConfigParams, email: 'test@test.com' })); + const bid = await runAuction(); + expectNoIdentity(bid); + }, cstgApiUrl, 'it should run the auction', undefined, false, clientSideGeneratedToken); + + testApiSuccessAndFailure(async function(apiSucceeds) { + config.setConfig(makePrebidConfig({ ...cstgConfigParams, email: 'test@test.com' })); + const promise = apiHelpers.respondAfterDelay(auctionDelayMs * 2, server); + + const bid = await runAuction(); + expectNoIdentity(bid); + expectGlobalToHaveNoUid2(); + await promise; + if (apiSucceeds) expectGlobalToHaveToken(clientSideGeneratedToken); + else expectGlobalToHaveNoUid2(); + }, cstgApiUrl, 'it should update the userId after the auction', 'there should be no global identity', false, clientSideGeneratedToken); + }) + + describe('when there is a token in the module cookie', function() { + describe('when originalIdentity matches', function() { + describe('When the storedToken is valid', function() { + it('it should use the stored token in the auction', async function() { + const refreshedIdentity = apiHelpers.makeTokenResponse(refreshedToken); + const moduleCookie = {originalIdentity: makeOriginalIdentity('test@test.com'), latestToken: refreshedIdentity}; + coreStorage.setCookie(moduleCookieName, JSON.stringify(moduleCookie), cookieHelpers.getFutureCookieExpiry()); + config.setConfig(makePrebidConfig({ ...cstgConfigParams, email: 'test@test.com', auctionDelay: 0, syncDelay: 1 })); + const bid = await runAuction(); + expectToken(bid, refreshedToken); + }); + }) + + describe('When the storedToken is expired and can be refreshed ', function() { + testApiSuccessAndFailure(async function(apiSucceeds) { + const refreshedIdentity = apiHelpers.makeTokenResponse(refreshedToken, true, true); + const moduleCookie = {originalIdentity: makeOriginalIdentity('test@test.com'), latestToken: refreshedIdentity}; + coreStorage.setCookie(moduleCookieName, JSON.stringify(moduleCookie), cookieHelpers.getFutureCookieExpiry()); + config.setConfig(makePrebidConfig({ ...cstgConfigParams, email: 'test@test.com' })); + apiHelpers.respondAfterDelay(auctionDelayMs / 10, server); + + const bid = await runAuction(); + + if (apiSucceeds) expectToken(bid, refreshedToken); + else expectNoIdentity(bid); + }, refreshApiUrl, 'it should use refreshed token in the auction', 'the auction should have no uid2'); + }) + + describe('When the storedToken is expired for refresh', function() { + testApiSuccessAndFailure(async function(apiSucceeds) { + const refreshedIdentity = apiHelpers.makeTokenResponse(refreshedToken, true, true, true); + const moduleCookie = {originalIdentity: makeOriginalIdentity('test@test.com'), latestToken: refreshedIdentity}; + coreStorage.setCookie(moduleCookieName, JSON.stringify(moduleCookie), cookieHelpers.getFutureCookieExpiry()); + config.setConfig(makePrebidConfig({ ...cstgConfigParams, email: 'test@test.com' })); + apiHelpers.respondAfterDelay(auctionDelayMs / 10, server); + + const bid = await runAuction(); + + if (apiSucceeds) expectToken(bid, clientSideGeneratedToken); + else expectNoIdentity(bid); + }, cstgApiUrl, 'it should use generated token in the auction', 'the auction should have no uid2', false, clientSideGeneratedToken); + }) + }) + + it('when originalIdentity not match, the auction should has no uid2', async function() { + const refreshedIdentity = apiHelpers.makeTokenResponse(refreshedToken); + const moduleCookie = {originalIdentity: makeOriginalIdentity('123@test.com'), latestToken: refreshedIdentity}; + coreStorage.setCookie(moduleCookieName, JSON.stringify(moduleCookie), cookieHelpers.getFutureCookieExpiry()); + config.setConfig(makePrebidConfig({ ...cstgConfigParams, email: 'test@test.com' })); + const bid = await runAuction(); + expectNoIdentity(bid); + }); + }) + }); + describe('When invalid CSTG configuration is provided', function () { + const invalidConfigs = [ + { + name: 'CSTG option is not a object', + cstgOptions: '' + }, + { + name: 'CSTG option is null', + cstgOptions: '' + }, + { + name: 'serverPublicKey is not a string', + cstgOptions: { subscriptionId: cstgConfigParams.subscriptionId, serverPublicKey: {} } + }, + { + name: 'serverPublicKey not match regular expression', + cstgOptions: { subscriptionId: cstgConfigParams.subscriptionId, serverPublicKey: 'serverPublicKey' } + }, + { + name: 'subscriptionId is not a string', + cstgOptions: { subscriptionId: {}, serverPublicKey: cstgConfigParams.serverPublicKey } + }, + { + name: 'subscriptionId is empty', + cstgOptions: { subscriptionId: '', serverPublicKey: cstgConfigParams.serverPublicKey } + }, + ] + invalidConfigs.forEach(function(scenario) { + describe(`When ${scenario.name}`, function() { + it('should not generate token using identity', async () => { + config.setConfig(makePrebidConfig({ ...scenario.cstgOptions, email: 'test@email.com' })); + const bid = await runAuction(); + expectNoIdentity(bid); + expectGlobalToHaveNoUid2(); + expectModuleStorageEmptyOrMissing(); + }); + }); + }); + }); + describe('When email is provided in different format', function () { + const testCases = [ + { originalEmail: 'TEst.TEST@Test.com ', normalizedEmail: 'test.test@test.com' }, + { originalEmail: 'test+test@test.com', normalizedEmail: 'test+test@test.com' }, + { originalEmail: ' testtest@test.com ', normalizedEmail: 'testtest@test.com' }, + { originalEmail: 'TEst.TEst+123@GMail.Com', normalizedEmail: 'testtest@gmail.com' } + ]; + testCases.forEach((testCase) => { + describe('it should normalize the email and generate token on normalized email', async () => { + testApiSuccessAndFailure(async function(apiSucceeds) { + config.setConfig(makePrebidConfig({ ...cstgConfigParams, email: testCase.originalEmail })); + apiHelpers.respondAfterDelay(auctionDelayMs / 10, server); + + await runAuction(); + if (apiSucceeds) { + expectModuleStorageToContain(undefined, clientSideGeneratedToken, { email: testCase.normalizedEmail }); + } else { + expectModuleStorageEmptyOrMissing(); + } + }, cstgApiUrl, 'the generated token should be stored in the module storage', 'the module storage should not be set', false, clientSideGeneratedToken); + }); + }); + }); + } + + describe('When neither token nor CSTG config provided', function () { + describe('when there is a non-cstg-derived token in the module cookie', function () { + it('the auction use stored token if it is valid', async function () { + const originalIdentity = apiHelpers.makeTokenResponse(initialToken); + const moduleCookie = {originalToken: originalIdentity, latestToken: originalIdentity}; + coreStorage.setCookie(moduleCookieName, JSON.stringify(moduleCookie), cookieHelpers.getFutureCookieExpiry()); + config.setConfig(makePrebidConfig({})); + const bid = await runAuction(); + expectToken(bid, initialToken); + }) + + it('the auction should has no uid2 if stored token is invalid', async function () { + const originalIdentity = apiHelpers.makeTokenResponse(initialToken, true, true, true); + const moduleCookie = {originalToken: originalIdentity, latestToken: originalIdentity}; + coreStorage.setCookie(moduleCookieName, JSON.stringify(moduleCookie), cookieHelpers.getFutureCookieExpiry()); + config.setConfig(makePrebidConfig({})); + const bid = await runAuction(); + expectNoIdentity(bid); + }) + }) + + describe('when there is a cstg-derived token in the module cookie', function () { + it('the auction use stored token if it is valid', async function () { + const originalIdentity = apiHelpers.makeTokenResponse(initialToken); + const moduleCookie = {originalIdentity: makeOriginalIdentity('123@test.com'), originalToken: originalIdentity, latestToken: originalIdentity}; + coreStorage.setCookie(moduleCookieName, JSON.stringify(moduleCookie), cookieHelpers.getFutureCookieExpiry()); + config.setConfig(makePrebidConfig({})); + const bid = await runAuction(); + expectToken(bid, initialToken); + }) + + it('the auction should has no uid2 if stored token is invalid', async function () { + const originalIdentity = apiHelpers.makeTokenResponse(initialToken, true, true, true); + const moduleCookie = {originalIdentity: makeOriginalIdentity('123@test.com'), originalToken: originalIdentity, latestToken: originalIdentity}; + coreStorage.setCookie(moduleCookieName, JSON.stringify(moduleCookie), cookieHelpers.getFutureCookieExpiry()); + config.setConfig(makePrebidConfig({})); + const bid = await runAuction(); + expectNoIdentity(bid); + }) + }) + + it('the auction should has no uid2', async function () { + config.setConfig(makePrebidConfig({})); + const bid = await runAuction(); + expectNoIdentity(bid); + }) + }) }); diff --git a/test/spec/modules/underdogmediaBidAdapter_spec.js b/test/spec/modules/underdogmediaBidAdapter_spec.js index 2d7c1f11178..c0e2e8dddce 100644 --- a/test/spec/modules/underdogmediaBidAdapter_spec.js +++ b/test/spec/modules/underdogmediaBidAdapter_spec.js @@ -5,6 +5,7 @@ import { spec, resetUserSync } from 'modules/underdogmediaBidAdapter.js'; +import { config } from '../../../src/config'; describe('UnderdogMedia adapter', function () { let bidRequests; @@ -763,6 +764,20 @@ describe('UnderdogMedia adapter', function () { expect(request.data.ref).to.equal(undefined); }); + it('should have pbTimeout to be 3001 if bidder timeout does not exists', function () { + config.setConfig({ bidderTimeout: '' }) + const request = spec.buildRequests(bidRequests, bidderRequest); + + expect(request.data.pbTimeout).to.equal(3001) + }) + + it('should have pbTimeout to be a numerical value if bidder timeout is in a string', function () { + config.setConfig({ bidderTimeout: '1000' }) + const request = spec.buildRequests(bidRequests, bidderRequest); + + expect(request.data.pbTimeout).to.equal(1000) + }) + it('should have pubcid if it exists', function () { let bidRequests = [{ adUnitCode: 'div-gpt-ad-1460505748561-0', diff --git a/test/spec/modules/undertoneBidAdapter_spec.js b/test/spec/modules/undertoneBidAdapter_spec.js index 7f9c2e7b3d5..5cf53c661a9 100644 --- a/test/spec/modules/undertoneBidAdapter_spec.js +++ b/test/spec/modules/undertoneBidAdapter_spec.js @@ -39,12 +39,19 @@ const videoBidReq = [{ maxDuration: 30 } }, - mediaTypes: {video: { - context: 'outstream', - playerSize: [640, 480], - placement: 1, - plcmt: 1 - }}, + ortb2Imp: { + ext: { + gpid: '/1111/gpid#728x90', + } + }, + mediaTypes: { + video: { + context: 'outstream', + playerSize: [640, 480], + placement: 1, + plcmt: 1 + } + }, sizes: [[300, 250], [300, 600]], bidId: '263be71e91dd9d', auctionId: '9ad1fa8d-2297-4660-a018-b39945054746' @@ -56,10 +63,19 @@ const videoBidReq = [{ placementId: '10433395', publisherId: 12345 }, - mediaTypes: {video: { - context: 'outstream', - playerSize: [640, 480] - }}, + ortb2Imp: { + ext: { + data: { + pbadslot: '/1111/pbadslot#728x90' + } + } + }, + mediaTypes: { + video: { + context: 'outstream', + playerSize: [640, 480] + } + }, sizes: [[300, 250], [300, 600]], bidId: '263be71e91dd9d', auctionId: '9ad1fa8d-2297-4660-a018-b39945054746' @@ -463,12 +479,14 @@ describe('Undertone Adapter', () => { expect(bidVideo.video.skippable).to.equal(true); expect(bidVideo.video.placement).to.equal(1); expect(bidVideo.video.plcmt).to.equal(1); + expect(bidVideo.gpid).to.equal('/1111/gpid#728x90'); expect(bidVideo2.video.skippable).to.equal(null); expect(bidVideo2.video.maxDuration).to.equal(null); expect(bidVideo2.video.playbackMethod).to.equal(null); expect(bidVideo2.video.placement).to.equal(null); expect(bidVideo2.video.plcmt).to.equal(null); + expect(bidVideo2.gpid).to.equal('/1111/pbadslot#728x90'); }); it('should send all userIds data to server', function () { const request = spec.buildRequests(bidReqUserIds, bidderReq); diff --git a/test/spec/modules/unicornBidAdapter_spec.js b/test/spec/modules/unicornBidAdapter_spec.js index 0abb09bfb78..bd9175dac1e 100644 --- a/test/spec/modules/unicornBidAdapter_spec.js +++ b/test/spec/modules/unicornBidAdapter_spec.js @@ -1,4 +1,5 @@ import {assert, expect} from 'chai'; +import * as utils from 'src/utils.js'; import {spec} from 'modules/unicornBidAdapter.js'; import * as _ from 'lodash'; @@ -496,6 +497,17 @@ describe('unicornBidAdapterTest', () => { }); describe('buildBidRequest', () => { + const removeUntestableAttrs = data => { + delete data['device']; + delete data['site']['domain']; + delete data['site']['page']; + delete data['id']; + data['imp'].forEach(imp => { + delete imp['id']; + }) + delete data['user']['id']; + return data; + }; before(function () { $$PREBID_GLOBAL$$.bidderSettings = { unicorn: { @@ -508,17 +520,6 @@ describe('unicornBidAdapterTest', () => { }); it('buildBidRequest', () => { const req = spec.buildRequests(validBidRequests, bidderRequest); - const removeUntestableAttrs = data => { - delete data['device']; - delete data['site']['domain']; - delete data['site']['page']; - delete data['id']; - data['imp'].forEach(imp => { - delete imp['id']; - }) - delete data['user']['id']; - return data; - }; const uid = JSON.parse(req.data)['user']['id']; const reqData = removeUntestableAttrs(JSON.parse(req.data)); const openRTBRequestData = removeUntestableAttrs(openRTBRequest); @@ -527,6 +528,28 @@ describe('unicornBidAdapterTest', () => { const uid2 = JSON.parse(req2.data)['user']['id']; assert.deepStrictEqual(uid, uid2); }); + it('test if contains ID5', () => { + let _validBidRequests = utils.deepClone(validBidRequests); + _validBidRequests[0].userId = { + id5id: { + uid: 'id5_XXXXX' + } + } + const req = spec.buildRequests(_validBidRequests, bidderRequest); + const reqData = removeUntestableAttrs(JSON.parse(req.data)); + const openRTBRequestData = removeUntestableAttrs(utils.deepClone(openRTBRequest)); + openRTBRequestData.user.eids = [ + { + source: 'id5-sync.com', + uids: [ + { + id: 'id5_XXXXX' + } + ] + } + ] + assert.deepStrictEqual(reqData, openRTBRequestData); + }) }); describe('interpretResponse', () => { diff --git a/test/spec/modules/unrulyBidAdapter_spec.js b/test/spec/modules/unrulyBidAdapter_spec.js index 6d1d8f9949f..abf1a54787d 100644 --- a/test/spec/modules/unrulyBidAdapter_spec.js +++ b/test/spec/modules/unrulyBidAdapter_spec.js @@ -42,9 +42,40 @@ describe('UnrulyAdapter', function () { } } - const createExchangeResponse = (...bids) => ({ - body: {bids} - }); + function createOutStreamExchangeAuctionConfig() { + return { + 'seller': 'https://nexxen.tech', + 'decisionLogicURL': 'https://nexxen.tech/padecisionlogic', + 'interestGroupBuyers': 'https://mydsp.com', + 'perBuyerSignals': { + 'https://mydsp.com': { + 'floor': 'bouttreefiddy' + } + } + } + }; + + function createExchangeResponse (bidList, auctionConfigs = null) { + let bids = []; + if (Array.isArray(bidList)) { + bids = bidList; + } else if (bidList) { + bids.push(bidList); + } + + if (!auctionConfigs) { + return { + 'body': {bids} + }; + } + + return { + 'body': { + bids, + auctionConfigs + } + } + }; const inStreamServerResponse = { 'requestId': '262594d5d1f8104', @@ -486,7 +517,8 @@ describe('UnrulyAdapter', function () { 'bidderRequestId': '12e00d17dff07b' } ], - 'invalidBidsCount': 0 + 'invalidBidsCount': 0, + 'prebidVersion': '$prebid.version$' } }; @@ -560,7 +592,8 @@ describe('UnrulyAdapter', function () { 'bidderRequestId': '12e00d17dff07b', } ], - 'invalidBidsCount': 0 + 'invalidBidsCount': 0, + 'prebidVersion': '$prebid.version$' } }; @@ -651,13 +684,235 @@ describe('UnrulyAdapter', function () { 'bidderRequestId': '12e00d17dff07b', } ], - 'invalidBidsCount': 0 + 'invalidBidsCount': 0, + 'prebidVersion': '$prebid.version$' } }; let result = adapter.buildRequests(mockBidRequests.bids, mockBidRequests); expect(result[0].data).to.deep.equal(expectedResult); }); + describe('Protected Audience Support', function() { + it('should return an array with 2 items and enabled protected audience', function () { + mockBidRequests = { + 'bidderCode': 'unruly', + 'fledgeEnabled': true, + 'bids': [ + { + 'bidder': 'unruly', + 'params': { + 'siteId': 233261, + }, + 'mediaTypes': { + 'video': { + 'context': 'outstream', + 'mimes': [ + 'video/mp4' + ], + 'playerSize': [ + [ + 640, + 480 + ] + ] + } + }, + 'adUnitCode': 'video2', + 'transactionId': 'a89619e3-137d-4cc5-9ed4-58a0b2a0bbc2', + 'sizes': [ + [ + 640, + 480 + ] + ], + 'bidId': '27a3ee1626a5c7', + 'bidderRequestId': '12e00d17dff07b', + 'ortb2Imp': { + 'ext': { + 'ae': 1 + } + } + }, + { + 'bidder': 'unruly', + 'params': { + 'siteId': 2234554, + }, + 'mediaTypes': { + 'video': { + 'context': 'outstream', + 'mimes': [ + 'video/mp4' + ], + 'playerSize': [ + [ + 640, + 480 + ] + ] + } + }, + 'adUnitCode': 'video2', + 'transactionId': 'a89619e3-137d-4cc5-9ed4-58a0b2a0bbc2', + 'sizes': [ + [ + 640, + 480 + ] + ], + 'bidId': '27a3ee1626a5c7', + 'bidderRequestId': '12e00d17dff07b', + 'ortb2Imp': { + 'ext': { + 'ae': 1 + } + } + } + ] + }; + + let result = adapter.buildRequests(mockBidRequests.bids, mockBidRequests); + expect(typeof result).to.equal('object'); + expect(result.length).to.equal(2); + expect(result[0].data.bidderRequest.bids.length).to.equal(1); + expect(result[1].data.bidderRequest.bids.length).to.equal(1); + expect(result[0].data.bidderRequest.bids[0].ortb2Imp.ext.ae).to.equal(1); + expect(result[1].data.bidderRequest.bids[0].ortb2Imp.ext.ae).to.equal(1); + }); + it('should return an array with 2 items and enabled protected audience on only one unit', function () { + mockBidRequests = { + 'bidderCode': 'unruly', + 'fledgeEnabled': true, + 'bids': [ + { + 'bidder': 'unruly', + 'params': { + 'siteId': 233261, + }, + 'mediaTypes': { + 'video': { + 'context': 'outstream', + 'mimes': [ + 'video/mp4' + ], + 'playerSize': [ + [ + 640, + 480 + ] + ] + } + }, + 'adUnitCode': 'video2', + 'transactionId': 'a89619e3-137d-4cc5-9ed4-58a0b2a0bbc2', + 'sizes': [ + [ + 640, + 480 + ] + ], + 'bidId': '27a3ee1626a5c7', + 'bidderRequestId': '12e00d17dff07b', + 'ortb2Imp': { + 'ext': { + 'ae': 1 + } + } + }, + { + 'bidder': 'unruly', + 'params': { + 'siteId': 2234554, + }, + 'mediaTypes': { + 'video': { + 'context': 'outstream', + 'mimes': [ + 'video/mp4' + ], + 'playerSize': [ + [ + 640, + 480 + ] + ] + } + }, + 'adUnitCode': 'video2', + 'transactionId': 'a89619e3-137d-4cc5-9ed4-58a0b2a0bbc2', + 'sizes': [ + [ + 640, + 480 + ] + ], + 'bidId': '27a3ee1626a5c7', + 'bidderRequestId': '12e00d17dff07b', + 'ortb2Imp': { + 'ext': {} + } + } + ] + }; + + let result = adapter.buildRequests(mockBidRequests.bids, mockBidRequests); + expect(typeof result).to.equal('object'); + expect(result.length).to.equal(2); + expect(result[0].data.bidderRequest.bids.length).to.equal(1); + expect(result[1].data.bidderRequest.bids.length).to.equal(1); + expect(result[0].data.bidderRequest.bids[0].ortb2Imp.ext.ae).to.equal(1); + expect(result[1].data.bidderRequest.bids[0].ortb2Imp.ext.ae).to.be.undefined; + }); + it('disables configured protected audience when fledge is not availble', function () { + mockBidRequests = { + 'bidderCode': 'unruly', + 'fledgeEnabled': false, + 'bids': [ + { + 'bidder': 'unruly', + 'params': { + 'siteId': 233261, + }, + 'mediaTypes': { + 'video': { + 'context': 'outstream', + 'mimes': [ + 'video/mp4' + ], + 'playerSize': [ + [ + 640, + 480 + ] + ] + } + }, + 'adUnitCode': 'video2', + 'transactionId': 'a89619e3-137d-4cc5-9ed4-58a0b2a0bbc2', + 'sizes': [ + [ + 640, + 480 + ] + ], + 'bidId': '27a3ee1626a5c7', + 'bidderRequestId': '12e00d17dff07b', + 'ortb2Imp': { + 'ext': { + 'ae': 1 + } + } + } + ] + }; + + let result = adapter.buildRequests(mockBidRequests.bids, mockBidRequests); + expect(typeof result).to.equal('object'); + expect(result.length).to.equal(1); + expect(result[0].data.bidderRequest.bids.length).to.equal(1); + expect(result[0].data.bidderRequest.bids[0].ortb2Imp.ext.ae).to.be.undefined; + }); + }); }); describe('interpretResponse', function () { @@ -705,7 +960,167 @@ describe('UnrulyAdapter', function () { renderer: fakeRenderer, mediaType: 'video' } - ]) + ]); + }); + + it('should return object with an array of bids and an array of auction configs when it receives a successful response from server', function () { + let bidId = '27a3ee1626a5c7' + const mockExchangeBid = createOutStreamExchangeBid({adUnitCode: 'video1', requestId: 'mockBidId'}); + const mockExchangeAuctionConfig = {}; + mockExchangeAuctionConfig[bidId] = createOutStreamExchangeAuctionConfig(); + const mockServerResponse = createExchangeResponse(mockExchangeBid, mockExchangeAuctionConfig); + const originalRequest = { + 'data': { + 'bidderRequest': { + 'bids': [ + { + 'bidder': 'unruly', + 'params': { + 'siteId': 233261, + }, + 'mediaTypes': { + 'banner': { + 'sizes': [ + [ + 640, + 480 + ], + [ + 640, + 480 + ], + [ + 300, + 250 + ], + [ + 300, + 250 + ] + ] + } + }, + 'adUnitCode': 'video2', + 'transactionId': 'a89619e3-137d-4cc5-9ed4-58a0b2a0bbc2', + 'bidId': bidId, + 'bidderRequestId': '12e00d17dff07b', + } + ] + } + } + }; + + expect(adapter.interpretResponse(mockServerResponse, originalRequest)).to.deep.equal({ + 'bids': [ + { + 'ext': { + 'statusCode': 1, + 'renderer': { + 'id': 'unruly_inarticle', + 'config': { + 'siteId': 123456, + 'targetingUUID': 'xxx-yyy-zzz' + }, + 'url': 'https://video.unrulymedia.com/native/prebid-loader.js' + }, + 'adUnitCode': 'video1' + }, + requestId: 'mockBidId', + bidderCode: 'unruly', + cpm: 20, + width: 323, + height: 323, + vastUrl: 'https://targeting.unrulymedia.com/in_article?uuid=74544e00-d43b-4f3a-a799-69d22ce979ce&supported_mime_type=application/javascript&supported_mime_type=video/mp4&tj=%7B%22site%22%3A%7B%22lang%22%3A%22en-GB%22%2C%22ref%22%3A%22%22%2C%22page%22%3A%22https%3A%2F%2Fdemo.unrulymedia.com%2FinArticle%2Finarticle_nypost_upbeat%2Ftravel_magazines.html%22%2C%22domain%22%3A%22demo.unrulymedia.com%22%7D%2C%22user%22%3A%7B%22profile%22%3A%7B%22quantcast%22%3A%7B%22segments%22%3A%5B%7B%22id%22%3A%22D%22%7D%2C%7B%22id%22%3A%22T%22%7D%5D%7D%7D%7D%7D&video_width=618&video_height=347', + netRevenue: true, + creativeId: 'mockBidId', + ttl: 360, + 'meta': { + 'mediaType': 'video', + 'videoContext': 'outstream' + }, + currency: 'USD', + renderer: fakeRenderer, + mediaType: 'video' + } + ], + 'fledgeAuctionConfigs': [{ + 'bidId': bidId, + 'config': { + 'seller': 'https://nexxen.tech', + 'decisionLogicURL': 'https://nexxen.tech/padecisionlogic', + 'interestGroupBuyers': 'https://mydsp.com', + 'perBuyerSignals': { + 'https://mydsp.com': { + 'floor': 'bouttreefiddy' + } + } + } + }] + }); + }); + + it('should return object with an array of auction configs when it receives a successful response from server without bids', function () { + let bidId = '27a3ee1626a5c7'; + const mockExchangeAuctionConfig = {}; + mockExchangeAuctionConfig[bidId] = createOutStreamExchangeAuctionConfig(); + const mockServerResponse = createExchangeResponse(null, mockExchangeAuctionConfig); + const originalRequest = { + 'data': { + 'bidderRequest': { + 'bids': [ + { + 'bidder': 'unruly', + 'params': { + 'siteId': 233261, + }, + 'mediaTypes': { + 'banner': { + 'sizes': [ + [ + 640, + 480 + ], + [ + 640, + 480 + ], + [ + 300, + 250 + ], + [ + 300, + 250 + ] + ] + } + }, + 'adUnitCode': 'video2', + 'transactionId': 'a89619e3-137d-4cc5-9ed4-58a0b2a0bbc2', + 'bidId': bidId, + 'bidderRequestId': '12e00d17dff07b' + } + ] + } + } + }; + + expect(adapter.interpretResponse(mockServerResponse, originalRequest)).to.deep.equal({ + 'bids': [], + 'fledgeAuctionConfigs': [{ + 'bidId': bidId, + 'config': { + 'seller': 'https://nexxen.tech', + 'decisionLogicURL': 'https://nexxen.tech/padecisionlogic', + 'interestGroupBuyers': 'https://mydsp.com', + 'perBuyerSignals': { + 'https://mydsp.com': { + 'floor': 'bouttreefiddy' + } + } + } + }] + }); }); it('should initialize and set the renderer', function () { @@ -875,7 +1290,7 @@ describe('UnrulyAdapter', function () { it('should return correct response for multiple bids', function () { const outStreamServerResponse = createOutStreamExchangeBid({adUnitCode: 'video1', requestId: 'mockBidId'}); - const mockServerResponse = createExchangeResponse(outStreamServerResponse, inStreamServerResponse, bannerServerResponse); + const mockServerResponse = createExchangeResponse([outStreamServerResponse, inStreamServerResponse, bannerServerResponse]); const expectedOutStreamResponse = outStreamServerResponse; expectedOutStreamResponse.mediaType = 'video'; @@ -890,7 +1305,7 @@ describe('UnrulyAdapter', function () { it('should return only valid bids', function () { const {ad, ...bannerServerResponseNoAd} = bannerServerResponse; - const mockServerResponse = createExchangeResponse(bannerServerResponseNoAd, inStreamServerResponse); + const mockServerResponse = createExchangeResponse([bannerServerResponseNoAd, inStreamServerResponse]); const expectedInStreamResponse = inStreamServerResponse; expectedInStreamResponse.mediaType = 'video'; diff --git a/test/spec/modules/userId_spec.js b/test/spec/modules/userId_spec.js index e2d4062399b..18f49f4943e 100644 --- a/test/spec/modules/userId_spec.js +++ b/test/spec/modules/userId_spec.js @@ -2,7 +2,7 @@ import { attachIdSystem, auctionDelay, coreStorage, dep, - findRootDomain, + findRootDomain, getConsentHash, init, PBJS_USER_ID_OPTOUT_NAME, requestBidsHook, @@ -12,7 +12,7 @@ import { setSubmoduleRegistry, syncDelay, } from 'modules/userId/index.js'; -import {createEidsArray} from 'modules/userId/eids.js'; +import {createEidsArray, EID_CONFIG} from 'modules/userId/eids.js'; import {config} from 'src/config.js'; import * as utils from 'src/utils.js'; import {getPrebidInternal} from 'src/utils.js'; @@ -21,7 +21,6 @@ import CONSTANTS from 'src/constants.json'; import {getGlobal} from 'src/prebidGlobal.js'; import {resetConsentData, } from 'modules/consentManagement.js'; import {server} from 'test/mocks/xhr.js'; -import {find} from 'src/polyfill.js'; import {unifiedIdSubmodule} from 'modules/unifiedIdSystem.js'; import {britepoolIdSubmodule} from 'modules/britepoolIdSystem.js'; import {id5IdSubmodule} from 'modules/id5IdSystem.js'; @@ -56,7 +55,7 @@ import {hook} from '../../../src/hook.js'; import {mockGdprConsent} from '../../helpers/consentData.js'; import {getPPID} from '../../../src/adserver.js'; import {uninstall as uninstallGdprEnforcement} from 'modules/gdprEnforcement.js'; -import {GDPR_GVLIDS} from '../../../src/consentHandler.js'; +import {allConsent, GDPR_GVLIDS, gdprDataHandler} from '../../../src/consentHandler.js'; import {MODULE_TYPE_UID} from '../../../src/activities/modules.js'; import {ACTIVITY_ENRICH_EIDS} from '../../../src/activities/activities.js'; import {ACTIVITY_PARAM_COMPONENT_NAME, ACTIVITY_PARAM_COMPONENT_TYPE} from '../../../src/activities/params.js'; @@ -86,6 +85,20 @@ describe('User ID', function () { } } + function createMockIdSubmodule(name, value, aliasName, eids) { + return { + name, + getId() { + return value; + }, + decode(v) { + return v; + }, + aliasName, + eids + } + } + function getAdUnitMock(code = 'adUnit-code') { return { code, @@ -109,12 +122,6 @@ describe('User ID', function () { return cfg; } - function findEid(eids, source) { - return find(eids, (eid) => { - if (eid.source === source) { return true; } - }); - } - let sandbox, consentData, startDelay, callbackDelay; function clearStack() { @@ -372,11 +379,96 @@ describe('User ID', function () { expect(bid).to.not.have.deep.nested.property('userIdAsEids'); }); }); - // setCookie is called once in order to store consentData - expect(coreStorage.setCookie.callCount).to.equal(1); }); }); + describe('createEidsArray', () => { + beforeEach(() => { + init(config); + setSubmoduleRegistry([ + createMockIdSubmodule('mockId1', null, null, + {'mockId1': {source: 'mock1source', atype: 1}}), + createMockIdSubmodule('mockId2v1', null, null, + {'mockId2v1': {source: 'mock2source', atype: 2, getEidExt: () => ({v: 1})}}), + createMockIdSubmodule('mockId2v2', null, null, + {'mockId2v2': {source: 'mock2source', atype: 2, getEidExt: () => ({v: 2})}}), + ]); + }); + + it('should group UIDs by source and ext', () => { + const eids = createEidsArray({ + mockId1: ['mock-1-1', 'mock-1-2'], + mockId2v1: ['mock-2-1', 'mock-2-2'], + mockId2v2: ['mock-2-1', 'mock-2-2'] + }); + expect(eids).to.eql([ + { + source: 'mock1source', + uids: [ + { + id: 'mock-1-1', + atype: 1, + }, + { + id: 'mock-1-2', + atype: 1, + } + ] + }, + { + source: 'mock2source', + ext: { + v: 1 + }, + uids: [ + { + id: 'mock-2-1', + atype: 2, + }, + { + id: 'mock-2-2', + atype: 2, + } + ] + }, + { + source: 'mock2source', + ext: { + v: 2 + }, + uids: [ + { + id: 'mock-2-1', + atype: 2, + }, + { + id: 'mock-2-2', + atype: 2, + } + ] + } + ]) + }); + + it('when merging with pubCommonId, should not alter its eids', () => { + const uid = { + pubProvidedId: [ + { + source: 'mock1Source', + uids: [ + {id: 'uid2'} + ] + } + ], + mockId1: 'uid1', + }; + const eids = createEidsArray(uid); + expect(eids).to.have.length(1); + expect(eids[0].uids.map(u => u.id)).to.have.members(['uid1', 'uid2']); + expect(uid.pubProvidedId[0].uids).to.eql([{id: 'uid2'}]); + }); + }) + it('pbjs.getUserIds', function (done) { init(config); setSubmoduleRegistry([sharedIdSystemSubmodule]); @@ -397,6 +489,139 @@ describe('User ID', function () { }) }); + it('pbjs.getUserIds(Async) should prioritize user ids according to config available to core', () => { + init(config); + + setSubmoduleRegistry([ + createMockIdSubmodule('mockId1Module', {id: {mockId1: 'mockId1_value'}}), + createMockIdSubmodule('mockId2Module', {id: {mockId2: 'mockId2_value', mockId3: 'mockId3_value_from_mockId2Module'}}), + createMockIdSubmodule('mockId3Module', {id: {mockId1: 'mockId1_value_from_mockId3Module', mockId2: 'mockId2_value_from_mockId3Module', mockId3: 'mockId3_value', mockId4: 'mockId4_value_from_mockId3Module'}}), + createMockIdSubmodule('mockId4Module', {id: {mockId4: 'mockId4_value'}}) + ]); + + config.setConfig({ + userSync: { + idPriority: { + mockId1: ['mockId3Module', 'mockId1Module'], + mockId4: ['mockId4Module', 'mockId3Module'] + }, + auctionDelay: 10, // with auctionDelay > 0, no auction is needed to complete init + userIds: [ + { name: 'mockId1Module' }, + { name: 'mockId2Module' }, + { name: 'mockId3Module' }, + { name: 'mockId4Module' } + ] + } + }); + + return getGlobal().getUserIdsAsync().then((uids) => { + expect(uids['mockId1']).to.deep.equal('mockId1_value_from_mockId3Module'); + expect(uids['mockId2']).to.deep.equal('mockId2_value'); + expect(uids['mockId3']).to.deep.equal('mockId3_value_from_mockId2Module'); + expect(uids['mockId4']).to.deep.equal('mockId4_value'); + }); + }); + + it('pbjs.getUserIds(Async) should prioritize user ids according to config available to core when config uses aliases', () => { + init(config); + + setSubmoduleRegistry([ + createMockIdSubmodule('mockId1Module', {id: {mockId1: 'mockId1_value'}}), + createMockIdSubmodule('mockId2Module', {id: {mockId2: 'mockId2_value', mockId3: 'mockId3_value_from_mockId2Module'}}, 'mockId2Module_alias'), + createMockIdSubmodule('mockId3Module', {id: {mockId1: 'mockId1_value_from_mockId3Module', mockId2: 'mockId2_value_from_mockId3Module', mockId3: 'mockId3_value', mockId4: 'mockId4_value_from_mockId3Module'}}, 'mockId3Module_alias'), + createMockIdSubmodule('mockId4Module', {id: {mockId4: 'mockId4_value'}}) + ]); + + config.setConfig({ + userSync: { + idPriority: { + mockId1: ['mockId3Module_alias', 'mockId1Module'], + mockId4: ['mockId4Module', 'mockId3Module'] + }, + auctionDelay: 10, // with auctionDelay > 0, no auction is needed to complete init + userIds: [ + { name: 'mockId1Module' }, + { name: 'mockId2Module' }, + { name: 'mockId3Module' }, + { name: 'mockId4Module' } + ] + } + }); + + return getGlobal().getUserIdsAsync().then((uids) => { + expect(uids['mockId1']).to.deep.equal('mockId1_value_from_mockId3Module'); + expect(uids['mockId2']).to.deep.equal('mockId2_value'); + expect(uids['mockId3']).to.deep.equal('mockId3_value_from_mockId2Module'); + expect(uids['mockId4']).to.deep.equal('mockId4_value'); + }); + }); + + it('pbjs.getUserIds(Async) should prioritize user ids according to config available to core when called multiple times', () => { + init(config); + + setSubmoduleRegistry([ + createMockIdSubmodule('mockId1Module', {id: {mockId1: 'mockId1_value', mockId2: 'mockId2_value_from_mockId1Module'}}), + createMockIdSubmodule('mockId2Module', {id: {mockId1: 'mockId1_value_from_mockId2Module', mockId2: 'mockId2_value'}}), + ]); + + config.setConfig({ + userSync: { + idPriority: { + mockId1: ['mockId2Module', 'mockId1Module'], + mockId2: ['mockId1Module', 'mockId2Module'] + }, + auctionDelay: 10, // with auctionDelay > 0, no auction is needed to complete init + userIds: [ + { name: 'mockId1Module' }, + { name: 'mockId2Module' } + ] + } + }); + + return getGlobal().getUserIdsAsync().then((uidsFirstRequest) => { + getGlobal().getUserIdsAsync().then((uidsSecondRequest) => { + expect(uidsFirstRequest['mockId1']).to.deep.equal('mockId1_value_from_mockId2Module'); + expect(uidsFirstRequest['mockId2']).to.deep.equal('mockId2_value_from_mockId1Module'); + expect(uidsFirstRequest).to.deep.equal(uidsSecondRequest); + }) + }); + }); + + it('pbjs.getUserIds(Async) with priority config but no collision', () => { + init(config); + + setSubmoduleRegistry([ + createMockIdSubmodule('mockId1Module', {id: {mockId1: 'mockId1_value'}}), + createMockIdSubmodule('mockId2Module', {id: {mockId2: 'mockId2_value'}}), + createMockIdSubmodule('mockId3Module', {id: undefined}), + createMockIdSubmodule('mockId4Module', {id: {mockId4: 'mockId4_value'}}) + ]); + + config.setConfig({ + userSync: { + idPriority: { + mockId1: ['mockId3Module', 'mockId1Module'], + mockId4: ['mockId2Module', 'mockId4Module'] + }, + auctionDelay: 10, // with auctionDelay > 0, no auction is needed to complete init + userIds: [ + { name: 'mockId1Module' }, + { name: 'mockId2Module' }, + { name: 'mockId3Module' }, + { name: 'mockId4Module' } + ] + } + }); + + return getGlobal().getUserIdsAsync().then((uids) => { + expect(uids['mockId1']).to.deep.equal('mockId1_value'); + expect(uids['mockId2']).to.deep.equal('mockId2_value'); + expect(uids['mockId3']).to.be.undefined; + expect(uids['mockId4']).to.deep.equal('mockId4_value'); + }); + }); + it('pbjs.getUserIdsAsEids', function (done) { init(config); setSubmoduleRegistry([sharedIdSystemSubmodule]); @@ -416,6 +641,88 @@ describe('User ID', function () { }); }); + it('pbjs.getUserIdsAsEids should prioritize user ids according to config available to core', () => { + init(config); + setSubmoduleRegistry([ + createMockIdSubmodule('mockId1Module', {id: {uid2: {id: 'uid2_value'}}}), + createMockIdSubmodule('mockId2Module', {id: {pubcid: 'pubcid_value', lipb: {lipbid: 'lipbid_value_from_mockId2Module'}}}), + createMockIdSubmodule('mockId3Module', {id: {uid2: {id: 'uid2_value_from_mockId3Module'}, pubcid: 'pubcid_value_from_mockId3Module', lipb: {lipbid: 'lipbid_value'}, merkleId: {id: 'merkleId_value_from_mockId3Module'}}}), + createMockIdSubmodule('mockId4Module', {id: {merkleId: {id: 'merkleId_value'}}}) + ]); + config.setConfig({ + userSync: { + idPriority: { + uid2: ['mockId3Module', 'mockId1Module'], + merkleId: ['mockId4Module', 'mockId3Module'] + }, + auctionDelay: 10, // with auctionDelay > 0, no auction is needed to complete init + userIds: [ + { name: 'mockId1Module' }, + { name: 'mockId2Module' }, + { name: 'mockId3Module' }, + { name: 'mockId4Module' } + ] + } + }); + + const ids = { + 'uid2': { id: 'uid2_value_from_mockId3Module' }, + 'pubcid': 'pubcid_value', + 'lipb': { lipbid: 'lipbid_value_from_mockId2Module' }, + 'merkleId': { id: 'merkleId_value' } + }; + + return getGlobal().getUserIdsAsync().then(() => { + const eids = getGlobal().getUserIdsAsEids(); + const expected = createEidsArray(ids); + expect(eids).to.deep.equal(expected); + }); + }); + + describe('EID updateConfig', () => { + function mockSubmod(name, eids) { + return createMockIdSubmodule(name, null, null, eids); + } + + it('does not choke if a submod does not provide an eids map', () => { + setSubmoduleRegistry([ + mockSubmod('mock1'), + mockSubmod('mock2') + ]); + expect(EID_CONFIG.size).to.equal(0); + }); + + it('should merge together submodules\' eid configs', () => { + setSubmoduleRegistry([ + mockSubmod('mock1', {mock1: {m: 1}}), + mockSubmod('mock2', {mock2: {m: 2}}) + ]); + expect(EID_CONFIG.get('mock1')).to.eql({m: 1}); + expect(EID_CONFIG.get('mock2')).to.eql({m: 2}); + }); + + it('should respect idPriority', () => { + config.setConfig({ + userSync: { + idPriority: { + m1: ['mod2', 'mod1'], + m2: ['mod1', 'mod2'] + }, + userIds: [ + { name: 'mod1' }, + { name: 'mod2' }, + ] + } + }); + setSubmoduleRegistry([ + mockSubmod('mod1', {m1: {i: 1}, m2: {i: 2}}), + mockSubmod('mod2', {m1: {i: 3}, m2: {i: 4}}) + ]); + expect(EID_CONFIG.get('m1')).to.eql({i: 3}); + expect(EID_CONFIG.get('m2')).to.eql({i: 2}); + }); + }) + it('should set googletag ppid correctly', function () { let adUnits = [getAdUnitMock()]; init(config); @@ -441,6 +748,59 @@ describe('User ID', function () { }); }); + it('should set googletag ppid correctly when prioritized according to config available to core', () => { + let adUnits = [getAdUnitMock()]; + init(config); + setSubmoduleRegistry([ + // some of the ids are padded to have length >= 32 characters + createMockIdSubmodule('mockId1Module', {id: {uid2: {id: 'uid2_value_7ac66c0f148de9519b8bd264312c4d64'}}}), + createMockIdSubmodule('mockId2Module', {id: {pubcid: 'pubcid_value_7ac66c0f148de9519b8bd264312c4d64', lipb: {lipbid: 'lipbid_value_from_mockId2Module_7ac66c0f148de9519b8bd264312c4d64'}}}), + createMockIdSubmodule('mockId3Module', { + id: { + uid2: { + id: 'uid2_value_from_mockId3Module_7ac66c0f148de9519b8bd264312c4d64' + }, + pubcid: 'pubcid_value_from_mockId3Module_7ac66c0f148de9519b8bd264312c4d64', + lipb: { + lipbid: 'lipbid_value_7ac66c0f148de9519b8bd264312c4d64' + }, + merkleId: { + id: 'merkleId_value_from_mockId3Module_7ac66c0f148de9519b8bd264312c4d64' + } + } + }, null, { + uid2: { + source: 'uidapi.com', + getValue(data) { return data.id } + } + }), + createMockIdSubmodule('mockId4Module', {id: {merkleId: {id: 'merkleId_value_7ac66c0f148de9519b8bd264312c4d64'}}}) + ]); + + // before ppid should not be set + expect(window.googletag._ppid).to.equal(undefined); + + config.setConfig({ + userSync: { + ppid: 'uidapi.com', + idPriority: { + uid2: ['mockId3Module', 'mockId1Module'], + merkleId: ['mockId4Module', 'mockId3Module'] + }, + userIds: [ + { name: 'mockId1Module' }, + { name: 'mockId2Module' }, + { name: 'mockId3Module' }, + { name: 'mockId4Module' } + ] + } + }); + + return expectImmediateBidHook(() => {}, {adUnits}).then(() => { + expect(window.googletag._ppid).to.equal('uid2valuefrommockId3Module7ac66c0f148de9519b8bd264312c4d64'); + }); + }); + describe('submodule callback', () => { const TEST_KEY = 'testKey'; @@ -504,6 +864,11 @@ describe('User ID', function () { }, decode(d) { return d + }, + eids: { + pubcid: { + source: 'pubcid.org', + } } }]); config.setConfig({ @@ -589,6 +954,58 @@ describe('User ID', function () { }) }); + it('should make PPID available to core and respect priority', () => { + init(config); + setSubmoduleRegistry([ + // some of the ids are padded to have length >= 32 characters + createMockIdSubmodule('mockId1Module', {id: {uid2: {id: 'uid2_value_7ac66c0f148de9519b8bd264312c4d64'}}}), + createMockIdSubmodule('mockId2Module', {id: {pubcid: 'pubcid_value_7ac66c0f148de9519b8bd264312c4d64', lipb: {lipbid: 'lipbid_value_from_mockId2Module_7ac66c0f148de9519b8bd264312c4d64'}}}), + createMockIdSubmodule('mockId3Module', { + id: { + uid2: { + id: 'uid2_value_from_mockId3Module_7ac66c0f148de9519b8bd264312c4d64' + }, + pubcid: 'pubcid_value_from_mockId3Module_7ac66c0f148de9519b8bd264312c4d64', + lipb: { + lipbid: 'lipbid_value_7ac66c0f148de9519b8bd264312c4d64' + }, + merkleId: { + id: 'merkleId_value_from_mockId3Module_7ac66c0f148de9519b8bd264312c4d64' + } + } + }, null, { + uid2: { + source: 'uidapi.com', + getValue(data) { return data.id } + } + }), + createMockIdSubmodule('mockId4Module', {id: {merkleId: {id: 'merkleId_value_7ac66c0f148de9519b8bd264312c4d64'}}}) + ]); + + // before ppid should not be set + expect(window.googletag._ppid).to.equal(undefined); + + config.setConfig({ + userSync: { + ppid: 'uidapi.com', + idPriority: { + uid2: ['mockId3Module', 'mockId1Module'], + merkleId: ['mockId4Module', 'mockId3Module'] + }, + userIds: [ + { name: 'mockId1Module' }, + { name: 'mockId2Module' }, + { name: 'mockId3Module' }, + { name: 'mockId4Module' } + ] + } + }); + + return getGlobal().refreshUserIds().then(() => { + expect(getPPID()).to.eql('uid2valuefrommockId3Module7ac66c0f148de9519b8bd264312c4d64'); + }) + }); + describe('refreshing before init is complete', () => { const MOCK_ID = {'MOCKID': '1111'}; let mockIdCallback; @@ -596,6 +1013,7 @@ describe('User ID', function () { beforeEach(() => { mockIdCallback = sinon.stub(); + coreStorage.setCookie('MOCKID', '', EXPIRED_COOKIE_DATE); let mockIdSystem = { name: 'mockId', decode: function(value) { @@ -763,9 +1181,66 @@ describe('User ID', function () { }); }); + it('pbjs.refreshUserIds updates priority config', () => { + init(config); + + setSubmoduleRegistry([ + createMockIdSubmodule('mockId1Module', {id: {mockId1: 'mockId1_value'}}), + createMockIdSubmodule('mockId2Module', {id: {mockId2: 'mockId2_value', mockId3: 'mockId3_value_from_mockId2Module'}}), + createMockIdSubmodule('mockId3Module', {id: {mockId1: 'mockId1_value_from_mockId3Module', mockId2: 'mockId2_value_from_mockId3Module', mockId3: 'mockId3_value', mockId4: 'mockId4_value_from_mockId3Module'}}), + createMockIdSubmodule('mockId4Module', {id: {mockId4: 'mockId4_value'}}) + ]); + + config.setConfig({ + userSync: { + idPriority: { + mockId1: ['mockId3Module', 'mockId1Module'], + mockId4: ['mockId4Module', 'mockId3Module'] + }, + auctionDelay: 10, // with auctionDelay > 0, no auction is needed to complete init + userIds: [ + { name: 'mockId1Module' }, + { name: 'mockId2Module' }, + { name: 'mockId3Module' }, + { name: 'mockId4Module' } + ] + } + }); + + return getGlobal().getUserIdsAsync().then((uids) => { + expect(uids['mockId1']).to.deep.equal('mockId1_value_from_mockId3Module'); + expect(uids['mockId2']).to.deep.equal('mockId2_value'); + expect(uids['mockId3']).to.deep.equal('mockId3_value_from_mockId2Module'); + expect(uids['mockId4']).to.deep.equal('mockId4_value'); + + config.setConfig({ + userSync: { + idPriority: { + mockId1: ['mockId1Module', 'mockId3Module'], + mockId4: ['mockId3Module', 'mockId4Module'] + }, + auctionDelay: 10, // with auctionDelay > 0, no auction is needed to complete init + userIds: [ + { name: 'mockId1Module' }, + { name: 'mockId2Module' }, + { name: 'mockId3Module' }, + { name: 'mockId4Module' } + ] + } + }); + + return getGlobal().getUserIdsAsync().then((uids) => { + expect(uids['mockId1']).to.deep.equal('mockId1_value'); + expect(uids['mockId2']).to.deep.equal('mockId2_value'); + expect(uids['mockId3']).to.deep.equal('mockId3_value_from_mockId2Module'); + expect(uids['mockId4']).to.deep.equal('mockId4_value_from_mockId3Module'); + }); + }); + }); + it('pbjs.refreshUserIds refreshes single', function() { coreStorage.setCookie('MOCKID', '', EXPIRED_COOKIE_DATE); - coreStorage.setCookie('REFRESH', '', EXPIRED_COOKIE_DATE); + coreStorage.setCookie('refreshedid', '', EXPIRED_COOKIE_DATE); let sandbox = sinon.createSandbox(); let mockIdCallback = sandbox.stub().returns({id: {'MOCKID': '1111'}}); @@ -2615,7 +3090,7 @@ describe('User ID', function () { expect(server.requests).to.be.empty; return endAuction(); }).then(() => { - expect(server.requests[0].url).to.equal('/any/unifiedid/url'); + expect(server.requests[0].url).to.match(/\/any\/unifiedid\/url/); }); }); @@ -2729,17 +3204,12 @@ describe('User ID', function () { } }; - consentData = { - gdprApplies: true, - consentString: 'mockString', - apiVersion: 1, - hasValidated: true // mock presence of GPDR enforcement module - } // clear cookies expStr = (new Date(Date.now() + 25000).toUTCString()); coreStorage.setCookie(mockIdCookieName, '', EXPIRED_COOKIE_DATE); coreStorage.setCookie(`${mockIdCookieName}_last`, '', EXPIRED_COOKIE_DATE); - coreStorage.setCookie(CONSENT_LOCAL_STORAGE_NAME, '', EXPIRED_COOKIE_DATE); + coreStorage.setCookie(`${mockIdCookieName}_cst`, '', EXPIRED_COOKIE_DATE); + allConsent.reset(); // init adUnits = [getAdUnitMock()]; @@ -2753,10 +3223,20 @@ describe('User ID', function () { config.resetConfig(); }); - it('calls getId if no stored consent data and refresh is not needed', function () { - coreStorage.setCookie(mockIdCookieName, JSON.stringify({id: '1234'}), expStr); - coreStorage.setCookie(`${mockIdCookieName}_last`, (new Date(Date.now() - 1 * 1000).toUTCString()), expStr); + function setStorage({ + val = JSON.stringify({id: '1234'}), + lastDelta = 60 * 1000, + cst = null + } = {}) { + coreStorage.setCookie(mockIdCookieName, val, expStr); + coreStorage.setCookie(`${mockIdCookieName}_last`, (new Date(Date.now() - lastDelta).toUTCString()), expStr); + if (cst != null) { + coreStorage.setCookie(`${mockIdCookieName}_cst`, cst, expStr); + } + } + it('calls getId if no stored consent data and refresh is not needed', function () { + setStorage({lastDelta: 1000}); config.setConfig(userIdConfig); let innerAdUnits; @@ -2770,9 +3250,7 @@ describe('User ID', function () { }); it('calls getId if no stored consent data but refresh is needed', function () { - coreStorage.setCookie(mockIdCookieName, JSON.stringify({id: '1234'}), expStr); - coreStorage.setCookie(`${mockIdCookieName}_last`, (new Date(Date.now() - 60 * 1000).toUTCString()), expStr); - + setStorage(); config.setConfig(userIdConfig); let innerAdUnits; @@ -2786,11 +3264,7 @@ describe('User ID', function () { }); it('calls getId if empty stored consent and refresh not needed', function () { - coreStorage.setCookie(mockIdCookieName, JSON.stringify({id: '1234'}), expStr); - coreStorage.setCookie(`${mockIdCookieName}_last`, (new Date(Date.now() - 1 * 1000).toUTCString()), expStr); - - setStoredConsentData(); - + setStorage({cst: ''}); config.setConfig(userIdConfig); let innerAdUnits; @@ -2804,10 +3278,10 @@ describe('User ID', function () { }); it('calls getId if stored consent does not match current consent and refresh not needed', function () { - coreStorage.setCookie(mockIdCookieName, JSON.stringify({id: '1234'}), expStr); - coreStorage.setCookie(`${mockIdCookieName}_last`, (new Date(Date.now() - 1 * 1000).toUTCString()), expStr); - - setStoredConsentData({...consentData, consentString: 'different'}); + setStorage({cst: getConsentHash()}); + gdprDataHandler.setConsentData({ + consentString: 'different' + }); config.setConfig(userIdConfig); @@ -2822,10 +3296,7 @@ describe('User ID', function () { }); it('does not call getId if stored consent matches current consent and refresh not needed', function () { - coreStorage.setCookie(mockIdCookieName, JSON.stringify({id: '1234'}), expStr); - coreStorage.setCookie(`${mockIdCookieName}_last`, (new Date(Date.now() - 1 * 1000).toUTCString()), expStr); - - setStoredConsentData({...consentData}); + setStorage({lastDelta: 1000, cst: getConsentHash()}); config.setConfig(userIdConfig); @@ -2950,6 +3421,73 @@ describe('User ID', function () { }).catch(done); }); + it('pbjs.getEncryptedEidsForSource should return prioritized id as non-encrypted string', (done) => { + init(config); + setSubmoduleRegistry([ + createMockIdSubmodule('mockId1Module', {id: {uid2: {id: 'uid2_value'}}}), + createMockIdSubmodule('mockId2Module', {id: {pubcid: 'pubcid_value', lipb: {lipbid: 'lipbid_value_from_mockId2Module'}}}), + createMockIdSubmodule('mockId3Module', {id: {uid2: {id: 'uid2_value_from_mockId3Module'}, pubcid: 'pubcid_value_from_mockId3Module', lipb: {lipbid: 'lipbid_value'}, merkleId: {id: 'merkleId_value_from_mockId3Module'}}}, null, { + uid2: { + source: 'uidapi.com', + getValue(data) { + return data.id + } + }, + pubcid: { + source: 'pubcid.org', + }, + lipb: { + source: 'liveintent.com', + getValue(data) { + return data.lipbid + } + } + }), + createMockIdSubmodule('mockId4Module', {id: {merkleId: {id: 'merkleId_value'}}}, null, { + merkleId: { + source: 'merkleinc.com', + getValue(data) { + return data.id + } + } + }) + ]); + config.setConfig({ + userSync: { + idPriority: { + uid2: ['mockId3Module', 'mockId1Module'], + merkleId: ['mockId4Module', 'mockId3Module'] + }, + auctionDelay: 10, // with auctionDelay > 0, no auction is needed to complete init + userIds: [ + { name: 'mockId1Module' }, + { name: 'mockId2Module' }, + { name: 'mockId3Module' }, + { name: 'mockId4Module' } + ] + } + }); + + const expctedIds = [ + 'pubcid_value', + 'uid2_value_from_mockId3Module', + 'merkleId_value', + 'lipbid_value_from_mockId2Module' + ]; + + const encrypt = false; + + Promise.all([ + getGlobal().getEncryptedEidsForSource('pubcid.org', encrypt), + getGlobal().getEncryptedEidsForSource('uidapi.com', encrypt), + getGlobal().getEncryptedEidsForSource('merkleinc.com', encrypt), + getGlobal().getEncryptedEidsForSource('liveintent.com', encrypt) + ]).then((result) => { + expect(result).to.deep.equal(expctedIds); + done(); + }) + }); + describe('pbjs.getEncryptedEidsForSource', () => { beforeEach(() => { init(config); @@ -3013,6 +3551,45 @@ describe('User ID', function () { done(); }); }); + + it('pbjs.getUserIdsAsEidBySource with priority config available to core', () => { + init(config); + setSubmoduleRegistry([ + createMockIdSubmodule('mockId1Module', {id: {uid2: {id: 'uid2_value'}}}), + createMockIdSubmodule('mockId2Module', {id: {pubcid: 'pubcid_value', lipb: {lipbid: 'lipbid_value_from_mockId2Module'}}}), + createMockIdSubmodule('mockId3Module', {id: {uid2: {id: 'uid2_value_from_mockId3Module'}, pubcid: 'pubcid_value_from_mockId3Module', lipb: {lipbid: 'lipbid_value'}, merkleId: {id: 'merkleId_value_from_mockId3Module'}}}), + createMockIdSubmodule('mockId4Module', {id: {merkleId: {id: 'merkleId_value'}}}) + ]); + config.setConfig({ + userSync: { + idPriority: { + uid2: ['mockId3Module', 'mockId1Module'], + merkleId: ['mockId4Module', 'mockId3Module'] + }, + auctionDelay: 10, // with auctionDelay > 0, no auction is needed to complete init + userIds: [ + { name: 'mockId1Module' }, + { name: 'mockId2Module' }, + { name: 'mockId3Module' }, + { name: 'mockId4Module' } + ] + } + }); + + const ids = { + 'uidapi.com': {'uid2': {id: 'uid2_value_from_mockId3Module'}}, + 'pubcid.org': {'pubcid': 'pubcid_value'}, + 'liveintent.com': {'lipb': {lipbid: 'lipbid_value_from_mockId2Module'}}, + 'merkleinc.com': {'merkleId': {id: 'merkleId_value'}} + }; + + return getGlobal().getUserIdsAsync().then(() => { + expect(getGlobal().getUserIdsAsEidBySource('pubcid.org')).to.deep.equal(createEidsArray(ids['pubcid.org'])[0]); + expect(getGlobal().getUserIdsAsEidBySource('uidapi.com')).to.deep.equal(createEidsArray(ids['uidapi.com'])[0]); + expect(getGlobal().getUserIdsAsEidBySource('merkleinc.com')).to.deep.equal(createEidsArray(ids['merkleinc.com'])[0]); + expect(getGlobal().getUserIdsAsEidBySource('liveintent.com')).to.deep.equal(createEidsArray(ids['liveintent.com'])[0]); + }); + }); }) }); }); diff --git a/test/spec/modules/viantOrtbBidAdapter_spec.js b/test/spec/modules/viantOrtbBidAdapter_spec.js new file mode 100644 index 00000000000..73fdb7f3dc8 --- /dev/null +++ b/test/spec/modules/viantOrtbBidAdapter_spec.js @@ -0,0 +1,475 @@ +import { spec, converter } from 'modules/viantOrtbBidAdapter.js'; +import {assert, expect} from 'chai'; +import { deepClone } from '../../../src/utils'; +import {buildWindowTree} from '../../helpers/refererDetectionHelper'; +import {detectReferer} from '../../../src/refererDetection'; +describe('viantOrtbBidAdapter', function () { + function testBuildRequests(bidRequests, bidderRequestBase) { + let clonedBidderRequest = deepClone(bidderRequestBase); + clonedBidderRequest.bids = bidRequests; + let requests = spec.buildRequests(bidRequests, clonedBidderRequest); + return requests + } + describe('isBidRequestValid', function() { + function makeBid() { + return { + 'bidder': 'viant', + 'params': { + 'publisherId': '464', + 'placementId': 'some-PlacementId_1' + }, + 'mediaTypes': { + 'banner': { + 'sizes': [ + [728, 90] + ] + } + }, + 'adUnitCode': 'adunit-code', + 'bidId': '30b31c1838de1e', + 'bidderRequestId': '22edbae2733bf6', + 'auctionId': '1d1a030790a475', + }; + } + + describe('core', function () { + it('should return true when required params found', function () { + expect(spec.isBidRequestValid(makeBid())).to.equal(true); + }); + + it('should return false when publisherId not passed', function () { + let bid = makeBid(); + delete bid.params.publisherId; + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + + it('should return true if placementId is not passed ', function () { + let bid = makeBid(); + delete bid.params.placementId; + bid.ortb2Imp = { + + } + expect(spec.isBidRequestValid(bid)).to.equal(true); + }); + + it('should return false if mediaTypes.banner is Not passed', function () { + let bid = makeBid(); + delete bid.mediaTypes + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + }); + + describe('banner', function () { + it('should return true if banner.pos is passed correctly', function () { + let bid = makeBid(); + bid.mediaTypes.banner.pos = 1; + expect(spec.isBidRequestValid(bid)).to.equal(true); + }); + }); + + describe('video', function () { + describe('and request config uses mediaTypes', () => { + function makeBid() { + return { + 'bidder': 'viant', + 'params': { + 'unit': '12345678', + 'delDomain': 'test-del-domain', + 'publisherId': '464', + 'placementId': 'some-PlacementId_2' + }, + 'mediaTypes': { + 'video': { + 'context': 'instream', + 'playerSize': [[640, 480]], + 'mimes': ['video/mp4'], + 'protocols': [1, 2, 3, 4, 5, 6, 7, 8], + 'api': [1, 3], + 'skip': 1, + 'skipafter': 5, + 'minduration': 10, + 'maxduration': 30 + } + }, + 'adUnitCode': 'adunit-code', + 'bidId': '30b31c1838de1e', + 'bidderRequestId': '22edbae2733bf6', + 'auctionId': '1d1a030790a475', + 'transactionId': '4008d88a-8137-410b-aa35-fbfdabcb478e' + } + } + it('should return true when required params found', function () { + expect(spec.isBidRequestValid(makeBid())).to.equal(true); + }); + + it('should return false when required params are not passed', function () { + let videoBidWithMediaTypes = Object.assign({}, makeBid()); + videoBidWithMediaTypes.params = {}; + expect(spec.isBidRequestValid(videoBidWithMediaTypes)).to.equal(false); + }); + }); + }); + + describe('native', function () { + describe('and request config uses mediaTypes', () => { + function makeBid() { + return { + 'bidder': 'viant', + 'params': { + 'unit': '12345678', + 'delDomain': 'test-del-domain', + 'publisherId': '464', + 'placementId': 'some-PlacementId_2' + }, + 'mediaTypes': { + 'video': { + 'context': 'instream', + 'playerSize': [[640, 480]], + 'mimes': ['video/mp4'], + 'protocols': [1, 2, 3, 4, 5, 6, 7, 8], + 'api': [1, 3], + 'skip': 1, + 'skipafter': 5, + 'minduration': 10, + 'maxduration': 30 + } + }, + 'adUnitCode': 'adunit-code', + 'bidId': '30b31c1838de1e', + 'bidderRequestId': '22edbae2733bf6', + 'auctionId': '1d1a030790a475', + 'transactionId': '4008d88a-8137-410b-aa35-fbfdabcb478e' + } + } + it('should return true when required params found', function () { + expect(spec.isBidRequestValid(makeBid())).to.equal(true); + }); + + it('should return false when required params are not passed', function () { + let nativeBidWithMediaTypes = Object.assign({}, makeBid()); + nativeBidWithMediaTypes.params = {}; + expect(spec.isBidRequestValid(nativeBidWithMediaTypes)).to.equal(false); + }); + }); + }); + }); + + describe('buildRequests-banner', function () { + const baseBannerBidRequests = [{ + 'bidder': 'viant', + 'params': { + 'publisherId': '464', + 'placementId': '1' + }, + 'mediaTypes': { + 'banner': { + 'sizes': [[728, 90]] + } + }, + 'gdprConsent': { + 'consentString': 'consentString', + 'gdprApplies': true, + }, + 'uspConsent': '1YYY', + 'sizes': [[728, 90]], + 'transactionId': '1111474f-58b1-4368-b812-84f8c937a099', + 'adUnitCode': 'div-gpt-ad-1460505748561-0', + 'bidId': '243310435309b5', + 'bidderRequestId': '18084284054531', + 'auctionId': 'e7b34fa3-8654-424e-8c49-03e509e53d8c', + 'src': 'client', + 'bidRequestsCount': 1 + }]; + + const testWindow = buildWindowTree(['https://www.example.com/test', 'https://www.example.com/other/page', 'https://www.example.com/third/page'], 'https://othersite.com/', 'https://example.com/canonical/page'); + const baseBidderRequestReferer = detectReferer(testWindow)(); + const baseBidderRequest = { + 'bidderCode': 'viant', + 'auctionId': 'e7b34fa3-8654-424e-8c49-03e509e53d8c', + 'bidderRequestId': '18084284054531', + 'auctionStart': 1540945362095, + 'timeout': 3000, + 'refererInfo': baseBidderRequestReferer, + 'start': 1540945362099, + 'doneCbCallCount': 0 + }; + + it('test regs', function () { + const gdprBaseBidderRequest = Object.assign({}, baseBidderRequest, { + gdprConsent: { + consentString: 'consentString', + gdprApplies: true, + }, + uspConsent: '1YYN' + }); + const request = testBuildRequests(baseBannerBidRequests, gdprBaseBidderRequest)[0]; + expect(request.data.regs.ext).to.have.property('gdpr', 1); + expect(request.data.regs.ext).to.have.property('us_privacy', '1YYN'); + }); + + it('sends bid request to our endpoint that makes sense', function () { + const request = testBuildRequests(baseBannerBidRequests, baseBidderRequest)[0]; + expect(request.method).to.equal('POST'); + expect(request.url).to.be.not.empty; + expect(request.data).to.be.not.null; + }); + it('sends bid requests to the correct endpoint', function () { + const url = testBuildRequests(baseBannerBidRequests, baseBidderRequest)[0].url; + expect(url).to.equal('https://bidders-us-east-1.adelphic.net/d/rtb/v25/prebid/bidder'); + }); + + it('sends site', function () { + const requestBody = testBuildRequests(baseBannerBidRequests, baseBidderRequest)[0].data; + expect(requestBody.site).to.be.not.null; + }); + + it('includes the ad size in the bid request', function () { + const requestBody = testBuildRequests(baseBannerBidRequests, baseBidderRequest)[0].data; + expect(requestBody.imp[0].banner.format[0].w).to.equal(728); + expect(requestBody.imp[0].banner.format[0].h).to.equal(90); + }); + + it('sets the banner pos correctly if sent', function () { + let clonedBannerRequests = deepClone(baseBannerBidRequests); + clonedBannerRequests[0].mediaTypes.banner.pos = 1; + + const requestBody = testBuildRequests(clonedBannerRequests, baseBidderRequest)[0].data; + expect(requestBody.imp[0].banner.pos).to.equal(1); + }); + }); + + if (FEATURES.VIDEO) { + describe('buildRequests-video', function () { + function makeBid() { + return { + 'bidder': 'viant', + 'params': { + 'unit': '12345678', + 'delDomain': 'test-del-domain', + 'publisherId': '464', + 'placementId': 'some-PlacementId_2' + }, + 'mediaTypes': { + 'video': { + 'context': 'instream', + 'playerSize': [[640, 480]], + 'mimes': ['video/mp4'], + 'protocols': [1, 2, 3, 4, 5, 6, 7, 8], + 'api': [1, 3], + 'skip': 1, + 'skipafter': 5, + 'minduration': 10, + 'maxduration': 31 + } + }, + 'adUnitCode': 'adunit-code', + 'bidId': '30b31c1838de1e', + 'bidderRequestId': '22edbae2733bf6', + 'auctionId': '1d1a030790a475', + 'transactionId': '4008d88a-8137-410b-aa35-fbfdabcb478e' + } + } + + it('assert video and its fields is present in imp ', function () { + let requests = spec.buildRequests([makeBid()], {referrerInfo: {}}); + let clonedRequests = deepClone(requests) + assert.equal(clonedRequests[0].data.imp[0].video.mimes[0], 'video/mp4') + assert.equal(clonedRequests[0].data.imp[0].video.maxduration, 31) + assert.equal(clonedRequests[0].data.imp[0].video.placement, 1) + assert.equal(clonedRequests[0].method, 'POST') + }); + }); + } + + describe('interpretResponse', function () { + const baseBannerBidRequests = [{ + 'bidder': 'viant', + 'params': { + 'publisherId': '464', + 'placementId': '1' + }, + 'mediaTypes': { + 'banner': { + 'sizes': [[728, 90]] + } + }, + 'sizes': [[728, 90]], + 'transactionId': '1111474f-58b1-4368-b812-84f8c937a099', + 'adUnitCode': 'div-gpt-ad-1460505748561-0', + 'bidId': '243310435309b5', + 'bidderRequestId': '18084284054531', + 'auctionId': 'e7b34fa3-8654-424e-8c49-03e509e53d8c', + 'src': 'client', + 'bidRequestsCount': 1 + }]; + + const testWindow = buildWindowTree(['https://www.example.com/test', 'https://www.example.com/other/page', 'https://www.example.com/third/page'], 'https://othersite.com/', 'https://example.com/canonical/page'); + const baseBidderRequestReferer = detectReferer(testWindow)(); + const baseBidderRequest = { + 'bidderCode': 'viant', + 'auctionId': 'e7b34fa3-8654-424e-8c49-03e509e53d8c', + 'bidderRequestId': '18084284054531', + 'auctionStart': 1540945362095, + 'timeout': 3000, + 'refererInfo': baseBidderRequestReferer, + 'start': 1540945362099, + 'doneCbCallCount': 0 + }; + + it('empty bid response test', function () { + const request = testBuildRequests(baseBannerBidRequests, baseBidderRequest)[0]; + let bidResponse = {nbr: 0}; // Unknown error + let bids = spec.interpretResponse({body: bidResponse}, request); + expect(bids.length).to.equal(0); + }); + + it('bid response is a banner', function () { + const request = testBuildRequests(baseBannerBidRequests, baseBidderRequest)[0]; + let bidResponse = { + seatbid: [{ + bid: [{ + impid: '243310435309b5', + price: 2, + w: 728, + h: 90, + crid: 'test-creative-id', + dealid: 'test-deal-id', + adm: 'test-ad-markup', + }] + }], + cur: 'USD' + }; + let bids = spec.interpretResponse({body: bidResponse}, request); + expect(bids.length).to.equal(1); + let bid = bids[0]; + it('should return the proper mediaType', function () { + it('should return a creativeId', function () { + expect(bid.mediaType).to.equal('banner'); + }); + }); + it('should return a price', function () { + expect(bid.cpm).to.equal(bidResponse.seatbid[0].bid[0].price); + }); + + it('should return a request id', function () { + expect(bid.requestId).to.equal(bidResponse.seatbid[0].bid[0].impid); + }); + + it('should return width and height for the creative', function () { + expect(bid.width).to.equal(bidResponse.seatbid[0].bid[0].w); + expect(bid.height).to.equal(bidResponse.seatbid[0].bid[0].h); + }); + it('should return a creativeId', function () { + expect(bid.creativeId).to.equal(bidResponse.seatbid[0].bid[0].crid); + }); + it('should return an ad', function () { + expect(bid.ad).to.equal(bidResponse.seatbid[0].bid[0].adm); + }); + + it('should return a deal id if it exists', function () { + expect(bid.dealId).to.equal(bidResponse.seatbid[0].bid[0].dealid); + }); + + it('should have a time-to-live of 5 minutes', function () { + expect(bid.ttl).to.equal(300); + }); + + it('should always return net revenue', function () { + expect(bid.netRevenue).to.equal(true); + }); + it('should return a currency', function () { + expect(bid.currency).to.equal(bidResponse.cur); + }); + }); + }); + describe('interpretResponse-Video', function () { + const baseVideoBidRequests = [{ + 'bidder': 'viant', + 'params': { + 'publisherId': '464', + 'placementId': '1' + }, + 'mediaTypes': { + 'video': { + 'context': 'instream', + 'playerSize': [[640, 480]], + 'mimes': ['video/mp4'], + 'protocols': [1, 2, 3, 4, 5, 6, 7, 8], + 'api': [1, 3], + 'skip': 1, + 'skipafter': 5, + 'minduration': 10, + 'maxduration': 31 + } + }, + 'sizes': [[640, 480]], + 'transactionId': '1111474f-58b1-4368-b812-84f8c937a099', + 'adUnitCode': 'div-gpt-ad-1460505748561-0', + 'bidId': '243310435309b5', + 'bidderRequestId': '18084284054531', + 'auctionId': 'e7b34fa3-8654-424e-8c49-03e509e53d8c', + 'src': 'client', + 'bidRequestsCount': 1 + }]; + + const testWindow = buildWindowTree(['https://www.example.com/test', 'https://www.example.com/other/page', 'https://www.example.com/third/page'], 'https://othersite.com/', 'https://example.com/canonical/page'); + const baseBidderRequestReferer = detectReferer(testWindow)(); + const baseBidderRequest = { + 'bidderCode': 'viant', + 'auctionId': 'e7b34fa3-8654-424e-8c49-03e509e53d8c', + 'bidderRequestId': '18084284054531', + 'auctionStart': 1540945362095, + 'timeout': 3000, + 'refererInfo': baseBidderRequestReferer, + 'start': 1540945362099, + 'doneCbCallCount': 0 + }; + + it('bid response is a video', function () { + const request = testBuildRequests(baseVideoBidRequests, baseBidderRequest)[0]; + const VIDEO_BID_RESPONSE = { + 'id': 'bidderRequestId', + 'bidid': 'e7b34fa3-8654-424e-8c49-03e509e53d8c', + 'seatbid': [ + { + 'bid': [ + { + 'id': '1', + 'impid': '243310435309b5', + 'price': 1.09, + 'adid': '144762342', + 'nurl': 'http://0.0.0.0:8181/nurl', + 'adm': '', + 'adomain': [ + 'https://dummydomain.com' + ], + 'cid': 'cid', + 'crid': 'crid', + 'iurl': 'iurl', + 'cat': [], + 'h': 480, + 'w': 640 + } + ] + } + ], + 'cur': 'USD' + }; + let bids = spec.interpretResponse({body: VIDEO_BID_RESPONSE}, request); + expect(bids.length).to.equal(1); + let bid = bids[0]; + it('should return the proper mediaType', function () { + expect(bid.mediaType).to.equal('video'); + }); + it('should return correct Ad Markup', function () { + expect(bid.vastXml).to.equal(''); + }); + it('should return correct Notification', function () { + expect(bid.vastUrl).to.equal('http://0.0.0.0:8181/nurl'); + }); + it('should return correct Cpm', function () { + expect(bid.cpm).to.equal(1.09); + }); + }); + }); +}); diff --git a/test/spec/modules/vidazooBidAdapter_spec.js b/test/spec/modules/vidazooBidAdapter_spec.js index 6e92bae8d59..864f2b8551c 100644 --- a/test/spec/modules/vidazooBidAdapter_spec.js +++ b/test/spec/modules/vidazooBidAdapter_spec.js @@ -40,7 +40,6 @@ const BID = { 'placementCode': 'div-gpt-ad-1460505748561-0', 'sizes': [[300, 250], [300, 600]], 'bidderRequestId': '1fdb5ff1b6eaa7', - 'auctionId': 'auction_id', 'bidRequestsCount': 4, 'bidderRequestsCount': 3, 'bidderWinsCount': 1, @@ -64,7 +63,6 @@ const VIDEO_BID = { tid: '56e184c6-bde9-497b-b9b9-cf47a61381ee', } }, - 'auctionId': 'auction_id', 'bidRequestsCount': 4, 'bidderRequestsCount': 3, 'bidderWinsCount': 1, @@ -142,6 +140,7 @@ const SERVER_RESPONSE = { cid: 'testcid123', results: [{ 'ad': '', + 'bidId': '2d52001cabd527-response', 'price': 0.8, 'creativeId': '12610997325162499419', 'exp': 30, @@ -287,7 +286,6 @@ describe('VidazooBidAdapter', function () { usPrivacy: 'consent_string', gppString: 'gpp_string', gppSid: [7], - auctionId: 'auction_id', bidRequestsCount: 4, bidderRequestsCount: 3, bidderWinsCount: 1, @@ -361,7 +359,6 @@ describe('VidazooBidAdapter', function () { usPrivacy: 'consent_string', gppString: 'gpp_string', gppSid: [7], - auctionId: 'auction_id', bidRequestsCount: 4, bidderRequestsCount: 3, bidderWinsCount: 1, @@ -413,8 +410,122 @@ describe('VidazooBidAdapter', function () { }); }); + it('should build single banner request for multiple bids', function () { + config.setConfig({ + bidderTimeout: 3000, + vidazoo: { + singleRequest: true, + chunkSize: 2 + } + }); + + const hashUrl = hashCode(BIDDER_REQUEST.refererInfo.page); + + const BID2 = utils.deepClone(BID); + BID2.bidId = '2d52001cabd528'; + BID2.adUnitCode = 'div-gpt-ad-12345-1'; + BID2.sizes = [[300, 250]]; + + const REQUEST_DATA = { + gdprConsent: 'consent_string', + gdpr: 1, + usPrivacy: 'consent_string', + gppString: 'gpp_string', + gppSid: [7], + bidRequestsCount: 4, + bidderRequestsCount: 3, + bidderWinsCount: 1, + bidderTimeout: 3000, + transactionId: 'c881914b-a3b5-4ecf-ad9c-1c2f37c6aabf', + bidderRequestId: '1fdb5ff1b6eaa7', + sizes: ['300x250', '300x600'], + sua: { + 'source': 2, + 'platform': { + 'brand': 'Android', + 'version': ['8', '0', '0'] + }, + 'browsers': [ + {'brand': 'Not_A Brand', 'version': ['99', '0', '0', '0']}, + {'brand': 'Google Chrome', 'version': ['109', '0', '5414', '119']}, + {'brand': 'Chromium', 'version': ['109', '0', '5414', '119']} + ], + 'mobile': 1, + 'model': 'SM-G955U', + 'bitness': '64', + 'architecture': '' + }, + url: 'https%3A%2F%2Fwww.greatsite.com', + referrer: 'https://www.somereferrer.com', + cb: 1000, + bidFloor: 0.1, + bidId: '2d52001cabd527', + adUnitCode: 'div-gpt-ad-12345-0', + publisherId: '59ac17c192832d0011283fe3', + dealId: 3, + sessionId: '', + uniqueDealId: `${hashUrl}_${Date.now().toString()}`, + bidderVersion: adapter.version, + prebidVersion: version, + schain: BID.schain, + ptrace: '1000', + res: `${window.top.screen.width}x${window.top.screen.height}`, + mediaTypes: [BANNER], + uqs: getTopWindowQueryParams(), + 'ext.param1': 'loremipsum', + 'ext.param2': 'dolorsitamet', + isStorageAllowed: true, + gpid: '1234567890', + cat: ['IAB2'], + pagecat: ['IAB2-2'], + webSessionId: webSessionId + }; + + const REQUEST_DATA2 = utils.deepClone(REQUEST_DATA); + REQUEST_DATA2.bidId = '2d52001cabd528'; + REQUEST_DATA2.adUnitCode = 'div-gpt-ad-12345-1'; + REQUEST_DATA2.sizes = ['300x250']; + REQUEST_DATA2.dealId = 4; + + const requests = adapter.buildRequests([BID, BID2], BIDDER_REQUEST); + expect(requests).to.have.length(1); + + expect(requests[0]).to.deep.equal({ + method: 'POST', + url: `${createDomain(SUB_DOMAIN)}/prebid/multi/59db6b3b4ffaa70004f45cdc`, + data: {bids: [REQUEST_DATA, REQUEST_DATA2]} + }); + }); + + it('should return seperated requests for video and banner if singleRequest is true', function () { + config.setConfig({ + bidderTimeout: 3000, + vidazoo: { + singleRequest: true, + chunkSize: 2 + } + }); + + const requests = adapter.buildRequests([BID, VIDEO_BID], BIDDER_REQUEST); + expect(requests).to.have.length(2); + }); + + it('should chunk requests if requests exceed chunkSize and singleRequest is true', function () { + config.setConfig({ + bidderTimeout: 3000, + vidazoo: { + singleRequest: true, + chunkSize: 2 + } + }); + + const requests = adapter.buildRequests([BID, BID, BID, BID], BIDDER_REQUEST); + expect(requests).to.have.length(2); + }); + after(function () { $$PREBID_GLOBAL$$.bidderSettings = {}; + config.resetConfig(); sandbox.restore(); }); }); @@ -515,6 +626,21 @@ describe('VidazooBidAdapter', function () { }); }); + it('should populate requestId from response in case of singleRequest true', function () { + config.setConfig({ + vidazoo: { + singleRequest: true, + chunkSize: 2 + } + }); + + const responses = adapter.interpretResponse(SERVER_RESPONSE, REQUEST); + expect(responses).to.have.length(1); + expect(responses[0].requestId).to.equal('2d52001cabd527-response'); + + config.resetConfig(); + }); + it('should take default TTL', function () { const serverResponse = utils.deepClone(SERVER_RESPONSE); delete serverResponse.body.results[0].exp; diff --git a/test/spec/modules/visxBidAdapter_spec.js b/test/spec/modules/visxBidAdapter_spec.js index 9a486cd6c34..5528705efd7 100755 --- a/test/spec/modules/visxBidAdapter_spec.js +++ b/test/spec/modules/visxBidAdapter_spec.js @@ -1,5 +1,5 @@ import { expect } from 'chai'; -import { spec } from 'modules/visxBidAdapter.js'; +import { spec, storage } from 'modules/visxBidAdapter.js'; import { config } from 'src/config.js'; import { newBidder } from 'src/adapters/bidderFactory.js'; import * as utils from 'src/utils.js'; @@ -82,6 +82,9 @@ describe('VisxAdapter', function () { }); return res; } + + let cookiesAreEnabledStub, localStorageIsEnabledStub; + const bidderRequest = { timeout: 3000, refererInfo: { @@ -180,6 +183,24 @@ describe('VisxAdapter', function () { 'ext': {'bidder': {'uid': 903537}} }]; + before(() => { + $$PREBID_GLOBAL$$.bidderSettings = { + visx: { + storageAllowed: false + } + }; + localStorageIsEnabledStub = sinon.stub(storage, 'localStorageIsEnabled'); + cookiesAreEnabledStub = sinon.stub(storage, 'cookiesAreEnabled'); + localStorageIsEnabledStub.returns(false); + cookiesAreEnabledStub.returns(false); + }); + + after(() => { + localStorageIsEnabledStub.restore(); + cookiesAreEnabledStub.restore(); + $$PREBID_GLOBAL$$.bidderSettings = {}; + }); + it('should attach valid params to the tag', function () { const firstBid = bidRequests[0]; const bids = [firstBid]; @@ -422,6 +443,7 @@ describe('VisxAdapter', function () { }); return res; } + let cookiesAreEnabledStub, localStorageIsEnabledStub; const bidderRequest = { timeout: 3000, refererInfo: { @@ -449,6 +471,24 @@ describe('VisxAdapter', function () { } ]; + before(() => { + $$PREBID_GLOBAL$$.bidderSettings = { + visx: { + storageAllowed: false + } + }; + localStorageIsEnabledStub = sinon.stub(storage, 'localStorageIsEnabled'); + cookiesAreEnabledStub = sinon.stub(storage, 'cookiesAreEnabled'); + localStorageIsEnabledStub.returns(false); + cookiesAreEnabledStub.returns(false); + }); + + after(() => { + localStorageIsEnabledStub.restore(); + cookiesAreEnabledStub.restore(); + $$PREBID_GLOBAL$$.bidderSettings = {}; + }); + it('should send requst for banner bid', function () { const request = spec.buildRequests([bidRequests[0]], bidderRequest); const payload = parseRequest(request.url); @@ -486,6 +526,7 @@ describe('VisxAdapter', function () { }); return res; } + let cookiesAreEnabledStub, localStorageIsEnabledStub; const bidderRequest = { timeout: 3000, refererInfo: { @@ -529,10 +570,23 @@ describe('VisxAdapter', function () { documentStub.withArgs('visx-adunit-element-2').returns({ id: 'visx-adunit-element-2' }); + + $$PREBID_GLOBAL$$.bidderSettings = { + visx: { + storageAllowed: false + } + }; + localStorageIsEnabledStub = sinon.stub(storage, 'localStorageIsEnabled'); + cookiesAreEnabledStub = sinon.stub(storage, 'cookiesAreEnabled'); + localStorageIsEnabledStub.returns(false); + cookiesAreEnabledStub.returns(false); }); after(function() { sandbox.restore(); + localStorageIsEnabledStub.restore(); + cookiesAreEnabledStub.restore(); + $$PREBID_GLOBAL$$.bidderSettings = {}; }); it('should find ad slot by ad unit code as element id', function () { @@ -1203,6 +1257,7 @@ describe('VisxAdapter', function () { const request = spec.buildRequests(bidRequests); const pendingUrl = 'https://t.visx.net/track/pending/123123123'; const winUrl = 'https://t.visx.net/track/win/53245341'; + const runtimeUrl = 'https://t.visx.net/track/status/12345678'; const expectedResponse = [ { 'requestId': '300bfeb0d71a5b', @@ -1227,7 +1282,8 @@ describe('VisxAdapter', function () { 'ext': { 'events': { 'pending': pendingUrl, - 'win': winUrl + 'win': winUrl, + 'runtime': runtimeUrl }, 'targeting': { 'hb_visx_product': 'understitial', @@ -1244,6 +1300,9 @@ describe('VisxAdapter', function () { pending: pendingUrl, win: winUrl, }); + utils.deepSetValue(serverResponse.bid[0], 'ext.visx.events', { + runtime: runtimeUrl + }); const result = spec.interpretResponse({'body': {'seatbid': [serverResponse]}}, request); expect(result).to.deep.equal(expectedResponse); }); @@ -1271,6 +1330,39 @@ describe('VisxAdapter', function () { expect(utils.triggerPixel.calledOnceWith(trackUrl)).to.equal(true); }); + it('onBidWon with runtime tracker (0 < timeToRespond <= 5000 )', function () { + const trackUrl = 'https://t.visx.net/track/win/123123123'; + const runtimeUrl = 'https://t.visx.net/track/status/12345678/{STATUS_CODE}'; + const bid = { auctionId: '1', ext: { events: { win: trackUrl, runtime: runtimeUrl } }, timeToRespond: 100 }; + spec.onBidWon(bid); + expect(utils.triggerPixel.calledTwice).to.equal(true); + expect(utils.triggerPixel.calledWith(trackUrl)).to.equal(true); + expect(utils.triggerPixel.calledWith(runtimeUrl.replace('{STATUS_CODE}', 999002))).to.equal(true); + }); + + it('onBidWon with runtime tracker (timeToRespond <= 0 )', function () { + const runtimeUrl = 'https://t.visx.net/track/status/12345678/{STATUS_CODE}'; + const bid = { auctionId: '2', ext: { events: { runtime: runtimeUrl } }, timeToRespond: 0 }; + spec.onBidWon(bid); + expect(utils.triggerPixel.calledOnceWith(runtimeUrl.replace('{STATUS_CODE}', 999000))).to.equal(true); + }); + + it('onBidWon with runtime tracker (timeToRespond > 5000 )', function () { + const runtimeUrl = 'https://t.visx.net/track/status/12345678/{STATUS_CODE}'; + const bid = { auctionId: '3', ext: { events: { runtime: runtimeUrl } }, timeToRespond: 5001 }; + spec.onBidWon(bid); + expect(utils.triggerPixel.calledOnceWith(runtimeUrl.replace('{STATUS_CODE}', 999100))).to.equal(true); + }); + + it('onBidWon runtime tracker should be called once per auction', function () { + const runtimeUrl = 'https://t.visx.net/track/status/12345678/{STATUS_CODE}'; + const bid1 = { auctionId: '4', ext: { events: { runtime: runtimeUrl } }, timeToRespond: 100 }; + spec.onBidWon(bid1); + const bid2 = { auctionId: '4', ext: { events: { runtime: runtimeUrl } }, timeToRespond: 200 }; + spec.onBidWon(bid2); + expect(utils.triggerPixel.calledOnceWith(runtimeUrl.replace('{STATUS_CODE}', 999002))).to.equal(true); + }); + it('onTimeout', function () { const data = [{ timeout: 3000, adUnitCode: 'adunit-code-1', auctionId: '1cbd2feafe5e8b', bidder: 'visx', bidId: '23423', params: [{ uid: '1' }] }]; const expectedData = [{ timeout: 3000, params: [{ uid: 1 }] }]; @@ -1323,4 +1415,100 @@ describe('VisxAdapter', function () { expect(query).to.deep.equal({}); }); }); + + describe('first party user id', function () { + const USER_ID_KEY = '__vads'; + const USER_ID_DUMMY_VALUE_COOKIE = 'dummy_id_cookie'; + const USER_ID_DUMMY_VALUE_LOCAL_STORAGE = 'dummy_id_local_storage'; + + let getDataFromLocalStorageStub, localStorageIsEnabledStub; + let getCookieStub, cookiesAreEnabledStub; + + const bidRequests = [ + { + 'bidder': 'visx', + 'params': { + 'uid': 903535 + }, + 'adUnitCode': 'adunit-code-1', + 'sizes': [[300, 250], [300, 600]], + 'bidId': '30b31c1838de1e', + 'bidderRequestId': '22edbae2733bf6', + 'auctionId': '1d1a030790a475', + } + ]; + const bidderRequest = { + timeout: 3000, + refererInfo: { + page: 'https://example.com' + } + }; + + beforeEach(() => { + $$PREBID_GLOBAL$$.bidderSettings = { + visx: { + storageAllowed: true + } + }; + cookiesAreEnabledStub = sinon.stub(storage, 'cookiesAreEnabled'); + localStorageIsEnabledStub = sinon.stub(storage, 'localStorageIsEnabled'); + }); + + afterEach(() => { + cookiesAreEnabledStub.restore(); + localStorageIsEnabledStub.restore(); + getCookieStub && getCookieStub.restore(); + getDataFromLocalStorageStub && getDataFromLocalStorageStub.restore(); + $$PREBID_GLOBAL$$.bidderSettings = {}; + }); + + it('should not pass user id if both cookies and local storage are not available', function () { + cookiesAreEnabledStub.returns(false); + localStorageIsEnabledStub.returns(false); + + const request = spec.buildRequests(bidRequests, bidderRequest); + + expect(request.data.user).to.be.undefined; + }); + + it('should get user id from cookie if available', function () { + cookiesAreEnabledStub.returns(true); + localStorageIsEnabledStub.returns(false); + getCookieStub = sinon.stub(storage, 'getCookie'); + getCookieStub.withArgs(USER_ID_KEY).returns(USER_ID_DUMMY_VALUE_COOKIE); + + const request = spec.buildRequests(bidRequests, bidderRequest); + + expect(request.data.user.ext.vads).to.equal(USER_ID_DUMMY_VALUE_COOKIE); + }); + + it('should get user id from local storage if available', function () { + cookiesAreEnabledStub.returns(false); + localStorageIsEnabledStub.returns(true); + getDataFromLocalStorageStub = sinon.stub(storage, 'getDataFromLocalStorage'); + getDataFromLocalStorageStub.withArgs(USER_ID_KEY).returns(USER_ID_DUMMY_VALUE_LOCAL_STORAGE); + + const request = spec.buildRequests(bidRequests, bidderRequest); + + expect(request.data.user.ext.vads).to.equal(USER_ID_DUMMY_VALUE_LOCAL_STORAGE); + }); + + it('should create user id and store it in cookies (if user id does not exist)', function () { + cookiesAreEnabledStub.returns(true); + localStorageIsEnabledStub.returns(false); + + const request = spec.buildRequests(bidRequests, bidderRequest); + expect(storage.getCookie(USER_ID_KEY)).to.be.a('string'); + expect(request.data.user.ext.vads).to.be.a('string'); + }); + + it('should create user id and store it in local storage (if user id does not exist)', function () { + cookiesAreEnabledStub.returns(false); + localStorageIsEnabledStub.returns(true); + + const request = spec.buildRequests(bidRequests, bidderRequest); + expect(storage.getDataFromLocalStorage(USER_ID_KEY)).to.be.a('string'); + expect(request.data.user.ext.vads).to.be.a('string'); + }); + }); }); diff --git a/test/spec/modules/voxBidAdapter_spec.js b/test/spec/modules/voxBidAdapter_spec.js index 923b0465e6c..5f4ada06c65 100644 --- a/test/spec/modules/voxBidAdapter_spec.js +++ b/test/spec/modules/voxBidAdapter_spec.js @@ -1,5 +1,6 @@ import { expect } from 'chai' import { spec } from 'modules/voxBidAdapter.js' +import {config} from 'src/config.js' function getSlotConfigs(mediaTypes, params) { return { @@ -175,6 +176,98 @@ describe('VOX Adapter', function() { expect(bid.transactionId).to.equal('31a58515-3634-4e90-9c96-f86196db1459') }) }) + it('should not set userid if not specified', function () { + const request = spec.buildRequests(validBidRequests, bidderRequest) + const data = JSON.parse(request.data) + data.bidRequests.forEach(bid => { + expect(bid.userId).to.be.undefined + }) + }) + + it('should set userid if specified', function () { + const requests = validBidRequests.map(bid => ({ + ...bid, + userId: { + tdid: 'TDID_USER_ID', + pubcid: 'PUBID_USER_ID' + } + })) + const request = spec.buildRequests(requests, bidderRequest) + const data = JSON.parse(request.data) + data.bidRequests.forEach(bid => { + expect(bid.userId.tdid).to.equal('TDID_USER_ID') + expect(bid.userId.pubcid).to.equal('PUBID_USER_ID') + }) + }) + + it('should not set schain if not specified', function () { + const request = spec.buildRequests(validBidRequests, bidderRequest) + const data = JSON.parse(request.data) + data.bidRequests.forEach(bid => { + expect(bid.schain).to.be.undefined + }) + }) + + it('should set schain if not specified', function () { + const requests = validBidRequests.map(bid => ({ + ...bid, + schain: { + validation: 'strict', + config: { + ver: '1.0' + } + } + })) + const request = spec.buildRequests(requests, bidderRequest) + const data = JSON.parse(request.data) + data.bidRequests.forEach(bid => { + expect(bid.schain.validation).to.equal('strict') + expect(bid.schain.config.ver).to.equal('1.0') + }) + }) + + describe('price floors', function () { + it('should be empty if floors module not configured', function () { + const request = spec.buildRequests(validBidRequests, bidderRequest) + const data = JSON.parse(request.data) + data.bidRequests.forEach(bid => { + expect(bid.floorInfo).to.be.empty + }) + }) + + it('should add correct floor values', function () { + const expectedFloors = [ 2, 2.7, 1.4 ] + const validBidRequests = expectedFloors.map(getBidWithFloor) + const request = spec.buildRequests(validBidRequests, bidderRequest) + const data = JSON.parse(request.data) + expectedFloors.forEach((floor, index) => { + expect(data.bidRequests[index].floorInfo.floor).to.equal(floor) + expect(data.bidRequests[index].floorInfo.currency).to.equal('USD') + }) + }) + + it('should request floor price in adserver currency', function () { + const configCurrency = 'DKK' + config.setConfig({ currency: { adServerCurrency: configCurrency } }) + const request = spec.buildRequests([ getBidWithFloor() ], bidderRequest) + const data = JSON.parse(request.data) + data.bidRequests.forEach(bid => { + expect(bid.floorInfo.currency).to.equal(configCurrency) + }) + }) + + function getBidWithFloor(floor) { + return { + ...validBidRequests[0], + getFloor: ({ currency }) => { + return { + currency: currency, + floor + } + } + } + } + }) describe('GDPR params', function() { describe('when there are not consent management platform', function() { diff --git a/test/spec/modules/vrtcalBidAdapter_spec.js b/test/spec/modules/vrtcalBidAdapter_spec.js index 764db3fa4f0..938934170e9 100644 --- a/test/spec/modules/vrtcalBidAdapter_spec.js +++ b/test/spec/modules/vrtcalBidAdapter_spec.js @@ -29,7 +29,12 @@ describe('vrtcalBidAdapter', function () { 'bidderRequestId': 'br0001', 'auctionId': 'auction0001', 'userIdAsEids': {}, - timeout: 435 + timeout: 435, + + refererInfo: { + page: 'page' + } + } ]; @@ -79,9 +84,12 @@ describe('vrtcalBidAdapter', function () { }); it('pass 3rd party IDs with the request when present', function () { - bidRequests[0].userIdAsEids = createEidsArray({ - tdid: 'TTD_ID_FROM_USER_ID_MODULE' - }); + bidRequests[0].userIdAsEids = [ + { + source: 'adserver.org', + uids: [{id: 'TTD_ID_FROM_USER_ID_MODULE', atype: 1, ext: {rtiPartner: 'TDID'}}] + } + ]; request = spec.buildRequests(bidRequests); expect(request[0].data).to.include(JSON.stringify({ext: {consent: 'gdpr-consent-string', eids: [{source: 'adserver.org', uids: [{id: 'TTD_ID_FROM_USER_ID_MODULE', atype: 1, ext: {rtiPartner: 'TDID'}}]}]}})); @@ -126,4 +134,39 @@ describe('vrtcalBidAdapter', function () { ).to.be.true }) }) + + describe('getUserSyncs', function() { + const syncurl_iframe = 'https://usync.vrtcal.com/i?ssp=1804&synctype=iframe'; + const syncurl_redirect = 'https://usync.vrtcal.com/i?ssp=1804&synctype=redirect'; + + it('base iframe sync pper config', function() { + expect(spec.getUserSyncs({ iframeEnabled: true }, {}, undefined, undefined)).to.deep.equal([{ + type: 'iframe', url: syncurl_iframe + '&us_privacy=&gdpr=0&gdpr_consent=&gpp=&gpp_sid=&surl=' + }]); + }); + + it('base redirect sync per config', function() { + expect(spec.getUserSyncs({ iframeEnabled: false }, {}, undefined, undefined)).to.deep.equal([{ + type: 'image', url: syncurl_redirect + '&us_privacy=&gdpr=0&gdpr_consent=&gpp=&gpp_sid=&surl=' + }]); + }); + + it('pass with ccpa data', function() { + expect(spec.getUserSyncs({ iframeEnabled: true }, {}, undefined, 'ccpa_consent_string', undefined)).to.deep.equal([{ + type: 'iframe', url: syncurl_iframe + '&us_privacy=ccpa_consent_string&gdpr=0&gdpr_consent=&gpp=&gpp_sid=&surl=' + }]); + }); + + it('pass with gdpr data', function() { + expect(spec.getUserSyncs({ iframeEnabled: true }, {}, {gdprApplies: 1, consentString: 'gdpr_consent_string'}, undefined, undefined)).to.deep.equal([{ + type: 'iframe', url: syncurl_iframe + '&us_privacy=&gdpr=1&gdpr_consent=gdpr_consent_string&gpp=&gpp_sid=&surl=' + }]); + }); + + it('pass with gpp data', function() { + expect(spec.getUserSyncs({ iframeEnabled: true }, {}, undefined, undefined, {gppString: 'gpp_consent_string', applicableSections: [1, 5]})).to.deep.equal([{ + type: 'iframe', url: syncurl_iframe + '&us_privacy=&gdpr=0&gdpr_consent=&gpp=gpp_consent_string&gpp_sid=1,5&surl=' + }]); + }); + }) }) diff --git a/test/spec/modules/xeBidAdapter_spec.js b/test/spec/modules/xeBidAdapter_spec.js index 38b334c32c5..914b0cacd71 100644 --- a/test/spec/modules/xeBidAdapter_spec.js +++ b/test/spec/modules/xeBidAdapter_spec.js @@ -41,414 +41,412 @@ defaultRequestVideo.mediaTypes = { skipppable: true } }; +describe('xeBidAdapter', () => { + describe('isBidRequestValid', function () { + it('should return false when request params is missing', function () { + const invalidRequest = deepClone(defaultRequest); + delete invalidRequest.params; + expect(spec.isBidRequestValid(invalidRequest)).to.equal(false); + }); -describe('isBidRequestValid', function () { - it('should return false when request params is missing', function () { - const invalidRequest = deepClone(defaultRequest); - delete invalidRequest.params; - expect(spec.isBidRequestValid(invalidRequest)).to.equal(false); - }); - - it('should return false when required env param is missing', function () { - const invalidRequest = deepClone(defaultRequest); - delete invalidRequest.params.env; - expect(spec.isBidRequestValid(invalidRequest)).to.equal(false); - }); + it('should return false when required env param is missing', function () { + const invalidRequest = deepClone(defaultRequest); + delete invalidRequest.params.env; + expect(spec.isBidRequestValid(invalidRequest)).to.equal(false); + }); - it('should return false when required placement param is missing', function () { - const invalidRequest = deepClone(defaultRequest); - delete invalidRequest.params.placement; - expect(spec.isBidRequestValid(invalidRequest)).to.equal(false); - }); + it('should return false when required placement param is missing', function () { + const invalidRequest = deepClone(defaultRequest); + delete invalidRequest.params.placement; + expect(spec.isBidRequestValid(invalidRequest)).to.equal(false); + }); - it('should return false when video.playerSize is missing', function () { - const invalidRequest = deepClone(defaultRequestVideo); - delete invalidRequest.mediaTypes.video.playerSize; - expect(spec.isBidRequestValid(invalidRequest)).to.equal(false); - }); + it('should return false when video.playerSize is missing', function () { + const invalidRequest = deepClone(defaultRequestVideo); + delete invalidRequest.mediaTypes.video.playerSize; + expect(spec.isBidRequestValid(invalidRequest)).to.equal(false); + }); - it('should return true when required params found', function () { - expect(spec.isBidRequestValid(defaultRequest)).to.equal(true); + it('should return true when required params found', function () { + expect(spec.isBidRequestValid(defaultRequest)).to.equal(true); + }); }); -}); -describe('buildRequests', function () { - beforeEach(function () { - config.resetConfig(); - }); + describe('buildRequests', function () { + beforeEach(function () { + config.resetConfig(); + }); - it('should send request with correct structure', function () { - const request = spec.buildRequests([defaultRequest], {}); - expect(request.method).to.equal('POST'); - expect(request.url).to.equal(ENDPOINT); - expect(request.options).to.have.property('contentType').and.to.equal('application/json'); - expect(request).to.have.property('data'); - }); + it('should send request with correct structure', function () { + const request = spec.buildRequests([defaultRequest], {}); + expect(request.method).to.equal('POST'); + expect(request.url).to.equal(ENDPOINT); + expect(request.options).to.have.property('contentType').and.to.equal('application/json'); + expect(request).to.have.property('data'); + }); - it('should build basic request structure', function () { - const request = JSON.parse(spec.buildRequests([defaultRequest], {}).data)[0]; - expect(request).to.have.property('bidId').and.to.equal(defaultRequest.bidId); - expect(request).to.have.property('auctionId').and.to.equal(defaultRequest.auctionId); - expect(request).to.have.property('transactionId').and.to.equal(defaultRequest.ortb2Imp.ext.tid); - expect(request).to.have.property('tz').and.to.equal(new Date().getTimezoneOffset()); - expect(request).to.have.property('bc').and.to.equal(1); - expect(request).to.have.property('floor').and.to.equal(null); - expect(request).to.have.property('banner').and.to.deep.equal({ sizes: [[300, 250], [300, 200]] }); - expect(request).to.have.property('gdprApplies').and.to.equal(0); - expect(request).to.have.property('consentString').and.to.equal(''); - expect(request).to.have.property('userEids').and.to.deep.equal([]); - expect(request).to.have.property('usPrivacy').and.to.equal(''); - expect(request).to.have.property('coppa').and.to.equal(0); - expect(request).to.have.property('sizes').and.to.deep.equal(['300x250', '300x200']); - expect(request).to.have.property('ext').and.to.deep.equal({}); - expect(request).to.have.property('env').and.to.deep.equal({ - env: 'xe', - placement: 'test-banner' - }); - expect(request).to.have.property('device').and.to.deep.equal({ - ua: navigator.userAgent, - lang: navigator.language + it('should build basic request structure', function () { + const request = JSON.parse(spec.buildRequests([defaultRequest], {}).data)[0]; + expect(request).to.have.property('bidId').and.to.equal(defaultRequest.bidId); + expect(request).to.have.property('auctionId').and.to.equal(defaultRequest.auctionId); + expect(request).to.have.property('transactionId').and.to.equal(defaultRequest.ortb2Imp.ext.tid); + expect(request).to.have.property('tz').and.to.equal(new Date().getTimezoneOffset()); + expect(request).to.have.property('bc').and.to.equal(1); + expect(request).to.have.property('floor').and.to.equal(null); + expect(request).to.have.property('banner').and.to.deep.equal({ sizes: [[300, 250], [300, 200]] }); + expect(request).to.have.property('gdprApplies').and.to.equal(0); + expect(request).to.have.property('consentString').and.to.equal(''); + expect(request).to.have.property('userEids').and.to.deep.equal([]); + expect(request).to.have.property('usPrivacy').and.to.equal(''); + expect(request).to.have.property('coppa').and.to.equal(0); + expect(request).to.have.property('sizes').and.to.deep.equal(['300x250', '300x200']); + expect(request).to.have.property('ext').and.to.deep.equal({}); + expect(request).to.have.property('env').and.to.deep.equal({ + env: 'xe', + placement: 'test-banner' + }); + expect(request).to.have.property('device').and.to.deep.equal({ + ua: navigator.userAgent, + lang: navigator.language + }); }); - }); - it('should build request with schain', function () { - const schainRequest = deepClone(defaultRequest); - schainRequest.schain = { - validation: 'strict', - config: { - ver: '1.0' - } - }; - const request = JSON.parse(spec.buildRequests([schainRequest], {}).data)[0]; - expect(request).to.have.property('schain').and.to.deep.equal({ - validation: 'strict', - config: { - ver: '1.0' - } + it('should build request with schain', function () { + const schainRequest = deepClone(defaultRequest); + schainRequest.schain = { + validation: 'strict', + config: { + ver: '1.0' + } + }; + const request = JSON.parse(spec.buildRequests([schainRequest], {}).data)[0]; + expect(request).to.have.property('schain').and.to.deep.equal({ + validation: 'strict', + config: { + ver: '1.0' + } + }); }); - }); - it('should build request with location', function () { - const bidderRequest = { - refererInfo: { - page: 'page', - location: 'location', - domain: 'domain', - ref: 'ref', - isAmp: false - } - }; - const request = JSON.parse(spec.buildRequests([defaultRequest], bidderRequest).data)[0]; - expect(request).to.have.property('location'); - const location = request.location; - expect(location).to.have.property('page').and.to.equal('page'); - expect(location).to.have.property('location').and.to.equal('location'); - expect(location).to.have.property('domain').and.to.equal('domain'); - expect(location).to.have.property('ref').and.to.equal('ref'); - expect(location).to.have.property('isAmp').and.to.equal(false); - }); + it('should build request with location', function () { + const bidderRequest = { + refererInfo: { + page: 'page', + location: 'location', + domain: 'domain', + ref: 'ref', + isAmp: false + } + }; + const request = JSON.parse(spec.buildRequests([defaultRequest], bidderRequest).data)[0]; + expect(request).to.have.property('location'); + const location = request.location; + expect(location).to.have.property('page').and.to.equal('page'); + expect(location).to.have.property('location').and.to.equal('location'); + expect(location).to.have.property('domain').and.to.equal('domain'); + expect(location).to.have.property('ref').and.to.equal('ref'); + expect(location).to.have.property('isAmp').and.to.equal(false); + }); - it('should build request with ortb2 info', function () { - const ortb2Request = deepClone(defaultRequest); - ortb2Request.ortb2 = { - site: { - name: 'name' - } - }; - const request = JSON.parse(spec.buildRequests([ortb2Request], {}).data)[0]; - expect(request).to.have.property('ortb2').and.to.deep.equal({ - site: { - name: 'name' - } + it('should build request with ortb2 info', function () { + const ortb2Request = deepClone(defaultRequest); + ortb2Request.ortb2 = { + site: { + name: 'name' + } + }; + const request = JSON.parse(spec.buildRequests([ortb2Request], {}).data)[0]; + expect(request).to.have.property('ortb2').and.to.deep.equal({ + site: { + name: 'name' + } + }); }); - }); - it('should build request with ortb2Imp info', function () { - const ortb2ImpRequest = deepClone(defaultRequest); - ortb2ImpRequest.ortb2Imp = { - ext: { - data: { - pbadslot: 'home1', - adUnitSpecificAttribute: '1' + it('should build request with ortb2Imp info', function () { + const ortb2ImpRequest = deepClone(defaultRequest); + ortb2ImpRequest.ortb2Imp = { + ext: { + data: { + pbadslot: 'home1', + adUnitSpecificAttribute: '1' + } } - } - }; - const request = JSON.parse(spec.buildRequests([ortb2ImpRequest], {}).data)[0]; - expect(request).to.have.property('ortb2Imp').and.to.deep.equal({ - ext: { - data: { - pbadslot: 'home1', - adUnitSpecificAttribute: '1' + }; + const request = JSON.parse(spec.buildRequests([ortb2ImpRequest], {}).data)[0]; + expect(request).to.have.property('ortb2Imp').and.to.deep.equal({ + ext: { + data: { + pbadslot: 'home1', + adUnitSpecificAttribute: '1' + } } - } + }); }); - }); - it('should build request with valid bidfloor', function () { - const bfRequest = deepClone(defaultRequest); - bfRequest.getFloor = () => ({ floor: 5, currency: 'USD' }); - const request = JSON.parse(spec.buildRequests([bfRequest], {}).data)[0]; - expect(request).to.have.property('floor').and.to.equal(5); - }); + it('should build request with valid bidfloor', function () { + const bfRequest = deepClone(defaultRequest); + bfRequest.getFloor = () => ({ floor: 5, currency: 'USD' }); + const request = JSON.parse(spec.buildRequests([bfRequest], {}).data)[0]; + expect(request).to.have.property('floor').and.to.equal(5); + }); - it('should build request with gdpr consent data if applies', function () { - const bidderRequest = { - gdprConsent: { - gdprApplies: true, - consentString: 'qwerty' - } - }; - const request = JSON.parse(spec.buildRequests([defaultRequest], bidderRequest).data)[0]; - expect(request).to.have.property('gdprApplies').and.equals(1); - expect(request).to.have.property('consentString').and.equals('qwerty'); - }); + it('should build request with gdpr consent data if applies', function () { + const bidderRequest = { + gdprConsent: { + gdprApplies: true, + consentString: 'qwerty' + } + }; + const request = JSON.parse(spec.buildRequests([defaultRequest], bidderRequest).data)[0]; + expect(request).to.have.property('gdprApplies').and.equals(1); + expect(request).to.have.property('consentString').and.equals('qwerty'); + }); - it('should build request with usp consent data if applies', function () { - const bidderRequest = { - uspConsent: '1YA-' - }; - const request = JSON.parse(spec.buildRequests([defaultRequest], bidderRequest).data)[0]; - expect(request).to.have.property('usPrivacy').and.equals('1YA-'); - }); + it('should build request with usp consent data if applies', function () { + const bidderRequest = { + uspConsent: '1YA-' + }; + const request = JSON.parse(spec.buildRequests([defaultRequest], bidderRequest).data)[0]; + expect(request).to.have.property('usPrivacy').and.equals('1YA-'); + }); - it('should build request with coppa 1', function () { - config.setConfig({ - coppa: true + it('should build request with coppa 1', function () { + config.setConfig({ + coppa: true + }); + const request = JSON.parse(spec.buildRequests([defaultRequest], {}).data)[0]; + expect(request).to.have.property('coppa').and.equals(1); }); - const request = JSON.parse(spec.buildRequests([defaultRequest], {}).data)[0]; - expect(request).to.have.property('coppa').and.equals(1); - }); - it('should build request with extended ids', function () { - const idRequest = deepClone(defaultRequest); - idRequest.userIdAsEids = createEidsArray({ - tdid: 'TTD_ID_FROM_USER_ID_MODULE', - pubcid: 'pubCommonId_FROM_USER_ID_MODULE' - }); - const request = JSON.parse(spec.buildRequests([idRequest], {}).data)[0]; - expect(request).to.have.property('userEids').and.deep.equal([ - { source: 'adserver.org', uids: [ { id: 'TTD_ID_FROM_USER_ID_MODULE', atype: 1, ext: { rtiPartner: 'TDID' } } ] }, - { source: 'pubcid.org', uids: [ { id: 'pubCommonId_FROM_USER_ID_MODULE', atype: 1 } ] } - ]); - }); + it('should build request with extended ids', function () { + const idRequest = deepClone(defaultRequest); + idRequest.userIdAsEids = [ + { source: 'adserver.org', uids: [ { id: 'TTD_ID_FROM_USER_ID_MODULE', atype: 1, ext: { rtiPartner: 'TDID' } } ] }, + { source: 'pubcid.org', uids: [ { id: 'pubCommonId_FROM_USER_ID_MODULE', atype: 1 } ] } + ]; + const request = JSON.parse(spec.buildRequests([idRequest], {}).data)[0]; + expect(request).to.have.property('userEids').and.deep.equal(idRequest.userIdAsEids); + }); - it('should build request with video', function () { - const request = JSON.parse(spec.buildRequests([defaultRequestVideo], {}).data)[0]; - expect(request).to.have.property('video').and.to.deep.equal({ - playerSize: [640, 480], - context: 'instream', - skipppable: true + it('should build request with video', function () { + const request = JSON.parse(spec.buildRequests([defaultRequestVideo], {}).data)[0]; + expect(request).to.have.property('video').and.to.deep.equal({ + playerSize: [640, 480], + context: 'instream', + skipppable: true + }); + expect(request).to.have.property('sizes').and.to.deep.equal(['640x480']); }); - expect(request).to.have.property('sizes').and.to.deep.equal(['640x480']); - }); -}); - -describe('interpretResponse', function () { - it('should return empty bids', function () { - const serverResponse = { - body: { - data: null - } - }; - - const invalidResponse = spec.interpretResponse(serverResponse, {}); - expect(invalidResponse).to.be.an('array').that.is.empty; }); - it('should interpret valid response', function () { - const serverResponse = { - body: { - data: [{ - requestId: 'qwerty', - cpm: 1, - currency: 'USD', - width: 300, - height: 250, - ttl: 600, - meta: { - advertiserDomains: ['xe.works'] - }, - ext: { - pixels: [ - [ 'iframe', 'surl1' ], - [ 'image', 'surl2' ], - ] - } - }] - } - }; - - const validResponse = spec.interpretResponse(serverResponse, { bidderRequest: defaultRequest }); - const bid = validResponse[0]; - expect(validResponse).to.be.an('array').that.is.not.empty; - expect(bid.requestId).to.equal('qwerty'); - expect(bid.cpm).to.equal(1); - expect(bid.currency).to.equal('USD'); - expect(bid.width).to.equal(300); - expect(bid.height).to.equal(250); - expect(bid.ttl).to.equal(600); - expect(bid.meta).to.deep.equal({ advertiserDomains: ['xe.works'] }); - }); + describe('interpretResponse', function () { + it('should return empty bids', function () { + const serverResponse = { + body: { + data: null + } + }; - it('should interpret valid banner response', function () { - const serverResponse = { - body: { - data: [{ - requestId: 'qwerty', - cpm: 1, - currency: 'USD', - width: 300, - height: 250, - ttl: 600, - mediaType: 'banner', - creativeId: 'xe-demo-banner', - ad: 'ad', - meta: {} - }] - } - }; - - const validResponseBanner = spec.interpretResponse(serverResponse, { bidderRequest: defaultRequest }); - const bid = validResponseBanner[0]; - expect(validResponseBanner).to.be.an('array').that.is.not.empty; - expect(bid.mediaType).to.equal('banner'); - expect(bid.creativeId).to.equal('xe-demo-banner'); - expect(bid.ad).to.equal('ad'); - }); + const invalidResponse = spec.interpretResponse(serverResponse, {}); + expect(invalidResponse).to.be.an('array').that.is.empty; + }); - it('should interpret valid video response', function () { - const serverResponse = { - body: { - data: [{ - requestId: 'qwerty', - cpm: 1, - currency: 'USD', - width: 600, - height: 480, - ttl: 600, - mediaType: 'video', - creativeId: 'xe-demo-video', - ad: 'vast-xml', - meta: {} - }] - } - }; - - const validResponseBanner = spec.interpretResponse(serverResponse, { bidderRequest: defaultRequestVideo }); - const bid = validResponseBanner[0]; - expect(validResponseBanner).to.be.an('array').that.is.not.empty; - expect(bid.mediaType).to.equal('video'); - expect(bid.creativeId).to.equal('xe-demo-video'); - expect(bid.ad).to.equal('vast-xml'); - }); -}); + it('should interpret valid response', function () { + const serverResponse = { + body: { + data: [{ + requestId: 'qwerty', + cpm: 1, + currency: 'USD', + width: 300, + height: 250, + ttl: 600, + meta: { + advertiserDomains: ['xe.works'] + }, + ext: { + pixels: [ + [ 'iframe', 'surl1' ], + [ 'image', 'surl2' ], + ] + } + }] + } + }; + + const validResponse = spec.interpretResponse(serverResponse, { bidderRequest: defaultRequest }); + const bid = validResponse[0]; + expect(validResponse).to.be.an('array').that.is.not.empty; + expect(bid.requestId).to.equal('qwerty'); + expect(bid.cpm).to.equal(1); + expect(bid.currency).to.equal('USD'); + expect(bid.width).to.equal(300); + expect(bid.height).to.equal(250); + expect(bid.ttl).to.equal(600); + expect(bid.meta).to.deep.equal({ advertiserDomains: ['xe.works'] }); + }); -describe('getUserSyncs', function () { - it('shoukd handle no params', function () { - const opts = spec.getUserSyncs({}, []); - expect(opts).to.be.an('array').that.is.empty; - }); + it('should interpret valid banner response', function () { + const serverResponse = { + body: { + data: [{ + requestId: 'qwerty', + cpm: 1, + currency: 'USD', + width: 300, + height: 250, + ttl: 600, + mediaType: 'banner', + creativeId: 'xe-demo-banner', + ad: 'ad', + meta: {} + }] + } + }; + + const validResponseBanner = spec.interpretResponse(serverResponse, { bidderRequest: defaultRequest }); + const bid = validResponseBanner[0]; + expect(validResponseBanner).to.be.an('array').that.is.not.empty; + expect(bid.mediaType).to.equal('banner'); + expect(bid.creativeId).to.equal('xe-demo-banner'); + expect(bid.ad).to.equal('ad'); + }); - it('should return empty if sync is not allowed', function () { - const opts = spec.getUserSyncs({iframeEnabled: false, pixelEnabled: false}); - expect(opts).to.be.an('array').that.is.empty; + it('should interpret valid video response', function () { + const serverResponse = { + body: { + data: [{ + requestId: 'qwerty', + cpm: 1, + currency: 'USD', + width: 600, + height: 480, + ttl: 600, + mediaType: 'video', + creativeId: 'xe-demo-video', + ad: 'vast-xml', + meta: {} + }] + } + }; + + const validResponseBanner = spec.interpretResponse(serverResponse, { bidderRequest: defaultRequestVideo }); + const bid = validResponseBanner[0]; + expect(validResponseBanner).to.be.an('array').that.is.not.empty; + expect(bid.mediaType).to.equal('video'); + expect(bid.creativeId).to.equal('xe-demo-video'); + expect(bid.ad).to.equal('vast-xml'); + }); }); - it('should allow iframe sync', function () { - const opts = spec.getUserSyncs({iframeEnabled: true, pixelEnabled: false}, [{ - body: { - data: [{ - requestId: 'qwerty', - ext: { - pixels: [ - [ 'iframe', 'surl1?a=b' ], - [ 'image', 'surl2?a=b' ], - ] - } - }] - } - }]); - expect(opts.length).to.equal(1); - expect(opts[0].type).to.equal('iframe'); - expect(opts[0].url).to.equal('surl1?a=b&us_privacy=&gdpr=0&gdpr_consent='); - }); + describe('getUserSyncs', function () { + it('shoukd handle no params', function () { + const opts = spec.getUserSyncs({}, []); + expect(opts).to.be.an('array').that.is.empty; + }); - it('should allow pixel sync', function () { - const opts = spec.getUserSyncs({iframeEnabled: false, pixelEnabled: true}, [{ - body: { - data: [{ - requestId: 'qwerty', - ext: { - pixels: [ - [ 'iframe', 'surl1?a=b' ], - [ 'image', 'surl2?a=b' ], - ] - } - }] - } - }]); - expect(opts.length).to.equal(1); - expect(opts[0].type).to.equal('image'); - expect(opts[0].url).to.equal('surl2?a=b&us_privacy=&gdpr=0&gdpr_consent='); - }); + it('should return empty if sync is not allowed', function () { + const opts = spec.getUserSyncs({iframeEnabled: false, pixelEnabled: false}); + expect(opts).to.be.an('array').that.is.empty; + }); - it('should allow pixel sync and parse consent params', function () { - const opts = spec.getUserSyncs({iframeEnabled: false, pixelEnabled: true}, [{ - body: { - data: [{ - requestId: 'qwerty', - ext: { - pixels: [ - [ 'iframe', 'surl1?a=b' ], - [ 'image', 'surl2?a=b' ], - ] - } - }] - } - }], { - gdprApplies: 1, - consentString: '1YA-' - }); - expect(opts.length).to.equal(1); - expect(opts[0].type).to.equal('image'); - expect(opts[0].url).to.equal('surl2?a=b&us_privacy=&gdpr=1&gdpr_consent=1YA-'); - }); -}); + it('should allow iframe sync', function () { + const opts = spec.getUserSyncs({iframeEnabled: true, pixelEnabled: false}, [{ + body: { + data: [{ + requestId: 'qwerty', + ext: { + pixels: [ + [ 'iframe', 'surl1?a=b' ], + [ 'image', 'surl2?a=b' ], + ] + } + }] + } + }]); + expect(opts.length).to.equal(1); + expect(opts[0].type).to.equal('iframe'); + expect(opts[0].url).to.equal('surl1?a=b&us_privacy=&gdpr=0&gdpr_consent='); + }); -describe('getBidFloor', function () { - it('should return null when getFloor is not a function', () => { - const bid = { getFloor: 2 }; - const result = getBidFloor(bid); - expect(result).to.be.null; - }); + it('should allow pixel sync', function () { + const opts = spec.getUserSyncs({iframeEnabled: false, pixelEnabled: true}, [{ + body: { + data: [{ + requestId: 'qwerty', + ext: { + pixels: [ + [ 'iframe', 'surl1?a=b' ], + [ 'image', 'surl2?a=b' ], + ] + } + }] + } + }]); + expect(opts.length).to.equal(1); + expect(opts[0].type).to.equal('image'); + expect(opts[0].url).to.equal('surl2?a=b&us_privacy=&gdpr=0&gdpr_consent='); + }); - it('should return null when getFloor doesnt return an object', () => { - const bid = { getFloor: () => 2 }; - const result = getBidFloor(bid); - expect(result).to.be.null; + it('should allow pixel sync and parse consent params', function () { + const opts = spec.getUserSyncs({iframeEnabled: false, pixelEnabled: true}, [{ + body: { + data: [{ + requestId: 'qwerty', + ext: { + pixels: [ + [ 'iframe', 'surl1?a=b' ], + [ 'image', 'surl2?a=b' ], + ] + } + }] + } + }], { + gdprApplies: 1, + consentString: '1YA-' + }); + expect(opts.length).to.equal(1); + expect(opts[0].type).to.equal('image'); + expect(opts[0].url).to.equal('surl2?a=b&us_privacy=&gdpr=1&gdpr_consent=1YA-'); + }); }); - it('should return null when floor is not a number', () => { - const bid = { - getFloor: () => ({ floor: 'string', currency: 'USD' }) - }; - const result = getBidFloor(bid); - expect(result).to.be.null; - }); + describe('getBidFloor', function () { + it('should return null when getFloor is not a function', () => { + const bid = { getFloor: 2 }; + const result = getBidFloor(bid); + expect(result).to.be.null; + }); - it('should return null when currency is not USD', () => { - const bid = { - getFloor: () => ({ floor: 5, currency: 'EUR' }) - }; - const result = getBidFloor(bid); - expect(result).to.be.null; - }); + it('should return null when getFloor doesnt return an object', () => { + const bid = { getFloor: () => 2 }; + const result = getBidFloor(bid); + expect(result).to.be.null; + }); - it('should return floor value when everything is correct', () => { - const bid = { - getFloor: () => ({ floor: 5, currency: 'USD' }) - }; - const result = getBidFloor(bid); - expect(result).to.equal(5); + it('should return null when floor is not a number', () => { + const bid = { + getFloor: () => ({ floor: 'string', currency: 'USD' }) + }; + const result = getBidFloor(bid); + expect(result).to.be.null; + }); + + it('should return null when currency is not USD', () => { + const bid = { + getFloor: () => ({ floor: 5, currency: 'EUR' }) + }; + const result = getBidFloor(bid); + expect(result).to.be.null; + }); + + it('should return floor value when everything is correct', () => { + const bid = { + getFloor: () => ({ floor: 5, currency: 'USD' }) + }; + const result = getBidFloor(bid); + expect(result).to.equal(5); + }); }); -}); +}) diff --git a/test/spec/modules/yahoosspBidAdapter_spec.js b/test/spec/modules/yahoosspBidAdapter_spec.js index b0030c89371..40dc2b3c63b 100644 --- a/test/spec/modules/yahoosspBidAdapter_spec.js +++ b/test/spec/modules/yahoosspBidAdapter_spec.js @@ -3,7 +3,6 @@ import { config } from 'src/config.js'; import { BANNER, VIDEO } from 'src/mediaTypes.js'; import { spec } from 'modules/yahoosspBidAdapter.js'; import {createEidsArray} from '../../../modules/userId/eids'; -import {deepClone} from '../../../src/utils'; const DEFAULT_BID_ID = '84ab500420319d'; const DEFAULT_BID_DCN = '2093845709823475'; @@ -13,18 +12,20 @@ const DEFAULT_AD_UNIT_CODE = '/19968336/header-bid-tag-1'; const DEFAULT_AD_UNIT_TYPE = 'banner'; const DEFAULT_PARAMS_BID_OVERRIDE = {}; const DEFAULT_VIDEO_CONTEXT = 'instream'; -const ADAPTER_VERSION = '1.0.2'; +const ADAPTER_VERSION = '1.1.0'; +const DEFAULT_BIDDER_CODE = 'yahooAds'; +const VALID_BIDDER_CODES = [DEFAULT_BIDDER_CODE, 'yahoossp', 'yahooAdvertising']; const PREBID_VERSION = '$prebid.version$'; const INTEGRATION_METHOD = 'prebid.js'; // Utility functions -const generateBidRequest = ({bidId, pos, adUnitCode, adUnitType, bidOverrideObject, videoContext, pubIdMode, ortb2}) => { +const generateBidRequest = ({bidderCode, bidId, pos, adUnitCode, adUnitType, bidOverrideObject, videoContext, pubIdMode, ortb2}) => { const bidRequest = { adUnitCode, auctionId: 'b06c5141-fe8f-4cdf-9d7d-54415490a917', bidId, bidderRequestsCount: 1, - bidder: 'yahoossp', + bidder: bidderCode, bidderRequestId: '7101db09af0db2', bidderWinsCount: 0, mediaTypes: {}, @@ -79,7 +80,7 @@ let generateBidderRequest = (bidRequestArray, adUnitCode, ortb2 = {}) => { adUnitCode: adUnitCode || 'default-adUnitCode', auctionId: 'd4c83a3b-18e4-4208-b98b-63848449c7aa', auctionStart: new Date().getTime(), - bidderCode: 'yahoossp', + bidderCode: bidRequestArray[0].bidder, bidderRequestId: '112f1c7c5d399a', bids: bidRequestArray, refererInfo: { @@ -107,8 +108,9 @@ let generateBidderRequest = (bidRequestArray, adUnitCode, ortb2 = {}) => { return bidderRequest; }; -const generateBuildRequestMock = ({bidId, pos, adUnitCode, adUnitType, bidOverrideObject, videoContext, pubIdMode, ortb2}) => { +const generateBuildRequestMock = ({bidderCode, bidId, pos, adUnitCode, adUnitType, bidOverrideObject, videoContext, pubIdMode, ortb2}) => { const bidRequestConfig = { + bidderCode: bidderCode || DEFAULT_BIDDER_CODE, bidId: bidId || DEFAULT_BID_ID, pos: pos || DEFAULT_BID_POS, adUnitCode: adUnitCode || DEFAULT_AD_UNIT_CODE, @@ -174,21 +176,28 @@ const generateResponseMock = (admPayloadType, vastVersion, videoContext) => { seatbid: [{ bid: [ bidResponse ], seat: 13107 }] } }; - const { validBidRequests, bidderRequest } = generateBuildRequestMock({adUnitType: admPayloadType, videoContext: videoContext}); + const { validBidRequests, bidderRequest } = generateBuildRequestMock({adUnitType: admPayloadType, videoContext}); const data = spec.buildRequests(validBidRequests, bidderRequest)[0].data; - return {serverResponse, data, bidderRequest}; } // Unit tests -describe('YahooSSP Bid Adapter:', () => { +describe('Yahoo Advertising Bid Adapter:', () => { + beforeEach(() => { + config.resetConfig(); + }); + describe('Validate basic properties', () => { it('should define the correct bidder code', () => { - expect(spec.code).to.equal('yahoossp') + expect(spec.code).to.equal('yahooAds'); + }); + + it('should define the correct bidder aliases', () => { + expect(spec.aliases).to.deep.equal(['yahoossp', 'yahooAdvertising']); }); it('should define the correct vendor ID', () => { - expect(spec.gvlid).to.equal(25) + expect(spec.gvlid).to.equal(25); }); }); @@ -271,33 +280,62 @@ describe('YahooSSP Bid Adapter:', () => { }); describe('user consent parameters are updated', () => { - let syncOptions = { + const syncOptions = { iframeEnabled: true, pixelEnabled: true }; - let pixelObjects = spec.getUserSyncs( - syncOptions, - SERVER_RESPONSES, - bidderRequest.gdprConsent, - bidderRequest.uspConsent, - bidderRequest.gppConsent - ); - pixelObjects.forEach(pixelObject => { - let url = pixelObject.url; - let urlParams = new URL(url).searchParams; - const expectedParams = { - 'baz': 'true', - 'gdpr_consent': bidderRequest.gdprConsent.consentString, - 'gdpr': bidderRequest.gdprConsent.gdprApplies ? '1' : '0', - 'us_privacy': bidderRequest.uspConsent, - 'gpp': bidderRequest.gppConsent.gppString, - 'gpp_sid': Array.isArray(bidderRequest.gppConsent.applicableSections) ? bidderRequest.gppConsent.applicableSections.join(',') : '' - } - for (const [key, value] of Object.entries(expectedParams)) { - it(`Updates the ${key} consent param in user sync URL ${url}`, () => { - expect(urlParams.get(key)).to.equal(value); - }); - }; + describe('when all consent data is set', () => { + const pixelObjects = spec.getUserSyncs( + syncOptions, + SERVER_RESPONSES, + bidderRequest.gdprConsent, + bidderRequest.uspConsent, + bidderRequest.gppConsent + ); + pixelObjects.forEach(pixelObject => { + let url = pixelObject.url; + let urlParams = new URL(url).searchParams; + const expectedParams = { + 'baz': 'true', + 'gdpr_consent': bidderRequest.gdprConsent.consentString, + 'gdpr': bidderRequest.gdprConsent.gdprApplies ? '1' : '0', + 'us_privacy': bidderRequest.uspConsent, + 'gpp': bidderRequest.gppConsent.gppString, + 'gpp_sid': Array.isArray(bidderRequest.gppConsent.applicableSections) ? bidderRequest.gppConsent.applicableSections.join(',') : '' + } + for (const [key, value] of Object.entries(expectedParams)) { + it(`Updates the ${key} consent param in user sync URL ${url}`, () => { + expect(urlParams.get(key)).to.equal(value); + }); + }; + }); + }); + + describe('when no consent data is set', () => { + const pixelObjects = spec.getUserSyncs( + syncOptions, + SERVER_RESPONSES, + undefined, + undefined, + undefined + ); + pixelObjects.forEach(pixelObject => { + let url = pixelObject.url; + let urlParams = new URL(url).searchParams; + const expectedParams = { + 'baz': 'true', + 'gdpr_consent': '', + 'gdpr': '0', + 'us_privacy': '', + 'gpp': '', + 'gpp_sid': '' + } + for (const [key, value] of Object.entries(expectedParams)) { + it(`Updates the ${key} consent param in user sync URL ${url}`, () => { + expect(urlParams.get(key)).to.equal(value); + }); + }; + }); }); }); }); @@ -647,9 +685,9 @@ describe('YahooSSP Bid Adapter:', () => { }; const { validBidRequests, bidderRequest } = generateBuildRequestMock({ortb2}); const data = spec.buildRequests(validBidRequests, bidderRequest)[0].data; - expect(data.user[param]).to.be.a('object'); - expect(data.user[param]).to.be.deep.include({[param]: {a: '123', b: '456'}}); - config.setConfig({ortb2: {}}); + const user = data.user; + expect(user[param]).to.be.a('object'); + expect(user[param]).to.be.deep.include({[param]: {a: '123', b: '456'}}); }); }); @@ -669,12 +707,14 @@ describe('YahooSSP Bid Adapter:', () => { }; const { validBidRequests, bidderRequest } = generateBuildRequestMock({ortb2}); const data = spec.buildRequests(validBidRequests, bidderRequest)[0].data; - expect(data.user.data[0][param]).to.exist; - expect(data.user.data[0][param]).to.be.a('string'); - expect(data.user.data[0][param]).to.be.equal(ortb2.user.data[0][param]); - expect(data.site.content.data[0][param]).to.exist; - expect(data.site.content.data[0][param]).to.be.a('string'); - expect(data.site.content.data[0][param]).to.be.equal(ortb2.site.content.data[0][param]); + const user = data.user; + const site = data.site; + expect(user.data[0][param]).to.exist; + expect(user.data[0][param]).to.be.a('string'); + expect(user.data[0][param]).to.be.equal(ortb2.user.data[0][param]); + expect(site.content.data[0][param]).to.exist; + expect(site.content.data[0][param]).to.be.a('string'); + expect(site.content.data[0][param]).to.be.equal(ortb2.site.content.data[0][param]); }); }); @@ -688,9 +728,10 @@ describe('YahooSSP Bid Adapter:', () => { }; const { validBidRequests, bidderRequest } = generateBuildRequestMock({ortb2}); const data = spec.buildRequests(validBidRequests, bidderRequest)[0].data; - expect(data.user.data[0][param]).to.exist; - expect(data.user.data[0][param]).to.be.a('array'); - expect(data.user.data[0][param]).to.be.equal(ortb2.user.data[0][param]); + const user = data.user; + expect(user.data[0][param]).to.exist; + expect(user.data[0][param]).to.be.a('array'); + expect(user.data[0][param]).to.be.equal(ortb2.user.data[0][param]); }); }); @@ -704,10 +745,10 @@ describe('YahooSSP Bid Adapter:', () => { }; const { validBidRequests, bidderRequest } = generateBuildRequestMock({ortb2}); const data = spec.buildRequests(validBidRequests, bidderRequest)[0].data; - expect(data.user.data[0][param]).to.exist; - expect(data.user.data[0][param]).to.be.a('object'); - expect(data.user.data[0][param]).to.be.equal(ortb2.user.data[0][param]); - config.setConfig({ortb2: {}}); + const user = data.user; + expect(user.data[0][param]).to.exist; + expect(user.data[0][param]).to.be.a('object'); + expect(user.data[0][param]).to.be.equal(ortb2.user.data[0][param]); }); }); @@ -815,26 +856,33 @@ describe('YahooSSP Bid Adapter:', () => { }); describe('Endpoint & Impression Request Mode:', () => { - it('should route request to config override endpoint', () => { - const { validBidRequests, bidderRequest } = generateBuildRequestMock({}); - const testOverrideEndpoint = 'http://foo.bar.baz.com/bidderRequest'; + afterEach(() => { config.setConfig({ - yahoossp: { - endpoint: testOverrideEndpoint + yahooAds: { + singleRequestMode: undefined } }); - const response = spec.buildRequests(validBidRequests, bidderRequest)[0]; - expect(response).to.deep.include( - { - method: 'POST', - url: testOverrideEndpoint - }); }); - it('should route request to /bidRequest endpoint when dcn & pos present', () => { - config.setConfig({ - yahoossp: {} + VALID_BIDDER_CODES.forEach(bidderCode => { + it(`should route request to config override endpoint for ${bidderCode} override config`, () => { + const { validBidRequests, bidderRequest } = generateBuildRequestMock({bidderCode}); + const testOverrideEndpoint = 'http://foo.bar.baz.com/bidderRequest'; + const cfg = {}; + cfg[bidderCode] = { + endpoint: testOverrideEndpoint + }; + config.setConfig(cfg); + const response = spec.buildRequests(validBidRequests, bidderRequest)[0]; + expect(response).to.deep.include( + { + method: 'POST', + url: testOverrideEndpoint + }); }); + }); + + it('should route request to /bidRequest endpoint when dcn & pos present', () => { const { validBidRequests, bidderRequest } = generateBuildRequestMock({}); const response = spec.buildRequests(validBidRequests, bidderRequest); expect(response[0]).to.deep.include({ @@ -862,15 +910,15 @@ describe('YahooSSP Bid Adapter:', () => { bidderRequest.bids = validBidRequests; config.setConfig({ - yahoossp: { + yahooAds: { singleRequestMode: true } }); - const data = spec.buildRequests(validBidRequests, bidderRequest).data; - expect(data.imp).to.be.an('array').with.lengthOf(2); + const responsePayload = spec.buildRequests(validBidRequests, bidderRequest)[0].data; + expect(responsePayload.imp).to.be.an('array').with.lengthOf(2); - expect(data.imp[0]).to.deep.include({ + expect(responsePayload.imp[0]).to.deep.include({ id: DEFAULT_BID_ID, ext: { pos: DEFAULT_BID_POS, @@ -878,7 +926,7 @@ describe('YahooSSP Bid Adapter:', () => { } }); - expect(data.imp[1]).to.deep.include({ + expect(responsePayload.imp[1]).to.deep.include({ id: BID_ID_2, ext: { pos: BID_POS_2, @@ -896,8 +944,9 @@ describe('YahooSSP Bid Adapter:', () => { it('buildRequests(): should return an array with the correct amount of request objects', () => { const { validBidRequests, bidderRequest } = generateBuildRequestMock({}); - const response = spec.buildRequests(validBidRequests, bidderRequest).bidderRequest; - expect(response.bids).to.be.an('array').to.have.lengthOf(1); + const reqs = spec.buildRequests(validBidRequests, bidderRequest); + expect(reqs).to.be.an('array').to.have.lengthOf(1); + expect(reqs[0]).to.be.an('object').that.has.keys('method', 'url', 'data', 'options', 'bidderRequest'); }); }); @@ -905,7 +954,7 @@ describe('YahooSSP Bid Adapter:', () => { it('should return request objects with the relevant custom headers and content type declaration', () => { const { validBidRequests, bidderRequest } = generateBuildRequestMock({}); bidderRequest.gdprConsent.gdprApplies = false; - const options = spec.buildRequests(validBidRequests, bidderRequest).options; + const options = spec.buildRequests(validBidRequests, bidderRequest)[0].options; expect(options).to.deep.equal( { contentType: 'application/json', @@ -920,22 +969,8 @@ describe('YahooSSP Bid Adapter:', () => { describe('User data', () => { it('should set the allowed sources user eids', () => { const { validBidRequests, bidderRequest } = generateBuildRequestMock({}); - validBidRequests[0].userIdAsEids = createEidsArray({ - admixerId: 'admixerId_FROM_USER_ID_MODULE', - adtelligentId: 'adtelligentId_FROM_USER_ID_MODULE', - amxId: 'amxId_FROM_USER_ID_MODULE', - britepoolid: 'britepoolid_FROM_USER_ID_MODULE', - deepintentId: 'deepintentId_FROM_USER_ID_MODULE', - publinkId: 'publinkId_FROM_USER_ID_MODULE', - intentIqId: 'intentIqId_FROM_USER_ID_MODULE', - idl_env: 'idl_env_FROM_USER_ID_MODULE', - imuid: 'imuid_FROM_USER_ID_MODULE', - criteoId: 'criteoId_FROM_USER_ID_MODULE', - fabrickId: 'fabrickId_FROM_USER_ID_MODULE', - }); - const data = spec.buildRequests(validBidRequests, bidderRequest).data; - - expect(data.user.ext.eids).to.deep.equal([ + validBidRequests[0].userIdAsEids = [ + {source: 'yahoo.com', uids: [{id: 'connectId_FROM_USER_ID_MODULE', atype: 3}]}, {source: 'admixer.net', uids: [{id: 'admixerId_FROM_USER_ID_MODULE', atype: 3}]}, {source: 'adtelligent.com', uids: [{id: 'adtelligentId_FROM_USER_ID_MODULE', atype: 3}]}, {source: 'amxdt.net', uids: [{id: 'amxId_FROM_USER_ID_MODULE', atype: 1}]}, @@ -947,7 +982,10 @@ describe('YahooSSP Bid Adapter:', () => { {source: 'intimatemerger.com', uids: [{id: 'imuid_FROM_USER_ID_MODULE', atype: 1}]}, {source: 'criteo.com', uids: [{id: 'criteoId_FROM_USER_ID_MODULE', atype: 1}]}, {source: 'neustar.biz', uids: [{id: 'fabrickId_FROM_USER_ID_MODULE', atype: 1}]} - ]); + ]; + const data = spec.buildRequests(validBidRequests, bidderRequest)[0].data; + + expect(data.user.ext.eids).to.deep.equal(validBidRequests[0].userIdAsEids); }); it('should not set not allowed user eids sources', () => { @@ -955,7 +993,7 @@ describe('YahooSSP Bid Adapter:', () => { validBidRequests[0].userIdAsEids = createEidsArray({ justId: 'justId_FROM_USER_ID_MODULE' }); - const data = spec.buildRequests(validBidRequests, bidderRequest).data; + const data = spec.buildRequests(validBidRequests, bidderRequest)[0].data; expect(data.user.ext.eids).to.deep.equal([]); }); @@ -964,7 +1002,7 @@ describe('YahooSSP Bid Adapter:', () => { describe('Request Payload oRTB bid validation:', () => { it('should generate a valid openRTB bid-request object in the data field', () => { const { validBidRequests, bidderRequest } = generateBuildRequestMock({}); - const data = spec.buildRequests(validBidRequests, bidderRequest).data; + const data = spec.buildRequests(validBidRequests, bidderRequest)[0].data; expect(data.site).to.deep.equal({ id: bidderRequest.bids[0].params.dcn, page: bidderRequest.refererInfo.page @@ -1013,7 +1051,7 @@ describe('YahooSSP Bid Adapter:', () => { it('should generate a valid openRTB imp.ext object in the bid-request', () => { const { validBidRequests, bidderRequest } = generateBuildRequestMock({}); const bid = validBidRequests[0]; - const data = spec.buildRequests(validBidRequests, bidderRequest).data; + const data = spec.buildRequests(validBidRequests, bidderRequest)[0].data; expect(data.imp[0].ext).to.deep.equal({ pos: bid.params.pos, dfp_ad_unit_code: DEFAULT_AD_UNIT_CODE @@ -1023,7 +1061,7 @@ describe('YahooSSP Bid Adapter:', () => { it('should use siteId value as site.id in the outbound bid-request when using "pubId" integration mode', () => { let { validBidRequests, bidderRequest } = generateBuildRequestMock({pubIdMode: true}); validBidRequests[0].params.siteId = '1234567'; - const data = spec.buildRequests(validBidRequests, bidderRequest).data; + const data = spec.buildRequests(validBidRequests, bidderRequest)[0].data; expect(data.site.id).to.equal('1234567'); }); @@ -1039,7 +1077,7 @@ describe('YahooSSP Bid Adapter:', () => { } } let { validBidRequests, bidderRequest } = generateBuildRequestMock({ortb2}); - const data = spec.buildRequests(validBidRequests, bidderRequest).data; + const data = spec.buildRequests(validBidRequests, bidderRequest)[0].data; expect(data.site.publisher).to.deep.equal({ ext: { publisherblob: 'pblob', @@ -1060,7 +1098,7 @@ describe('YahooSSP Bid Adapter:', () => { } } let { validBidRequests, bidderRequest } = generateBuildRequestMock({pubIdMode: true, ortb2}); - const data = spec.buildRequests(validBidRequests, bidderRequest).data; + const data = spec.buildRequests(validBidRequests, bidderRequest)[0].data; expect(data.site.publisher).to.deep.equal({ id: DEFAULT_PUBID, ext: { @@ -1073,17 +1111,13 @@ describe('YahooSSP Bid Adapter:', () => { it('should use placementId value as imp.tagid in the outbound bid-request when using "pubId" integration mode', () => { let { validBidRequests, bidderRequest } = generateBuildRequestMock({pubIdMode: true}); validBidRequests[0].params.placementId = 'header-300x250'; - const data = spec.buildRequests(validBidRequests, bidderRequest).data; + const data = spec.buildRequests(validBidRequests, bidderRequest)[0].data; expect(data.imp[0].tagid).to.deep.equal('header-300x250'); }); }); describe('Request Payload oRTB bid.imp validation:', () => { - // Validate Banner imp imp when yahoossp.mode=undefined - it('should generate a valid "Banner" imp object', () => { - config.setConfig({ - yahoossp: {} - }); + it('should generate a valid "Banner" imp object when mode config override is undefined', () => { const { validBidRequests, bidderRequest } = generateBuildRequestMock({}); const data = spec.buildRequests(validBidRequests, bidderRequest)[0].data; expect(data.imp[0].video).to.not.exist; @@ -1093,74 +1127,82 @@ describe('YahooSSP Bid Adapter:', () => { }); }); - // Validate Banner imp when yahoossp.mode="banner" - it('should generate a valid "Banner" imp object', () => { - config.setConfig({ - yahoossp: { mode: 'banner' } - }); - const { validBidRequests, bidderRequest } = generateBuildRequestMock({}); - const data = spec.buildRequests(validBidRequests, bidderRequest)[0].data; - expect(data.imp[0].video).to.not.exist; - expect(data.imp[0].banner).to.deep.equal({ - mimes: ['text/html', 'text/javascript', 'application/javascript', 'image/jpg'], - format: [{w: 300, h: 250}, {w: 300, h: 600}] + // Validate Banner imp when config value for mode="banner" + VALID_BIDDER_CODES.forEach(bidderCode => { + it(`should generate a valid "Banner" imp object for ${bidderCode} config override`, () => { + const cfg = {}; + cfg[bidderCode] = { + mode: BANNER + }; + config.setConfig(cfg); + const { validBidRequests, bidderRequest } = generateBuildRequestMock({bidderCode}); + const data = spec.buildRequests(validBidRequests, bidderRequest)[0].data; + expect(data.imp[0].video).to.not.exist; + expect(data.imp[0].banner).to.deep.equal({ + mimes: ['text/html', 'text/javascript', 'application/javascript', 'image/jpg'], + format: [{w: 300, h: 250}, {w: 300, h: 600}] + }); }); - }); - // Validate Video imp - it('should generate a valid "Video" only imp object', () => { - config.setConfig({ - yahoossp: { mode: 'video' } - }); - const { validBidRequests, bidderRequest } = generateBuildRequestMock({adUnitType: 'video'}); - const data = spec.buildRequests(validBidRequests, bidderRequest)[0].data; - expect(data.imp[0].banner).to.not.exist; - expect(data.imp[0].video).to.deep.equal({ - mimes: ['video/mp4', 'application/javascript'], - w: 300, - h: 250, - api: [2], - protocols: [2, 5], - startdelay: 0, - linearity: 1, - maxbitrate: undefined, - maxduration: undefined, - minduration: undefined, - delivery: undefined, - pos: undefined, - playbackmethod: undefined, - rewarded: undefined, - placement: undefined + // Validate Video imp + it(`should generate a valid "Video" only imp object for ${bidderCode} config override`, () => { + const cfg = {}; + cfg[bidderCode] = { + mode: VIDEO + }; + config.setConfig(cfg); + const { validBidRequests, bidderRequest } = generateBuildRequestMock({bidderCode, adUnitType: 'video'}); + const data = spec.buildRequests(validBidRequests, bidderRequest)[0].data; + expect(data.imp[0].banner).to.not.exist; + expect(data.imp[0].video).to.deep.equal({ + mimes: ['video/mp4', 'application/javascript'], + w: 300, + h: 250, + api: [2], + protocols: [2, 5], + startdelay: 0, + linearity: 1, + maxbitrate: undefined, + maxduration: undefined, + minduration: undefined, + delivery: undefined, + pos: undefined, + playbackmethod: undefined, + rewarded: undefined, + placement: undefined + }); }); - }); - // Validate multi-format Video+banner imp - it('should generate a valid multi-format "Video + Banner" imp object', () => { - config.setConfig({ - yahoossp: { mode: 'all' } - }); - const { validBidRequests, bidderRequest } = generateBuildRequestMock({adUnitType: 'multi-format'}); - const data = spec.buildRequests(validBidRequests, bidderRequest)[0].data; - expect(data.imp[0].banner).to.deep.equal({ - mimes: ['text/html', 'text/javascript', 'application/javascript', 'image/jpg'], - format: [{w: 300, h: 250}, {w: 300, h: 600}] - }); - expect(data.imp[0].video).to.deep.equal({ - mimes: ['video/mp4', 'application/javascript'], - w: 300, - h: 250, - api: [2], - protocols: [2, 5], - startdelay: 0, - linearity: 1, - maxbitrate: undefined, - maxduration: undefined, - minduration: undefined, - delivery: undefined, - pos: undefined, - playbackmethod: undefined, - rewarded: undefined, - placement: undefined + // Validate multi-format Video+banner imp + it(`should generate a valid multi-format "Video + Banner" imp object for ${bidderCode} config override`, () => { + const cfg = {}; + cfg[bidderCode] = { + mode: 'all' + }; + config.setConfig(cfg); + const { validBidRequests, bidderRequest } = generateBuildRequestMock({bidderCode, adUnitType: 'multi-format'}); + const data = spec.buildRequests(validBidRequests, bidderRequest)[0].data; + expect(data.imp[0].banner).to.deep.equal({ + mimes: ['text/html', 'text/javascript', 'application/javascript', 'image/jpg'], + format: [{w: 300, h: 250}, {w: 300, h: 600}] + }); + expect(data.imp[0].video).to.deep.equal({ + mimes: ['video/mp4', 'application/javascript'], + w: 300, + h: 250, + api: [2], + protocols: [2, 5], + startdelay: 0, + linearity: 1, + maxbitrate: undefined, + maxduration: undefined, + minduration: undefined, + delivery: undefined, + pos: undefined, + playbackmethod: undefined, + rewarded: undefined, + placement: undefined + }); }); }); @@ -1179,7 +1221,6 @@ describe('YahooSSP Bid Adapter:', () => { invalidKey5: undefined }; const data = spec.buildRequests(validBidRequests, bidderRequest)[0].data; - expect(data.imp[0].ext.kvs).to.deep.equal({ key1: 'String', key2: 123456, @@ -1192,10 +1233,6 @@ describe('YahooSSP Bid Adapter:', () => { describe('Multiple adUnit validations:', () => { // Multiple banner adUnits it('should generate multiple bid-requests for each adUnit - 2 banner only', () => { - config.setConfig({ - yahoossp: { mode: 'banner' } - }); - const BID_ID_2 = '84ab50xxxxx'; const BID_POS_2 = 'footer'; const AD_UNIT_CODE_2 = 'test-ad-unit-code-123'; @@ -1209,12 +1246,12 @@ describe('YahooSSP Bid Adapter:', () => { validBidRequests = [bidRequest, bidRequest2, bidRequest3]; bidderRequest.bids = validBidRequests; - const response = spec.buildRequests(validBidRequests, bidderRequest) - expect(response).to.be.a('array'); - expect(response.length).to.equal(2); - response.forEach((obj) => { - expect(obj.data.imp[0].video).to.not.exist - expect(obj.data.imp[0].banner).to.deep.equal({ + const reqs = spec.buildRequests(validBidRequests, bidderRequest) + expect(reqs).to.be.a('array'); + expect(reqs.length).to.equal(2); + reqs.forEach(req => { + expect(req.data.imp[0].video).to.not.exist + expect(req.data.imp[0].banner).to.deep.equal({ mimes: ['text/html', 'text/javascript', 'application/javascript', 'image/jpg'], format: [{w: 300, h: 250}, {w: 300, h: 600}] }); @@ -1223,9 +1260,11 @@ describe('YahooSSP Bid Adapter:', () => { // Multiple video adUnits it('should generate multiple bid-requests for each adUnit - 2 video only', () => { - config.setConfig({ - yahoossp: { mode: 'video' } - }); + const cfg = {}; + cfg[DEFAULT_BIDDER_CODE] = { + mode: VIDEO + }; + config.setConfig(cfg); const BID_ID_2 = '84ab50xxxxx'; const BID_POS_2 = 'footer'; const AD_UNIT_CODE_2 = 'test-ad-unit-code-123'; @@ -1233,18 +1272,18 @@ describe('YahooSSP Bid Adapter:', () => { const BID_POS_3 = 'hero'; const AD_UNIT_CODE_3 = 'video-ad-unit'; - let { bidRequest, validBidRequests, bidderRequest } = generateBuildRequestMock({adUnitType: 'video'}); // video - const { bidRequest: bidRequest2 } = generateBuildRequestMock({bidId: BID_ID_2, pos: BID_POS_2, adUnitCode: AD_UNIT_CODE_2, adUnitType: 'video'}); // video - const { bidRequest: bidRequest3 } = generateBuildRequestMock({bidId: BID_ID_3, pos: BID_POS_3, adUnitCode: AD_UNIT_CODE_3}); // banner (should be filtered) + let {bidRequest, validBidRequests, bidderRequest} = generateBuildRequestMock({adUnitType: 'video'}); // video + const {bidRequest: bidRequest2} = generateBuildRequestMock({bidId: BID_ID_2, pos: BID_POS_2, adUnitCode: AD_UNIT_CODE_2, adUnitType: 'video'}); // video + const {bidRequest: bidRequest3} = generateBuildRequestMock({bidId: BID_ID_3, pos: BID_POS_3, adUnitCode: AD_UNIT_CODE_3}); // banner (should be filtered) validBidRequests = [bidRequest, bidRequest2, bidRequest3]; bidderRequest.bids = validBidRequests; - const response = spec.buildRequests(validBidRequests, bidderRequest) - expect(response).to.be.a('array'); - expect(response.length).to.equal(2); - response.forEach((obj) => { - expect(obj.data.imp[0].banner).to.not.exist - expect(obj.data.imp[0].video).to.deep.equal({ + const reqs = spec.buildRequests(validBidRequests, bidderRequest) + expect(reqs).to.be.a('array'); + expect(reqs.length).to.equal(2); + reqs.forEach(req => { + expect(req.data.imp[0].banner).to.not.exist + expect(req.data.imp[0].video).to.deep.equal({ mimes: ['video/mp4', 'application/javascript'], w: 300, h: 250, @@ -1265,9 +1304,9 @@ describe('YahooSSP Bid Adapter:', () => { }); // Mixed adUnits 1-banner, 1-video, 1-native (should filter out native) it('should generate multiple bid-requests for both "video & banner" adUnits', () => { - config.setConfig({ - yahoossp: { mode: 'all' } - }); + const cfg = {}; + cfg[DEFAULT_BIDDER_CODE] = { mode: 'all' }; + config.setConfig(cfg); const BID_ID_2 = '84ab50xxxxx'; const BID_POS_2 = 'footer'; const AD_UNIT_CODE_2 = 'video-ad-unit'; @@ -1281,21 +1320,21 @@ describe('YahooSSP Bid Adapter:', () => { validBidRequests = [bidRequest, bidRequest2, bidRequest3]; bidderRequest.bids = validBidRequests; - const response = spec.buildRequests(validBidRequests, bidderRequest); - expect(response).to.be.a('array'); - expect(response.length).to.equal(2); - response.forEach((obj) => { - expect(obj.data.imp[0].native).to.not.exist; + const reqs = spec.buildRequests(validBidRequests, bidderRequest); + expect(reqs).to.be.a('array'); + expect(reqs.length).to.equal(2); + reqs.forEach(req => { + expect(req.data.imp[0].native).to.not.exist; }); - const data1 = response[0].data; + const data1 = reqs[0].data; expect(data1.imp[0].video).to.not.exist; expect(data1.imp[0].banner).to.deep.equal({ mimes: ['text/html', 'text/javascript', 'application/javascript', 'image/jpg'], format: [{w: 300, h: 250}, {w: 300, h: 600}] }); - const data2 = response[1].data; + const data2 = reqs[1].data; expect(data2.imp[0].banner).to.not.exist; expect(data2.imp[0].video).to.deep.equal({ mimes: ['video/mp4', 'application/javascript'], @@ -1318,90 +1357,98 @@ describe('YahooSSP Bid Adapter:', () => { }); describe('Video params firstlook & bidOverride validations:', () => { - it('should first look at params.bidOverride for video placement data', () => { - config.setConfig({ - yahoossp: { mode: 'video' } - }); - const bidOverride = { - imp: { - video: { - mimes: ['video/mp4'], - w: 400, - h: 350, - api: [1], - protocols: [1, 3], - startdelay: 0, - linearity: 1, - maxbitrate: 400000, - maxduration: 3600, - minduration: 1500, - delivery: 1, - pos: 123456, - playbackmethod: 1, - rewarded: 1, - placement: 1 + VALID_BIDDER_CODES.forEach(bidderCode => { + it(`should first look at params.bidOverride for video placement data for ${bidderCode} config override`, () => { + const cfg = {}; + cfg[bidderCode] = { + mode: VIDEO + }; + config.setConfig(cfg); + const bidOverride = { + imp: { + video: { + mimes: ['video/mp4'], + w: 400, + h: 350, + api: [1], + protocols: [1, 3], + startdelay: 0, + linearity: 1, + maxbitrate: 400000, + maxduration: 3600, + minduration: 1500, + delivery: 1, + pos: 123456, + playbackmethod: 1, + rewarded: 1, + placement: 1 + } } } - } - const { validBidRequests, bidderRequest } = generateBuildRequestMock({adUnitType: 'video', bidOverrideObject: bidOverride}); - const data = spec.buildRequests(validBidRequests, bidderRequest)[0].data; - expect(data.imp[0].video).to.deep.equal(bidOverride.imp.video); - }); - - it('should second look at bid.mediaTypes.video for video placement data', () => { - config.setConfig({ - yahoossp: { mode: 'video' } - }); - let { bidRequest, bidderRequest } = generateBuildRequestMock({adUnitType: 'video'}); - bidRequest.mediaTypes.video = { - mimes: ['video/mp4'], - playerSize: [400, 350], - api: [1], - protocols: [1, 3], - startdelay: 0, - linearity: 1, - maxbitrate: 400000, - maxduration: 3600, - minduration: 1500, - delivery: 1, - pos: 123456, - playbackmethod: 1, - placement: 1 - } - const validBidRequests = [bidRequest]; - bidderRequest.bids = validBidRequests; - const data = spec.buildRequests(validBidRequests, bidderRequest)[0].data; - expect(data.imp[0].video).to.deep.equal({ - mimes: ['video/mp4'], - w: 400, - h: 350, - api: [1], - protocols: [1, 3], - startdelay: 0, - linearity: 1, - maxbitrate: 400000, - maxduration: 3600, - minduration: 1500, - delivery: 1, - pos: 123456, - playbackmethod: 1, - placement: 1, - rewarded: undefined + const { validBidRequests, bidderRequest } = generateBuildRequestMock({bidderCode, adUnitType: 'video', bidOverrideObject: bidOverride}); + const data = spec.buildRequests(validBidRequests, bidderRequest)[0].data; + expect(data.imp[0].video).to.deep.equal(bidOverride.imp.video); }); - }); - it('should use params.bidOverride.device.ip override', () => { - config.setConfig({ - yahoossp: { mode: 'all' } + it(`should second look at bid.mediaTypes.video for video placement data for ${bidderCode} config override`, () => { + const cfg = {}; + cfg[bidderCode] = { + mode: VIDEO + }; + config.setConfig(cfg); + let { bidRequest, bidderRequest } = generateBuildRequestMock({bidderCode, adUnitType: 'video'}); + bidRequest.mediaTypes.video = { + mimes: ['video/mp4'], + playerSize: [400, 350], + api: [1], + protocols: [1, 3], + startdelay: 0, + linearity: 1, + maxbitrate: 400000, + maxduration: 3600, + minduration: 1500, + delivery: 1, + pos: 123456, + playbackmethod: 1, + placement: 1 + } + const validBidRequests = [bidRequest]; + bidderRequest.bids = validBidRequests; + const data = spec.buildRequests(validBidRequests, bidderRequest)[0].data; + expect(data.imp[0].video).to.deep.equal({ + mimes: ['video/mp4'], + w: 400, + h: 350, + api: [1], + protocols: [1, 3], + startdelay: 0, + linearity: 1, + maxbitrate: 400000, + maxduration: 3600, + minduration: 1500, + delivery: 1, + pos: 123456, + playbackmethod: 1, + placement: 1, + rewarded: undefined + }); }); - const bidOverride = { - device: { - ip: '1.2.3.4' + + it(`should use params.bidOverride.device.ip override for ${bidderCode} config override`, () => { + const cfg = {}; + cfg[bidderCode] = { + mode: 'all' + }; + config.setConfig(cfg); + const bidOverride = { + device: { + ip: '1.2.3.4' + } } - } - const { validBidRequests, bidderRequest } = generateBuildRequestMock({adUnitType: 'video', bidOverrideObject: bidOverride}); - const data = spec.buildRequests(validBidRequests, bidderRequest)[0].data; - expect(data.device.ip).to.deep.equal(bidOverride.device.ip); + const { validBidRequests, bidderRequest } = generateBuildRequestMock({bidderCode, adUnitType: 'video', bidOverrideObject: bidOverride}); + const data = spec.buildRequests(validBidRequests, bidderRequest)[0].data; + expect(data.device.ip).to.deep.equal(bidOverride.device.ip); + }); }); }); // #endregion buildRequests(): @@ -1417,6 +1464,22 @@ describe('YahooSSP Bid Adapter:', () => { }); describe('for mediaTypes: "video"', () => { + beforeEach(() => { + config.setConfig({ + yahooAds: { + mode: VIDEO + } + }); + }); + + afterEach(() => { + config.setConfig({ + yahooAds: { + mode: undefined + } + }); + }); + it('should insert video VPAID payload into vastXml', () => { const { serverResponse, bidderRequest } = generateResponseMock('video'); const response = spec.interpretResponse(serverResponse, {bidderRequest}); @@ -1434,28 +1497,38 @@ describe('YahooSSP Bid Adapter:', () => { expect(response[0].mediaType).to.equal('video'); }) - it('should insert video DAP O2 Player into ad', () => { - const { serverResponse, bidderRequest } = generateResponseMock('dap-o2', 'vpaid'); - const response = spec.interpretResponse(serverResponse, {bidderRequest}); - expect(response[0].ad).to.equal(''); - expect(response[0].vastUrl).to.be.undefined; - expect(response[0].vastXml).to.be.undefined; - expect(response[0].mediaType).to.equal('banner'); - }); + describe('wrapped in video players for display inventory', () => { + beforeEach(() => { + config.setConfig({ + yahooAds: { + mode: undefined + } + }); + }); - it('should insert video DAP Unified Player into ad', () => { - const { serverResponse, bidderRequest } = generateResponseMock('dap-up', 'vpaid'); - const response = spec.interpretResponse(serverResponse, {bidderRequest}); - expect(response[0].ad).to.equal(''); - expect(response[0].vastUrl).to.be.undefined; - expect(response[0].vastXml).to.be.undefined; - expect(response[0].mediaType).to.equal('banner'); - }) + it('should insert video DAP O2 Player into ad', () => { + const { serverResponse, bidderRequest } = generateResponseMock('dap-o2', 'vpaid'); + const response = spec.interpretResponse(serverResponse, {bidderRequest}); + expect(response[0].ad).to.equal(''); + expect(response[0].vastUrl).to.be.undefined; + expect(response[0].vastXml).to.be.undefined; + expect(response[0].mediaType).to.equal('banner'); + }); + + it('should insert video DAP Unified Player into ad', () => { + const { serverResponse, bidderRequest } = generateResponseMock('dap-up', 'vpaid'); + const response = spec.interpretResponse(serverResponse, {bidderRequest}); + expect(response[0].ad).to.equal(''); + expect(response[0].vastUrl).to.be.undefined; + expect(response[0].vastXml).to.be.undefined; + expect(response[0].mediaType).to.equal('banner'); + }) + }); }); describe('Support Advertiser domains', () => { it('should append bid-response adomain to meta.advertiserDomains', () => { - const { serverResponse, bidderRequest } = generateResponseMock('video', 'vpaid'); + const { serverResponse, bidderRequest } = generateResponseMock('banner'); const response = spec.interpretResponse(serverResponse, {bidderRequest}); expect(response[0].meta.advertiserDomains).to.be.a('array'); expect(response[0].meta.advertiserDomains[0]).to.equal('advertiser-domain.com'); @@ -1490,53 +1563,55 @@ describe('YahooSSP Bid Adapter:', () => { }); describe('Time To Live (ttl)', () => { - const UNSUPPORTED_TTL_FORMATS = ['string', [1, 2, 3], true, false, null, undefined]; - UNSUPPORTED_TTL_FORMATS.forEach(param => { - it('should not allow unsupported global yahoossp.ttl formats and default to 300', () => { - const { serverResponse, bidderRequest } = generateResponseMock('banner'); - config.setConfig({ - yahoossp: { ttl: param } + VALID_BIDDER_CODES.forEach(bidderCode => { + const UNSUPPORTED_TTL_FORMATS = ['string', [1, 2, 3], true, false, null, undefined]; + UNSUPPORTED_TTL_FORMATS.forEach(param => { + it(`should not allow unsupported global ${bidderCode}.ttl formats and default to 300`, () => { + const { serverResponse, bidderRequest } = generateResponseMock('banner'); + const cfg = {}; + cfg['yahooAds'] = { ttl: param }; + config.setConfig(cfg); + const response = spec.interpretResponse(serverResponse, {bidderRequest}); + expect(response[0].ttl).to.equal(300); }); - const response = spec.interpretResponse(serverResponse, {bidderRequest}); - expect(response[0].ttl).to.equal(300); - }); - it('should not allow unsupported params.ttl formats and default to 300', () => { - const { serverResponse, bidderRequest } = generateResponseMock('banner'); - bidderRequest.bids[0].params.ttl = param; - const response = spec.interpretResponse(serverResponse, {bidderRequest}); - expect(response[0].ttl).to.equal(300); + it('should not allow unsupported params.ttl formats and default to 300', () => { + const { serverResponse, bidderRequest } = generateResponseMock('banner'); + bidderRequest.bids[0].params.ttl = param; + const response = spec.interpretResponse(serverResponse, {bidderRequest}); + expect(response[0].ttl).to.equal(300); + }); }); - }); - const UNSUPPORTED_TTL_VALUES = [-1, 3601]; - UNSUPPORTED_TTL_VALUES.forEach(param => { - it('should not allow invalid global yahoossp.ttl values 3600 < ttl < 0 and default to 300', () => { - const { serverResponse, bidderRequest } = generateResponseMock('banner'); - config.setConfig({ - yahoossp: { ttl: param } + const UNSUPPORTED_TTL_VALUES = [-1, 3601]; + UNSUPPORTED_TTL_VALUES.forEach(param => { + it('should not allow invalid global config ttl values 3600 < ttl < 0 and default to 300', () => { + const { serverResponse, bidderRequest } = generateResponseMock('banner'); + config.setConfig({ + yahooAds: { ttl: param } + }); + const response = spec.interpretResponse(serverResponse, {bidderRequest}); + expect(response[0].ttl).to.equal(300); + }); + + it('should not allow invalid params.ttl values 3600 < ttl < 0 and default to 300', () => { + const { serverResponse, bidderRequest } = generateResponseMock('banner'); + bidderRequest.bids[0].params.ttl = param; + const response = spec.interpretResponse(serverResponse, {bidderRequest}); + expect(response[0].ttl).to.equal(300); }); - const response = spec.interpretResponse(serverResponse, {bidderRequest}); - expect(response[0].ttl).to.equal(300); }); - it('should not allow invalid params.ttl values 3600 < ttl < 0 and default to 300', () => { + it('should give presedence to Gloabl ttl over params.ttl ', () => { const { serverResponse, bidderRequest } = generateResponseMock('banner'); - bidderRequest.bids[0].params.ttl = param; + config.setConfig({ + yahooAds: { ttl: 500 } + }); + bidderRequest.bids[0].params.ttl = 400; const response = spec.interpretResponse(serverResponse, {bidderRequest}); - expect(response[0].ttl).to.equal(300); + expect(response[0].ttl).to.equal(500); }); }); - - it('should give presedence to Gloabl ttl over params.ttl ', () => { - const { serverResponse, bidderRequest } = generateResponseMock('banner'); - config.setConfig({ - yahoossp: { ttl: 500 } - }); - bidderRequest.bids[0].params.ttl = 400; - const response = spec.interpretResponse(serverResponse, {bidderRequest}); - expect(response[0].ttl).to.equal(500); - }); }); describe('Aliasing support', () => { diff --git a/test/spec/modules/yandexBidAdapter_spec.js b/test/spec/modules/yandexBidAdapter_spec.js index 12d413a9c93..c5f088a2306 100644 --- a/test/spec/modules/yandexBidAdapter_spec.js +++ b/test/spec/modules/yandexBidAdapter_spec.js @@ -1,8 +1,8 @@ import { assert, expect } from 'chai'; import { spec, NATIVE_ASSETS } from 'modules/yandexBidAdapter.js'; -import { parseUrl } from 'src/utils.js'; +import * as utils from 'src/utils.js'; import { BANNER, NATIVE } from '../../../src/mediaTypes'; -import {OPENRTB} from '../../../modules/rtbhouseBidAdapter'; +import { config } from '../../../src/config'; describe('Yandex adapter', function () { describe('isBidRequestValid', function () { @@ -71,7 +71,7 @@ describe('Yandex adapter', function () { expect(method).to.equal('POST'); - const parsedRequestUrl = parseUrl(url); + const parsedRequestUrl = utils.parseUrl(url); const { search: query } = parsedRequestUrl expect(parsedRequestUrl.hostname).to.equal('bs.yandex.ru'); @@ -90,6 +90,65 @@ describe('Yandex adapter', function () { expect(data.site.ref).to.equal('https://ya.ru/'); }); + it('should send currency if defined', function () { + config.setConfig({ + currency: { + adServerCurrency: 'USD' + } + }); + + const bannerRequest = getBidRequest(); + const requests = spec.buildRequests([bannerRequest], bidderRequest); + const { url } = requests[0]; + const parsedRequestUrl = utils.parseUrl(url); + const { search: query } = parsedRequestUrl + + expect(query['ssp-cur']).to.equal('USD'); + }); + + it('should send eids and ortb2 user data if defined', function() { + const bidRequestExtra = { + userIdAsEids: [{ + source: 'sharedid.org', + uids: [{ id: '01', atype: 1 }], + }], + ortb2: { + user: { + data: [ + { + ext: { segtax: 600, segclass: '1' }, + name: 'example.com', + segment: [{ id: '243' }], + }, + { + ext: { segtax: 600, segclass: '1' }, + name: 'ads.example.org', + segment: [{ id: '243' }], + }, + ], + }, + }, + }; + const expected = { + ext: { + eids: bidRequestExtra.userIdAsEids, + }, + data: bidRequestExtra.ortb2.user.data, + }; + + const bannerRequest = getBidRequest(bidRequestExtra); + const requests = spec.buildRequests([bannerRequest], bidderRequest); + + expect(requests).to.have.lengthOf(1); + const request = requests[0]; + + expect(request.data).to.exist; + const { data } = request; + + expect(data.user).to.exist; + expect(data.user).to.deep.equal(expected); + }); + describe('banner', () => { it('should create valid banner object', () => { const bannerRequest = getBidRequest({ @@ -273,7 +332,7 @@ describe('Yandex adapter', function () { }, }); }); - }) + }); }); describe('interpretResponse', function () { @@ -294,6 +353,7 @@ describe('Yandex adapter', function () { 'example.com' ], adid: 'yabs.123=', + nurl: 'https://example.com/nurl/?price=${AUCTION_PRICE}&cur=${AUCTION_CURRENCY}', } ] }], @@ -314,11 +374,12 @@ describe('Yandex adapter', function () { const rtbBid = result[0]; expect(rtbBid.width).to.equal(300); expect(rtbBid.height).to.equal(250); - expect(rtbBid.cpm).to.be.within(0.1, 0.5); + expect(rtbBid.cpm).to.be.within(0.3, 0.3); expect(rtbBid.ad).to.equal(''); expect(rtbBid.currency).to.equal('USD'); expect(rtbBid.netRevenue).to.equal(true); expect(rtbBid.ttl).to.equal(180); + expect(rtbBid.nurl).to.equal('https://example.com/nurl/?price=0.3&cur=USD'); expect(rtbBid.meta.advertiserDomains).to.deep.equal(['example.com']); }); @@ -425,6 +486,55 @@ describe('Yandex adapter', function () { }); }); }); + + describe('onBidWon', function() { + beforeEach(function() { + sinon.stub(utils, 'triggerPixel'); + }); + afterEach(function() { + utils.triggerPixel.restore(); + }); + + it('Should not trigger pixel if bid does not contain nurl', function() { + const result = spec.onBidWon({}); + expect(utils.triggerPixel.callCount).to.equal(0) + }) + + it('Should trigger pixel if bid has nurl', function() { + const result = spec.onBidWon({ + nurl: 'https://example.com/some-tracker', + timeToRespond: 378, + }); + expect(utils.triggerPixel.callCount).to.equal(1) + expect(utils.triggerPixel.getCall(0).args[0]).to.equal('https://example.com/some-tracker?rtt=378') + }) + + it('Should trigger pixel if bid has nurl with path & params', function() { + const result = spec.onBidWon({ + nurl: 'https://example.com/some-tracker/abcdxyz?param1=1¶m2=2', + timeToRespond: 378, + }); + expect(utils.triggerPixel.callCount).to.equal(1) + expect(utils.triggerPixel.getCall(0).args[0]).to.equal('https://example.com/some-tracker/abcdxyz?param1=1¶m2=2&rtt=378') + }) + + it('Should trigger pixel if bid has nurl with path & params and rtt macros', function() { + const result = spec.onBidWon({ + nurl: 'https://example.com/some-tracker/abcdxyz?param1=1¶m2=2&custom-rtt=${RTT}', + timeToRespond: 378, + }); + expect(utils.triggerPixel.callCount).to.equal(1) + expect(utils.triggerPixel.getCall(0).args[0]).to.equal('https://example.com/some-tracker/abcdxyz?param1=1¶m2=2&custom-rtt=378') + }) + + it('Should trigger pixel if bid has nurl and there is no timeToRespond param, but has rtt macros in nurl', function() { + const result = spec.onBidWon({ + nurl: 'https://example.com/some-tracker/abcdxyz?param1=1¶m2=2&custom-rtt=${RTT}', + }); + expect(utils.triggerPixel.callCount).to.equal(1) + expect(utils.triggerPixel.getCall(0).args[0]).to.equal('https://example.com/some-tracker/abcdxyz?param1=1¶m2=2&custom-rtt=-1') + }) + }) }); function getBidConfig() { diff --git a/test/spec/modules/yieldloveBidAdapter_spec.js b/test/spec/modules/yieldloveBidAdapter_spec.js new file mode 100644 index 00000000000..b142eef0ffa --- /dev/null +++ b/test/spec/modules/yieldloveBidAdapter_spec.js @@ -0,0 +1,128 @@ +import { expect } from 'chai'; +import { spec } from 'modules/yieldloveBidAdapter.js'; + +const ENDPOINT_URL = 'https://s2s.yieldlove-ad-serving.net/openrtb2/auction'; + +// test params +const pid = 34437; +const rid = 'website.com'; + +describe('Yieldlove Bid Adaper', function () { + const bidRequests = [ + { + 'bidder': 'yieldlove', + 'adUnitCode': 'adunit-code', + 'sizes': [ [300, 250] ], + 'params': { + pid, + rid + } + } + ]; + + const serverResponse = { + body: { + seatbid: [ + { + bid: [ + { + impid: 'aaaa', + price: 0.5, + w: 300, + h: 250, + adm: '
test
', + crid: '1234', + } + ] + } + ], + ext: {} + } + } + + describe('isBidRequestValid', () => { + const bid = bidRequests[0]; + + it('should return true when required params found', function () { + expect(spec.isBidRequestValid(bid)).to.equal(true); + }); + + it('should return false when required params are not present', function () { + const invalidBid = { ...bid, params: {} }; + expect(spec.isBidRequestValid(invalidBid)).to.equal(false); + }); + + it('should return false when required param "pid" is not present', function () { + const invalidBid = { ...bid, params: { ...bid.params, pid: undefined } }; + expect(spec.isBidRequestValid(invalidBid)).to.equal(false); + }); + + it('should return false when required param "rid" is not present', function () { + const invalidBid = { ...bid, params: { ...bid.params, rid: undefined } }; + expect(spec.isBidRequestValid(invalidBid)).to.equal(false); + }); + }); + + describe('buildRequests', () => { + it('should build the request', function () { + const request = spec.buildRequests(bidRequests, {}); + const payload = request.data; + const url = request.url; + + expect(url).to.equal(ENDPOINT_URL); + + expect(payload.site).to.exist; + expect(payload.site.publisher).to.exist; + expect(payload.site.publisher.id).to.exist; + expect(payload.site.publisher.id).to.equal(rid); + expect(payload.site.domain).to.exist; + expect(payload.site.domain).to.equal(rid); + + expect(payload.imp).to.exist; + expect(payload.imp[0]).to.exist; + expect(payload.imp[0].ext).to.exist; + expect(payload.imp[0].ext.prebid).to.exist; + expect(payload.imp[0].ext.prebid.storedrequest).to.exist; + expect(payload.imp[0].ext.prebid.storedrequest.id).to.exist; + expect(payload.imp[0].ext.prebid.storedrequest.id).to.equal(pid.toString()); + }); + }); + + describe('interpretResponse', () => { + it('should interpret the response by pushing it in the bids elem', function () { + const allResponses = spec.interpretResponse(serverResponse); + const response = allResponses[0]; + const seatbid = serverResponse.body.seatbid[0].bid[0]; + + expect(response.requestId).to.exist; + expect(response.requestId).to.equal(seatbid.impid); + expect(response.cpm).to.exist; + expect(response.cpm).to.equal(seatbid.price); + expect(response.width).to.exist; + expect(response.width).to.equal(seatbid.w); + expect(response.height).to.exist; + expect(response.height).to.equal(seatbid.h); + expect(response.ad).to.exist; + expect(response.ad).to.equal(seatbid.adm); + expect(response.ttl).to.exist; + expect(response.creativeId).to.exist; + expect(response.creativeId).to.equal(seatbid.crid); + expect(response.netRevenue).to.exist; + expect(response.currency).to.exist; + }); + }); + + describe('getUserSyncs', function() { + it('should retrieve user iframe syncs', function () { + expect(spec.getUserSyncs({ iframeEnabled: true }, [serverResponse], undefined, undefined)).to.deep.equal([{ + type: 'iframe', + url: 'https://cdn-a.yieldlove.com/load-cookie.html?endpoint=yieldlove&max_sync_count=100&gdpr=NaN&gdpr_consent=&' + }]); + + expect(spec.getUserSyncs({ iframeEnabled: true }, [serverResponse], { gdprApplies: true, consentString: 'example' }, undefined)).to.deep.equal([{ + type: 'iframe', + url: 'https://cdn-a.yieldlove.com/load-cookie.html?endpoint=yieldlove&max_sync_count=100&gdpr=1&gdpr_consent=example&' + }]); + }); + }); +}) diff --git a/test/spec/modules/yieldmoBidAdapter_spec.js b/test/spec/modules/yieldmoBidAdapter_spec.js index 3706f770da8..edb3ef3af27 100644 --- a/test/spec/modules/yieldmoBidAdapter_spec.js +++ b/test/spec/modules/yieldmoBidAdapter_spec.js @@ -47,7 +47,7 @@ describe('YieldmoAdapter', function () { video: { playerSize: [640, 480], context: 'instream', - mimes: ['video/mp4'] + mimes: ['video/mp4'], }, }, params: { @@ -61,11 +61,11 @@ describe('YieldmoAdapter', function () { api: [2, 3], skipppable: true, playbackmethod: [1, 2], - ...videoParams - } + ...videoParams, + }, }, transactionId: '54a58774-7a41-494e-8cbc-fa7b79164f0c', - ...rootParams + ...rootParams, }); const mockBidderRequest = (params = {}, bids = [mockBannerBid()]) => ({ @@ -74,7 +74,6 @@ describe('YieldmoAdapter', function () { bidderRequestId: '14c4ede8c693f', bids, auctionStart: 1520001292880, - timeout: 3000, start: 1520001292884, doneCbCallCount: 0, refererInfo: { @@ -169,6 +168,14 @@ describe('YieldmoAdapter', function () { expect(requests[0].url).to.be.equal(BANNER_ENDPOINT); }); + it('should pass default timeout in bid request', function () { + const requests = build([mockBannerBid()]); + expect(requests[0].data.tmax).to.equal(400); + }); + it('should pass tmax to bid request', function () { + const requests = build([mockBannerBid()], mockBidderRequest({timeout: 1000})); + expect(requests[0].data.tmax).to.equal(1000); + }); it('should not blow up if crumbs is undefined', function () { expect(function () { build([mockBannerBid({crumbs: undefined})]); @@ -425,6 +432,18 @@ describe('YieldmoAdapter', function () { expect(requests[0].url).to.be.equal(VIDEO_ENDPOINT); }); + it('should not require params.video if required props in mediaTypes.video', function () { + videoBid.mediaTypes.video = { + ...videoBid.mediaTypes.video, + ...videoBid.params.video + }; + delete videoBid.params.video; + const requests = build([videoBid]); + expect(requests.length).to.equal(1); + expect(requests[0].method).to.equal('POST'); + expect(requests[0].url).to.be.equal(VIDEO_ENDPOINT); + }); + it('should add mediaTypes.video prop to the imp.video prop', function () { utils.deepAccess(videoBid, 'mediaTypes.video')['minduration'] = 40; expect(buildVideoBidAndGetVideoParam().minduration).to.equal(40); @@ -441,12 +460,36 @@ describe('YieldmoAdapter', function () { expect(buildVideoBidAndGetVideoParam().minduration).to.deep.equal(['video/mp4']); }); + it('should add plcmt value to the imp.video', function () { + const videoBid = mockVideoBid({}, {}, { plcmt: 1 }); + expect(utils.deepAccess(videoBid, 'params.video')['plcmt']).to.equal(1); + }); + + it('should add start delay if plcmt value is not 1', function () { + const videoBid = mockVideoBid({}, {}, { plcmt: 2 }); + expect(build([videoBid])[0].data.imp[0].video.startdelay).to.equal(0); + }); + it('should override mediaTypes.video.mimes prop if params.video.mimes is present', function () { utils.deepAccess(videoBid, 'mediaTypes.video')['mimes'] = ['video/mp4']; utils.deepAccess(videoBid, 'params.video')['mimes'] = ['video/mkv']; expect(buildVideoBidAndGetVideoParam().mimes).to.deep.equal(['video/mkv']); }); + it('should validate protocol in video bid request', function () { + expect( + spec.isBidRequestValid( + mockVideoBid({}, {}, { protocols: [2, 3, 11] }) + ) + ).to.be.true; + + expect( + spec.isBidRequestValid( + mockVideoBid({}, {}, { protocols: [2, 3, 10] }) + ) + ).to.be.false; + }); + describe('video.skip state check', () => { it('should not set video.skip if neither *.video.skip nor *.video.skippable is present', function () { utils.deepAccess(videoBid, 'mediaTypes.video')['skippable'] = false; diff --git a/test/spec/modules/zeta_global_sspAnalyticsAdapter_spec.js b/test/spec/modules/zeta_global_sspAnalyticsAdapter_spec.js index 15a1155f378..5194a6a526a 100644 --- a/test/spec/modules/zeta_global_sspAnalyticsAdapter_spec.js +++ b/test/spec/modules/zeta_global_sspAnalyticsAdapter_spec.js @@ -1,20 +1,21 @@ import zetaAnalyticsAdapter from 'modules/zeta_global_sspAnalyticsAdapter.js'; import {config} from 'src/config'; import CONSTANTS from 'src/constants.json'; +import {server} from '../../mocks/xhr.js'; import {logError} from '../../../src/utils'; let utils = require('src/utils'); let events = require('src/events'); -const MOCK = { - STUB: { - 'auctionId': '25c6d7f5-699a-4bfc-87c9-996f915341fa' - }, +const EVENTS = { AUCTION_END: { 'auctionId': '75e394d9-ccce-4978-9238-91e6a1ac88a1', 'timestamp': 1638441234544, 'auctionEnd': 1638441234784, 'auctionStatus': 'completed', + 'metrics': { + 'someMetric': 1 + }, 'adUnits': [ { 'code': '/19968336/header-bid-tag-0', @@ -74,13 +75,6 @@ const MOCK = { 'bids': [ { 'bidder': 'zeta_global_ssp', - 'params': { - 'sid': 111, - 'tags': { - 'shortname': 'prebid_analytics_event_test_shortname', - 'position': 'test_position' - } - }, 'mediaTypes': { 'banner': { 'sizes': [ @@ -309,6 +303,9 @@ const MOCK = { 'cpm': 2.258302852806723, 'currency': 'USD', 'ad': 'test_ad', + 'metrics': { + 'someMetric': 0 + }, 'ttl': 200, 'creativeId': '456456456', 'netRevenue': true, @@ -344,11 +341,7 @@ const MOCK = { 'status': 'rendered', 'params': [ { - 'sid': 111, - 'tags': { - 'shortname': 'prebid_analytics_event_test_shortname', - 'position': 'test_position' - } + 'nonZetaParam': 'nonZetaValue' } ] }, @@ -358,14 +351,11 @@ const MOCK = { describe('Zeta Global SSP Analytics Adapter', function() { let sandbox; - let xhr; let requests; beforeEach(function() { sandbox = sinon.sandbox.create(); - requests = []; - xhr = sandbox.useFakeXMLHttpRequest(); - xhr.onCreate = request => requests.push(request); + requests = server.requests; sandbox.stub(events, 'getEvents').returns([]); }); @@ -395,33 +385,45 @@ describe('Zeta Global SSP Analytics Adapter', function() { zetaAnalyticsAdapter.disableAnalytics(); }); - it('events are sent', function() { - this.timeout(5000); - events.emit(CONSTANTS.EVENTS.AUCTION_INIT, MOCK.STUB); - events.emit(CONSTANTS.EVENTS.AUCTION_END, MOCK.AUCTION_END); - events.emit(CONSTANTS.EVENTS.BID_ADJUSTMENT, MOCK.STUB); - events.emit(CONSTANTS.EVENTS.BID_TIMEOUT, MOCK.STUB); - events.emit(CONSTANTS.EVENTS.BID_REQUESTED, MOCK.STUB); - events.emit(CONSTANTS.EVENTS.BID_RESPONSE, MOCK.STUB); - events.emit(CONSTANTS.EVENTS.NO_BID, MOCK.STUB); - events.emit(CONSTANTS.EVENTS.BID_WON, MOCK.STUB); - events.emit(CONSTANTS.EVENTS.BIDDER_DONE, MOCK.STUB); - events.emit(CONSTANTS.EVENTS.BIDDER_ERROR, MOCK.STUB); - events.emit(CONSTANTS.EVENTS.SET_TARGETING, MOCK.STUB); - events.emit(CONSTANTS.EVENTS.BEFORE_REQUEST_BIDS, MOCK.STUB); - events.emit(CONSTANTS.EVENTS.BEFORE_BIDDER_HTTP, MOCK.STUB); - events.emit(CONSTANTS.EVENTS.REQUEST_BIDS, MOCK.STUB); - events.emit(CONSTANTS.EVENTS.ADD_AD_UNITS, MOCK.STUB); - events.emit(CONSTANTS.EVENTS.AD_RENDER_FAILED, MOCK.STUB); - events.emit(CONSTANTS.EVENTS.AD_RENDER_SUCCEEDED, MOCK.AD_RENDER_SUCCEEDED); - events.emit(CONSTANTS.EVENTS.TCF2_ENFORCEMENT, MOCK.STUB); - events.emit(CONSTANTS.EVENTS.AUCTION_DEBUG, MOCK.STUB); - events.emit(CONSTANTS.EVENTS.BID_VIEWABLE, MOCK.STUB); - events.emit(CONSTANTS.EVENTS.STALE_RENDER, MOCK.STUB); + it('Move ZetaParams through analytics events', function() { + this.timeout(3000); + + events.emit(CONSTANTS.EVENTS.AUCTION_END, EVENTS.AUCTION_END); + events.emit(CONSTANTS.EVENTS.AD_RENDER_SUCCEEDED, EVENTS.AD_RENDER_SUCCEEDED); + + expect(requests.length).to.equal(2); + const auctionEnd = JSON.parse(requests[0].requestBody); + const auctionSucceeded = JSON.parse(requests[1].requestBody); + + expect(auctionSucceeded.bid.params[0]).to.be.deep.equal(EVENTS.AUCTION_END.adUnits[0].bids[0].params); + expect(EVENTS.AUCTION_END.adUnits[0].bids[0].bidder).to.be.equal('zeta_global_ssp'); + }); + + it('Keep only needed fields', function() { + this.timeout(3000); + + events.emit(CONSTANTS.EVENTS.AUCTION_END, EVENTS.AUCTION_END); + events.emit(CONSTANTS.EVENTS.AD_RENDER_SUCCEEDED, EVENTS.AD_RENDER_SUCCEEDED); expect(requests.length).to.equal(2); - expect(JSON.parse(requests[0].requestBody)).to.deep.equal(MOCK.AUCTION_END); - expect(JSON.parse(requests[1].requestBody)).to.deep.equal(MOCK.AD_RENDER_SUCCEEDED); + const auctionEnd = JSON.parse(requests[0].requestBody); + const auctionSucceeded = JSON.parse(requests[1].requestBody); + + expect(auctionEnd.adUnitCodes[0]).to.be.equal('/19968336/header-bid-tag-0'); + expect(auctionEnd.adUnits[0].bids[0].bidder).to.be.equal('zeta_global_ssp'); + expect(auctionEnd.auctionEnd).to.be.equal(1638441234784); + expect(auctionEnd.auctionId).to.be.equal('75e394d9-ccce-4978-9238-91e6a1ac88a1'); + expect(auctionEnd.bidderRequests[0].bidderCode).to.be.equal('zeta_global_ssp'); + expect(auctionEnd.bidsReceived[0].bidderCode).to.be.equal('zeta_global_ssp'); + expect(auctionEnd.noBids[0].bidder).to.be.equal('appnexus'); + + expect(auctionSucceeded.adId).to.be.equal('5759bb3ef7be1e8'); + expect(auctionSucceeded.bid.auctionId).to.be.equal('75e394d9-ccce-4978-9238-91e6a1ac88a1'); + expect(auctionSucceeded.bid.requestId).to.be.equal('206be9a13236af'); + expect(auctionSucceeded.bid.bidderCode).to.be.equal('zeta_global_ssp'); + expect(auctionSucceeded.bid.creativeId).to.be.equal('456456456'); + expect(auctionSucceeded.bid.size).to.be.equal('480x320'); + expect(auctionSucceeded.doc.location.hostname).to.be.equal('localhost'); }); }); }); diff --git a/test/spec/modules/zeta_global_sspBidAdapter_spec.js b/test/spec/modules/zeta_global_sspBidAdapter_spec.js index 601f4546a29..9ef97019a98 100644 --- a/test/spec/modules/zeta_global_sspBidAdapter_spec.js +++ b/test/spec/modules/zeta_global_sspBidAdapter_spec.js @@ -1,5 +1,6 @@ import {spec} from '../../../modules/zeta_global_sspBidAdapter.js' import {BANNER, VIDEO} from '../../../src/mediaTypes'; +import {deepClone} from '../../../src/utils'; describe('Zeta Ssp Bid Adapter', function () { const eids = [ @@ -50,7 +51,6 @@ describe('Zeta Ssp Bid Adapter', function () { someTag: 444, }, sid: 'publisherId', - shortname: 'test_shortname', tagid: 'test_tag_id', site: { page: 'testPage' @@ -124,7 +124,24 @@ describe('Zeta Ssp Bid Adapter', function () { uspConsent: 'someCCPAString', params: params, userIdAsEids: eids, - timeout: 500 + timeout: 500, + ortb2: { + user: { + data: [ + { + ext: { + segtax: 600, + segclass: 'classifier_v1' + }, + segment: [ + { id: '3' }, + { id: '44' }, + { id: '59' } + ] + } + ] + } + } }]; const bannerWithFewSizesRequest = [{ @@ -253,11 +270,13 @@ describe('Zeta Ssp Bid Adapter', function () { }; it('Test the bid validation function', function () { - const validBid = spec.isBidRequestValid(bannerRequest[0]); - const invalidBid = spec.isBidRequestValid(null); + const invalidBid = deepClone(bannerRequest[0]); + invalidBid.params = {}; + const isValidBid = spec.isBidRequestValid(bannerRequest[0]); + const isInvalidBid = spec.isBidRequestValid(null); - expect(validBid).to.be.true; - expect(invalidBid).to.be.false; + expect(isValidBid).to.be.true; + expect(isInvalidBid).to.be.false; }); it('Test provide eids', function () { @@ -453,7 +472,7 @@ describe('Zeta Ssp Bid Adapter', function () { it('Test required params in banner request', function () { const request = spec.buildRequests(bannerRequest, bannerRequest[0]); const payload = JSON.parse(request.data); - expect(request.url).to.eql('https://ssp.disqus.com/bid/prebid?shortname=test_shortname'); + expect(request.url).to.eql('https://ssp.disqus.com/bid/prebid?sid=publisherId'); expect(payload.ext.sid).to.eql('publisherId'); expect(payload.ext.tags.someTag).to.eql(444); expect(payload.ext.tags.shortname).to.be.undefined; @@ -462,7 +481,7 @@ describe('Zeta Ssp Bid Adapter', function () { it('Test required params in video request', function () { const request = spec.buildRequests(videoRequest, videoRequest[0]); const payload = JSON.parse(request.data); - expect(request.url).to.eql('https://ssp.disqus.com/bid/prebid?shortname=test_shortname'); + expect(request.url).to.eql('https://ssp.disqus.com/bid/prebid?sid=publisherId'); expect(payload.ext.sid).to.eql('publisherId'); expect(payload.ext.tags.someTag).to.eql(444); expect(payload.ext.tags.shortname).to.be.undefined; @@ -471,7 +490,7 @@ describe('Zeta Ssp Bid Adapter', function () { it('Test multi imp', function () { const request = spec.buildRequests(multiImpRequest, multiImpRequest[0]); const payload = JSON.parse(request.data); - expect(request.url).to.eql('https://ssp.disqus.com/bid/prebid?shortname=test_shortname'); + expect(request.url).to.eql('https://ssp.disqus.com/bid/prebid?sid=publisherId'); expect(payload.imp.length).to.eql(2); @@ -604,4 +623,13 @@ describe('Zeta Ssp Bid Adapter', function () { expect(bidResponse[0].ad).to.eql(zetaResponse.body.seatbid[0].bid[0].adm); expect(bidResponse[0].vastXml).to.be.undefined; }); + + it('Test provide segments into the request', function () { + const request = spec.buildRequests(bannerRequest, bannerRequest[0]); + const payload = JSON.parse(request.data); + expect(payload.user.data[0].segment.length).to.eql(3); + expect(payload.user.data[0].segment[0].id).to.eql('3'); + expect(payload.user.data[0].segment[1].id).to.eql('44'); + expect(payload.user.data[0].segment[2].id).to.eql('59'); + }); }); diff --git a/test/spec/modules/zmaticooBidAdapter_spec.js b/test/spec/modules/zmaticooBidAdapter_spec.js new file mode 100644 index 00000000000..898dccf1fde --- /dev/null +++ b/test/spec/modules/zmaticooBidAdapter_spec.js @@ -0,0 +1,83 @@ +import {spec} from '../../../modules/zmaticooBidAdapter.js' + +describe('zMaticoo Bidder Adapter', function () { + const bannerRequest = [{ + bidId: '1234511', + auctionId: '223', + mediaTypes: { + banner: { + sizes: [[320, 50]], + } + }, + refererInfo: { + page: 'testprebid.com' + }, + params: { + user: { + uid: '12345', + buyeruid: '12345' + }, + device: { + ip: '111.222.33.44', + geo: { + country: 'USA' + } + }, + pubId: 'prebid-test', + test: 1 + } + }]; + + it('Test the bid validation function', function () { + const validBid = spec.isBidRequestValid(bannerRequest[0]); + const invalidBid = spec.isBidRequestValid(null); + + expect(validBid).to.be.true; + expect(invalidBid).to.be.false; + }); + + it('Test the request processing function', function () { + const request = spec.buildRequests(bannerRequest, bannerRequest[0]); + expect(request).to.not.be.empty; + + const payload = request.data; + expect(payload).to.not.be.empty; + }); + + const responseBody = { + id: '12345', + seatbid: [ + { + bid: [ + { + id: 'auctionId', + impid: 'impId', + price: 0.0, + adm: 'adMarkup', + crid: 'creativeId', + h: 50, + w: 320 + } + ] + } + ], + cur: 'USD' + }; + + it('Test the response parsing function', function () { + const receivedBid = responseBody.seatbid[0].bid[0]; + const response = {}; + response.body = responseBody; + + const bidResponse = spec.interpretResponse(response, null); + expect(bidResponse).to.not.be.empty; + + const bid = bidResponse[0]; + expect(bid).to.not.be.empty; + expect(bid.ad).to.equal(receivedBid.adm); + expect(bid.cpm).to.equal(receivedBid.price); + expect(bid.height).to.equal(receivedBid.h); + expect(bid.width).to.equal(receivedBid.w); + expect(bid.requestId).to.equal(receivedBid.impid); + }); +}); diff --git a/test/spec/native_spec.js b/test/spec/native_spec.js index dee177d4b9b..9cfee6f5cd8 100644 --- a/test/spec/native_spec.js +++ b/test/spec/native_spec.js @@ -631,7 +631,8 @@ describe('native.js', function () { eventtrackers: [ { event: 1, method: 1, url: 'https://sampleurl.com' }, { event: 1, method: 2, url: 'https://sampleurljs.com' } - ] + ], + imptrackers: [ 'https://sample-imp.com' ] } describe('toLegacyResponse', () => { it('returns assets in legacy format for ortb responses', () => { @@ -640,8 +641,9 @@ describe('native.js', function () { expect(actual.title).to.equal('vtitle'); expect(actual.clickUrl).to.equal('url'); expect(actual.javascriptTrackers).to.equal(''); - expect(actual.impressionTrackers.length).to.equal(1); - expect(actual.impressionTrackers[0]).to.equal('https://sampleurl.com'); + expect(actual.impressionTrackers.length).to.equal(2); + expect(actual.impressionTrackers).to.contain('https://sampleurl.com'); + expect(actual.impressionTrackers).to.contain('https://sample-imp.com'); }); }); }); @@ -860,6 +862,9 @@ describe('validate native', function () { }] }, address: {}, + privacyLink: { + required: true + } }, }, }; @@ -915,6 +920,7 @@ describe('validate native', function () { type: 9, } }); + expect(ortb.privacy).to.equal(1); }); ['bogusKey', 'clickUrl', 'privacyLink'].forEach(nativeKey => { @@ -1022,11 +1028,14 @@ describe('validate native', function () { expect(oldNativeRequest.sponsoredBy).to.include({ required: true, len: 25 - }) + }); expect(oldNativeRequest.body).to.include({ required: true, len: 140 - }) + }); + expect(oldNativeRequest.privacyLink).to.include({ + required: false + }); }); if (FEATURES.NATIVE) { @@ -1197,6 +1206,12 @@ describe('legacyPropertiesToOrtbNative', () => { expect(native.jstracker).to.eql('some-markupsome-other-markup'); }) }); + describe('privacylink', () => { + it('should convert privacyLink to privacy', () => { + const native = legacyPropertiesToOrtbNative({privacyLink: 'https:/my-privacy-link.com'}); + expect(native.privacy).to.eql('https:/my-privacy-link.com'); + }) + }) }); describe('fireImpressionTrackers', () => { diff --git a/test/spec/unit/core/adapterManager_spec.js b/test/spec/unit/core/adapterManager_spec.js index cff26df2e4d..dac70696b4b 100644 --- a/test/spec/unit/core/adapterManager_spec.js +++ b/test/spec/unit/core/adapterManager_spec.js @@ -964,15 +964,42 @@ describe('adapterManager tests', function () { 'start': 1462918897460 }]; - it('invokes callBids on the S2S adapter', function () { - adapterManager.callBids( - getAdUnits(), - bidRequests, - () => {}, - () => () => {} - ); - sinon.assert.calledTwice(prebidServerAdapterMock.callBids); - }); + describe('invokes callBids on the S2S adapter', () => { + let onTimelyResponse, timedOut, done; + beforeEach(() => { + done = sinon.stub(); + onTimelyResponse = sinon.stub(); + prebidServerAdapterMock.callBids.callsFake((_1, _2, _3, done) => { + done(timedOut); + }); + }) + + function runTest() { + adapterManager.callBids( + getAdUnits(), + bidRequests, + () => {}, + done, + undefined, + undefined, + onTimelyResponse + ); + sinon.assert.calledTwice(prebidServerAdapterMock.callBids); + sinon.assert.calledTwice(done); + } + + it('and marks requests as timely if the adapter says timedOut = false', function () { + timedOut = false; + runTest(); + bidRequests.forEach(br => sinon.assert.calledWith(onTimelyResponse, br.bidderRequestId)); + }); + + it('and does NOT mark them as timely if it says timedOut = true', () => { + timedOut = true; + runTest(); + sinon.assert.notCalled(onTimelyResponse); + }) + }) // Enable this test when prebidServer adapter is made 1.0 compliant it('invokes callBids with only s2s bids', function () { diff --git a/test/spec/unit/core/ajax_spec.js b/test/spec/unit/core/ajax_spec.js new file mode 100644 index 00000000000..dd03ad1a761 --- /dev/null +++ b/test/spec/unit/core/ajax_spec.js @@ -0,0 +1,429 @@ +import {attachCallbacks, dep, fetcherFactory, toFetchRequest} from '../../../../src/ajax.js'; +import {config} from 'src/config.js'; +import {server} from '../../../mocks/xhr.js'; +import * as utils from 'src/utils.js'; +import {logError} from 'src/utils.js'; + +const EXAMPLE_URL = 'https://www.example.com'; + +describe('fetcherFactory', () => { + let clock; + + beforeEach(() => { + clock = sinon.useFakeTimers(); + server.autoTimeout = true; + }); + + afterEach(() => { + clock.runAll(); + clock.restore(); + config.resetConfig(); + }); + + Object.entries({ + 'URL': EXAMPLE_URL, + 'request object': new Request(EXAMPLE_URL) + }).forEach(([t, resource]) => { + it(`times out after timeout when fetching ${t}`, (done) => { + const fetch = fetcherFactory(1000); + const resp = fetch(resource); + clock.tick(900); + expect(server.requests[0].fetch.request.signal.aborted).to.be.false; + clock.tick(100); + expect(server.requests[0].fetch.request.signal.aborted).to.be.true; + resp.catch(() => done()); + }); + }); + + it('does not timeout after it completes', () => { + const fetch = fetcherFactory(1000); + const resp = fetch(EXAMPLE_URL); + server.requests[0].respond(); + return resp.then(() => { + clock.tick(2000); + expect(server.requests[0].fetch.request.signal.aborted).to.be.false; + }); + }); + + Object.entries({ + 'disableAjaxTimeout is set'() { + const fetcher = fetcherFactory(1000); + config.setConfig({disableAjaxTimeout: true}); + return fetcher; + }, + 'timeout is null'() { + return fetcherFactory(null); + }, + }).forEach(([t, mkFetcher]) => { + it(`does not timeout if ${t}`, (done) => { + const fetch = mkFetcher(); + const pm = fetch(EXAMPLE_URL); + clock.tick(2000); + server.requests[0].respond(); + pm.then(() => done()); + }); + }); + + Object.entries({ + 'local URL': ['/local.html', window.origin], + 'remote URL': [EXAMPLE_URL + '/remote.html', EXAMPLE_URL], + 'request with local URL': [new Request('/local.html'), window.origin], + 'request with remote URL': [new Request(EXAMPLE_URL + '/remote.html'), EXAMPLE_URL] + }).forEach(([t, [resource, expectedOrigin]]) => { + describe(`using ${t}`, () => { + it('calls request, passing origin', () => { + const request = sinon.stub(); + const fetch = fetcherFactory(1000, {request}); + fetch(resource); + sinon.assert.calledWith(request, expectedOrigin); + }); + + Object.entries({ + success: 'respond', + error: 'error' + }).forEach(([t, method]) => { + it(`calls done on ${t}, passing origin`, () => { + const done = sinon.stub(); + const fetch = fetcherFactory(1000, {done}); + const req = fetch(resource).catch(() => null).then(() => { + sinon.assert.calledWith(done, expectedOrigin); + }); + server.requests[0][method](); + return req; + }); + }); + }); + }); +}); + +describe('toFetchRequest', () => { + Object.entries({ + 'simple POST': { + url: EXAMPLE_URL, + data: 'data', + expect: { + request: { + url: EXAMPLE_URL + '/', + method: 'POST', + }, + text: 'data', + headers: { + 'content-type': 'text/plain' + } + } + }, + 'POST with headers': { + url: EXAMPLE_URL, + data: '{"json": "body"}', + options: { + contentType: 'application/json', + customHeaders: { + 'x-custom': 'value' + } + }, + expect: { + request: { + url: EXAMPLE_URL + '/', + method: 'POST', + }, + text: '{"json": "body"}', + headers: { + 'content-type': 'application/json', + 'X-Custom': 'value' + } + } + }, + 'simple GET': { + url: EXAMPLE_URL, + data: {p1: 'v1', p2: 'v2'}, + options: { + method: 'GET', + }, + expect: { + request: { + url: EXAMPLE_URL + '/?p1=v1&p2=v2', + method: 'GET' + }, + text: '', + headers: { + 'content-type': 'text/plain' + } + } + }, + 'GET with credentials': { + url: EXAMPLE_URL, + data: null, + options: { + method: 'GET', + withCredentials: true, + }, + expect: { + request: { + url: EXAMPLE_URL + '/', + method: 'GET', + credentials: 'include' + }, + text: '', + headers: { + 'content-type': 'text/plain' + } + } + } + }).forEach(([t, {url, data, options, expect: {request, text, headers}}]) => { + it(`can build ${t}`, () => { + const req = toFetchRequest(url, data, options); + return req.text().then(body => { + Object.entries(request).forEach(([prop, val]) => { + expect(req[prop]).to.eql(val); + }); + const hdr = new Headers(headers); + Array.from(req.headers.entries()).forEach(([name, val]) => { + expect(hdr.get(name)).to.eql(val); + }); + expect(body).to.eql(text); + }); + }); + }); + + describe('browsingTopics', () => { + Object.entries({ + 'browsingTopics = true': [{browsingTopics: true}, true], + 'browsingTopics = false': [{browsingTopics: false}, false], + 'browsingTopics is undef': [{}, false] + }).forEach(([t, [opts, shouldBeSet]]) => { + describe(`when options has ${t}`, () => { + const sandbox = sinon.createSandbox(); + afterEach(() => { + sandbox.restore(); + }); + + it(`should ${!shouldBeSet ? 'not ' : ''}be set when in a secure context`, () => { + sandbox.stub(window, 'isSecureContext').get(() => true); + toFetchRequest(EXAMPLE_URL, null, opts); + sinon.assert.calledWithMatch(dep.makeRequest, sinon.match.any, {browsingTopics: shouldBeSet ? true : undefined}); + }); + it(`should not be set when not in a secure context`, () => { + sandbox.stub(window, 'isSecureContext').get(() => false); + toFetchRequest(EXAMPLE_URL, null, opts); + sinon.assert.calledWithMatch(dep.makeRequest, sinon.match.any, {browsingTopics: undefined}); + }); + }) + }) + }) +}); + +describe('attachCallbacks', () => { + const sampleHeaders = new Headers({ + 'x-1': 'v1', + 'x-2': 'v2' + }); + + function responseFactory(body, props) { + props = Object.assign({headers: sampleHeaders, url: EXAMPLE_URL}, props); + return function () { + return { + response: Object.defineProperties(new Response(body, props), { + url: { + get: () => props.url + } + }), + body: body || '' + }; + }; + } + + function expectNullXHR(response, reason) { + return new Promise((resolve, reject) => { + attachCallbacks(Promise.resolve(response), { + success: () => { + reject(new Error('should not succeed')); + }, + error(statusText, xhr) { + expect(statusText).to.eql(''); + sinon.assert.match(xhr, { + readyState: XMLHttpRequest.DONE, + status: 0, + statusText: '', + responseText: '', + response: '', + responseXML: null, + reason + }); + expect(xhr.getResponseHeader('any')).to.be.null; + resolve(); + } + }); + }); + } + + it('runs error callback on rejections', () => { + const err = new Error(); + return expectNullXHR(Promise.reject(err), err); + }); + + it('sets timedOut = true on fetch timeout', (done) => { + const ctl = new AbortController(); + ctl.abort(); + attachCallbacks(fetch('/', {signal: ctl.signal}), { + error(_, xhr) { + expect(xhr.timedOut).to.be.true; + done(); + } + }); + }) + + Object.entries({ + '2xx response': { + success: true, + makeResponse: responseFactory('body', {status: 200, statusText: 'OK'}) + }, + '2xx response with no body': { + success: true, + makeResponse: responseFactory(null, {status: 204, statusText: 'No content'}) + }, + '2xx response with XML': { + success: true, + xml: true, + makeResponse: responseFactory('', { + status: 200, + statusText: 'OK', + headers: {'content-type': 'application/xml;charset=UTF8'} + }) + }, + '2xx response with HTML': { + success: true, + xml: true, + makeResponse: responseFactory('

', { + status: 200, + statusText: 'OK', + headers: {'content-type': 'text/html;charset=UTF-8'} + }) + }, + '304 response': { + success: true, + makeResponse: responseFactory(null, {status: 304, statusText: 'Moved permanently'}) + }, + '4xx response': { + success: false, + makeResponse: responseFactory('body', {status: 400, statusText: 'Invalid request'}) + }, + '5xx response': { + success: false, + makeResponse: responseFactory('body', {status: 503, statusText: 'Gateway error'}) + }, + '4xx response with XML': { + success: false, + xml: true, + makeResponse: responseFactory('', { + status: 404, + statusText: 'Not found', + headers: { + 'content-type': 'application/xml' + } + }) + } + }).forEach(([t, {success, makeResponse, xml}]) => { + const cbType = success ? 'success' : 'error'; + + describe(`for ${t}`, () => { + let sandbox, response, body; + beforeEach(() => { + sandbox = sinon.sandbox.create(); + sandbox.spy(utils, 'logError'); + ({response, body} = makeResponse()); + }); + + afterEach(() => { + sandbox.restore(); + }) + + function checkXHR(xhr) { + utils.logError.resetHistory(); + const serialized = JSON.parse(JSON.stringify(xhr)) + // serialization of `responseXML` should not generate console messages + sinon.assert.notCalled(utils.logError); + + sinon.assert.match(serialized, { + readyState: XMLHttpRequest.DONE, + status: response.status, + statusText: response.statusText, + responseType: '', + responseURL: response.url, + response: body, + responseText: body, + }); + if (xml) { + expect(xhr.responseXML.querySelectorAll('*').length > 0).to.be.true; + } else { + expect(serialized.responseXML).to.not.exist; + } + Array.from(response.headers.entries()).forEach(([name, value]) => { + expect(xhr.getResponseHeader(name)).to.eql(value); + }); + expect(xhr.getResponseHeader('$$missing-header')).to.be.null; + } + + it(`runs ${cbType} callback`, (done) => { + attachCallbacks(Promise.resolve(response), { + success(payload, xhr) { + expect(success).to.be.true; + expect(payload).to.eql(body); + checkXHR(xhr); + done(); + }, + error(statusText, xhr) { + expect(success).to.be.false; + expect(statusText).to.eql(response.statusText); + checkXHR(xhr); + done(); + } + }); + }); + + it(`runs error callback if body cannot be retrieved`, () => { + const err = new Error(); + response.text = () => Promise.reject(err); + return expectNullXHR(response, err); + }); + + if (success) { + it('accepts a single function as success callback', (done) => { + attachCallbacks(Promise.resolve(response), function (payload, xhr) { + expect(payload).to.eql(body); + checkXHR(xhr); + done(); + }) + }) + } + }); + }); + + describe('callback exceptions', () => { + Object.entries({ + success: responseFactory(null, {status: 204}), + error: responseFactory('', {status: 400}), + }).forEach(([cbType, makeResponse]) => { + it(`do not choke ${cbType} callbacks`, () => { + const {response} = makeResponse(); + return new Promise((resolve) => { + const result = {success: false, error: false}; + attachCallbacks(Promise.resolve(response), { + success() { + result.success = true; + throw new Error(); + }, + error() { + result.error = true; + throw new Error(); + } + }); + setTimeout(() => resolve(result), 20); + }).then(result => { + Object.entries(result).forEach(([typ, ran]) => { + expect(ran).to.be[typ === cbType ? 'true' : 'false'] + }) + }); + }); + }); + }); +}); diff --git a/test/spec/unit/core/bidderFactory_spec.js b/test/spec/unit/core/bidderFactory_spec.js index 751c90af50f..16042ca52dd 100644 --- a/test/spec/unit/core/bidderFactory_spec.js +++ b/test/spec/unit/core/bidderFactory_spec.js @@ -13,9 +13,8 @@ import {stubAuctionIndex} from '../../../helpers/indexStub.js'; import {bidderSettings} from '../../../../src/bidderSettings.js'; import {decorateAdUnitsWithNativeParams} from '../../../../src/native.js'; import * as activityRules from 'src/activities/rules.js'; -import {sandbox} from 'sinon'; import {MODULE_TYPE_BIDDER} from '../../../../src/activities/modules.js'; -import {ACTIVITY_TRANSMIT_TID} from '../../../../src/activities/activities.js'; +import {ACTIVITY_TRANSMIT_TID, ACTIVITY_TRANSMIT_UFPD} from '../../../../src/activities/activities.js'; const CODE = 'sampleBidder'; const MOCK_BIDS_REQUEST = { @@ -39,133 +38,112 @@ const MOCK_BIDS_REQUEST = { ] } -function onTimelyResponseStub() { - -} - before(() => { hook.ready(); }); let wrappedCallback = config.callbackWithBidder(CODE); -describe('bidders created by newBidder', function () { - let spec; - let bidder; - let addBidResponseStub; - let doneStub; - - beforeEach(function () { - spec = { - code: CODE, - isBidRequestValid: sinon.stub(), - buildRequests: sinon.stub(), - interpretResponse: sinon.stub(), - getUserSyncs: sinon.stub() - }; - - addBidResponseStub = sinon.stub(); - addBidResponseStub.reject = sinon.stub(); - doneStub = sinon.stub(); - }); - - describe('when the ajax response is irrelevant', function () { - let sandbox; - let ajaxStub; - let getConfigSpy; - let aliasRegistryStub, aliasRegistry; +describe('bidderFactory', () => { + let onTimelyResponseStub; + beforeEach(() => { + onTimelyResponseStub = sinon.stub(); + }) + describe('bidders created by newBidder', function () { + let spec; + let bidder; + let addBidResponseStub; + let doneStub; beforeEach(function () { - sandbox = sinon.sandbox.create(); - sandbox.stub(activityRules, 'isActivityAllowed').callsFake(() => true); - ajaxStub = sandbox.stub(ajax, 'ajax'); - addBidResponseStub.reset(); - getConfigSpy = sandbox.spy(config, 'getConfig'); - doneStub.reset(); - aliasRegistry = {}; - aliasRegistryStub = sandbox.stub(adapterManager, 'aliasRegistry'); - aliasRegistryStub.get(() => aliasRegistry); - }); + spec = { + code: CODE, + isBidRequestValid: sinon.stub(), + buildRequests: sinon.stub(), + interpretResponse: sinon.stub(), + getUserSyncs: sinon.stub() + }; - afterEach(function () { - sandbox.restore(); + addBidResponseStub = sinon.stub(); + addBidResponseStub.reject = sinon.stub(); + doneStub = sinon.stub(); }); - it('should let registerSyncs run with invalid alias and aliasSync enabled', function () { - config.setConfig({ - userSync: { - aliasSyncEnabled: true - } - }); - spec.code = 'fakeBidder'; - const bidder = newBidder(spec); - bidder.callBids({ bids: [] }, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); - expect(getConfigSpy.withArgs('userSync.filterSettings').calledOnce).to.equal(true); - }); + describe('when the ajax response is irrelevant', function () { + let sandbox; + let ajaxStub; + let getConfigSpy; + let aliasRegistryStub, aliasRegistry; - it('should let registerSyncs run with valid alias and aliasSync enabled', function () { - config.setConfig({ - userSync: { - aliasSyncEnabled: true - } + beforeEach(function () { + sandbox = sinon.sandbox.create(); + sandbox.stub(activityRules, 'isActivityAllowed').callsFake(() => true); + ajaxStub = sandbox.stub(ajax, 'ajax'); + addBidResponseStub.reset(); + getConfigSpy = sandbox.spy(config, 'getConfig'); + doneStub.reset(); + aliasRegistry = {}; + aliasRegistryStub = sandbox.stub(adapterManager, 'aliasRegistry'); + aliasRegistryStub.get(() => aliasRegistry); }); - spec.code = 'aliasBidder'; - const bidder = newBidder(spec); - bidder.callBids({ bids: [] }, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); - expect(getConfigSpy.withArgs('userSync.filterSettings').calledOnce).to.equal(true); - }); - it('should let registerSyncs run with invalid alias and aliasSync disabled', function () { - config.setConfig({ - userSync: { - aliasSyncEnabled: false - } + afterEach(function () { + sandbox.restore(); }); - spec.code = 'fakeBidder'; - const bidder = newBidder(spec); - bidder.callBids({ bids: [] }, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); - expect(getConfigSpy.withArgs('userSync.filterSettings').calledOnce).to.equal(true); - }); - it('should not let registerSyncs run with valid alias and aliasSync disabled', function () { - config.setConfig({ - userSync: { - aliasSyncEnabled: false - } + it('should let registerSyncs run with invalid alias and aliasSync enabled', function () { + config.setConfig({ + userSync: { + aliasSyncEnabled: true + } + }); + spec.code = 'fakeBidder'; + const bidder = newBidder(spec); + bidder.callBids({ bids: [] }, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); + expect(getConfigSpy.withArgs('userSync.filterSettings').calledOnce).to.equal(true); }); - spec.code = 'aliasBidder'; - const bidder = newBidder(spec); - aliasRegistry = {[spec.code]: CODE}; - bidder.callBids({ bids: [] }, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); - expect(getConfigSpy.withArgs('userSync.filterSettings').calledOnce).to.equal(false); - }); - describe('transaction IDs', () => { - Object.entries({ - 'be hidden': false, - 'not be hidden': true, - }).forEach(([t, allowed]) => { - const expectation = allowed ? (val) => expect(val).to.exist : (val) => expect(val).to.not.exist; + it('should let registerSyncs run with valid alias and aliasSync enabled', function () { + config.setConfig({ + userSync: { + aliasSyncEnabled: true + } + }); + spec.code = 'aliasBidder'; + const bidder = newBidder(spec); + bidder.callBids({ bids: [] }, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); + expect(getConfigSpy.withArgs('userSync.filterSettings').calledOnce).to.equal(true); + }); - function checkBidRequest(br) { - ['auctionId', 'transactionId'].forEach((prop) => expectation(br[prop])); - } + it('should let registerSyncs run with invalid alias and aliasSync disabled', function () { + config.setConfig({ + userSync: { + aliasSyncEnabled: false + } + }); + spec.code = 'fakeBidder'; + const bidder = newBidder(spec); + bidder.callBids({ bids: [] }, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); + expect(getConfigSpy.withArgs('userSync.filterSettings').calledOnce).to.equal(true); + }); - function checkBidderRequest(br) { - expectation(br.auctionId); - br.bids.forEach(checkBidRequest); - } + it('should not let registerSyncs run with valid alias and aliasSync disabled', function () { + config.setConfig({ + userSync: { + aliasSyncEnabled: false + } + }); + spec.code = 'aliasBidder'; + const bidder = newBidder(spec); + aliasRegistry = {[spec.code]: CODE}; + bidder.callBids({ bids: [] }, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); + expect(getConfigSpy.withArgs('userSync.filterSettings').calledOnce).to.equal(false); + }); - it(`should ${t} from the spec logic when the transmitTid activity is${allowed ? '' : ' not'} allowed`, () => { - spec.isBidRequestValid.callsFake(br => { - checkBidRequest(br); - return true; - }); - spec.buildRequests.callsFake((bidReqs, bidderReq) => { - checkBidderRequest(bidderReq); - bidReqs.forEach(checkBidRequest); - return {method: 'POST'}; - }); + describe('transaction IDs', () => { + beforeEach(() => { + activityRules.isActivityAllowed.reset(); + ajaxStub.callsFake((_, callback) => callback.success(null, {getResponseHeader: sinon.stub()})); spec.interpretResponse.callsFake(() => [ { requestId: 'bid', @@ -176,599 +154,929 @@ describe('bidders created by newBidder', function () { currency: 'USD' } ]) - activityRules.isActivityAllowed.reset(); - activityRules.isActivityAllowed.callsFake(() => allowed); + }); - ajaxStub.callsFake((_, callback) => callback.success(null, {getResponseHeader: sinon.stub()})); + Object.entries({ + 'be hidden': false, + 'not be hidden': true, + }).forEach(([t, allowed]) => { + const expectation = allowed ? (val) => expect(val).to.exist : (val) => expect(val).to.not.exist; - const bidder = newBidder(spec); + function checkBidRequest(br) { + ['auctionId', 'transactionId'].forEach((prop) => expectation(br[prop])); + } + + function checkBidderRequest(br) { + expectation(br.auctionId); + br.bids.forEach(checkBidRequest); + } + + it(`should ${t} from the spec logic when the transmitTid activity is${allowed ? '' : ' not'} allowed`, () => { + spec.isBidRequestValid.callsFake(br => { + checkBidRequest(br); + return true; + }); + spec.buildRequests.callsFake((bidReqs, bidderReq) => { + checkBidderRequest(bidderReq); + bidReqs.forEach(checkBidRequest); + return {method: 'POST'}; + }); + activityRules.isActivityAllowed.callsFake(() => allowed); + + const bidder = newBidder(spec); + + bidder.callBids({ + bidderCode: 'mockBidder', + auctionId: 'aid', + bids: [ + { + adUnitCode: 'mockAU', + bidId: 'bid', + transactionId: 'tid', + auctionId: 'aid' + } + ] + }, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); + + sinon.assert.calledWithMatch(activityRules.isActivityAllowed, ACTIVITY_TRANSMIT_TID, { + componentType: MODULE_TYPE_BIDDER, + componentName: 'mockBidder' + }); + sinon.assert.calledWithMatch(addBidResponseStub, sinon.match.any, { + transactionId: 'tid', + auctionId: 'aid' + }) + }); + }); - bidder.callBids({ + it('should not be hidden from request methods', (done) => { + const bidderRequest = { bidderCode: 'mockBidder', auctionId: 'aid', + getAID() { return this.auctionId }, bids: [ { adUnitCode: 'mockAU', bidId: 'bid', transactionId: 'tid', - auctionId: 'aid' + auctionId: 'aid', + getTIDs() { + return [this.auctionId, this.transactionId] + } } ] - }, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); - - sinon.assert.calledWithMatch(activityRules.isActivityAllowed, ACTIVITY_TRANSMIT_TID, { - componentType: MODULE_TYPE_BIDDER, - componentName: 'mockBidder' + }; + activityRules.isActivityAllowed.callsFake(() => false); + spec.isBidRequestValid.returns(true); + spec.buildRequests.callsFake((reqs, bidderReq) => { + expect(bidderReq.getAID()).to.eql('aid'); + expect(reqs[0].getTIDs()).to.eql(['aid', 'tid']); + done(); }); - sinon.assert.calledWithMatch(addBidResponseStub, sinon.match.any, { - transactionId: 'tid', - auctionId: 'aid' - }) - }); + newBidder(spec).callBids(bidderRequest, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); + }) }); - }); - - it('should handle bad bid requests gracefully', function () { - const bidder = newBidder(spec); - spec.getUserSyncs.returns([]); + it('should handle bad bid requests gracefully', function () { + const bidder = newBidder(spec); - bidder.callBids({}); - bidder.callBids({ bids: 'nothing useful' }, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); + spec.getUserSyncs.returns([]); - expect(ajaxStub.called).to.equal(false); - expect(spec.isBidRequestValid.called).to.equal(false); - expect(spec.buildRequests.called).to.equal(false); - expect(spec.interpretResponse.called).to.equal(false); - }); + bidder.callBids({}); + bidder.callBids({ bids: 'nothing useful' }, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); - it('should call buildRequests(bidRequest) the params are valid', function () { - const bidder = newBidder(spec); + expect(ajaxStub.called).to.equal(false); + expect(spec.isBidRequestValid.called).to.equal(false); + expect(spec.buildRequests.called).to.equal(false); + expect(spec.interpretResponse.called).to.equal(false); + }); - spec.isBidRequestValid.returns(true); - spec.buildRequests.returns([]); + it('should call buildRequests(bidRequest) the params are valid', function () { + const bidder = newBidder(spec); - bidder.callBids(MOCK_BIDS_REQUEST, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); + spec.isBidRequestValid.returns(true); + spec.buildRequests.returns([]); - expect(ajaxStub.called).to.equal(false); - expect(spec.isBidRequestValid.calledTwice).to.equal(true); - expect(spec.buildRequests.calledOnce).to.equal(true); - expect(spec.buildRequests.firstCall.args[0]).to.deep.equal(MOCK_BIDS_REQUEST.bids); - }); + bidder.callBids(MOCK_BIDS_REQUEST, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); - it('should not call buildRequests the params are invalid', function () { - const bidder = newBidder(spec); + expect(ajaxStub.called).to.equal(false); + expect(spec.isBidRequestValid.calledTwice).to.equal(true); + expect(spec.buildRequests.calledOnce).to.equal(true); + expect(spec.buildRequests.firstCall.args[0]).to.deep.equal(MOCK_BIDS_REQUEST.bids); + }); - spec.isBidRequestValid.returns(false); - spec.buildRequests.returns([]); + it('should not call buildRequests the params are invalid', function () { + const bidder = newBidder(spec); - bidder.callBids(MOCK_BIDS_REQUEST, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); + spec.isBidRequestValid.returns(false); + spec.buildRequests.returns([]); - expect(ajaxStub.called).to.equal(false); - expect(spec.isBidRequestValid.calledTwice).to.equal(true); - expect(spec.buildRequests.called).to.equal(false); - }); + bidder.callBids(MOCK_BIDS_REQUEST, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); - it('should filter out invalid bids before calling buildRequests', function () { - const bidder = newBidder(spec); + expect(ajaxStub.called).to.equal(false); + expect(spec.isBidRequestValid.calledTwice).to.equal(true); + expect(spec.buildRequests.called).to.equal(false); + }); - spec.isBidRequestValid.onFirstCall().returns(true); - spec.isBidRequestValid.onSecondCall().returns(false); - spec.buildRequests.returns([]); + it('should filter out invalid bids before calling buildRequests', function () { + const bidder = newBidder(spec); - bidder.callBids(MOCK_BIDS_REQUEST, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); + spec.isBidRequestValid.onFirstCall().returns(true); + spec.isBidRequestValid.onSecondCall().returns(false); + spec.buildRequests.returns([]); - expect(ajaxStub.called).to.equal(false); - expect(spec.isBidRequestValid.calledTwice).to.equal(true); - expect(spec.buildRequests.calledOnce).to.equal(true); - expect(spec.buildRequests.firstCall.args[0]).to.deep.equal([MOCK_BIDS_REQUEST.bids[0]]); - }); + bidder.callBids(MOCK_BIDS_REQUEST, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); - it('should make no server requests if the spec doesn\'t return any', function () { - const bidder = newBidder(spec); + expect(ajaxStub.called).to.equal(false); + expect(spec.isBidRequestValid.calledTwice).to.equal(true); + expect(spec.buildRequests.calledOnce).to.equal(true); + expect(spec.buildRequests.firstCall.args[0]).to.deep.equal([MOCK_BIDS_REQUEST.bids[0]]); + }); - spec.isBidRequestValid.returns(true); - spec.buildRequests.returns([]); + it('should make no server requests if the spec doesn\'t return any', function () { + const bidder = newBidder(spec); - bidder.callBids(MOCK_BIDS_REQUEST, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); + spec.isBidRequestValid.returns(true); + spec.buildRequests.returns([]); - expect(ajaxStub.called).to.equal(false); - }); + bidder.callBids(MOCK_BIDS_REQUEST, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); - it('should make the appropriate POST request', function () { - const bidder = newBidder(spec); - const url = 'test.url.com'; - const data = { arg: 2 }; - spec.isBidRequestValid.returns(true); - spec.buildRequests.returns({ - method: 'POST', - url: url, - data: data + expect(ajaxStub.called).to.equal(false); }); - bidder.callBids(MOCK_BIDS_REQUEST, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); + it('should make the appropriate POST request', function () { + const bidder = newBidder(spec); + const url = 'test.url.com'; + const data = { arg: 2 }; + spec.isBidRequestValid.returns(true); + spec.buildRequests.returns({ + method: 'POST', + url: url, + data: data + }); - expect(ajaxStub.calledOnce).to.equal(true); - expect(ajaxStub.firstCall.args[0]).to.equal(url); - expect(ajaxStub.firstCall.args[2]).to.equal(JSON.stringify(data)); - expect(ajaxStub.firstCall.args[3]).to.deep.equal({ - method: 'POST', - contentType: 'text/plain', - withCredentials: true - }); - }); + bidder.callBids(MOCK_BIDS_REQUEST, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); - it('should make the appropriate POST request when options are passed', function () { - const bidder = newBidder(spec); - const url = 'test.url.com'; - const data = { arg: 2 }; - const options = { contentType: 'application/json' }; - spec.isBidRequestValid.returns(true); - spec.buildRequests.returns({ - method: 'POST', - url: url, - data: data, - options: options + expect(ajaxStub.calledOnce).to.equal(true); + expect(ajaxStub.firstCall.args[0]).to.equal(url); + expect(ajaxStub.firstCall.args[2]).to.equal(JSON.stringify(data)); + sinon.assert.match(ajaxStub.firstCall.args[3], { + method: 'POST', + contentType: 'text/plain', + withCredentials: true + }); }); - bidder.callBids(MOCK_BIDS_REQUEST, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); + it('should make the appropriate POST request when options are passed', function () { + const bidder = newBidder(spec); + const url = 'test.url.com'; + const data = { arg: 2 }; + const options = { contentType: 'application/json' }; + spec.isBidRequestValid.returns(true); + spec.buildRequests.returns({ + method: 'POST', + url: url, + data: data, + options: options + }); - expect(ajaxStub.calledOnce).to.equal(true); - expect(ajaxStub.firstCall.args[0]).to.equal(url); - expect(ajaxStub.firstCall.args[2]).to.equal(JSON.stringify(data)); - expect(ajaxStub.firstCall.args[3]).to.deep.equal({ - method: 'POST', - contentType: 'application/json', - withCredentials: true - }); - }); + bidder.callBids(MOCK_BIDS_REQUEST, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); - it('should make the appropriate GET request', function () { - const bidder = newBidder(spec); - const url = 'test.url.com'; - const data = { arg: 2 }; - spec.isBidRequestValid.returns(true); - spec.buildRequests.returns({ - method: 'GET', - url: url, - data: data + expect(ajaxStub.calledOnce).to.equal(true); + expect(ajaxStub.firstCall.args[0]).to.equal(url); + expect(ajaxStub.firstCall.args[2]).to.equal(JSON.stringify(data)); + sinon.assert.match(ajaxStub.firstCall.args[3], { + method: 'POST', + contentType: 'application/json', + withCredentials: true + }) }); - bidder.callBids(MOCK_BIDS_REQUEST, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); + it('should make the appropriate GET request', function () { + const bidder = newBidder(spec); + const url = 'test.url.com'; + const data = { arg: 2 }; + spec.isBidRequestValid.returns(true); + spec.buildRequests.returns({ + method: 'GET', + url: url, + data: data + }); + + bidder.callBids(MOCK_BIDS_REQUEST, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); - expect(ajaxStub.calledOnce).to.equal(true); - expect(ajaxStub.firstCall.args[0]).to.equal(`${url}?arg=2`); - expect(ajaxStub.firstCall.args[2]).to.be.undefined; - expect(ajaxStub.firstCall.args[3]).to.deep.equal({ - method: 'GET', - withCredentials: true + expect(ajaxStub.calledOnce).to.equal(true); + expect(ajaxStub.firstCall.args[0]).to.equal(`${url}?arg=2`); + expect(ajaxStub.firstCall.args[2]).to.be.undefined; + sinon.assert.match(ajaxStub.firstCall.args[3], { + method: 'GET', + withCredentials: true + }) }); - }); - it('should make the appropriate GET request when options are passed', function () { - const bidder = newBidder(spec); - const url = 'test.url.com'; - const data = { arg: 2 }; - const opt = { withCredentials: false } - spec.isBidRequestValid.returns(true); - spec.buildRequests.returns({ - method: 'GET', - url: url, - data: data, - options: opt + it('should make the appropriate GET request when options are passed', function () { + const bidder = newBidder(spec); + const url = 'test.url.com'; + const data = { arg: 2 }; + const opt = { withCredentials: false } + spec.isBidRequestValid.returns(true); + spec.buildRequests.returns({ + method: 'GET', + url: url, + data: data, + options: opt + }); + + bidder.callBids(MOCK_BIDS_REQUEST, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); + + expect(ajaxStub.calledOnce).to.equal(true); + expect(ajaxStub.firstCall.args[0]).to.equal(`${url}?arg=2`); + expect(ajaxStub.firstCall.args[2]).to.be.undefined; + sinon.assert.match(ajaxStub.firstCall.args[3], { + method: 'GET', + withCredentials: false + }) }); - bidder.callBids(MOCK_BIDS_REQUEST, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); + it('should make multiple calls if the spec returns them', function () { + const bidder = newBidder(spec); + const url = 'test.url.com'; + const data = { arg: 2 }; + spec.isBidRequestValid.returns(true); + spec.buildRequests.returns([ + { + method: 'POST', + url: url, + data: data + }, + { + method: 'GET', + url: url, + data: data + } + ]); - expect(ajaxStub.calledOnce).to.equal(true); - expect(ajaxStub.firstCall.args[0]).to.equal(`${url}?arg=2`); - expect(ajaxStub.firstCall.args[2]).to.be.undefined; - expect(ajaxStub.firstCall.args[3]).to.deep.equal({ - method: 'GET', - withCredentials: false + bidder.callBids(MOCK_BIDS_REQUEST, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); + + expect(ajaxStub.calledTwice).to.equal(true); }); - }); - it('should make multiple calls if the spec returns them', function () { - const bidder = newBidder(spec); - const url = 'test.url.com'; - const data = { arg: 2 }; - spec.isBidRequestValid.returns(true); - spec.buildRequests.returns([ - { - method: 'POST', - url: url, - data: data - }, - { - method: 'GET', - url: url, - data: data - } - ]); + describe('browsingTopics ajax option', () => { + let transmitUfpdAllowed, bidder, origBS; + before(() => { + origBS = window.$$PREBID_GLOBAL$$.bidderSettings; + }) - bidder.callBids(MOCK_BIDS_REQUEST, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); + after(() => { + window.$$PREBID_GLOBAL$$.bidderSettings = origBS; + }); - expect(ajaxStub.calledTwice).to.equal(true); - }); + beforeEach(() => { + activityRules.isActivityAllowed.reset(); + activityRules.isActivityAllowed.callsFake((activity) => activity === ACTIVITY_TRANSMIT_UFPD ? transmitUfpdAllowed : true); + bidder = newBidder(spec); + spec.isBidRequestValid.returns(true); + }); - it('should not add bids for each placement code if no requests are given', function () { - const bidder = newBidder(spec); + it(`should be set to false when adapter sets browsingTopics = false`, () => { + transmitUfpdAllowed = true; + spec.buildRequests.returns([ + { + method: 'GET', + url: 'url', + options: { + browsingTopics: false + } + } + ]); + bidder.callBids(MOCK_BIDS_REQUEST, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); + sinon.assert.calledWith(ajaxStub, 'url', sinon.match.any, sinon.match.any, sinon.match({ + browsingTopics: false + })); + }); - spec.isBidRequestValid.returns(true); - spec.buildRequests.returns([]); - spec.interpretResponse.returns([]); - spec.getUserSyncs.returns([]); + Object.entries({ + 'omitted': [undefined, true], + 'enabled': [true, true], + 'disabled': [false, false] + }).forEach(([t, [topicsHeader, enabled]]) => { + describe(`when bidderSettings.topicsHeader is ${t}`, () => { + beforeEach(() => { + window.$$PREBID_GLOBAL$$.bidderSettings = { + [CODE]: { + topicsHeader: topicsHeader + } + } + }); + + afterEach(() => { + delete window.$$PREBID_GLOBAL$$.bidderSettings[CODE]; + }); + + Object.entries({ + 'allowed': true, + 'not allowed': false + }).forEach(([t, allow]) => { + const shouldBeSet = allow && enabled; + + it(`should be set to ${shouldBeSet} when transmitUfpd is ${t}`, () => { + transmitUfpdAllowed = allow; + spec.buildRequests.returns([ + { + method: 'GET', + url: '1', + }, + { + method: 'POST', + url: '2', + data: {} + }, + { + method: 'GET', + url: '3', + options: { + browsingTopics: true + } + }, + { + method: 'POST', + url: '4', + data: {}, + options: { + browsingTopics: true + } + } + ]); + bidder.callBids(MOCK_BIDS_REQUEST, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); + ['1', '2', '3', '4'].forEach(url => { + sinon.assert.calledWith( + ajaxStub, + url, + sinon.match.any, + sinon.match.any, + sinon.match({browsingTopics: shouldBeSet}) + ); + }); + }); + }); + }) + }) + }); - bidder.callBids(MOCK_BIDS_REQUEST, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); + it('should not add bids for each placement code if no requests are given', function () { + const bidder = newBidder(spec); - expect(addBidResponseStub.callCount).to.equal(0); - }); + spec.isBidRequestValid.returns(true); + spec.buildRequests.returns([]); + spec.interpretResponse.returns([]); + spec.getUserSyncs.returns([]); - it('should emit BEFORE_BIDDER_HTTP events before network requests', function () { - const bidder = newBidder(spec); - const req = { - method: 'POST', - url: 'test.url.com', - data: { arg: 2 } - }; + bidder.callBids(MOCK_BIDS_REQUEST, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); - spec.isBidRequestValid.returns(true); - spec.buildRequests.returns([req, req]); + expect(addBidResponseStub.callCount).to.equal(0); + }); - const eventEmitterSpy = sinon.spy(events, 'emit'); - bidder.callBids(MOCK_BIDS_REQUEST, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); + it('should emit BEFORE_BIDDER_HTTP events before network requests', function () { + const bidder = newBidder(spec); + const req = { + method: 'POST', + url: 'test.url.com', + data: { arg: 2 } + }; - expect(ajaxStub.calledTwice).to.equal(true); - expect(eventEmitterSpy.getCalls() - .filter(call => call.args[0] === CONSTANTS.EVENTS.BEFORE_BIDDER_HTTP) - ).to.length(2); + spec.isBidRequestValid.returns(true); + spec.buildRequests.returns([req, req]); - eventEmitterSpy.restore(); - }); - }); + const eventEmitterSpy = sinon.spy(events, 'emit'); + bidder.callBids(MOCK_BIDS_REQUEST, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); - describe('when the ajax call succeeds', function () { - let ajaxStub; - let userSyncStub; - let logErrorSpy; + expect(ajaxStub.calledTwice).to.equal(true); + expect(eventEmitterSpy.getCalls() + .filter(call => call.args[0] === CONSTANTS.EVENTS.BEFORE_BIDDER_HTTP) + ).to.length(2); - beforeEach(function () { - ajaxStub = sinon.stub(ajax, 'ajax').callsFake(function(url, callbacks) { - const fakeResponse = sinon.stub(); - fakeResponse.returns('headerContent'); - callbacks.success('response body', { getResponseHeader: fakeResponse }); + eventEmitterSpy.restore(); }); - addBidResponseStub.reset(); - doneStub.resetBehavior(); - userSyncStub = sinon.stub(userSync, 'registerSync') - logErrorSpy = sinon.spy(utils, 'logError'); }); - afterEach(function () { - ajaxStub.restore(); - userSyncStub.restore(); - utils.logError.restore(); - }); + describe('when the ajax call succeeds', function () { + let ajaxStub; + let userSyncStub; + let logErrorSpy; - it('should call spec.interpretResponse() with the response content', function () { - const bidder = newBidder(spec); + beforeEach(function () { + ajaxStub = sinon.stub(ajax, 'ajax').callsFake(function(url, callbacks) { + const fakeResponse = sinon.stub(); + fakeResponse.returns('headerContent'); + callbacks.success('response body', { getResponseHeader: fakeResponse }); + }); + addBidResponseStub.reset(); + doneStub.resetBehavior(); + userSyncStub = sinon.stub(userSync, 'registerSync') + logErrorSpy = sinon.spy(utils, 'logError'); + }); - spec.isBidRequestValid.returns(true); - spec.buildRequests.returns({ - method: 'POST', - url: 'test.url.com', - data: {} + afterEach(function () { + ajaxStub.restore(); + userSyncStub.restore(); + utils.logError.restore(); }); - spec.getUserSyncs.returns([]); - bidder.callBids(MOCK_BIDS_REQUEST, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); + it('should call onTimelyResponse', () => { + const bidder = newBidder(spec); + spec.isBidRequestValid.returns(true); + spec.buildRequests.returns({method: 'POST', url: 'test', data: {}}); + bidder.callBids(MOCK_BIDS_REQUEST, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); + sinon.assert.called(onTimelyResponseStub); + }) - expect(spec.interpretResponse.calledOnce).to.equal(true); - const response = spec.interpretResponse.firstCall.args[0] - expect(response.body).to.equal('response body') - expect(response.headers.get('some-header')).to.equal('headerContent'); - expect(spec.interpretResponse.firstCall.args[1]).to.deep.equal({ - method: 'POST', - url: 'test.url.com', - data: {} + it('should call spec.interpretResponse() with the response content', function () { + const bidder = newBidder(spec); + + spec.isBidRequestValid.returns(true); + spec.buildRequests.returns({ + method: 'POST', + url: 'test.url.com', + data: {} + }); + spec.getUserSyncs.returns([]); + + bidder.callBids(MOCK_BIDS_REQUEST, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); + + expect(spec.interpretResponse.calledOnce).to.equal(true); + const response = spec.interpretResponse.firstCall.args[0] + expect(response.body).to.equal('response body') + expect(response.headers.get('some-header')).to.equal('headerContent'); + expect(spec.interpretResponse.firstCall.args[1]).to.deep.equal({ + method: 'POST', + url: 'test.url.com', + data: {} + }); + expect(doneStub.calledOnce).to.equal(true); }); - expect(doneStub.calledOnce).to.equal(true); - }); - it('should call spec.interpretResponse() once for each request made', function () { - const bidder = newBidder(spec); + it('should call spec.interpretResponse() once for each request made', function () { + const bidder = newBidder(spec); - spec.isBidRequestValid.returns(true); - spec.buildRequests.returns([ - { + spec.isBidRequestValid.returns(true); + spec.buildRequests.returns([ + { + method: 'POST', + url: 'test.url.com', + data: {} + }, + { + method: 'POST', + url: 'test.url.com', + data: {} + }, + ]); + spec.getUserSyncs.returns([]); + + bidder.callBids(MOCK_BIDS_REQUEST, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); + + expect(spec.interpretResponse.calledTwice).to.equal(true); + expect(doneStub.calledOnce).to.equal(true); + }); + + it('should only add bids for valid adUnit code into the auction, even if the bidder doesn\'t bid on all of them', function () { + const bidder = newBidder(spec); + + const bid = { + creativeId: 'creative-id', + requestId: '1', + ad: 'ad-url.com', + cpm: 0.5, + height: 200, + width: 300, + adUnitCode: 'mock/placement', + currency: 'USD', + netRevenue: true, + ttl: 300, + bidderCode: 'sampleBidder', + sampleBidder: {advertiserId: '12345', networkId: '111222'} + }; + const bidderRequest = Object.assign({}, MOCK_BIDS_REQUEST); + bidderRequest.bids[0].bidder = 'sampleBidder'; + spec.isBidRequestValid.returns(true); + spec.buildRequests.returns({ method: 'POST', url: 'test.url.com', data: {} - }, - { + }); + spec.getUserSyncs.returns([]); + + spec.interpretResponse.returns(bid); + + bidder.callBids(bidderRequest, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); + + expect(addBidResponseStub.calledOnce).to.equal(true); + expect(addBidResponseStub.firstCall.args[0]).to.equal('mock/placement'); + let bidObject = addBidResponseStub.firstCall.args[1]; + // checking the fields added by our code + expect(bidObject.originalCpm).to.equal(bid.cpm); + expect(bidObject.originalCurrency).to.equal(bid.currency); + expect(doneStub.calledOnce).to.equal(true); + expect(logErrorSpy.callCount).to.equal(0); + expect(bidObject.meta).to.exist; + expect(bidObject.meta).to.deep.equal({advertiserId: '12345', networkId: '111222'}); + }); + + it('should call spec.getUserSyncs() with the response', function () { + const bidder = newBidder(spec); + + spec.isBidRequestValid.returns(true); + spec.buildRequests.returns({ method: 'POST', url: 'test.url.com', data: {} - }, - ]); - spec.getUserSyncs.returns([]); + }); + spec.getUserSyncs.returns([]); - bidder.callBids(MOCK_BIDS_REQUEST, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); + bidder.callBids(MOCK_BIDS_REQUEST, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); - expect(spec.interpretResponse.calledTwice).to.equal(true); - expect(doneStub.calledOnce).to.equal(true); - }); + expect(spec.getUserSyncs.calledOnce).to.equal(true); + expect(spec.getUserSyncs.firstCall.args[1].length).to.equal(1); + expect(spec.getUserSyncs.firstCall.args[1][0].body).to.equal('response body'); + expect(spec.getUserSyncs.firstCall.args[1][0].headers).to.have.property('get'); + expect(spec.getUserSyncs.firstCall.args[1][0].headers.get).to.be.a('function'); + }); - it('should only add bids for valid adUnit code into the auction, even if the bidder doesn\'t bid on all of them', function () { - const bidder = newBidder(spec); + it('should register usersync pixels', function () { + const bidder = newBidder(spec); - const bid = { - creativeId: 'creative-id', - requestId: '1', - ad: 'ad-url.com', - cpm: 0.5, - height: 200, - width: 300, - adUnitCode: 'mock/placement', - currency: 'USD', - netRevenue: true, - ttl: 300, - bidderCode: 'sampleBidder', - sampleBidder: {advertiserId: '12345', networkId: '111222'} - }; - const bidderRequest = Object.assign({}, MOCK_BIDS_REQUEST); - bidderRequest.bids[0].bidder = 'sampleBidder'; - spec.isBidRequestValid.returns(true); - spec.buildRequests.returns({ - method: 'POST', - url: 'test.url.com', - data: {} + spec.isBidRequestValid.returns(false); + spec.buildRequests.returns([]); + spec.getUserSyncs.returns([{ + type: 'iframe', + url: 'usersync.com' + }]); + + bidder.callBids(MOCK_BIDS_REQUEST, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); + + expect(userSyncStub.called).to.equal(true); + expect(userSyncStub.firstCall.args[0]).to.equal('iframe'); + expect(userSyncStub.firstCall.args[1]).to.equal(spec.code); + expect(userSyncStub.firstCall.args[2]).to.equal('usersync.com'); }); - spec.getUserSyncs.returns([]); - spec.interpretResponse.returns(bid); + it('should logError and reject bid when required bid response params are missing', function () { + const bidder = newBidder(spec); - bidder.callBids(bidderRequest, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); + const bid = { + requestId: '1', + ad: 'ad-url.com', + cpm: 0.5, + height: 200, + width: 300, + placementCode: 'mock/placement' + }; + spec.isBidRequestValid.returns(true); + spec.buildRequests.returns({ + method: 'POST', + url: 'test.url.com', + data: {} + }); + spec.getUserSyncs.returns([]); - expect(addBidResponseStub.calledOnce).to.equal(true); - expect(addBidResponseStub.firstCall.args[0]).to.equal('mock/placement'); - let bidObject = addBidResponseStub.firstCall.args[1]; - // checking the fields added by our code - expect(bidObject.originalCpm).to.equal(bid.cpm); - expect(bidObject.originalCurrency).to.equal(bid.currency); - expect(doneStub.calledOnce).to.equal(true); - expect(logErrorSpy.callCount).to.equal(0); - expect(bidObject.meta).to.exist; - expect(bidObject.meta).to.deep.equal({advertiserId: '12345', networkId: '111222'}); - }); + spec.interpretResponse.returns(bid); - it('should call spec.getUserSyncs() with the response', function () { - const bidder = newBidder(spec); + bidder.callBids(MOCK_BIDS_REQUEST, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); - spec.isBidRequestValid.returns(true); - spec.buildRequests.returns({ - method: 'POST', - url: 'test.url.com', - data: {} + expect(logErrorSpy.calledOnce).to.equal(true); + expect(addBidResponseStub.reject.calledOnce).to.be.true; }); - spec.getUserSyncs.returns([]); - bidder.callBids(MOCK_BIDS_REQUEST, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); + it('should logError and reject bid when required response params are undefined', function () { + const bidder = newBidder(spec); - expect(spec.getUserSyncs.calledOnce).to.equal(true); - expect(spec.getUserSyncs.firstCall.args[1].length).to.equal(1); - expect(spec.getUserSyncs.firstCall.args[1][0].body).to.equal('response body'); - expect(spec.getUserSyncs.firstCall.args[1][0].headers).to.have.property('get'); - expect(spec.getUserSyncs.firstCall.args[1][0].headers.get).to.be.a('function'); - }); + const bid = { + 'ad': 'creative', + 'cpm': '1.99', + 'width': 300, + 'height': 250, + 'requestId': '1', + 'creativeId': 'some-id', + 'currency': undefined, + 'netRevenue': true, + 'ttl': 360 + }; + + spec.isBidRequestValid.returns(true); + spec.buildRequests.returns({ + method: 'POST', + url: 'test.url.com', + data: {} + }); + spec.getUserSyncs.returns([]); - it('should register usersync pixels', function () { - const bidder = newBidder(spec); + spec.interpretResponse.returns(bid); - spec.isBidRequestValid.returns(false); - spec.buildRequests.returns([]); - spec.getUserSyncs.returns([{ - type: 'iframe', - url: 'usersync.com' - }]); + bidder.callBids(MOCK_BIDS_REQUEST, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); - bidder.callBids(MOCK_BIDS_REQUEST, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); + expect(logErrorSpy.calledOnce).to.equal(true); + expect(addBidResponseStub.reject.calledOnce).to.be.true; + }); - expect(userSyncStub.called).to.equal(true); - expect(userSyncStub.firstCall.args[0]).to.equal('iframe'); - expect(userSyncStub.firstCall.args[1]).to.equal(spec.code); - expect(userSyncStub.firstCall.args[2]).to.equal('usersync.com'); - }); + it('should require requestId from interpretResponse', () => { + const bidder = newBidder(spec); + const bid = { + 'ad': 'creative', + 'cpm': '1.99', + 'creativeId': 'some-id', + 'currency': 'USD', + 'netRevenue': true, + 'ttl': 360 + }; + spec.isBidRequestValid.returns(true); + spec.buildRequests.returns({ + method: 'POST', + url: 'test.url.com', + data: {} + }); + spec.getUserSyncs.returns([]); + spec.interpretResponse.returns(bid); - it('should logError and reject bid when required bid response params are missing', function () { - const bidder = newBidder(spec); + bidder.callBids(MOCK_BIDS_REQUEST, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); - const bid = { - requestId: '1', - ad: 'ad-url.com', - cpm: 0.5, - height: 200, - width: 300, - placementCode: 'mock/placement' - }; - spec.isBidRequestValid.returns(true); - spec.buildRequests.returns({ - method: 'POST', - url: 'test.url.com', - data: {} + expect(addBidResponseStub.called).to.be.false; + expect(addBidResponseStub.reject.calledOnce).to.be.true; }); - spec.getUserSyncs.returns([]); + }); - spec.interpretResponse.returns(bid); + describe('when the ajax call fails', function () { + let ajaxStub; + let callBidderErrorStub; + let eventEmitterStub; + let xhrErrorMock; - bidder.callBids(MOCK_BIDS_REQUEST, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); + beforeEach(function () { + xhrErrorMock = { + status: 500, + statusText: 'Internal Server Error' + }; + ajaxStub = sinon.stub(ajax, 'ajax').callsFake(function(url, callbacks) { + callbacks.error('ajax call failed.', xhrErrorMock); + }); + callBidderErrorStub = sinon.stub(adapterManager, 'callBidderError'); + eventEmitterStub = sinon.stub(events, 'emit'); + addBidResponseStub.reset(); + doneStub.reset(); + }); - expect(logErrorSpy.calledOnce).to.equal(true); - expect(addBidResponseStub.reject.calledOnce).to.be.true; - }); + afterEach(function () { + ajaxStub.restore(); + callBidderErrorStub.restore(); + eventEmitterStub.restore(); + }); - it('should logError and reject bid when required response params are undefined', function () { - const bidder = newBidder(spec); + Object.entries({ + 'timeouts': true, + 'other errors': false + }).forEach(([t, timedOut]) => { + it(`should ${timedOut ? 'NOT ' : ''}call onTimelyResponse on ${t}`, () => { + Object.assign(xhrErrorMock, {timedOut}); + const bidder = newBidder(spec); + spec.isBidRequestValid.returns(true); + spec.buildRequests.returns({method: 'POST', url: 'test', data: {}}); + bidder.callBids(MOCK_BIDS_REQUEST, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); + sinon.assert[timedOut ? 'notCalled' : 'called'](onTimelyResponseStub); + }) + }) - const bid = { - 'ad': 'creative', - 'cpm': '1.99', - 'width': 300, - 'height': 250, - 'requestId': '1', - 'creativeId': 'some-id', - 'currency': undefined, - 'netRevenue': true, - 'ttl': 360 - }; + it('should not spec.interpretResponse()', function () { + const bidder = newBidder(spec); - spec.isBidRequestValid.returns(true); - spec.buildRequests.returns({ - method: 'POST', - url: 'test.url.com', - data: {} + spec.isBidRequestValid.returns(true); + spec.buildRequests.returns({ + method: 'POST', + url: 'test.url.com', + data: {} + }); + spec.getUserSyncs.returns([]); + + bidder.callBids(MOCK_BIDS_REQUEST, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); + + expect(spec.interpretResponse.called).to.equal(false); + expect(doneStub.calledOnce).to.equal(true); + expect(callBidderErrorStub.calledOnce).to.equal(true); + expect(callBidderErrorStub.firstCall.args[0]).to.equal(CODE); + expect(callBidderErrorStub.firstCall.args[1]).to.equal(xhrErrorMock); + expect(callBidderErrorStub.firstCall.args[2]).to.equal(MOCK_BIDS_REQUEST); + sinon.assert.calledWith(eventEmitterStub, CONSTANTS.EVENTS.BIDDER_ERROR, { + error: xhrErrorMock, + bidderRequest: MOCK_BIDS_REQUEST + }); }); - spec.getUserSyncs.returns([]); - spec.interpretResponse.returns(bid); + it('should not add bids for each adunit code into the auction', function () { + const bidder = newBidder(spec); - bidder.callBids(MOCK_BIDS_REQUEST, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); + spec.isBidRequestValid.returns(true); + spec.buildRequests.returns({ + method: 'POST', + url: 'test.url.com', + data: {} + }); + spec.interpretResponse.returns([]); + spec.getUserSyncs.returns([]); + + bidder.callBids(MOCK_BIDS_REQUEST, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); + + expect(addBidResponseStub.callCount).to.equal(0); + expect(doneStub.calledOnce).to.equal(true); + expect(callBidderErrorStub.calledOnce).to.equal(true); + expect(callBidderErrorStub.firstCall.args[0]).to.equal(CODE); + expect(callBidderErrorStub.firstCall.args[1]).to.equal(xhrErrorMock); + expect(callBidderErrorStub.firstCall.args[2]).to.equal(MOCK_BIDS_REQUEST); + sinon.assert.calledWith(eventEmitterStub, CONSTANTS.EVENTS.BIDDER_ERROR, { + error: xhrErrorMock, + bidderRequest: MOCK_BIDS_REQUEST + }); + }); - expect(logErrorSpy.calledOnce).to.equal(true); - expect(addBidResponseStub.reject.calledOnce).to.be.true; - }); + it('should call spec.getUserSyncs() with no responses', function () { + const bidder = newBidder(spec); - it('should require requestId from interpretResponse', () => { - const bidder = newBidder(spec); - const bid = { - 'ad': 'creative', - 'cpm': '1.99', - 'creativeId': 'some-id', - 'currency': 'USD', - 'netRevenue': true, - 'ttl': 360 - }; - spec.isBidRequestValid.returns(true); - spec.buildRequests.returns({ - method: 'POST', - url: 'test.url.com', - data: {} + spec.isBidRequestValid.returns(true); + spec.buildRequests.returns({ + method: 'POST', + url: 'test.url.com', + data: {} + }); + spec.getUserSyncs.returns([]); + + bidder.callBids(MOCK_BIDS_REQUEST, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); + + expect(spec.getUserSyncs.calledOnce).to.equal(true); + expect(spec.getUserSyncs.firstCall.args[1]).to.deep.equal([]); + expect(doneStub.calledOnce).to.equal(true); + expect(callBidderErrorStub.calledOnce).to.equal(true); + expect(callBidderErrorStub.firstCall.args[0]).to.equal(CODE); + expect(callBidderErrorStub.firstCall.args[1]).to.equal(xhrErrorMock); + expect(callBidderErrorStub.firstCall.args[2]).to.equal(MOCK_BIDS_REQUEST); + sinon.assert.calledWith(eventEmitterStub, CONSTANTS.EVENTS.BIDDER_ERROR, { + error: xhrErrorMock, + bidderRequest: MOCK_BIDS_REQUEST + }); }); - spec.getUserSyncs.returns([]); - spec.interpretResponse.returns(bid); - bidder.callBids(MOCK_BIDS_REQUEST, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); + it('should call spec.getUserSyncs() with no responses', function () { + const bidder = newBidder(spec); - expect(addBidResponseStub.called).to.be.false; - expect(addBidResponseStub.reject.calledOnce).to.be.true; + spec.isBidRequestValid.returns(true); + spec.buildRequests.returns({ + method: 'POST', + url: 'test.url.com', + data: {} + }); + spec.getUserSyncs.returns([]); + + bidder.callBids(MOCK_BIDS_REQUEST, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); + + expect(spec.getUserSyncs.calledOnce).to.equal(true); + expect(spec.getUserSyncs.firstCall.args[1]).to.deep.equal([]); + expect(doneStub.calledOnce).to.equal(true); + expect(callBidderErrorStub.calledOnce).to.equal(true); + expect(callBidderErrorStub.firstCall.args[0]).to.equal(CODE); + expect(callBidderErrorStub.firstCall.args[1]).to.equal(xhrErrorMock); + expect(callBidderErrorStub.firstCall.args[2]).to.equal(MOCK_BIDS_REQUEST); + sinon.assert.calledWith(eventEmitterStub, CONSTANTS.EVENTS.BIDDER_ERROR, { + error: xhrErrorMock, + bidderRequest: MOCK_BIDS_REQUEST + }); + }); }); }); - describe('when the ajax call fails', function () { - let ajaxStub; - let callBidderErrorStub; - let eventEmitterStub; - let xhrErrorMock = { - status: 500, - statusText: 'Internal Server Error' - }; + describe('registerBidder', function () { + let registerBidAdapterStub; + let aliasBidAdapterStub; beforeEach(function () { - ajaxStub = sinon.stub(ajax, 'ajax').callsFake(function(url, callbacks) { - callbacks.error('ajax call failed.', xhrErrorMock); - }); - callBidderErrorStub = sinon.stub(adapterManager, 'callBidderError'); - eventEmitterStub = sinon.stub(events, 'emit'); - addBidResponseStub.reset(); - doneStub.reset(); + registerBidAdapterStub = sinon.stub(adapterManager, 'registerBidAdapter'); + aliasBidAdapterStub = sinon.stub(adapterManager, 'aliasBidAdapter'); }); afterEach(function () { - ajaxStub.restore(); - callBidderErrorStub.restore(); - eventEmitterStub.restore(); + registerBidAdapterStub.restore(); + aliasBidAdapterStub.restore(); }); - it('should not spec.interpretResponse()', function () { - const bidder = newBidder(spec); + function newEmptySpec() { + return { + code: CODE, + isBidRequestValid: function() { }, + buildRequests: function() { }, + interpretResponse: function() { }, + }; + } - spec.isBidRequestValid.returns(true); - spec.buildRequests.returns({ - method: 'POST', - url: 'test.url.com', - data: {} - }); - spec.getUserSyncs.returns([]); - - bidder.callBids(MOCK_BIDS_REQUEST, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); - - expect(spec.interpretResponse.called).to.equal(false); - expect(doneStub.calledOnce).to.equal(true); - expect(callBidderErrorStub.calledOnce).to.equal(true); - expect(callBidderErrorStub.firstCall.args[0]).to.equal(CODE); - expect(callBidderErrorStub.firstCall.args[1]).to.equal(xhrErrorMock); - expect(callBidderErrorStub.firstCall.args[2]).to.equal(MOCK_BIDS_REQUEST); - sinon.assert.calledWith(eventEmitterStub, CONSTANTS.EVENTS.BIDDER_ERROR, { - error: xhrErrorMock, - bidderRequest: MOCK_BIDS_REQUEST - }); - }); + it('should register a bidder with the adapterManager', function () { + registerBidder(newEmptySpec()); + expect(registerBidAdapterStub.calledOnce).to.equal(true); + expect(registerBidAdapterStub.firstCall.args[0]).to.have.property('callBids'); + expect(registerBidAdapterStub.firstCall.args[0].callBids).to.be.a('function'); - it('should not add bids for each adunit code into the auction', function () { - const bidder = newBidder(spec); + expect(registerBidAdapterStub.firstCall.args[1]).to.equal(CODE); + expect(registerBidAdapterStub.firstCall.args[2]).to.be.undefined; + }); - spec.isBidRequestValid.returns(true); - spec.buildRequests.returns({ - method: 'POST', - url: 'test.url.com', - data: {} - }); - spec.interpretResponse.returns([]); - spec.getUserSyncs.returns([]); - - bidder.callBids(MOCK_BIDS_REQUEST, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); - - expect(addBidResponseStub.callCount).to.equal(0); - expect(doneStub.calledOnce).to.equal(true); - expect(callBidderErrorStub.calledOnce).to.equal(true); - expect(callBidderErrorStub.firstCall.args[0]).to.equal(CODE); - expect(callBidderErrorStub.firstCall.args[1]).to.equal(xhrErrorMock); - expect(callBidderErrorStub.firstCall.args[2]).to.equal(MOCK_BIDS_REQUEST); - sinon.assert.calledWith(eventEmitterStub, CONSTANTS.EVENTS.BIDDER_ERROR, { - error: xhrErrorMock, - bidderRequest: MOCK_BIDS_REQUEST - }); + it('should register a bidder with the appropriate mediaTypes', function () { + const thisSpec = Object.assign(newEmptySpec(), { supportedMediaTypes: ['video'] }); + registerBidder(thisSpec); + expect(registerBidAdapterStub.calledOnce).to.equal(true); + expect(registerBidAdapterStub.firstCall.args[2]).to.deep.equal({supportedMediaTypes: ['video']}); }); - it('should call spec.getUserSyncs() with no responses', function () { - const bidder = newBidder(spec); + it('should register bidders with the appropriate aliases', function () { + const thisSpec = Object.assign(newEmptySpec(), { aliases: ['foo', 'bar'] }); + registerBidder(thisSpec); - spec.isBidRequestValid.returns(true); - spec.buildRequests.returns({ - method: 'POST', - url: 'test.url.com', - data: {} - }); - spec.getUserSyncs.returns([]); - - bidder.callBids(MOCK_BIDS_REQUEST, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); - - expect(spec.getUserSyncs.calledOnce).to.equal(true); - expect(spec.getUserSyncs.firstCall.args[1]).to.deep.equal([]); - expect(doneStub.calledOnce).to.equal(true); - expect(callBidderErrorStub.calledOnce).to.equal(true); - expect(callBidderErrorStub.firstCall.args[0]).to.equal(CODE); - expect(callBidderErrorStub.firstCall.args[1]).to.equal(xhrErrorMock); - expect(callBidderErrorStub.firstCall.args[2]).to.equal(MOCK_BIDS_REQUEST); - sinon.assert.calledWith(eventEmitterStub, CONSTANTS.EVENTS.BIDDER_ERROR, { - error: xhrErrorMock, - bidderRequest: MOCK_BIDS_REQUEST - }); + expect(registerBidAdapterStub.calledThrice).to.equal(true); + + // Make sure our later calls don't override the bidder code from previous calls. + expect(registerBidAdapterStub.firstCall.args[0].getBidderCode()).to.equal(CODE); + expect(registerBidAdapterStub.secondCall.args[0].getBidderCode()).to.equal('foo') + expect(registerBidAdapterStub.thirdCall.args[0].getBidderCode()).to.equal('bar') + + expect(registerBidAdapterStub.firstCall.args[1]).to.equal(CODE); + expect(registerBidAdapterStub.secondCall.args[1]).to.equal('foo') + expect(registerBidAdapterStub.thirdCall.args[1]).to.equal('bar') }); - it('should call spec.getUserSyncs() with no responses', function () { - const bidder = newBidder(spec); + it('should register alias with their gvlid', function() { + const aliases = [ + { + code: 'foo', + gvlid: 1 + }, + { + code: 'bar', + gvlid: 2 + }, + { + code: 'baz' + } + ] + const thisSpec = Object.assign(newEmptySpec(), { aliases: aliases }); + registerBidder(thisSpec); + + expect(registerBidAdapterStub.getCall(1).args[0].getSpec().gvlid).to.equal(1); + expect(registerBidAdapterStub.getCall(2).args[0].getSpec().gvlid).to.equal(2); + expect(registerBidAdapterStub.getCall(3).args[0].getSpec().gvlid).to.equal(undefined); + }) + + it('should register alias with skipPbsAliasing', function() { + const aliases = [ + { + code: 'foo', + skipPbsAliasing: true + }, + { + code: 'bar', + skipPbsAliasing: false + }, + { + code: 'baz' + } + ] + const thisSpec = Object.assign(newEmptySpec(), { aliases: aliases }); + registerBidder(thisSpec); + + expect(registerBidAdapterStub.getCall(1).args[0].getSpec().skipPbsAliasing).to.equal(true); + expect(registerBidAdapterStub.getCall(2).args[0].getSpec().skipPbsAliasing).to.equal(false); + expect(registerBidAdapterStub.getCall(3).args[0].getSpec().skipPbsAliasing).to.equal(undefined); + }) + }) + + describe('validate bid response: ', function () { + let spec; + let indexStub, adUnits, bidderRequests; + let addBidResponseStub; + let doneStub; + let ajaxStub; + let logErrorSpy; + + let bids = [{ + 'ad': 'creative', + 'cpm': '1.99', + 'width': 300, + 'height': 250, + 'requestId': '1', + 'creativeId': 'some-id', + 'currency': 'USD', + 'netRevenue': true, + 'ttl': 360 + }]; + + beforeEach(function () { + spec = { + code: CODE, + isBidRequestValid: sinon.stub(), + buildRequests: sinon.stub(), + interpretResponse: sinon.stub(), + }; spec.isBidRequestValid.returns(true); spec.buildRequests.returns({ @@ -776,212 +1084,138 @@ describe('bidders created by newBidder', function () { url: 'test.url.com', data: {} }); - spec.getUserSyncs.returns([]); - - bidder.callBids(MOCK_BIDS_REQUEST, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); - - expect(spec.getUserSyncs.calledOnce).to.equal(true); - expect(spec.getUserSyncs.firstCall.args[1]).to.deep.equal([]); - expect(doneStub.calledOnce).to.equal(true); - expect(callBidderErrorStub.calledOnce).to.equal(true); - expect(callBidderErrorStub.firstCall.args[0]).to.equal(CODE); - expect(callBidderErrorStub.firstCall.args[1]).to.equal(xhrErrorMock); - expect(callBidderErrorStub.firstCall.args[2]).to.equal(MOCK_BIDS_REQUEST); - sinon.assert.calledWith(eventEmitterStub, CONSTANTS.EVENTS.BIDDER_ERROR, { - error: xhrErrorMock, - bidderRequest: MOCK_BIDS_REQUEST + + addBidResponseStub = sinon.stub(); + addBidResponseStub.reject = sinon.stub(); + doneStub = sinon.stub(); + ajaxStub = sinon.stub(ajax, 'ajax').callsFake(function(url, callbacks) { + const fakeResponse = sinon.stub(); + fakeResponse.returns('headerContent'); + callbacks.success('response body', { getResponseHeader: fakeResponse }); }); + logErrorSpy = sinon.spy(utils, 'logError'); + indexStub = sinon.stub(auctionManager, 'index'); + adUnits = []; + bidderRequests = []; + indexStub.get(() => stubAuctionIndex({adUnits: adUnits, bidderRequests: bidderRequests})) }); - }); -}); -describe('registerBidder', function () { - let registerBidAdapterStub; - let aliasBidAdapterStub; - - beforeEach(function () { - registerBidAdapterStub = sinon.stub(adapterManager, 'registerBidAdapter'); - aliasBidAdapterStub = sinon.stub(adapterManager, 'aliasBidAdapter'); - }); - - afterEach(function () { - registerBidAdapterStub.restore(); - aliasBidAdapterStub.restore(); - }); - - function newEmptySpec() { - return { - code: CODE, - isBidRequestValid: function() { }, - buildRequests: function() { }, - interpretResponse: function() { }, - }; - } - - it('should register a bidder with the adapterManager', function () { - registerBidder(newEmptySpec()); - expect(registerBidAdapterStub.calledOnce).to.equal(true); - expect(registerBidAdapterStub.firstCall.args[0]).to.have.property('callBids'); - expect(registerBidAdapterStub.firstCall.args[0].callBids).to.be.a('function'); - - expect(registerBidAdapterStub.firstCall.args[1]).to.equal(CODE); - expect(registerBidAdapterStub.firstCall.args[2]).to.be.undefined; - }); - - it('should register a bidder with the appropriate mediaTypes', function () { - const thisSpec = Object.assign(newEmptySpec(), { supportedMediaTypes: ['video'] }); - registerBidder(thisSpec); - expect(registerBidAdapterStub.calledOnce).to.equal(true); - expect(registerBidAdapterStub.firstCall.args[2]).to.deep.equal({supportedMediaTypes: ['video']}); - }); - - it('should register bidders with the appropriate aliases', function () { - const thisSpec = Object.assign(newEmptySpec(), { aliases: ['foo', 'bar'] }); - registerBidder(thisSpec); - - expect(registerBidAdapterStub.calledThrice).to.equal(true); - - // Make sure our later calls don't override the bidder code from previous calls. - expect(registerBidAdapterStub.firstCall.args[0].getBidderCode()).to.equal(CODE); - expect(registerBidAdapterStub.secondCall.args[0].getBidderCode()).to.equal('foo') - expect(registerBidAdapterStub.thirdCall.args[0].getBidderCode()).to.equal('bar') - - expect(registerBidAdapterStub.firstCall.args[1]).to.equal(CODE); - expect(registerBidAdapterStub.secondCall.args[1]).to.equal('foo') - expect(registerBidAdapterStub.thirdCall.args[1]).to.equal('bar') - }); + afterEach(function () { + ajaxStub.restore(); + logErrorSpy.restore(); + indexStub.restore; + }); - it('should register alias with their gvlid', function() { - const aliases = [ - { - code: 'foo', - gvlid: 1 - }, - { - code: 'bar', - gvlid: 2 - }, - { - code: 'baz' - } - ] - const thisSpec = Object.assign(newEmptySpec(), { aliases: aliases }); - registerBidder(thisSpec); + if (FEATURES.NATIVE) { + it('should add native bids that do have required assets', function () { + adUnits = [{ + transactionId: 'au', + nativeParams: { + title: {'required': true}, + } + }] + decorateAdUnitsWithNativeParams(adUnits); + let bidRequest = { + bids: [{ + bidId: '1', + auctionId: 'first-bid-id', + adUnitCode: 'mock/placement', + transactionId: 'au', + params: { + param: 5 + }, + mediaType: 'native', + }] + }; + + let bids1 = Object.assign({}, + bids[0], + { + 'mediaType': 'native', + 'native': { + 'title': 'Native Creative', + 'clickUrl': 'https://www.link.example', + } + } + ); - expect(registerBidAdapterStub.getCall(1).args[0].getSpec().gvlid).to.equal(1); - expect(registerBidAdapterStub.getCall(2).args[0].getSpec().gvlid).to.equal(2); - expect(registerBidAdapterStub.getCall(3).args[0].getSpec().gvlid).to.equal(undefined); - }) + const bidder = newBidder(spec); - it('should register alias with skipPbsAliasing', function() { - const aliases = [ - { - code: 'foo', - skipPbsAliasing: true - }, - { - code: 'bar', - skipPbsAliasing: false - }, - { - code: 'baz' - } - ] - const thisSpec = Object.assign(newEmptySpec(), { aliases: aliases }); - registerBidder(thisSpec); + spec.interpretResponse.returns(bids1); + bidder.callBids(bidRequest, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); - expect(registerBidAdapterStub.getCall(1).args[0].getSpec().skipPbsAliasing).to.equal(true); - expect(registerBidAdapterStub.getCall(2).args[0].getSpec().skipPbsAliasing).to.equal(false); - expect(registerBidAdapterStub.getCall(3).args[0].getSpec().skipPbsAliasing).to.equal(undefined); - }) -}) + expect(addBidResponseStub.calledOnce).to.equal(true); + expect(addBidResponseStub.firstCall.args[0]).to.equal('mock/placement'); + expect(logErrorSpy.callCount).to.equal(0); + }); -describe('validate bid response: ', function () { - let spec; - let indexStub, adUnits, bidderRequests; - let addBidResponseStub; - let doneStub; - let ajaxStub; - let logErrorSpy; - - let bids = [{ - 'ad': 'creative', - 'cpm': '1.99', - 'width': 300, - 'height': 250, - 'requestId': '1', - 'creativeId': 'some-id', - 'currency': 'USD', - 'netRevenue': true, - 'ttl': 360 - }]; - - beforeEach(function () { - spec = { - code: CODE, - isBidRequestValid: sinon.stub(), - buildRequests: sinon.stub(), - interpretResponse: sinon.stub(), - }; - - spec.isBidRequestValid.returns(true); - spec.buildRequests.returns({ - method: 'POST', - url: 'test.url.com', - data: {} - }); + it('should not add native bids that do not have required assets', function () { + adUnits = [{ + transactionId: 'au', + nativeParams: { + title: {'required': true}, + }, + }]; + decorateAdUnitsWithNativeParams(adUnits); + let bidRequest = { + bids: [{ + bidId: '1', + auctionId: 'first-bid-id', + adUnitCode: 'mock/placement', + transactionId: 'au', + params: { + param: 5 + }, + mediaType: 'native', + }] + }; + let bids1 = Object.assign({}, + bids[0], + { + bidderCode: CODE, + mediaType: 'native', + native: { + title: undefined, + clickUrl: 'https://www.link.example', + } + } + ); - addBidResponseStub = sinon.stub(); - addBidResponseStub.reject = sinon.stub(); - doneStub = sinon.stub(); - ajaxStub = sinon.stub(ajax, 'ajax').callsFake(function(url, callbacks) { - const fakeResponse = sinon.stub(); - fakeResponse.returns('headerContent'); - callbacks.success('response body', { getResponseHeader: fakeResponse }); - }); - logErrorSpy = sinon.spy(utils, 'logError'); - indexStub = sinon.stub(auctionManager, 'index'); - adUnits = []; - bidderRequests = []; - indexStub.get(() => stubAuctionIndex({adUnits: adUnits, bidderRequests: bidderRequests})) - }); + const bidder = newBidder(spec); + spec.interpretResponse.returns(bids1); + bidder.callBids(bidRequest, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); - afterEach(function () { - ajaxStub.restore(); - logErrorSpy.restore(); - indexStub.restore; - }); + expect(addBidResponseStub.called).to.equal(false); + expect(addBidResponseStub.reject.calledOnce).to.be.true; + expect(logErrorSpy.calledWithMatch('Ignoring bid: Native bid missing some required properties.')).to.equal(true); + }); + } - if (FEATURES.NATIVE) { - it('should add native bids that do have required assets', function () { + it('should add bid when renderer is present on outstream bids', function () { adUnits = [{ transactionId: 'au', - nativeParams: { - title: {'required': true}, + mediaTypes: { + video: {context: 'outstream'} } }] - decorateAdUnitsWithNativeParams(adUnits); let bidRequest = { bids: [{ bidId: '1', auctionId: 'first-bid-id', - adUnitCode: 'mock/placement', transactionId: 'au', + adUnitCode: 'mock/placement', params: { param: 5 }, - mediaType: 'native', }] }; let bids1 = Object.assign({}, bids[0], { - 'mediaType': 'native', - 'native': { - 'title': 'Native Creative', - 'clickUrl': 'https://www.link.example', - } + bidderCode: CODE, + mediaType: 'video', + renderer: {render: () => true, url: 'render.js'}, } ); @@ -995,394 +1229,335 @@ describe('validate bid response: ', function () { expect(logErrorSpy.callCount).to.equal(0); }); - it('should not add native bids that do not have required assets', function () { - adUnits = [{ - transactionId: 'au', - nativeParams: { - title: {'required': true}, - }, - }]; - decorateAdUnitsWithNativeParams(adUnits); + it('should add banner bids that have no width or height but single adunit size', function () { let bidRequest = { bids: [{ + bidder: CODE, bidId: '1', auctionId: 'first-bid-id', adUnitCode: 'mock/placement', - transactionId: 'au', params: { param: 5 }, - mediaType: 'native', + sizes: [[300, 250]], }] }; + bidderRequests = [bidRequest]; let bids1 = Object.assign({}, bids[0], { - bidderCode: CODE, - mediaType: 'native', - native: { - title: undefined, - clickUrl: 'https://www.link.example', - } + width: undefined, + height: undefined } ); const bidder = newBidder(spec); + spec.interpretResponse.returns(bids1); bidder.callBids(bidRequest, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); - expect(addBidResponseStub.called).to.equal(false); - expect(addBidResponseStub.reject.calledOnce).to.be.true; - expect(logErrorSpy.calledWithMatch('Ignoring bid: Native bid missing some required properties.')).to.equal(true); + expect(addBidResponseStub.calledOnce).to.equal(true); + expect(addBidResponseStub.firstCall.args[0]).to.equal('mock/placement'); + expect(logErrorSpy.callCount).to.equal(0); }); - } - - it('should add bid when renderer is present on outstream bids', function () { - adUnits = [{ - transactionId: 'au', - mediaTypes: { - video: {context: 'outstream'} - } - }] - let bidRequest = { - bids: [{ - bidId: '1', - auctionId: 'first-bid-id', - transactionId: 'au', - adUnitCode: 'mock/placement', - params: { - param: 5 - }, - }] - }; - - let bids1 = Object.assign({}, - bids[0], - { - bidderCode: CODE, - mediaType: 'video', - renderer: {render: () => true, url: 'render.js'}, - } - ); - - const bidder = newBidder(spec); - - spec.interpretResponse.returns(bids1); - bidder.callBids(bidRequest, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); - - expect(addBidResponseStub.calledOnce).to.equal(true); - expect(addBidResponseStub.firstCall.args[0]).to.equal('mock/placement'); - expect(logErrorSpy.callCount).to.equal(0); - }); - - it('should add banner bids that have no width or height but single adunit size', function () { - let bidRequest = { - bids: [{ - bidder: CODE, - bidId: '1', - auctionId: 'first-bid-id', - adUnitCode: 'mock/placement', - params: { - param: 5 - }, - sizes: [[300, 250]], - }] - }; - bidderRequests = [bidRequest]; - let bids1 = Object.assign({}, - bids[0], - { - width: undefined, - height: undefined - } - ); - - const bidder = newBidder(spec); - - spec.interpretResponse.returns(bids1); - bidder.callBids(bidRequest, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); - - expect(addBidResponseStub.calledOnce).to.equal(true); - expect(addBidResponseStub.firstCall.args[0]).to.equal('mock/placement'); - expect(logErrorSpy.callCount).to.equal(0); - }); - - describe(' Check for alternateBiddersList ', function() { - let bidRequest; - let bids1; - let logWarnSpy; - let bidderSettingStub, aliasRegistryStub; - let aliasRegistry; - beforeEach(function () { - bidRequest = { + it('should disregard auctionId/transactionId set by the adapter', () => { + let bidderRequest = { bids: [{ - bidId: '1', bidder: CODE, - auctionId: 'first-bid-id', - adUnitCode: 'mock/placement', - transactionId: 'au', + bidId: '1', + auctionId: 'aid', + transactionId: 'tid', + adUnitCode: 'au', }] }; + const bidder = newBidder(spec); + spec.interpretResponse.returns(Object.assign({}, bids[0], {transactionId: 'ignored', auctionId: 'ignored'})); + bidder.callBids(bidderRequest, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); + sinon.assert.calledWith(addBidResponseStub, sinon.match.any, sinon.match({ + transactionId: 'tid', + auctionId: 'aid' + })); + }) - bids1 = Object.assign({}, - bids[0], - { - bidderCode: 'validalternatebidder', - adapterCode: 'knownadapter1' - } - ); - logWarnSpy = sinon.spy(utils, 'logWarn'); - bidderSettingStub = sinon.stub(bidderSettings, 'get'); - aliasRegistry = {}; - aliasRegistryStub = sinon.stub(adapterManager, 'aliasRegistry'); - aliasRegistryStub.get(() => aliasRegistry); - }); + describe(' Check for alternateBiddersList ', function() { + let bidRequest; + let bids1; + let logWarnSpy; + let bidderSettingStub, aliasRegistryStub; + let aliasRegistry; - afterEach(function () { - logWarnSpy.restore(); - bidderSettingStub.restore(); - aliasRegistryStub.restore(); - }); + beforeEach(function () { + bidRequest = { + bids: [{ + bidId: '1', + bidder: CODE, + auctionId: 'first-bid-id', + adUnitCode: 'mock/placement', + transactionId: 'au', + }] + }; + + bids1 = Object.assign({}, + bids[0], + { + bidderCode: 'validalternatebidder', + adapterCode: 'knownadapter1' + } + ); + logWarnSpy = sinon.spy(utils, 'logWarn'); + bidderSettingStub = sinon.stub(bidderSettings, 'get'); + aliasRegistry = {}; + aliasRegistryStub = sinon.stub(adapterManager, 'aliasRegistry'); + aliasRegistryStub.get(() => aliasRegistry); + }); - it('should log warning when bidder is unknown and allowAlternateBidderCodes flag is false', function () { - bidderSettingStub.returns(false); + afterEach(function () { + logWarnSpy.restore(); + bidderSettingStub.restore(); + aliasRegistryStub.restore(); + }); - const bidder = newBidder(spec); - spec.interpretResponse.returns(bids1); - bidder.callBids(bidRequest, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); + it('should log warning when bidder is unknown and allowAlternateBidderCodes flag is false', function () { + bidderSettingStub.returns(false); - expect(addBidResponseStub.called).to.equal(false); - expect(addBidResponseStub.reject.calledOnce).to.be.true; - expect(logWarnSpy.callCount).to.equal(1); - }); + const bidder = newBidder(spec); + spec.interpretResponse.returns(bids1); + bidder.callBids(bidRequest, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); - it('should reject the bid, when allowAlternateBidderCodes flag is undefined (default should be false)', function () { - bidderSettingStub.returns(undefined); + expect(addBidResponseStub.called).to.equal(false); + expect(addBidResponseStub.reject.calledOnce).to.be.true; + expect(logWarnSpy.callCount).to.equal(1); + }); - const bidder = newBidder(spec); - spec.interpretResponse.returns(bids1); - bidder.callBids(bidRequest, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); + it('should reject the bid, when allowAlternateBidderCodes flag is undefined (default should be false)', function () { + bidderSettingStub.returns(undefined); - expect(addBidResponseStub.called).to.equal(false); - expect(addBidResponseStub.reject.calledOnce).to.be.true; - }); + const bidder = newBidder(spec); + spec.interpretResponse.returns(bids1); + bidder.callBids(bidRequest, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); - it('should log warning when the particular bidder is not specified in allowedAlternateBidderCodes and allowAlternateBidderCodes flag is true', function () { - bidderSettingStub.withArgs(CODE, 'allowAlternateBidderCodes').returns(true); - bidderSettingStub.withArgs(CODE, 'allowedAlternateBidderCodes').returns(['invalidAlternateBidder02']); + expect(addBidResponseStub.called).to.equal(false); + expect(addBidResponseStub.reject.calledOnce).to.be.true; + }); - const bidder = newBidder(spec); - spec.interpretResponse.returns(bids1); - bidder.callBids(bidRequest, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); + it('should log warning when the particular bidder is not specified in allowedAlternateBidderCodes and allowAlternateBidderCodes flag is true', function () { + bidderSettingStub.withArgs(CODE, 'allowAlternateBidderCodes').returns(true); + bidderSettingStub.withArgs(CODE, 'allowedAlternateBidderCodes').returns(['invalidAlternateBidder02']); - expect(addBidResponseStub.called).to.equal(false); - expect(addBidResponseStub.reject.calledOnce).to.be.true; - expect(logWarnSpy.callCount).to.equal(1); - }); + const bidder = newBidder(spec); + spec.interpretResponse.returns(bids1); + bidder.callBids(bidRequest, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); - it('should accept the bid, when allowedAlternateBidderCodes is empty and allowAlternateBidderCodes flag is true', function () { - bidderSettingStub.withArgs(CODE, 'allowAlternateBidderCodes').returns(true); - bidderSettingStub.withArgs(CODE, 'allowedAlternateBidderCodes').returns(); + expect(addBidResponseStub.called).to.equal(false); + expect(addBidResponseStub.reject.calledOnce).to.be.true; + expect(logWarnSpy.callCount).to.equal(1); + }); - const bidder = newBidder(spec); - spec.interpretResponse.returns(bids1); - bidder.callBids(bidRequest, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); + it('should accept the bid, when allowedAlternateBidderCodes is empty and allowAlternateBidderCodes flag is true', function () { + bidderSettingStub.withArgs(CODE, 'allowAlternateBidderCodes').returns(true); + bidderSettingStub.withArgs(CODE, 'allowedAlternateBidderCodes').returns(); - expect(addBidResponseStub.calledOnce).to.equal(true); - expect(logWarnSpy.callCount).to.equal(0); - expect(logErrorSpy.callCount).to.equal(0); - }); + const bidder = newBidder(spec); + spec.interpretResponse.returns(bids1); + bidder.callBids(bidRequest, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); - it('should accept the bid, when allowedAlternateBidderCodes is marked as * and allowAlternateBidderCodes flag is true', function () { - bidderSettingStub.withArgs(CODE, 'allowAlternateBidderCodes').returns(true); - bidderSettingStub.withArgs(CODE, 'allowedAlternateBidderCodes').returns(['*']); + expect(addBidResponseStub.calledOnce).to.equal(true); + expect(logWarnSpy.callCount).to.equal(0); + expect(logErrorSpy.callCount).to.equal(0); + }); - const bidder = newBidder(spec); - spec.interpretResponse.returns(bids1); - bidder.callBids(bidRequest, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); + it('should accept the bid, when allowedAlternateBidderCodes is marked as * and allowAlternateBidderCodes flag is true', function () { + bidderSettingStub.withArgs(CODE, 'allowAlternateBidderCodes').returns(true); + bidderSettingStub.withArgs(CODE, 'allowedAlternateBidderCodes').returns(['*']); - expect(addBidResponseStub.calledOnce).to.equal(true); - expect(logWarnSpy.callCount).to.equal(0); - expect(logErrorSpy.callCount).to.equal(0); - }); + const bidder = newBidder(spec); + spec.interpretResponse.returns(bids1); + bidder.callBids(bidRequest, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); - it('should accept the bid, when allowedAlternateBidderCodes is marked as * (with space) and allowAlternateBidderCodes flag is true', function () { - bidderSettingStub.withArgs(CODE, 'allowAlternateBidderCodes').returns(true); - bidderSettingStub.withArgs(CODE, 'allowedAlternateBidderCodes').returns([' * ']); + expect(addBidResponseStub.calledOnce).to.equal(true); + expect(logWarnSpy.callCount).to.equal(0); + expect(logErrorSpy.callCount).to.equal(0); + }); - const bidder = newBidder(spec); - spec.interpretResponse.returns(bids1); - bidder.callBids(bidRequest, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); + it('should accept the bid, when allowedAlternateBidderCodes is marked as * (with space) and allowAlternateBidderCodes flag is true', function () { + bidderSettingStub.withArgs(CODE, 'allowAlternateBidderCodes').returns(true); + bidderSettingStub.withArgs(CODE, 'allowedAlternateBidderCodes').returns([' * ']); - expect(addBidResponseStub.calledOnce).to.equal(true); - expect(logWarnSpy.callCount).to.equal(0); - expect(logErrorSpy.callCount).to.equal(0); - }); + const bidder = newBidder(spec); + spec.interpretResponse.returns(bids1); + bidder.callBids(bidRequest, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); - it('should not accept the bid, when allowedAlternateBidderCodes is marked as empty array and allowAlternateBidderCodes flag is true', function () { - bidderSettingStub.withArgs(CODE, 'allowAlternateBidderCodes').returns(true); - bidderSettingStub.withArgs(CODE, 'allowedAlternateBidderCodes').returns([]); + expect(addBidResponseStub.calledOnce).to.equal(true); + expect(logWarnSpy.callCount).to.equal(0); + expect(logErrorSpy.callCount).to.equal(0); + }); - const bidder = newBidder(spec); - spec.interpretResponse.returns(bids1); - bidder.callBids(bidRequest, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); + it('should not accept the bid, when allowedAlternateBidderCodes is marked as empty array and allowAlternateBidderCodes flag is true', function () { + bidderSettingStub.withArgs(CODE, 'allowAlternateBidderCodes').returns(true); + bidderSettingStub.withArgs(CODE, 'allowedAlternateBidderCodes').returns([]); - expect(addBidResponseStub.called).to.equal(false); - expect(addBidResponseStub.reject.calledOnce).to.be.true; - expect(logWarnSpy.callCount).to.equal(1); - }); + const bidder = newBidder(spec); + spec.interpretResponse.returns(bids1); + bidder.callBids(bidRequest, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); - it('should accept the bid, when allowedAlternateBidderCodes contains bidder name and allowAlternateBidderCodes flag is true', function () { - bidderSettingStub.withArgs(CODE, 'allowAlternateBidderCodes').returns(true); - bidderSettingStub.withArgs(CODE, 'allowedAlternateBidderCodes').returns(['validAlternateBidder']); + expect(addBidResponseStub.called).to.equal(false); + expect(addBidResponseStub.reject.calledOnce).to.be.true; + expect(logWarnSpy.callCount).to.equal(1); + }); - const bidder = newBidder(spec); - spec.interpretResponse.returns(bids1); - bidder.callBids(bidRequest, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); + it('should accept the bid, when allowedAlternateBidderCodes contains bidder name and allowAlternateBidderCodes flag is true', function () { + bidderSettingStub.withArgs(CODE, 'allowAlternateBidderCodes').returns(true); + bidderSettingStub.withArgs(CODE, 'allowedAlternateBidderCodes').returns(['validAlternateBidder']); - expect(addBidResponseStub.called).to.equal(true); - expect(logWarnSpy.callCount).to.equal(0); - expect(logErrorSpy.callCount).to.equal(0); - }); + const bidder = newBidder(spec); + spec.interpretResponse.returns(bids1); + bidder.callBids(bidRequest, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); - it('should not accept the bid, when bidder is an alias but bidderSetting is missing for the bidder. It should fallback to standard setting and reject the bid', function () { - bidderSettingStub.withArgs(CODE, 'allowAlternateBidderCodes').returns(false); - aliasRegistry = {'validAlternateBidder': CODE}; + expect(addBidResponseStub.called).to.equal(true); + expect(logWarnSpy.callCount).to.equal(0); + expect(logErrorSpy.callCount).to.equal(0); + }); - const bidder = newBidder(spec); - spec.interpretResponse.returns(bids1); - bidder.callBids(bidRequest, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); + it('should not accept the bid, when bidder is an alias but bidderSetting is missing for the bidder. It should fallback to standard setting and reject the bid', function () { + bidderSettingStub.withArgs(CODE, 'allowAlternateBidderCodes').returns(false); + aliasRegistry = {'validAlternateBidder': CODE}; - expect(addBidResponseStub.called).to.equal(false); - expect(logWarnSpy.callCount).to.equal(1); - expect(addBidResponseStub.reject.calledOnce).to.be.true; - }); + const bidder = newBidder(spec); + spec.interpretResponse.returns(bids1); + bidder.callBids(bidRequest, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); - it('should not accept the bid, when bidderSetting is missing for the bidder. It should fallback to standard setting and reject the bid', function () { - bidderSettingStub.withArgs(CODE, 'allowAlternateBidderCodes').returns(false); + expect(addBidResponseStub.called).to.equal(false); + expect(logWarnSpy.callCount).to.equal(1); + expect(addBidResponseStub.reject.calledOnce).to.be.true; + }); - const bidder = newBidder(spec); - spec.interpretResponse.returns(bids1); - bidder.callBids(bidRequest, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); + it('should not accept the bid, when bidderSetting is missing for the bidder. It should fallback to standard setting and reject the bid', function () { + bidderSettingStub.withArgs(CODE, 'allowAlternateBidderCodes').returns(false); - expect(addBidResponseStub.called).to.equal(false); - expect(addBidResponseStub.reject.calledOnce).to.be.true; - expect(logWarnSpy.callCount).to.equal(1); + const bidder = newBidder(spec); + spec.interpretResponse.returns(bids1); + bidder.callBids(bidRequest, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); + + expect(addBidResponseStub.called).to.equal(false); + expect(addBidResponseStub.reject.calledOnce).to.be.true; + expect(logWarnSpy.callCount).to.equal(1); + }); }); - }); - describe('when interpretResponse returns BidderAuctionResponse', function() { - const bidRequest = { - bids: [{ + describe('when interpretResponse returns BidderAuctionResponse', function() { + const bidRequest = { + auctionId: 'aid', + bids: [{ + bidId: '1', + bidder: CODE, + auctionId: 'aid', + adUnitCode: 'mock/placement', + transactionId: 'au', + }] + }; + const fledgeAuctionConfig = { bidId: '1', - bidder: CODE, - auctionId: 'first-bid-id', - adUnitCode: 'mock/placement', - transactionId: 'au', - }] - }; - const fledgeAuctionConfig = { - bidId: '1', - config: { - foo: 'bar' - } - } - describe('when response has FLEDGE auction config', function() { - let fledgeStub; - - function fledgeHook(next, ...args) { - fledgeStub(...args); + config: { + foo: 'bar' + } } + describe('when response has FLEDGE auction config', function() { + let fledgeStub; - before(() => { - addComponentAuction.before(fledgeHook); - }); + function fledgeHook(next, ...args) { + fledgeStub(...args); + } - after(() => { - addComponentAuction.getHooks({hook: fledgeHook}).remove(); - }) + before(() => { + addComponentAuction.before(fledgeHook); + }); - beforeEach(function () { - fledgeStub = sinon.stub(); - }); + after(() => { + addComponentAuction.getHooks({hook: fledgeHook}).remove(); + }) - it('should unwrap bids', function() { - const bidder = newBidder(spec); - spec.interpretResponse.returns({ - bids: bids, - fledgeAuctionConfigs: [] + beforeEach(function () { + fledgeStub = sinon.stub(); }); - bidder.callBids(bidRequest, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); - expect(addBidResponseStub.calledOnce).to.equal(true); - expect(addBidResponseStub.firstCall.args[0]).to.equal('mock/placement'); - }); - it('should call fledgeManager with FLEDGE configs', function() { - const bidder = newBidder(spec); - spec.interpretResponse.returns({ - bids: bids, - fledgeAuctionConfigs: [fledgeAuctionConfig] + it('should unwrap bids', function() { + const bidder = newBidder(spec); + spec.interpretResponse.returns({ + bids: bids, + fledgeAuctionConfigs: [] + }); + bidder.callBids(bidRequest, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); + expect(addBidResponseStub.calledOnce).to.equal(true); + expect(addBidResponseStub.firstCall.args[0]).to.equal('mock/placement'); }); - bidder.callBids(bidRequest, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); - expect(fledgeStub.calledOnce).to.equal(true); - sinon.assert.calledWith(fledgeStub, 'mock/placement', fledgeAuctionConfig.config); - expect(addBidResponseStub.calledOnce).to.equal(true); - expect(addBidResponseStub.firstCall.args[0]).to.equal('mock/placement'); - }) + it('should call fledgeManager with FLEDGE configs', function() { + const bidder = newBidder(spec); + spec.interpretResponse.returns({ + bids: bids, + fledgeAuctionConfigs: [fledgeAuctionConfig] + }); + bidder.callBids(bidRequest, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); - it('should call fledgeManager with FLEDGE configs even if no bids returned', function() { - const bidder = newBidder(spec); - spec.interpretResponse.returns({ - bids: [], - fledgeAuctionConfigs: [fledgeAuctionConfig] - }); - bidder.callBids(bidRequest, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); + expect(fledgeStub.calledOnce).to.equal(true); + sinon.assert.calledWith(fledgeStub, bidRequest.bids[0], fledgeAuctionConfig.config); + expect(addBidResponseStub.calledOnce).to.equal(true); + expect(addBidResponseStub.firstCall.args[0]).to.equal('mock/placement'); + }) + + it('should call fledgeManager with FLEDGE configs even if no bids returned', function() { + const bidder = newBidder(spec); + spec.interpretResponse.returns({ + bids: [], + fledgeAuctionConfigs: [fledgeAuctionConfig] + }); + bidder.callBids(bidRequest, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); - expect(fledgeStub.calledOnce).to.be.true; - sinon.assert.calledWith(fledgeStub, 'mock/placement', fledgeAuctionConfig.config); - expect(addBidResponseStub.calledOnce).to.equal(false); + expect(fledgeStub.calledOnce).to.be.true; + sinon.assert.calledWith(fledgeStub, bidRequest.bids[0], fledgeAuctionConfig.config); + expect(addBidResponseStub.calledOnce).to.equal(false); + }) }) }) - }) -}); + }); -describe('bid response isValid', () => { - describe('size check', () => { - let req, index; + describe('bid response isValid', () => { + describe('size check', () => { + let req, index; - beforeEach(() => { - req = { - ...MOCK_BIDS_REQUEST.bids[0], - mediaTypes: { - banner: { - sizes: [[1, 2], [3, 4]] + beforeEach(() => { + req = { + ...MOCK_BIDS_REQUEST.bids[0], + mediaTypes: { + banner: { + sizes: [[1, 2], [3, 4]] + } } } - } - }); + }); - function mkResponse(width, height) { - return { - requestId: req.bidId, - width, - height, - cpm: 1, - ttl: 60, - creativeId: '123', - netRevenue: true, - currency: 'USD', - mediaType: 'banner', + function mkResponse(width, height) { + return { + requestId: req.bidId, + width, + height, + cpm: 1, + ttl: 60, + creativeId: '123', + netRevenue: true, + currency: 'USD', + mediaType: 'banner', + } } - } - function checkValid(bid) { - return isValid('au', bid, {index: stubAuctionIndex({bidRequests: [req]})}); - } + function checkValid(bid) { + return isValid('au', bid, {index: stubAuctionIndex({bidRequests: [req]})}); + } - it('should succeed when response has a size that was in request', () => { - expect(checkValid(mkResponse(3, 4))).to.be.true; - }); - }) -}); + it('should succeed when response has a size that was in request', () => { + expect(checkValid(mkResponse(3, 4))).to.be.true; + }); + }) + }); +}) diff --git a/test/spec/unit/core/consentHandler_spec.js b/test/spec/unit/core/consentHandler_spec.js index 98b317e0d36..1bcad3216ce 100644 --- a/test/spec/unit/core/consentHandler_spec.js +++ b/test/spec/unit/core/consentHandler_spec.js @@ -1,4 +1,4 @@ -import {ConsentHandler, gvlidRegistry} from '../../../../src/consentHandler.js'; +import {ConsentHandler, gvlidRegistry, multiHandler} from '../../../../src/consentHandler.js'; describe('Consent data handler', () => { let handler; @@ -56,6 +56,88 @@ describe('Consent data handler', () => { }) }) }); + + describe('getHash', () => { + it('is defined when null', () => { + expect(handler.hash).be.a('string'); + }); + it('changes when a field is updated', () => { + const h1 = handler.hash; + handler.setConsentData({field: 'value', enabled: false}); + const h2 = handler.hash; + expect(h2).to.not.eql(h1); + handler.setConsentData({field: 'value', enabled: true}); + const h3 = handler.hash; + expect(h3).to.not.eql(h2); + expect(h3).to.not.eql(h1); + }); + it('does not change when fields are unchanged', () => { + handler.setConsentData({field: 'value', enabled: true}); + const h1 = handler.hash; + handler.setConsentData({field: 'value', enabled: true}); + expect(handler.hash).to.eql(h1); + }); + it('does not change when non-hashFields are updated', () => { + handler.hashFields = ['field', 'enabled']; + handler.setConsentData({field: 'value', enabled: true}); + const h1 = handler.hash; + handler.setConsentData({field: 'value', enabled: true, other: 'data'}); + expect(handler.hash).to.eql(h1); + }) + }) +}); + +describe('multiHandler', () => { + let handlers, multi; + beforeEach(() => { + handlers = {h1: {}, h2: {}}; + multi = multiHandler(handlers); + }); + + ['getConsentData', 'getConsentMeta'].forEach(method => { + describe(method, () => { + it('combines results from underlying handlers', () => { + handlers.h1[method] = () => 'one'; + handlers.h2[method] = () => 'two'; + expect(multi[method]()).to.eql({ + h1: 'one', + h2: 'two', + }) + }); + }); + }); + + describe('.promise', () => { + it('resolves all underlying promises', (done) => { + handlers.h1.promise = Promise.resolve('one'); + let resolver, result; + handlers.h2.promise = new Promise((resolve) => { resolver = resolve }); + multi.promise.then((val) => { + result = val; + expect(result).to.eql({ + h1: 'one', + h2: 'two' + }); + done(); + }) + handlers.h1.promise.then(() => { + expect(result).to.not.exist; + resolver('two'); + }); + }) + }); + + describe('.hash', () => { + ['h1', 'h2'].forEach((handler, i) => { + it(`changes when handler #${i + 1} changes hash`, () => { + handlers.h1.hash = 'one'; + handlers.h2.hash = 'two' + const first = multi.hash; + handlers[handler].hash = 'new'; + expect(multi.hash).to.not.eql(first); + }) + }) + }) }) describe('gvlidRegistry', () => { diff --git a/test/spec/unit/core/events_spec.js b/test/spec/unit/core/events_spec.js new file mode 100644 index 00000000000..e1451f657b5 --- /dev/null +++ b/test/spec/unit/core/events_spec.js @@ -0,0 +1,45 @@ +import {config} from 'src/config.js'; +import {emit, clearEvents, getEvents, on, off} from '../../../../src/events.js'; +import * as utils from '../../../../src/utils.js' + +describe('events', () => { + let clock; + beforeEach(() => { + clock = sinon.useFakeTimers(); + clearEvents(); + }); + afterEach(() => { + clock.restore(); + }); + + it('should clear event log using eventHistoryTTL config', () => { + emit('testEvent', {}); + expect(getEvents().length).to.eql(1); + config.setConfig({eventHistoryTTL: 1}); + clock.tick(500); + expect(getEvents().length).to.eql(1); + clock.tick(6000); + expect(getEvents().length).to.eql(0); + }); + + it('should take history TTL in seconds', () => { + emit('testEvent', {}); + config.setConfig({eventHistoryTTL: 1000}); + clock.tick(10000); + expect(getEvents().length).to.eql(1); + }); + + it('should include the eventString if a callback fails', () => { + const logErrorStub = sinon.stub(utils, 'logError'); + const eventString = 'bidWon'; + let fn = function() { throw new Error('Test error'); }; + on(eventString, fn); + + emit(eventString, {}); + + sinon.assert.calledWith(logErrorStub, 'Error executing handler:', 'events.js', sinon.match.instanceOf(Error), eventString); + + off(eventString, fn); + logErrorStub.restore(); + }); +}) diff --git a/test/spec/unit/core/targeting_spec.js b/test/spec/unit/core/targeting_spec.js index f16d6208087..ba9aeff70d1 100644 --- a/test/spec/unit/core/targeting_spec.js +++ b/test/spec/unit/core/targeting_spec.js @@ -1,13 +1,19 @@ -import { expect } from 'chai'; -import { targeting as targetingInstance, filters, getHighestCpmBidsFromBidPool, sortByDealAndPriceBucketOrCpm } from 'src/targeting.js'; -import { config } from 'src/config.js'; -import { createBidReceived } from 'test/fixtures/fixtures.js'; +import {expect} from 'chai'; +import { + filters, + getHighestCpmBidsFromBidPool, + sortByDealAndPriceBucketOrCpm, + targeting as targetingInstance +} from 'src/targeting.js'; +import {config} from 'src/config.js'; +import {createBidReceived} from 'test/fixtures/fixtures.js'; import CONSTANTS from 'src/constants.json'; -import { auctionManager } from 'src/auctionManager.js'; +import {auctionManager} from 'src/auctionManager.js'; import * as utils from 'src/utils.js'; import {deepClone} from 'src/utils.js'; import {createBid} from '../../../../src/bidfactory.js'; import {hook} from '../../../../src/hook.js'; +import {getHighestCpm} from '../../../../src/utils/reducers.js'; function mkBid(bid, status = CONSTANTS.STATUS.GOOD) { return Object.assign(createBid(status), bid); @@ -451,7 +457,7 @@ describe('targeting tests', function () { } }); - const bids = getHighestCpmBidsFromBidPool(bidsReceived, utils.getHighestCpm, 2); + const bids = getHighestCpmBidsFromBidPool(bidsReceived, getHighestCpm, 2); expect(bids.length).to.equal(3); expect(bids[0].adId).to.equal('8383838'); @@ -467,7 +473,7 @@ describe('targeting tests', function () { } }); - const bids = getHighestCpmBidsFromBidPool(bidsReceived, utils.getHighestCpm, 2); + const bids = getHighestCpmBidsFromBidPool(bidsReceived, getHighestCpm, 2); expect(bids.length).to.equal(3); expect(bids[0].adId).to.equal('8383838'); @@ -949,6 +955,7 @@ describe('targeting tests', function () { expect(bids.length).to.equal(1); expect(bids[0].adId).to.equal('adid-1'); + expect(bids[0].latestTargetedAuctionId).to.equal(2); useBidCache = false; @@ -956,6 +963,7 @@ describe('targeting tests', function () { expect(bids.length).to.equal(1); expect(bids[0].adId).to.equal('adid-2'); + expect(bids[0].latestTargetedAuctionId).to.equal(2); }); it('should use bidCacheFilterFunction', function() { @@ -983,9 +991,13 @@ describe('targeting tests', function () { expect(bids.length).to.equal(4); expect(bids[0].adId).to.equal('adid-1'); + expect(bids[0].latestTargetedAuctionId).to.equal(2); expect(bids[1].adId).to.equal('adid-4'); + expect(bids[1].latestTargetedAuctionId).to.equal(2); expect(bids[2].adId).to.equal('adid-5'); + expect(bids[2].latestTargetedAuctionId).to.equal(2); expect(bids[3].adId).to.equal('adid-8'); + expect(bids[3].latestTargetedAuctionId).to.equal(2); // Bid Caching Off, No Filter Function useBidCache = false; @@ -994,9 +1006,13 @@ describe('targeting tests', function () { expect(bids.length).to.equal(4); expect(bids[0].adId).to.equal('adid-2'); + expect(bids[0].latestTargetedAuctionId).to.equal(2); expect(bids[1].adId).to.equal('adid-4'); + expect(bids[1].latestTargetedAuctionId).to.equal(2); expect(bids[2].adId).to.equal('adid-6'); + expect(bids[2].latestTargetedAuctionId).to.equal(2); expect(bids[3].adId).to.equal('adid-8'); + expect(bids[3].latestTargetedAuctionId).to.equal(2); // Bid Caching On AGAIN, No Filter Function (should be same as first time) useBidCache = true; @@ -1005,9 +1021,13 @@ describe('targeting tests', function () { expect(bids.length).to.equal(4); expect(bids[0].adId).to.equal('adid-1'); + expect(bids[0].latestTargetedAuctionId).to.equal(2); expect(bids[1].adId).to.equal('adid-4'); + expect(bids[1].latestTargetedAuctionId).to.equal(2); expect(bids[2].adId).to.equal('adid-5'); + expect(bids[2].latestTargetedAuctionId).to.equal(2); expect(bids[3].adId).to.equal('adid-8'); + expect(bids[3].latestTargetedAuctionId).to.equal(2); // Bid Caching On, with Filter Function to Exclude video useBidCache = true; @@ -1020,9 +1040,13 @@ describe('targeting tests', function () { expect(bids.length).to.equal(4); expect(bids[0].adId).to.equal('adid-1'); + expect(bids[0].latestTargetedAuctionId).to.equal(2); expect(bids[1].adId).to.equal('adid-4'); + expect(bids[1].latestTargetedAuctionId).to.equal(2); expect(bids[2].adId).to.equal('adid-6'); + expect(bids[2].latestTargetedAuctionId).to.equal(2); expect(bids[3].adId).to.equal('adid-8'); + expect(bids[3].latestTargetedAuctionId).to.equal(2); // filter function should have been called for each cached bid (4 times) expect(bcffCalled).to.equal(4); @@ -1038,9 +1062,13 @@ describe('targeting tests', function () { expect(bids.length).to.equal(4); expect(bids[0].adId).to.equal('adid-2'); + expect(bids[0].latestTargetedAuctionId).to.equal(2); expect(bids[1].adId).to.equal('adid-4'); + expect(bids[1].latestTargetedAuctionId).to.equal(2); expect(bids[2].adId).to.equal('adid-6'); + expect(bids[2].latestTargetedAuctionId).to.equal(2); expect(bids[3].adId).to.equal('adid-8'); + expect(bids[3].latestTargetedAuctionId).to.equal(2); // filter function should not have been called expect(bcffCalled).to.equal(0); }); diff --git a/test/spec/unit/pbjs_api_spec.js b/test/spec/unit/pbjs_api_spec.js index 5c361d186c0..39123d4aa41 100644 --- a/test/spec/unit/pbjs_api_spec.js +++ b/test/spec/unit/pbjs_api_spec.js @@ -14,7 +14,7 @@ import { config as configObj } from 'src/config.js'; import * as ajaxLib from 'src/ajax.js'; import * as auctionModule from 'src/auction.js'; import { registerBidder } from 'src/adapters/bidderFactory.js'; -import { _sendAdToCreative } from 'src/secureCreatives.js'; +import {resizeRemoteCreative} from 'src/secureCreatives.js'; import {find} from 'src/polyfill.js'; import * as pbjsModule from 'src/prebid.js'; import {hook} from '../../../src/hook.js'; @@ -25,7 +25,7 @@ import {stubAuctionIndex} from '../../helpers/indexStub.js'; import {createBid} from '../../../src/bidfactory.js'; import {enrichFPD} from '../../../src/fpd/enrichment.js'; import {mockFpdEnrichments} from '../../helpers/fpd.js'; - +import {generateUUID} from '../../../src/utils.js'; var assert = require('chai').assert; var expect = require('chai').expect; @@ -43,13 +43,13 @@ var adUnits = getAdUnits(); var adUnitCodes = getAdUnits().map(unit => unit.code); var bidsBackHandler = function() {}; const timeout = 2000; -var auction = auctionManager.createAuction({adUnits, adUnitCodes, callback: bidsBackHandler, cbTimeout: timeout}); -auction.getBidRequests = getBidRequests; -auction.getBidsReceived = getBidResponses; -auction.getAdUnits = getAdUnits; -auction.getAuctionStatus = function() { return auctionModule.AUCTION_COMPLETED } +const auctionId = generateUUID(); +let auction; function resetAuction() { + if (auction == null) { + auction = auctionManager.createAuction({adUnits, adUnitCodes, callback: bidsBackHandler, cbTimeout: timeout, labels: undefined, auctionId: auctionId}); + } $$PREBID_GLOBAL$$.setConfig({ enableSendAllBids: false }); auction.getBidRequests = getBidRequests; auction.getBidsReceived = getBidResponses; @@ -1104,7 +1104,7 @@ describe('Unit: Prebid Module', function () { adUnitCode: config.adUnitCodes[0], }; - _sendAdToCreative(mockAdObject, sinon.stub()); + resizeRemoteCreative(mockAdObject); expect(slots[0].spyGetSlotElementId.called).to.equal(false); expect(slots[1].spyGetSlotElementId.called).to.equal(true); @@ -1233,7 +1233,8 @@ describe('Unit: Prebid Module', function () { } }, getElementsByTagName: sinon.stub(), - querySelector: sinon.stub() + querySelector: sinon.stub(), + createElement: sinon.stub(), }; elStub = { @@ -1264,7 +1265,7 @@ describe('Unit: Prebid Module', function () { it('should require doc and id params', function () { $$PREBID_GLOBAL$$.renderAd(); - var error = 'Error trying to write ad Id :undefined to the page. Missing adId'; + var error = 'Error rendering ad (id: undefined): missing adId'; assert.ok(spyLogError.calledWith(error), 'expected param error was logged'); }); @@ -1289,14 +1290,13 @@ describe('Unit: Prebid Module', function () { adUrl: 'http://server.example.com/ad/ad.js' }); $$PREBID_GLOBAL$$.renderAd(doc, bidId); - assert.ok(elStub.insertBefore.called, 'url was written to iframe in doc'); + sinon.assert.calledWith(doc.createElement, 'iframe'); }); it('should log an error when no ad or url', function () { pushBidResponseToAuction({}); $$PREBID_GLOBAL$$.renderAd(doc, bidId); - var error = 'Error trying to write ad. No ad for bid response id: ' + bidId; - assert.ok(spyLogError.calledWith(error), 'expected error was logged'); + sinon.assert.called(spyLogError); }); it('should log an error when not in an iFrame', function () { @@ -1305,7 +1305,7 @@ describe('Unit: Prebid Module', function () { }); inIframe = false; $$PREBID_GLOBAL$$.renderAd(document, bidId); - const error = 'Error trying to write ad. Ad render call ad id ' + bidId + ' was prevented from writing to the main document.'; + const error = `Error rendering ad (id: ${bidId}): renderAd was prevented from writing to the main document.`; assert.ok(spyLogError.calledWith(error), 'expected error was logged'); }); @@ -1326,14 +1326,14 @@ describe('Unit: Prebid Module', function () { doc.write = sinon.stub().throws(error); $$PREBID_GLOBAL$$.renderAd(doc, bidId); - var errorMessage = 'Error trying to write ad Id :' + bidId + ' to the page:' + error.message; + var errorMessage = `Error rendering ad (id: ${bidId}): doc write error` assert.ok(spyLogError.calledWith(errorMessage), 'expected error was logged'); }); it('should log an error when ad not found', function () { var fakeId = 99; $$PREBID_GLOBAL$$.renderAd(doc, fakeId); - var error = 'Error trying to write ad. Cannot find ad by given id : ' + fakeId; + var error = `Error rendering ad (id: ${fakeId}): Cannot find ad '${fakeId}'` assert.ok(spyLogError.calledWith(error), 'expected error was logged'); }); @@ -1345,14 +1345,6 @@ describe('Unit: Prebid Module', function () { assert.deepEqual($$PREBID_GLOBAL$$.getAllWinningBids()[0], adResponse); }); - it('should replace ${CLICKTHROUGH} macro in winning bids response', function () { - pushBidResponseToAuction({ - ad: "" - }); - $$PREBID_GLOBAL$$.renderAd(doc, bidId, {clickThrough: 'https://someadserverclickurl.com'}); - expect(adResponse).to.have.property('ad').and.to.match(/https:\/\/someadserverclickurl\.com/i); - }); - it('fires billing url if present on s2s bid', function () { const burl = 'http://www.example.com/burl'; pushBidResponseToAuction({ @@ -2736,6 +2728,13 @@ describe('Unit: Prebid Module', function () { events.on.restore(); }); + it('should emit event BID_ACCEPTED when invoked', function () { + var callback = sinon.spy(); + $$PREBID_GLOBAL$$.onEvent('bidAccepted', callback); + events.emit(CONSTANTS.EVENTS.BID_ACCEPTED); + sinon.assert.calledOnce(callback); + }); + describe('beforeRequestBids', function () { let bidRequestedHandler; let beforeRequestBidsHandler; @@ -3304,16 +3303,20 @@ describe('Unit: Prebid Module', function () { const highestBid = $$PREBID_GLOBAL$$.getHighestUnusedBidResponseForAdUnitCode('/19968336/header-bid-tag-0'); expect(highestBid).to.deep.equal(_bidsReceived[2]) }) - }) + }); - describe('getHighestCpm', () => { + describe('getHighestCpmBids', () => { after(() => { resetAuction(); }); it('returns an array containing the highest bid object for the given adUnitCode', function () { - const highestCpmBids = $$PREBID_GLOBAL$$.getHighestCpmBids('/19968336/header-bid-tag-0'); + const adUnitcode = '/19968336/header-bid-tag-0'; + targeting.setLatestAuctionForAdUnit(adUnitcode, auctionId) + const highestCpmBids = $$PREBID_GLOBAL$$.getHighestCpmBids(adUnitcode); expect(highestCpmBids.length).to.equal(1); - expect(highestCpmBids[0]).to.deep.equal(auctionManager.getBidsReceived()[1]); + const expectedBid = auctionManager.getBidsReceived()[1]; + expectedBid.latestTargetedAuctionId = auctionId; + expect(highestCpmBids[0]).to.deep.equal(expectedBid); }); it('returns an empty array when the given adUnit is not found', function () { diff --git a/test/spec/unit/secureCreatives_spec.js b/test/spec/unit/secureCreatives_spec.js index 7d5f9af35dd..895bf03165a 100644 --- a/test/spec/unit/secureCreatives_spec.js +++ b/test/spec/unit/secureCreatives_spec.js @@ -1,6 +1,4 @@ -import { - _sendAdToCreative, getReplier, receiveMessage -} from 'src/secureCreatives.js'; +import {getReplier, receiveMessage} from 'src/secureCreatives.js'; import * as utils from 'src/utils.js'; import {getAdUnits, getBidRequests, getBidResponses} from 'test/fixtures/fixtures.js'; import {auctionManager} from 'src/auctionManager.js'; @@ -8,10 +6,11 @@ import * as auctionModule from 'src/auction.js'; import * as native from 'src/native.js'; import {fireNativeTrackers, getAllAssetsMessage} from 'src/native.js'; import * as events from 'src/events.js'; -import { config as configObj } from 'src/config.js'; +import {config as configObj} from 'src/config.js'; import 'src/prebid.js'; -import { expect } from 'chai'; +import {expect} from 'chai'; +import {handleRender} from '../../../src/adRendering.js'; var CONSTANTS = require('src/constants.json'); @@ -54,37 +53,45 @@ describe('secureCreatives', () => { }); }); - describe('_sendAdToCreative', () => { - beforeEach(function () { - sinon.stub(utils, 'logError'); - sinon.stub(utils, 'logWarn'); + describe('handleRender', () => { + let bidResponse, renderFn, result; + beforeEach(() => { + result = null; + renderFn = sinon.stub().callsFake((r) => { result = r; }); + bidResponse = { + adId: 123 + } }); - afterEach(function () { - utils.logError.restore(); - utils.logWarn.restore(); + it('does not invoke renderFn, but the renderer instead, if the ad has one', () => { + const renderer = { + url: 'some-custom-renderer', + render: sinon.spy() + } + handleRender(renderFn, {bidResponse: {renderer}}); + sinon.assert.notCalled(renderFn); + sinon.assert.called(renderer.render); }); - it('should macro replace ${AUCTION_PRICE} with the winning bid for ad and adUrl', () => { - const oldVal = window.googletag; - const oldapntag = window.apntag; - window.apntag = null - window.googletag = null; - const mockAdObject = { - adId: 'someAdId', - ad: '', - adUrl: 'http://creative.prebid.org/${AUCTION_PRICE}', - width: 300, - height: 250, - renderer: null, - cpm: '1.00', - adUnitCode: 'some_dom_id' - }; - const reply = sinon.spy(); - _sendAdToCreative(mockAdObject, reply); - expect(reply.args[0][0].ad).to.equal(''); - expect(reply.args[0][0].adUrl).to.equal('http://creative.prebid.org/1.00'); - window.googletag = oldVal; - window.apntag = oldapntag; + + ['ad', 'adUrl'].forEach((prop) => { + describe(`on ${prop}`, () => { + it('replaces AUCTION_PRICE macro', () => { + bidResponse[prop] = 'pre${AUCTION_PRICE}post'; + bidResponse.cpm = 123; + handleRender(renderFn, {adId: 123, bidResponse}); + expect(result[prop]).to.eql('pre123post'); + }); + it('replaces CLICKTHROUGH macro', () => { + bidResponse[prop] = 'pre${CLICKTHROUGH}post'; + handleRender(renderFn, {adId: 123, bidResponse, options: {clickUrl: 'clk'}}); + expect(result[prop]).to.eql('preclkpost'); + }); + it('defaults CLICKTHROUGH to empty string', () => { + bidResponse[prop] = 'pre${CLICKTHROUGH}post'; + handleRender(renderFn, {adId: 123, bidResponse}); + expect(result[prop]).to.eql('prepost'); + }); + }); }); }); diff --git a/test/spec/unit/utils/reducers_spec.js b/test/spec/unit/utils/reducers_spec.js new file mode 100644 index 00000000000..95bf3b74041 --- /dev/null +++ b/test/spec/unit/utils/reducers_spec.js @@ -0,0 +1,124 @@ +import { + tiebreakCompare, + keyCompare, + simpleCompare, + minimum, + maximum, + getHighestCpm, + getOldestHighestCpmBid, getLatestHighestCpmBid, reverseCompare +} from '../../../../src/utils/reducers.js'; +import assert from 'assert'; + +describe('reducers', () => { + describe('simpleCompare', () => { + Object.entries({ + '<': [10, 20, -1], + '===': [123, 123, 0], + '>': [30, -10, 1] + }).forEach(([t, [a, b, expected]]) => { + it(`returns ${expected} when a ${t} b`, () => { + expect(simpleCompare(a, b)).to.equal(expected); + }) + }) + }); + + describe('keyCompare', () => { + Object.entries({ + '<': [{k: -123}, {k: 0}, -1], + '===': [{k: 0}, {k: 0}, 0], + '>': [{k: 2}, {k: 1}, 1] + }).forEach(([t, [a, b, expected]]) => { + it(`returns ${expected} when key(a) ${t} key(b)`, () => { + expect(keyCompare(item => item.k)(a, b)).to.equal(expected); + }) + }) + }); + + describe('tiebreakCompare', () => { + Object.entries({ + 'first compare says a < b': [{main: 1, tie: 2}, {main: 2, tie: 1}, -1], + 'first compare says a > b': [{main: 2, tie: 1}, {main: 1, tie: 2}, 1], + 'first compare ties, second says a < b': [{main: 0, tie: 1}, {main: 0, tie: 2}, -1], + 'first compare ties, second says a > b': [{main: 0, tie: 2}, {main: 0, tie: 1}, 1], + 'all compares tie': [{main: 0, tie: 0}, {main: 0, tie: 0}, 0] + }).forEach(([t, [a, b, expected]]) => { + it(`should return ${expected} when ${t}`, () => { + const cmp = tiebreakCompare(keyCompare(item => item.main), keyCompare(item => item.tie)); + expect(cmp(a, b)).to.equal(expected); + }) + }) + }); + + const SAMPLE_ARR = [-10, 20, 20, 123, 400]; + + Object.entries({ + 'minimum': [minimum, ['minimum', -10], ['maximum', 400]], + 'maximum': [maximum, ['maximum', 400], ['minimum', -10]] + }).forEach(([t, [fn, simple, reversed]]) => { + describe(t, () => { + it(`should find ${simple[0]} using simple compare`, () => { + expect(SAMPLE_ARR.reduce(fn(simpleCompare))).to.equal(simple[1]); + }); + it(`should find ${reversed[0]} using reverse compare`, () => { + expect(SAMPLE_ARR.reduce(fn(reverseCompare()))).to.equal(reversed[1]); + }); + }) + }); + + describe('getHighestCpm', function () { + it('should pick the highest cpm', function () { + let a = { + cpm: 2, + timeToRespond: 100 + }; + let b = { + cpm: 1, + timeToRespond: 100 + }; + expect(getHighestCpm(a, b)).to.eql(a); + expect(getHighestCpm(b, a)).to.eql(a); + }); + + it('should pick the lowest timeToRespond cpm in case of tie', function () { + let a = { + cpm: 1, + timeToRespond: 100 + }; + let b = { + cpm: 1, + timeToRespond: 50 + }; + expect(getHighestCpm(a, b)).to.eql(b); + expect(getHighestCpm(b, a)).to.eql(b); + }); + }); + + describe('getOldestHighestCpmBid', () => { + it('should pick the oldest in case of tie using responseTimeStamp', function () { + let a = { + cpm: 1, + responseTimestamp: 1000 + }; + let b = { + cpm: 1, + responseTimestamp: 2000 + }; + expect(getOldestHighestCpmBid(a, b)).to.eql(a); + expect(getOldestHighestCpmBid(b, a)).to.eql(a); + }); + }); + describe('getLatestHighestCpmBid', () => { + it('should pick the latest in case of tie using responseTimeStamp', function () { + let a = { + cpm: 1, + responseTimestamp: 1000 + }; + let b = { + cpm: 1, + responseTimestamp: 2000 + }; + expect(getLatestHighestCpmBid(a, b)).to.eql(b); + expect(getLatestHighestCpmBid(b, a)).to.eql(b); + }); + }); +}) diff --git a/test/spec/unit/utils/ttlCollection_spec.js b/test/spec/unit/utils/ttlCollection_spec.js new file mode 100644 index 00000000000..29c6c438855 --- /dev/null +++ b/test/spec/unit/utils/ttlCollection_spec.js @@ -0,0 +1,180 @@ +import {ttlCollection} from '../../../../src/utils/ttlCollection.js'; + +describe('ttlCollection', () => { + it('can add & retrieve items', () => { + const coll = ttlCollection(); + expect(coll.toArray()).to.eql([]); + coll.add(1); + coll.add(2); + expect(coll.toArray()).to.eql([1, 2]); + }); + + it('can clear', () => { + const coll = ttlCollection(); + coll.add('item'); + coll.clear(); + expect(coll.toArray()).to.eql([]); + }); + + it('can be iterated over', () => { + const coll = ttlCollection(); + coll.add('1'); + coll.add('2'); + expect(Array.from(coll)).to.eql(['1', '2']); + }) + + describe('autopurge', () => { + let clock, pms, waitForPromises; + const SLACK = 2000; + beforeEach(() => { + clock = sinon.useFakeTimers(); + pms = []; + waitForPromises = () => Promise.all(pms); + }); + afterEach(() => { + clock.restore(); + }); + + Object.entries({ + 'defer': (value) => { + const pm = Promise.resolve(value); + pms.push(pm); + return pm; + }, + 'do not defer': (value) => value, + }).forEach(([t, resolve]) => { + describe(`when ttl/startTime ${t}`, () => { + let coll; + beforeEach(() => { + coll = ttlCollection({ + startTime: (item) => resolve(item.start == null ? new Date().getTime() : item.start), + ttl: (item) => resolve(item.ttl), + slack: SLACK + }) + }); + + it('should clear items after enough time has passed', () => { + coll.add({no: 'ttl'}); + coll.add({ttl: 1000}); + coll.add({ttl: 4000}); + return waitForPromises().then(() => { + clock.tick(500); + expect(coll.toArray()).to.eql([{no: 'ttl'}, {ttl: 1000}, {ttl: 4000}]); + clock.tick(SLACK + 500); + expect(coll.toArray()).to.eql([{no: 'ttl'}, {ttl: 4000}]); + clock.tick(3000); + expect(coll.toArray()).to.eql([{no: 'ttl'}]); + }); + }); + + it('should not wait too long if a shorter ttl shows up', () => { + coll.add({ttl: 4000}); + coll.add({ttl: 1000}); + return waitForPromises().then(() => { + clock.tick(1000 + SLACK); + expect(coll.toArray()).to.eql([ + {ttl: 4000} + ]); + }); + }); + + it('should not wait more if later ttls are within slack', () => { + coll.add({start: 0, ttl: 4000}); + return waitForPromises().then(() => { + clock.tick(4000); + coll.add({start: 0, ttl: 5000}); + return waitForPromises().then(() => { + clock.tick(SLACK); + expect(coll.toArray()).to.eql([]); + }); + }); + }); + + it('should clear items ASAP if they expire in the past', () => { + clock.tick(10000); + coll.add({start: 0, ttl: 1000}); + return waitForPromises().then(() => { + clock.tick(SLACK); + expect(coll.toArray()).to.eql([]); + }); + }); + + it('should clear items ASAP if they have ttl = 0', () => { + coll.add({ttl: 0}); + return waitForPromises().then(() => { + clock.tick(SLACK); + expect(coll.toArray()).to.eql([]); + }); + }); + + describe('refresh', () => { + it('should refresh missing TTLs', () => { + const item = {}; + coll.add(item); + return waitForPromises().then(() => { + item.ttl = 1000; + return waitForPromises().then(() => { + clock.tick(1000 + SLACK); + expect(coll.toArray()).to.eql([item]); + coll.refresh(); + return waitForPromises().then(() => { + clock.tick(1); + expect(coll.toArray()).to.eql([]); + }); + }); + }); + }); + + it('should refresh existing TTLs', () => { + const item = { + ttl: 1000 + }; + coll.add(item); + return waitForPromises().then(() => { + clock.tick(1000); + item.ttl = 4000; + coll.refresh(); + return waitForPromises().then(() => { + clock.tick(SLACK); + expect(coll.toArray()).to.eql([item]); + clock.tick(3000); + expect(coll.toArray()).to.eql([]); + }); + }); + }); + + it('should discard initial TTL if it does not resolve before a refresh', () => { + let resolveTTL; + const item = { + ttl: new Promise((resolve) => { + resolveTTL = resolve; + }) + }; + coll.add(item); + item.ttl = null; + coll.refresh(); + resolveTTL(1000); + return waitForPromises().then(() => { + clock.tick(1000 + SLACK + 1000); + expect(coll.toArray()).to.eql([item]); + }); + }); + + it('should discard TTLs on clear', () => { + const item = { + ttl: 1000 + }; + coll.add(item); + coll.clear(); + item.ttl = null; + coll.add(item); + return waitForPromises().then(() => { + clock.tick(1000 + SLACK + 1000); + expect(coll.toArray()).to.eql([item]); + }); + }); + }); + }); + }); + }); +}); diff --git a/test/spec/utils_spec.js b/test/spec/utils_spec.js index e26683074c8..098582c0af6 100644 --- a/test/spec/utils_spec.js +++ b/test/spec/utils_spec.js @@ -2,7 +2,9 @@ import {getAdServerTargeting} from 'test/fixtures/fixtures.js'; import {expect} from 'chai'; import CONSTANTS from 'src/constants.json'; import * as utils from 'src/utils.js'; -import {deepEqual, memoize, waitForElementToLoad} from 'src/utils.js'; +import {getHighestCpm, getLatestHighestCpmBid, getOldestHighestCpmBid} from '../../src/utils/reducers.js'; +import {binarySearch, deepEqual, memoize, waitForElementToLoad} from 'src/utils.js'; +import {convertCamelToUnderscore} from '../../libraries/appnexusUtils/anUtils.js'; var assert = require('assert'); @@ -39,28 +41,6 @@ describe('Utils', function () { }); }); - describe('tryAppendQueryString', function () { - it('should append query string to existing url', function () { - var url = 'www.a.com?'; - var key = 'b'; - var value = 'c'; - - var output = utils.tryAppendQueryString(url, key, value); - - var expectedResult = url + key + '=' + encodeURIComponent(value) + '&'; - assert.equal(output, expectedResult); - }); - - it('should return existing url, if the value is empty', function () { - var url = 'www.a.com?'; - var key = 'b'; - var value = ''; - - var output = utils.tryAppendQueryString(url, key, value); - assert.equal(output, url); - }); - }); - describe('parseQueryStringParameters', function () { it('should append query string to existing using the input obj', function () { var obj = { @@ -538,72 +518,6 @@ describe('Utils', function () { }); }); - describe('getHighestCpm', function () { - it('should pick the existing highest cpm', function () { - let previous = { - cpm: 2, - timeToRespond: 100 - }; - let current = { - cpm: 1, - timeToRespond: 100 - }; - assert.equal(utils.getHighestCpm(previous, current), previous); - }); - - it('should pick the new highest cpm', function () { - let previous = { - cpm: 1, - timeToRespond: 100 - }; - let current = { - cpm: 2, - timeToRespond: 100 - }; - assert.equal(utils.getHighestCpm(previous, current), current); - }); - - it('should pick the fastest cpm in case of tie', function () { - let previous = { - cpm: 1, - timeToRespond: 100 - }; - let current = { - cpm: 1, - timeToRespond: 50 - }; - assert.equal(utils.getHighestCpm(previous, current), current); - }); - - it('should pick the oldest in case of tie using responseTimeStamp', function () { - let previous = { - cpm: 1, - timeToRespond: 100, - responseTimestamp: 1000 - }; - let current = { - cpm: 1, - timeToRespond: 50, - responseTimestamp: 2000 - }; - assert.equal(utils.getOldestHighestCpmBid(previous, current), previous); - }); - - it('should pick the latest in case of tie using responseTimeStamp', function () { - let previous = { - cpm: 1, - timeToRespond: 100, - responseTimestamp: 1000 - }; - let current = { - cpm: 1, - timeToRespond: 50, - responseTimestamp: 2000 - }; - assert.equal(utils.getLatestHighestCpmBid(previous, current), current); - }); - }); - describe('polyfill test', function () { it('should not add polyfill to array', function() { var arr = ['hello', 'world']; @@ -765,43 +679,15 @@ describe('Utils', function () { describe('convertCamelToUnderscore', function () { it('returns converted string value using underscore syntax instead of camelCase', function () { let var1 = 'placementIdTest'; - let test1 = utils.convertCamelToUnderscore(var1); + let test1 = convertCamelToUnderscore(var1); expect(test1).to.equal('placement_id_test'); let var2 = 'my_test_value'; - let test2 = utils.convertCamelToUnderscore(var2); + let test2 = convertCamelToUnderscore(var2); expect(test2).to.equal(var2); }); }); - describe('getAdUnitSizes', function () { - it('returns an empty response when adUnits is undefined', function () { - let sizes = utils.getAdUnitSizes(); - expect(sizes).to.be.undefined; - }); - - it('returns an empty array when invalid data is present in adUnit object', function () { - let sizes = utils.getAdUnitSizes({ sizes: 300 }); - expect(sizes).to.deep.equal([]); - }); - - it('retuns an array of arrays when reading from adUnit.sizes', function () { - let sizes = utils.getAdUnitSizes({ sizes: [300, 250] }); - expect(sizes).to.deep.equal([[300, 250]]); - - sizes = utils.getAdUnitSizes({ sizes: [[300, 250], [300, 600]] }); - expect(sizes).to.deep.equal([[300, 250], [300, 600]]); - }); - - it('returns an array of arrays when reading from adUnit.mediaTypes.banner.sizes', function () { - let sizes = utils.getAdUnitSizes({ mediaTypes: { banner: { sizes: [300, 250] } } }); - expect(sizes).to.deep.equal([[300, 250]]); - - sizes = utils.getAdUnitSizes({ mediaTypes: { banner: { sizes: [[300, 250], [300, 600]] } } }); - expect(sizes).to.deep.equal([[300, 250], [300, 600]]); - }); - }); - describe('URL helpers', function () { describe('parseUrl()', function () { let parsed; @@ -1233,5 +1119,44 @@ describe('memoize', () => { mem('one', 'three'); expect(mem('one', 'three')).to.eql(['one', 'three']); expect(fn.callCount).to.eql(2); - }) + }); + + describe('binarySearch', () => { + [ + { + arr: [], + tests: [ + ['any', 0] + ] + }, + { + arr: [10], + tests: [ + [5, 0], + [10, 0], + [20, 1], + ], + }, + { + arr: [10, 20, 30, 30, 40], + tests: [ + [5, 0], + [15, 1], + [10, 0], + [30, 2], + [35, 4], + [40, 4], + [100, 5] + ] + } + ].forEach(({arr, tests}) => { + describe(`on ${arr}`, () => { + tests.forEach(([el, pos]) => { + it(`finds index for ${el} => ${pos}`, () => { + expect(binarySearch(arr, el)).to.equal(pos); + }); + }); + }); + }) + }); }) diff --git a/test/spec/videoCache_spec.js b/test/spec/videoCache_spec.js index c7c0b2eb329..c746fdd2afd 100644 --- a/test/spec/videoCache_spec.js +++ b/test/spec/videoCache_spec.js @@ -174,7 +174,7 @@ describe('The video cache', function () { const request = server.requests[0]; request.method.should.equal('POST'); request.url.should.equal('https://prebid.adnxs.com/pbc/v1/cache'); - request.requestHeaders['Content-Type'].should.equal('text/plain;charset=utf-8'); + request.requestHeaders['Content-Type'].should.equal('text/plain'); let payload = { puts: [{ type: 'xml', @@ -224,7 +224,7 @@ describe('The video cache', function () { const request = server.requests[0]; request.method.should.equal('POST'); request.url.should.equal('https://prebid.adnxs.com/pbc/v1/cache'); - request.requestHeaders['Content-Type'].should.equal('text/plain;charset=utf-8'); + request.requestHeaders['Content-Type'].should.equal('text/plain'); let payload = { puts: [{ type: 'xml', @@ -295,7 +295,7 @@ describe('The video cache', function () { const request = server.requests[0]; request.method.should.equal('POST'); request.url.should.equal('https://prebid.adnxs.com/pbc/v1/cache'); - request.requestHeaders['Content-Type'].should.equal('text/plain;charset=utf-8'); + request.requestHeaders['Content-Type'].should.equal('text/plain'); let payload = { puts: [{ type: 'xml', @@ -356,7 +356,7 @@ describe('The video cache', function () { const request = server.requests[0]; request.method.should.equal('POST'); request.url.should.equal('https://prebid.adnxs.com/pbc/v1/cache'); - request.requestHeaders['Content-Type'].should.equal('text/plain;charset=utf-8'); + request.requestHeaders['Content-Type'].should.equal('text/plain'); JSON.parse(request.requestBody).should.deep.equal({ puts: [{ diff --git a/test/spec/video_spec.js b/test/spec/video_spec.js index 61621c7ec42..35d0a4fef24 100644 --- a/test/spec/video_spec.js +++ b/test/spec/video_spec.js @@ -1,4 +1,4 @@ -import { isValidVideoBid } from 'src/video.js'; +import {fillVideoDefaults, isValidVideoBid} from 'src/video.js'; import {hook} from '../../src/hook.js'; import {stubAuctionIndex} from '../helpers/indexStub.js'; @@ -7,97 +7,169 @@ describe('video.js', function () { hook.ready(); }); - it('validates valid instream bids', function () { - const bid = { - adId: '456xyz', - vastUrl: 'http://www.example.com/vastUrl', - transactionId: 'au' - }; - const adUnits = [{ - transactionId: 'au', - mediaTypes: { - video: {context: 'instream'} - } - }]; - const valid = isValidVideoBid(bid, {index: stubAuctionIndex({adUnits})}); - expect(valid).to.equal(true); - }); + describe('fillVideoDefaults', () => { + function fillDefaults(videoMediaType = {}) { + const adUnit = {mediaTypes: {video: videoMediaType}}; + fillVideoDefaults(adUnit); + return adUnit.mediaTypes.video; + } - it('catches invalid instream bids', function () { - const bid = { - transactionId: 'au' - }; - const adUnits = [{ - transactionId: 'au', - mediaTypes: { - video: {context: 'instream'} - } - }]; - const valid = isValidVideoBid(bid, {index: stubAuctionIndex({adUnits})}); - expect(valid).to.equal(false); - }); + describe('should set plcmt = 4 when', () => { + it('context is "outstream"', () => { + expect(fillDefaults({context: 'outstream'})).to.eql({ + context: 'outstream', + plcmt: 4 + }) + }); + [2, 3, 4].forEach(placement => { + it(`placemement is "${placement}"`, () => { + expect(fillDefaults({placement})).to.eql({ + placement, + plcmt: 4 + }); + }) + }); + }); + describe('should set plcmt = 2 when', () => { + [2, 6].forEach(playbackmethod => { + it(`playbackmethod is "${playbackmethod}"`, () => { + expect(fillDefaults({playbackmethod})).to.eql({ + playbackmethod, + plcmt: 2, + }); + }); + }); + }); + describe('should not set plcmt when', () => { + Object.entries({ + 'it was set by pub (context=outstream)': { + expected: 1, + video: { + context: 'outstream', + plcmt: 1 + } + }, + 'it was set by pub (placement=2)': { + expected: 1, + video: { + placement: 2, + plcmt: 1 + } + }, + 'placement not in 2, 3, 4': { + expected: undefined, + video: { + placement: 1 + } + }, + 'it was set by pub (playbackmethod=2)': { + expected: 1, + video: { + plcmt: 1, + playbackmethod: 2 + } + } + }).forEach(([t, {expected, video}]) => { + it(t, () => { + expect(fillDefaults(video).plcmt).to.eql(expected); + }) + }) + }) + }) - it('catches invalid bids when prebid-cache is disabled', function () { - const adUnits = [{ - transactionId: 'au', - bidder: 'vastOnlyVideoBidder', - mediaTypes: {video: {}}, - }]; + describe('isValidVideoBid', () => { + it('validates valid instream bids', function () { + const bid = { + adId: '456xyz', + vastUrl: 'http://www.example.com/vastUrl', + transactionId: 'au' + }; + const adUnits = [{ + transactionId: 'au', + mediaTypes: { + video: {context: 'instream'} + } + }]; + const valid = isValidVideoBid(bid, {index: stubAuctionIndex({adUnits})}); + expect(valid).to.equal(true); + }); - const valid = isValidVideoBid({ transactionId: 'au', vastXml: 'vast' }, {index: stubAuctionIndex({adUnits})}); + it('catches invalid instream bids', function () { + const bid = { + transactionId: 'au' + }; + const adUnits = [{ + transactionId: 'au', + mediaTypes: { + video: {context: 'instream'} + } + }]; + const valid = isValidVideoBid(bid, {index: stubAuctionIndex({adUnits})}); + expect(valid).to.equal(false); + }); - expect(valid).to.equal(false); - }); + it('catches invalid bids when prebid-cache is disabled', function () { + const adUnits = [{ + transactionId: 'au', + bidder: 'vastOnlyVideoBidder', + mediaTypes: {video: {}}, + }]; - it('validates valid outstream bids', function () { - const bid = { - transactionId: 'au', - renderer: { - url: 'render.url', - render: () => true, - } - }; - const adUnits = [{ - transactionId: 'au', - mediaTypes: { - video: {context: 'outstream'} - } - }]; - const valid = isValidVideoBid(bid, {index: stubAuctionIndex({adUnits})}); - expect(valid).to.equal(true); - }); + const valid = isValidVideoBid({ transactionId: 'au', vastXml: 'vast' }, {index: stubAuctionIndex({adUnits})}); - it('validates valid outstream bids with a publisher defined renderer', function () { - const bid = { - transactionId: 'au', - }; - const adUnits = [{ - transactionId: 'au', - mediaTypes: { - video: { - context: 'outstream', + expect(valid).to.equal(false); + }); + + it('validates valid outstream bids', function () { + const bid = { + transactionId: 'au', + renderer: { + url: 'render.url', + render: () => true, } - }, - renderer: { - url: 'render.url', - render: () => true, - } - }]; - const valid = isValidVideoBid(bid, {index: stubAuctionIndex({adUnits})}); - expect(valid).to.equal(true); - }); + }; + const adUnits = [{ + transactionId: 'au', + mediaTypes: { + video: {context: 'outstream'} + } + }]; + const valid = isValidVideoBid(bid, {index: stubAuctionIndex({adUnits})}); + expect(valid).to.equal(true); + }); - it('catches invalid outstream bids', function () { - const bid = { - transactionId: 'au', - }; - const adUnits = [{ - transactionId: 'au', - mediaTypes: { - video: {context: 'outstream'} - } - }]; - const valid = isValidVideoBid(bid, {index: stubAuctionIndex({adUnits})}); - expect(valid).to.equal(false); - }); + it('validates valid outstream bids with a publisher defined renderer', function () { + const bid = { + transactionId: 'au', + }; + const adUnits = [{ + transactionId: 'au', + mediaTypes: { + video: { + context: 'outstream', + } + }, + renderer: { + url: 'render.url', + render: () => true, + } + }]; + const valid = isValidVideoBid(bid, {index: stubAuctionIndex({adUnits})}); + expect(valid).to.equal(true); + }); + + it('catches invalid outstream bids', function () { + const bid = { + transactionId: 'au', + }; + const adUnits = [{ + transactionId: 'au', + mediaTypes: { + video: {context: 'outstream'} + } + }]; + const valid = isValidVideoBid(bid, {index: stubAuctionIndex({adUnits})}); + expect(valid).to.equal(false); + }); + }) }); diff --git a/test/test_deps.js b/test/test_deps.js index 35713106f8c..c8a3bcc9426 100644 --- a/test/test_deps.js +++ b/test/test_deps.js @@ -4,6 +4,16 @@ window.process = { } }; +window.addEventListener('error', function (ev) { + // eslint-disable-next-line no-console + console.error('Uncaught exception:', ev.error, ev.error?.stack); +}) + +window.addEventListener('unhandledrejection', function (ev) { + // eslint-disable-next-line no-console + console.error('Unhandled rejection:', ev.reason); +}) + require('test/helpers/consentData.js'); require('test/helpers/prebidGlobal.js'); require('test/mocks/adloaderStub.js'); diff --git a/webpack.creative.js b/webpack.creative.js new file mode 100644 index 00000000000..7279455e155 --- /dev/null +++ b/webpack.creative.js @@ -0,0 +1,19 @@ +const path = require('path'); + +module.exports = { + mode: 'production', + resolve: { + modules: [ + path.resolve('.'), + 'node_modules' + ], + }, + entry: { + 'creative': { + import: './libraries/creativeRender/crossDomain.js', + }, + }, + output: { + path: path.resolve('./build/creative'), + }, +}