diff --git a/.babelrc.js b/.babelrc.js index 43303f59a8b..add243a5b5d 100644 --- a/.babelrc.js +++ b/.babelrc.js @@ -1,25 +1 @@ - -let path = require('path'); - -function useLocal(module) { - return require.resolve(module, { - paths: [ - __dirname - ] - }) -} - -module.exports = { - "presets": [ - [ - useLocal('@babel/preset-env'), - { - "useBuiltIns": "entry" - } - ] - ], - "plugins": [ - path.resolve(__dirname, './plugins/pbjsGlobals.js'), - useLocal('babel-plugin-transform-object-assign') - ] -}; +module.exports = require('./babelConfig.js')(); diff --git a/.circleci/config.yml b/.circleci/config.yml index 404026d9446..784b520ecc1 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -3,11 +3,11 @@ # Check https://circleci.com/docs/2.0/language-javascript/ for more details # -aliases: +aliases: - &environment docker: # specify the version you desire here - - image: circleci/node:12.16.1 + - image: circleci/node:12.16.1-browsers resource_class: xlarge # Specify service dependencies here if necessary # CircleCI maintains a library of pre-built images @@ -72,7 +72,7 @@ jobs: build: <<: *environment steps: *unit_test_steps - + e2etest: <<: *environment steps: *endtoend_test_steps diff --git a/.eslintrc.js b/.eslintrc.js index 78e4fb1bb33..d3379d70919 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -23,10 +23,13 @@ module.exports = { 'BROWSERSTACK_USERNAME': false, 'BROWSERSTACK_KEY': false }, + // use babel as parser for fancy syntax + parser: '@babel/eslint-parser', parserOptions: { sourceType: 'module', ecmaVersion: 2018, }, + rules: { 'comma-dangle': 'off', semi: 'off', @@ -49,5 +52,9 @@ module.exports = { rules: { 'prebid/validate-imports': ['error', allowedModules[key]] } - })) + })).concat([{ + // code in other packages (such as plugins/eslint) is not "seen" by babel and its parser will complain. + files: 'plugins/*/**/*.js', + parser: 'esprima' + }]) }; diff --git a/.github/workflows/issue_tracker.yml b/.github/workflows/issue_tracker.yml new file mode 100644 index 00000000000..4397337b4c7 --- /dev/null +++ b/.github/workflows/issue_tracker.yml @@ -0,0 +1,89 @@ +name: Issue tracking +on: + issues: + types: + - opened +jobs: + track_issue: + runs-on: ubuntu-latest + steps: + - name: Generate token + id: generate_token + uses: tibdex/github-app-token@36464acb844fc53b9b8b2401da68844f6b05ebb0 + with: + app_id: ${{ secrets.ISSUE_APP_ID }} + private_key: ${{ secrets.ISSUE_APP_PEM }} + + - name: Get project data + env: + GITHUB_TOKEN: ${{ steps.generate_token.outputs.token }} + ORGANIZATION: prebid + DATE_FIELD: Created on + PROJECT_NUMBER: 2 + run: | + gh api graphql -f query=' + query($org: String!, $number: Int!) { + organization(login: $org){ + projectNext(number: $number) { + id + fields(first:100) { + nodes { + id + name + settings + } + } + } + } + }' -f org=$ORGANIZATION -F number=$PROJECT_NUMBER > project_data.json + + echo 'PROJECT_ID='$(jq '.data.organization.projectNext.id' project_data.json) >> $GITHUB_ENV + echo 'DATE_FIELD_ID='$(jq '.data.organization.projectNext.fields.nodes[] | select(.name== "'"$DATE_FIELD"'") | .id' project_data.json) >> $GITHUB_ENV + + - name: Add issue to project + env: + GITHUB_TOKEN: ${{ steps.generate_token.outputs.token }} + ISSUE_ID: ${{ github.event.issue.node_id }} + run: | + gh api graphql -f query=' + mutation($project:ID!, $issue:ID!) { + addProjectNextItem(input: {projectId: $project, contentId: $issue}) { + projectNextItem { + id, + content { + ... on Issue { + createdAt + } + ... on PullRequest { + createdAt + } + } + } + } + }' -f project=$PROJECT_ID -f issue=$ISSUE_ID > issue_data.json + + echo 'ITEM_ID='$(jq '.data.addProjectNextItem.projectNextItem.id' issue_data.json) >> $GITHUB_ENV + echo 'ITEM_CREATION_DATE='$(jq '.data.addProjectNextItem.projectNextItem.content.createdAt' issue_data.json) >> $GITHUB_ENV + + - name: Set fields + env: + GITHUB_TOKEN: ${{ steps.generate_token.outputs.token }} + run: | + gh api graphql -f query=' + mutation ( + $project: ID! + $item: ID! + $date_field: ID! + $date_value: String! + ) { + set_creation_date: updateProjectNextItemField(input: { + projectId: $project + itemId: $item + fieldId: $date_field + value: $date_value + }) { + projectNextItem { + id + } + } + }' -f project=$PROJECT_ID -f item=$ITEM_ID -f date_field=$DATE_FIELD_ID -f date_value=$ITEM_CREATION_DATE --silent diff --git a/README.md b/README.md index bc0e64afa06..42d747b20b6 100644 --- a/README.md +++ b/README.md @@ -130,16 +130,22 @@ Once setup, run the following command to globally install the `gulp-cli` package ## Build for Development -To build the project on your local machine, run: +To build the project on your local machine we recommend, running: - $ gulp serve + $ gulp serve-and-test --file -This runs some code quality checks, starts a web server at `http://localhost:9999` serving from the project root and generates the following files: +This will run testing but not linting. A web server will start at `http://localhost:9999` serving from the project root and generates the following files: + `./build/dev/prebid.js` - Full source code for dev and debug + `./build/dev/prebid.js.map` - Source map for dev and debug -+ `./build/dist/prebid.js` - Minified production code -+ `./prebid.js_.zip` - Distributable zip archive ++ `./build/dev/prebid-core.js` ++ `./build/dev/prebid-core.js.map` + + +Development may be a bit slower but if you prefer linting and additional watch files you can also still run just: + + $ gulp serve + ### Build Optimization diff --git a/RELEASE_SCHEDULE.md b/RELEASE_SCHEDULE.md index bfbd0772c3e..b68495ed4ae 100644 --- a/RELEASE_SCHEDULE.md +++ b/RELEASE_SCHEDULE.md @@ -3,12 +3,7 @@ - [Release Process](#release-process) - [1. Make sure that all PRs have been named and labeled properly per the PR Process](#1-make-sure-that-all-prs-have-been-named-and-labeled-properly-per-the-pr-process) - [2. Make sure all browserstack tests are passing](#2-make-sure-all-browserstack-tests-are-passing) - - [3. Prepare Prebid Code](#3-prepare-prebid-code) - - [4. Verify the Release](#4-verify-the-release) - - [5. Create a GitHub release](#5-create-a-github-release) - - [6. Update coveralls _(skip for legacy)_](#6-update-coveralls-skip-for-legacy) - - [7. Distribute the code](#7-distribute-the-code) - - [8. Increment Version for Next Release](#8-increment-version-for-next-release) + - [3. Start the release](#3-start-the-release) - [Beta Releases](#beta-releases) - [FAQs](#faqs) @@ -21,12 +16,10 @@ it will be about a week before the Prebid Org [Download Page](http://prebid.org/ You can determine what is in a given build using the [releases page](https://github.com/prebid/Prebid.js/releases) -Announcements regarding releases will be made to the #headerbidding-dev channel in subredditadops.slack.com. +Announcements regarding releases will be made to the #prebid-js channel in prebid.slack.com. ## Release Process -_Note: If `github.com/prebid/Prebid.js` is not configured as the git origin for your repo, all of the following git commands will have to be modified to reference the proper remote (e.g. `upstream`)_ - ### 1. Make sure that all PRs have been named and labeled properly per the [PR Process](https://github.com/prebid/Prebid.js/blob/master/PR_REVIEW.md#general-pr-review-process) * Do this by checking the latest draft release from the [releases page](https://github.com/prebid/Prebid.js/releases) and make sure nothing appears in the first section called "In This Release". If they do, please open the PRs and add the appropriate labels. * Do a quick check that all the titles/descriptions look ok, and if not, adjust the PR title. @@ -57,61 +50,10 @@ _Note: If `github.com/prebid/Prebid.js` is not configured as the git origin for ``` -### 3. Prepare Prebid Code - - Update the package.json version to become the current release. Then commit your changes. - - ``` - git commit -m "Prebid 4.x.x Release" - git push - ``` - -### 4. Verify the Release - - Make sure your there are no more merges to master branch. Prebid code is clean and up to date. - -### 5. Create a GitHub release - - Edit the most recent [release notes](https://github.com/prebid/Prebid.js/releases) draft and make sure the correct version is set and the master branch is selected in the dropdown. Click `Publish release`. GitHub will create release tag. - - Pull these changes locally by running command - ``` - git pull - git fetch --tags - ``` - - and verify the tag. - -### 6. Update coveralls _(skip for legacy)_ - - We use https://coveralls.io/ to show parts of code covered by unit tests. - - Set the environment variables. You may want to add these to your `~/.bashrc` for convenience. - ``` - export COVERALLS_SERVICE_NAME="travis-ci" - export COVERALLS_REPO_TOKEN="talk to Matt Kendall" - ``` - - Run `gulp coveralls` to update code coverage history. - -### 7. Distribute the code - - _Note: do not go to step 8 until step 7 has been verified completed._ - - Reach out to any of the Appnexus folks to trigger the jenkins job. - - // TODO: - Jenkins job is moving files to appnexus cdn, pushing prebid.js to npm, purging cache and sending notification to slack. - Move all the files from Appnexus CDN to jsDelivr and create bash script to do above tasks. - -### 8. Increment Version for Next Release - - Update the version by manually editing Prebid's `package.json` to become "4.x.x-pre" (using the values for the next release). Then commit your changes. - ``` - git commit -m "Increment pre version" - git push - ``` +### 3. Start the release +Follow the instructions at https://github.com/prebid/prebidjs-releaser. Note that you will need to be a member of the [https://github.com/orgs/prebid/teams/prebidjs-release](prebidjs-release) GitHub team. + ## Beta Releases Prebid.js features may be released as Beta or as Generally Available (GA). diff --git a/allowedModules.js b/allowedModules.js index 81920cdc15f..be9a2dc2abf 100644 --- a/allowedModules.js +++ b/allowedModules.js @@ -1,12 +1,5 @@ const sharedWhiteList = [ - 'core-js-pure/features/array/find', // no ie11 - 'core-js-pure/features/array/includes', // no ie11 - 'core-js-pure/features/set', // ie11 supports Set but not Set#values - 'core-js-pure/features/string/includes', // no ie11 - 'core-js-pure/features/number/is-integer', // no ie11, - 'core-js-pure/features/array/from', // no ie11 - 'core-js-pure/web/url-search-params' // no ie11 ]; module.exports = { diff --git a/babelConfig.js b/babelConfig.js new file mode 100644 index 00000000000..c1ddc11b689 --- /dev/null +++ b/babelConfig.js @@ -0,0 +1,30 @@ + +let path = require('path'); + +function useLocal(module) { + return require.resolve(module, { + paths: [ + __dirname + ] + }) +} + +module.exports = function (test = false) { + return { + 'presets': [ + [ + useLocal('@babel/preset-env'), + { + 'useBuiltIns': 'entry', + 'corejs': '3.13.0', + // a lot of tests use sinon.stub & others that stopped working on ES6 modules with webpack 5 + 'modules': test ? 'commonjs' : 'auto', + } + ] + ], + 'plugins': [ + path.resolve(__dirname, './plugins/pbjsGlobals.js'), + useLocal('babel-plugin-transform-object-assign'), + ], + } +} diff --git a/gulpfile.js b/gulpfile.js index 6ecfee1b672..0c4ebc50653 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -8,7 +8,6 @@ var gutil = require('gulp-util'); var connect = require('gulp-connect'); var webpack = require('webpack'); var webpackStream = require('webpack-stream'); -var terser = require('gulp-terser'); var gulpClean = require('gulp-clean'); var KarmaServer = require('karma').Server; var karmaConfMaker = require('./karma.conf.maker.js'); @@ -117,7 +116,10 @@ viewReview.displayName = 'view-review'; function makeDevpackPkg() { var cloned = _.cloneDeep(webpackConfig); - cloned.devtool = 'source-map'; + Object.assign(cloned, { + devtool: 'source-map', + mode: 'development' + }) var externalModules = helpers.getArgModules(); const analyticsSources = helpers.getAnalyticsSources(); @@ -132,7 +134,9 @@ function makeDevpackPkg() { function makeWebpackPkg() { var cloned = _.cloneDeep(webpackConfig); - delete cloned.devtool; + if (!argv.sourceMaps) { + delete cloned.devtool; + } var externalModules = helpers.getArgModules(); @@ -142,11 +146,19 @@ function makeWebpackPkg() { return gulp.src([].concat(moduleSources, analyticsSources, 'src/prebid.js')) .pipe(helpers.nameModules(externalModules)) .pipe(webpackStream(cloned, webpack)) - .pipe(terser()) - .pipe(gulpif(file => file.basename === 'prebid-core.js', header(banner, { prebid: prebid }))) .pipe(gulp.dest('build/dist')); } +function addBanner() { + const sm = argv.sourceMaps; + + return gulp.src(['build/dist/prebid-core.js']) + .pipe(gulpif(sm, sourcemaps.init({loadMaps: true}))) + .pipe(header(banner, {prebid})) + .pipe(gulpif(sm, sourcemaps.write('.'))) + .pipe(gulp.dest('build/dist')) +} + function getModulesListToAddInBanner(modules) { return (modules.length > 0) ? modules.join(', ') : 'All available modules in current version.'; } @@ -171,6 +183,7 @@ function nodeBundle(modules) { function bundle(dev, moduleArr) { var modules = moduleArr || helpers.getArgModules(); var allModules = helpers.getModuleNames(modules); + const sm = dev || argv.sourceMaps; if (modules.length === 0) { modules = allModules.filter(module => explicitModules.indexOf(module) === -1); @@ -202,13 +215,13 @@ function bundle(dev, moduleArr) { ) // Need to uodate the "Modules: ..." section in comment with the current modules list .pipe(replace(/(Modules: )(.*?)(\*\/)/, ('$1' + getModulesListToAddInBanner(helpers.getArgModules()) + ' $3'))) - .pipe(gulpif(dev, sourcemaps.init({ loadMaps: true }))) + .pipe(gulpif(sm, sourcemaps.init({ loadMaps: true }))) .pipe(concat(outputFileName)) .pipe(gulpif(!argv.manualEnable, footer('\n<%= global %>.processQueue();', { global: prebid.globalVarName } ))) - .pipe(gulpif(dev, sourcemaps.write('.'))); + .pipe(gulpif(sm, sourcemaps.write('.'))); } // Run the unit tests. @@ -397,7 +410,7 @@ gulp.task(clean); gulp.task(escapePostbidConfig); gulp.task('build-bundle-dev', gulp.series(makeDevpackPkg, gulpBundle.bind(null, true))); -gulp.task('build-bundle-prod', gulp.series(makeWebpackPkg, gulpBundle.bind(null, false))); +gulp.task('build-bundle-prod', gulp.series(makeWebpackPkg, addBanner, gulpBundle.bind(null, false))); // public tasks (dependencies are needed for each task since they can be ran on their own) gulp.task('test-only', test); @@ -416,7 +429,7 @@ gulp.task('serve-fast', gulp.series(clean, gulp.parallel('build-bundle-dev', wat gulp.task('serve-and-test', gulp.series(clean, gulp.parallel('build-bundle-dev', watchFast, testTaskMaker({watch: true})))); gulp.task('serve-fake', gulp.series(clean, gulp.parallel('build-bundle-dev', watch), injectFakeServerEndpointDev, test, startFakeServer)); -gulp.task('default', gulp.series(clean, makeWebpackPkg)); +gulp.task('default', gulp.series(clean, 'build-bundle-prod')); gulp.task('e2e-test', gulp.series(clean, setupE2e, gulp.parallel('build-bundle-prod', watch), injectFakeServerEndpoint, test)); // other tasks diff --git a/integrationExamples/gpt/adloox.html b/integrationExamples/gpt/adloox.html index e8920cf2ee1..fd61267479d 100644 --- a/integrationExamples/gpt/adloox.html +++ b/integrationExamples/gpt/adloox.html @@ -161,7 +161,11 @@ }, rubicon: { singleRequest: true - } + }, + // RTD module honors pageUrl for referrer detection and + // the analytics module uses this for the 'pageurl' macro + // N.B. set this to a non-example.com URL to see the video + //pageUrl: 'https://yourdomain.com/some/path/to/content.html' }); pbjs.enableAnalytics({ provider: 'adloox', diff --git a/integrationExamples/gpt/amp/creative.html b/integrationExamples/gpt/amp/creative.html index 86f669dd6b5..384b81107cc 100644 --- a/integrationExamples/gpt/amp/creative.html +++ b/integrationExamples/gpt/amp/creative.html @@ -1,38 +1,16 @@ + diff --git a/integrationExamples/gpt/esp_example.html b/integrationExamples/gpt/esp_example.html new file mode 100644 index 00000000000..c39a67243cc --- /dev/null +++ b/integrationExamples/gpt/esp_example.html @@ -0,0 +1,177 @@ + + + + + + + + + + + + +

Basic Prebid.js Example

+ +
Div-1
+
+ +
+ +
+ +
Div-2
+
+ +
+ + + + \ No newline at end of file diff --git a/integrationExamples/gpt/haloRtdProvider_example.html b/integrationExamples/gpt/hadronRtdProvider_example.html similarity index 87% rename from integrationExamples/gpt/haloRtdProvider_example.html rename to integrationExamples/gpt/hadronRtdProvider_example.html index 14debbd2698..065c8379956 100644 --- a/integrationExamples/gpt/haloRtdProvider_example.html +++ b/integrationExamples/gpt/hadronRtdProvider_example.html @@ -1,8 +1,8 @@ -Halo Id: -
+Hadron Id: +
-Halo Real-Time Data: +Hadron Real-Time Data:
diff --git a/integrationExamples/gpt/idImportLibrary_example.html b/integrationExamples/gpt/idImportLibrary_example.html index 07a4f0fe1c5..363e8015f53 100644 --- a/integrationExamples/gpt/idImportLibrary_example.html +++ b/integrationExamples/gpt/idImportLibrary_example.html @@ -69,10 +69,10 @@ name: "zeotapIdPlus" }, { - name: 'haloId', + name: 'hadronId', storage: { type: "html5", - name: "haloId", + name: "hadronId", expires: 28 } }, { diff --git a/integrationExamples/gpt/idward_segments_example.html b/integrationExamples/gpt/idward_segments_example.html new file mode 100644 index 00000000000..9bc06124c77 --- /dev/null +++ b/integrationExamples/gpt/idward_segments_example.html @@ -0,0 +1,112 @@ + + + + + + + + + + + + + +

Prebid.js Test

+
Div-1
+
+ +
+
First Party Data (ortb2) Sent to Bidding Adapter
+
+ + diff --git a/integrationExamples/gpt/userId_example.html b/integrationExamples/gpt/userId_example.html index 653dd9c59f3..e11a0b626c9 100644 --- a/integrationExamples/gpt/userId_example.html +++ b/integrationExamples/gpt/userId_example.html @@ -215,10 +215,10 @@ "name": "zeotapIdPlus" }, { - "name": "haloId", + "name": "hadronId", "storage": { "type": "cookie", - "name": "haloId", + "name": "hadronId", "expires": 28 } }, @@ -252,6 +252,9 @@ "params": { "cid": 5126 // Set your Intimate Merger Customer ID here for production } + }, + { + "name": "dacId" } ], "syncDelay": 5000, diff --git a/integrationExamples/gpt/weboramaRtdProvider_example.html b/integrationExamples/gpt/weboramaRtdProvider_example.html index 66e4a57d2a6..b81ec52b2c4 100644 --- a/integrationExamples/gpt/weboramaRtdProvider_example.html +++ b/integrationExamples/gpt/weboramaRtdProvider_example.html @@ -1,135 +1,174 @@ - + + + - - + + - + - - -
-

-test webo ctx using prebid.js -

-
-

Basic Prebid.js Example

-
Div-1
-
- -
- + }); + } + + + // in case PBJS doesn't load + setTimeout(function () { + initAdserver(); + }, FAILSAFE_TIMEOUT); + + googletag.cmd.push(function () { + googletag.defineSlot('/1056029/webo-ctx-prebid', div_1_sizes, 'div-gpt-ad-1620653642627-0').addService(googletag.pubads()); + googletag.pubads().disableInitialLoad(); + googletag.enableServices(); + }); + + + +
+

+ test webo ctx using prebid.js +

+
+

Basic Prebid.js Example

+
Div-1
+
+ +
+ - + + \ No newline at end of file diff --git a/integrationExamples/gpt/x-domain/creative.html b/integrationExamples/gpt/x-domain/creative.html index fce46bb380f..bea8b70b4fe 100644 --- a/integrationExamples/gpt/x-domain/creative.html +++ b/integrationExamples/gpt/x-domain/creative.html @@ -2,37 +2,45 @@ // this script can be returned by an ad server delivering a cross domain iframe, into which the // creative will be rendered, e.g. DFP delivering a SafeFrame -let windowLocation = window.location; -var urlParser = document.createElement('a'); +const windowLocation = window.location; +const urlParser = document.createElement('a'); urlParser.href = '%%PATTERN:url%%'; -var publisherDomain = urlParser.protocol + '//' + urlParser.hostname; +const publisherDomain = urlParser.protocol + '//' + urlParser.hostname; +const adId = '%%PATTERN:hb_adid%%'; + +function receiveMessage(ev) { + const origin = ev.origin || ev.originalEvent.origin; + if (origin === publisherDomain) { + renderAd(ev); + } +} function renderAd(ev) { - var key = ev.message ? 'message' : 'data'; - var adObject = {}; - try { - adObject = JSON.parse(ev[key]); - } catch (e) { - return; - } + const key = ev.message ? 'message' : 'data'; + let adObject = {}; + try { + adObject = JSON.parse(ev[key]); + } catch (e) { + return; + } - var origin = ev.origin || ev.originalEvent.origin; - if (adObject.message && adObject.message === 'Prebid Response' && - publisherDomain === origin && - adObject.adId === '%%PATTERN:hb_adid%%' && - (adObject.ad || adObject.adUrl)) { - var body = window.document.body; - var ad = adObject.ad; - var url = adObject.adUrl; - var width = adObject.width; - var height = adObject.height; + if (adObject.message && adObject.message === 'Prebid Response' && + adObject.adId === adId) { + try { + const body = window.document.body; + const ad = adObject.ad; + const url = adObject.adUrl; + const width = adObject.width; + const height = adObject.height; if (adObject.mediaType === 'video') { + signalRenderResult(false, { + reason: 'preventWritingOnMainDocument', + message: `Cannot render video ad ${adId}` + }); console.log('Error trying to write ad.'); - } else - - if (ad) { - var frame = document.createElement('iframe'); + } else if (ad) { + const frame = document.createElement('iframe'); frame.setAttribute('FRAMEBORDER', 0); frame.setAttribute('SCROLLING', 'no'); frame.setAttribute('MARGINHEIGHT', 0); @@ -46,24 +54,50 @@ frame.contentDocument.open(); frame.contentDocument.write(ad); frame.contentDocument.close(); + signalRenderResult(true); } else if (url) { body.insertAdjacentHTML('beforeend', ''); + signalRenderResult(true); } else { - console.log('Error trying to write ad. No ad for bid response id: ' + id); + signalRenderResult(false, { + reason: 'noAd', + message: `No ad for ${adId}` + }); + console.log(`Error trying to write ad. No ad markup or adUrl for ${adId}`); } + } catch (e) { + signalRenderResult(false, {reason: 'exception', message: e.message}); + console.log(`Error in rendering ad`, e); + } + } + + function signalRenderResult(success, {reason, message} = {}) { + const payload = { + message: 'Prebid Event', + adId, + event: success ? 'adRenderSucceeded' : 'adRenderFailed', } + if (!success) { + payload.info = {reason, message}; + } + window.parent.postMessage(JSON.stringify(payload), publisherDomain); } +} + + function requestAdFromPrebid() { - var message = JSON.stringify({ + const message = JSON.stringify({ message: 'Prebid Request', - adId: '%%PATTERN:hb_adid%%' + adId }); - window.parent.postMessage(message, publisherDomain); + const channel = new MessageChannel(); + channel.port1.onmessage = renderAd; + window.parent.postMessage(message, publisherDomain, [channel.port2]); } function listenAdFromPrebid() { - window.addEventListener('message', renderAd, false); + window.addEventListener('message', receiveMessage, false); } listenAdFromPrebid(); diff --git a/karma.conf.maker.js b/karma.conf.maker.js index be51947dae8..b5c6b44e4fd 100644 --- a/karma.conf.maker.js +++ b/karma.conf.maker.js @@ -2,6 +2,7 @@ // // For more information, see http://karma-runner.github.io/1.0/config/configuration-file.html +const babelConfig = require('./babelConfig.js'); var _ = require('lodash'); var webpackConf = require('./webpack.conf.js'); var karmaConstants = require('karma').constants; @@ -10,10 +11,19 @@ function newWebpackConfig(codeCoverage) { // Make a clone here because we plan on mutating this object, and don't want parallel tasks to trample each other. var webpackConfig = _.cloneDeep(webpackConf); - // remove optimize plugin for tests - webpackConfig.plugins.pop() + Object.assign(webpackConfig, { + mode: 'development', + devtool: 'inline-source-map', + }); - webpackConfig.devtool = 'inline-source-map'; + delete webpackConfig.entry; + + webpackConfig.module.rules + .flatMap((r) => r.use) + .filter((use) => use.loader === 'babel-loader') + .forEach((use) => { + use.options = babelConfig(true); + }); if (codeCoverage) { webpackConfig.module.rules.push({ diff --git a/modules/.submodules.json b/modules/.submodules.json index 2e77873dc78..85e4658cc61 100644 --- a/modules/.submodules.json +++ b/modules/.submodules.json @@ -7,16 +7,20 @@ "britepoolIdSystem", "connectIdSystem", "criteoIdSystem", + "dacIdSystem", "deepintentDpesIdSystem", "dmdIdSystem", "fabrickIdSystem", "flocIdSystem", + "hadronIdSystem", "haloIdSystem", "id5IdSystem", + "ftrackIdSystem", "identityLinkIdSystem", "idxIdSystem", "imuIdSystem", "intentIqIdSystem", + "justIdSystem", "kinessoIdSystem", "liveIntentIdSystem", "lotamePanoramaIdSystem", @@ -32,6 +36,7 @@ "quantcastIdSystem", "sharedIdSystem", "tapadIdSystem", + "trustpidSystem", "uid2IdSystem", "unifiedIdSystem", "verizonMediaIdSystem", @@ -46,6 +51,7 @@ "browsiRtdProvider", "dgkeywordRtdProvider", "geoedgeRtdProvider", + "hadronRtdProvider", "haloRtdProvider", "iasRtdProvider", "jwplayerRtdProvider", diff --git a/modules/33acrossBidAdapter.js b/modules/33acrossBidAdapter.js index af67bb2bf48..498e6cf8634 100644 --- a/modules/33acrossBidAdapter.js +++ b/modules/33acrossBidAdapter.js @@ -20,6 +20,7 @@ const END_POINT = 'https://ssc.33across.com/api/v1/hb'; const SYNC_ENDPOINT = 'https://ssc-cms.33across.com/ps/?m=xch&rt=html&ru=deb'; const CURRENCY = 'USD'; +const GVLID = 58; const GUID_PATTERN = /^[a-zA-Z0-9_-]{22}$/; const PRODUCT = { @@ -735,6 +736,7 @@ export const spec = { code: BIDDER_CODE, supportedMediaTypes: [ BANNER, VIDEO ], + gvlid: GVLID, isBidRequestValid, buildRequests, interpretResponse, diff --git a/modules/adagioBidAdapter.js b/modules/adagioBidAdapter.js index b000772f214..a24bc889411 100644 --- a/modules/adagioBidAdapter.js +++ b/modules/adagioBidAdapter.js @@ -1,18 +1,37 @@ -import find from 'core-js-pure/features/array/find.js'; +import {find} from '../src/polyfill.js'; import { - isInteger, isArray, deepAccess, mergeDeep, logWarn, logInfo, logError, getWindowTop, getWindowSelf, generateUUID, _map, - getDNT, parseUrl, getUniqueIdentifierStr, isNumber, cleanObj, isFn, inIframe, deepClone, getGptSlotInfoForAdUnitCode + _map, + cleanObj, + deepAccess, + deepClone, + generateUUID, + getDNT, + getGptSlotInfoForAdUnitCode, + getUniqueIdentifierStr, + getWindowSelf, + getWindowTop, + inIframe, + isArray, + isFn, + isInteger, + isNumber, + logError, + logInfo, + logWarn, + mergeDeep, + parseUrl } from '../src/utils.js'; -import { config } from '../src/config.js'; +import {config} from '../src/config.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; -import { loadExternalScript } from '../src/adloader.js'; -import { verify } from 'criteo-direct-rsa-validate/build/verify.js'; -import { getStorageManager } from '../src/storageManager.js'; -import { getRefererInfo } from '../src/refererDetection.js'; -import { createEidsArray } from './userId/eids.js'; -import { BANNER, NATIVE, VIDEO } from '../src/mediaTypes.js'; -import { Renderer } from '../src/Renderer.js'; -import { OUTSTREAM } from '../src/video.js'; +import {loadExternalScript} from '../src/adloader.js'; +import {verify} from 'criteo-direct-rsa-validate/build/verify.js'; +import {getStorageManager} from '../src/storageManager.js'; +import {getRefererInfo} from '../src/refererDetection.js'; +import {createEidsArray} from './userId/eids.js'; +import {BANNER, NATIVE, VIDEO} from '../src/mediaTypes.js'; +import {Renderer} from '../src/Renderer.js'; +import {OUTSTREAM} from '../src/video.js'; + const BIDDER_CODE = 'adagio'; const LOG_PREFIX = 'Adagio:'; const FEATURES_VERSION = '1'; @@ -21,7 +40,7 @@ const SUPPORTED_MEDIA_TYPES = [BANNER, NATIVE, VIDEO]; const ADAGIO_TAG_URL = 'https://script.4dex.io/localstore.js'; const ADAGIO_LOCALSTORAGE_KEY = 'adagioScript'; const GVLID = 617; -export const storage = getStorageManager(GVLID, 'adagio'); +export const storage = getStorageManager({gvlid: GVLID, bidderCode: BIDDER_CODE}); export const RENDERER_URL = 'https://script.4dex.io/outstream-player.js'; const MAX_SESS_DURATION = 30 * 60 * 1000; const ADAGIO_PUBKEY = 'AL16XT44Sfp+8SHVF1UdC7hydPSMVLMhsYknKDdwqq+0ToDSJrP0+Qh0ki9JJI2uYm/6VEYo8TJED9WfMkiJ4vf02CW3RvSWwc35bif2SK1L8Nn/GfFYr/2/GG/Rm0vUsv+vBHky6nuuYls20Og0HDhMgaOlXoQ/cxMuiy5QSktp'; @@ -268,8 +287,6 @@ function getSite(bidderRequest) { } else if (refererInfo.stack && refererInfo.stack.length && refererInfo.stack[0]) { // important note check if refererInfo.stack[0] is 'thruly' because a `null` value // will be considered as "localhost" by the parseUrl function. - // As the isBidRequestValid returns false when it does not reach the referer - // this should never called. const url = parseUrl(refererInfo.stack[0]); domain = url.hostname; } @@ -806,7 +823,7 @@ function getPrintNumber(adUnitCode, bidderRequest) { return 1; } const adagioBid = find(bidderRequest.bids, bid => bid.adUnitCode === adUnitCode); - return adagioBid.bidRequestsCount || 1; + return adagioBid.bidderRequestsCount || 1; } /** @@ -873,12 +890,6 @@ export const spec = { autoFillParams(bid); - if (!internal.getRefererInfo().reachedTop) { - logWarn(`${LOG_PREFIX} the main page url is unreachabled.`); - // internal.enqueue(debugData()); - return false; - } - if (!(bid.params.organizationId && bid.params.site && bid.params.placement)) { logWarn(`${LOG_PREFIX} at least one required param is missing.`); // internal.enqueue(debugData()); diff --git a/modules/adagioBidAdapter.md b/modules/adagioBidAdapter.md index 2779ced8cea..889822d9bd4 100644 --- a/modules/adagioBidAdapter.md +++ b/modules/adagioBidAdapter.md @@ -18,7 +18,7 @@ Below, the list of Adagio params and where they can be set. | ---------- | ------------- | ------------- | | siteId | x | | organizationId (obsolete) | | x -| site (obsolete) | | x +| site (obsolete) | | x | pagetype | x | x | environment | x | x | category | x | x diff --git a/modules/adbookpspBidAdapter.js b/modules/adbookpspBidAdapter.js index ca4795c574f..de8a3598be1 100644 --- a/modules/adbookpspBidAdapter.js +++ b/modules/adbookpspBidAdapter.js @@ -1,13 +1,24 @@ -import includes from 'core-js-pure/features/array/includes.js'; -import find from 'core-js-pure/features/array/find'; -import { config } from '../src/config.js'; -import { BANNER, NATIVE, VIDEO } from '../src/mediaTypes.js'; -import { getStorageManager } from '../src/storageManager.js'; +import {find, includes} from '../src/polyfill.js'; +import {config} from '../src/config.js'; +import {BANNER, NATIVE, VIDEO} from '../src/mediaTypes.js'; +import {getStorageManager} from '../src/storageManager.js'; import { - isPlainObject, deepSetValue, deepAccess, logWarn, inIframe, isNumber, logError, isArray, uniques, - flatten, triggerPixel, isStr, isEmptyStr, generateUUID + deepAccess, + deepSetValue, + flatten, + generateUUID, + inIframe, + isArray, + isEmptyStr, + isNumber, + isPlainObject, + isStr, + logError, + logWarn, + triggerPixel, + uniques } from '../src/utils.js'; -import { registerBidder } from '../src/adapters/bidderFactory.js'; +import {registerBidder} from '../src/adapters/bidderFactory.js'; /** * CONSTANTS @@ -363,7 +374,7 @@ function impBidsToPrebidBids( } const impToPrebidBid = - (bidderRequestBody, bidResponseCurrency, referrer, targetingMap) => (bid) => { + (bidderRequestBody, bidResponseCurrency, referrer, targetingMap) => (bid, bidIndex) => { try { const bidRequest = findBidRequest(bidderRequestBody, bid); @@ -377,7 +388,7 @@ const impToPrebidBid = let prebidBid = { ad: bid.adm, adId: bid.adid, - adserverTargeting: targetingMap[bid.impid], + adserverTargeting: targetingMap[bidIndex], adUnitCode: bidRequest.tagid, bidderRequestId: bidderRequestBody.id, bidId: bid.id, @@ -408,6 +419,9 @@ const impToPrebidBid = }; } + if (deepAccess(bid, 'ext.pa_win') === true) { + prebidBid.auctionWinner = true; + } return prebidBid; } catch (error) { logError(`${BIDDER_CODE}: Error while building bid`, error); @@ -429,29 +443,43 @@ function buildTargetingMap(bids) { const values = impIds.reduce((result, id) => { result[id] = { lineItemIds: [], + orderIds: [], dealIds: [], adIds: [], + adAndOrderIndexes: [] }; return result; }, {}); - bids.forEach((bid) => { - values[bid.impid].lineItemIds.push(bid.ext.liid); - values[bid.impid].dealIds.push(bid.dealid); - values[bid.impid].adIds.push(bid.adid); + bids.forEach((bid, bidIndex) => { + let impId = bid.impid; + values[impId].lineItemIds.push(bid.ext.liid); + values[impId].dealIds.push(bid.dealid); + values[impId].adIds.push(bid.adid); + + if (deepAccess(bid, 'ext.ordid')) { + values[impId].orderIds.push(bid.ext.ordid); + bid.ext.ordid.split(TARGETING_VALUE_SEPARATOR).forEach((ordid, ordIndex) => { + let adIdIndex = values[impId].adIds.indexOf(bid.adid); + values[impId].adAndOrderIndexes.push(adIdIndex + '_' + ordIndex) + }) + } }); const targetingMap = {}; - for (const id of impIds) { - targetingMap[id] = { + bids.forEach((bid, bidIndex) => { + let id = bid.impid; + + targetingMap[bidIndex] = { hb_liid_adbookpsp: values[id].lineItemIds.join(TARGETING_VALUE_SEPARATOR), hb_deal_adbookpsp: values[id].dealIds.join(TARGETING_VALUE_SEPARATOR), + hb_ad_ord_adbookpsp: values[id].adAndOrderIndexes.join(TARGETING_VALUE_SEPARATOR), hb_adid_c_adbookpsp: values[id].adIds.join(TARGETING_VALUE_SEPARATOR), + hb_ordid_adbookpsp: values[id].orderIds.join(TARGETING_VALUE_SEPARATOR), }; - } - + }) return targetingMap; } @@ -560,7 +588,7 @@ function bannerHasSingleSize(bidRequest) { * USER SYNC */ -export const storage = getStorageManager(); +export const storage = getStorageManager({bidderCode: BIDDER_CODE}); function getUserSyncs(syncOptions, responses, gdprConsent, uspConsent) { return responses diff --git a/modules/adgenerationBidAdapter.js b/modules/adgenerationBidAdapter.js index 4dd320d3f24..e0d3a881cad 100644 --- a/modules/adgenerationBidAdapter.js +++ b/modules/adgenerationBidAdapter.js @@ -25,7 +25,7 @@ export const spec = { * @return ServerRequest Info describing the request to the server. */ buildRequests: function (validBidRequests, bidderRequest) { - const ADGENE_PREBID_VERSION = '1.2.0'; + const ADGENE_PREBID_VERSION = '1.3.0'; let serverRequests = []; for (let i = 0, len = validBidRequests.length; i < len; i++) { const validReq = validBidRequests[i]; @@ -50,6 +50,12 @@ export const spec = { data = tryAppendQueryString(data, 'imark', '1'); } data = tryAppendQueryString(data, 'tp', bidderRequest.refererInfo.referer); + if (isIos()) { + const hyperId = getHyperId(validReq); + if (hyperId != null) { + data = tryAppendQueryString(data, 'hyper_id', hyperId); + } + } // remove the trailing "&" if (data.lastIndexOf('&') === data.length - 1) { data = data.substring(0, data.length - 1); @@ -263,4 +269,20 @@ function getCurrencyType() { return 'JPY'; } +/** + * + * @param validReq request + * @return {null|string} + */ +function getHyperId(validReq) { + if (validReq.userId && validReq.userId.novatiq && validReq.userId.novatiq.snowflake.syncResponse === 1) { + return validReq.userId.novatiq.snowflake.id; + } + return null; +} + +function isIos() { + return (/(ios|ipod|ipad|iphone)/i).test(window.navigator.userAgent); +} + registerBidder(spec); diff --git a/modules/adhashBidAdapter.js b/modules/adhashBidAdapter.js index c94a4e35efd..7f5af047993 100644 --- a/modules/adhashBidAdapter.js +++ b/modules/adhashBidAdapter.js @@ -1,12 +1,85 @@ -import { registerBidder } from '../src/adapters/bidderFactory.js'; -import includes from 'core-js-pure/features/array/includes.js'; -import { BANNER } from '../src/mediaTypes.js'; +import {registerBidder} from '../src/adapters/bidderFactory.js'; +import {includes} from '../src/polyfill.js'; +import {BANNER} from '../src/mediaTypes.js'; const VERSION = '1.0'; +const BAD_WORD_STEP = 0.1; +const BAD_WORD_MIN = 0.2; + +/** + * Function that checks the page where the ads are being served for brand safety. + * If unsafe words are found the scoring of that page increases. + * If it becomes greater than the maximum allowed score false is returned. + * The rules may vary based on the website language or the publisher. + * The AdHash bidder will not bid on unsafe pages (according to 4A's). + * @param badWords list of scoring rules to chech against + * @param maxScore maximum allowed score for that bidding + * @returns boolean flag is the page safe + */ +function brandSafety(badWords, maxScore) { + /** + * Performs the ROT13 encoding on the string argument and returns the resulting string. + * The Adhash bidder uses ROT13 so that the response is not blocked by: + * - ad blocking software + * - parental control software + * - corporate firewalls + * due to the bad words contained in the response. + * @param value The input string. + * @returns string Returns the ROT13 version of the given string. + */ + const rot13 = value => { + const input = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'; + const output = 'NOPQRSTUVWXYZABCDEFGHIJKLMnopqrstuvwxyzabcdefghijklm'; + const index = x => input.indexOf(x); + const translate = x => index(x) > -1 ? output[index(x)] : x; + return value.split('').map(translate).join(''); + }; + + /** + * Calculates the scoring for each bad word with dimishing returns + * @param {integer} points points that this word costs + * @param {integer} occurances number of occurances + * @returns {float} final score + */ + const scoreCalculator = (points, occurances) => { + let positive = true; + if (points < 0) { + points *= -1; + positive = false; + } + let result = 0; + for (let i = 0; i < occurances; i++) { + result += Math.max(points - i * BAD_WORD_STEP, BAD_WORD_MIN); + } + return positive ? result : -result; + }; + + // Default parameters if the bidder is unable to send some of them + badWords = badWords || []; + maxScore = parseInt(maxScore) || 10; + + try { + let score = 0; + const content = window.top.document.body.innerText.toLowerCase(); + const words = content.trim().split(/\s+/).length; + for (const [word, rule, points] of badWords) { + if (rule === 'full' && new RegExp('\\b' + rot13(word) + '\\b', 'i').test(content)) { + const occurances = content.match(new RegExp('\\b' + rot13(word) + '\\b', 'g')).length; + score += scoreCalculator(points, occurances); + } else if (rule === 'partial' && content.indexOf(rot13(word.toLowerCase())) > -1) { + const occurances = content.match(new RegExp(rot13(word), 'g')).length; + score += scoreCalculator(points, occurances); + } + } + return score < maxScore * words / 500; + } catch (e) { + return true; + } +} export const spec = { code: 'adhash', - url: 'https://bidder.adhash.org/rtb?version=' + VERSION + '&prebid=true', + url: 'https://bidder.adhash.com/rtb?version=' + VERSION + '&prebid=true', supportedMediaTypes: [ BANNER ], isBidRequestValid: (bid) => { @@ -37,7 +110,7 @@ export const spec = { var size = validBidRequests[i].sizes[index].join('x'); bidRequests.push({ method: 'POST', - url: url, + url: url + '&publisher=' + validBidRequests[i].params.publisherId, bidRequest: validBidRequests[i], data: { timezone: new Date().getTimezoneOffset() / 60, @@ -59,7 +132,8 @@ export const spec = { blockedCreatives: [], currentTimestamp: new Date().getTime(), recentAds: [], - GDPR: gdprConsent + GDPRApplies: gdprConsent ? gdprConsent.gdprApplies : null, + GDPR: gdprConsent ? gdprConsent.consentString : null }, options: { withCredentials: false, @@ -73,7 +147,11 @@ export const spec = { interpretResponse: (serverResponse, request) => { const responseBody = serverResponse ? serverResponse.body : {}; - if (!responseBody.creatives || responseBody.creatives.length === 0) { + if ( + !responseBody.creatives || + responseBody.creatives.length === 0 || + !brandSafety(responseBody.badWords, responseBody.maxScore) + ) { return []; } @@ -87,7 +165,7 @@ export const spec = { cpm: responseBody.creatives[0].costEUR, ad: `
- + `, width: request.bidRequest.sizes[0][0], height: request.bidRequest.sizes[0][1], diff --git a/modules/adkernelAdnAnalyticsAdapter.js b/modules/adkernelAdnAnalyticsAdapter.js index 2b4e67736f3..de5d59ca6f8 100644 --- a/modules/adkernelAdnAnalyticsAdapter.js +++ b/modules/adkernelAdnAnalyticsAdapter.js @@ -10,7 +10,7 @@ const GVLID = 14; const ANALYTICS_VERSION = '1.0.2'; const DEFAULT_QUEUE_TIMEOUT = 4000; const DEFAULT_HOST = 'tag.adkernel.com'; -const storageObj = getStorageManager(GVLID); +const storageObj = getStorageManager({gvlid: GVLID}); const ADK_HB_EVENTS = { AUCTION_INIT: 'auctionInit', diff --git a/modules/adkernelBidAdapter.js b/modules/adkernelBidAdapter.js index c23eca2f96a..c2d6ca4d4dd 100644 --- a/modules/adkernelBidAdapter.js +++ b/modules/adkernelBidAdapter.js @@ -1,11 +1,26 @@ import { - isStr, isArray, isPlainObject, deepSetValue, isNumber, deepAccess, getAdUnitSizes, parseGPTSingleSizeArrayToRtbSize, - cleanObj, contains, getDNT, parseUrl, createTrackPixelHtml, _each, isArrayOfNums, mergeDeep, isEmpty, inIframe + _each, + cleanObj, + contains, + createTrackPixelHtml, + deepAccess, + deepSetValue, + getAdUnitSizes, + getDNT, + inIframe, + isArray, + isArrayOfNums, + isEmpty, + isNumber, + isPlainObject, + isStr, + mergeDeep, + parseGPTSingleSizeArrayToRtbSize, + parseUrl } from '../src/utils.js'; import {BANNER, NATIVE, VIDEO} from '../src/mediaTypes.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; -import find from 'core-js-pure/features/array/find.js'; -import includes from 'core-js-pure/features/array/includes.js'; +import {find, includes} from '../src/polyfill.js'; import {config} from '../src/config.js'; /* @@ -78,7 +93,8 @@ export const spec = { {code: 'catapultx'}, {code: 'ergadx'}, {code: 'turktelekom'}, - {code: 'felixads'} + {code: 'felixads'}, + {code: 'motionspots'} ], supportedMediaTypes: [BANNER, VIDEO, NATIVE], diff --git a/modules/adlooxAnalyticsAdapter.js b/modules/adlooxAnalyticsAdapter.js index 781b8db830a..1091b87a22d 100644 --- a/modules/adlooxAnalyticsAdapter.js +++ b/modules/adlooxAnalyticsAdapter.js @@ -6,14 +6,27 @@ import adapterManager from '../src/adapterManager.js'; import adapter from '../src/AnalyticsAdapter.js'; -import { loadExternalScript } from '../src/adloader.js'; -import { auctionManager } from '../src/auctionManager.js'; -import { AUCTION_COMPLETED } from '../src/auction.js'; +import {loadExternalScript} from '../src/adloader.js'; +import {auctionManager} from '../src/auctionManager.js'; +import {AUCTION_COMPLETED} from '../src/auction.js'; import CONSTANTS from '../src/constants.json'; -import find from 'core-js-pure/features/array/find.js'; +import {find} from '../src/polyfill.js'; +import {getRefererInfo} from '../src/refererDetection.js'; import { - deepAccess, logInfo, isPlainObject, logError, isStr, isNumber, getGptSlotInfoForAdUnitCode, - isFn, mergeDeep, logMessage, insertElement, logWarn, getUniqueIdentifierStr, parseUrl + deepAccess, + getGptSlotInfoForAdUnitCode, + getUniqueIdentifierStr, + insertElement, + isFn, + isNumber, + isPlainObject, + isStr, + logError, + logInfo, + logMessage, + logWarn, + mergeDeep, + parseUrl } from '../src/utils.js'; const MODULE = 'adlooxAnalyticsAdapter'; @@ -46,6 +59,10 @@ MACRO['targetelt'] = function(b, c) { MACRO['creatype'] = function(b, c) { return b.mediaType == 'video' ? ADLOOX_MEDIATYPE.VIDEO : ADLOOX_MEDIATYPE.DISPLAY; }; +MACRO['pageurl'] = function(b, c) { + const refererInfo = getRefererInfo(); + return (refererInfo.canonicalUrl || refererInfo.referer || '').substr(0, 300).split(/[?#]/)[0]; +}; MACRO['pbadslot'] = function(b, c) { const adUnit = find(auctionManager.getAdUnits(), a => b.adUnitCode === a.code); return deepAccess(adUnit, 'ortb2Imp.ext.data.pbadslot') || getGptSlotInfoForAdUnitCode(b.adUnitCode).gptSlot || b.adUnitCode; diff --git a/modules/adlooxAnalyticsAdapter.md b/modules/adlooxAnalyticsAdapter.md index c4618a2e3aa..e21261d0b8d 100644 --- a/modules/adlooxAnalyticsAdapter.md +++ b/modules/adlooxAnalyticsAdapter.md @@ -127,6 +127,7 @@ The following macros are available * `%%pbadslot%%`: [Prebid Ad Slot](https://docs.prebid.org/features/pbAdSlot.html) returns [`AdUnit.code`](https://docs.prebid.org/features/pbAdSlot.html) if set otherwise returns [`AdUnit.code`](https://docs.prebid.org/dev-docs/adunit-reference.html#adunit) * it is recommended you read the [Prebid Ad Slot section in the Adloox RTD Provider documentation](./adlooxRtdProvider.md#prebid-ad-slot) + * `%%pageurl%%`: [`canonicalUrl`](https://docs.prebid.org/dev-docs/publisher-api-reference/setConfig.html#setConfig-Page-URL) from the [`refererInfo` object](https://docs.prebid.org/dev-docs/bidder-adaptor.html#referrers) otherwise uses `referer` ### Functions diff --git a/modules/adlooxRtdProvider.js b/modules/adlooxRtdProvider.js index 34d1428ea1d..bb8334ec8fe 100644 --- a/modules/adlooxRtdProvider.js +++ b/modules/adlooxRtdProvider.js @@ -11,16 +11,29 @@ /* eslint standard/no-callback-literal: "off" */ /* eslint prebid/validate-imports: "off" */ -import { command as analyticsCommand, COMMAND } from './adlooxAnalyticsAdapter.js'; -import { config as _config } from '../src/config.js'; -import { submodule } from '../src/hook.js'; -import { ajax } from '../src/ajax.js'; -import { getGlobal } from '../src/prebidGlobal.js'; +import {command as analyticsCommand, COMMAND} from './adlooxAnalyticsAdapter.js'; +import {config as _config} from '../src/config.js'; +import {submodule} from '../src/hook.js'; +import {ajax} from '../src/ajax.js'; +import {getGlobal} from '../src/prebidGlobal.js'; +import {getRefererInfo} from '../src/refererDetection.js'; import { - getAdUnitSizes, logInfo, isPlainObject, logError, isStr, isInteger, isArray, isBoolean, mergeDeep, deepAccess, - _each, deepSetValue, logWarn, getGptSlotInfoForAdUnitCode + _each, + deepAccess, + deepSetValue, + getAdUnitSizes, + getGptSlotInfoForAdUnitCode, + isArray, + isBoolean, + isInteger, + isPlainObject, + isStr, + logError, + logInfo, + logWarn, + mergeDeep } from '../src/utils.js'; -import includes from 'core-js-pure/features/array/includes.js'; +import {includes} from '../src/polyfill.js'; const MODULE_NAME = 'adloox'; const MODULE = `${MODULE_NAME}RtdProvider`; @@ -285,6 +298,7 @@ function getBidRequestData(reqBidsConfigObj, callback, config, userConsent) { } } + const refererInfo = getRefererInfo(); const args = [ [ 'v', `pbjs-${getGlobal().version}` ], [ 'c', config.params.clientid ], @@ -293,7 +307,7 @@ function getBidRequestData(reqBidsConfigObj, callback, config, userConsent) { [ 'imp', config.params.imps ], [ 'fc_ip', config.params.freqcap_ip ], [ 'fc_ipua', config.params.freqcap_ipua ], - [ 'pn', document.location.pathname ] + [ 'pn', (refererInfo.canonicalUrl || refererInfo.referer || '').substr(0, 300).split(/[?#]/)[0] ] ]; if (!adUnits.length) { diff --git a/modules/admanBidAdapter.js b/modules/admanBidAdapter.js index 666e9aea309..241864c50fc 100644 --- a/modules/admanBidAdapter.js +++ b/modules/admanBidAdapter.js @@ -5,7 +5,7 @@ import {config} from '../src/config.js'; const BIDDER_CODE = 'adman'; const AD_URL = 'https://pub.admanmedia.com/?c=o&m=multi'; -const URL_SYNC = 'https://pub.admanmedia.com'; +const URL_SYNC = 'https://sync.admanmedia.com'; function isBidResponseValid(bid) { if (!bid.requestId || !bid.cpm || !bid.creativeId || diff --git a/modules/admaruBidAdapter.js b/modules/admaruBidAdapter.js new file mode 100644 index 00000000000..65f62c77e26 --- /dev/null +++ b/modules/admaruBidAdapter.js @@ -0,0 +1,81 @@ +import {registerBidder} from '../src/adapters/bidderFactory.js'; +import {BANNER} from '../src/mediaTypes.js'; + +const ADMARU_ENDPOINT = 'https://p1.admaru.net/AdCall'; +const BIDDER_CODE = 'admaru'; + +const DEFAULT_BID_TTL = 360; + +function parseBid(rawBid, currency) { + const bid = {}; + + bid.cpm = rawBid.price; + bid.impid = rawBid.impid; + bid.requestId = rawBid.impid; + bid.netRevenue = true; + bid.dealId = ''; + bid.creativeId = rawBid.crid; + bid.currency = currency; + bid.ad = rawBid.adm; + bid.width = rawBid.w; + bid.height = rawBid.h; + bid.mediaType = BANNER; + bid.ttl = DEFAULT_BID_TTL; + + return bid; +} + +export const spec = { + code: BIDDER_CODE, + supportedMediaTypes: [BANNER], + + isBidRequestValid: function (bid) { + return !!(bid && bid.params && bid.params.pub_id && bid.params.adspace_id); + }, + + buildRequests: function (validBidRequests, bidderRequest) { + return validBidRequests.map(bid => { + const payload = { + pub_id: bid.params.pub_id, + adspace_id: bid.params.adspace_id, + bidderRequestId: bid.bidderRequestId, + bidId: bid.bidId + }; + + return { + method: 'GET', + url: ADMARU_ENDPOINT, + data: payload, + } + }) + }, + + interpretResponse: function (serverResponse, bidRequest) { + const bidResponses = []; + let bid = null; + + if (!serverResponse.hasOwnProperty('body') || !serverResponse.body.hasOwnProperty('seatbid')) { + return bidResponses; + } + + const serverBody = serverResponse.body; + const seatbid = serverBody.seatbid; + + for (let i = 0; i < seatbid.length; i++) { + if (!seatbid[i].hasOwnProperty('bid')) { + continue; + } + + const innerBids = seatbid[i].bid; + for (let j = 0; j < innerBids.length; j++) { + bid = parseBid(innerBids[j], serverBody.cur); + + bidResponses.push(bid); + } + } + + return bidResponses; + } +} + +registerBidder(spec); diff --git a/modules/admaruBidAdapter.md b/modules/admaruBidAdapter.md new file mode 100644 index 00000000000..9985a660ac6 --- /dev/null +++ b/modules/admaruBidAdapter.md @@ -0,0 +1,34 @@ +# Overview + +``` +Module Name: Admaru Bidder Adapter +Module Type: Bidder Adapter +Maintainer: support@admaru.com +``` + +# Description + +Module that connects to Admaru demand sources + +# Test Parameters +``` + var adUnits = [ + { + code: 'test-div', + mediaTypes: { + banner: { + sizes: [[300, 250]], // a display size + } + }, + bids: [ + { + bidder: "admaru", + params: { + pub_id: '1234', // string - required + adspace_id: '1234' // string - required + } + } + ] + } + ]; +``` diff --git a/modules/adnowBidAdapter.js b/modules/adnowBidAdapter.js index badf57ed5c9..472d0fdb2e1 100644 --- a/modules/adnowBidAdapter.js +++ b/modules/adnowBidAdapter.js @@ -1,7 +1,7 @@ -import { registerBidder } from '../src/adapters/bidderFactory.js'; -import { NATIVE, BANNER } from '../src/mediaTypes.js'; -import { parseSizesInput, deepAccess, parseQueryStringParameters } from '../src/utils.js'; -import includes from 'core-js-pure/features/array/includes.js'; +import {registerBidder} from '../src/adapters/bidderFactory.js'; +import {BANNER, NATIVE} from '../src/mediaTypes.js'; +import {deepAccess, parseQueryStringParameters, parseSizesInput} from '../src/utils.js'; +import {includes} from '../src/polyfill.js'; const BIDDER_CODE = 'adnow'; const ENDPOINT = 'https://n.ads3-adnow.com/a'; diff --git a/modules/adnuntiusBidAdapter.js b/modules/adnuntiusBidAdapter.js index f05cd9f9f32..9e05ea664d8 100644 --- a/modules/adnuntiusBidAdapter.js +++ b/modules/adnuntiusBidAdapter.js @@ -27,7 +27,7 @@ const getSegmentsFromOrtb = function (ortb2) { } const handleMeta = function () { - const storage = getStorageManager(GVLID, 'adnuntius') + const storage = getStorageManager({gvlid: GVLID, bidderCode: BIDDER_CODE}) let adnMeta = null if (storage.localStorageIsEnabled()) { adnMeta = JSON.parse(storage.getDataFromLocalStorage('adn.metaData')) diff --git a/modules/adnuntiusRtdProvider.js b/modules/adnuntiusRtdProvider.js new file mode 100644 index 00000000000..9234a30aa33 --- /dev/null +++ b/modules/adnuntiusRtdProvider.js @@ -0,0 +1,96 @@ + +import { submodule } from '../src/hook.js' +import { logError, logInfo } from '../src/utils.js' +import { ajax } from '../src/ajax.js'; + +import { config as sourceConfig } from '../src/config.js'; + +const GVLID = 855; + +function init(config, userConsent) { + if (!config.params || !config.params.providers) return false + logInfo(userConsent) + return true; +} + +// Make sure that ajax has a function as callback +function prepProvider(provider) { + // Map parameter to something that adnuntius endpoint understands. + const mappedParameters = { + siteId: 's', + userId: 'browserId', + browserId: 'browserId', + folderId: 'folderId' + } + + const tzo = new Date().getTimezoneOffset(); + const URL = ['https://data.adnuntius.com/usr?tzo=' + tzo] + Object.keys(provider).forEach(key => { + URL.push(`${mappedParameters[key]}=${provider[key]}`) + }) + + return new Promise((resolve, reject) => { + ajax(URL.join('&'), { + success: function (res) { + const response = JSON.parse(res) + resolve(response) + }, + error: function (err) { reject(err) } + }); + }); +} + +function setGlobalConfig(config, segments) { + const ortbSegments = { + ortb2: { + user: { + data: [{ + name: 'adnuntius', + segment: segments + }] + } + } + } + if (config.params && config.params.bidders) { + sourceConfig.mergeBidderConfig({ + bidders: config.params.bidders, + config: ortbSegments + }) + } else { + sourceConfig.mergeConfig(ortbSegments) + } +} + +function alterBidRequests(reqBidsConfigObj, callback, config, userConsent) { + const gdpr = userConsent && userConsent.gdpr; + let allowedToRun = true + if (gdpr) { + if (userConsent.gdpr.gdprApplies) { + if (gdpr.gdprApplies && !gdpr.vendorData.vendorConsents[GVLID]) allowedToRun = false; + } + } + if (allowedToRun) { + const providerRequests = config.params.providers.map(provider => prepProvider(provider)) + + Promise.allSettled(providerRequests).then((values) => { + const segments = values.reduce((segments, array) => (array.status === 'fulfilled') ? segments.concat(array.value.segments) : [], []).map(segmentId => ({ id: segmentId })) + setGlobalConfig(config, segments) + callback(); + }) + .catch(err => logError('ADN: err', err)); + } else callback(); +} + +/** @type {RtdSubmodule} */ +export const adnuntiusSubmodule = { + name: 'adnuntius', + init: init, + getBidRequestData: alterBidRequests, + setGlobalConfig: setGlobalConfig, +}; + +export function beforeInit() { + submodule('realTimeData', adnuntiusSubmodule); +} + +beforeInit(); diff --git a/modules/adnuntiusRtdProvider.md b/modules/adnuntiusRtdProvider.md new file mode 100644 index 00000000000..e62eba13e2c --- /dev/null +++ b/modules/adnuntiusRtdProvider.md @@ -0,0 +1,41 @@ +### Overview + +The Adnuntius Real Time Data Provider will request segments from adnuntius data, based on what is defined in the realTimeData object. It uses the siteId and userId that a publisher provides. These will have to correspond to a previously uploaded user to Adnuntius Data. + +### Integration + +1. Build the adnuntiusRTD module into the Prebid.js package with: + +``` +gulp build --modules=adnuntiusRtdProvider,... +``` + +2. Use `setConfig` to instruct Prebid.js to initilaize the adnuntiusRtdProvider module, as specified below. + +### Configuration + +``` +var pbjs = pbjs || { que: [] } +pbjs.que.push(function () { + pbjs.setConfig({ + realTimeData: { + auctionDelay: 300, + dataProviders: [ + { + name: 'adnuntius', + waitForIt: true, + params: { + bidders: ['adnuntius'], + providers: [{ + siteId: 'site123', + userId: 'user123' + }] + } + } + ] + }, + }); +}); +``` + +Please reach out to Adnuntius if you need more info about this: prebid@adnuntius.com diff --git a/modules/adomikAnalyticsAdapter.js b/modules/adomikAnalyticsAdapter.js index e81f6e50054..44c0c6868cf 100644 --- a/modules/adomikAnalyticsAdapter.js +++ b/modules/adomikAnalyticsAdapter.js @@ -1,9 +1,8 @@ import adapter from '../src/AnalyticsAdapter.js'; import CONSTANTS from '../src/constants.json'; import adapterManager from '../src/adapterManager.js'; -import { logInfo } from '../src/utils.js'; -import find from 'core-js-pure/features/array/find.js'; -import findIndex from 'core-js-pure/features/array/find-index.js'; +import {logInfo} from '../src/utils.js'; +import {find, findIndex} from '../src/polyfill.js'; // Events used in adomik analytics adapter const auctionInit = CONSTANTS.EVENTS.AUCTION_INIT; diff --git a/modules/adotBidAdapter.js b/modules/adotBidAdapter.js index ddd9531eb43..a28ab4257df 100644 --- a/modules/adotBidAdapter.js +++ b/modules/adotBidAdapter.js @@ -1,313 +1,238 @@ import {Renderer} from '../src/Renderer.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import {BANNER, NATIVE, VIDEO} from '../src/mediaTypes.js'; -import {isStr, isArray, isNumber, isPlainObject, isBoolean, logError, replaceAuctionPrice} from '../src/utils.js'; -import find from 'core-js-pure/features/array/find.js'; -import { config } from '../src/config.js'; +import {isArray, isBoolean, isFn, isPlainObject, isStr, logError, replaceAuctionPrice} from '../src/utils.js'; +import {find} from '../src/polyfill.js'; +import {config} from '../src/config.js'; +import { OUTSTREAM } from '../src/video.js'; -const ADAPTER_VERSION = 'v1.0.0'; +const BIDDER_CODE = 'adot'; +const ADAPTER_VERSION = 'v2.0.0'; const BID_METHOD = 'POST'; const BIDDER_URL = 'https://dsp.adotmob.com/headerbidding{PUBLISHER_PATH}/bidrequest'; -const FIRST_PRICE = 1; -const NET_REVENUE = true; -// eslint-disable-next-line no-template-curly-in-string -const AUCTION_PRICE = '${AUCTION_PRICE}'; -const TTL = 10; - -const SUPPORTED_VIDEO_CONTEXTS = ['instream', 'outstream']; -const SUPPORTED_INSTREAM_CONTEXTS = ['pre-roll', 'mid-roll', 'post-roll']; -const SUPPORTED_VIDEO_MIMES = ['video/mp4']; -const BID_SUPPORTED_MEDIA_TYPES = ['banner', 'video', 'native']; - +const REQUIRED_VIDEO_PARAMS = ['mimes', 'minduration', 'maxduration', 'protocols']; const DOMAIN_REGEX = new RegExp('//([^/]*)'); -const OUTSTREAM_VIDEO_PLAYER_URL = 'https://adserver.adotmob.com/video/player.min.js'; - +const FIRST_PRICE = 1; +const IMP_BUILDER = { banner: buildBanner, video: buildVideo, native: buildNative }; const NATIVE_PLACEMENTS = { - title: {id: 1, name: 'title'}, - icon: {id: 2, type: 1, name: 'img'}, - image: {id: 3, type: 3, name: 'img'}, - sponsoredBy: {id: 4, name: 'data', type: 1}, - body: {id: 5, name: 'data', type: 2}, - cta: {id: 6, type: 12, name: 'data'} + title: { id: 1, name: 'title' }, + icon: { id: 2, type: 1, name: 'img' }, + image: { id: 3, type: 3, name: 'img' }, + sponsoredBy: { id: 4, name: 'data', type: 1 }, + body: { id: 5, name: 'data', type: 2 }, + cta: { id: 6, type: 12, name: 'data' } }; -const NATIVE_ID_MAPPING = {1: 'title', 2: 'icon', 3: 'image', 4: 'sponsoredBy', 5: 'body', 6: 'cta'}; -const NATIVE_PRESET_FORMATTERS = { - image: formatNativePresetImage -} - -function isNone(value) { - return (value === null) || (value === undefined); -} - -function groupBy(values, key) { - const groups = values.reduce((acc, value) => { - const groupId = value[key]; - - if (!acc[groupId]) acc[groupId] = []; - acc[groupId].push(value); - - return acc; - }, {}); - - return Object - .keys(groups) - .map(id => ({id, key, values: groups[id]})); -} - -function validateMediaTypes(mediaTypes, allowedMediaTypes) { - if (!isPlainObject(mediaTypes)) return false; - if (!allowedMediaTypes.some(mediaType => mediaType in mediaTypes)) return false; - - if (isBanner(mediaTypes)) { - if (!validateBanner(mediaTypes.banner)) return false; - } - - if (isVideo(mediaTypes)) { - if (!validateVideo(mediaTypes.video)) return false; +const NATIVE_ID_MAPPING = { 1: 'title', 2: 'icon', 3: 'image', 4: 'sponsoredBy', 5: 'body', 6: 'cta' }; +const OUTSTREAM_VIDEO_PLAYER_URL = 'https://adserver.adotmob.com/video/player.min.js'; +const BID_RESPONSE_NET_REVENUE = true; +const BID_RESPONSE_TTL = 10; +const DEFAULT_CURRENCY = 'USD'; + +/** + * Parse string in plain object + * + * @param {string} data + * @returns {object|null} Parsed object or null + */ +function tryParse(data) { + try { + return JSON.parse(data); + } catch (err) { + logError(err); + return null; } - - return true; -} - -function isBanner(mediaTypes) { - return isPlainObject(mediaTypes) && isPlainObject(mediaTypes.banner); -} - -function isVideo(mediaTypes) { - return isPlainObject(mediaTypes) && 'video' in mediaTypes; -} - -function validateBanner(banner) { - return isPlainObject(banner) && - isArray(banner.sizes) && - (banner.sizes.length > 0) && - banner.sizes.every(validateMediaSizes); } -function validateVideo(video) { - if (!isPlainObject(video)) return false; - if (!isStr(video.context)) return false; - if (SUPPORTED_VIDEO_CONTEXTS.indexOf(video.context) === -1) return false; - - if (!video.playerSize) return true; - if (!isArray(video.playerSize)) return false; - - return video.playerSize.every(validateMediaSizes); -} - -function validateMediaSizes(mediaSize) { - return isArray(mediaSize) && - (mediaSize.length === 2) && - mediaSize.every(size => (isNumber(size) && size >= 0)); -} - -function validateParameters(parameters, adUnit) { - if (isVideo(adUnit.mediaTypes)) { - if (!isPlainObject(parameters)) return false; - if (!isPlainObject(adUnit.mediaTypes.video)) return false; - if (!validateVideoParameters(parameters.video, adUnit)) return false; - } - - return true; +/** + * Extract domain from given url + * + * @param {string} url + * @returns {string|null} Extracted domain + */ +function extractDomainFromURL(url) { + if (!url || !isStr(url)) return null; + const domain = url.match(DOMAIN_REGEX); + if (isArray(domain) && domain.length === 2) return domain[1]; + return null; } -function validateVideoParameters(videoParams, adUnit) { - const video = adUnit.mediaTypes.video; - - if (!video) return false; +/** + * Create and return site OpenRtb object from given bidderRequest + * + * @param {BidderRequest} bidderRequest + * @returns {Site|null} Formatted Site OpenRtb object or null + */ +function getOpenRTBSiteObject(bidderRequest) { + if (!bidderRequest || !bidderRequest.refererInfo) return null; - if (!isArray(video.mimes)) return false; - if (video.mimes.length === 0) return false; - if (!video.mimes.every(isStr)) return false; - - if (video.minDuration && !isNumber(video.minDuration)) return false; - if (video.maxDuration && !isNumber(video.maxDuration)) return false; - - if (!isArray(video.protocols)) return false; - if (video.protocols.length === 0) return false; - if (!video.protocols.every(isNumber)) return false; - - if (isInstream(video)) { - if (!videoParams.instreamContext) return false; - if (SUPPORTED_INSTREAM_CONTEXTS.indexOf(videoParams.instreamContext) === -1) return false; - } - - return true; -} + const domain = extractDomainFromURL(bidderRequest.refererInfo.referer); + const publisherId = config.getConfig('adot.publisherId'); -function validateServerRequest(serverRequest) { - return isPlainObject(serverRequest) && - isPlainObject(serverRequest.data) && - isArray(serverRequest.data.imp) && - isPlainObject(serverRequest._adot_internal) && - isArray(serverRequest._adot_internal.impressions) -} + if (!domain) return null; -function createServerRequestFromAdUnits(adUnits, bidRequestId, adUnitContext) { - const publisherPath = config.getConfig('adot.publisherPath') === undefined ? '' : '/' + config.getConfig('adot.publisherPath'); return { - method: BID_METHOD, - url: BIDDER_URL.replace('{PUBLISHER_PATH}', publisherPath), - data: generateBidRequestsFromAdUnits(adUnits, bidRequestId, adUnitContext), - _adot_internal: generateAdotInternal(adUnits) - } -} - -function generateAdotInternal(adUnits) { - const impressions = adUnits.reduce((acc, adUnit) => { - const {bidId, mediaTypes, adUnitCode, params} = adUnit; - const base = {bidId, adUnitCode, container: params.video && params.video.container}; - - const imps = Object - .keys(mediaTypes) - .reduce((acc, mediaType, index) => { - const data = mediaTypes[mediaType]; - const impressionId = `${bidId}_${index}`; - - if (mediaType !== 'banner') return acc.concat({...base, impressionId}); - - const bannerImps = data.sizes.map((item, i) => ({...base, impressionId: `${impressionId}_${i}`})); - - return acc.concat(bannerImps); - }, []); - - return acc.concat(imps); - }, []); - - return {impressions}; + page: bidderRequest.refererInfo.referer, + domain: domain, + name: domain, + publisher: { + id: publisherId + } + }; } -function generateBidRequestsFromAdUnits(adUnits, bidRequestId, adUnitContext) { +/** + * Create and return Device OpenRtb object + * + * @returns {Device} Formatted Device OpenRtb object or null + */ +function getOpenRTBDeviceObject() { + return { ua: navigator.userAgent, language: navigator.language }; +} + +/** + * Create and return User OpenRtb object + * + * @param {BidderRequest} bidderRequest + * @returns {User|null} Formatted User OpenRtb object or null + */ +function getOpenRTBUserObject(bidderRequest) { + if (!bidderRequest || !bidderRequest.gdprConsent || !isStr(bidderRequest.gdprConsent.consentString)) return null; + return { ext: { consent: bidderRequest.gdprConsent.consentString } }; +} + +/** + * Create and return Regs OpenRtb object + * + * @param {BidderRequest} bidderRequest + * @returns {Regs|null} Formatted Regs OpenRtb object or null + */ +function getOpenRTBRegsObject(bidderRequest) { + if (!bidderRequest || !bidderRequest.gdprConsent || !isBoolean(bidderRequest.gdprConsent.gdprApplies)) return null; + return { ext: { gdpr: bidderRequest.gdprConsent.gdprApplies } }; +} + +/** + * Create and return Ext OpenRtb object + * + * @param {BidderRequest} bidderRequest + * @returns {Ext|null} Formatted Ext OpenRtb object or null + */ +function getOpenRTBExtObject() { return { - id: bidRequestId, - imp: adUnits.reduce(generateImpressionsFromAdUnit, []), - site: generateSiteFromAdUnitContext(adUnitContext), - device: getDeviceInfo(), - user: getUserInfoFromAdUnitContext(adUnitContext), - regs: getRegulationFromAdUnitContext(adUnitContext), - at: FIRST_PRICE, - ext: generateBidRequestExtension() + adot: { adapter_version: ADAPTER_VERSION }, + should_use_gzip: true }; } -function generateImpressionsFromAdUnit(acc, adUnit) { - const {bidId, mediaTypes, params} = adUnit; - const {placementId} = params; - const pmp = {}; - const ext = {placementId}; - - if (placementId) pmp.deals = [{id: placementId}] - - const imps = Object - .keys(mediaTypes) - .reduce((acc, mediaType, index) => { - const data = mediaTypes[mediaType]; - const impId = `${bidId}_${index}`; - - if (mediaType === 'banner') return acc.concat(generateBannerFromAdUnit(impId, data, params)); - if (mediaType === 'video') return acc.concat({id: impId, video: generateVideoFromAdUnit(data, params), pmp, ext}); - if (mediaType === 'native') return acc.concat({id: impId, native: generateNativeFromAdUnit(data), pmp, ext}); - }, []); - - return acc.concat(imps); -} - -function isImpressionAVideo(impression) { - return isPlainObject(impression) && isPlainObject(impression.video); +/** + * Return MediaType from MediaTypes object + * + * @param {MediaType} mediaTypes Prebid MediaTypes + * @returns {string|null} Mediatype or null if not found + */ +function getMediaType(mediaTypes) { + if (mediaTypes.banner) return 'banner'; + if (mediaTypes.video) return 'video'; + if (mediaTypes.native) return 'native'; + return null; } -function generateBannerFromAdUnit(impId, data, params) { - const {position, placementId} = params; - const pos = position || 0; - const pmp = {}; - const ext = {placementId}; - - if (placementId) pmp.deals = [{id: placementId}] +/** + * Build OpenRtb imp banner from given bidderRequest and media + * + * @param {Banner} banner MediaType Banner Object + * @param {BidderRequest} bidderRequest + * @returns {OpenRtbBanner} OpenRtb banner object + */ +function buildBanner(banner, bidderRequest) { + const pos = bidderRequest.position || 0; + const format = (banner.sizes || []).map(([w, h]) => ({ w, h })); + return { format, pos }; +} + +/** + * Build object with w and h value depending on given video media + * + * @param {Video} video MediaType Video Object + * @returns {Object} Size as { w: number; h: number } + */ +function getVideoSize(video) { + const sizes = video.playerSize || []; + const format = sizes.length > 0 ? sizes[0] : []; - return data.sizes.map(([w, h], index) => ({id: `${impId}_${index}`, banner: {format: [{w, h}], w, h, pos}, pmp, ext})); + return { + w: format[0] || null, + h: format[1] || null + }; } -function generateVideoFromAdUnit(data, params) { - const {playerSize} = data; - const video = data - - const hasPlayerSize = isArray(playerSize) && playerSize.length > 0; - const {minDuration, maxDuration, protocols} = video; - - const size = {width: hasPlayerSize ? playerSize[0][0] : null, height: hasPlayerSize ? playerSize[0][1] : null}; - const duration = {min: isNumber(minDuration) ? minDuration : null, max: isNumber(maxDuration) ? maxDuration : null}; - const startdelay = computeStartDelay(data, params); +/** + * Build OpenRtb imp video from given bidderRequest and media + * + * @param {Video} video MediaType Video Object + * @returns {OpenRtbVideo} OpenRtb video object + */ +function buildVideo(video) { + const { w, h } = getVideoSize(video); return { - mimes: SUPPORTED_VIDEO_MIMES, - skip: video.skippable || 0, - w: size.width, - h: size.height, - startdelay: startdelay, + api: video.api, + w, + h, linearity: video.linearity || null, - minduration: duration.min, - maxduration: duration.max, - protocols, - api: getApi(protocols), - format: hasPlayerSize ? playerSize.map(s => { - return {w: s[0], h: s[1]}; - }) : null, - pos: video.position || 0 + mimes: video.mimes, + minduration: video.minduration, + maxduration: video.maxduration, + placement: video.placement, + playbackmethod: video.playbackmethod, + pos: video.position || 0, + protocols: video.protocols, + skip: video.skip || 0, + startdelay: video.startdelay }; } -function getApi(protocols) { - let defaultValue = [2]; - let listProtocols = [ - {key: 'VPAID_1_0', value: 1}, - {key: 'VPAID_2_0', value: 2}, - {key: 'MRAID_1', value: 3}, - {key: 'ORMMA', value: 4}, - {key: 'MRAID_2', value: 5}, - {key: 'MRAID_3', value: 6}, - ]; - if (protocols) { - return listProtocols.filter(p => { - return protocols.indexOf(p.key) !== -1; - }).map(p => p.value) - } else { - return defaultValue; - } -} - -function isInstream(video) { - return isPlainObject(video) && (video.context === 'instream'); -} +/** + * Check if given Native Media is an asset of type Image. + * + * Return default native assets if given media is an asset + * Return given native assets if given media is not an asset + * + * @param {NativeMedia} native Native Mediatype + * @returns {OpenRtbNativeAssets} + */ +function cleanNativeMedia(native) { + if (native.type !== 'image') return native; -function isOutstream(video) { - return isPlainObject(video) && (video.startdelay === null) + return { + image: { required: true, sizes: native.sizes }, + title: { required: true }, + sponsoredBy: { required: true }, + body: { required: false }, + cta: { required: false }, + icon: { required: false } + }; } -function computeStartDelay(data, params) { - if (isInstream(data)) { - if (params.video.instreamContext === 'pre-roll') return 0; - if (params.video.instreamContext === 'mid-roll') return -1; - if (params.video.instreamContext === 'post-roll') return -2; - } - - return null; -} +/** + * Build Native OpenRtb Imp from Native Mediatype + * + * @param {NativeMedia} native Native Mediatype + * @returns {OpenRtbNative} + */ +function buildNative(native) { + native = cleanNativeMedia(native); -function generateNativeFromAdUnit(data) { - const {type} = data; - const presetFormatter = type && NATIVE_PRESET_FORMATTERS[data.type]; - const nativeFields = presetFormatter ? presetFormatter(data) : data; + const assets = Object.keys(native) + .reduce((nativeAssets, assetKey) => { + const asset = native[assetKey]; + const assetInfo = NATIVE_PLACEMENTS[assetKey]; - const assets = Object - .keys(nativeFields) - .reduce((acc, placement) => { - const placementData = nativeFields[placement]; - const assetInfo = NATIVE_PLACEMENTS[placement]; + if (!assetInfo) return nativeAssets; - if (!assetInfo) return acc; + const { id, name, type } = assetInfo; + const { required, len, sizes = [] } = asset; - const {id, name, type} = assetInfo; - const {required, len, sizes = []} = placementData; let wmin; let hmin; @@ -319,249 +244,165 @@ function generateNativeFromAdUnit(data) { hmin = sizes[1]; } - const content = {}; + const newAsset = {}; - if (type) content.type = type; - if (len) content.len = len; - if (wmin) content.wmin = wmin; - if (hmin) content.hmin = hmin; + if (type) newAsset.type = type; + if (len) newAsset.len = len; + if (wmin) newAsset.wmin = wmin; + if (hmin) newAsset.hmin = hmin; - acc.push({id, required, [name]: content}); + nativeAssets.push({ id, required, [name]: newAsset }); - return acc; + return nativeAssets; }, []); - return { - request: JSON.stringify({assets}) - }; + return { request: JSON.stringify({ assets }) }; } -function formatNativePresetImage(data) { - const sizes = data.sizes; +/** + * Build OpenRtb Imp object from given Adunit and Context + * + * @param {AdUnit} adUnit PrebidJS Adunit + * @param {BidderRequest} bidderRequest PrebidJS Bidder Request + * @returns {Imp} OpenRtb Impression + */ +function buildImpFromAdUnit(adUnit, bidderRequest) { + const { bidId, mediaTypes, params, adUnitCode } = adUnit; + const mediaType = getMediaType(mediaTypes); - return { - image: { - required: true, - sizes - }, - title: { - required: true - }, - sponsoredBy: { - required: true - }, - body: { - required: false - }, - cta: { - required: false - }, - icon: { - required: false - } - }; -} + if (!mediaType) return null; -function generateSiteFromAdUnitContext(adUnitContext) { - if (!adUnitContext || !adUnitContext.refererInfo) return null; - - const domain = extractSiteDomainFromURL(adUnitContext.refererInfo.referer); - const publisherId = config.getConfig('adot.publisherId'); - - if (!domain) return null; - - return { - page: adUnitContext.refererInfo.referer, - domain: domain, - name: domain, - publisher: { - id: publisherId - } - }; -} - -function extractSiteDomainFromURL(url) { - if (!url || !isStr(url)) return null; - - const domain = url.match(DOMAIN_REGEX); - - if (isArray(domain) && domain.length === 2) return domain[1]; - - return null; -} - -function getDeviceInfo() { - return {ua: navigator.userAgent, language: navigator.language}; -} - -function getUserInfoFromAdUnitContext(adUnitContext) { - if (!adUnitContext || !adUnitContext.gdprConsent) return null; - if (!isStr(adUnitContext.gdprConsent.consentString)) return null; - - return { - ext: { - consent: adUnitContext.gdprConsent.consentString - } - }; -} - -function getRegulationFromAdUnitContext(adUnitContext) { - if (!adUnitContext || !adUnitContext.gdprConsent) return null; - if (!isBoolean(adUnitContext.gdprConsent.gdprApplies)) return null; + const media = IMP_BUILDER[mediaType](mediaTypes[mediaType], bidderRequest, adUnit) + const currency = config.getConfig('currency.adServerCurrency') || DEFAULT_CURRENCY; + const bidfloor = getMainFloor(adUnit, media.format, mediaType, currency); return { + id: bidId, ext: { - gdpr: adUnitContext.gdprConsent.gdprApplies - } - }; -} - -function generateBidRequestExtension() { - return { - adot: {adapter_version: ADAPTER_VERSION}, - should_use_gzip: true + placementId: params.placementId, + adUnitCode, + container: params.video && params.video.container + }, + [mediaType]: media, + bidfloorcur: currency, + bidfloor }; } -function validateServerResponse(serverResponse) { - return isPlainObject(serverResponse) && - isPlainObject(serverResponse.body) && - isStr(serverResponse.body.cur) && - isArray(serverResponse.body.seatbid); -} - -function seatBidsToAds(seatBid, bidResponse, serverRequest) { - return seatBid.bid - .filter(bid => validateBids(bid, serverRequest)) - .map(bid => generateAdFromBid(bid, bidResponse, serverRequest)); -} - -function validateBids(bid, serverRequest) { - if (!isPlainObject(bid)) return false; - if (!isStr(bid.impid)) return false; - if (!isStr(bid.crid)) return false; - if (!isNumber(bid.price)) return false; - - if (!isPlainObject(bid.ext)) return false; - if (!isPlainObject(bid.ext.adot)) return false; - if (!isStr(bid.ext.adot.media_type)) return false; - if (BID_SUPPORTED_MEDIA_TYPES.indexOf(bid.ext.adot.media_type) === -1) return false; - - if (!bid.adm && !bid.nurl) return false; - if (bid.adm) { - if (!isStr(bid.adm)) return false; - if (bid.adm.indexOf(AUCTION_PRICE) === -1) return false; - } - if (bid.nurl) { - if (!isStr(bid.nurl)) return false; - if (bid.nurl.indexOf(AUCTION_PRICE) === -1) return false; - } - - if (isBidABanner(bid)) { - if (!isNumber(bid.h)) return false; - if (!isNumber(bid.w)) return false; - } - if (isBidAVideo(bid)) { - if (!(isNone(bid.h) || isNumber(bid.h))) return false; - if (!(isNone(bid.w) || isNumber(bid.w))) return false; - } - - const impression = getImpressionData(serverRequest, bid.impid); - - if (!isPlainObject(impression.openRTB)) return false; - if (!isPlainObject(impression.internal)) return false; - if (!isStr(impression.internal.adUnitCode)) return false; - - if (isBidABanner(bid)) { - if (!isPlainObject(impression.openRTB.banner)) return false; - } - if (isBidAVideo(bid)) { - if (!isPlainObject(impression.openRTB.video)) return false; - } - if (isBidANative(bid)) { - if (!isPlainObject(impression.openRTB.native) || !tryParse(bid.adm)) return false; - } - +/** + * Return if given video is Valid. + * A video is defined as valid if it contains all required fields + * + * @param {VideoMedia} video + * @returns {boolean} + */ +function isValidVideo(video) { + if (REQUIRED_VIDEO_PARAMS.some((param) => video[param] === undefined)) return false; return true; } -function isBidABanner(bid) { - return isPlainObject(bid) && - isPlainObject(bid.ext) && - isPlainObject(bid.ext.adot) && - bid.ext.adot.media_type === 'banner'; -} - -function isBidAVideo(bid) { - return isPlainObject(bid) && - isPlainObject(bid.ext) && - isPlainObject(bid.ext.adot) && - bid.ext.adot.media_type === 'video'; -} - -function isBidANative(bid) { - return isPlainObject(bid) && - isPlainObject(bid.ext) && - isPlainObject(bid.ext.adot) && - bid.ext.adot.media_type === 'native'; -} - -function getImpressionData(serverRequest, impressionId) { - const openRTBImpression = find(serverRequest.data.imp, imp => imp.id === impressionId); - const internalImpression = find(serverRequest._adot_internal.impressions, imp => imp.impressionId === impressionId); - +/** + * Return if given bid is Valid. + * A bid is defined as valid if it media is a valid video or other media + * + * @param {Bid} bid + * @returns {boolean} + */ +function isBidRequestValid(bid) { + const video = bid.mediaTypes.video; + return !video || isValidVideo(video); +} + +/** + * Build OpenRtb request from Prebid AdUnits and Bidder request + * + * @param {Array} adUnits Array of PrebidJS Adunit + * @param {BidderRequest} bidderRequest PrebidJS BidderRequest + * @param {string} requestId Request ID + * + * @returns {OpenRTBBidRequest} OpenRTB bid request + */ +function buildBidRequest(adUnits, bidderRequest, requestId) { + const data = { + id: requestId, + imp: adUnits.map((adUnit) => buildImpFromAdUnit(adUnit, bidderRequest)).filter((item) => !!item), + site: getOpenRTBSiteObject(bidderRequest), + device: getOpenRTBDeviceObject(), + user: getOpenRTBUserObject(bidderRequest), + regs: getOpenRTBRegsObject(bidderRequest), + ext: getOpenRTBExtObject(), + at: FIRST_PRICE + }; + return data; +} + +/** + * Build PrebidJS Ajax request + * + * @param {Array} adUnits Array of PrebidJS Adunit + * @param {BidderRequest} bidderRequest PrebidJS BidderRequest + * @param {string} bidderUrl Adot Bidder URL + * @param {string} requestId Request ID + * @returns + */ +function buildAjaxRequest(adUnits, bidderRequest, bidderUrl, requestId) { return { - id: impressionId, - openRTB: openRTBImpression || null, - internal: internalImpression || null + method: BID_METHOD, + url: bidderUrl, + data: buildBidRequest(adUnits, bidderRequest, requestId) }; } -function generateAdFromBid(bid, bidResponse, serverRequest) { - const impressionData = getImpressionData(serverRequest, bid.impid); - const isVideo = isBidAVideo(bid); - const base = { - requestId: impressionData.internal.bidId, - cpm: bid.price, - currency: bidResponse.cur, - ttl: TTL, - creativeId: bid.crid, - netRevenue: NET_REVENUE, - mediaType: bid.ext.adot.media_type, - }; - - if (bid.adomain) { - base.meta = { advertiserDomains: bid.adomain }; - } - - if (isBidANative(bid)) return {...base, native: formatNativeData(bid)}; - - const size = getSizeFromBid(bid, impressionData); - const creative = getCreativeFromBid(bid, impressionData); - - return { - ...base, - height: size.height, - width: size.width, - ad: creative.markup, - adUrl: creative.markupUrl, - vastXml: isVideo && !isStr(creative.markupUrl) ? creative.markup : null, - vastUrl: isVideo && isStr(creative.markupUrl) ? creative.markupUrl : null, - renderer: creative.renderer - }; +/** + * Split given PrebidJS Request in Dictionnary + * + * @param {Array} validBidRequests + * @returns {Dictionnary} + */ +function splitAdUnits(validBidRequests) { + return validBidRequests.reduce((adUnits, adUnit) => { + const bidderRequestId = adUnit.bidderRequestId; + if (!adUnits[bidderRequestId]) { + adUnits[bidderRequestId] = []; + } + adUnits[bidderRequestId].push(adUnit); + return adUnits; + }, {}); } -function formatNativeData({adm, price}) { +/** + * Build Ajax request Array + * + * @param {Array} validBidRequests + * @param {BidderRequest} bidderRequest + * @returns {Array} + */ +function buildRequests(validBidRequests, bidderRequest) { + const adUnits = splitAdUnits(validBidRequests); + const publisherPathConfig = config.getConfig('adot.publisherPath'); + const publisherPath = publisherPathConfig === undefined ? '' : '/' + publisherPathConfig; + const bidderUrl = BIDDER_URL.replace('{PUBLISHER_PATH}', publisherPath); + + return Object.keys(adUnits).map((requestId) => buildAjaxRequest(adUnits[requestId], bidderRequest, bidderUrl, requestId)); +} + +/** + * Build Native PrebidJS Response grom OpenRtb Response + * + * @param {OpenRtbBid} bid + * + * @returns {NativeAssets} Native PrebidJS + */ +function buildNativeBidData(bid) { + const { adm, price } = bid; const parsedAdm = tryParse(adm); - const {assets, link: {url, clicktrackers}, imptrackers, jstracker} = parsedAdm.native; - const placements = NATIVE_PLACEMENTS; - const placementIds = NATIVE_ID_MAPPING; + const { assets, link: { url, clicktrackers }, imptrackers, jstracker } = parsedAdm.native; return assets.reduce((acc, asset) => { - const placementName = placementIds[asset.id]; - const content = placementName && asset[placements[placementName].name]; + const placementName = NATIVE_ID_MAPPING[asset.id]; + const content = placementName && asset[NATIVE_PLACEMENTS[placementName].name]; if (!content) return acc; - acc[placementName] = content.text || content.value || {url: content.url, width: content.w, height: content.h}; + acc[placementName] = content.text || content.value || { url: content.url, width: content.w, height: content.h }; return acc; }, { clickUrl: url, @@ -571,57 +412,38 @@ function formatNativeData({adm, price}) { }); } -function getSizeFromBid(bid, impressionData) { - if (isNumber(bid.w) && isNumber(bid.h)) { - return { width: bid.w, height: bid.h }; - } - - if (isImpressionAVideo(impressionData.openRTB)) { - const { video } = impressionData.openRTB; - - if (isNumber(video.w) && isNumber(video.h)) { - return { width: video.w, height: video.h }; - } - } - - return { width: null, height: null }; -} - -function getCreativeFromBid(bid, impressionData) { - const shouldUseAdMarkup = !!bid.adm; - const price = bid.price; +/** + * Return Adot Renderer if given Bid is a video one + * + * @param {OpenRtbBid} bid + * @param {string} mediaType + * @returns {any|null} + */ +function buildRenderer(bid, mediaType) { + if (!(mediaType === VIDEO && + bid.ext && + bid.ext.adot && + bid.ext.adot.container && + bid.ext.adot.adUnitCode && + bid.ext.adot.video && + bid.ext.adot.video.type === OUTSTREAM)) return null; + + const container = bid.ext.adot.container + const adUnitCode = bid.ext.adot.adUnitCode - return { - markup: shouldUseAdMarkup ? replaceAuctionPrice(bid.adm, price) : null, - markupUrl: !shouldUseAdMarkup ? replaceAuctionPrice(bid.nurl, price) : null, - renderer: getRendererFromBid(bid, impressionData) - }; -} - -function getRendererFromBid(bid, impressionData) { - const isOutstreamImpression = isBidAVideo(bid) && - isImpressionAVideo(impressionData.openRTB) && - isOutstream(impressionData.openRTB.video); - - return isOutstreamImpression - ? buildOutstreamRenderer(impressionData) - : null; -} - -function buildOutstreamRenderer(impressionData) { const renderer = Renderer.install({ url: OUTSTREAM_VIDEO_PLAYER_URL, loaded: false, - adUnitCode: impressionData.internal.adUnitCode + adUnitCode: adUnitCode }); renderer.setRender((ad) => { ad.renderer.push(() => { - const container = impressionData.internal.container - ? document.querySelector(impressionData.internal.container) - : document.getElementById(impressionData.internal.adUnitCode); + const domContainer = container + ? document.querySelector(container) + : document.getElementById(adUnitCode); - const player = new window.VASTPlayer(container); + const player = new window.VASTPlayer(domContainer); player.on('ready', () => { player.adVolume = 0; @@ -641,54 +463,181 @@ function buildOutstreamRenderer(impressionData) { return renderer; } -function tryParse(data) { - try { - return JSON.parse(data); - } catch (err) { - logError(err); - return null; - } +/** + * Build PrebidJS response from OpenRtbBid + * + * @param {OpenRtbBid} bid + * @param {string} mediaType + * @returns {Object} + */ +function buildCreativeBidData(bid, mediaType) { + const adm = bid.adm ? replaceAuctionPrice(bid.adm, bid.price) : null; + const nurl = (!bid.adm && bid.nurl) ? replaceAuctionPrice(bid.nurl, bid.price) : null; + + return { + width: bid.ext.adot.size && bid.ext.adot.size.w, + height: bid.ext.adot.size && bid.ext.adot.size.h, + ad: adm, + adUrl: nurl, + vastXml: mediaType === VIDEO && !isStr(nurl) ? adm : null, + vastUrl: mediaType === VIDEO && isStr(nurl) ? nurl : null, + renderer: buildRenderer(bid, mediaType) + }; } -const adotBidderSpec = { - code: 'adot', - supportedMediaTypes: [BANNER, VIDEO, NATIVE], - isBidRequestValid(adUnit) { - const allowedBidderCodes = [this.code]; - - return isPlainObject(adUnit) && - allowedBidderCodes.indexOf(adUnit.bidder) !== -1 && - isStr(adUnit.adUnitCode) && - isStr(adUnit.bidderRequestId) && - isStr(adUnit.bidId) && - validateMediaTypes(adUnit.mediaTypes, this.supportedMediaTypes) && - validateParameters(adUnit.params, adUnit); - }, - buildRequests(adUnits, adUnitContext) { - if (!adUnits) return null; - - return groupBy(adUnits, 'bidderRequestId').map(group => { - const bidRequestId = group.id; - const adUnits = groupBy(group.values, 'bidId').map((group) => { - const length = group.values.length; - return length > 0 && group.values[length - 1] - }); +/** + * Return if given bid and imp are valid + * + * @param {OpenRtbBid} bid OpenRtb Bid + * @param {Imp} imp OpenRtb Imp + * @returns {boolean} + */ +function isBidImpInvalid(bid, imp) { + return !bid || !imp; +} + +/** + * Build PrebidJS Bid Response from given OpenRTB Bid + * + * @param {OpenRtbBid} bid + * @param {OpenRtbBidResponse} bidResponse + * @param {OpenRtbBid} imp + * @returns {PrebidJSResponse} + */ +function buildBidResponse(bid, bidResponse, imp) { + if (isBidImpInvalid(bid, imp)) return null; + const mediaType = bid.ext.adot.media_type; + const baseBid = { + requestId: bid.impid, + cpm: bid.price, + currency: bidResponse.cur, + ttl: BID_RESPONSE_TTL, + creativeId: bid.crid, + netRevenue: BID_RESPONSE_NET_REVENUE, + mediaType + }; - return createServerRequestFromAdUnits(adUnits, bidRequestId, adUnitContext) + if (bid.dealid) baseBid.dealId = bid.dealid; + if (bid.adomain) baseBid.meta = { advertiserDomains: bid.adomain }; + + if (mediaType === NATIVE) return { ...baseBid, native: buildNativeBidData(bid) }; + return { ...baseBid, ...buildCreativeBidData(bid, mediaType) }; +} + +/** + * Find OpenRtb Imp from request with same id that given bid + * + * @param {OpenRtbBid} bid + * @param {OpenRtbRequest} bidRequest + * @returns {Imp} OpenRtb Imp + */ +function getImpfromBid(bid, bidRequest) { + if (!bidRequest || !bidRequest.imp) return null; + const imps = bidRequest.imp; + return find(imps, (imp) => imp.id === bid.impid); +} + +/** + * Return if given response is valid + * + * @param {OpenRtbBidResponse} response + * @returns {boolean} + */ +function isValidResponse(response) { + return isPlainObject(response) && + isPlainObject(response.body) && + isStr(response.body.cur) && + isArray(response.body.seatbid); +} + +/** + * Return if given request is valid + * + * @param {OpenRtbRequest} request + * @returns {boolean} + */ +function isValidRequest(request) { + return isPlainObject(request) && + isPlainObject(request.data) && + isArray(request.data.imp); +} + +/** + * Interpret given OpenRtb Response to build PrebidJS Response + * + * @param {OpenRtbBidResponse} serverResponse + * @param {OpenRtbRequest} request + * @returns {PrebidJSResponse} + */ +function interpretResponse(serverResponse, request) { + if (!isValidResponse(serverResponse) || !isValidRequest(request)) return []; + + const bidsResponse = serverResponse.body; + const bidRequest = request.data; + + return bidsResponse.seatbid.reduce((pbsResponse, seatbid) => { + if (!seatbid || !isArray(seatbid.bid)) return pbsResponse; + seatbid.bid.forEach((bid) => { + const imp = getImpfromBid(bid, bidRequest); + const bidResponse = buildBidResponse(bid, bidsResponse, imp); + if (bidResponse) pbsResponse.push(bidResponse); }); - }, - interpretResponse(serverResponse, serverRequest) { - if (!validateServerRequest(serverRequest)) return []; - if (!validateServerResponse(serverResponse)) return []; - - const bidResponse = serverResponse.body; + return pbsResponse; + }, []); +} - return bidResponse.seatbid - .filter(seatBid => isPlainObject(seatBid) && isArray(seatBid.bid)) - .reduce((acc, seatBid) => acc.concat(seatBidsToAds(seatBid, bidResponse, serverRequest)), []); - } +/** + * Call Adunit getFloor function with given argument to get specific floor. + * Return 0 by default + * + * @param {AdUnit} adUnit + * @param {Array|string} size Adunit size or * + * @param {string} mediaType + * @param {string} currency USD by default + * + * @returns {number} Floor price + */ +function getFloor(adUnit, size, mediaType, currency) { + if (!isFn(adUnit.getFloor)) return 0; + + const floorResult = adUnit.getFloor({ currency, mediaType, size }); + + return floorResult.currency === currency ? floorResult.floor : 0; +} + +/** + * Call getFloor for each format and return the lower floor + * Return 0 by default + * + * interface Format { w: number; h: number } + * + * @param {AdUnit} adUnit + * @param {Array} formats Media formats + * @param {string} mediaType + * @param {string} currency USD by default + * + * @returns {number} Lower floor. + */ +function getMainFloor(adUnit, formats, mediaType, currency) { + if (!formats) return getFloor(adUnit, '*', mediaType, currency); + + return formats.reduce((bidFloor, format) => { + const floor = getFloor(adUnit, [format.w, format.h], mediaType, currency) + const maxFloor = bidFloor || Number.MAX_SAFE_INTEGER; + return floor !== 0 && floor < maxFloor ? floor : bidFloor; + }, null) || 0; +} + +/** + * Adot PrebidJS Adapter + */ +export const spec = { + code: BIDDER_CODE, + supportedMediaTypes: [BANNER, NATIVE, VIDEO], + isBidRequestValid, + buildRequests, + interpretResponse, + getFloor }; -registerBidder(adotBidderSpec); - -export {adotBidderSpec as spec}; +registerBidder(spec); diff --git a/modules/adotBidAdapter.md b/modules/adotBidAdapter.md index 894a592ec18..d1622e5f901 100644 --- a/modules/adotBidAdapter.md +++ b/modules/adotBidAdapter.md @@ -6,7 +6,7 @@ Adot Bidder Adapter is a module that enables the communication between the Prebi - Module name: Adot Bidder Adapter - Module type: Bidder Adapter -- Maintainer: `aurelien.giudici@adotmob.com` +- Maintainer: `alexandre.lorin@adotmob.com` - Supported media types: `banner`, `video`, `native` ## Example ad units @@ -34,9 +34,9 @@ const adUnit = { ### Video ad unit -#### Outstream video ad unit +#### Video ad unit -Adot Bidder Adapter accepts outstream video ad units using the following ad unit format: +Adot Bidder Adapter accepts video ad units using the following ad unit format: ```javascript const adUnit = { @@ -51,9 +51,9 @@ const adUnit = { // Content MIME types supported by the ad unit. mimes: ['video/mp4'], // Minimum accepted video ad duration (in seconds). - minDuration: 5, + minduration: 5, // Maximum accepted video ad duration (in seconds). - maxDuration: 35, + maxduration: 35, // Video protocols supported by the ad unit (see the OpenRTB 2.5 specifications, // section 5.8). protocols: [2, 3] @@ -61,45 +61,7 @@ const adUnit = { }, bids: [{ bidder: 'adot', - params: { - video: {} - } - }] -} -``` - -#### Instream video ad unit - -Adot Bidder Adapter accepts instream video ad units using the following ad unit format: - -```javascript -const adUnit = { - code: 'test-div', - mediaTypes: { - video: { - // Video context. Must be 'instream'. - context: 'instream', - // Video dimensions supported by the video ad unit. - // Each ad unit size is formatted as follows: [width, height]. - playerSize: [[300, 250]], - // Content MIME types supported by the ad unit. - mimes: ['video/mp4'], - // Minimum accepted video ad duration (in seconds). - minDuration: 5, - // Maximum accepted video ad duration (in seconds). - maxDuration: 35, - // Video protocols supported by the ad unit (see the OpenRTB 2.5 specifications, - // section 5.8). - protocols: [2, 3] - } - }, - bids: [{ - bidder: 'adot', - params: { - video: { - instreamContext: 'pre-roll' - } - } + params: {} }] } ``` diff --git a/modules/adplusBidAdapter.js b/modules/adplusBidAdapter.js index c001781a792..4707ca2ff5a 100644 --- a/modules/adplusBidAdapter.js +++ b/modules/adplusBidAdapter.js @@ -8,7 +8,7 @@ export const BIDDER_CODE = 'adplus'; export const ADPLUS_ENDPOINT = 'https://ssp.ad-plus.com.tr/server/headerBidding'; export const DGID_CODE = 'adplus_dg_id'; export const SESSION_CODE = 'adplus_s_id'; -export const storage = getStorageManager(undefined, BIDDER_CODE); +export const storage = getStorageManager({bidderCode: BIDDER_CODE}); const COOKIE_EXP = 1000 * 60 * 60 * 24; // 1 day // #endregion diff --git a/modules/adpod.js b/modules/adpod.js index ddceed1c344..b7c459fd66f 100644 --- a/modules/adpod.js +++ b/modules/adpod.js @@ -13,23 +13,36 @@ */ import { - generateUUID, deepAccess, logWarn, logInfo, isArrayOfNums, isArray, isNumber, logError, groupBy, compareOn, - isPlainObject + compareOn, + deepAccess, + generateUUID, + groupBy, + isArray, + isArrayOfNums, + isNumber, + isPlainObject, + logError, + logInfo, + logWarn } from '../src/utils.js'; -import { addBidToAuction, doCallbacksIfTimedout, AUCTION_IN_PROGRESS, callPrebidCache, getPriceByGranularity, getPriceGranularity } from '../src/auction.js'; -import { checkAdUnitSetup } from '../src/prebid.js'; -import { checkVideoBidSetup } from '../src/video.js'; -import { setupBeforeHookFnOnce, module } from '../src/hook.js'; -import { store } from '../src/videoCache.js'; -import { config } from '../src/config.js'; -import { ADPOD } from '../src/mediaTypes.js'; -import Set from 'core-js-pure/features/set'; -import find from 'core-js-pure/features/array/find.js'; -import { auctionManager } from '../src/auctionManager.js'; +import { + addBidToAuction, + AUCTION_IN_PROGRESS, + callPrebidCache, + doCallbacksIfTimedout, + getPriceByGranularity, + getPriceGranularity +} from '../src/auction.js'; +import {checkAdUnitSetup} from '../src/prebid.js'; +import {checkVideoBidSetup} from '../src/video.js'; +import {module, setupBeforeHookFnOnce} from '../src/hook.js'; +import {store} from '../src/videoCache.js'; +import {config} from '../src/config.js'; +import {ADPOD} from '../src/mediaTypes.js'; +import {find, arrayFrom as from} from '../src/polyfill.js'; +import {auctionManager} from '../src/auctionManager.js'; import CONSTANTS from '../src/constants.json'; -const from = require('core-js-pure/features/array/from.js'); - const TARGETING_KEY_PB_CAT_DUR = 'hb_pb_cat_dur'; const TARGETING_KEY_CACHE_ID = 'hb_cache_id'; @@ -122,7 +135,7 @@ function getPricePartForAdpodKey(bid) { const adpodDealPrefix = config.getConfig(`adpod.dealTier.${bid.bidderCode}.prefix`); pricePart = (adpodDealPrefix) ? adpodDealPrefix + deepAccess(bid, 'video.dealTier') : deepAccess(bid, 'video.dealTier'); } else { - const granularity = getPriceGranularity(bid.mediaType); + const granularity = getPriceGranularity(bid); pricePart = getPriceByGranularity(granularity)(bid); } return pricePart @@ -223,10 +236,9 @@ function firePrebidCacheCall(auctionInstance, bidList, afterBidAdded) { * @param {*} auctionInstance running context of the auction * @param {Object} bidResponse incoming bid; if adpod, will be processed through hook function. If not adpod, returns to original function. * @param {Function} afterBidAdded callback function used when Prebid Cache responds - * @param {Object} bidderRequest copy of bid's associated bidderRequest object + * @param {Object} videoConfig mediaTypes.video from the bid's adUnit */ -export function callPrebidCacheHook(fn, auctionInstance, bidResponse, afterBidAdded, bidderRequest) { - let videoConfig = deepAccess(bidderRequest, 'mediaTypes.video'); +export function callPrebidCacheHook(fn, auctionInstance, bidResponse, afterBidAdded, videoConfig) { if (videoConfig && videoConfig.context === ADPOD) { let brandCategoryExclusion = config.getConfig('adpod.brandCategoryExclusion'); let adServerCatId = deepAccess(bidResponse, 'meta.adServerCatId'); @@ -250,7 +262,7 @@ export function callPrebidCacheHook(fn, auctionInstance, bidResponse, afterBidAd } } } else { - fn.call(this, auctionInstance, bidResponse, afterBidAdded, bidderRequest); + fn.call(this, auctionInstance, bidResponse, afterBidAdded, videoConfig); } } @@ -310,18 +322,17 @@ export function checkAdUnitSetupHook(fn, adUnits) { * (eg if range was [5, 15, 30] -> 2s is rounded to 5s; 17s is rounded back to 15s; 18s is rounded up to 30s) * - if the bid is above the range of the listed durations (and outside the buffer), reject the bid * - set the rounded duration value in the `bid.video.durationBucket` field for accepted bids - * @param {Object} bidderRequest copy of the bidderRequest object associated to bidResponse + * @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(bidderRequest, bidResponse) { +function checkBidDuration(videoMediaType, bidResponse) { const buffer = 2; let bidDuration = deepAccess(bidResponse, 'video.durationSeconds'); - let videoConfig = deepAccess(bidderRequest, 'mediaTypes.video'); - let adUnitRanges = videoConfig.durationRangeSec; + let adUnitRanges = videoMediaType.durationRangeSec; adUnitRanges.sort((a, b) => a - b); // ensure the ranges are sorted in numeric order - if (!videoConfig.requireExactDuration) { + if (!videoMediaType.requireExactDuration) { let max = Math.max(...adUnitRanges); if (bidDuration <= (max + buffer)) { let nextHighestRange = find(adUnitRanges, range => (range + buffer) >= bidDuration); @@ -346,12 +357,12 @@ function checkBidDuration(bidderRequest, bidResponse) { * If it's found to not be an adpod bid, it will return to original function via hook logic * @param {Function} fn reference to original function (used by hook logic) * @param {Object} bid incoming bid object - * @param {Object} bidRequest bidRequest object of associated bid + * @param {Object} adUnit adUnit object of associated bid * @param {Object} videoMediaType copy of the `bidRequest.mediaTypes.video` object; used in original function * @param {String} context value of the `bidRequest.mediaTypes.video.context` field; used in original function * @returns {boolean} this return is only used for adpod bids */ -export function checkVideoBidSetupHook(fn, bid, bidRequest, videoMediaType, context) { +export function checkVideoBidSetupHook(fn, bid, adUnit, videoMediaType, context) { if (context === ADPOD) { let result = true; let brandCategoryExclusion = config.getConfig('adpod.brandCategoryExclusion'); @@ -367,7 +378,7 @@ export function checkVideoBidSetupHook(fn, bid, bidRequest, videoMediaType, cont if (!deepAccess(bid, 'video.durationSeconds') || bid.video.durationSeconds <= 0) { result = false; } else { - let isBidGood = checkBidDuration(bidRequest, bid); + let isBidGood = checkBidDuration(videoMediaType, bid); if (!isBidGood) result = false; } } @@ -382,7 +393,7 @@ export function checkVideoBidSetupHook(fn, bid, bidRequest, videoMediaType, cont fn.bail(result); } else { - fn.call(this, bid, bidRequest, videoMediaType, context); + fn.call(this, bid, adUnit, videoMediaType, context); } } diff --git a/modules/adprimeBidAdapter.js b/modules/adprimeBidAdapter.js index 2b5a7e15af2..d64874c393e 100644 --- a/modules/adprimeBidAdapter.js +++ b/modules/adprimeBidAdapter.js @@ -1,10 +1,11 @@ import {registerBidder} from '../src/adapters/bidderFactory.js'; import { BANNER, NATIVE, VIDEO } from '../src/mediaTypes.js'; import { isFn, deepAccess, logMessage } from '../src/utils.js'; +import { config } from '../src/config.js'; const BIDDER_CODE = 'adprime'; const AD_URL = 'https://delta.adprime.com/pbjs'; -const SYNC_URL = 'https://delta.adprime.com'; +const SYNC_URL = 'https://sync.adprime.com'; function isBidResponseValid(bid) { if (!bid.requestId || !bid.cpm || !bid.creativeId || @@ -150,7 +151,8 @@ export const spec = { }, getUserSyncs: (syncOptions, serverResponses, gdprConsent, uspConsent) => { - let syncUrl = SYNC_URL + 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}`; @@ -161,12 +163,15 @@ export const spec = { if (uspConsent && uspConsent.consentString) { syncUrl += `&ccpa_consent=${uspConsent.consentString}`; } + + const coppa = config.getConfig('coppa') ? 1 : 0; + syncUrl += `&coppa=${coppa}`; + return [{ - type: 'image', + type: syncType, url: syncUrl }]; } - }; registerBidder(spec); diff --git a/modules/adqueryBidAdapter.js b/modules/adqueryBidAdapter.js index ce31f64d705..348bdc90808 100644 --- a/modules/adqueryBidAdapter.js +++ b/modules/adqueryBidAdapter.js @@ -11,7 +11,7 @@ const ADQUERY_USER_SYNC_DOMAIN = ADQUERY_BIDDER_DOMAIN_PROTOCOL + '://' + ADQUER const ADQUERY_DEFAULT_CURRENCY = 'PLN'; const ADQUERY_NET_REVENUE = true; const ADQUERY_TTL = 360; -const storage = getStorageManager(ADQUERY_GVLID); +const storage = getStorageManager({gvlid: ADQUERY_GVLID, bidderCode: ADQUERY_BIDDER_CODE}); /** @type {BidderSpec} */ export const spec = { diff --git a/modules/adqueryIdSystem.js b/modules/adqueryIdSystem.js index 5357c1a1ffd..85421bf588d 100644 --- a/modules/adqueryIdSystem.js +++ b/modules/adqueryIdSystem.js @@ -13,7 +13,7 @@ import * as utils from '../src/utils.js'; const MODULE_NAME = 'qid'; const AU_GVLID = 902; -export const storage = getStorageManager(AU_GVLID, 'qid'); +export const storage = getStorageManager({gvlid: AU_GVLID, moduleName: 'qid'}); /** * Param or default. diff --git a/modules/adrelevantisBidAdapter.js b/modules/adrelevantisBidAdapter.js index 649031d1e3b..3d4de7c7b9d 100644 --- a/modules/adrelevantisBidAdapter.js +++ b/modules/adrelevantisBidAdapter.js @@ -1,14 +1,26 @@ -import { Renderer } from '../src/Renderer.js'; +import {Renderer} from '../src/Renderer.js'; import { - logError, convertTypes, convertCamelToUnderscore, isArray, deepClone, logWarn, logMessage, getBidRequest, deepAccess, - isStr, createTrackPixelHtml, isEmpty, transformBidderParamKeywords, chunk, isArrayOfNums + chunk, + convertCamelToUnderscore, + convertTypes, + createTrackPixelHtml, + deepAccess, + deepClone, + getBidRequest, + isArray, + isArrayOfNums, + isEmpty, + isStr, + logError, + logMessage, + logWarn, + transformBidderParamKeywords } from '../src/utils.js'; -import { config } from '../src/config.js'; -import { registerBidder } from '../src/adapters/bidderFactory.js'; -import { BANNER, NATIVE, VIDEO } from '../src/mediaTypes.js'; -import find from 'core-js-pure/features/array/find.js'; -import includes from 'core-js-pure/features/array/includes.js'; -import { OUTSTREAM, INSTREAM } from '../src/video.js'; +import {config} from '../src/config.js'; +import {registerBidder} from '../src/adapters/bidderFactory.js'; +import {BANNER, NATIVE, VIDEO} from '../src/mediaTypes.js'; +import {find, includes} from '../src/polyfill.js'; +import {INSTREAM, OUTSTREAM} from '../src/video.js'; const BIDDER_CODE = 'adrelevantis'; const URL = 'https://ssp.adrelevantis.com/prebid'; @@ -127,7 +139,7 @@ export const spec = { if (fpdcfg && fpdcfg.context) { let fdata = { keywords: fpdcfg.context.keywords || '', - category: fpdcfg.context.category || '' + category: fpdcfg.context.data.category || '' } payload.fpd = fdata; } diff --git a/modules/adrinoBidAdapter.js b/modules/adrinoBidAdapter.js new file mode 100644 index 00000000000..4520066c3e7 --- /dev/null +++ b/modules/adrinoBidAdapter.js @@ -0,0 +1,74 @@ +import {registerBidder} from '../src/adapters/bidderFactory.js'; +import {triggerPixel} from '../src/utils.js'; +import {NATIVE} from '../src/mediaTypes.js'; + +const BIDDER_CODE = 'adrino'; +const REQUEST_METHOD = 'POST'; +const BIDDER_HOST = 'https://prd-prebid-bidder.adrino.io'; +const GVLID = 1072; + +export const spec = { + code: BIDDER_CODE, + gvlid: GVLID, + supportedMediaTypes: [NATIVE], + + isBidRequestValid: function (bid) { + return !!(bid.bidId) && + !!(bid.params) && + !!(bid.params.hash) && + (typeof bid.params.hash === 'string') && + !!(bid.mediaTypes) && + Object.keys(bid.mediaTypes).includes(NATIVE) && + (bid.bidder === BIDDER_CODE); + }, + + buildRequests: function (validBidRequests, bidderRequest) { + const bidRequests = []; + + for (let i = 0; i < validBidRequests.length; i++) { + let requestData = { + bidId: validBidRequests[i].bidId, + nativeParams: validBidRequests[i].nativeParams, + placementHash: validBidRequests[i].params.hash, + referer: bidderRequest.refererInfo.referer, + userAgent: navigator.userAgent, + } + + if (bidderRequest && bidderRequest.gdprConsent) { + requestData.gdprConsent = { + consentString: bidderRequest.gdprConsent.consentString, + consentRequired: bidderRequest.gdprConsent.gdprApplies + } + } + + bidRequests.push({ + method: REQUEST_METHOD, + url: BIDDER_HOST + '/bidder/bid/', + data: requestData, + options: { + contentType: 'application/json', + withCredentials: false, + } + }); + } + + return bidRequests; + }, + + interpretResponse: function (serverResponse, bidRequest) { + const response = serverResponse.body; + const bidResponses = []; + if (!response.noAd) { + bidResponses.push(response); + } + return bidResponses; + }, + + onBidWon: function (bid) { + if (bid['requestId']) { + triggerPixel(BIDDER_HOST + '/bidder/won/' + bid['requestId']); + } + } +}; + +registerBidder(spec); diff --git a/modules/adrinoBidAdapter.md b/modules/adrinoBidAdapter.md new file mode 100644 index 00000000000..5ec63a72736 --- /dev/null +++ b/modules/adrinoBidAdapter.md @@ -0,0 +1,45 @@ +# Overview + +``` +Module Name: Adrino Bidder Adapter +Module Type: Bidder Adapter +Maintainer: dev@adrino.pl +``` + +# Description + +Module connects to Adrino bidder to fetch bids. Only native format is supported. + +# Test Parameters + +``` +var adUnits = [ + code: '/12345678/prebid_native_example_1', + mediaTypes: { + native: { + image: { + required: true, + sizes: [[300, 210],[300,150],[140,100]] + }, + title: { + required: true + }, + sponsoredBy: { + required: false + }, + body: { + required: false + }, + icon: { + required: false + } + } + }, + bids: [{ + bidder: 'adrino', + params: { + hash: 'abcdef123456' + } + }] +]; +``` diff --git a/modules/adriverBidAdapter.js b/modules/adriverBidAdapter.js index 67e039e4692..5ab417520e9 100644 --- a/modules/adriverBidAdapter.js +++ b/modules/adriverBidAdapter.js @@ -1,13 +1,14 @@ // ADRIVER BID ADAPTER for Prebid 1.13 import { logInfo, getWindowLocation, getBidIdParameter, _each } from '../src/utils.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; +import { getStorageManager } from '../src/storageManager.js'; const BIDDER_CODE = 'adriver'; const ADRIVER_BID_URL = 'https://pb.adriver.ru/cgi-bin/bid.cgi'; const TIME_TO_LIVE = 3000; +export const storage = getStorageManager({bidderCode: BIDDER_CODE}); export const spec = { - code: BIDDER_CODE, /** @@ -98,6 +99,15 @@ export const spec = { }); }); + let userid = validBidRequests[0].userId; + let adrcidCookie = storage.getDataFromLocalStorage('adrcid') || validBidRequests[0].userId.adrcid; + + if (adrcidCookie) { + payload.adrcid = adrcidCookie; + payload.id5 = userid.id5id; + payload.sharedid = userid.pubcid; + payload.unifiedid = userid.tdid; + } const payloadString = JSON.stringify(payload); return { diff --git a/modules/adriverIdSystem.js b/modules/adriverIdSystem.js new file mode 100644 index 00000000000..6a492fac508 --- /dev/null +++ b/modules/adriverIdSystem.js @@ -0,0 +1,83 @@ +/** + * This module adds AdriverId to the User ID module + * The {@link module:modules/userId} module is required + * @module modules/adriverIdSubmodule + * @requires module:modules/userId + */ + +import { logError, isPlainObject } from '../src/utils.js' +import { ajax } from '../src/ajax.js'; +import { submodule } from '../src/hook.js'; +import { getStorageManager } from '../src/storageManager.js'; + +const MODULE_NAME = 'adriverId'; + +export const storage = getStorageManager(); + +/** @type {Submodule} */ +export const adriverIdSubmodule = { + /** + * 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 {{adriverId:string}} + */ + decode(value) { + return { adrcid: value } + }, + /** + * 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) { + if (!isPlainObject(config.params)) { + config.params = {}; + } + const url = 'https://ad.adriver.ru/cgi-bin/json.cgi?sid=1&ad=719473&bt=55&pid=3198680&bid=7189165&bn=7189165&tuid=1'; + const resp = function (callback) { + let creationDate = storage.getDataFromLocalStorage('adrcid_cd') || storage.getCookie('adrcid_cd'); + let cookie = storage.getDataFromLocalStorage('adrcid') || storage.getCookie('adrcid'); + + if (cookie && creationDate && ((new Date().getTime() - creationDate) < 86400000)) { + const responseObj = cookie; + callback(responseObj); + } else { + const callbacks = { + success: response => { + let responseObj; + if (response) { + try { + responseObj = JSON.parse(response).adrcid; + } catch (error) { + logError(error); + } + let now = new Date(); + now.setTime(now.getTime() + 86400 * 1825 * 1000); + storage.setCookie('adrcid', responseObj, now.toUTCString(), 'Lax'); + storage.setDataInLocalStorage('adrcid', responseObj); + storage.setCookie('adrcid_cd', new Date().getTime(), now.toUTCString(), 'Lax'); + storage.setDataInLocalStorage('adrcid_cd', new Date().getTime()); + } + callback(responseObj); + }, + error: error => { + logError(`${MODULE_NAME}: ID fetch encountered an error`, error); + callback(); + } + }; + ajax(url, callbacks, undefined, {method: 'GET'}); + } + }; + return {callback: resp}; + } +}; + +submodule('userId', adriverIdSubmodule); diff --git a/modules/adriverIdSystem.md b/modules/adriverIdSystem.md new file mode 100644 index 00000000000..797318ba977 --- /dev/null +++ b/modules/adriverIdSystem.md @@ -0,0 +1,19 @@ +# Overview + +Module Name: AdRiver Id System +Module Type: User Id System +Maintainer: support@adriver.ru + +# Description + +Adriver user identification system + +## Example configuration for publishers: + +pbjs.setConfig({ + userSync: { + userIds: [{ + name: 'adriverId' + }] + } +}); \ No newline at end of file diff --git a/modules/adtargetBidAdapter.js b/modules/adtargetBidAdapter.js index 0ad0177815a..a07b0de0f67 100644 --- a/modules/adtargetBidAdapter.js +++ b/modules/adtargetBidAdapter.js @@ -1,8 +1,8 @@ -import { deepAccess, isArray, chunk, _map, flatten, 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 'core-js-pure/features/array/find.js'; +import {_map, chunk, 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'; 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 44a9c90d438..f309ed4e96e 100644 --- a/modules/adtelligentBidAdapter.js +++ b/modules/adtelligentBidAdapter.js @@ -1,9 +1,9 @@ -import { deepAccess, isArray, chunk, _map, flatten, convertTypes, 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 'core-js-pure/features/array/find.js'; +import {_map, chunk, convertTypes, 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'; const subdomainSuffixes = ['', 1, 2]; const AUCTION_PATH = '/v2/auction/'; @@ -18,7 +18,6 @@ const HOST_GETTERS = { navelix: () => 'ghb.hb.navelix.com', appaloosa: () => 'ghb.hb.appaloosa.media', onefiftytwomedia: () => 'ghb.ads.152media.com', - mediafuse: () => 'ghb.hbmp.mediafuse.com', bidsxchange: () => 'ghb.hbd.bidsxchange.com', streamkey: () => 'ghb.hb.streamkey.net', } @@ -37,11 +36,7 @@ export const spec = { code: BIDDER_CODE, gvlid: 410, aliases: ['onefiftytwomedia', 'selectmedia', 'appaloosa', 'bidsxchange', 'streamkey', - { code: 'navelix', gvlid: 380 }, - { - code: 'mediafuse', - skipPbsAliasing: true - } + { code: 'navelix', gvlid: 380 } ], supportedMediaTypes: [VIDEO, BANNER], isBidRequestValid: function (bid) { diff --git a/modules/adtrueBidAdapter.js b/modules/adtrueBidAdapter.js index df848fba823..283e1273150 100644 --- a/modules/adtrueBidAdapter.js +++ b/modules/adtrueBidAdapter.js @@ -4,8 +4,8 @@ import {BANNER, NATIVE, VIDEO} from '../src/mediaTypes.js'; import {config} from '../src/config.js'; import {getStorageManager} from '../src/storageManager.js'; -const storage = getStorageManager(); const BIDDER_CODE = 'adtrue'; +const storage = getStorageManager({bidderCode: BIDDER_CODE}); const ADTRUE_CURRENCY = 'USD'; const ENDPOINT_URL = 'https://hb.adtrue.com/prebid/auction'; const LOG_WARN_PREFIX = 'AdTrue: '; diff --git a/modules/advangelistsBidAdapter.js b/modules/advangelistsBidAdapter.js index 854c65b1f22..605e19cfc66 100755 --- a/modules/advangelistsBidAdapter.js +++ b/modules/advangelistsBidAdapter.js @@ -1,9 +1,8 @@ -import { isEmpty, deepAccess, isFn, parseSizesInput, generateUUID, parseUrl } from '../src/utils.js'; -import { config } from '../src/config.js'; -import { registerBidder } from '../src/adapters/bidderFactory.js'; -import { VIDEO, BANNER } from '../src/mediaTypes.js'; -import find from 'core-js-pure/features/array/find.js'; -import includes from 'core-js-pure/features/array/includes.js'; +import {deepAccess, generateUUID, isEmpty, isFn, parseSizesInput, parseUrl} from '../src/utils.js'; +import {config} from '../src/config.js'; +import {registerBidder} from '../src/adapters/bidderFactory.js'; +import {BANNER, VIDEO} from '../src/mediaTypes.js'; +import {find, includes} from '../src/polyfill.js'; const ADAPTER_VERSION = '1.0'; const BIDDER_CODE = 'advangelists'; diff --git a/modules/adxpremiumAnalyticsAdapter.js b/modules/adxpremiumAnalyticsAdapter.js index 3e30de14052..9066c26fb00 100644 --- a/modules/adxpremiumAnalyticsAdapter.js +++ b/modules/adxpremiumAnalyticsAdapter.js @@ -1,9 +1,9 @@ -import { logError, logInfo, deepClone } from '../src/utils.js'; -import { ajax } from '../src/ajax.js'; +import {deepClone, logError, logInfo} from '../src/utils.js'; +import {ajax} from '../src/ajax.js'; import adapter from '../src/AnalyticsAdapter.js'; import adapterManager from '../src/adapterManager.js'; import CONSTANTS from '../src/constants.json'; -import includes from 'core-js-pure/features/array/includes.js'; +import {includes} from '../src/polyfill.js'; const analyticsType = 'endpoint'; const defaultUrl = 'https://adxpremium.services/graphql'; diff --git a/modules/adyoulikeBidAdapter.js b/modules/adyoulikeBidAdapter.js index 155e8ca3c7a..1a0074e668b 100644 --- a/modules/adyoulikeBidAdapter.js +++ b/modules/adyoulikeBidAdapter.js @@ -1,14 +1,15 @@ -import { deepAccess, buildUrl, parseSizesInput } from '../src/utils.js'; -import { registerBidder } from '../src/adapters/bidderFactory.js'; -import { config } from '../src/config.js'; -import { createEidsArray } from './userId/eids.js'; -import find from 'core-js-pure/features/array/find.js'; +import {buildUrl, deepAccess, parseSizesInput} from '../src/utils.js'; +import {registerBidder} from '../src/adapters/bidderFactory.js'; +import {config} from '../src/config.js'; +import {createEidsArray} from './userId/eids.js'; +import {find} from '../src/polyfill.js'; import {BANNER, NATIVE, VIDEO} from '../src/mediaTypes.js'; const VERSION = '1.0'; const BIDDER_CODE = 'adyoulike'; const DEFAULT_DC = 'hb-api'; const CURRENCY = 'USD'; +const GVLID = 259; const NATIVE_IMAGE = { image: { @@ -36,6 +37,7 @@ const NATIVE_IMAGE = { export const spec = { code: BIDDER_CODE, + gvlid: GVLID, supportedMediaTypes: [BANNER, NATIVE, VIDEO], aliases: ['ayl'], // short code /** @@ -59,6 +61,7 @@ export const spec = { * @return ServerRequest Info describing the request to the server. */ buildRequests: function (bidRequests, bidderRequest) { + let hasVideo = false; const payload = { Version: VERSION, Bids: bidRequests.reduce((accumulator, bidReq) => { @@ -86,6 +89,7 @@ export const spec = { accumulator[bidReq.bidId].Native = nativeReq; } if (mediatype === VIDEO) { + hasVideo = true; accumulator[bidReq.bidId].Video = bidReq.mediaTypes.video; const size = bidReq.mediaTypes.video.playerSize; @@ -120,7 +124,7 @@ export const spec = { return { method: 'POST', - url: createEndpoint(bidRequests, bidderRequest), + url: createEndpoint(bidRequests, bidderRequest, hasVideo), data, options }; @@ -215,12 +219,13 @@ function getPageRefreshed() { } /* Create endpoint url */ -function createEndpoint(bidRequests, bidderRequest) { +function createEndpoint(bidRequests, bidderRequest, hasVideo) { let host = getHostname(bidRequests); + const endpoint = hasVideo ? '/hb-api/prebid-video/v1' : '/hb-api/prebid/v1'; return buildUrl({ protocol: 'https', host: `${DEFAULT_DC}${host}.omnitagjs.com`, - pathname: '/hb-api/prebid/v1', + pathname: endpoint, search: createEndpointQS(bidderRequest) }); } @@ -431,7 +436,7 @@ function getNativeAssets(response, nativeConfig) { const icurl = getImageUrl(adJson, deepAccess(adJson, 'Content.Preview.Sponsor.Logo.Resource'), iconSize[0], iconSize[1]); - if (url) { + if (icurl) { native[key] = { url: icurl, width: iconSize[0], diff --git a/modules/afpBidAdapter.js b/modules/afpBidAdapter.js index 68941ff17c9..6565942bcc8 100644 --- a/modules/afpBidAdapter.js +++ b/modules/afpBidAdapter.js @@ -1,7 +1,7 @@ -import includes from 'core-js-pure/features/array/includes.js' -import { registerBidder } from '../src/adapters/bidderFactory.js' -import { Renderer } from '../src/Renderer.js' -import { BANNER, VIDEO } from '../src/mediaTypes.js' +import {includes} from '../src/polyfill.js'; +import {registerBidder} from '../src/adapters/bidderFactory.js'; +import {Renderer} from '../src/Renderer.js'; +import {BANNER, VIDEO} from '../src/mediaTypes.js'; export const IS_DEV = location.hostname === 'localhost' export const BIDDER_CODE = 'afp' diff --git a/modules/airgridRtdProvider.js b/modules/airgridRtdProvider.js index f5403cca3eb..b2e78a7df78 100644 --- a/modules/airgridRtdProvider.js +++ b/modules/airgridRtdProvider.js @@ -16,7 +16,7 @@ const SUBMODULE_NAME = 'airgrid'; const AG_TCF_ID = 782; export const AG_AUDIENCE_IDS_KEY = 'edkt_matched_audience_ids' -export const storage = getStorageManager(AG_TCF_ID, SUBMODULE_NAME); +export const storage = getStorageManager({gvlid: AG_TCF_ID, moduleName: SUBMODULE_NAME}); /** * Attach script tag to DOM diff --git a/modules/akamaiDapRtdProvider.js b/modules/akamaiDapRtdProvider.js index d143a53fbf4..aca984d39c8 100644 --- a/modules/akamaiDapRtdProvider.js +++ b/modules/akamaiDapRtdProvider.js @@ -15,7 +15,7 @@ const MODULE_NAME = 'realTimeData'; const SUBMODULE_NAME = 'dap'; export const SEGMENTS_STORAGE_KEY = 'akamaiDapSegments'; -export const storage = getStorageManager(null, SUBMODULE_NAME); +export const storage = getStorageManager({gvlid: null, moduleName: SUBMODULE_NAME}); /** * Lazy merge objects. diff --git a/modules/amxBidAdapter.js b/modules/amxBidAdapter.js index d48245e9604..d1754936d7f 100644 --- a/modules/amxBidAdapter.js +++ b/modules/amxBidAdapter.js @@ -5,7 +5,7 @@ import { config } from '../src/config.js'; import { getStorageManager } from '../src/storageManager.js'; const BIDDER_CODE = 'amx'; -const storage = getStorageManager(737, BIDDER_CODE); +const storage = getStorageManager({gvlid: 737, 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.1'; diff --git a/modules/aniviewBidAdapter.js b/modules/aniviewBidAdapter.js index 53249e92a77..7760aa2b47b 100644 --- a/modules/aniviewBidAdapter.js +++ b/modules/aniviewBidAdapter.js @@ -309,7 +309,7 @@ function getUserSyncs(syncOptions, serverResponses) { export const spec = { code: BIDDER_CODE, gvlid: GVLID, - aliases: ['avantisvideo', 'selectmediavideo', 'vidcrunch', 'openwebvideo', 'didnavideo'], + aliases: ['avantisvideo', 'selectmediavideo', 'vidcrunch', 'openwebvideo', 'didnavideo', 'ottadvisors'], supportedMediaTypes: [VIDEO, BANNER], isBidRequestValid, buildRequests, diff --git a/modules/appnexusBidAdapter.js b/modules/appnexusBidAdapter.js index 5480d1eedca..2758fc2d03a 100644 --- a/modules/appnexusBidAdapter.js +++ b/modules/appnexusBidAdapter.js @@ -1,13 +1,37 @@ -import { convertCamelToUnderscore, isArray, isNumber, isPlainObject, logError, logInfo, deepAccess, logMessage, convertTypes, isStr, getParameterByName, deepClone, chunk, logWarn, getBidRequest, createTrackPixelHtml, isEmpty, transformBidderParamKeywords, getMaxValueFromArray, fill, getMinValueFromArray, isArrayOfNums, isFn, isAllowZeroCpmBidsEnabled } from '../src/utils.js'; -import { Renderer } from '../src/Renderer.js'; -import { config } from '../src/config.js'; -import { registerBidder, getIabSubCategory } from '../src/adapters/bidderFactory.js'; -import { BANNER, NATIVE, VIDEO, ADPOD } from '../src/mediaTypes.js'; -import { auctionManager } from '../src/auctionManager.js'; -import find from 'core-js-pure/features/array/find.js'; -import includes from 'core-js-pure/features/array/includes.js'; -import { OUTSTREAM, INSTREAM } from '../src/video.js'; -import { getStorageManager } from '../src/storageManager.js'; +import { + chunk, + convertCamelToUnderscore, + convertTypes, + createTrackPixelHtml, + deepAccess, + deepClone, + fill, + getBidRequest, + getMaxValueFromArray, + getMinValueFromArray, + getParameterByName, + isArray, + isArrayOfNums, + isEmpty, + isFn, + isNumber, + isPlainObject, + isStr, + logError, + logInfo, + logMessage, + logWarn, + transformBidderParamKeywords +} from '../src/utils.js'; +import {Renderer} from '../src/Renderer.js'; +import {config} from '../src/config.js'; +import {getIabSubCategory, registerBidder} from '../src/adapters/bidderFactory.js'; +import {ADPOD, BANNER, NATIVE, VIDEO} from '../src/mediaTypes.js'; +import {auctionManager} from '../src/auctionManager.js'; +import {find, includes} from '../src/polyfill.js'; +import {INSTREAM, OUTSTREAM} from '../src/video.js'; +import {getStorageManager} from '../src/storageManager.js'; +import {bidderSettings} from '../src/bidderSettings.js'; const BIDDER_CODE = 'appnexus'; const URL = 'https://ib.adnxs.com/ut/v3/prebid'; @@ -60,7 +84,7 @@ const SCRIPT_TAG_START = ' 0) { + aucKeywords.forEach(deleteValues); + } + + payload.keywords = aucKeywords; + } + if (config.getConfig('adpod.brandCategoryExclusion')) { payload.brand_category_uniqueness = true; } @@ -258,6 +293,13 @@ export const spec = { addUserId(eids, deepAccess(bidRequests[0], `userId.idl_env`), 'liveramp.com', null); addUserId(eids, deepAccess(bidRequests[0], `userId.tdid`), 'adserver.org', 'TDID'); addUserId(eids, deepAccess(bidRequests[0], `userId.uid2.id`), 'uidapi.com', 'UID2'); + if (bidRequests[0].userId.pubProvidedId) { + bidRequests[0].userId.pubProvidedId.forEach(ppId => { + ppId.uids.forEach(uid => { + eids.push({ source: ppId.source, id: uid.id }); + }); + }); + } if (eids.length) { payload.eids = eids; @@ -292,7 +334,7 @@ export const spec = { serverResponse.tags.forEach(serverBid => { const rtbBid = getRtbBid(serverBid); if (rtbBid) { - const cpmCheck = (isAllowZeroCpmBidsEnabled(bidderRequest.bidderCode)) ? rtbBid.cpm >= 0 : rtbBid.cpm > 0; + const cpmCheck = (bidderSettings.get(bidderRequest.bidderCode, 'allowZeroCpmBids') === true) ? rtbBid.cpm >= 0 : rtbBid.cpm > 0; if (cpmCheck && includes(this.supportedMediaTypes, rtbBid.ad_type)) { const bid = newBid(serverBid, rtbBid, bidderRequest); bid.mediaType = parseMediaType(rtbBid); @@ -348,11 +390,20 @@ export const spec = { }, transformBidParams: function (params, isOpenRtb) { + let conversionFn = transformBidderParamKeywords; + if (isOpenRtb === true) { + let s2sConfig = config.getConfig('s2sConfig'); + let s2sEndpointUrl = deepAccess(s2sConfig, 'endpoint.p1Consent'); + if (s2sEndpointUrl && s2sEndpointUrl.match('/openrtb2/prebid')) { + conversionFn = convertKeywordsToString; + } + } + params = convertTypes({ 'member': 'string', 'invCode': 'string', 'placementId': 'number', - 'keywords': transformBidderParamKeywords, + 'keywords': conversionFn, 'publisherId': 'number' }, params); @@ -1145,4 +1196,31 @@ function getBidFloor(bid) { return null; } +// keywords: { 'genre': ['rock', 'pop'], 'pets': ['dog'] } goes to 'genre=rock,genre=pop,pets=dog' +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])) { + if (keywords[key][0] === '') { + result += `${key},` + } else { + keywords[key].forEach(val => { + result += `${key}=${val},` + }); + } + } + }); + + // remove last trailing comma + result = result.substring(0, result.length - 1); + return result; +} + registerBidder(spec); diff --git a/modules/apstreamBidAdapter.js b/modules/apstreamBidAdapter.js index f2d4189f237..b69fffb8b6b 100644 --- a/modules/apstreamBidAdapter.js +++ b/modules/apstreamBidAdapter.js @@ -8,7 +8,7 @@ const CONSTANTS = { BIDDER_CODE: 'apstream', GVLID: 394 }; -const storage = getStorageManager(CONSTANTS.GVLID, CONSTANTS.BIDDER_CODE); +const storage = getStorageManager({gvlid: CONSTANTS.GVLID, bidderCode: CONSTANTS.BIDDER_CODE}); var dsuModule = (function() { 'use strict'; diff --git a/modules/asealBidAdapter.js b/modules/asealBidAdapter.js new file mode 100644 index 00000000000..559afefa94b --- /dev/null +++ b/modules/asealBidAdapter.js @@ -0,0 +1,58 @@ +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { BANNER } from '../src/mediaTypes.js'; +import { config } from '../src/config.js'; + +export const BIDDER_CODE = 'aseal'; +const SUPPORTED_AD_TYPES = [BANNER]; +export const API_ENDPOINT = 'https://tkprebid.aotter.net/prebid/adapter'; +export const HEADER_AOTTER_VERSION = 'prebid_0.0.1'; + +export const spec = { + code: BIDDER_CODE, + aliases: ['aotter', 'trek'], + supportedMediaTypes: SUPPORTED_AD_TYPES, + + isBidRequestValid: (bid) => !!bid.params.placeUid && typeof bid.params.placeUid === 'string', + + buildRequests: (validBidRequests, bidderRequest) => { + if (validBidRequests.length === 0) { + return []; + } + + const clientId = + config.getConfig('aseal.clientId') || ''; + + const data = { + bids: validBidRequests, + refererInfo: bidderRequest.refererInfo, + }; + + const options = { + contentType: 'application/json', + withCredentials: true, + customHeaders: { + 'x-aotter-clientid': clientId, + 'x-aotter-version': HEADER_AOTTER_VERSION, + }, + }; + + 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/asealBidAdapter.md b/modules/asealBidAdapter.md new file mode 100644 index 00000000000..d13b802f736 --- /dev/null +++ b/modules/asealBidAdapter.md @@ -0,0 +1,52 @@ +# Overview + +``` +Module Name: Aseal Bid Adapter +Module Type: Bidder Adapter +Maintainer: tech-service@aotter.net +``` + +# Description + +Module that connects to Aseal server for bids. +Supported Ad Formats: + +- Banner + +# Configuration + +Following configuration is required: + +```js +pbjs.setConfig({ + aseal: { + clientId: "YOUR_CLIENT_ID" + } +}); +``` + +# Ad Unit Example + +```js +var adUnits = [ + { + code: "banner-div", + mediaTypes: { + banner: { + sizes: [ + [300, 250], + [300, 600] + ] + } + }, + bids: [ + { + bidder: "aseal", + params: { + placeUid: "f4a74f73-9a74-4a87-91c9-545c6316c23d" + } + } + ] + } +]; +``` diff --git a/modules/automatadBidAdapter.js b/modules/automatadBidAdapter.js index 2cfcfbe98b4..726bbef9bd6 100644 --- a/modules/automatadBidAdapter.js +++ b/modules/automatadBidAdapter.js @@ -5,7 +5,7 @@ import {ajax} from '../src/ajax.js' const BIDDER = 'automatad' -const ENDPOINT_URL = 'https://rtb2.automatad.com/ortb2' +const ENDPOINT_URL = 'https://bid.atmtd.com' const DEFAULT_BID_TTL = 30 const DEFAULT_CURRENCY = 'USD' @@ -57,7 +57,7 @@ export const spec = { const payloadString = JSON.stringify(openrtbRequest) return { method: 'POST', - url: ENDPOINT_URL + '/resp', + url: ENDPOINT_URL + '/request', data: payloadString, options: { contentType: 'application/json', @@ -72,6 +72,7 @@ export const spec = { const response = (serverResponse || {}).body if (response && response.seatbid && response.seatbid[0].bid && response.seatbid[0].bid.length) { + var bidid = response.bidid response.seatbid.forEach(bidObj => { bidObj.bid.forEach(bid => { bidResponses.push({ @@ -88,6 +89,7 @@ export const spec = { height: bid.h, netRevenue: DEFAULT_NET_REVENUE, nurl: bid.nurl, + bidId: bidid }) }) }) @@ -97,11 +99,9 @@ export const spec = { return bidResponses }, - getUserSyncs: function(syncOptions, serverResponse) { - return [{ - type: 'iframe', - url: 'https://rtb2.automatad.com/ortb2/async_usersync' - }] + onTimeout: function(timeoutData) { + const timeoutUrl = ENDPOINT_URL + '/timeout' + ajax(timeoutUrl, null, JSON.stringify(timeoutData)) }, onBidWon: function(bid) { if (!bid.nurl) { return } @@ -116,6 +116,9 @@ export const spec = { ).replace( /\$\{AUCTION_CURRENCY\}/, winCurr + ).replace( + /\$\{AUCTON_BID_ID\}/, + bid.bidId ).replace( /\$\{AUCTION_ID\}/, bid.auctionId diff --git a/modules/axonixBidAdapter.js b/modules/axonixBidAdapter.js index 7cd8f63bd2a..a790a89a0c1 100644 --- a/modules/axonixBidAdapter.js +++ b/modules/axonixBidAdapter.js @@ -177,7 +177,7 @@ export const spec = { const { nurl } = bid || {}; if (bid.nurl) { - triggerPixel(replaceAuctionPrice(nurl, bid.cpm)); + triggerPixel(replaceAuctionPrice(nurl, bid.originalCpm || bid.cpm)); }; } } diff --git a/modules/beachfrontBidAdapter.js b/modules/beachfrontBidAdapter.js index e705156d4a2..1c341e4dc51 100644 --- a/modules/beachfrontBidAdapter.js +++ b/modules/beachfrontBidAdapter.js @@ -1,10 +1,19 @@ -import { logWarn, deepAccess, deepSetValue, deepClone, isArray, parseSizesInput, isFn, parseUrl, getUniqueIdentifierStr } from '../src/utils.js'; -import { config } from '../src/config.js'; -import { registerBidder } from '../src/adapters/bidderFactory.js'; -import { Renderer } from '../src/Renderer.js'; -import { VIDEO, BANNER } from '../src/mediaTypes.js'; -import find from 'core-js-pure/features/array/find.js'; -import includes from 'core-js-pure/features/array/includes.js'; +import { + deepAccess, + deepClone, + deepSetValue, + getUniqueIdentifierStr, + isArray, + isFn, + logWarn, + parseSizesInput, + parseUrl +} from '../src/utils.js'; +import {config} from '../src/config.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_NAME = 'BFIO_PREBID'; diff --git a/modules/betweenBidAdapter.js b/modules/betweenBidAdapter.js index b2f63488e12..acf574a3fe2 100644 --- a/modules/betweenBidAdapter.js +++ b/modules/betweenBidAdapter.js @@ -1,12 +1,12 @@ import {registerBidder} from '../src/adapters/bidderFactory.js'; import { getAdUnitSizes, parseSizesInput } from '../src/utils.js'; import { getRefererInfo } from '../src/refererDetection.js'; +import {includes} from '../src/polyfill.js' const BIDDER_CODE = 'between'; let ENDPOINT = 'https://ads.betweendigital.com/adjson?t=prebid'; const CODE_TYPES = ['inpage', 'preroll', 'midroll', 'postroll']; -const includes = require('core-js-pure/features/array/includes.js'); export const spec = { code: BIDDER_CODE, aliases: ['btw'], @@ -118,7 +118,7 @@ export const spec = { mediaType: serverResponse.body[i].mediaType, ttl: serverResponse.body[i].ttl, creativeId: serverResponse.body[i].creativeid, - currency: serverResponse.body[i].currency || 'RUB', + currency: serverResponse.body[i].currency || 'USD', netRevenue: serverResponse.body[i].netRevenue || true, ad: serverResponse.body[i].ad, meta: { @@ -158,10 +158,16 @@ export const spec = { // type: 'iframe', // url: 'https://acdn.adnxs.com/dmp/async_usersync.html' // }); - syncs.push({ - type: 'iframe', - url: 'https://ads.betweendigital.com/sspmatch-iframe' - }); + syncs.push( + { + type: 'iframe', + url: 'https://ads.betweendigital.com/sspmatch-iframe' + }, + { + type: 'image', + url: 'https://ads.betweendigital.com/sspmatch' + } + ); return syncs; } } diff --git a/modules/bidViewability.js b/modules/bidViewability.js index 545d57940da..837eccd00c1 100644 --- a/modules/bidViewability.js +++ b/modules/bidViewability.js @@ -2,13 +2,13 @@ // GPT API is used to find when a bid is viewable, https://developers.google.com/publisher-tag/reference#googletag.events.impressionviewableevent // Does not work with other than GPT integration -import { config } from '../src/config.js'; +import {config} from '../src/config.js'; import * as events from '../src/events.js'; import CONSTANTS from '../src/constants.json'; -import { logWarn, isFn, triggerPixel } from '../src/utils.js'; -import { getGlobal } from '../src/prebidGlobal.js'; -import adapterManager, { gdprDataHandler, uspDataHandler } from '../src/adapterManager.js'; -import find from 'core-js-pure/features/array/find.js'; +import {isFn, logWarn, triggerPixel} from '../src/utils.js'; +import {getGlobal} from '../src/prebidGlobal.js'; +import adapterManager, {gdprDataHandler, uspDataHandler} from '../src/adapterManager.js'; +import {find} from '../src/polyfill.js'; const MODULE_NAME = 'bidViewability'; const CONFIG_ENABLED = 'enabled'; diff --git a/modules/big-richmediaBidAdapter.js b/modules/big-richmediaBidAdapter.js new file mode 100644 index 00000000000..cd8b2462eb8 --- /dev/null +++ b/modules/big-richmediaBidAdapter.js @@ -0,0 +1,117 @@ +import {BANNER, VIDEO} from '../src/mediaTypes.js'; +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 + +const BIDDER_CODE = 'big-richmedia'; + +const metadataByRequestId = {}; + +export const spec = { + version: '1.4.0', + code: BIDDER_CODE, + gvlid: baseAdapter.GVLID, // use base adapter gvlid + supportedMediaTypes: [ BANNER, 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) { + if (!baseAdapter.isBidRequestValid) { return true; } + return baseAdapter.isBidRequestValid(bid); + }, + + /** + * Make a server request from the list of BidRequests. + * + * @param {BidRequest[]} bidRequests A non-empty list of bid requests which should be sent to the Server. + * @return ServerRequest Info describing the request to the server. + */ + buildRequests: function (bidRequests, bidderRequest) { + if (!baseAdapter.buildRequests) { return []; } + + const publisherId = config.getConfig('bigRichmedia.publisherId'); + if (typeof publisherId !== 'string') { return []; } + + bidRequests.forEach(bidRequest => { + if (bidRequest.params.format === 'skin' && bidRequest.mediaTypes.banner) { + bidRequest.mediaTypes.banner.sizes.push([1800, 1000]); + } + metadataByRequestId[bidRequest.bidId] = { placementId: bidRequest.adUnitCode, bidder: bidRequest.bidder }; + }); + return baseAdapter.buildRequests(bidRequests, bidderRequest); + }, + + /** + * 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, params) { + const publisherId = config.getConfig('bigRichmedia.publisherId'); + if (typeof publisherId !== 'string') { return []; } + + const bids = baseAdapter.interpretResponse(serverResponse, params); + bids.forEach(bid => { + const { placementId, bidder } = metadataByRequestId[bid.requestId] || {}; + const { width = 1, height = 1, ad, creativeId = '', cpm, vastXml, vastUrl } = bid; + const bidRequest = params.bidderRequest.bids.find(({ bidId }) => bidId === bid.requestId); + const format = (bidRequest && bidRequest.params && bidRequest.params.format) || 'video-sticky-footer'; + const isReplayable = bidRequest && bidRequest.params && bidRequest.params.isReplayable; + const customSelector = bidRequest && bidRequest.params && bidRequest.params.customSelector; + const renderParams = { + adm: ad, + vastXml, + vastUrl, + width, + height, + placementId, + bidId: bid.requestId, + creativeId: `${creativeId}`, + bidder, + cpm, + format, + customSelector, + isReplayable + }; + const encoded = window.btoa(JSON.stringify(renderParams)); + bid.ad = ` + `; + + if (bid.mediaType !== 'banner') { // in case this is a video + bid.mediaType = 'banner'; + delete bid.renderer; + delete bid.vastUrl; + delete bid.vastXml; + bid.width = 1; + bid.height = 1; + } + }); + return bids; + }, + + getUserSyncs: function (syncOptions, responses, gdprConsent) { + if (!baseAdapter.getUserSyncs) { return []; } + return baseAdapter.getUserSyncs(syncOptions, responses, gdprConsent); + }, + + transformBidParams: function (params, isOpenRtb) { + if (!baseAdapter.transformBidParams) { return params; } + return baseAdapter.transformBidParams(params, isOpenRtb); + }, + + /** + * Add element selector to javascript tracker to improve native viewability + * @param {Bid} bid + */ + onBidWon: function (bid) { + if (!baseAdapter.onBidWon) { return; } + baseAdapter.onBidWon(bid); + } +} + +registerBidder(spec); diff --git a/modules/big-richmediaBidAdapter.md b/modules/big-richmediaBidAdapter.md new file mode 100644 index 00000000000..26f77e527fb --- /dev/null +++ b/modules/big-richmediaBidAdapter.md @@ -0,0 +1,82 @@ +# Overview + +``` +Module Name: BI.Garage Rich Media +Module Type: Bidder Adapter +Maintainer: mediaconsortium-develop@bi.garage.co.jp +``` + +# Description + +Module which renders richmedia demand from a Xandr seat + +### Global configuration + +```javascript +pbjs.setConfig({ + debug: false, + // …, + bigRichmedia: { + publisherId: 'A7FN99NZ98F5ZD4G', // Required + }, +}); +``` + +# AdUnit Configuration +```javascript +var adUnits = [ + // Skin adUnit + { + code: 'banner-div', + mediaTypes: { + banner: { + sizes: [[300, 250], [300,600]] + } + }, + bids: [{ + bidder: 'big-richmedia', + params: { + placementId: 12345, + format: 'skin' // This will automatically add 1800x1000 size to banner mediaType + } + }] + }, + // Video outstream adUnit + { + code: 'video-outstream', + sizes: [[300, 250]], + mediaTypes: { + video: { + playerSize: [[300, 250]], + context: 'outstream', + // Certain ORTB 2.5 video values can be read from the mediatypes object; below are examples of supported params. + // To note - appnexus supports additional values for our system that are not part of the ORTB spec. If you want + // to use these values, they will have to be declared in the bids[].params.video object instead using the appnexus syntax. + // Between the corresponding values of the mediaTypes.video and params.video objects, the properties in params.video will + // take precedence if declared; eg in the example below, the `skippable: true` setting will be used instead of the `skip: 0`. + minduration: 1, + maxduration: 60, + skip: 0, // 1 - true, 0 - false + skipafter: 5, + playbackmethod: [2], // note - we only support options 1-4 at this time + api: [1,2,3] // note - option 6 is not supported at this time + } + }, + bids: [ + { + bidder: 'big-richmedia', + params: { + placementId: 12345, + video: { + skippable: true, + playback_method: 'auto_play_sound_off' + }, + format: 'video-sticky-footer', // or 'video-sticky-top' + isReplayable: true // Default to false - choose if the video should be replayable or not. + customSelector: '#nav-bar' // custom selector for navbar + } + } + ] + } +]; +``` diff --git a/modules/bizzclickBidAdapter.js b/modules/bizzclickBidAdapter.js index 38195f8f9d9..6223626834d 100644 --- a/modules/bizzclickBidAdapter.js +++ b/modules/bizzclickBidAdapter.js @@ -88,11 +88,13 @@ export const spec = { host: location.host }, source: { - tid: bidRequest.transactionId + tid: bidRequest.transactionId, + ext: { + schain: {} + } }, regs: { coppa: config.getConfig('coppa') === true ? 1 : 0, - ext: {} }, user: { ext: {} @@ -115,7 +117,7 @@ export const spec = { } if (bidRequest.schain) { - data.source.ext.schain = bidRequest.schain; + deepSetValue(data, 'source.ext.schain', bidRequest.schain); } let connection = navigator.connection || navigator.webkitConnection; diff --git a/modules/bluebillywigBidAdapter.js b/modules/bluebillywigBidAdapter.js index 03fb0b92c8f..d362dfa5fdb 100644 --- a/modules/bluebillywigBidAdapter.js +++ b/modules/bluebillywigBidAdapter.js @@ -1,10 +1,10 @@ -import { deepAccess, deepSetValue, deepClone, logWarn, logError } from '../src/utils.js'; -import find from 'core-js-pure/features/array/find.js'; -import { registerBidder } from '../src/adapters/bidderFactory.js'; -import { VIDEO } from '../src/mediaTypes.js'; -import { config } from '../src/config.js'; -import { Renderer } from '../src/Renderer.js'; -import { createEidsArray } from './userId/eids.js'; +import {deepAccess, deepClone, deepSetValue, logError, logWarn} from '../src/utils.js'; +import {find} from '../src/polyfill.js'; +import {registerBidder} from '../src/adapters/bidderFactory.js'; +import {VIDEO} from '../src/mediaTypes.js'; +import {config} from '../src/config.js'; +import {Renderer} from '../src/Renderer.js'; +import {createEidsArray} from './userId/eids.js'; const DEV_MODE = window.location.search.match(/bbpbs_debug=true/); diff --git a/modules/brandmetricsRtdProvider.js b/modules/brandmetricsRtdProvider.js new file mode 100644 index 00000000000..60d3c98f15e --- /dev/null +++ b/modules/brandmetricsRtdProvider.js @@ -0,0 +1,168 @@ +/** + * This module adds brandmetrics provider to the real time data module + * The {@link module:modules/realTimeData} module is required + * The module will load load the brandmetrics script and set survey- targeting to ad units of specific bidders. + * @module modules/brandmetricsRtdProvider + * @requires module:modules/realTimeData + */ +import { config } from '../src/config.js' +import { submodule } from '../src/hook.js' +import { deepSetValue, mergeDeep, logError, deepAccess } from '../src/utils.js' +import {loadExternalScript} from '../src/adloader.js' +const MODULE_NAME = 'brandmetrics' +const MODULE_CODE = MODULE_NAME +const RECEIVED_EVENTS = [] +const GVL_ID = 422 +const TCF_PURPOSES = [1, 7] + +function init (config, userConsent) { + const hasConsent = checkConsent(userConsent) + + if (hasConsent) { + const moduleConfig = getMergedConfig(config) + initializeBrandmetrics(moduleConfig.params.scriptId) + } + return hasConsent +} + +/** + * Checks TCF and USP consents + * @param {Object} 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] + } + + 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' + } + + return consent +} + +/** +* 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) { + const targetingConf = event.conf.displayOption || {} + if (targetingConf.type === 'pbjs') { + setBidderTargeting(reqBidsConfigObj, moduleConfig, targetingConf.targetKey || 'brandmetrics_survey', event.survey.measurementId) + } + } + callback() + } + + if (RECEIVED_EVENTS.length > 0) { + callBidTargeting(RECEIVED_EVENTS[RECEIVED_EVENTS.length - 1]) + } else { + window._brandmetrics = window._brandmetrics || [] + window._brandmetrics.push({ + cmd: '_addeventlistener', + val: { + event: 'surveyloaded', + reEmitLast: true, + handler: (ev) => { + RECEIVED_EVENTS.push(ev) + if (RECEIVED_EVENTS.length === 1) { + // Call bid targeting only for the first received event, if called subsequently, last event from the RECEIVED_EVENTS array is used + callBidTargeting(ev) + } + }, + } + }) + } +} + +/** + * Sets bid targeting of specific bidders + * @param {Object} reqBidsConfigObj + * @param {string} key Targeting key + * @param {string} val Targeting value + */ +function setBidderTargeting (reqBidsConfigObj, moduleConfig, key, val) { + const bidders = deepAccess(moduleConfig, 'params.bidders') + if (bidders && bidders.length > 0) { + const ortb2 = {} + deepSetValue(ortb2, 'ortb2.user.ext.data.' + key, val) + config.setBidderConfig({ + bidders: bidders, + config: ortb2 + }) + } +} + +/** + * Add the brandmetrics script to the page. + * @param {string} scriptId - The script- id provided by brandmetrics or brandmetrics partner + */ +function initializeBrandmetrics(scriptId) { + if (scriptId) { + const path = 'https://cdn.brandmetrics.com/survey/script/' + const file = scriptId + '.js' + const url = path + file + + loadExternalScript(url, MODULE_CODE) + } +} + +/** + * Merges a provided config with default values + * @param {Object} customConfig + * @returns + */ +function getMergedConfig(customConfig) { + return mergeDeep({ + waitForIt: false, + params: { + bidders: [], + scriptId: undefined, + } + }, customConfig) +} + +/** @type {RtdSubmodule} */ +export const brandmetricsSubmodule = { + name: MODULE_NAME, + getBidRequestData: function (reqBidsConfigObj, callback, customConfig) { + try { + const moduleConfig = getMergedConfig(customConfig) + if (moduleConfig.waitForIt) { + processBrandmetricsEvents(reqBidsConfigObj, moduleConfig, callback) + } else { + callback() + } + } catch (e) { + logError(e) + } + }, + init: init +} + +submodule('realTimeData', brandmetricsSubmodule) diff --git a/modules/brandmetricsRtdProvider.md b/modules/brandmetricsRtdProvider.md new file mode 100644 index 00000000000..89ee6bb75cf --- /dev/null +++ b/modules/brandmetricsRtdProvider.md @@ -0,0 +1,40 @@ +# Brandmetrics Real-time Data Submodule +This module is intended to be used by brandmetrics (https://brandmetrics.com) partners and sets targeting keywords to bids if the browser is eligeble to see a brandmetrics survey. +The module hooks in to brandmetrics events and requires a brandmetrics script to be running. The module can optionally load and initialize brandmetrics by providing the 'scriptId'- parameter. + +## Usage +Compile the Brandmetrics RTD module into your Prebid build: +``` +gulp build --modules=rtdModule,brandmetricsRtdProvider +``` + +> Note that the global RTD module, `rtdModule`, is a prerequisite of the Brandmetrics RTD module. + +Enable the Brandmetrics RTD in your Prebid configuration, using the below format: + +```javascript +pbjs.setConfig({ + ..., + realTimeData: { + auctionDelay: 500, // auction delay + dataProviders: [{ + name: 'brandmetrics', + waitForIt: true // should be true if there's an `auctionDelay`, + params: { + scriptId: '00000000-0000-0000-0000-000000000000', + bidders: ['ozone'] + } + }] + }, + ... +}) +``` + +## Parameters +| Name | Type | Description | Default | +| ----------------- | -------------------- | ------------------ | ------------------ | +| name | String | This should always be `brandmetrics` | - | +| waitForIt | Boolean | Should be `true` if there's an `auctionDelay` defined (recommended) | `false` | +| 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` | diff --git a/modules/bridgewellBidAdapter.js b/modules/bridgewellBidAdapter.js index 5d545b6f722..b141763af8e 100644 --- a/modules/bridgewellBidAdapter.js +++ b/modules/bridgewellBidAdapter.js @@ -1,7 +1,7 @@ -import { _each, inIframe, deepSetValue } from '../src/utils.js'; -import { registerBidder } from '../src/adapters/bidderFactory.js'; -import { BANNER, NATIVE } from '../src/mediaTypes.js'; -import find from 'core-js-pure/features/array/find.js'; +import {_each, deepSetValue, inIframe} from '../src/utils.js'; +import {registerBidder} from '../src/adapters/bidderFactory.js'; +import {BANNER, NATIVE} from '../src/mediaTypes.js'; +import {find} from '../src/polyfill.js'; const BIDDER_CODE = 'bridgewell'; const REQUEST_ENDPOINT = 'https://prebid.scupio.com/recweb/prebid.aspx?cb='; diff --git a/modules/browsiRtdProvider.js b/modules/browsiRtdProvider.js index a1943afda8d..15f2d58010d 100644 --- a/modules/browsiRtdProvider.js +++ b/modules/browsiRtdProvider.js @@ -15,14 +15,15 @@ * @property {?string} keyName */ -import { deepClone, logError, isGptPubadsDefined, isNumber, isFn, deepSetValue } from '../src/utils.js'; +import {deepClone, deepSetValue, isFn, isGptPubadsDefined, isNumber, logError, logInfo, generateUUID} from '../src/utils.js'; import {submodule} from '../src/hook.js'; import {ajaxBuilder} from '../src/ajax.js'; import {loadExternalScript} from '../src/adloader.js'; import {getStorageManager} from '../src/storageManager.js'; -import find from 'core-js-pure/features/array/find.js'; +import {find, includes} from '../src/polyfill.js'; import {getGlobal} from '../src/prebidGlobal.js'; -import includes from 'core-js-pure/features/array/includes.js'; +import * as events from '../src/events.js'; +import CONSTANTS from '../src/constants.json'; const storage = getStorageManager(); @@ -107,6 +108,7 @@ export function setData(data) { } function getRTD(auc) { + logInfo(`Browsi RTD provider is fetching data for ${auc}`); try { const _bp = (_browsiData && _browsiData.p) || {}; return auc.reduce((rp, uc) => { @@ -332,13 +334,23 @@ export const browsiSubmodule = { getBidRequestData: setBidRequestsData }; -function getTargetingData(uc) { +function getTargetingData(uc, c, us, a) { const targetingData = getRTD(uc); + const auctionId = a.auctionId uc.forEach(auc => { if (isNumber(_ic[auc])) { _ic[auc] = _ic[auc] + 1; } + const transactionId = a.adUnits.find(adUnit => adUnit.code === auc).transactionId; + events.emit(CONSTANTS.EVENTS.BILLABLE_EVENT, { + vendor: 'browsi', + type: 'adRequest', + billingId: generateUUID(), + transactionId: transactionId, + auctionId: auctionId + }) }); + logInfo('Browsi RTD provider returned targeting data', targetingData, 'for', uc) return targetingData; } diff --git a/modules/ccxBidAdapter.js b/modules/ccxBidAdapter.js index 38bc99f1d83..65d1ced30e2 100644 --- a/modules/ccxBidAdapter.js +++ b/modules/ccxBidAdapter.js @@ -3,8 +3,8 @@ import { registerBidder } from '../src/adapters/bidderFactory.js' import { config } from '../src/config.js' import { getStorageManager } from '../src/storageManager.js'; -const storage = getStorageManager(); const BIDDER_CODE = 'ccx' +const storage = getStorageManager({bidderCode: BIDDER_CODE}); const BID_URL = 'https://delivery.clickonometrics.pl/ortb/prebid/bid' const SUPPORTED_VIDEO_PROTOCOLS = [2, 3, 5, 6] const SUPPORTED_VIDEO_MIMES = ['video/mp4', 'video/x-flv'] diff --git a/modules/cleanmedianetBidAdapter.js b/modules/cleanmedianetBidAdapter.js index 3c2d3c51bf5..3fda9917715 100644 --- a/modules/cleanmedianetBidAdapter.js +++ b/modules/cleanmedianetBidAdapter.js @@ -1,9 +1,9 @@ -import { getDNT, inIframe, isArray, isNumber, logError, deepAccess, logWarn } from '../src/utils.js'; +import {deepAccess, getDNT, inIframe, isArray, isNumber, logError, logWarn} from '../src/utils.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import {config} from '../src/config.js'; import {Renderer} from '../src/Renderer.js'; import {BANNER, VIDEO} from '../src/mediaTypes.js'; -import includes from 'core-js-pure/features/array/includes.js'; +import {includes} from '../src/polyfill.js'; export const helper = { getTopWindowDomain: function (url) { diff --git a/modules/colossussspBidAdapter.js b/modules/colossussspBidAdapter.js index b1d36d1095c..c1b6e31ff2e 100644 --- a/modules/colossussspBidAdapter.js +++ b/modules/colossussspBidAdapter.js @@ -2,10 +2,11 @@ import { getWindowTop, deepAccess, logMessage } from '../src/utils.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; import { BANNER, NATIVE, VIDEO } from '../src/mediaTypes.js'; import { ajax } from '../src/ajax.js'; +import { config } from '../src/config.js'; const BIDDER_CODE = 'colossusssp'; const G_URL = 'https://colossusssp.com/?c=o&m=multi'; -const G_URL_SYNC = 'https://colossusssp.com/?c=o&m=cookie'; +const G_URL_SYNC = 'https://sync.colossusssp.com'; function isBidResponseValid(bid) { if (!bid.requestId || !bid.cpm || !bid.creativeId || !bid.ttl || !bid.currency) { @@ -47,7 +48,10 @@ export const spec = { * @return boolean True if this is a valid bid, and false otherwise. */ isBidRequestValid: (bid) => { - return Boolean(bid.bidId && bid.params && !isNaN(bid.params.placement_id)); + const validPlacamentId = bid.params && !isNaN(bid.params.placement_id); + const validGroupId = bid.params && !isNaN(bid.params.group_id); + + return Boolean(bid.bidId && (validPlacamentId || validGroupId)); }, /** @@ -57,12 +61,33 @@ export const spec = { * @return ServerRequest Info describing the request to the server. */ buildRequests: (validBidRequests, bidderRequest) => { - const winTop = getWindowTop(); - const location = winTop.location; + let deviceWidth = 0; + let deviceHeight = 0; + let winLocation; + + try { + const winTop = getWindowTop(); + deviceWidth = winTop.screen.width; + deviceHeight = winTop.screen.height; + winLocation = winTop.location; + } catch (e) { + logMessage(e); + winLocation = window.location; + } + + const refferUrl = bidderRequest.refererInfo && bidderRequest.refererInfo.referer; + let refferLocation; + try { + refferLocation = refferUrl && new URL(refferUrl); + } catch (e) { + logMessage(e); + } + + const location = refferLocation || winLocation; let placements = []; let request = { - deviceWidth: winTop.screen.width, - deviceHeight: winTop.screen.height, + deviceWidth, + deviceHeight, language: (navigator && navigator.language) ? navigator.language : '', secure: location.protocol === 'https:' ? 1 : 0, host: location.host, @@ -172,10 +197,26 @@ export const spec = { return response; }, - getUserSyncs: () => { + getUserSyncs: (syncOptions, serverResponses, gdprConsent, uspConsent) => { + let syncType = syncOptions.iframeEnabled ? 'html' : 'hms.gif'; + let syncUrl = G_URL_SYNC + `/${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: 'image', - url: G_URL_SYNC + type: syncType, + url: syncUrl }]; }, diff --git a/modules/compassBidAdapter.js b/modules/compassBidAdapter.js index a1d1ff9c8c3..77f918276bc 100644 --- a/modules/compassBidAdapter.js +++ b/modules/compassBidAdapter.js @@ -97,7 +97,7 @@ export const spec = { isBidRequestValid: (bid = {}) => { const { params, bidId, mediaTypes } = bid; - let valid = Boolean(bidId && params && params.placementId); + let valid = Boolean(bidId && params && (params.placementId || params.endpointId)); if (mediaTypes && mediaTypes[BANNER]) { valid = valid && Boolean(mediaTypes[BANNER] && mediaTypes[BANNER].sizes); diff --git a/modules/concertBidAdapter.js b/modules/concertBidAdapter.js index 9a55e9cef1d..99e2492fb94 100644 --- a/modules/concertBidAdapter.js +++ b/modules/concertBidAdapter.js @@ -166,7 +166,7 @@ export const spec = { registerBidder(spec); -const storage = getStorageManager(); +const storage = getStorageManager({bidderCode: BIDDER_CODE}); /** * Check or generate a UID for the current user. diff --git a/modules/connectIdSystem.js b/modules/connectIdSystem.js index ac44a8b5a2d..2da2eda4c77 100644 --- a/modules/connectIdSystem.js +++ b/modules/connectIdSystem.js @@ -7,8 +7,8 @@ import {ajax} from '../src/ajax.js'; import {submodule} from '../src/hook.js'; -import {logError, formatQS} from '../src/utils.js'; -import includes from 'core-js-pure/features/array/includes.js'; +import {formatQS, logError} from '../src/utils.js'; +import {includes} from '../src/polyfill.js'; const MODULE_NAME = 'connectId'; const VENDOR_ID = 25; diff --git a/modules/consentManagement.js b/modules/consentManagement.js index ecd0c0eec4b..f0355749055 100644 --- a/modules/consentManagement.js +++ b/modules/consentManagement.js @@ -1,15 +1,13 @@ - /** * This module adds GDPR consentManagement support to prebid.js. It interacts with * supported CMPs (Consent Management Platforms) to grab the user's consent information * and make it available for any GDPR supported adapters to read/pass this information to * their system. */ -import { logInfo, isFn, getAdUnitSizes, logWarn, isStr, isPlainObject, logError, isNumber } from '../src/utils.js'; -import { config } from '../src/config.js'; -import { gdprDataHandler } from '../src/adapterManager.js'; -import includes from 'core-js-pure/features/array/includes.js'; -import strIncludes from 'core-js-pure/features/string/includes.js'; +import {getAdUnitSizes, isFn, isNumber, isPlainObject, isStr, logError, logInfo, logWarn} from '../src/utils.js'; +import {config} from '../src/config.js'; +import {gdprDataHandler} from '../src/adapterManager.js'; +import {includes} from '../src/polyfill.js'; const DEFAULT_CMP = 'iab'; const DEFAULT_CONSENT_TIMEOUT = 10000; @@ -36,23 +34,22 @@ const cmpCallMap = { /** * This function reads the consent string from the config to obtain the consent information of the user. - * @param {function(string)} cmpSuccess acts as a success callback when the value is read from config; pass along consentObject (string) from CMP - * @param {function(string)} cmpError acts as an error callback while interacting with the config string; pass along an error message (string) - * @param {object} hookConfig contains module related variables (see comment in requestBidsHook function) + * @param {function({})} onSuccess acts as a success callback when the value is read from config; pass along consentObject from CMP */ -function lookupStaticConsentData(cmpSuccess, cmpError, hookConfig) { - cmpSuccess(staticConsentData, hookConfig); +function lookupStaticConsentData({onSuccess}) { + onSuccess(staticConsentData); } /** * 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(string)} cmpSuccess acts as a success callback when CMP returns a value; pass along consentObject (string) from CMP - * @param {function(string)} cmpError acts as an error callback while interacting with CMP; pass along an error message (string) - * @param {object} hookConfig contains module related variables (see comment in requestBidsHook function) + * @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) + * @param width + * @param height size info passed to the SafeFrame API (used only for TCFv1 when Prebid is running within a safeframe) */ -function lookupIabConsent(cmpSuccess, cmpError, hookConfig) { +function lookupIabConsent({onSuccess, onError, width, height}) { function findCMP() { let f = window; let cmpFrame; @@ -102,10 +99,10 @@ function lookupIabConsent(cmpSuccess, cmpError, hookConfig) { logInfo('Received a response from CMP', tcfData); if (success) { if (tcfData.gdprApplies === false || tcfData.eventStatus === 'tcloaded' || tcfData.eventStatus === 'useractioncomplete') { - cmpSuccess(tcfData, hookConfig); + processCmpData(tcfData, {onSuccess, onError}); } } else { - cmpError('CMP unable to register callback function. Please check CMP setup.', hookConfig); + onError('CMP unable to register callback function. Please check CMP setup.'); } } @@ -115,7 +112,7 @@ function lookupIabConsent(cmpSuccess, cmpError, hookConfig) { function afterEach() { if (cmpResponse.getConsentData && cmpResponse.getVendorConsents) { logInfo('Received all requested responses from CMP', cmpResponse); - cmpSuccess(cmpResponse, hookConfig); + processCmpData(cmpResponse, {onSuccess, onError}); } } @@ -136,7 +133,7 @@ function lookupIabConsent(cmpSuccess, cmpError, hookConfig) { let { cmpFrame, cmpFunction } = findCMP(); if (!cmpFrame) { - return cmpError('CMP not found.', hookConfig); + return onError('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) @@ -183,16 +180,6 @@ function lookupIabConsent(cmpSuccess, cmpError, hookConfig) { } } - // find sizes from adUnits object - let adUnits = hookConfig.adUnits; - let width = 1; - let height = 1; - if (Array.isArray(adUnits) && adUnits.length > 0) { - let sizes = getAdUnitSizes(adUnits[0]); - width = sizes[0][0]; - height = sizes[0][1]; - } - window.$sf.ext.register(width, height, sfCallback); window.$sf.ext.cmp(commandName); } @@ -249,7 +236,7 @@ function lookupIabConsent(cmpSuccess, cmpError, hookConfig) { function readPostMessageResponse(event) { let cmpDataPkgName = `${apiName}Return`; - let json = (typeof event.data === 'string' && strIncludes(event.data, cmpDataPkgName)) ? JSON.parse(event.data) : event.data; + let json = (typeof event.data === 'string' && includes(event.data, cmpDataPkgName)) ? JSON.parse(event.data) : event.data; if (json[cmpDataPkgName] && json[cmpDataPkgName].callId) { let 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 @@ -261,6 +248,70 @@ function lookupIabConsent(cmpSuccess, cmpError, hookConfig) { } } +/** + * Look up consent data and store it in the `consentData` global as well as `adapterManager.js`' gdprDataHandler. + * + * @param cb A callback that takes: a boolean that is true if the auction should be canceled; an error message and extra + * error arguments that will be undefined if there's no error. + * @param width if we are running in an iframe, the TCFv1 spec requires us to use the SafeFrame API to find the CMP - which + * in turn requires width and height. + * @param height see width above + */ +function loadConsentData(cb, width = 1, height = 1) { + let isDone = false; + let timer = null; + + function done(consentData, shouldCancelAuction, errMsg, ...extraArgs) { + if (timer != null) { + clearTimeout(timer); + } + isDone = true; + gdprDataHandler.setConsentData(consentData); + if (cb != null) { + cb(shouldCancelAuction, errMsg, ...extraArgs); + } + } + + if (!includes(Object.keys(cmpCallMap), userCMP)) { + done(null, false, `CMP framework (${userCMP}) is not a supported framework. Aborting consentManagement module and resuming auction.`); + return; + } + + const callbacks = { + onSuccess: (data) => done(data, false), + onError: function (msg, ...extraArgs) { + let consentData = null; + let shouldCancelAuction = true; + if (allowAuction.value && cmpVersion === 1) { + // still set the consentData to undefined when there is a problem as per config options + consentData = storeConsentData(undefined); + shouldCancelAuction = false; + } + done(consentData, shouldCancelAuction, msg, ...extraArgs); + } + } + cmpCallMap[userCMP]({ + width, + height, + ...callbacks + }); + + if (!isDone) { + if (consentTimeout === 0) { + processCmpData(undefined, callbacks); + } else { + timer = setTimeout(function () { + if (cmpVersion === 2) { + // for TCFv2, we allow the auction to continue on timeout + done(storeConsentData(undefined), false, `No response from CMP, continuing auction...`) + } else { + callbacks.onError('CMP workflow exceeded timeout threshold.'); + } + }, consentTimeout); + } + } +} + /** * If consentManagement module is enabled (ie included in setConfig), this hook function will attempt to fetch the * user's encoded consent string from the supported CMP. Once obtained, the module will store this @@ -270,48 +321,60 @@ function lookupIabConsent(cmpSuccess, cmpError, hookConfig) { * @param {function} fn required; The next function in the chain, used by hook.js */ export function requestBidsHook(fn, reqBidsConfigObj) { - // preserves all module related variables for the current auction instance (used primiarily for concurrent auctions) - const hookConfig = { - context: this, - args: [reqBidsConfigObj], - nextFn: fn, - adUnits: reqBidsConfigObj.adUnits || $$PREBID_GLOBAL$$.adUnits, - bidsBackHandler: reqBidsConfigObj.bidsBackHandler, - haveExited: false, - timer: null - }; - - // in case we already have consent (eg during bid refresh) - if (consentData) { - logInfo('User consent information already known. Pulling internally stored information...'); - return exitModule(null, hookConfig); - } + const load = (() => { + if (consentData) { + logInfo('User consent information already known. Pulling internally stored information...'); + return function (cb) { + // eslint-disable-next-line standard/no-callback-literal + cb(false); + } + } else { + // find sizes from adUnits object + let adUnits = reqBidsConfigObj.adUnits || $$PREBID_GLOBAL$$.adUnits; + let width = 1; + let height = 1; + if (Array.isArray(adUnits) && adUnits.length > 0) { + let sizes = getAdUnitSizes(adUnits[0]); + width = sizes[0][0]; + height = sizes[0][1]; + } - if (!includes(Object.keys(cmpCallMap), userCMP)) { - logWarn(`CMP framework (${userCMP}) is not a supported framework. Aborting consentManagement module and resuming auction.`); - return hookConfig.nextFn.apply(hookConfig.context, hookConfig.args); - } + return function (cb) { + loadConsentData(cb, width, height); + } + } + })(); - cmpCallMap[userCMP].call(this, processCmpData, cmpFailed, hookConfig); + load(function (shouldCancelAuction, errMsg, ...extraArgs) { + if (errMsg) { + let log = logWarn; + if (cmpVersion === 1 && !shouldCancelAuction) { + errMsg = `${errMsg} 'allowAuctionWithoutConsent' activated.`; + } else if (shouldCancelAuction) { + log = logError; + errMsg = `${errMsg} Canceling auction as per consentManagement config.`; + } + log(errMsg, ...extraArgs); + } - // only let this code run if module is still active (ie if the callbacks used by CMPs haven't already finished) - if (!hookConfig.haveExited) { - if (consentTimeout === 0) { - processCmpData(undefined, hookConfig); + if (shouldCancelAuction) { + if (typeof reqBidsConfigObj.bidsBackHandler === 'function') { + reqBidsConfigObj.bidsBackHandler(); + } else { + logError('Error executing bidsBackHandler'); + } } else { - hookConfig.timer = setTimeout(cmpTimedOut.bind(null, hookConfig), consentTimeout); + fn.call(this, reqBidsConfigObj); } - } + }); } /** * This function checks the consent data provided by CMP to ensure it's in an expected state. - * If it's bad, we exit the module depending on config settings. - * If it's good, then we store the value and exits the module. - * @param {object} consentObject required; object returned by CMP that contains user's consent choices - * @param {object} hookConfig contains module related variables (see comment in requestBidsHook function) + * If it's bad, we call `onError` + * If it's good, then we store the value and call `onSuccess` */ -function processCmpData(consentObject, hookConfig) { +function processCmpData(consentObject, {onSuccess, onError}) { function checkV1Data(consentObject) { let gdprApplies = consentObject && consentObject.getConsentData && consentObject.getConsentData.gdprApplies; return !!( @@ -347,57 +410,19 @@ function processCmpData(consentObject, hookConfig) { // determine which set of checks to run based on cmpVersion let checkFn = (cmpVersion === 1) ? checkV1Data : (cmpVersion === 2) ? checkV2Data : null; - // Raise deprecation warning if 'allowAuctionWithoutConsent' is used with TCF 2. - if (allowAuction.definedInConfig && cmpVersion === 2) { - logWarn(`'allowAuctionWithoutConsent' ignored for TCF 2`); - } else if (!allowAuction.definedInConfig && cmpVersion === 1) { - logInfo(`'allowAuctionWithoutConsent' using system default: (${DEFAULT_ALLOW_AUCTION_WO_CONSENT}).`); - } - if (isFn(checkFn)) { if (checkFn(consentObject)) { - cmpFailed(`CMP returned unexpected value during lookup process.`, hookConfig, consentObject); + onError(`CMP returned unexpected value during lookup process.`, consentObject); } else { - clearTimeout(hookConfig.timer); - storeConsentData(consentObject); - exitModule(null, hookConfig); + onSuccess(storeConsentData(consentObject)); } } else { - cmpFailed('Unable to derive CMP version to process data. Consent object does not conform to TCF v1 or v2 specs.', hookConfig, consentObject); + onError('Unable to derive CMP version to process data. Consent object does not conform to TCF v1 or v2 specs.', consentObject); } } /** - * General timeout callback when interacting with CMP takes too long. - */ -function cmpTimedOut(hookConfig) { - if (cmpVersion === 2) { - logWarn(`No response from CMP, continuing auction...`) - storeConsentData(undefined); - exitModule(null, hookConfig) - } else { - cmpFailed('CMP workflow exceeded timeout threshold.', hookConfig); - } -} - -/** - * This function contains the controlled steps to perform when there's a problem with CMP. - * @param {string} errMsg required; should be a short descriptive message for why the failure/issue happened. - * @param {object} hookConfig contains module related variables (see comment in requestBidsHook function) - * @param {object} extraArgs contains additional data that's passed along in the error/warning messages for easier debugging -*/ -function cmpFailed(errMsg, hookConfig, extraArgs) { - clearTimeout(hookConfig.timer); - - // still set the consentData to undefined when there is a problem as per config options - if (allowAuction.value && cmpVersion === 1) { - storeConsentData(undefined); - } - exitModule(errMsg, hookConfig, extraArgs); -} - -/** - * Stores CMP data locally in module and then invokes gdprDataHandler.setConsentData() to make information available in adaptermanager.js for later in the auction + * Stores CMP data locally in module to make information available in adaptermanager.js for later in the auction * @param {object} cmpConsentObject required; an object representing user's consent choices (can be undefined in certain use-cases for this function only) */ function storeConsentData(cmpConsentObject) { @@ -418,50 +443,7 @@ function storeConsentData(cmpConsentObject) { }; } consentData.apiVersion = cmpVersion; - gdprDataHandler.setConsentData(consentData); -} - -/** - * This function handles the exit logic for the module. - * While there are several paths in the module's logic to call this function, we only allow 1 of the 3 potential exits to happen before suppressing others. - * - * We prevent multiple exits to avoid conflicting messages in the console depending on certain scenarios. - * One scenario could be auction was canceled due to timeout with CMP being reached. - * While the timeout is the accepted exit and runs first, the CMP's callback still tries to process the user's data (which normally leads to a good exit). - * In this case, the good exit will be suppressed since we already decided to cancel the auction. - * - * Three exit paths are: - * 1. good exit where auction runs (CMP data is processed normally). - * 2. bad exit but auction still continues (warning message is logged, CMP data is undefined and still passed along). - * 3. bad exit with auction canceled (error message is logged). - * @param {string} errMsg optional; only to be used when there was a 'bad' exit. String is a descriptive message for the failure/issue encountered. - * @param {object} hookConfig contains module related variables (see comment in requestBidsHook function) - * @param {object} extraArgs contains additional data that's passed along in the error/warning messages for easier debugging - */ -function exitModule(errMsg, hookConfig, extraArgs) { - if (hookConfig.haveExited === false) { - hookConfig.haveExited = true; - - let context = hookConfig.context; - let args = hookConfig.args; - let nextFn = hookConfig.nextFn; - - if (errMsg) { - if (allowAuction.value && cmpVersion === 1) { - logWarn(errMsg + ` 'allowAuctionWithoutConsent' activated.`, extraArgs); - nextFn.apply(context, args); - } else { - logError(errMsg + ' Canceling auction as per consentManagement config.', extraArgs); - if (typeof hookConfig.bidsBackHandler === 'function') { - hookConfig.bidsBackHandler(); - } else { - logError('Error executing bidsBackHandler'); - } - } - } else { - nextFn.apply(context, args); - } - } + return consentData; } /** @@ -471,7 +453,7 @@ export function resetConsentData() { consentData = undefined; userCMP = undefined; cmpVersion = 0; - gdprDataHandler.setConsentData(null); + gdprDataHandler.reset(); } /** @@ -522,5 +504,14 @@ export function setConsentConfig(config) { $$PREBID_GLOBAL$$.requestBids.before(requestBidsHook, 50); } addedConsentHook = true; + gdprDataHandler.enable(); + loadConsentData(); // immediately look up consent data to make it available without requiring an auction + + // Raise deprecation warning if 'allowAuctionWithoutConsent' is used with TCF 2. + if (allowAuction.definedInConfig && cmpVersion === 2) { + logWarn(`'allowAuctionWithoutConsent' ignored for TCF 2`); + } else if (!allowAuction.definedInConfig && cmpVersion === 1) { + logInfo(`'allowAuctionWithoutConsent' using system default: (${DEFAULT_ALLOW_AUCTION_WO_CONSENT}).`); + } } config.getConfig('consentManagement', config => setConsentConfig(config.consentManagement)); diff --git a/modules/consentManagementUsp.js b/modules/consentManagementUsp.js index 4a4c4ae0a55..e98b41d5c9e 100644 --- a/modules/consentManagementUsp.js +++ b/modules/consentManagementUsp.js @@ -27,23 +27,17 @@ const uspCallMap = { /** * This function reads the consent string from the config to obtain the consent information of the user. - * @param {function(string)} cmpSuccess acts as a success callback when the value is read from config; pass along consentObject (string) from CMP - * @param {function(string)} cmpError acts as an error callback while interacting with the config string; pass along an error message (string) - * @param {object} hookConfig contains module related variables (see comment in requestBidsHook function) */ -function lookupStaticConsentData(cmpSuccess, cmpError, hookConfig) { - cmpSuccess(staticConsentData, hookConfig); +function lookupStaticConsentData({onSuccess}) { + onSuccess(staticConsentData); } /** * This function handles interacting with an USP compliant consent manager to obtain the consent information of the user. * Given the async nature of the USP's API, we pass in acting success/error callback functions to exit this function * based on the appropriate result. - * @param {function(string)} uspSuccess acts as a success callback when USPAPI returns a value; pass along consentObject (string) from USPAPI - * @param {function(string)} uspError acts as an error callback while interacting with USPAPI; pass along an error message (string) - * @param {object} hookConfig contains module related variables (see comment in requestBidsHook function) */ -function lookupUspConsent(uspSuccess, uspError, hookConfig) { +function lookupUspConsent({onSuccess, onError}) { function findUsp() { let f = window; let uspapiFrame; @@ -78,9 +72,9 @@ function lookupUspConsent(uspSuccess, uspError, hookConfig) { function afterEach() { if (uspResponse.usPrivacy) { - uspSuccess(uspResponse, hookConfig); + processUspData(uspResponse, {onSuccess, onError}) } else { - uspError('Unable to get USP consent string.', hookConfig); + onError('Unable to get USP consent string.'); } } @@ -100,7 +94,7 @@ function lookupUspConsent(uspSuccess, uspError, hookConfig) { let { uspapiFrame, uspapiFunction } = findUsp(); if (!uspapiFrame) { - return uspError('USP CMP not found.', hookConfig); + return onError('USP CMP not found.'); } // to collect the consent information from the user, we perform a call to USPAPI @@ -165,119 +159,92 @@ function lookupUspConsent(uspSuccess, uspError, hookConfig) { } /** - * If consentManagementUSP module is enabled (ie included in setConfig), this hook function will attempt to fetch the - * user's encoded consent string from the supported USPAPI. Once obtained, the module will store this - * data as part of a uspConsent object which gets transferred to adapterManager's uspDataHandler object. - * This information is later added into the bidRequest object for any supported adapters to read/pass along to their system. - * @param {object} reqBidsConfigObj required; This is the same param that's used in pbjs.requestBids. - * @param {function} fn required; The next function in the chain, used by hook.js + * Lookup consent data and store it in the `consentData` global as well as `adapterManager.js`' uspDataHanlder. + * + * @param cb a callback that takes an error message and extra error arguments; all args will be undefined if consent + * data was retrieved successfully. */ -export function requestBidsHook(fn, reqBidsConfigObj) { - // preserves all module related variables for the current auction instance (used primiarily for concurrent auctions) - const hookConfig = { - context: this, - args: [reqBidsConfigObj], - nextFn: fn, - adUnits: reqBidsConfigObj.adUnits || $$PREBID_GLOBAL$$.adUnits, - bidsBackHandler: reqBidsConfigObj.bidsBackHandler, - haveExited: false, - timer: null - }; +function loadConsentData(cb) { + let timer = null; + let isDone = false; + + function done(consentData, errMsg, ...extraArgs) { + if (timer != null) { + clearTimeout(timer); + } + isDone = true; + uspDataHandler.setConsentData(consentData); + if (cb != null) { + cb(errMsg, ...extraArgs) + } + } if (!uspCallMap[consentAPI]) { - logWarn(`USP framework (${consentAPI}) is not a supported framework. Aborting consentManagement module and resuming auction.`); - return hookConfig.nextFn.apply(hookConfig.context, hookConfig.args); + done(null, `USP framework (${consentAPI}) is not a supported framework. Aborting consentManagement module and resuming auction.`); + return; + } + + const callbacks = { + onSuccess: done, + onError: function (errMsg, ...extraArgs) { + done(null, `${errMsg} Resuming auction without consent data as per consentManagement config.`, ...extraArgs); + } } - uspCallMap[consentAPI].call(this, processUspData, uspapiFailed, hookConfig); + uspCallMap[consentAPI](callbacks); - // only let this code run if module is still active (ie if the callbacks used by USPs haven't already finished) - if (!hookConfig.haveExited) { + if (!isDone) { if (consentTimeout === 0) { - processUspData(undefined, hookConfig); + processUspData(undefined, callbacks); } else { - hookConfig.timer = setTimeout(uspapiTimeout.bind(null, hookConfig), consentTimeout); + timer = setTimeout(callbacks.onError.bind(null, 'USPAPI workflow exceeded timeout threshold.'), consentTimeout) } } } +/** + * If consentManagementUSP module is enabled (ie included in setConfig), this hook function will attempt to fetch the + * user's encoded consent string from the supported USPAPI. Once obtained, the module will store this + * data as part of a uspConsent object which gets transferred to adapterManager's uspDataHandler object. + * This information is later added into the bidRequest object for any supported adapters to read/pass along to their system. + * @param {object} reqBidsConfigObj required; This is the same param that's used in pbjs.requestBids. + * @param {function} fn required; The next function in the chain, used by hook.js + */ +export function requestBidsHook(fn, reqBidsConfigObj) { + loadConsentData((errMsg, ...extraArgs) => { + if (errMsg != null) { + logWarn(errMsg, ...extraArgs); + } + fn.call(this, reqBidsConfigObj); + }); +} + /** * This function checks the consent data provided by USPAPI to ensure it's in an expected state. * If it's bad, we exit the module depending on config settings. * If it's good, then we store the value and exits the module. * @param {object} consentObject required; object returned by USPAPI that contains user's consent choices - * @param {object} hookConfig contains module related variables (see comment in requestBidsHook function) + * @param {function(string)} onSuccess callback accepting the resolved consent USP consent string + * @param {function(string, ...{}?)} onError callback accepting error message and any extra error arguments (used purely for logging) */ -function processUspData(consentObject, hookConfig) { +function processUspData(consentObject, {onSuccess, onError}) { const valid = !!(consentObject && consentObject.usPrivacy); if (!valid) { - uspapiFailed(`USPAPI returned unexpected value during lookup process.`, hookConfig, consentObject); + onError(`USPAPI returned unexpected value during lookup process.`, consentObject); return; } - clearTimeout(hookConfig.timer); storeUspConsentData(consentObject); - exitModule(null, hookConfig); -} - -/** - * General timeout callback when interacting with USPAPI takes too long. - */ -function uspapiTimeout(hookConfig) { - uspapiFailed('USPAPI workflow exceeded timeout threshold.', hookConfig); -} - -/** - * This function contains the controlled steps to perform when there's a problem with USPAPI. - * @param {string} errMsg required; should be a short descriptive message for why the failure/issue happened. - * @param {object} hookConfig contains module related variables (see comment in requestBidsHook function) - * @param {object} extraArgs contains additional data that's passed along in the error/warning messages for easier debugging -*/ -function uspapiFailed(errMsg, hookConfig, extraArgs) { - clearTimeout(hookConfig.timer); - - exitModule(errMsg, hookConfig, extraArgs); + onSuccess(consentData); } /** * Stores USP data locally in module and then invokes uspDataHandler.setConsentData() to make information available in adaptermanger.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 {object} consentObject required; an object representing user's consent choices (can be undefined in certain use-cases for this function only) */ function storeUspConsentData(consentObject) { if (consentObject && consentObject.usPrivacy) { consentData = consentObject.usPrivacy; - uspDataHandler.setConsentData(consentData); - } -} - -/** - * This function handles the exit logic for the module. - * There are a couple paths in the module's logic to call this function and we only allow 1 of the 2 potential exits to happen before suppressing others. - * - * We prevent multiple exits to avoid conflicting messages in the console depending on certain scenarios. - * One scenario could be auction was canceled due to timeout with USPAPI being reached. - * While the timeout is the accepted exit and runs first, the USP's callback still tries to process the user's data (which normally leads to a good exit). - * In this case, the good exit will be suppressed since we already decided to cancel the auction. - * - * Three exit paths are: - * 1. good exit where auction runs (USPAPI data is processed normally). - * 2. bad exit but auction still continues (warning message is logged, USPAPI data is undefined and still passed along). - * @param {string} errMsg optional; only to be used when there was a 'bad' exit. String is a descriptive message for the failure/issue encountered. - * @param {object} hookConfig contains module related variables (see comment in requestBidsHook function) - * @param {object} extraArgs contains additional data that's passed along in the error/warning messages for easier debugging - */ -function exitModule(errMsg, hookConfig, extraArgs) { - if (hookConfig.haveExited === false) { - hookConfig.haveExited = true; - - let context = hookConfig.context; - let args = hookConfig.args; - let nextFn = hookConfig.nextFn; - - if (errMsg) { - logWarn(errMsg + ' Resuming auction without consent data as per consentManagement config.', extraArgs); - } - nextFn.apply(context, args); } } @@ -287,7 +254,7 @@ function exitModule(errMsg, hookConfig, extraArgs) { export function resetConsentData() { consentData = undefined; consentAPI = undefined; - uspDataHandler.setConsentData(null); + uspDataHandler.reset(); } /** @@ -328,5 +295,7 @@ export function setConsentConfig(config) { $$PREBID_GLOBAL$$.requestBids.before(requestBidsHook, 50); } addedConsentHook = true; + uspDataHandler.enable(); + loadConsentData(); // immediately look up consent data to make it available without requiring an auction } config.getConfig('consentManagement', config => setConsentConfig(config.consentManagement)); diff --git a/modules/consumableBidAdapter.md b/modules/consumableBidAdapter.md index 2189494ebd4..ba472899c49 100644 --- a/modules/consumableBidAdapter.md +++ b/modules/consumableBidAdapter.md @@ -4,7 +4,7 @@ Module Name: Consumable Bid Adapter Module Type: Consumable Adapter -Maintainer: naffis@consumable.com +Maintainer: prebid@consumable.com # Description diff --git a/modules/conversantBidAdapter.js b/modules/conversantBidAdapter.js index 92b5d47277e..7ee8b1b7681 100644 --- a/modules/conversantBidAdapter.js +++ b/modules/conversantBidAdapter.js @@ -2,11 +2,12 @@ import { logWarn, isStr, deepAccess, isArray, getBidIdParameter, deepSetValue, i import {registerBidder} from '../src/adapters/bidderFactory.js'; import {BANNER, VIDEO} from '../src/mediaTypes.js'; import {getStorageManager} from '../src/storageManager.js'; +import { config } from '../src/config.js'; const GVLID = 24; -export const storage = getStorageManager(GVLID); 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'; export const spec = { @@ -76,6 +77,9 @@ export const spec = { displaymanager: 'Prebid.js', displaymanagerver: '$prebid.version$' }; + if (bid.ortb2Imp) { + mergeDeep(imp, bid.ortb2Imp); + } copyOptProperty(bid.params.tag_id, imp, 'tagid'); @@ -132,6 +136,12 @@ export const spec = { 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) { // Add GDPR flag and consent string if (bidderRequest.gdprConsent) { @@ -167,6 +177,9 @@ export const spec = { payload.user = {ext: userExt}; } + const firstPartyData = config.getConfig('ortb2') || {}; + mergeDeep(payload, firstPartyData); + return { method: 'POST', url: bidurl, diff --git a/modules/craftBidAdapter.js b/modules/craftBidAdapter.js index 812ec53d686..61ca4f929e7 100644 --- a/modules/craftBidAdapter.js +++ b/modules/craftBidAdapter.js @@ -1,16 +1,24 @@ -import { logError, convertTypes, convertCamelToUnderscore, isArray, deepAccess, getBidRequest, isEmpty, transformBidderParamKeywords } 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'; -import find from 'core-js-pure/features/array/find.js'; -import includes from 'core-js-pure/features/array/includes.js'; -import { getStorageManager } from '../src/storageManager.js'; +import { + convertCamelToUnderscore, + convertTypes, + deepAccess, + getBidRequest, + isArray, + isEmpty, + logError, + transformBidderParamKeywords +} 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'; +import {find, includes} from '../src/polyfill.js'; +import {getStorageManager} from '../src/storageManager.js'; import {ajax} from '../src/ajax.js'; const BIDDER_CODE = 'craft'; const URL_BASE = 'https://gacraft.jp/prebid-v3'; const TTL = 360; -const storage = getStorageManager(); +const storage = getStorageManager({bidderCode: BIDDER_CODE}); export const spec = { code: BIDDER_CODE, diff --git a/modules/criteoBidAdapter.js b/modules/criteoBidAdapter.js index 4dfb5d38f4c..75d41d970a9 100644 --- a/modules/criteoBidAdapter.js +++ b/modules/criteoBidAdapter.js @@ -1,11 +1,11 @@ -import { isArray, getUniqueIdentifierStr, parseUrl, deepAccess, logWarn, logError, logInfo } from '../src/utils.js'; +import {deepAccess, getUniqueIdentifierStr, isArray, logError, logInfo, logWarn, parseUrl} from '../src/utils.js'; 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 'core-js-pure/features/array/find.js'; -import { verify } from 'criteo-direct-rsa-validate/build/verify.js'; // ref#2 -import { getStorageManager } from '../src/storageManager.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'; const GVLID = 91; export const ADAPTER_VERSION = 34; @@ -13,7 +13,7 @@ const BIDDER_CODE = 'criteo'; const CDB_ENDPOINT = 'https://bidder.criteo.com/cdb'; const PROFILE_ID_INLINE = 207; export const PROFILE_ID_PUBLISHERTAG = 185; -const storage = getStorageManager(GVLID); +const storage = getStorageManager({gvlid: GVLID, bidderCode: BIDDER_CODE}); const LOG_PREFIX = 'Criteo: '; /* @@ -281,6 +281,7 @@ function checkNativeSendId(bidRequest) { */ function buildCdbRequest(context, bidRequests, bidderRequest) { let networkId; + let schain; const request = { publisher: { url: context.url, @@ -288,6 +289,7 @@ function buildCdbRequest(context, bidRequests, bidderRequest) { }, slots: bidRequests.map(bidRequest => { networkId = bidRequest.params.networkId || networkId; + schain = bidRequest.schain || schain; const slot = { impid: bidRequest.adUnitCode, transactionid: bidRequest.transactionId, @@ -310,9 +312,9 @@ function buildCdbRequest(context, bidRequests, bidderRequest) { if (!checkNativeSendId(bidRequest)) { logWarn(LOG_PREFIX + 'all native assets containing URL should be sent as placeholders with sendId(icon, image, clickUrl, displayUrl, privacyLink, privacyIcon)'); } - slot.sizes = parseSizes(retrieveBannerSizes(bidRequest), parseNativeSize); + slot.sizes = parseSizes(deepAccess(bidRequest, 'mediaTypes.banner.sizes'), parseNativeSize); } else { - slot.sizes = parseSizes(retrieveBannerSizes(bidRequest), parseSize); + slot.sizes = parseSizes(deepAccess(bidRequest, 'mediaTypes.banner.sizes'), parseSize); } if (hasVideoMediaType(bidRequest)) { const video = { @@ -344,6 +346,13 @@ function buildCdbRequest(context, bidRequests, bidderRequest) { if (networkId) { request.publisher.networkid = networkId; } + if (schain) { + request.source = { + ext: { + schain: schain + } + } + }; request.user = { ext: bidderRequest.userExt }; @@ -366,11 +375,10 @@ function buildCdbRequest(context, bidRequests, bidderRequest) { return request; } -function retrieveBannerSizes(bidRequest) { - return deepAccess(bidRequest, 'mediaTypes.banner.sizes') || bidRequest.sizes; -} - function parseSizes(sizes, parser) { + if (sizes == undefined) { + return []; + } if (Array.isArray(sizes[0])) { // is there several sizes ? (ie. [[728,90],[200,300]]) return sizes.map(size => parser(size)); } diff --git a/modules/criteoIdSystem.js b/modules/criteoIdSystem.js index ecf7b3aaac4..c73c4422a77 100644 --- a/modules/criteoIdSystem.js +++ b/modules/criteoIdSystem.js @@ -13,7 +13,7 @@ import { getStorageManager } from '../src/storageManager.js'; const gvlid = 91; const bidderCode = 'criteo'; -export const storage = getStorageManager(gvlid, bidderCode); +export const storage = getStorageManager({gvlid: gvlid, moduleName: bidderCode}); const bididStorageKey = 'cto_bidid'; const bundleStorageKey = 'cto_bundle'; diff --git a/modules/currency.js b/modules/currency.js index 5f7add764ad..a59a9880af1 100644 --- a/modules/currency.js +++ b/modules/currency.js @@ -20,6 +20,25 @@ export var currencyRates = {}; var bidderCurrencyDefault = {}; var defaultRates; +export const ready = (() => { + let isDone, resolver, promise; + function reset() { + isDone = false; + resolver = null; + promise = new Promise((resolve) => { + resolver = resolve; + if (isDone) resolve(); + }) + } + function done() { + isDone = true; + if (resolver != null) { resolver() } + } + reset(); + + return {done, reset, promise: () => promise} +})(); + /** * Configuration function for currency * @param {string} [config.adServerCurrency = 'USD'] @@ -138,11 +157,15 @@ function initCurrency(url) { logInfo('currencyRates set to ' + JSON.stringify(currencyRates)); currencyRatesLoaded = true; processBidResponseQueue(); + ready.done(); } catch (e) { errorSettingsRates('Failed to parse currencyRates response: ' + response); } }, - error: errorSettingsRates + error: function (...args) { + errorSettingsRates(...args); + ready.done(); + } } ); } @@ -197,6 +220,8 @@ export function addBidResponseHook(fn, adUnitCode, bid) { bidResponseQueue.push(wrapFunction(fn, this, [adUnitCode, bid])); if (!currencySupportEnabled || currencyRatesLoaded) { processBidResponseQueue(); + } else { + fn.bail(ready.promise()); } } @@ -219,10 +244,7 @@ function wrapFunction(fn, context, params) { } } catch (e) { logWarn('Returning NO_BID, getCurrencyConversion threw error: ', e); - params[1] = createBid(CONSTANTS.STATUS.NO_BID, { - bidder: bid.bidderCode || bid.bidder, - bidId: bid.requestId - }); + params[1] = createBid(CONSTANTS.STATUS.NO_BID, bid.getIdentifiers()); } } return fn.apply(context, params); diff --git a/modules/cwireBidAdapter.js b/modules/cwireBidAdapter.js index c9caa78e5e7..c0a24b49a3c 100644 --- a/modules/cwireBidAdapter.js +++ b/modules/cwireBidAdapter.js @@ -1,22 +1,22 @@ import {registerBidder} from '../src/adapters/bidderFactory.js'; -import { getRefererInfo } from '../src/refererDetection.js'; -import { getStorageManager } from '../src/storageManager.js'; +import {getRefererInfo} from '../src/refererDetection.js'; +import {getStorageManager} from '../src/storageManager.js'; import {BANNER, VIDEO} from '../src/mediaTypes.js'; -import { OUTSTREAM } from '../src/video.js'; +import {OUTSTREAM} from '../src/video.js'; import { - isArray, - isNumber, - generateUUID, - parseSizesInput, deepAccess, + generateUUID, + getBidIdParameter, getParameterByName, getValue, - getBidIdParameter, + isArray, + isNumber, logError, logWarn, + parseSizesInput, } from '../src/utils.js'; -import { Renderer } from '../src/Renderer.js'; -import find from 'core-js-pure/features/array/find.js'; +import {Renderer} from '../src/Renderer.js'; +import {find} from '../src/polyfill.js'; // ------------------------------------ const BIDDER_CODE = 'cwire'; @@ -28,7 +28,7 @@ const LS_CWID_KEY = 'cw_cwid'; const CW_GROUPS_QUERY = 'cwgroups'; const CW_CREATIVE_QUERY = 'cwcreative'; -const storage = getStorageManager(); +const storage = getStorageManager({bidderCode: BIDDER_CODE}); /** * ------------------------------------ @@ -91,14 +91,17 @@ export const mapSlotsData = function(validBidRequests) { const slots = []; validBidRequests.forEach(bid => { const bidObj = {}; + // get testing / debug params + let cwcreative = getValue(bid.params, 'cwcreative'); + let refgroups = getValue(bid.params, 'refgroups'); + let cwapikey = getValue(bid.params, 'cwapikey'); + // get the pacement and page ids let placementId = getValue(bid.params, 'placementId'); let pageId = getValue(bid.params, 'pageId'); - let adUnitElementId = getValue(bid.params, 'adUnitElementId'); // get the rest of the auction/bid/transaction info bidObj.auctionId = getBidIdParameter('auctionId', bid); bidObj.adUnitCode = getBidIdParameter('adUnitCode', bid); - bidObj.adUnitElementId = adUnitElementId; bidObj.bidId = getBidIdParameter('bidId', bid); bidObj.bidderRequestId = getBidIdParameter('bidderRequestId', bid); bidObj.placementId = placementId; @@ -106,6 +109,9 @@ export const mapSlotsData = function(validBidRequests) { bidObj.mediaTypes = getBidIdParameter('mediaTypes', bid); bidObj.transactionId = getBidIdParameter('transactionId', bid); bidObj.sizes = getSlotSizes(bid); + bidObj.cwcreative = cwcreative; + bidObj.refgroups = refgroups; + bidObj.cwapikey = cwapikey; slots.push(bidObj); }); @@ -124,11 +130,6 @@ export const spec = { isBidRequestValid: function(bid) { bid.params = bid.params || {}; - // if ad unit elemt id not provided - use adUnitCode by default - if (!bid.params.adUnitElementId) { - bid.params.adUnitElementId = bid.code; - } - if (!bid.params.placementId || !isNumber(bid.params.placementId)) { logError('placementId not provided or invalid'); return false; @@ -142,6 +143,21 @@ export const spec = { return true; }, + /** + * ------------------------------------ + * itterate trough slots array and try + * to extract first occurence of a given + * key, if not found - return null + * ------------------------------------ + */ + getFirstValueOrNull: function(slots, key) { + const found = slots.find((item) => { + return (typeof item[key] !== 'undefined'); + }); + + return (found) ? found[key] : null; + }, + /** * ------------------------------------ * Make a server request from the @@ -162,9 +178,19 @@ export const spec = { let refgroups = []; - const cwCreativeId = getQueryVariable(CW_CREATIVE_QUERY); + const cwCreativeId = parseInt(getQueryVariable(CW_CREATIVE_QUERY), 10) || null; + const cwCreativeIdFromConfig = this.getFirstValueOrNull(slots, 'cwcreative'); + const refGroupsFromConfig = this.getFirstValueOrNull(slots, 'refgroups'); + const cwApiKeyFromConfig = this.getFirstValueOrNull(slots, 'cwapikey'); const rgQuery = getQueryVariable(CW_GROUPS_QUERY); + + if (refGroupsFromConfig !== null) { + refgroups = refGroupsFromConfig.split(','); + } + if (rgQuery !== null) { + // override if query param is present + refgroups = []; refgroups = rgQuery.split(','); } @@ -173,8 +199,9 @@ export const spec = { const payload = { cwid: localStorageCWID, refgroups, - cwcreative: cwCreativeId, + cwcreative: cwCreativeId || cwCreativeIdFromConfig, slots: slots, + cwapikey: cwApiKeyFromConfig, httpRef: referer || '', pageViewId: CW_PAGE_VIEW_ID, }; diff --git a/modules/cwireBidAdapter.md b/modules/cwireBidAdapter.md index fc0889e05ad..b42c7a02489 100644 --- a/modules/cwireBidAdapter.md +++ b/modules/cwireBidAdapter.md @@ -2,7 +2,7 @@ Module Name: C-WIRE Bid Adapter Module Type: Adagio Adapter -Maintainer: dragan@cwire.ch +Maintainer: publishers@cwire.ch ## Description @@ -17,7 +17,10 @@ Below, the list of C-WIRE params and where they can be set. | ---------- | ------------- | ------------- | ---- | ---------| | pageId | | x | number | YES | | placementId | | x | number | YES | -| adUnitElementId | | x | string | NO | +| refgroups | | x | string | NO | +| cwcreative | | x | integer | NO | +| cwapikey | | x | string | NO | + ### adUnit configuration @@ -35,9 +38,11 @@ var adUnits = [ params: { pageId: 1422, // required - number placementId: 2211521, // required - number - adUnitElementId: 'other_div', // optional, div id to write to, if not set it will default to ad unit code + cwcreative: 42, // optional - id of creative to force + refgroups: 'test-user', // optional - name of group or coma separated list of groups to force + cwapikey: 'api_key_xyz', // optional - api key for integration testing } }] } ]; -``` \ No newline at end of file +``` diff --git a/modules/dacIdSystem.js b/modules/dacIdSystem.js new file mode 100644 index 00000000000..73b5c7420cf --- /dev/null +++ b/modules/dacIdSystem.js @@ -0,0 +1,57 @@ +/** + * This module adds dacId to the User ID module + * The {@link module:modules/userId} module is required + * @module modules/dacIdSystem + * @requires module:modules/userId + */ + +import { submodule } from '../src/hook.js'; +import { getStorageManager } from '../src/storageManager.js'; + +export const storage = getStorageManager(); + +export const cookieKey = '_a1_f'; + +export const dacIdSystemSubmodule = { + /** + * used to link submodule with config + * @type {string} + */ + name: 'dacId', + + /** + * performs action to obtain id + * @function + * @returns { {id: {dacId: string}} | undefined } + */ + getId: function() { + const newId = storage.getCookie(cookieKey); + if (!newId) { + return undefined; + } + const result = { + dacId: newId + } + return {id: result}; + }, + + /** + * decode the stored id value for passing to bid requests + * @function + * @param { {dacId: string} } value + * @returns { {dacId: {id: string} } | undefined } + */ + decode: function(value) { + if (value && typeof value === 'object') { + const result = {}; + if (value.dacId) { + result.id = value.dacId + } + return {dacId: result}; + } + return undefined; + }, + +} + +submodule('userId', dacIdSystemSubmodule); diff --git a/modules/dacIdSystem.md b/modules/dacIdSystem.md new file mode 100644 index 00000000000..b422d0a536d --- /dev/null +++ b/modules/dacIdSystem.md @@ -0,0 +1,28 @@ +## DAC User ID Submodule + +DAC ID, provided by [D.A.Consortium Inc.](https://www.dac.co.jp/), is ID for ad targeting by using 1st party cookie. +Please contact D.A.Consortium Inc. before using this ID. + +## Building Prebid with DAC ID Support + +First, make sure to add the DAC ID submodule to your Prebid.js package with: + +``` +gulp build --modules=dacIdSystem +``` + +The following configuration parameters are available: + +```javascript +pbjs.setConfig({ + userSync: { + userIds: [{ + name: 'dacId' + }] + } +}); +``` + +| Param under userSync.userIds[] | Scope | Type | Description | Example | +| --- | --- | --- | --- | --- | +| name | Required | String | The name of this module. | `"dacId"` | diff --git a/modules/dailyhuntBidAdapter.js b/modules/dailyhuntBidAdapter.js index cdcc9f1d038..ffa84ff88fd 100644 --- a/modules/dailyhuntBidAdapter.js +++ b/modules/dailyhuntBidAdapter.js @@ -1,9 +1,9 @@ -import { registerBidder } from '../src/adapters/bidderFactory.js'; +import {registerBidder} from '../src/adapters/bidderFactory.js'; import * as mediaTypes from '../src/mediaTypes.js'; -import {deepAccess, _map, isEmpty} from '../src/utils.js'; -import { ajax } from '../src/ajax.js'; -import find from 'core-js-pure/features/array/find.js'; -import { OUTSTREAM, INSTREAM } from '../src/video.js'; +import {_map, deepAccess, isEmpty} from '../src/utils.js'; +import {ajax} from '../src/ajax.js'; +import {find} from '../src/polyfill.js'; +import {INSTREAM, OUTSTREAM} from '../src/video.js'; const BIDDER_CODE = 'dailyhunt'; const BIDDER_ALIAS = 'dh'; diff --git a/modules/datablocksBidAdapter.js b/modules/datablocksBidAdapter.js index 43039e070c3..b240db1dd25 100644 --- a/modules/datablocksBidAdapter.js +++ b/modules/datablocksBidAdapter.js @@ -4,7 +4,7 @@ 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'; -export const storage = getStorageManager(); +export const storage = getStorageManager({bidderCode: 'datablocks'}); const NATIVE_ID_MAP = {}; const NATIVE_PARAMS = { diff --git a/modules/dchain.js b/modules/dchain.js index 6a1bd1ebf70..fbe78fc5c86 100644 --- a/modules/dchain.js +++ b/modules/dchain.js @@ -1,7 +1,7 @@ -import includes from 'core-js-pure/features/array/includes.js'; -import { config } from '../src/config.js'; -import { getHook } from '../src/hook.js'; -import { _each, isStr, isArray, isPlainObject, hasOwn, deepClone, deepAccess, logWarn, logError } from '../src/utils.js'; +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'; const shouldBeAString = ' should be a string'; const shouldBeAnObject = ' should be an object'; diff --git a/modules/debugging/bidInterceptor.js b/modules/debugging/bidInterceptor.js new file mode 100644 index 00000000000..2a179641424 --- /dev/null +++ b/modules/debugging/bidInterceptor.js @@ -0,0 +1,229 @@ +import { + deepAccess, + deepClone, + deepEqual, + delayExecution, + prefixLog, + mergeDeep +} from '../../src/utils.js'; +const { logMessage, logWarn, logError } = prefixLog('DEBUG:'); + +/** + * @typedef {Number|String|boolean|null|undefined} Scalar + */ + +export function BidInterceptor(opts = {}) { + ({setTimeout: this.setTimeout = window.setTimeout.bind(window)} = opts); + this.rules = []; +} + +Object.assign(BidInterceptor.prototype, { + DEFAULT_RULE_OPTIONS: { + delay: 0 + }, + serializeConfig(ruleDefs) { + function isSerializable(ruleDef, i) { + const serializable = deepEqual(ruleDef, JSON.parse(JSON.stringify(ruleDef)), {checkTypes: true}); + if (!serializable && !deepAccess(ruleDef, 'options.suppressWarnings')) { + logWarn(`Bid interceptor rule definition #${i + 1} is not serializable and will be lost after a refresh. Rule definition: `, ruleDef); + } + return serializable; + } + return ruleDefs.filter(isSerializable); + }, + updateConfig(config) { + this.rules = (config.intercept || []).map((ruleDef, i) => this.rule(ruleDef, i + 1)) + }, + /** + * @typedef {Object} RuleOptions + * @property {Number} [delay=0] delay between bid interception and mocking of response (to simulate network delay) + * @property {boolean} [suppressWarnings=false] if enabled, do not warn about unserializable rules + * + * @typedef {Object} Rule + * @property {Number} no rule number (used only as an identifier for logging) + * @property {function({}, {}): boolean} match a predicate function that tests a bid against this rule + * @property {ReplacerFn} replacer generator function for mock bid responses + * @property {RuleOptions} options + */ + + /** + * @param {{}} ruleDef + * @param {Number} ruleNo + * @returns {Rule} + */ + rule(ruleDef, ruleNo) { + return { + no: ruleNo, + match: this.matcher(ruleDef.when, ruleNo), + replace: this.replacer(ruleDef.then || {}, ruleNo), + options: Object.assign({}, this.DEFAULT_RULE_OPTIONS, ruleDef.options), + } + }, + /** + * @typedef {Function} MatchPredicate + * @param {*} candidate a bid to match, or a portion of it if used inside an ObjectMather. + * e.g. matcher((bid, bidRequest) => ....) or matcher({property: (property, bidRequest) => ...}) + * @param {BidRequest} bidRequest the request `candidate` belongs to + * @returns {boolean} + * + * @typedef {{[key]: Scalar|RegExp|MatchPredicate|ObjectMatcher}} ObjectMatcher + */ + + /** + * @param {MatchPredicate|ObjectMatcher} matchDef matcher definition + * @param {Number} ruleNo + * @returns {MatchPredicate} a predicate function that matches a bid against the given `matchDef` + */ + matcher(matchDef, ruleNo) { + if (typeof matchDef === 'function') { + return matchDef; + } + if (typeof matchDef !== 'object') { + logError(`Invalid 'when' definition for debug bid interceptor (in rule #${ruleNo})`); + return () => false; + } + function matches(candidate, {ref = matchDef, args = []}) { + return Object.entries(ref).map(([key, val]) => { + const cVal = candidate[key]; + if (val instanceof RegExp) { + return val.exec(cVal) != null; + } + if (typeof val === 'function') { + return !!val(cVal, ...args); + } + if (typeof val === 'object') { + return matches(cVal, {ref: val, args}); + } + return cVal === val; + }).every((i) => i); + } + return (candidate, ...args) => matches(candidate, {args}); + }, + /** + * @typedef {Function} ReplacerFn + * @param {*} bid a bid that was intercepted + * @param {BidRequest} bidRequest the request `bid` belongs to + * @returns {*} the response to mock for `bid`, or a portion of it if used inside an ObjectReplacer. + * e.g. replacer((bid, bidRequest) => mockResponse) or replacer({property: (bid, bidRequest) => mockProperty}) + * + * @typedef {{[key]: ReplacerFn|ObjectReplacer|*}} ObjectReplacer + */ + + /** + * @param {ReplacerFn|ObjectReplacer} replDef replacer definition + * @param ruleNo + * @return {ReplacerFn} + */ + replacer(replDef, ruleNo) { + let replFn; + if (typeof replDef === 'function') { + replFn = ({args}) => replDef(...args); + } else if (typeof replDef !== 'object') { + logError(`Invalid 'then' definition for debug bid interceptor (in rule #${ruleNo})`); + replFn = () => ({}); + } else { + replFn = ({args, ref = replDef}) => { + const result = {}; + Object.entries(ref).forEach(([key, val]) => { + if (typeof val === 'function') { + result[key] = val(...args); + } else if (typeof val === 'object') { + result[key] = replFn({args, ref: val}) + } else { + result[key] = val; + } + }); + return result; + } + } + return (bid, ...args) => { + const response = this.responseDefaults(bid); + mergeDeep(response, replFn({args: [bid, ...args]})); + if (!response.ad) { + response.ad = this.defaultAd(bid, response); + } + response.isDebug = true; + return response; + } + }, + responseDefaults(bid) { + return { + requestId: bid.bidId, + cpm: 3.5764, + currency: 'EUR', + width: 300, + height: 250, + ttl: 360, + creativeId: 'mock-creative-id', + netRevenue: false, + meta: {} + }; + }, + defaultAd(bid, bidResponse) { + return ``; + }, + /** + * Match a candidate bid against all registered rules. + * + * @param {{}} candidate + * @param args + * @returns {Rule|undefined} the first matching rule, or undefined if no match was found. + */ + match(candidate, ...args) { + return this.rules.find((rule) => rule.match(candidate, ...args)); + }, + /** + * Match a set of bids against all registered rules. + * + * @param bids + * @param bidRequest + * @returns {[{bid: *, rule: Rule}[], *[]]} a 2-tuple for matching bids (decorated with the matching rule) and + * non-matching bids. + */ + matchAll(bids, bidRequest) { + const [matches, remainder] = [[], []]; + bids.forEach((bid) => { + const rule = this.match(bid, bidRequest); + if (rule != null) { + matches.push({rule: rule, bid: bid}); + } else { + remainder.push(bid); + } + }) + return [matches, remainder]; + }, + /** + * Run a set of bids against all registered rules, filter out those that match, + * and generate mock responses for them. + * + * @param {{}[]} bids? + * @param {BidRequest} bidRequest + * @param {function(*)} addBid called once for each mock response + * @param {function()} done called once after all mock responses have been run through `addBid` + * @returns {{bids: {}[], bidRequest: {}} remaining bids that did not match any rule (this applies also to + * bidRequest.bids) + */ + intercept({bids, bidRequest, addBid, done}) { + if (bids == null) { + bids = bidRequest.bids; + } + const [matches, remainder] = this.matchAll(bids, bidRequest); + if (matches.length > 0) { + const callDone = delayExecution(done, matches.length); + matches.forEach((match) => { + const mockResponse = match.rule.replace(match.bid, bidRequest); + const delay = match.rule.options.delay; + logMessage(`Intercepted bid request (matching rule #${match.rule.no}), mocking response in ${delay}ms. Request, response:`, match.bid, mockResponse) + this.setTimeout(() => { + addBid(mockResponse, match.bid); + callDone(); + }, delay) + }); + bidRequest = deepClone(bidRequest); + bids = bidRequest.bids = remainder; + } else { + this.setTimeout(done, 0); + } + return {bids, bidRequest}; + } +}); diff --git a/modules/debugging/index.js b/modules/debugging/index.js new file mode 100644 index 00000000000..72692c3fc98 --- /dev/null +++ b/modules/debugging/index.js @@ -0,0 +1,62 @@ +import {deepClone, delayExecution} from '../../src/utils.js'; +import {processBidderRequests} from '../../src/adapters/bidderFactory.js'; +import {BidInterceptor} from './bidInterceptor.js'; +import {hook} from '../../src/hook.js'; +import {pbsBidInterceptor} from './pbsInterceptor.js'; +import { + onDisableOverrides, + onEnableOverrides, + saveDebuggingConfig +} from '../../src/debugging.js'; + +const interceptorHooks = []; +const bidInterceptor = new BidInterceptor(); + +saveDebuggingConfig.before(function (next, debugConfig, ...args) { + if (debugConfig.intercept) { + debugConfig = deepClone(debugConfig); + debugConfig.intercept = bidInterceptor.serializeConfig(debugConfig.intercept); + } + next(debugConfig, ...args); +}); + +function resetHooks(enable) { + interceptorHooks.forEach(([getHookFn, interceptor]) => { + getHookFn().getHooks({hook: interceptor}).remove(); + }); + if (enable) { + interceptorHooks.forEach(([getHookFn, interceptor]) => { + getHookFn().before(interceptor); + }) + } +} + +onEnableOverrides.push((overrides) => { + bidInterceptor.updateConfig(overrides); + resetHooks(true); +}); + +onDisableOverrides.push(() => { + bidInterceptor.updateConfig({}); + resetHooks(false); +}) + +function registerBidInterceptor(getHookFn, interceptor) { + const interceptBids = (...args) => bidInterceptor.intercept(...args); + interceptorHooks.push([getHookFn, function (next, ...args) { + interceptor(next, interceptBids, ...args) + }]); +} + +export function bidderBidInterceptor(next, interceptBids, spec, bids, bidRequest, ajax, wrapCallback, cbs) { + const done = delayExecution(cbs.onCompletion, 2); + ({bids, bidRequest} = interceptBids({bids, bidRequest, addBid: cbs.onBid, done})); + if (bids.length === 0) { + done(); + } else { + next(spec, bids, bidRequest, ajax, wrapCallback, {...cbs, onCompletion: done}); + } +} + +registerBidInterceptor(() => processBidderRequests, bidderBidInterceptor); +registerBidInterceptor(() => hook.get('processPBSRequest'), pbsBidInterceptor); diff --git a/modules/debugging/pbsInterceptor.js b/modules/debugging/pbsInterceptor.js new file mode 100644 index 00000000000..5af2384cad9 --- /dev/null +++ b/modules/debugging/pbsInterceptor.js @@ -0,0 +1,38 @@ +import {deepClone, delayExecution} from '../../src/utils.js'; +import {createBid} from '../../src/bidfactory.js'; +import * as CONSTANTS from '../../src/constants.json'; + +export function pbsBidInterceptor (next, interceptBids, s2sBidRequest, bidRequests, ajax, { + onResponse, + onError, + onBid +}) { + let responseArgs; + const done = delayExecution(() => onResponse(...responseArgs), bidRequests.length + 1) + function signalResponse(...args) { + responseArgs = args; + done(); + } + function addBid(bid, bidRequest) { + onBid({ + adUnit: bidRequest.adUnitCode, + bid: Object.assign(createBid(CONSTANTS.STATUS.GOOD, bidRequest), bid) + }) + } + bidRequests = bidRequests + .map((req) => interceptBids({bidRequest: req, addBid, done}).bidRequest) + .filter((req) => req.bids.length > 0) + + if (bidRequests.length > 0) { + const bidIds = new Set(); + bidRequests.forEach((req) => req.bids.forEach((bid) => bidIds.add(bid.bidId))); + s2sBidRequest = deepClone(s2sBidRequest); + s2sBidRequest.ad_units.forEach((unit) => { + unit.bids = unit.bids.filter((bid) => bidIds.has(bid.bid_id)); + }) + s2sBidRequest.ad_units = s2sBidRequest.ad_units.filter((unit) => unit.bids.length > 0); + next(s2sBidRequest, bidRequests, ajax, {onResponse: signalResponse, onError, onBid}); + } else { + signalResponse(true, []); + } +} diff --git a/modules/deepintentDpesIdSystem.js b/modules/deepintentDpesIdSystem.js index 375c8c07ed1..43c7af1b3cc 100644 --- a/modules/deepintentDpesIdSystem.js +++ b/modules/deepintentDpesIdSystem.js @@ -9,7 +9,7 @@ import { submodule } from '../src/hook.js'; import { getStorageManager } from '../src/storageManager.js'; const MODULE_NAME = 'deepintentId'; -export const storage = getStorageManager(null, MODULE_NAME); +export const storage = getStorageManager({gvlid: null, moduleName: MODULE_NAME}); /** @type {Submodule} */ export const deepintentDpesSubmodule = { diff --git a/modules/dfpAdServerVideo.js b/modules/dfpAdServerVideo.js index 79cb03ec001..7f8ad3351fa 100644 --- a/modules/dfpAdServerVideo.js +++ b/modules/dfpAdServerVideo.js @@ -9,7 +9,7 @@ import { config } from '../src/config.js'; import { getHook, submodule } from '../src/hook.js'; import { auctionManager } from '../src/auctionManager.js'; import { gdprDataHandler, uspDataHandler } from '../src/adapterManager.js'; -import events from '../src/events.js'; +import * as events from '../src/events.js'; import CONSTANTS from '../src/constants.json'; /** @@ -88,7 +88,14 @@ export function buildDfpVideoUrl(options) { sz: parseSizesInput(deepAccess(adUnit, 'mediaTypes.video.playerSize')).join('|'), url: encodeURIComponent(location.href), }; - const encodedCustomParams = getCustParams(bid, options); + + const urlSearchComponent = urlComponents.search; + const urlSzParam = urlSearchComponent && urlSearchComponent.sz + if (urlSzParam) { + derivedParams.sz = urlSzParam + '|' + derivedParams.sz; + } + + let encodedCustomParams = getCustParams(bid, options, urlSearchComponent && urlSearchComponent.cust_params); const queryParams = Object.assign({}, defaultParamConstants, @@ -111,12 +118,11 @@ export function buildDfpVideoUrl(options) { const uspConsent = uspDataHandler.getConsentData(); if (uspConsent) { queryParams.us_privacy = uspConsent; } - return buildUrl({ + return buildUrl(Object.assign({ protocol: 'https', host: 'securepubads.g.doubleclick.net', - pathname: '/gampad/ads', - search: queryParams - }); + pathname: '/gampad/ads' + }, urlComponents, { search: queryParams })); } export function notifyTranslationModule(fn) { @@ -227,9 +233,7 @@ function buildUrlFromAdserverUrlComponents(components, bid, options) { const descriptionUrl = getDescriptionUrl(bid, components, 'search'); if (descriptionUrl) { components.search.description_url = descriptionUrl; } - const encodedCustomParams = getCustParams(bid, options); - components.search.cust_params = (components.search.cust_params) ? components.search.cust_params + '%26' + encodedCustomParams : encodedCustomParams; - + components.search.cust_params = getCustParams(bid, options, components.search.cust_params); return buildUrl(components); } @@ -258,7 +262,7 @@ function getDescriptionUrl(bid, components, prop) { * @param {Object} options this is the options passed in from the `buildDfpVideoUrl` function * @return {Object} Encoded key value pairs for cust_params */ -function getCustParams(bid, options) { +function getCustParams(bid, options, urlCustParams) { const adserverTargeting = (bid && bid.adserverTargeting) || {}; let allTargetingData = {}; @@ -281,7 +285,12 @@ function getCustParams(bid, options) { // merge the prebid + publisher targeting sets const publisherTargetingSet = deepAccess(options, 'params.cust_params'); const targetingSet = Object.assign({}, prebidTargetingSet, publisherTargetingSet); - return encodeURIComponent(formatQS(targetingSet)); + let encodedParams = encodeURIComponent(formatQS(targetingSet)); + if (urlCustParams) { + encodedParams = urlCustParams + '%26' + encodedParams; + } + + return encodedParams; } registerVideoSupport('dfp', { diff --git a/modules/displayioBidAdapter.js b/modules/displayioBidAdapter.js new file mode 100644 index 00000000000..55a2f4a8604 --- /dev/null +++ b/modules/displayioBidAdapter.js @@ -0,0 +1,157 @@ +import {registerBidder} from '../src/adapters/bidderFactory.js'; +import {BANNER, VIDEO} from '../src/mediaTypes.js'; + +const BIDDER_VERSION = '1.0.0'; +const BIDDER_CODE = 'displayio'; +const GVLID = 999; +const BID_TTL = 300; +const SUPPORTED_AD_TYPES = [BANNER, VIDEO]; +const DEFAULT_CURRENCY = 'USD'; + +export const spec = { + code: BIDDER_CODE, + gvlid: GVLID, + supportedMediaTypes: SUPPORTED_AD_TYPES, + isBidRequestValid: function(bid) { + return !!(bid.params && bid.params.placementId && bid.params.siteId && + bid.params.adsSrvDomain && bid.params.cdnDomain); + }, + buildRequests: function (bidRequests, bidderRequest) { + return bidRequests.map(bid => { + let url = '//' + bid.params.adsSrvDomain + '/srv?method=getPlacement&app=' + + bid.params.siteId + '&placement=' + bid.params.placementId; + const data = this._getPayload(bid, bidderRequest); + return { + method: 'POST', + headers: {'Content-Type': 'application/json;charset=utf-8'}, + url, + data + }; + }); + }, + interpretResponse: function (serverResponse, serverRequest) { + const ads = serverResponse.body.data.ads; + const bidResponses = []; + const { data } = serverRequest.data; + if (ads.length) { + const adData = ads[0].ad.data; + const bidResponse = { + requestId: data.id, + cpm: adData.ecpm, + width: adData.w, + height: adData.h, + netRevenue: true, + ttl: BID_TTL, + creativeId: adData.adId || 0, + currency: DEFAULT_CURRENCY, + referrer: data.data.ref, + mediaType: ads[0].ad.subtype, + ad: adData.markup, + placement: data.placement, + adData: adData + }; + if (bidResponse.vastUrl === 'videoVast') { + bidResponse.vastUrl = adData.videos[0].url + } + bidResponses.push(bidResponse); + } + return bidResponses; + }, + _getPayload: function (bid, bidderRequest) { + const connection = navigator.connection || navigator.mozConnection || navigator.webkitConnection; + const userSession = 'us_web_xxxxxxxxxxxx'.replace(/[x]/g, c => { + let r = Math.random() * 16 | 0; + let v = c === 'x' ? r : (r & 0x3 | 0x8); + return v.toString(16); + }); + const { params } = bid; + const { siteId, placementId } = params; + const { refererInfo, uspConsent, gdprConsent } = bidderRequest; + const mediation = {consent: '-1', gdpr: '-1'}; + if (gdprConsent) { + if (gdprConsent.consentString !== undefined) { + mediation.consent = gdprConsent.consentString; + } + if (gdprConsent.gdprApplies !== undefined) { + mediation.gdpr = gdprConsent.gdprApplies ? '1' : '0'; + } + } + const payload = { + userSession, + data: { + id: bid.bidId, + action: 'getPlacement', + app: siteId, + placement: placementId, + data: { + pagecat: params.pageCategory ? params.pageCategory.split(',').map(k => k.trim()) : [], + keywords: params.keywords ? params.keywords.split(',').map(k => k.trim()) : [], + lang_content: document.documentElement.lang, + lang: window.navigator.language, + domain: window.location.hostname, + page: window.location.href, + ref: refererInfo.referer, + userids: _getUserIDs(), + geo: '', + }, + complianceData: { + child: '-1', + us_privacy: uspConsent, + dnt: window.navigator.doNotTrack, + iabConsent: {}, + mediation: { + consent: mediation.consent, + gdpr: mediation.gdpr, + } + }, + integration: 'JS', + omidpn: 'Displayio', + mediationPlatform: 0, + prebidVersion: BIDDER_VERSION, + device: { + w: window.screen.width, + h: window.screen.height, + connection_type: connection ? connection.effectiveType : '', + } + } + } + if (navigator.permissions) { + navigator.permissions.query({ name: 'geolocation' }) + .then((result) => { + if (result.state === 'granted') { + payload.data.data.geo = _getGeoData(); + } + }); + } + return payload + } +}; + +function _getUserIDs () { + let ids = {}; + try { + ids = window.owpbjs.getUserIdsAsEids(); + } catch (e) {} + return ids; +} + +async function _getGeoData () { + let geoData = null; + const getCurrentPosition = () => { + return new Promise((resolve, reject) => + navigator.geolocation.getCurrentPosition(resolve, reject) + ); + } + try { + const position = await getCurrentPosition(); + let {latitude, longitude, accuracy} = position.coords; + geoData = { + 'lat': latitude, + 'lng': longitude, + 'precision': accuracy + }; + } catch (e) {} + return geoData +} + +registerBidder(spec); diff --git a/modules/displayioBidAdapter.md b/modules/displayioBidAdapter.md new file mode 100644 index 00000000000..41505ee966e --- /dev/null +++ b/modules/displayioBidAdapter.md @@ -0,0 +1,148 @@ +# Overview + +``` +Module Name: DisplayIO Bidder Adapter +Module Type: Bidder Adapter +``` + +# Description + +Module that connects to display.io's demand sources. +Web mobile (not relevant for web desktop). + + +#Features +| Feature | | Feature | | +|---------------|---------------------------------------------------------|-----------------------|-----| +| Bidder Code | displayio | Prebid member | no | +| Media Types | Banner, video.
Sizes (display 320x480 / vertical video) | GVL ID | no | +| GDPR Support | yes | Prebid.js Adapter | yes | +| USP Support | yes | Prebid Server Adapter | no | + + +#Global configuration +```javascript + + + +`; - bid.mediaType = BANNER; - } - // Common properties - bid.cpm = parseFloat(bidObject.price); - bid.creativeId = bidObject.crid; - bid.currency = bidObject.currency ? bidObject.currency.toUpperCase() : 'USD'; - - // Deal ID. Composite ads can have multiple line items and the ID of the first - // dealID line item will be used. - if (isNumber(bidObject.lid) && bidObject.buying_type && bidObject.buying_type !== 'rtb') { - bid.dealId = bidObject.lid; - } else if (Array.isArray(bidObject.lid) && - Array.isArray(bidObject.buying_type) && - bidObject.lid.length === bidObject.buying_type.length) { - let isDeal = false; - bidObject.buying_type.forEach((bt, i) => { - if (isDeal) return; - if (bt && bt !== 'rtb') { - isDeal = true; - bid.dealId = bidObject.lid[i]; - } - }); - } + serverResponse.body.seatbid.forEach(seatbid => { + if (!Array.isArray(seatbid.bid)) return; - bid.height = bidObject.h; - bid.netRevenue = bidObject.isNet ? bidObject.isNet : false; - bid.requestId = bidObject.id; - bid.ttl = 300; - bid.width = bidObject.w; + seatbid.bid.forEach(bidObject => { + if (!bidObject.adm || !bidObject.price || bidObject.hasOwnProperty('errorCode')) { + return; + } + const bidRequest = getBidRequest(bidObject.impid, [bidderRequest]); + const idExt = deepAccess(bidObject, `ext.${BIDDER_CODE}`); + + const bid = { + requestId: bidObject.impid, + cpm: bidObject.price, + creativeId: bidObject.crid, + currency: serverResponse.body.cur.toUpperCase() || 'USD', + dealId: (typeof idExt.buying_type === 'string' && idExt.buying_type !== 'rtb') ? idExt.line_item_id : undefined, + meta: { + advertiserDomains: bidObject.adomain ? bidObject.adomain : [] + }, + netRevenue: idExt.is_net || false, + ttl: CREATIVE_TTL + } - if (!bid.width || !bid.height) { - bid.width = 1; - bid.height = 1; - } + ID_RESPONSE.buildAd(bid, bidRequest, bidObject); - if (bidObject.adomain) { - bid.meta = { - advertiserDomains: bidObject.adomain - }; - } + ID_RAZR.addBidData({ + bidRequest, + bid + }); - bids.push(bid); + bids.push(bid); + }); }); + return bids; }, @@ -208,17 +206,14 @@ export const spec = { * @param {ServerResponse[]} serverResponses List of server's responses. * @return {UserSync[]} The user syncs which should be dropped. */ - getUserSyncs: function(syncOptions, serverResponses) { + getUserSyncs(syncOptions, serverResponses) { if (syncOptions.pixelEnabled) { const syncs = []; serverResponses.forEach(response => { - response.body.bid.forEach(bidObject => { - if (isArray(bidObject.sync)) { - bidObject.sync.forEach(syncElement => { - if (syncs.indexOf(syncElement) === -1) { - syncs.push(syncElement); - } - }); + const syncArr = deepAccess(response, `body.ext.${BIDDER_CODE}.sync`, []); + syncArr.forEach(syncElement => { + if (syncs.indexOf(syncElement) === -1) { + syncs.push(syncElement); } }); }); @@ -228,525 +223,374 @@ export const spec = { } }; -function isInstreamVideo(bid) { - const mediaTypes = Object.keys(deepAccess(bid, 'mediaTypes', {})); - const videoMediaType = deepAccess(bid, 'mediaTypes.video'); - const context = deepAccess(bid, 'mediaTypes.video.context'); - return bid.mediaType === 'video' || (mediaTypes.length === 1 && videoMediaType && context !== 'outstream'); -} - -function isOutstreamVideo(bid) { - const videoMediaType = deepAccess(bid, 'mediaTypes.video'); - const context = deepAccess(bid, 'mediaTypes.video.context'); - return videoMediaType && context === 'outstream'; -} - -function getVideoTargetingParams(bid) { - const result = {}; - Object.keys(Object(bid.mediaTypes.video)) - .filter(key => includes(VIDEO_TARGETING, key)) - .forEach(key => { - result[ key ] = bid.mediaTypes.video[ key ]; - }); - Object.keys(Object(bid.params.video)) - .filter(key => includes(VIDEO_TARGETING, key)) - .forEach(key => { - result[ key ] = bid.params.video[ key ]; - }); - return result; -} +registerBidder(spec); -function getBidFloor(bid) { - if (!isFn(bid.getFloor)) { - return null; - } - const floor = bid.getFloor({ - currency: 'USD', - mediaType: '*', - size: '*' - }); - if (isPlainObject(floor) && !isNaN(floor.floor) && floor.currency === 'USD') { - return floor.floor; - } - return null; -} - -function outstreamRender(bid) { - bid.renderer.push(() => { - window.ANOutstreamVideo.renderAd({ - sizes: [bid.width, bid.height], - targetId: bid.adUnitCode, - adResponse: bid.adResponse, - rendererOptions: bid.renderer.getConfig() - }, handleOutstreamRendererEvents.bind(null, bid)); - }); -} - -function handleOutstreamRendererEvents(bid, id, eventName) { - bid.renderer.handleVideoEvent({ id, eventName }); -} - -function createRenderer(bidRequest) { - const renderer = Renderer.install({ - id: bidRequest.adUnitCode, - url: RENDERER_URL, - loaded: false, - config: deepAccess(bidRequest, 'renderer.options'), - adUnitCode: bidRequest.adUnitCode - }); - try { - renderer.setRender(outstreamRender); - } catch (err) { - logWarn('Prebid Error calling setRender on renderer', err); - } - return renderer; -} - -function getNormalizedBidRequest(bid) { - let adUnitId = getBidIdParameter('adUnitCode', bid) || null; - let placementId = getBidIdParameter('placementId', bid.params) || null; - let publisherId = null; - let placementKey = null; - - if (placementId === null) { - publisherId = getBidIdParameter('publisherId', bid.params) || null; - placementKey = getBidIdParameter('placementKey', bid.params) || null; - } - const keyValues = getBidIdParameter('keyValues', bid.params) || null; - const singleSizeFilter = getBidIdParameter('size', bid.params) || null; - const bidId = getBidIdParameter('bidId', bid); - const transactionId = getBidIdParameter('transactionId', bid); - const currency = config.getConfig('currency.adServerCurrency'); - - let normalizedBidRequest = {}; - if (isInstreamVideo(bid)) { - normalizedBidRequest.adTypes = [ VIDEO ]; - } - if (isInstreamVideo(bid) || isOutstreamVideo(bid)) { - normalizedBidRequest.video = getVideoTargetingParams(bid); - } - if (placementId) { - normalizedBidRequest.placementId = placementId; - } else { - if (publisherId) { - normalizedBidRequest.publisherId = publisherId; - } - if (placementKey) { - normalizedBidRequest.placementKey = placementKey; +const ID_REQUEST = { + buildServerRequests(requestObject, bidRequests, bidderRequest) { + const requests = []; + if (config.getConfig('improvedigital.singleRequest') === true) { + requestObject.imp = bidRequests.map((bidRequest) => this.buildImp(bidRequest)); + requests[0] = this.formatRequest(requestObject, bidderRequest); + } else { + bidRequests.map((bidRequest) => { + const request = deepClone(requestObject); + request.id = bidRequest.bidId || getUniqueIdentifierStr(); + request.imp = [this.buildImp(bidRequest)]; + deepSetValue(request, 'source.tid', bidRequest.transactionId); + requests.push(this.formatRequest(request, bidderRequest)); + }); } - } - if (keyValues) { - normalizedBidRequest.keyValues = keyValues; - } + return requests; + }, - if (config.getConfig('improvedigital.usePrebidSizes') === true && !isInstreamVideo(bid) && !isOutstreamVideo(bid) && bid.sizes && bid.sizes.length > 0) { - normalizedBidRequest.format = bid.sizes; - } else if (singleSizeFilter && singleSizeFilter.w && singleSizeFilter.h) { - normalizedBidRequest.size = {}; - normalizedBidRequest.size.h = singleSizeFilter.h; - normalizedBidRequest.size.w = singleSizeFilter.w; - } + formatRequest(request, bidderRequest) { + return { + method: 'POST', + url: REQUEST_URL, + data: JSON.stringify(request), + bidderRequest + } + }, - if (bidId) { - normalizedBidRequest.id = bidId; - } - if (adUnitId) { - normalizedBidRequest.adUnitId = adUnitId; - } - if (transactionId) { - normalizedBidRequest.transactionId = transactionId; - } - if (currency) { - normalizedBidRequest.currency = currency; - } - // Floor - let bidFloor = getBidFloor(bid); - let bidFloorCur = null; - if (!bidFloor) { - bidFloor = getBidIdParameter('bidFloor', bid.params); - bidFloorCur = getBidIdParameter('bidFloorCur', bid.params); - } - if (bidFloor) { - normalizedBidRequest.bidFloor = bidFloor; - normalizedBidRequest.bidFloorCur = bidFloorCur ? bidFloorCur.toUpperCase() : 'USD'; - } - return normalizedBidRequest; -} + buildImp(bidRequest) { + const imp = { + id: getBidIdParameter('bidId', bidRequest) || getUniqueIdentifierStr(), + secure: ID_UTIL.toBit(window.location.protocol === 'https:'), + }; -function getNormalizedNativeAd(rawNative) { - const native = {}; - if (!rawNative || !isArray(rawNative.assets)) { - return null; - } - // Assets - rawNative.assets.forEach(asset => { - if (asset.title) { - native.title = asset.title.text; - } else if (asset.data) { - switch (asset.data.type) { - case 1: - native.sponsoredBy = asset.data.value; - break; - case 2: - native.body = asset.data.value; - break; - case 3: - native.rating = asset.data.value; - break; - case 4: - native.likes = asset.data.value; - break; - case 5: - native.downloads = asset.data.value; - break; - case 6: - native.price = asset.data.value; - break; - case 7: - native.salePrice = asset.data.value; - break; - case 8: - native.phone = asset.data.value; - break; - case 9: - native.address = asset.data.value; - break; - case 10: - native.body2 = asset.data.value; - break; - case 11: - native.displayUrl = asset.data.value; - break; - case 12: - native.cta = asset.data.value; - break; - } - } else if (asset.img) { - switch (asset.img.type) { - case 2: - native.icon = { - url: asset.img.url, - width: asset.img.w, - height: asset.img.h - }; - break; - case 3: - native.image = { - url: asset.img.url, - width: asset.img.w, - height: asset.img.h - }; - break; - } + // Floor + const bidFloor = this.getBidFloor(bidRequest) || getBidIdParameter('bidFloor', bidRequest.params); + if (bidFloor) { + const bidFloorCur = getBidIdParameter('bidFloorCur', bidRequest.params) || 'USD'; + deepSetValue(imp, 'bidfloor', bidFloor); + deepSetValue(imp, 'bidfloorcur', bidFloorCur ? bidFloorCur.toUpperCase() : undefined); } - }); - // Trackers - if (rawNative.eventtrackers) { - native.impressionTrackers = []; - rawNative.eventtrackers.forEach(tracker => { - // Only handle impression event. Viewability events are not supported yet. - if (tracker.event !== 1) return; - switch (tracker.method) { - case 1: // img - native.impressionTrackers.push(tracker.url); - break; - case 2: // js - // javascriptTrackers is a string. If there's more than one JS tracker in bid response, the last script will be used. - native.javascriptTrackers = ``; - break; - } - }); - } else { - native.impressionTrackers = rawNative.imptrackers || []; - native.javascriptTrackers = rawNative.jstracker; - } - if (rawNative.link) { - native.clickUrl = rawNative.link.url; - native.clickTrackers = rawNative.link.clicktrackers; - } - if (rawNative.privacy) { - native.privacyLink = rawNative.privacy; - } - return native; -} -registerBidder(spec); -export function ImproveDigitalAdServerJSClient(endPoint) { - this.CONSTANTS = { - AD_SERVER_BASE_URL: 'ice.360yield.com', - END_POINT: endPoint || 'hb', - AD_SERVER_URL_PARAM: 'jsonp=', - CLIENT_VERSION: 'JS-6.4.0', - MAX_URL_LENGTH: 2083, - ERROR_CODES: { - MISSING_PLACEMENT_PARAMS: 2, - LIB_VERSION_MISSING: 3 - }, - RETURN_OBJ_TYPE: { - DEFAULT: 0, - URL_PARAMS_SPLIT: 1 + const placementId = getBidIdParameter('placementId', bidRequest.params); + if (placementId) { + deepSetValue(imp, 'ext.bidder.placementId', placementId); + } else { + deepSetValue(imp, 'ext.bidder.publisherId', getBidIdParameter('publisherId', bidRequest.params)); + deepSetValue(imp, 'ext.bidder.placementKey', getBidIdParameter('placementKey', bidRequest.params)); } - }; - this.getErrorReturn = function(errorCode) { - return { - idMappings: {}, - requests: {}, - 'errorCode': errorCode - }; - }; + deepSetValue(imp, 'ext.bidder.keyValues', getBidIdParameter('keyValues', bidRequest.params) || undefined); - this.createRequest = function(requestObject, requestParameters, extraRequestParameters) { - if (!requestParameters.libVersion) { - return this.getErrorReturn(this.CONSTANTS.ERROR_CODES.LIB_VERSION_MISSING); - } + // Adding GPID + const gpid = deepAccess(bidRequest, 'ortb2Imp.ext.gpid') || + deepAccess(bidRequest, 'ortb2Imp.ext.data.pbadslot') || + deepAccess(bidRequest, 'ortb2Imp.ext.data.adserver.adslot'); - requestParameters.returnObjType = requestParameters.returnObjType || this.CONSTANTS.RETURN_OBJ_TYPE.DEFAULT; - requestParameters.adServerBaseUrl = 'https://' + (requestParameters.adServerBaseUrl || this.CONSTANTS.AD_SERVER_BASE_URL); + deepSetValue(imp, 'ext.gpid', gpid); - let impressionObjects = []; - let impressionObject; - if (isArray(requestObject)) { - for (let counter = 0; counter < requestObject.length; counter++) { - impressionObject = this.createImpressionObject(requestObject[counter]); - impressionObjects.push(impressionObject); - } - } else { - impressionObject = this.createImpressionObject(requestObject); - impressionObjects.push(impressionObject); + // Adding Interstitial Signal + if (deepAccess(bidRequest, 'ortb2Imp.instl')) { + imp.instl = 1; } - let returnIdMappings = true; - if (requestParameters.returnObjType === this.CONSTANTS.RETURN_OBJ_TYPE.URL_PARAMS_SPLIT) { - returnIdMappings = false; + const videoParams = deepAccess(bidRequest, 'mediaTypes.video'); + if (videoParams) { + imp.video = this.buildVideoRequest(bidRequest); + deepSetValue(imp, 'ext.is_rewarded_inventory', (videoParams.rewarded === 1 || deepAccess(videoParams, 'ext.rewarded') === 1) || undefined); } - let returnObject = {}; - returnObject.requests = []; - if (returnIdMappings) { - returnObject.idMappings = []; + if (deepAccess(bidRequest, 'mediaTypes.banner')) { + imp.banner = this.buildBannerRequest(bidRequest); } - let errors = null; - let baseUrl = `${requestParameters.adServerBaseUrl}/${this.CONSTANTS.END_POINT}?${this.CONSTANTS.AD_SERVER_URL_PARAM}`; + if (deepAccess(bidRequest, 'mediaTypes.native')) { + imp.native = this.buildNativeRequest(bidRequest); + } - let bidRequestObject = { - bid_request: this.createBasicBidRequestObject(requestParameters, extraRequestParameters) - }; - for (let counter = 0; counter < impressionObjects.length; counter++) { - impressionObject = impressionObjects[counter]; - - if (impressionObject.errorCode) { - errors = errors || []; - errors.push({ - errorCode: impressionObject.errorCode, - adUnitId: impressionObject.adUnitId - }); - } else { - if (returnIdMappings) { - returnObject.idMappings.push({ - adUnitId: impressionObject.adUnitId, - id: impressionObject.impressionObject.id - }); - } - bidRequestObject.bid_request.imp = bidRequestObject.bid_request.imp || []; - bidRequestObject.bid_request.imp.push(impressionObject.impressionObject); - - let writeLongRequest = false; - const outputUri = baseUrl + encodeURIComponent(JSON.stringify(bidRequestObject)); - if (outputUri.length > this.CONSTANTS.MAX_URL_LENGTH) { - writeLongRequest = true; - if (bidRequestObject.bid_request.imp.length > 1) { - // Pop the current request and process it again in the next iteration - bidRequestObject.bid_request.imp.pop(); - if (returnIdMappings) { - returnObject.idMappings.pop(); - } - counter--; - } - } + return imp; + }, - if (writeLongRequest || - !requestParameters.singleRequestMode || - counter === impressionObjects.length - 1) { - returnObject.requests.push(this.formatRequest(requestParameters, bidRequestObject)); - bidRequestObject = { - bid_request: this.createBasicBidRequestObject(requestParameters, extraRequestParameters) - }; - } - } + buildVideoRequest(bidRequest) { + const videoParams = deepClone(bidRequest.mediaTypes.video); + const videoImproveParams = deepClone(deepAccess(bidRequest, 'params.video', {})); + const video = {...videoParams, ...videoImproveParams}; + + if (Array.isArray(video.playerSize)) { + // Player size can be defined as [w, h] or [[w, h]] + const size = Array.isArray(video.playerSize[0]) ? video.playerSize[0] : video.playerSize; + video.w = size[0]; + video.h = size[1]; } + video.placement = this.isOutstreamVideo(bidRequest) ? VIDEO_PARAMS.PLACEMENT_TYPE.OUTSTREAM : VIDEO_PARAMS.PLACEMENT_TYPE.INSTREAM; - if (errors) { - returnObject.errors = errors; + // Mimes is required + if (!video.mimes) { + video.mimes = VIDEO_PARAMS.DEFAULT_MIMES; } - return returnObject; - }; + // skip must be 0 or 1 + if (video.skip !== 1) { + delete video.skipmin; + delete video.skipafter; + if (video.skip !== 0) { + logWarn(`video.skip: invalid value '${video.skip}'. Expected 0 or 1`); + delete video.skip; + } + } + + Object.keys(video).forEach(prop => { + if (VIDEO_PARAMS.SUPPORTED_PROPERTIES.indexOf(prop) === -1) delete video[prop]; + }); + return video; + }, - this.formatRequest = function(requestParameters, bidRequestObject) { - switch (requestParameters.returnObjType) { - case this.CONSTANTS.RETURN_OBJ_TYPE.URL_PARAMS_SPLIT: - return { - method: 'GET', - url: `${requestParameters.adServerBaseUrl}/${this.CONSTANTS.END_POINT}`, - data: `${this.CONSTANTS.AD_SERVER_URL_PARAM}${encodeURIComponent(JSON.stringify(bidRequestObject))}` + buildBannerRequest(bidRequest) { + // Set the desired creative sizes + // Input Format: array of pairs, i.e. [[300, 250], [250, 250]] + // Unless improvedigital.usePrebidSizes == true, no sizes are sent to the server + // and the sizes defined in the server for the placement will be used + const banner = {}; + if (config.getConfig('improvedigital.usePrebidSizes') === true && bidRequest.sizes) { + // Convert sizes from [x, y] to { w: x, h: y} + banner.format = bidRequest.sizes.map(sizePair => ({w: sizePair[0], h: sizePair[1]})); + } + return banner; + }, + + buildNativeRequest(bidRequest) { + const nativeParams = bidRequest.mediaTypes.native; + const request = { + assets: [], + } + for (let i of Object.keys(nativeParams)) { + const assetOrtbParams = NATIVE_DATA.ASSETS[i]; + if (assetOrtbParams) { + const assetParams = nativeParams[i]; + const asset = { + id: assetOrtbParams.id, + required: ID_UTIL.toBit(assetParams.required), }; - default: - const baseUrl = `${requestParameters.adServerBaseUrl}/` + - `${this.CONSTANTS.END_POINT}?${this.CONSTANTS.AD_SERVER_URL_PARAM}`; - return { - url: baseUrl + encodeURIComponent(JSON.stringify(bidRequestObject)) + switch (assetOrtbParams.assetType) { + case NATIVE_DATA.ASSET_TYPES.TITLE: + asset.title = {len: assetParams.len || assetOrtbParams.default.len}; + break; + case NATIVE_DATA.ASSET_TYPES.DATA: + asset.data = cleanObj({type: assetOrtbParams.type, len: assetParams.len}) + break; + case NATIVE_DATA.ASSET_TYPES.IMG: + asset.img = cleanObj({ + type: assetOrtbParams.type, + w: deepAccess(assetParams, 'sizes.0'), + h: deepAccess(assetParams, 'sizes.1'), + wmin: deepAccess(assetParams, 'aspect_ratios.0.min_width'), + hmin: deepAccess(assetParams, 'aspect_ratios.0.min_height') + }); + break; + default: + return; } + request.assets.push(asset); + } } - }; + return { ver: NATIVE_DATA.VERSION, request: JSON.stringify(request) }; + }, - this.createBasicBidRequestObject = function(requestParameters, extraRequestParameters) { - let impressionBidRequestObject = {}; - impressionBidRequestObject.secure = 1; - if (requestParameters.requestId) { - impressionBidRequestObject.id = requestParameters.requestId; - } else { - impressionBidRequestObject.id = getUniqueIdentifierStr(); - } - if (requestParameters.domain) { - impressionBidRequestObject.domain = requestParameters.domain; - } - if (requestParameters.page) { - impressionBidRequestObject.page = requestParameters.page; - } - if (requestParameters.ref) { - impressionBidRequestObject.ref = requestParameters.ref; - } - if (requestParameters.callback) { - impressionBidRequestObject.callback = requestParameters.callback; - } - if (requestParameters.libVersion) { - impressionBidRequestObject.version = requestParameters.libVersion + '-' + this.CONSTANTS.CLIENT_VERSION; - } - if (requestParameters.referrer) { - impressionBidRequestObject.referrer = requestParameters.referrer; - } - if (requestParameters.gdpr || requestParameters.gdpr === 0) { - impressionBidRequestObject.gdpr = requestParameters.gdpr; - } - if (requestParameters.usPrivacy) { - impressionBidRequestObject.us_privacy = requestParameters.usPrivacy; - } - if (requestParameters.schain) { - impressionBidRequestObject.schain = requestParameters.schain; - } - if (requestParameters.pagecat) { - impressionBidRequestObject.pagecat = requestParameters.pagecat; - } - if (requestParameters.genre) { - impressionBidRequestObject.genre = requestParameters.genre; - } - if (requestParameters.user) { - impressionBidRequestObject.user = requestParameters.user; + isOutstreamVideo(bidRequest) { + return deepAccess(bidRequest, 'mediaTypes.video.context') === 'outstream'; + }, + + getBidFloor(bidRequest) { + if (!isFn(bidRequest.getFloor)) { + return null; } - if (extraRequestParameters) { - for (let prop in extraRequestParameters) { - impressionBidRequestObject[prop] = extraRequestParameters[prop]; - } + const floor = bidRequest.getFloor({ + currency: 'USD', + mediaType: '*', + size: '*' + }); + if (isPlainObject(floor) && !isNaN(floor.floor) && floor.currency === 'USD') { + return floor.floor; } + return null; + }, - return impressionBidRequestObject; - }; - - this.createImpressionObject = function(placementObject) { - let outputObject = {}; - let impressionObject = {}; - outputObject.impressionObject = impressionObject; + buildSiteOrApp(request, bidderRequest) { + const app = {}; + const configAppSettings = config.getConfig('app') || {}; + const fpdAppSettings = config.getConfig('ortb2.app') || {}; + mergeDeep(app, configAppSettings, fpdAppSettings); - if (placementObject.id) { - impressionObject.id = placementObject.id; + if (Object.keys(app).length !== 0) { + request.app = app; } else { - impressionObject.id = getUniqueIdentifierStr(); - } - if (placementObject.adTypes) { - impressionObject.ad_types = placementObject.adTypes; - } - if (placementObject.adUnitId) { - outputObject.adUnitId = placementObject.adUnitId; - } - if (placementObject.currency) { - impressionObject.currency = placementObject.currency.toUpperCase(); - } - if (placementObject.bidFloor) { - impressionObject.bidfloor = placementObject.bidFloor; - } - if (placementObject.bidFloorCur) { - impressionObject.bidfloorcur = placementObject.bidFloorCur.toUpperCase(); - } - if (placementObject.placementId) { - impressionObject.pid = placementObject.placementId; - } - if (placementObject.publisherId) { - impressionObject.pubid = placementObject.publisherId; - } - if (placementObject.placementKey) { - impressionObject.pkey = placementObject.placementKey; - } - if (placementObject.transactionId) { - impressionObject.tid = placementObject.transactionId; - } - if (!isEmpty(placementObject.video)) { - const video = Object.assign({}, placementObject.video); - // skip must be 0 or 1 - if (video.skip !== 1) { - delete video.skipmin; - delete video.skipafter; - if (video.skip !== 0) { - logWarn(`video.skip: invalid value '${video.skip}'. Expected 0 or 1`); - delete video.skip; - } - } - if (!isEmpty(video)) { - impressionObject.video = video; + const site = {}; + const url = config.getConfig('pageUrl') || deepAccess(bidderRequest, 'refererInfo.referer'); + if (url) { + site.page = url; + site.domain = parseUrl(url).hostname; } + const configSiteSettings = config.getConfig('site') || {}; + const fpdSiteSettings = config.getConfig('ortb2.site') || {}; + mergeDeep(site, configSiteSettings, fpdSiteSettings); + request.site = site; } - if (placementObject.keyValues) { - for (let key in placementObject.keyValues) { - for (let valueCounter = 0; valueCounter < placementObject.keyValues[key].length; valueCounter++) { - impressionObject.kvw = impressionObject.kvw || {}; - impressionObject.kvw[key] = impressionObject.kvw[key] || []; - impressionObject.kvw[key].push(placementObject.keyValues[key][valueCounter]); - } + }, +}; + +const ID_RESPONSE = { + buildAd(bid, bidRequest, bidResponse) { + if (bidRequest.mediaTypes && Object.keys(bidRequest.mediaTypes).length === 1) { + if (deepAccess(bidRequest, 'mediaTypes.video')) { + this.buildVideoAd(bid, bidRequest, bidResponse); + } else if (deepAccess(bidRequest, 'mediaTypes.banner')) { + this.buildBannerAd(bid, bidRequest, bidResponse); + } else if (deepAccess(bidRequest, 'mediaTypes.native')) { + this.buildNativeAd(bid, bidRequest, bidResponse) + } + } else { + if (bidResponse.adm.search(/^ sizePair.length === 2 && - isInteger(sizePair[0]) && - isInteger(sizePair[1]) && - sizePair[0] >= 0 && - sizePair[1] >= 0) - .map(sizePair => { - return { w: sizePair[0], h: sizePair[1] } - }); - if (format.length > 0) { - impressionObject.banner.format = format; + buildBannerAd(bid, bidRequest, bidResponse) { + bid.mediaType = BANNER; + bid.ad = bidResponse.adm; + bid.width = bidResponse.w; + bid.height = bidResponse.h; + }, + + buildNativeAd(bid, bidRequest, bidResponse) { + bid.mediaType = NATIVE; + const nativeResponse = JSON.parse(bidResponse.adm); + const nativeAd = { + clickUrl: deepAccess(nativeResponse, 'link.url'), + clickTrackers: deepAccess(nativeResponse, 'link.clicktrackers'), + privacyLink: nativeResponse.privacy + } + // Trackers + if (nativeResponse.eventtrackers) { + nativeAd.impressionTrackers = []; + nativeResponse.eventtrackers.forEach(tracker => { + // Only handle impression event. Viewability events are not supported yet. + if (tracker.event !== 1) return; + switch (tracker.method) { + case 1: // img + nativeAd.impressionTrackers.push(tracker.url); + break; + case 2: // js + // javascriptTrackers is a string. If there's more than one JS tracker in bid response, the last script will be used. + nativeAd.javascriptTrackers = ``; + break; + } + }); + } else { + nativeAd.impressionTrackers = nativeResponse.imptrackers || []; + nativeAd.javascriptTrackers = nativeResponse.jstracker; + } + nativeResponse.assets.map(asset => { + const assetParams = NATIVE_DATA.getAssetById(asset.id); + switch (assetParams.assetType) { + case NATIVE_DATA.ASSET_TYPES.TITLE: + nativeAd.title = asset.title.text; + break; + case NATIVE_DATA.ASSET_TYPES.DATA: + nativeAd[assetParams.name] = asset.data.value; + break; + case NATIVE_DATA.ASSET_TYPES.IMG: + nativeAd[assetParams.name] = { + url: asset.img.url, + width: asset.img.w, + height: asset.img.h, + }; + break; } + }); + bid.native = nativeAd; + }, +}; + +const ID_OUTSTREAM = { + RENDERER_URL: 'https://acdn.adnxs.com/video/outstream/ANOutstreamVideo.js', + createRenderer(bidRequest) { + const renderer = Renderer.install({ + id: bidRequest.adUnitCode, + url: this.RENDERER_URL, + config: deepAccess(bidRequest, 'renderer.options'), + adUnitCode: bidRequest.adUnitCode + }); + try { + renderer.setRender(this.render); + } catch (err) { + logWarn('Prebid Error calling setRender on renderer', err); } + return renderer; + }, + + render(bid) { + bid.renderer.push(() => { + window.ANOutstreamVideo.renderAd({ + sizes: [bid.width, bid.height], + targetId: bid.adUnitCode, + adResponse: bid.adResponse, + rendererOptions: bid.renderer.getConfig() + }, ID_OUTSTREAM.handleRendererEvents.bind(null, bid)); + }); + }, + + handleRendererEvents(bid, id, eventName) { + bid.renderer.handleVideoEvent({ id, eventName }); + }, +}; - if (!impressionObject.pid && - !impressionObject.pubid && - !impressionObject.pkey && - !(impressionObject.banner && impressionObject.banner.w && impressionObject.banner.h)) { - outputObject.impressionObject = null; - outputObject.errorCode = this.CONSTANTS.ERROR_CODES.MISSING_PLACEMENT_PARAMS; +const ID_RAZR = { + RENDERER_URL: 'https://razr.improvedigital.com/renderer.js', + addBidData({bid, bidRequest}) { + if (this.isValidBid(bid)) { + bid.renderer = Renderer.install({ + url: this.RENDERER_URL, + config: {bidRequest} + }); + bid.renderer.setRender(this.render); } - return outputObject; - }; -} + }, + + isValidBid(bid) { + return bid && /razr:\/\//.test(bid.ad); + }, + + render(bid) { + const {bidRequest} = bid.renderer.getConfig(); + + const payload = { + type: 'prebid', + bidRequest, + bid, + config: mergeDeep( + {}, + config.getConfig('improvedigital.rendererConfig'), + deepAccess(bidRequest, 'params.rendererConfig') + ) + }; + + const razr = window.razr = window.razr || {}; + razr.queue = razr.queue || []; + razr.queue.push(payload); + } +}; + +const ID_UTIL = { + toBit(val) { + return val ? 1 : 0; + }, +}; diff --git a/modules/insticatorBidAdapter.js b/modules/insticatorBidAdapter.js index 47a3353a897..4122166a151 100644 --- a/modules/insticatorBidAdapter.js +++ b/modules/insticatorBidAdapter.js @@ -1,13 +1,9 @@ -import { config } from '../src/config.js'; -import { BANNER } from '../src/mediaTypes.js'; -import { registerBidder } from '../src/adapters/bidderFactory.js'; -import { - deepAccess, - generateUUID, - logError, -} from '../src/utils.js'; -import { getStorageManager } from '../src/storageManager.js'; -import find from 'core-js-pure/features/array/find.js'; +import {config} from '../src/config.js'; +import {BANNER} from '../src/mediaTypes.js'; +import {registerBidder} from '../src/adapters/bidderFactory.js'; +import {deepAccess, generateUUID, logError, isArray} from '../src/utils.js'; +import {getStorageManager} from '../src/storageManager.js'; +import {find} from '../src/polyfill.js'; const BIDDER_CODE = 'insticator'; const ENDPOINT = 'https://ex.ingage.tech/v1/openrtb'; // production endpoint @@ -16,7 +12,7 @@ const USER_ID_COOKIE_EXP = 2592000000; // 30 days const BID_TTL = 300; // 5 minutes const GVLID = 910; -export const storage = getStorageManager(GVLID, BIDDER_CODE); +export const storage = getStorageManager({gvlid: GVLID, bidderCode: BIDDER_CODE}); config.setDefaults({ insticator: { @@ -54,6 +50,12 @@ function setUserId(userId) { function buildImpression(bidRequest) { const format = []; + const ext = { + insticator: { + adUnitId: bidRequest.params.adUnitId, + }, + } + const sizes = deepAccess(bidRequest, 'mediaTypes.banner.sizes') || bidRequest.sizes; @@ -64,17 +66,19 @@ function buildImpression(bidRequest) { }); } + const gpid = deepAccess(bidRequest, 'ortb2Imp.ext.gpid'); + + if (gpid) { + ext.gpid = gpid; + } + return { id: bidRequest.bidId, tagid: bidRequest.adUnitCode, banner: { format, }, - ext: { - insticator: { - adUnitId: bidRequest.params.adUnitId, - }, - }, + ext, }; } @@ -121,6 +125,24 @@ function buildUser() { }; } +function extractSchain(bids, requestId) { + if (!bids || bids.length === 0 || !bids[0].schain) return; + + const schain = bids[0].schain; + if (schain && schain.nodes && schain.nodes.length && schain.nodes[0]) { + schain.nodes[0].rid = requestId; + } + + return schain; +} + +function extractEids(bids) { + if (!bids) return; + + const bid = bids.find(bid => isArray(bid.userIdAsEids) && bid.userIdAsEids.length > 0); + return bid ? bid.userIdAsEids : bids[0].userIdAsEids; +} + function buildRequest(validBidRequests, bidderRequest) { const req = { id: bidderRequest.bidderRequestId, @@ -138,21 +160,50 @@ function buildRequest(validBidRequests, bidderRequest) { regs: buildRegs(bidderRequest), user: buildUser(), imp: validBidRequests.map((bidRequest) => buildImpression(bidRequest)), + ext: { + insticator: { + adapter: { + vendor: 'prebid', + prebid: '$prebid.version$' + } + } + } }; const params = config.getConfig('insticator.params'); if (params) { req.ext = { - insticator: params, + insticator: {...req.ext.insticator, ...params}, }; } + const schain = extractSchain(validBidRequests, bidderRequest.bidderRequestId); + + if (schain) { + req.source.ext = { schain }; + } + + const eids = extractEids(validBidRequests); + + if (eids) { + req.user.ext = { eids }; + } + return req; } function buildBid(bid, bidderRequest) { const originalBid = find(bidderRequest.bids, (b) => b.bidId === bid.impid); + let meta = {} + + if (bid.ext && bid.ext.meta) { + meta = bid.ext.meta + } + + if (bid.adomain) { + meta.advertiserDomains = bid.adomain + } return { requestId: bid.impid, @@ -166,9 +217,7 @@ function buildBid(bid, bidderRequest) { mediaType: 'banner', ad: bid.adm, adUnitCode: originalBid.adUnitCode, - meta: { - advertiserDomains: bid.bidADomain && bid.bidADomain.length ? bid.bidADomain : [] - }, + ...(Object.keys(meta).length > 0 ? {meta} : {}) }; } diff --git a/modules/integr8BidAdapter.js b/modules/integr8BidAdapter.js index 321c3c4c1ab..d61fe624c59 100644 --- a/modules/integr8BidAdapter.js +++ b/modules/integr8BidAdapter.js @@ -10,7 +10,7 @@ const SIZE_SEPARATOR = ';'; const BISKO_ID = 'biskoId'; const STORAGE_ID = 'bisko-sid'; const SEGMENTS = 'biskoSegments'; -const storage = getStorageManager(); +const storage = getStorageManager({bidderCode: BIDDER_CODE}); export const spec = { code: BIDDER_CODE, diff --git a/modules/intentIqIdSystem.js b/modules/intentIqIdSystem.js index 2092f9a185a..d60ab3962ae 100644 --- a/modules/intentIqIdSystem.js +++ b/modules/intentIqIdSystem.js @@ -15,7 +15,7 @@ const PCID_EXPIRY = 365; const MODULE_NAME = 'intentIqId'; export const FIRST_PARTY_KEY = '_iiq_fdata'; -export const storage = getStorageManager(undefined, MODULE_NAME); +export const storage = getStorageManager({gvlid: undefined, moduleName: MODULE_NAME}); const INVALID_ID = 'INVALID_ID'; diff --git a/modules/interactiveOffersBidAdapter.js b/modules/interactiveOffersBidAdapter.js index c1757198eca..d8a106623fd 100644 --- a/modules/interactiveOffersBidAdapter.js +++ b/modules/interactiveOffersBidAdapter.js @@ -83,7 +83,8 @@ function parseRequestPrebidjsToOpenRTB(prebidRequest) { let openRTBRequest = JSON.parse(JSON.stringify(DEFAULT['OpenRTBBidRequest'])); openRTBRequest.id = prebidRequest.auctionId; openRTBRequest.ext = { - auctionstart: Date.now() + refererInfo: prebidRequest.refererInfo, + auctionId: prebidRequest.auctionId }; openRTBRequest.site = JSON.parse(JSON.stringify(DEFAULT['OpenRTBBidRequestSite'])); @@ -111,15 +112,17 @@ function parseRequestPrebidjsToOpenRTB(prebidRequest) { openRTBRequest.user = JSON.parse(JSON.stringify(DEFAULT['OpenRTBBidRequestUser'])); openRTBRequest.imp = []; - prebidRequest.bids.forEach(function(bid, impId) { - impId++; + prebidRequest.bids.forEach(function(bid) { if (!ret.partnerId) { ret.partnerId = bid.params.partnerId; } let imp = JSON.parse(JSON.stringify(DEFAULT['OpenRTBBidRequestImp'])); - imp.id = impId; + imp.id = bid.bidId; imp.secure = secure; - imp.tagid = bid.bidId; + imp.tagid = bid.adUnitCode; + imp.ext = { + rawdata: bid + }; openRTBRequest.site.publisher.id = openRTBRequest.site.publisher.id || 0; openRTBRequest.tmax = openRTBRequest.tmax || bid.params.tmax || 0; @@ -152,7 +155,7 @@ function parseResponseOpenRTBToPrebidjs(openRTBResponse) { if (seatbid.bid && seatbid.bid.forEach) { seatbid.bid.forEach(function(bid) { let prebid = JSON.parse(JSON.stringify(DEFAULT['PrebidBid'])); - prebid.requestId = bid.ext.tagid; + prebid.requestId = bid.impid; prebid.ad = bid.adm; prebid.creativeId = bid.crid; prebid.cpm = bid.price; diff --git a/modules/intersectionRtdProvider.js b/modules/intersectionRtdProvider.js index 4404c4148fe..c7d03b25b57 100644 --- a/modules/intersectionRtdProvider.js +++ b/modules/intersectionRtdProvider.js @@ -2,8 +2,9 @@ import {submodule} from '../src/hook.js'; import {isFn, logError} from '../src/utils.js'; import {config} from '../src/config.js'; import {getGlobal} from '../src/prebidGlobal.js'; -import includes from 'core-js-pure/features/array/includes.js'; +import {includes} from '../src/polyfill.js'; import '../src/adapterManager.js'; + let observerAvailable = true; function getIntersectionData(requestBidsObject, onDone, providerConfig, userConsent) { const intersectionMap = {}; diff --git a/modules/invibesBidAdapter.js b/modules/invibesBidAdapter.js index bc4387695c3..e83786f3857 100644 --- a/modules/invibesBidAdapter.js +++ b/modules/invibesBidAdapter.js @@ -16,7 +16,7 @@ const CONSTANTS = { META_TAXONOMY: ['networkId', 'networkName', 'agencyId', 'agencyName', 'advertiserId', 'advertiserName', 'advertiserDomains', 'brandId', 'brandName', 'primaryCatId', 'secondaryCatIds', 'mediaType'] }; -const storage = getStorageManager(CONSTANTS.INVIBES_VENDOR_ID); +const storage = getStorageManager({gvlid: CONSTANTS.INVIBES_VENDOR_ID, bidderCode: CONSTANTS.BIDDER_CODE}); export const spec = { code: CONSTANTS.BIDDER_CODE, diff --git a/modules/iqzoneBidAdapter.js b/modules/iqzoneBidAdapter.js index 5268f8935be..6c0a2e5f56d 100644 --- a/modules/iqzoneBidAdapter.js +++ b/modules/iqzoneBidAdapter.js @@ -5,6 +5,7 @@ import { config } from '../src/config.js'; const BIDDER_CODE = 'iqzone'; const AD_URL = 'https://smartssp-us-east.iqzone.com/pbjs'; +const SYNC_URL = 'https://cs.smartssp.iqzone.com'; function isBidResponseValid(bid) { if (!bid.requestId || !bid.cpm || !bid.creativeId || @@ -27,16 +28,23 @@ function isBidResponseValid(bid) { function getPlacementReqData(bid) { const { params, bidId, mediaTypes } = bid; const schain = bid.schain || {}; - const { placementId } = params; + const { placementId, endpointId } = params; const bidfloor = getBidFloor(bid); const placement = { - placementId, 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; @@ -88,7 +96,7 @@ export const spec = { isBidRequestValid: (bid = {}) => { const { params, bidId, mediaTypes } = bid; - let valid = Boolean(bidId && params && params.placementId); + let valid = Boolean(bidId && params && (params.placementId || params.endpointId)); if (mediaTypes && mediaTypes[BANNER]) { valid = valid && Boolean(mediaTypes[BANNER] && mediaTypes[BANNER].sizes); @@ -170,6 +178,29 @@ export const spec = { } } 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 + }]; } }; diff --git a/modules/iqzoneBidAdapter.md b/modules/iqzoneBidAdapter.md index 75e82d59b3e..3808bdbabec 100644 --- a/modules/iqzoneBidAdapter.md +++ b/modules/iqzoneBidAdapter.md @@ -75,6 +75,22 @@ IQZone bid adapter supports Banner, Video (instream and outstream) and Native. } } ] + }, + { + code: 'adunit1', + mediaTypes: { + banner: { + sizes: [ [300, 250], [320, 50] ], + } + }, + bids: [ + { + bidder: 'iqzone', + params: { + endpointId: 'testBanner', + } + } + ] } ]; ``` \ No newline at end of file diff --git a/modules/ixBidAdapter.js b/modules/ixBidAdapter.js index 4e5606ce476..7cec6172ba4 100644 --- a/modules/ixBidAdapter.js +++ b/modules/ixBidAdapter.js @@ -1,14 +1,30 @@ -import { deepAccess, parseGPTSingleSizeArray, inIframe, deepClone, logError, logWarn, isFn, contains, isInteger, isArray, deepSetValue, parseQueryStringParameters, isEmpty, mergeDeep, convertTypes, hasDeviceAccess } from '../src/utils.js'; -import { BANNER, VIDEO } from '../src/mediaTypes.js'; -import { config } from '../src/config.js'; +import { + contains, + convertTypes, + deepAccess, + deepClone, + deepSetValue, + hasDeviceAccess, + inIframe, + isArray, + isEmpty, + isFn, + isInteger, + logError, + logWarn, + mergeDeep, + parseGPTSingleSizeArray, + parseQueryStringParameters +} from '../src/utils.js'; +import {BANNER, VIDEO} from '../src/mediaTypes.js'; +import {config} from '../src/config.js'; import CONSTANTS from '../src/constants.json'; -import { getStorageManager, validateStorageEnforcement } from '../src/storageManager.js'; -import events from '../src/events.js'; -import find from 'core-js-pure/features/array/find.js'; -import { registerBidder } from '../src/adapters/bidderFactory.js'; -import { INSTREAM, OUTSTREAM } from '../src/video.js'; -import includes from 'core-js-pure/features/array/includes.js'; -import { Renderer } from '../src/Renderer.js'; +import {getStorageManager, validateStorageEnforcement} from '../src/storageManager.js'; +import * as events from '../src/events.js'; +import {find, includes} from '../src/polyfill.js'; +import {registerBidder} from '../src/adapters/bidderFactory.js'; +import {INSTREAM, OUTSTREAM} from '../src/video.js'; +import {Renderer} from '../src/Renderer.js'; const BIDDER_CODE = 'ix'; const ALIAS_BIDDER_CODE = 'roundel'; @@ -58,22 +74,18 @@ const SOURCE_RTI_MAPPING = { 'crwdcntrl.net': '', // Lotame Panorama ID, lotamePanoramaId 'epsilon.com': '', // Publisher Link, publinkId 'audigent.com': '', // Halo ID from Audigent, haloId - 'pubcid.org': '' // SharedID, pubcid + 'pubcid.org': '', // SharedID, pubcid + 'trustpid.com': '' // Trustpid }; const PROVIDERS = [ 'britepoolid', - 'id5id', 'lipbid', - 'haloId', 'criteoId', - 'lotamePanoramaId', 'merkleId', 'parrableId', 'connectid', 'tapadId', 'quantcastId', - 'pubcid', - 'TDID', 'flocId', 'pubProvidedId' ]; @@ -88,7 +100,7 @@ const VIDEO_PARAMS_ALLOW_LIST = [ ]; const LOCAL_STORAGE_KEY = 'ixdiag'; let hasRegisteredHandler = false; -export const storage = getStorageManager(GLOBAL_VENDOR_ID, BIDDER_CODE); +export const storage = getStorageManager({gvlid: GLOBAL_VENDOR_ID, bidderCode: BIDDER_CODE}); // Possible values for bidResponse.seatBid[].bid[].mtype which indicates the type of the creative markup so that it can properly be associated with the right sub-object of the BidRequest.Imp. const MEDIA_TYPES = { @@ -207,10 +219,6 @@ function bidToImp(bid) { imp.ext.sid = String(bid.params.id); } - const dfpAdUnitCode = deepAccess(bid, 'ortb2Imp.ext.data.adserver.adslot'); - if (dfpAdUnitCode) { - imp.ext.dfp_ad_unit_code = dfpAdUnitCode; - } return imp; } @@ -492,6 +500,7 @@ function buildRequest(validBidRequests, bidderRequest, impressions, version) { // Get ids from Prebid User ID Modules let eidInfo = getEidInfo(deepAccess(validBidRequests, '0.userIdAsEids'), deepAccess(validBidRequests, '0.userId.flocId')); let userEids = eidInfo.toSend; + const pageUrl = getPageUrl() || deepAccess(bidderRequest, 'refererInfo.referer'); // RTI ids will be included in the bid request if the function getIdentityInfo() is loaded // and if the data for the partner exist @@ -590,8 +599,8 @@ function buildRequest(validBidRequests, bidderRequest, impressions, version) { deepSetValue(r, 'regs.ext.us_privacy', bidderRequest.uspConsent); } - if (bidderRequest.refererInfo) { - r.site.page = bidderRequest.refererInfo.referer; + if (pageUrl) { + r.site.page = pageUrl; } } @@ -688,9 +697,10 @@ function buildRequest(validBidRequests, bidderRequest, impressions, version) { currentImpressionSize = encodeURIComponent(JSON.stringify({ impressionObjects })).length; } + const gpid = impressions[transactionIds[adUnitIndex]].gpid; + const dfpAdUnitCode = impressions[transactionIds[adUnitIndex]].dfp_ad_unit_code; if (impressionObjects.length && BANNER in impressionObjects[0]) { - const { id, banner: { topframe }, ext } = impressionObjects[0]; - const gpid = impressions[transactionIds[adUnitIndex]].gpid; + const { id, banner: { topframe } } = impressionObjects[0]; const _bannerImpression = { id, banner: { @@ -699,9 +709,9 @@ function buildRequest(validBidRequests, bidderRequest, impressions, version) { }, } - if (ext.dfp_ad_unit_code || gpid) { + if (dfpAdUnitCode || gpid) { _bannerImpression.ext = {}; - _bannerImpression.ext.dfp_ad_unit_code = ext.dfp_ad_unit_code; + _bannerImpression.ext.dfp_ad_unit_code = dfpAdUnitCode; _bannerImpression.ext.gpid = gpid; } @@ -717,6 +727,8 @@ function buildRequest(validBidRequests, bidderRequest, impressions, version) { r.ext.ixdiag.msd += missingCount; r.ext.ixdiag.msi += missingBannerImpressions.length; } else { + // set imp.ext.gpid to resolved gpid for each imp + impressionObjects.forEach(imp => deepSetValue(imp, 'ext.gpid', gpid)); r.imp.push(...impressionObjects); } @@ -893,6 +905,7 @@ function createVideoImps(validBidRequest, videoImps) { videoImps[validBidRequest.transactionId] = {}; videoImps[validBidRequest.transactionId].ixImps = []; videoImps[validBidRequest.transactionId].ixImps.push(imp); + videoImps[validBidRequest.transactionId].gpid = deepAccess(validBidRequest, 'ortb2Imp.ext.gpid'); } } @@ -913,16 +926,19 @@ function createBannerImps(validBidRequest, missingBannerSizes, bannerImps) { const bannerSizeDefined = includesSize(deepAccess(validBidRequest, 'mediaTypes.banner.sizes'), deepAccess(validBidRequest, 'params.size')); + if (!bannerImps.hasOwnProperty(validBidRequest.transactionId)) { + bannerImps[validBidRequest.transactionId] = {}; + } + + bannerImps[validBidRequest.transactionId].gpid = deepAccess(validBidRequest, 'ortb2Imp.ext.gpid'); + bannerImps[validBidRequest.transactionId].dfp_ad_unit_code = deepAccess(validBidRequest, 'ortb2Imp.ext.data.adserver.adslot'); + // Create IX imps from params.size if (bannerSizeDefined) { - if (!bannerImps.hasOwnProperty(validBidRequest.transactionId)) { - bannerImps[validBidRequest.transactionId] = {}; - } if (!bannerImps[validBidRequest.transactionId].hasOwnProperty('ixImps')) { bannerImps[validBidRequest.transactionId].ixImps = [] } bannerImps[validBidRequest.transactionId].ixImps.push(imp); - bannerImps[validBidRequest.transactionId].gpid = deepAccess(validBidRequest, 'ortb2Imp.ext.gpid'); } if (ixConfig.hasOwnProperty('detectMissingSizes') && ixConfig.detectMissingSizes) { @@ -930,6 +946,20 @@ function createBannerImps(validBidRequest, missingBannerSizes, bannerImps) { } } +/** + * Returns the `pageUrl` set by publisher on the page if it is an valid url + */ +function getPageUrl() { + const pageUrl = config.getConfig('pageUrl'); + try { + const url = new URL(pageUrl); + return url.href; + } catch (_) { + logWarn(`IX Bid Adapter: invalid pageUrl config property value set: ${pageUrl}`); + return undefined; + } +} + /** * Determines IX configuration type based on IX params * @param {object} valid IX configured param @@ -1334,7 +1364,7 @@ export const spec = { bid = parseBid(innerBids[j], responseBody.cur, bidRequest); if (!deepAccess(bid, 'mediaTypes.video.renderer') && deepAccess(bid, 'mediaTypes.video.context') === 'outstream') { - bid.mediaTypes.video.renderer = createRenderer(innerBids[j].bidId); + bid.renderer = createRenderer(innerBids[j].bidId); } bids.push(bid); diff --git a/modules/jixieBidAdapter.js b/modules/jixieBidAdapter.js index 119fcdf142b..de509853fed 100644 --- a/modules/jixieBidAdapter.js +++ b/modules/jixieBidAdapter.js @@ -6,9 +6,9 @@ import { BANNER, VIDEO } from '../src/mediaTypes.js'; import { ajax } from '../src/ajax.js'; import { getRefererInfo } from '../src/refererDetection.js'; import { Renderer } from '../src/Renderer.js'; -export const storage = getStorageManager(); 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'; diff --git a/modules/justIdSystem.js b/modules/justIdSystem.js new file mode 100644 index 00000000000..15b1c90da4e --- /dev/null +++ b/modules/justIdSystem.js @@ -0,0 +1,206 @@ +/** + * This module adds JustId to the User ID module + * The {@link module:modules/userId} module is required + * @module modules/justIdSystem + * @requires module:modules/userId + */ + +import * as utils from '../src/utils.js' +import { submodule } from '../src/hook.js' +import { loadExternalScript } from '../src/adloader.js' +import {includes} from '../src/polyfill.js'; + +const MODULE_NAME = 'justId'; +const EXTERNAL_SCRIPT_MODULE_CODE = 'justtag'; +const LOG_PREFIX = 'User ID - JustId submodule: '; +const GVLID = 160; +const DEFAULT_PARTNER = 'pbjs-just-id-module'; +const DEFAULT_ATM_VAR_NAME = '__atm'; + +const MODE_BASIC = 'BASIC'; +const MODE_COMBINED = 'COMBINED'; +const DEFAULT_MODE = MODE_BASIC; + +export const EX_URL_REQUIRED = new Error(`params.url is required in ${MODE_COMBINED} mode`); +export const EX_INVALID_MODE = new Error(`Invalid params.mode. Allowed values: ${MODE_BASIC}, ${MODE_COMBINED}`); + +/** @type {Submodule} */ +export const justIdSubmodule = { + /** + * used to link submodule with config + * @type {string} + */ + name: MODULE_NAME, + /** + * required for the gdpr enforcement module + */ + gvlid: GVLID, + + /** + * decode the stored id value for passing to bid requests + * @function + * @param {{uid:string}} value + * @returns {{justId:string}} + */ + decode(value) { + utils.logInfo(LOG_PREFIX, 'decode', value); + const justId = value && value.uid; + return justId && {justId: justId}; + }, + + /** + * 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, consentData, cacheIdObj) { + utils.logInfo(LOG_PREFIX, 'getId', config, consentData, cacheIdObj); + + var configWrapper + try { + configWrapper = new ConfigWrapper(config); + } catch (e) { + utils.logError(LOG_PREFIX, e); + } + + return configWrapper && { + callback: function(cbFun) { + try { + utils.logInfo(LOG_PREFIX, 'fetching uid...'); + + var uidProvider = configWrapper.isCombinedMode() + ? new CombinedUidProvider(configWrapper, consentData, cacheIdObj) + : new BasicUidProvider(configWrapper); + + uidProvider.getUid(justId => { + if (utils.isEmptyStr(justId)) { + utils.logError(LOG_PREFIX, 'empty uid!'); + cbFun(); + return; + } + cbFun({uid: justId}); + }, err => { + utils.logError(LOG_PREFIX, 'error during fetching', err); + cbFun(); + }); + } catch (e) { + utils.logError(LOG_PREFIX, 'Error during fetching...', e); + } + } + }; + } +}; + +export const ConfigWrapper = function(config) { + this.getConfig = function() { + return config; + } + + this.getMode = function() { + return (params().mode || DEFAULT_MODE).toUpperCase(); + } + + this.getPartner = function() { + return params().partner || DEFAULT_PARTNER; + } + + this.isCombinedMode = function() { + return this.getMode() === MODE_COMBINED; + } + + this.getAtmVarName = function() { + return params().atmVarName || DEFAULT_ATM_VAR_NAME; + } + + this.getUrl = function() { + const u = params().url; + const url = new URL(u); + url.searchParams.append('sourceId', this.getPartner()); + return url.toString(); + } + + function params() { + return config.params || {}; + } + + // validation + if (!includes([MODE_BASIC, MODE_COMBINED], this.getMode())) { + throw EX_INVALID_MODE; + } + + var url = params().url; + if (this.isCombinedMode() && (utils.isEmptyStr(url) || !utils.isStr(url))) { + throw EX_URL_REQUIRED; + } +} + +const CombinedUidProvider = function(configWrapper, consentData, cacheIdObj) { + const url = configWrapper.getUrl(); + + this.getUid = function(idCallback, errCallback) { + const scriptTag = loadExternalScript(url, EXTERNAL_SCRIPT_MODULE_CODE, () => { + utils.logInfo(LOG_PREFIX, 'script loaded', url); + + const eventDetails = { + detail: { + config: configWrapper.getConfig(), + consentData: consentData, + cacheIdObj: cacheIdObj + } + } + + scriptTag.dispatchEvent(new CustomEvent('prebidGetId', eventDetails)); + }) + + scriptTag.addEventListener('justIdReady', event => { + utils.logInfo(LOG_PREFIX, 'received justId', event); + idCallback(event.detail && event.detail.justId); + }); + + scriptTag.onerror = errCallback; + } +} + +const BasicUidProvider = function(configWrapper) { + const atmVarName = configWrapper.getAtmVarName(); + + this.getUid = function(idCallback, errCallback) { + var atm = getAtm(); + if (typeof atm !== 'function') { // it may be AsyncFunction, so we can't use utils.isFn + utils.logInfo(LOG_PREFIX, 'ATM function not found!', atmVarName, atm); + errCallback('ATM not found'); + return + } + + atm = function() { // stub is replaced after ATM is loaded so we must refer them directly by global variable + return getAtm().apply(this, arguments); + } + + atm('getReadyState', () => { + Promise.resolve(atm('getVersion')) // atm('getVersion') returns string || Promise + .then(atmVersion => { + utils.logInfo(LOG_PREFIX, 'ATM Version', atmVersion); + if (utils.isStr(atmVersion)) { // getVersion command was introduced in same ATM version as getUid command + atm('getUid', idCallback); + } else { + errCallback('ATM getUid not supported'); + } + }) + }); + } + + function getAtm() { + return jtUtils.getAtm(atmVarName); + } +} + +export const jtUtils = { + getAtm(atmVarName) { + return window[atmVarName]; + } +} + +submodule('userId', justIdSubmodule); diff --git a/modules/justIdSystem.md b/modules/justIdSystem.md new file mode 100644 index 00000000000..f58deef8010 --- /dev/null +++ b/modules/justIdSystem.md @@ -0,0 +1,70 @@ +## Just ID User ID Submodule + +For assistance setting up your module please contact us at [prebid@justtag.com](prebid@justtag.com). + +First, make sure to add the Just ID submodule to your Prebid.js package with: + +``` +gulp build --modules=userId,justIdSystem +``` + +### Modes + +- **BASIC** - in this mode we rely on Justtag library that already exists on publisher page. Typicaly that library expose global variable called `__atm` + +- **COMBINED** - Just ID generation process may differ between various cases depends on publishers. This mode combines our js library with prebid for ease of integration + +### Disclosure + +This module in `COMBINED` mode loads external JavaScript to generate optimal quality user ID. It is possible to retrieve user ID, without loading additional script by this module in `BASIC` mode. + +### Just ID Example + +ex. 1. Mode `COMBINED` + +``` +pbjs.setConfig({ + userSync: { + userIds: [{ + name: 'justId', + params: { + mode: 'COMBINED', + url: 'https://id.nsaudience.pl/getId.js', // required in COMBINED mode + partner: 'pbjs-just-id-module' // optional, may be required in some custom integrations with Justtag + } + }] + } +}); +``` + +ex. 2. Mode `BASIC` + +``` +pbjs.setConfig({ + userSync: { + userIds: [{ + name: 'justId', + params: { + mode: 'BASIC', // default + atmVarName: '__atm' // optional + } + }] + } +}); +``` + +### Prebid Params + +Individual params may be set for the Just ID Submodule. + +## Parameter Descriptions for the `userSync` Configuration Section +The below parameters apply only to the Just ID integration. + +| Param under usersync.userIds[] | Scope | Type | Description | Example | +| --- | --- | --- | --- | --- | +| name | Required | String | ID of the module - `'justId'` | `'justId'` | +| params | Optional | Object | Details for Just ID syncing. | | +| params.mode | Optional | String | Mode in which the module works. Available Modes: `'COMBINED'`, `'BASIC'`(default) | `'COMBINED'` | +| params.atmVarName | Optional | String | Name of global object property that point to Justtag ATM Library. Defaults to `'__atm'` | `'__atm'` | +| params.url | Optional | String | API Url, **required** in `COMBINED` mode | `'https://id.nsaudience.pl/getId.js'` | +| params.partner | Optional | String | This is the Justtag Partner Id which may be required in some custom integrations with Justtag | `'some-publisher'` | diff --git a/modules/justpremiumBidAdapter.js b/modules/justpremiumBidAdapter.js index 56f9935ea6e..9993421ad1a 100644 --- a/modules/justpremiumBidAdapter.js +++ b/modules/justpremiumBidAdapter.js @@ -4,7 +4,7 @@ import { deepAccess } from '../src/utils.js'; const BIDDER_CODE = 'justpremium' const GVLID = 62 const ENDPOINT_URL = 'https://pre.ads.justpremium.com/v/2.0/t/xhr' -const JP_ADAPTER_VERSION = '1.8.1' +const JP_ADAPTER_VERSION = '1.8.2' const pixels = [] export const spec = { @@ -19,6 +19,7 @@ export const spec = { buildRequests: (validBidRequests, bidderRequest) => { const c = preparePubCond(validBidRequests) const dim = getWebsiteDim() + const ggExt = getGumGumParams() const payload = { zone: validBidRequests.map(b => { return parseInt(b.params.zone) @@ -32,7 +33,8 @@ export const spec = { wh: dim.innerHeight, c: c, id: validBidRequests[0].params.zone, - sizes: {} + sizes: {}, + ggExt: ggExt } validBidRequests.forEach(b => { const zone = b.params.zone @@ -253,4 +255,19 @@ function getWebsiteDim () { } } +function getGumGumParams () { + if (!window.top) return null + + const urlParams = new URLSearchParams(window.top.location.search) + const ggParams = { + 'ggAdbuyid': urlParams.get('gg_adbuyid'), + 'ggDealid': urlParams.get('gg_dealid'), + 'ggEadbuyid': urlParams.get('gg_eadbuyid') + } + + const checkIfEmpty = (obj) => Object.keys(obj).length === 0 ? null : obj + const removeNullEntries = (obj) => Object.fromEntries(Object.entries(obj).filter(([_, v]) => v != null)) + return checkIfEmpty(removeNullEntries(ggParams)) +} + registerBidder(spec) diff --git a/modules/jwplayerRtdProvider.js b/modules/jwplayerRtdProvider.js index 8332e720ae7..814aed59d85 100644 --- a/modules/jwplayerRtdProvider.js +++ b/modules/jwplayerRtdProvider.js @@ -9,12 +9,12 @@ * @requires module:modules/realTimeData */ -import { submodule } from '../src/hook.js'; -import { config } from '../src/config.js'; -import { ajaxBuilder } from '../src/ajax.js'; -import { logError } from '../src/utils.js'; -import find from 'core-js-pure/features/array/find.js'; -import { getGlobal } from '../src/prebidGlobal.js'; +import {submodule} from '../src/hook.js'; +import {config} from '../src/config.js'; +import {ajaxBuilder} from '../src/ajax.js'; +import {logError} from '../src/utils.js'; +import {find} from '../src/polyfill.js'; +import {getGlobal} from '../src/prebidGlobal.js'; const SUBMODULE_NAME = 'jwplayer'; const segCache = {}; @@ -155,8 +155,10 @@ export function enrichAdUnits(adUnits) { if (!vat) { return; } + const contentId = getContentId(vat.mediaID); + const contentData = getContentData(vat.segments); const targeting = formatTargetingResponse(vat); - addTargetingToBids(adUnit.bids, targeting); + enrichBids(adUnit.bids, targeting, contentId, contentData); }; loadVat(jwTargeting, onVatResponse); }); @@ -235,6 +237,9 @@ export function getVatFromPlayer(playerID, mediaID) { }; } +/* + deprecated + */ export function formatTargetingResponse(vat) { const { segments, mediaID } = vat; const targeting = {}; @@ -243,23 +248,83 @@ export function formatTargetingResponse(vat) { } if (mediaID) { - const id = 'jw_' + mediaID; targeting.content = { - id + id: getContentId(mediaID) } } return targeting; } -function addTargetingToBids(bids, targeting) { - if (!bids || !targeting) { +export function getContentId(mediaID) { + if (!mediaID) { + return; + } + + return 'jw_' + mediaID; +} + +export function getContentData(segments) { + if (!segments || !segments.length) { + return; + } + + const formattedSegments = segments.reduce((convertedSegments, rawSegment) => { + convertedSegments.push({ + id: rawSegment, + value: rawSegment + }); + return convertedSegments; + }, []); + + return { + name: 'jwplayer', + ext: { + segtax: 502 + }, + segment: formattedSegments + }; +} + +export function addOrtbSiteContent(bid, contentId, contentData) { + if (!contentId && !contentData) { return; } - bids.forEach(bid => addTargetingToBid(bid, targeting)); + let ortb2 = bid.ortb2 || {}; + let site = ortb2.site = ortb2.site || {}; + let content = site.content = site.content || {}; + + if (contentId) { + content.id = contentId; + } + + if (contentData) { + const data = content.data = content.data || []; + data.push(contentData); + } + + bid.ortb2 = ortb2; } +function enrichBids(bids, targeting, contentId, contentData) { + if (!bids) { + return; + } + + bids.forEach(bid => { + addTargetingToBid(bid, targeting); + addOrtbSiteContent(bid, contentId, contentData); + }); +} + +/* + deprecated + */ export function addTargetingToBid(bid, targeting) { + if (!targeting) { + return; + } + const rtd = bid.rtd || {}; const jwRtd = {}; jwRtd[SUBMODULE_NAME] = Object.assign({}, rtd[SUBMODULE_NAME], { targeting }); diff --git a/modules/jwplayerRtdProvider.md b/modules/jwplayerRtdProvider.md index 7fb1bb13d74..77f65909040 100644 --- a/modules/jwplayerRtdProvider.md +++ b/modules/jwplayerRtdProvider.md @@ -81,20 +81,30 @@ realTimeData = { # Usage for Bid Adapters: Implement the `buildRequests` function. When it is called, the `bidRequests` param will be an array of bids. -Each bid for which targeting information was found will conform to the following object structure: +Each bid for which targeting information was found will have a ortb2 param conforming to the [oRTB v2 object structure](https://www.iab.com/wp-content/uploads/2016/03/OpenRTB-API-Specification-Version-2-5-FINAL.pdf). The `ortb2` object will contain our proprietaty targeting segments in a format compliant with the [IAB's segment taxonomy structure](https://github.com/InteractiveAdvertisingBureau/openrtb/blob/master/extensions/community_extensions/segtax.md). + +Example: ```javascript { adUnitCode: 'xyz', bidId: 'abc', ..., - rtd: { - jwplayer: { - targeting: { - segments: ['123', '456'], - content: { - id: 'jw_abc123' - } + ortb2: { + site: { + content: { + id: 'jw_abc123', + data: [{ + name: 'jwplayer', + ext: { + segtax: 502 + }, + segment: [{ + id: '123' + }, { + id: '456' + }] + }] } } } @@ -102,9 +112,15 @@ Each bid for which targeting information was found will conform to the following ``` where: -- `segments` is an array of jwpseg targeting segments, of type string. -- `content` is an object containing metadata for the media. It may contain the following information: - - `id` is a unique identifier for the specific media asset. +- `ortb2` is an object containing first party data + - `site` is an object containing page specific information + - `content` is an object containing metadata for the media. It may contain the following information: + - `id` is a unique identifier for the specific media asset + - `data` is an array containing segment taxonomy objects that have the following parameters: + - `name` is the `jwplayer` string indicating the provider name + - `ext.segtax` whose `502` value is the unique identifier for JW Player's proprietary taxonomy + - `segment` is an array containing the segment taxonomy values as an object where: + - `id` is the string representation of the data segment value. **Example:** diff --git a/modules/kargoBidAdapter.js b/modules/kargoBidAdapter.js index ff56c97e7b7..098e38b2c43 100644 --- a/modules/kargoBidAdapter.js +++ b/modules/kargoBidAdapter.js @@ -10,7 +10,7 @@ const SYNC = 'https://crb.kargo.com/api/v1/initsyncrnd/{UUID}?seed={SEED}&idx={I const SYNC_COUNT = 5; const GVLID = 972; const SUPPORTED_MEDIA_TYPES = [BANNER, VIDEO] -const storage = getStorageManager(GVLID, BIDDER_CODE); +const storage = getStorageManager({gvlid: GVLID, bidderCode: BIDDER_CODE}); let sessionId, lastPageUrl, @@ -87,7 +87,7 @@ export const spec = { creativeId: adUnit.id, dealId: adUnit.targetingCustom, netRevenue: true, - currency: bidRequest.currency, + currency: adUnit.currency || bidRequest.currency, meta: meta }); } @@ -172,28 +172,6 @@ export const spec = { return spec._getCrbFromCookie(); }, - _getKruxUserId() { - return spec._getLocalStorageSafely('kxkar_user'); - }, - - _getKruxSegments() { - return spec._getLocalStorageSafely('kxkar_segs'); - }, - - _getKrux() { - const segmentsStr = spec._getKruxSegments(); - let segments = []; - - if (segmentsStr) { - segments = segmentsStr.split(','); - } - - return { - userID: spec._getKruxUserId(), - segments: segments - }; - }, - _getLocalStorageSafely(key) { try { return storage.getDataFromLocalStorage(key); @@ -205,7 +183,7 @@ export const spec = { _getUserIds(tdid, usp, gdpr) { const crb = spec._getCrb(); const userIds = { - kargoID: crb.userId, + kargoID: crb.lexId, clientID: crb.clientId, crbIDs: crb.syncIds || {}, optOut: crb.optOut, @@ -235,7 +213,6 @@ export const spec = { _getAllMetadata(tdid, usp, gdpr) { return { userIDs: spec._getUserIds(tdid, usp, gdpr), - krux: spec._getKrux(), pageURL: window.location.href, rawCRB: spec._readCookie('krg_crb'), rawCRBLocalStorage: spec._getLocalStorageSafely('krg_crb') diff --git a/modules/koblerBidAdapter.js b/modules/koblerBidAdapter.js index 49be80e969c..80aa038a9f7 100644 --- a/modules/koblerBidAdapter.js +++ b/modules/koblerBidAdapter.js @@ -63,7 +63,7 @@ export const onBidWon = function (bid) { const adServerPrice = deepAccess(bid, 'adserverTargeting.hb_pb', 0); const adServerPriceCurrency = config.getConfig('currency.adServerCurrency') || SUPPORTED_CURRENCY; if (isStr(bid.nurl) && bid.nurl !== '') { - const winNotificationUrl = replaceAuctionPrice(bid.nurl, cpm) + const winNotificationUrl = replaceAuctionPrice(bid.nurl, bid.originalCpm || cpm) .replace(/\${AUCTION_PRICE_CURRENCY}/g, cpmCurrency) .replace(/\${AD_SERVER_PRICE}/g, adServerPrice) .replace(/\${AD_SERVER_PRICE_CURRENCY}/g, adServerPriceCurrency); diff --git a/modules/kubientBidAdapter.js b/modules/kubientBidAdapter.js index 07c614230a7..46360572576 100644 --- a/modules/kubientBidAdapter.js +++ b/modules/kubientBidAdapter.js @@ -1,6 +1,7 @@ import { isArray, deepAccess } from '../src/utils.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import {BANNER, VIDEO} from '../src/mediaTypes.js'; +import { config } from '../src/config.js'; const BIDDER_CODE = 'kubient'; const END_POINT = 'https://kssp.kbntx.ch/kubprebidjs'; @@ -23,22 +24,23 @@ export const spec = { return; } return validBidRequests.map(function (bid) { - let floor = 0.0; + let adSlot = { + bidId: bid.bidId, + zoneId: bid.params.zoneid || '' + }; + if (typeof bid.getFloor === 'function') { const mediaType = (Object.keys(bid.mediaTypes).length == 1) ? Object.keys(bid.mediaTypes)[0] : '*'; const sizes = bid.sizes || '*'; const floorInfo = bid.getFloor({currency: 'USD', mediaType: mediaType, size: sizes}); - if (typeof floorInfo === 'object' && floorInfo.currency === 'USD' && !isNaN(parseFloat(floorInfo.floor))) { - floor = parseFloat(floorInfo.floor); + if (typeof floorInfo === 'object' && floorInfo.currency === 'USD') { + let floor = parseFloat(floorInfo.floor) + if (!isNaN(floor) && floor > 0) { + adSlot.floor = parseFloat(floorInfo.floor); + } } } - let adSlot = { - bidId: bid.bidId, - zoneId: bid.params.zoneid || '', - floor: floor || 0.0 - }; - if (bid.mediaTypes.banner) { adSlot.banner = bid.mediaTypes.banner; } @@ -59,7 +61,11 @@ export const spec = { gdpr: (bidderRequest.gdprConsent && bidderRequest.gdprConsent.gdprApplies) ? 1 : 0, consentGiven: kubientGetConsentGiven(bidderRequest.gdprConsent), uspConsent: bidderRequest.uspConsent - }; + } + + if (config.getConfig('coppa') === true) { + data.coppa = 1 + } if (bidderRequest.refererInfo && bidderRequest.refererInfo.referer) { data.referer = bidderRequest.refererInfo.referer @@ -109,31 +115,39 @@ export const spec = { return bidResponses; }, getUserSyncs: function (syncOptions, serverResponses, gdprConsent, uspConsent) { - const syncs = []; - let gdprParams = ''; - if (gdprConsent && typeof gdprConsent.consentString === 'string') { - gdprParams = `?consent_str=${gdprConsent.consentString}`; + let kubientSync = kubientGetSyncInclude(config); + + if (!syncOptions.pixelEnabled || kubientSync.image === 'exclude') { + return []; + } + + let values = {}; + if (gdprConsent) { if (typeof gdprConsent.gdprApplies === 'boolean') { - gdprParams = gdprParams + `&gdpr=${Number(gdprConsent.gdprApplies)}`; + values['gdpr'] = Number(gdprConsent.gdprApplies); + } + if (typeof gdprConsent.consentString === 'string') { + values['consent'] = gdprConsent.consentString; } - gdprParams = gdprParams + `&consent_given=` + kubientGetConsentGiven(gdprConsent); - } - if (syncOptions.iframeEnabled) { - syncs.push({ - type: 'iframe', - url: 'https://kdmp.kbntx.ch/init.html' + gdprParams - }); } - if (syncOptions.pixelEnabled) { - syncs.push({ - type: 'image', - url: 'https://kdmp.kbntx.ch/init.png' + gdprParams - }); + + if (uspConsent) { + values['usp'] = uspConsent; } - return syncs; + + return [{ + type: 'image', + url: 'https://matching.kubient.net/match/sp?' + encodeQueryData(values) + }]; } }; +function encodeQueryData(data) { + return Object.keys(data).map(function(key) { + return [key, data[key]].map(encodeURIComponent).join('='); + }).join('&'); +} + function kubientGetConsentGiven(gdprConsent) { let consentGiven = 0; if (typeof gdprConsent !== 'undefined') { @@ -149,4 +163,22 @@ function kubientGetConsentGiven(gdprConsent) { } return consentGiven; } + +function kubientGetSyncInclude(config) { + try { + let kubientSync = {}; + if (config.getConfig('userSync').filterSettings != null && typeof config.getConfig('userSync').filterSettings != 'undefined') { + let filterSettings = config.getConfig('userSync').filterSettings + if (filterSettings.iframe !== null && typeof filterSettings.iframe !== 'undefined') { + kubientSync.iframe = ((isArray(filterSettings.image.bidders) && filterSettings.iframe.bidders.indexOf('kubient') !== -1) || filterSettings.iframe.bidders === '*') ? filterSettings.iframe.filter : 'exclude'; + } + if (filterSettings.image !== null && typeof filterSettings.image !== 'undefined') { + kubientSync.image = ((isArray(filterSettings.image.bidders) && filterSettings.image.bidders.indexOf('kubient') !== -1) || filterSettings.image.bidders === '*') ? filterSettings.image.filter : 'exclude'; + } + } + return kubientSync; + } catch (e) { + return null; + } +} registerBidder(spec); diff --git a/modules/limelightDigitalBidAdapter.js b/modules/limelightDigitalBidAdapter.js index b04b2124dd8..a278a587038 100644 --- a/modules/limelightDigitalBidAdapter.js +++ b/modules/limelightDigitalBidAdapter.js @@ -1,7 +1,7 @@ -import { logMessage, groupBy, uniques, flatten, deepAccess } from '../src/utils.js'; +import { logMessage, groupBy, flatten, uniques } from '../src/utils.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; import { BANNER, VIDEO } from '../src/mediaTypes.js'; -import {ajax} from '../src/ajax.js'; +import { ajax } from '../src/ajax.js'; const BIDDER_CODE = 'limelightDigital'; @@ -94,23 +94,20 @@ export const spec = { }, getUserSyncs: (syncOptions, serverResponses, gdprConsent, uspConsent) => { - const syncs = serverResponses.map(response => response.body).reduce(flatten, []) - .map(response => deepAccess(response, 'ext.sync')).filter(Boolean); - const iframeSyncUrls = !syncOptions.iframeEnabled ? [] : syncs.map(sync => sync.iframe).filter(Boolean) - .filter(uniques).map(url => { - return { - type: 'iframe', - url: url - } - }); - const pixelSyncUrls = !syncOptions.pixelEnabled ? [] : syncs.map(sync => sync.pixel).filter(Boolean) - .filter(uniques).map(url => { - return { - type: 'image', - url: url - } - }); - return [iframeSyncUrls, pixelSyncUrls].reduce(flatten, []); + const iframeSyncs = []; + const imageSyncs = []; + for (let i = 0; i < serverResponses.length; i++) { + const serverResponseHeaders = serverResponses[i].headers; + const imgSync = (serverResponseHeaders != null && syncOptions.pixelEnabled) ? serverResponseHeaders.get('X-PLL-UserSync-Image') : null + const iframeSync = (serverResponseHeaders != null && syncOptions.iframeEnabled) ? serverResponseHeaders.get('X-PLL-UserSync-Iframe') : null + if (iframeSync != null) { + iframeSyncs.push(iframeSync) + } else if (imgSync != null) { + imageSyncs.push(imgSync) + } + } + return [iframeSyncs.filter(uniques).map(it => { return { type: 'iframe', url: it } }), + imageSyncs.filter(uniques).map(it => { return { type: 'image', url: it } })].reduce(flatten, []).filter(uniques); } }; diff --git a/modules/limelightDigitalBidAdapter.md b/modules/limelightDigitalBidAdapter.md index ab69ef8eaa4..a4abb6f1411 100644 --- a/modules/limelightDigitalBidAdapter.md +++ b/modules/limelightDigitalBidAdapter.md @@ -22,7 +22,7 @@ var adUnits = [{ bids: [{ bidder: 'limelightDigital', params: { - host: 'exchange.ortb.net', + host: 'exchange-9qao.ortb.net', adUnitId: 0, adUnitType: 'banner' } @@ -38,7 +38,7 @@ var videoAdUnit = [{ bids: [{ bidder: 'limelightDigital', params: { - host: 'exchange.ortb.net', + host: 'exchange-9qao.ortb.net', adUnitId: 0, adUnitType: 'video' } diff --git a/modules/liveIntentIdSystem.js b/modules/liveIntentIdSystem.js index 91415daa497..68cbb3b2412 100644 --- a/modules/liveIntentIdSystem.js +++ b/modules/liveIntentIdSystem.js @@ -13,7 +13,7 @@ import { getStorageManager } from '../src/storageManager.js'; import { MinimalLiveConnect } from 'live-connect-js/esm/minimal-live-connect.js'; const MODULE_NAME = 'liveIntentId'; -export const storage = getStorageManager(null, MODULE_NAME); +export const storage = getStorageManager({gvlid: null, moduleName: MODULE_NAME}); const calls = { ajaxGet: (url, onSuccess, onError, timeout) => { ajaxBuilder(timeout)( diff --git a/modules/livewrappedAnalyticsAdapter.js b/modules/livewrappedAnalyticsAdapter.js index 25b919956e0..1116fd99ba0 100644 --- a/modules/livewrappedAnalyticsAdapter.js +++ b/modules/livewrappedAnalyticsAdapter.js @@ -86,6 +86,8 @@ let livewrappedAnalyticsAdapter = Object.assign(adapter({EMPTYURL, ANALYTICSTYPE bidResponse.readyToSend = 1; bidResponse.mediaType = args.mediaType == 'native' ? 2 : (args.mediaType == 'video' ? 4 : 1); bidResponse.floorData = args.floorData; + bidResponse.meta = args.meta; + if (!bidResponse.ttr) { bidResponse.ttr = time - bidResponse.start; } @@ -115,6 +117,8 @@ let livewrappedAnalyticsAdapter = Object.assign(adapter({EMPTYURL, ANALYTICSTYPE wonBid.won = true; wonBid.floorData = args.floorData; wonBid.rUp = args.rUp; + wonBid.meta = args.meta; + wonBid.dealId = args.dealId; if (wonBid.sendStatus != 0) { livewrappedAnalyticsAdapter.sendEvents(); } @@ -251,7 +255,8 @@ function getResponses(gdpr, auctionIds) { auctionId: auctionIdPos, auc: bid.auc, buc: bid.buc, - lw: bid.lw + lw: bid.lw, + meta: bid.meta }); } }); @@ -290,7 +295,9 @@ function getWins(gdpr, auctionIds) { auc: bid.auc, buc: bid.buc, lw: bid.lw, - rUp: bid.rUp + rUp: bid.rUp, + meta: bid.meta, + dealId: bid.dealId }); } }); diff --git a/modules/livewrappedBidAdapter.js b/modules/livewrappedBidAdapter.js index 6b7c055b295..32e09e4b28e 100644 --- a/modules/livewrappedBidAdapter.js +++ b/modules/livewrappedBidAdapter.js @@ -1,13 +1,12 @@ -import { isSafariBrowser, deepAccess, getWindowTop, mergeDeep } from '../src/utils.js'; -import { registerBidder } from '../src/adapters/bidderFactory.js'; -import { config } from '../src/config.js'; -import find from 'core-js-pure/features/array/find.js'; -import { BANNER, NATIVE, VIDEO } from '../src/mediaTypes.js'; -import { getStorageManager } from '../src/storageManager.js'; - -export const storage = getStorageManager(); +import {deepAccess, getWindowTop, isSafariBrowser, mergeDeep} 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 {getStorageManager} from '../src/storageManager.js'; const BIDDER_CODE = 'livewrapped'; +export const storage = getStorageManager({bidderCode: BIDDER_CODE}); export const URL = 'https://lwadm.com/ad'; const VERSION = '1.4'; diff --git a/modules/lkqdBidAdapter.js b/modules/lkqdBidAdapter.js new file mode 100644 index 00000000000..275ab38915d --- /dev/null +++ b/modules/lkqdBidAdapter.js @@ -0,0 +1,234 @@ +import { logError, _each, generateUUID, buildUrl } from '../src/utils.js'; +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { config } from '../src/config.js'; +import { VIDEO } from '../src/mediaTypes.js'; + +const BIDDER_CODE = 'lkqd'; +const BID_TTL_DEFAULT = 300; +const MIMES_TYPES = ['application/x-mpegURL', 'video/mp4', 'video/H264']; +const PROTOCOLS = [1, 2, 3, 4, 5, 6, 7, 8]; + +const PARAM_VOLUME_DEFAULT = '100'; +const DEFAULT_SIZES = [[640, 480]]; + +function calculateSizes(VIDEO_BID, bid) { + const userProvided = bid.sizes && Array.isArray(bid.sizes) ? (Array.isArray(bid.sizes[0]) ? bid.sizes : [bid.sizes]) : DEFAULT_SIZES; + const preBidProvided = VIDEO_BID.playerSize && Array.isArray(VIDEO_BID.playerSize) ? (Array.isArray(VIDEO_BID.playerSize[0]) ? VIDEO_BID.playerSize : [VIDEO_BID.playerSize]) : null; + + return preBidProvided || userProvided; +} + +function isSet(value) { + return value != null; +} + +export const spec = { + code: BIDDER_CODE, + aliases: [], + supportedMediaTypes: [VIDEO], + isBidRequestValid: function(bid) { + return bid.bidder === BIDDER_CODE && bid.params && Object.keys(bid.params).length > 0 && + ((isSet(bid.params.publisherId) && parseInt(bid.params.publisherId) > 0) || (isSet(bid.params.placementId) && parseInt(bid.params.placementId) > 0)) && + bid.params.siteId != null; + }, + buildRequests: function(validBidRequests, bidderRequest) { + const BIDDER_REQUEST = bidderRequest || {}; + const serverRequestObjects = []; + const UTC_OFFSET = new Date().getTimezoneOffset(); + const UA = navigator.userAgent; + const IP = navigator.ip ? navigator.ip : 'prebid.js'; + const USP = BIDDER_REQUEST.uspConsent || null; + const REFERER = BIDDER_REQUEST.refererInfo ? new URL(BIDDER_REQUEST.refererInfo.referer).hostname : window.location.hostname; + const BIDDER_GDPR = BIDDER_REQUEST.gdprConsent && BIDDER_REQUEST.gdprConsent.gdprApplies ? 1 : null; + const BIDDER_GDPRS = BIDDER_REQUEST.gdprConsent && BIDDER_REQUEST.gdprConsent.consentString ? BIDDER_REQUEST.gdprConsent.consentString : null; + + _each(validBidRequests, (bid) => { + const DOMAIN = bid.params.pageurl || REFERER; + const GDPR = BIDDER_GDPR || bid.params.gdpr || null; + const GDPRS = BIDDER_GDPRS || bid.params.gdprs || null; + const DNT = bid.params.dnt || null; + const BID_FLOOR = bid.params.flrd > bid.params.flrmp ? bid.params.flrd : bid.params.flrmp; + const VIDEO_BID = bid.video ? bid.video : {}; + + const requestData = { + id: generateUUID(), + imp: [], + site: { + domain: DOMAIN + }, + device: { + ua: UA, + geo: { + utcoffset: UTC_OFFSET + }, + ip: IP + }, + user: { + ext: {} + }, + test: 0, + at: 2, + tmax: bid.params.timeout || config.getConfig('bidderTimeout') || 100, + cur: ['USD'], + regs: { + ext: { + us_privacy: USP + } + } + } + + if (isSet(DNT)) { + requestData.device.dnt = DNT; + } + + if (isSet(config.getConfig('coppa'))) { + requestData.regs.coppa = config.getConfig('coppa') === true ? 1 : 0; + } + + if (isSet(GDPR)) { + requestData.regs.ext.gdpr = GDPR; + requestData.regs.ext.gdprs = GDPRS; + } + + if (isSet(bid.params.aid) || isSet(bid.params.appname) || isSet(bid.params.bundleid)) { + requestData.app = { + id: bid.params.aid, + name: bid.params.appname, + bundle: bid.params.bundleid + } + + if (bid.params.contentId) { + requestData.app.content = { + id: bid.params.contentId, + title: bid.params.contentTitle, + len: bid.params.contentLength, + url: bid.params.contentUrl + }; + } + } + + if (isSet(bid.params.idfa) || isSet(bid.params.aid)) { + requestData.device.ifa = bid.params.idfa || bid.params.aid; + } + + if (bid.schain) { + requestData.source = { + ext: { + schain: bid.schain + } + }; + } else if (bid.params.schain) { + const section = bid.params.schain.split('!'); + const verComplete = section[0].split(','); + const node = section[1].split(','); + + requestData.source = { + ext: { + schain: { + validation: 'strict', + config: { + ver: verComplete[0], + complete: parseInt(verComplete[1]), + nodes: [ + { + asi: decodeURIComponent(node[0]), + sid: decodeURIComponent(node[1]), + hp: parseInt(node[2]), + rid: decodeURIComponent(node[3]), + name: decodeURIComponent(node[4]), + domain: decodeURIComponent(node[5]) + } + ] + } + } + } + }; + } + + _each(calculateSizes(VIDEO_BID, bid), (sizes) => { + const impObj = { + id: generateUUID(), + displaymanager: bid.bidder, + bidfloor: BID_FLOOR, + video: { + mimes: VIDEO_BID.mimes || MIMES_TYPES, + protocols: VIDEO_BID.protocols || PROTOCOLS, + nvol: bid.params.volume || PARAM_VOLUME_DEFAULT, + w: sizes[0], + h: sizes[1], + skip: VIDEO_BID.skip || 0, + playbackmethod: VIDEO_BID.playbackmethod || [1], + placement: (bid.params.execution === 'outstream' || VIDEO_BID.context === 'outstream') ? 5 : 1, + ext: { + lkqdcustomparameters: {} + }, + }, + bidfloorcur: 'USD', + secure: 1 + }; + + for (let k = 1; k <= 40; k++) { + if (bid.params.hasOwnProperty(`c${k}`) && bid.params[`c${k}`]) { + impObj.video.ext.lkqdcustomparameters[`c${k}`] = bid.params[`c${k}`]; + } + } + + requestData.imp.push(impObj); + }); + + serverRequestObjects.push({ + method: 'POST', + url: buildUrl({ + protocol: 'https', + hostname: 'rtb.lkqd.net', + pathname: '/ad', + search: { + pid: bid.params.publisherId || bid.params.placementId, + sid: bid.params.siteId, + output: 'rtb', + prebid: true + } + }), + data: requestData + }); + }); + + return serverRequestObjects; + }, + interpretResponse: function(serverResponse, bidRequest) { + const serverBody = serverResponse.body; + const bidResponses = []; + + if (serverBody && serverBody.seatbid) { + _each(serverBody.seatbid, (seatbid) => { + _each(seatbid.bid, (bid) => { + if (bid.price > 0) { + const bidResponse = { + requestId: bidRequest.id, + creativeId: bid.crid, + cpm: bid.price, + width: bid.w, + height: bid.h, + currency: serverBody.cur, + netRevenue: true, + ttl: BID_TTL_DEFAULT, + ad: bid.adm, + meta: { + advertiserDomains: bid.adomain && Array.isArray(bid.adomain) ? bid.adomain : [], + mediaType: VIDEO + } + }; + + bidResponses.push(bidResponse); + } + }); + }); + } else { + logError('Error: No server response or server response was empty for the requested URL'); + } + + return bidResponses; + } +} + +registerBidder(spec); diff --git a/modules/lkqdBidAdapter.md b/modules/lkqdBidAdapter.md index 1bd57ced4e0..9d7d24edda7 100644 --- a/modules/lkqdBidAdapter.md +++ b/modules/lkqdBidAdapter.md @@ -12,7 +12,7 @@ Connects to LKQD exchange for bids. LKQD bid adapter supports Video ads currently. -For more information about [LKQD Ad Serving and Management](http://www.lkqd.com/ad-serving-and-management/), please contact [info@lkqd.com](info@lkqd.com). +For more information about [LKQD Ad Serving and Management](http://www.lkqd.com/ad-serving-and-management/), please contact [vgi-video-prebid@verve.com](vgi-video-prebid@verve.com). # Sample Ad Unit: For Publishers ```javascript diff --git a/modules/lotamePanoramaIdSystem.js b/modules/lotamePanoramaIdSystem.js index 82503a57e9e..a03626d4a1f 100644 --- a/modules/lotamePanoramaIdSystem.js +++ b/modules/lotamePanoramaIdSystem.js @@ -29,7 +29,7 @@ const DAY_MS = 60 * 60 * 24 * 1000; const MISSING_CORE_CONSENT = 111; const GVLID = 95; -export const storage = getStorageManager(GVLID, MODULE_NAME); +export const storage = getStorageManager({gvlid: GVLID, moduleName: MODULE_NAME}); let cookieDomain; /** diff --git a/modules/lunamediahbBidAdapter.js b/modules/lunamediahbBidAdapter.js index 2798eef33e4..ebd88d34940 100644 --- a/modules/lunamediahbBidAdapter.js +++ b/modules/lunamediahbBidAdapter.js @@ -1,9 +1,11 @@ import { logMessage } 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'; const BIDDER_CODE = 'lunamediahb'; const AD_URL = 'https://balancer.lmgssp.com/?c=o&m=multi'; +const SYNC_URL = 'https://cookie.lmgssp.com'; function isBidResponseValid(bid) { if (!bid.requestId || !bid.cpm || !bid.creativeId || @@ -14,7 +16,7 @@ function isBidResponseValid(bid) { case BANNER: return Boolean(bid.width && bid.height && bid.ad); case VIDEO: - return Boolean(bid.vastUrl); + return Boolean(bid.vastUrl) || Boolean(bid.vastXml); case NATIVE: return Boolean(bid.native && bid.native.impressionTrackers); default: @@ -74,10 +76,13 @@ export const spec = { if (mediaType && mediaType[BANNER] && mediaType[BANNER].sizes) { placement.sizes = mediaType[BANNER].sizes; placement.traffic = BANNER; - } else if (mediaType && mediaType[VIDEO] && mediaType[VIDEO].playerSize) { - placement.wPlayer = mediaType[VIDEO].playerSize[0]; - placement.hPlayer = mediaType[VIDEO].playerSize[1]; + } else if (mediaType && mediaType[VIDEO]) { + if (mediaType[VIDEO].playerSize) { + placement.wPlayer = mediaType[VIDEO].playerSize[0]; + placement.hPlayer = mediaType[VIDEO].playerSize[1]; + } placement.traffic = VIDEO; + placement.videoContext = mediaType[VIDEO].context || 'instream' } else if (mediaType && mediaType[NATIVE]) { placement.native = mediaType[NATIVE]; placement.traffic = NATIVE; @@ -102,6 +107,29 @@ export const spec = { } 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/malltvBidAdapter.js b/modules/malltvBidAdapter.js index a7c5b2a9dde..53f745d4004 100644 --- a/modules/malltvBidAdapter.js +++ b/modules/malltvBidAdapter.js @@ -9,7 +9,7 @@ const SIZE_SEPARATOR = ';'; const BISKO_ID = 'biskoId'; const STORAGE_ID = 'bisko-sid'; const SEGMENTS = 'biskoSegments'; -const storage = getStorageManager(); +const storage = getStorageManager({bidderCode: BIDDER_CODE}); export const spec = { code: BIDDER_CODE, @@ -41,7 +41,8 @@ export const spec = { let contents = []; let data = {}; let auctionId = bidderRequest ? bidderRequest.auctionId : ''; - + let gdrpApplies = true; + let gdprConsent = ''; let placements = validBidRequests.map(bidRequest => { if (!propertyId) { propertyId = bidRequest.params.propertyId; } if (!pageViewGuid && bidRequest.params) { pageViewGuid = bidRequest.params.pageViewGuid || ''; } @@ -49,7 +50,8 @@ export const spec = { if (!url && bidderRequest) { url = bidderRequest.refererInfo.referer; } if (!contents.length && bidRequest.params.contents && bidRequest.params.contents.length) { contents = bidRequest.params.contents; } if (Object.keys(data).length === 0 && bidRequest.params.data && Object.keys(bidRequest.params.data).length !== 0) { data = bidRequest.params.data; } - + if (bidderRequest && bidRequest.gdprConsent) { gdrpApplies = bidderRequest.gdprConsent && bidderRequest.gdprConsent.gdprApplies ? bidderRequest.gdprConsent.gdprApplies : true; } + if (bidderRequest && bidRequest.gdprConsent) { gdprConsent = bidderRequest.gdprConsent && bidderRequest.gdprConsent.consentString ? bidderRequest.gdprConsent.consentString : ''; } let adUnitId = bidRequest.adUnitCode; let placementId = bidRequest.params.placementId; let sizes = generateSizeParam(bidRequest.sizes); @@ -75,7 +77,9 @@ export const spec = { requestid: bidderRequestId, placements: placements, contents: contents, - data: data + data: data, + gdpr_applies: gdrpApplies, + gdpr_consent: gdprConsent, } return [{ diff --git a/modules/malltvBidAdapter.md b/modules/malltvBidAdapter.md index e32eb54f90f..6b695ee8526 100644 --- a/modules/malltvBidAdapter.md +++ b/modules/malltvBidAdapter.md @@ -1,68 +1,81 @@ # Overview + Module Name: MallTV Bidder Adapter Module Type: Bidder Adapter -Maintainer: arditb@gjirafa.com +Maintainer: myhedin@gjirafa.com # Description + MallTV Bidder Adapter for Prebid.js. # Test Parameters + ```js var adUnits = [ - { - code: 'test-div', - mediaTypes: { - banner: { - sizes: [ - [300, 250], - [300, 300] - ] - } + { + code: "test-div", + mediaTypes: { + banner: { + sizes: [ + [300, 250], + [300, 300], + ], + }, + }, + bids: [ + { + bidder: "malltv", + params: { + propertyId: "105134", //Required + placementId: "846832", //Required + data: { + //Optional + catalogs: [ + { + catalogId: 9, + items: ["193", "4", "1"], + }, + ], + inventory: { + category: ["tech"], + query: ["iphone 12"], + }, + }, }, - bids: [{ - bidder: 'malltv', - params: { - propertyId: '105134', //Required - placementId: '846832', //Required - data: { //Optional - catalogs: [{ - catalogId: 9, - items: ["193", "4", "1"] - }], - inventory: { - category: ["tech"], - query: ["iphone 12"] - } - } - } - }] + }, + ], + }, + { + code: "test-div", + mediaTypes: { + video: { + context: "instream", + }, }, - { - code: 'test-div', - mediaTypes: { - video: { - context: 'instream' - } + bids: [ + { + bidder: "malltv", + params: { + propertyId: "105134", //Required + placementId: "846832", //Required + data: { + //Optional + catalogs: [ + { + catalogId: 9, + items: ["193", "4", "1"], + }, + ], + inventory: { + category: ["tech"], + query: ["iphone 12"], + }, + }, }, - bids: [{ - bidder: 'malltv', - params: { - propertyId: '105134', //Required - placementId: '846832', //Required - data: { //Optional - catalogs: [{ - catalogId: 9, - items: ["193", "4", "1"] - }], - inventory: { - category: ["tech"], - query: ["iphone 12"] - } - } - } - }] - } + }, + ], + }, ]; ``` diff --git a/modules/mantisBidAdapter.js b/modules/mantisBidAdapter.js index 61b7c31c8e4..8d62b0ffba7 100644 --- a/modules/mantisBidAdapter.js +++ b/modules/mantisBidAdapter.js @@ -1,7 +1,7 @@ import {registerBidder} from '../src/adapters/bidderFactory.js'; import { getStorageManager } from '../src/storageManager.js'; -export const storage = getStorageManager(); +export const storage = getStorageManager({bidderCode: 'mantis'}); function inIframe() { try { diff --git a/modules/marsmediaBidAdapter.js b/modules/marsmediaBidAdapter.js index 79e2148084a..92374b748c7 100644 --- a/modules/marsmediaBidAdapter.js +++ b/modules/marsmediaBidAdapter.js @@ -16,8 +16,7 @@ function MarsmediaAdapter() { let SUPPORTED_VIDEO_DELIVERY = [1]; let SUPPORTED_VIDEO_API = [1, 2, 5]; let slotsToBids = {}; - let that = this; - let version = '2.4'; + let version = '2.5'; this.isBidRequestValid = function (bid) { return !!(bid.params && bid.params.zoneId); @@ -288,7 +287,6 @@ function MarsmediaAdapter() { let bidRequest = slotsToBids[bid.impid]; let bidResponse = { requestId: bidRequest.bidId, - bidderCode: that.code, cpm: parseFloat(bid.price), width: bid.w, height: bid.h, diff --git a/modules/mass.js b/modules/mass.js index 01135e7ddff..f38f833f4d3 100644 --- a/modules/mass.js +++ b/modules/mass.js @@ -2,9 +2,9 @@ * This module adds MASS support to Prebid.js. */ -import { config } from '../src/config.js'; -import { getHook } from '../src/hook.js'; -import find from 'core-js-pure/features/array/find.js'; +import {config} from '../src/config.js'; +import {getHook} from '../src/hook.js'; +import {auctionManager} from '../src/auctionManager.js'; const defaultCfg = { dealIdPattern: /^MASS/i @@ -78,7 +78,7 @@ export function updateRenderers() { /** * Before hook for 'addBidResponse'. */ -export function addBidResponseHook(next, adUnitCode, bid) { +export function addBidResponseHook(next, adUnitCode, bid, {index = auctionManager.index} = {}) { let renderer; for (let i = 0; i < renderers.length; i++) { if (renderers[i].match(bid)) { @@ -88,9 +88,7 @@ export function addBidResponseHook(next, adUnitCode, bid) { } if (renderer) { - const bidRequest = find(this.bidderRequest.bids, bidRequest => - bidRequest.bidId === bid.requestId - ); + const bidRequest = index.getBidRequest(bid); matchedBids[bid.requestId] = { renderer, diff --git a/modules/mediaforceBidAdapter.js b/modules/mediaforceBidAdapter.js index 7d4f22b7916..c686a2e378d 100644 --- a/modules/mediaforceBidAdapter.js +++ b/modules/mediaforceBidAdapter.js @@ -262,7 +262,7 @@ export const spec = { onBidWon: function(bid) { const cpm = deepAccess(bid, 'adserverTargeting.hb_pb') || ''; if (isStr(bid.burl) && bid.burl !== '') { - bid.burl = replaceAuctionPrice(bid.burl, cpm); + bid.burl = replaceAuctionPrice(bid.burl, bid.originalCpm || cpm); triggerPixel(bid.burl); } }, diff --git a/modules/mediafuseBidAdapter.js b/modules/mediafuseBidAdapter.js new file mode 100644 index 00000000000..b77c965802e --- /dev/null +++ b/modules/mediafuseBidAdapter.js @@ -0,0 +1,1145 @@ +import { convertCamelToUnderscore, isArray, isNumber, isPlainObject, logError, logInfo, deepAccess, logMessage, convertTypes, isStr, getParameterByName, deepClone, chunk, logWarn, getBidRequest, createTrackPixelHtml, isEmpty, transformBidderParamKeywords, getMaxValueFromArray, fill, getMinValueFromArray, isArrayOfNums, isFn } from '../src/utils.js'; +import { Renderer } from '../src/Renderer.js'; +import { config } from '../src/config.js'; +import { registerBidder, getIabSubCategory } from '../src/adapters/bidderFactory.js'; +import { BANNER, NATIVE, VIDEO, ADPOD } from '../src/mediaTypes.js'; +import { auctionManager } from '../src/auctionManager.js'; +import {find, includes} from '../src/polyfill.js'; +import { OUTSTREAM, INSTREAM } from '../src/video.js'; +import { getStorageManager } from '../src/storageManager.js'; +import { bidderSettings } from '../src/bidderSettings.js'; + +const BIDDER_CODE = 'mediafuse'; +const URL = 'https://ib.adnxs.com/ut/v3/prebid'; +const URL_SIMPLE = 'https://ib.adnxs-simple.com/ut/v3/prebid'; +const VIDEO_TARGETING = ['id', 'minduration', 'maxduration', + 'skippable', 'playback_method', 'frameworks', 'context', 'skipoffset']; +const VIDEO_RTB_TARGETING = ['minduration', 'maxduration', 'skip', 'skipafter', 'playbackmethod', 'api']; +const USER_PARAMS = ['age', 'externalUid', 'segments', 'gender', 'dnt', 'language']; +const APP_DEVICE_PARAMS = ['geo', 'device_id']; // appid is collected separately +const DEBUG_PARAMS = ['enabled', 'dongle', 'member_id', 'debug_timeout']; +const VIDEO_MAPPING = { + playback_method: { + 'unknown': 0, + 'auto_play_sound_on': 1, + 'auto_play_sound_off': 2, + 'click_to_play': 3, + 'mouse_over': 4, + 'auto_play_sound_unknown': 5 + }, + context: { + 'unknown': 0, + 'pre_roll': 1, + 'mid_roll': 2, + 'post_roll': 3, + 'outstream': 4, + 'in-banner': 5 + } +}; +const NATIVE_MAPPING = { + body: 'description', + body2: 'desc2', + cta: 'ctatext', + image: { + serverName: 'main_image', + requiredParams: { required: true } + }, + icon: { + serverName: 'icon', + requiredParams: { required: true } + }, + sponsoredBy: 'sponsored_by', + privacyLink: 'privacy_link', + salePrice: 'saleprice', + displayUrl: 'displayurl' +}; +const SOURCE = 'pbjs'; +const MAX_IMPS_PER_REQUEST = 15; +const mappingFileUrl = 'https://acdn.adnxs-simple.com/prebid/mediafuse-mapping/mappings.json'; +const SCRIPT_TAG_START = ' includes(USER_PARAMS, param)) + .forEach((param) => { + let uparam = convertCamelToUnderscore(param); + if (param === 'segments' && isArray(userObjBid.params.user[param])) { + let segs = []; + userObjBid.params.user[param].forEach(val => { + if (isNumber(val)) { + segs.push({'id': val}); + } else if (isPlainObject(val)) { + segs.push(val); + } + }); + userObj[uparam] = segs; + } else if (param !== 'segments') { + userObj[uparam] = userObjBid.params.user[param]; + } + }); + } + + const appDeviceObjBid = find(bidRequests, hasAppDeviceInfo); + let appDeviceObj; + if (appDeviceObjBid && appDeviceObjBid.params && appDeviceObjBid.params.app) { + appDeviceObj = {}; + Object.keys(appDeviceObjBid.params.app) + .filter(param => includes(APP_DEVICE_PARAMS, param)) + .forEach(param => appDeviceObj[param] = appDeviceObjBid.params.app[param]); + } + + const appIdObjBid = find(bidRequests, hasAppId); + let appIdObj; + if (appIdObjBid && appIdObjBid.params && appDeviceObjBid.params.app && appDeviceObjBid.params.app.id) { + appIdObj = { + appid: appIdObjBid.params.app.id + }; + } + + let debugObj = {}; + let debugObjParams = {}; + const debugCookieName = 'apn_prebid_debug'; + const debugCookie = storage.getCookie(debugCookieName) || null; + + if (debugCookie) { + try { + debugObj = JSON.parse(debugCookie); + } catch (e) { + logError('MediaFuse Debug Auction Cookie Error:\n\n' + e); + } + } else { + const debugBidRequest = find(bidRequests, hasDebug); + if (debugBidRequest && debugBidRequest.debug) { + debugObj = debugBidRequest.debug; + } + } + + if (debugObj && debugObj.enabled) { + Object.keys(debugObj) + .filter(param => includes(DEBUG_PARAMS, param)) + .forEach(param => { + debugObjParams[param] = debugObj[param]; + }); + } + + const memberIdBid = find(bidRequests, hasMemberId); + const member = memberIdBid ? parseInt(memberIdBid.params.member, 10) : 0; + const schain = bidRequests[0].schain; + const omidSupport = find(bidRequests, hasOmidSupport); + + const payload = { + tags: [...tags], + user: userObj, + sdk: { + source: SOURCE, + version: '$prebid.version$' + }, + schain: schain + }; + + if (omidSupport) { + payload['iab_support'] = { + omidpn: 'Mediafuse', + omidpv: '$prebid.version$' + } + } + + if (member > 0) { + payload.member_id = member; + } + + if (appDeviceObjBid) { + payload.device = appDeviceObj + } + if (appIdObjBid) { + payload.app = appIdObj; + } + + let auctionKeywords = config.getConfig('mediafuseAuctionKeywords'); + if (isPlainObject(auctionKeywords)) { + let aucKeywords = transformBidderParamKeywords(auctionKeywords); + + if (aucKeywords.length > 0) { + aucKeywords.forEach(deleteValues); + } + + payload.keywords = aucKeywords; + } + + if (config.getConfig('adpod.brandCategoryExclusion')) { + payload.brand_category_uniqueness = true; + } + + if (debugObjParams.enabled) { + payload.debug = debugObjParams; + logInfo('MediaFuse Debug Auction Settings:\n\n' + JSON.stringify(debugObjParams, null, 4)); + } + + if (bidderRequest && bidderRequest.gdprConsent) { + // note - objects for impbus use underscore instead of camelCase + payload.gdpr_consent = { + consent_string: bidderRequest.gdprConsent.consentString, + consent_required: bidderRequest.gdprConsent.gdprApplies + }; + + 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); + payload.gdpr_consent.addtl_consent = acStr.split('.').map(id => parseInt(id, 10)); + } + } + + if (bidderRequest && bidderRequest.uspConsent) { + payload.us_privacy = bidderRequest.uspConsent + } + + if (bidderRequest && bidderRequest.refererInfo) { + let refererinfo = { + rd_ref: encodeURIComponent(bidderRequest.refererInfo.referer), + rd_top: bidderRequest.refererInfo.reachedTop, + rd_ifs: bidderRequest.refererInfo.numIframes, + rd_stk: bidderRequest.refererInfo.stack.map((url) => encodeURIComponent(url)).join(',') + } + payload.referrer_detection = refererinfo; + } + + const hasAdPodBid = find(bidRequests, hasAdPod); + if (hasAdPodBid) { + bidRequests.filter(hasAdPod).forEach(adPodBid => { + const adPodTags = createAdPodRequest(tags, adPodBid); + // don't need the original adpod placement because it's in adPodTags + const nonPodTags = payload.tags.filter(tag => tag.uuid !== adPodBid.bidId); + payload.tags = [...nonPodTags, ...adPodTags]; + }); + } + + if (bidRequests[0].userId) { + let eids = []; + + addUserId(eids, deepAccess(bidRequests[0], `userId.flocId.id`), 'chrome.com', null); + addUserId(eids, deepAccess(bidRequests[0], `userId.criteoId`), 'criteo.com', null); + addUserId(eids, deepAccess(bidRequests[0], `userId.netId`), 'netid.de', null); + addUserId(eids, deepAccess(bidRequests[0], `userId.idl_env`), 'liveramp.com', null); + addUserId(eids, deepAccess(bidRequests[0], `userId.tdid`), 'adserver.org', 'TDID'); + addUserId(eids, deepAccess(bidRequests[0], `userId.uid2.id`), 'uidapi.com', 'UID2'); + + if (eids.length) { + payload.eids = eids; + } + } + + if (tags[0].publisher_id) { + payload.publisher_id = tags[0].publisher_id; + } + + const request = formatRequest(payload, bidderRequest); + return 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: function (serverResponse, { bidderRequest }) { + serverResponse = serverResponse.body; + const bids = []; + if (!serverResponse || serverResponse.error) { + let errorMessage = `in response for ${bidderRequest.bidderCode} adapter`; + if (serverResponse && serverResponse.error) { errorMessage += `: ${serverResponse.error}`; } + logError(errorMessage); + return bids; + } + + if (serverResponse.tags) { + serverResponse.tags.forEach(serverBid => { + const rtbBid = getRtbBid(serverBid); + if (rtbBid) { + const cpmCheck = (bidderSettings.get(bidderRequest.bidderCode, 'allowZeroCpmBids') === true) ? rtbBid.cpm >= 0 : rtbBid.cpm > 0; + if (cpmCheck && includes(this.supportedMediaTypes, rtbBid.ad_type)) { + const bid = newBid(serverBid, rtbBid, bidderRequest); + bid.mediaType = parseMediaType(rtbBid); + bids.push(bid); + } + } + }); + } + + if (serverResponse.debug && serverResponse.debug.debug_info) { + let debugHeader = 'MediaFuse Debug Auction for Prebid\n\n' + let debugText = debugHeader + serverResponse.debug.debug_info + debugText = debugText + .replace(/(|)/gm, '\t') // Tables + .replace(/(<\/td>|<\/th>)/gm, '\n') // Tables + .replace(/^
/gm, '') // Remove leading
+ .replace(/(
\n|
)/gm, '\n') //
+ .replace(/

(.*)<\/h1>/gm, '\n\n===== $1 =====\n\n') // Header H1 + .replace(/(.*)<\/h[2-6]>/gm, '\n\n*** $1 ***\n\n') // Headers + .replace(/(<([^>]+)>)/igm, ''); // Remove any other tags + // logMessage('https://console.appnexus.com/docs/understanding-the-debug-auction'); + logMessage(debugText); + } + + return bids; + }, + + /** + * @typedef {Object} mappingFileInfo + * @property {string} url mapping file json url + * @property {number} refreshInDays prebid stores mapping data in localstorage so you can return in how many days you want to update value stored in localstorage. + * @property {string} localStorageKey unique key to store your mapping json in localstorage + */ + + /** + * Returns mapping file info. This info will be used by bidderFactory to preload mapping file and store data in local storage + * @returns {mappingFileInfo} + */ + getMappingFileInfo: function () { + return { + url: mappingFileUrl, + refreshInDays: 2 + } + }, + + getUserSyncs: function (syncOptions, responses, gdprConsent) { + if (syncOptions.iframeEnabled && hasPurpose1Consent({gdprConsent})) { + return [{ + type: 'iframe', + url: 'https://acdn.adnxs.com/dmp/async_usersync.html' + }]; + } + }, + + transformBidParams: function (params, isOpenRtb) { + params = convertTypes({ + 'member': 'string', + 'invCode': 'string', + 'placementId': 'number', + 'keywords': transformBidderParamKeywords, + 'publisherId': 'number' + }, params); + + if (isOpenRtb) { + params.use_pmt_rule = (typeof params.usePaymentRule === 'boolean') ? params.usePaymentRule : false; + if (params.usePaymentRule) { delete params.usePaymentRule; } + + if (isPopulatedArray(params.keywords)) { + params.keywords.forEach(deleteValues); + } + + Object.keys(params).forEach(paramKey => { + let convertedKey = convertCamelToUnderscore(paramKey); + if (convertedKey !== paramKey) { + params[convertedKey] = params[paramKey]; + delete params[paramKey]; + } + }); + } + + return params; + }, + + /** + * Add element selector to javascript tracker to improve native viewability + * @param {Bid} bid + */ + onBidWon: function (bid) { + if (bid.native) { + reloadViewabilityScriptWithCorrectParameters(bid); + } + } +} + +function isPopulatedArray(arr) { + return !!(isArray(arr) && arr.length > 0); +} + +function deleteValues(keyPairObj) { + if (isPopulatedArray(keyPairObj.value) && keyPairObj.value[0] === '') { + delete keyPairObj.value; + } +} + +function reloadViewabilityScriptWithCorrectParameters(bid) { + let viewJsPayload = getMediafuseViewabilityScriptFromJsTrackers(bid.native.javascriptTrackers); + + if (viewJsPayload) { + let prebidParams = 'pbjs_adid=' + bid.adId + ';pbjs_auc=' + bid.adUnitCode; + + let jsTrackerSrc = getViewabilityScriptUrlFromPayload(viewJsPayload) + + let newJsTrackerSrc = jsTrackerSrc.replace('dom_id=%native_dom_id%', prebidParams); + + // find iframe containing script tag + let frameArray = document.getElementsByTagName('iframe'); + + // boolean var to modify only one script. That way if there are muliple scripts, + // they won't all point to the same creative. + let modifiedAScript = false; + + // first, loop on all ifames + for (let i = 0; i < frameArray.length && !modifiedAScript; i++) { + let currentFrame = frameArray[i]; + try { + // IE-compatible, see https://stackoverflow.com/a/3999191/2112089 + let nestedDoc = currentFrame.contentDocument || currentFrame.contentWindow.document; + + if (nestedDoc) { + // if the doc is present, we look for our jstracker + let scriptArray = nestedDoc.getElementsByTagName('script'); + for (let j = 0; j < scriptArray.length && !modifiedAScript; j++) { + let currentScript = scriptArray[j]; + if (currentScript.getAttribute('data-src') == jsTrackerSrc) { + currentScript.setAttribute('src', newJsTrackerSrc); + currentScript.setAttribute('data-src', ''); + if (currentScript.removeAttribute) { + currentScript.removeAttribute('data-src'); + } + modifiedAScript = true; + } + } + } + } catch (exception) { + // trying to access a cross-domain iframe raises a SecurityError + // this is expected and ignored + if (!(exception instanceof DOMException && exception.name === 'SecurityError')) { + // all other cases are raised again to be treated by the calling function + throw exception; + } + } + } + } +} + +function strIsMediafuseViewabilityScript(str) { + let regexMatchUrlStart = str.match(VIEWABILITY_URL_START); + let viewUrlStartInStr = regexMatchUrlStart != null && regexMatchUrlStart.length >= 1; + + let regexMatchFileName = str.match(VIEWABILITY_FILE_NAME); + let fileNameInStr = regexMatchFileName != null && regexMatchFileName.length >= 1; + + return str.startsWith(SCRIPT_TAG_START) && fileNameInStr && viewUrlStartInStr; +} + +function getMediafuseViewabilityScriptFromJsTrackers(jsTrackerArray) { + let viewJsPayload; + if (isStr(jsTrackerArray) && strIsMediafuseViewabilityScript(jsTrackerArray)) { + viewJsPayload = jsTrackerArray; + } else if (isArray(jsTrackerArray)) { + for (let i = 0; i < jsTrackerArray.length; i++) { + let currentJsTracker = jsTrackerArray[i]; + if (strIsMediafuseViewabilityScript(currentJsTracker)) { + viewJsPayload = currentJsTracker; + } + } + } + return viewJsPayload; +} + +function getViewabilityScriptUrlFromPayload(viewJsPayload) { + // extracting the content of the src attribute + // -> substring between src=" and " + let indexOfFirstQuote = viewJsPayload.indexOf('src="') + 5; // offset of 5: the length of 'src=' + 1 + let indexOfSecondQuote = viewJsPayload.indexOf('"', indexOfFirstQuote); + let jsTrackerSrc = viewJsPayload.substring(indexOfFirstQuote, indexOfSecondQuote); + return jsTrackerSrc; +} + +function hasPurpose1Consent(bidderRequest) { + let result = true; + if (bidderRequest && bidderRequest.gdprConsent) { + if (bidderRequest.gdprConsent.gdprApplies && bidderRequest.gdprConsent.apiVersion === 2) { + result = !!(deepAccess(bidderRequest.gdprConsent, 'vendorData.purpose.consents.1') === true); + } + } + return result; +} + +function formatRequest(payload, bidderRequest) { + let request = []; + let options = { + withCredentials: true + }; + + let endpointUrl = URL; + + if (!hasPurpose1Consent(bidderRequest)) { + endpointUrl = URL_SIMPLE; + } + + if (getParameterByName('apn_test').toUpperCase() === 'TRUE' || config.getConfig('apn_test') === true) { + options.customHeaders = { + 'X-Is-Test': 1 + } + } + + if (payload.tags.length > MAX_IMPS_PER_REQUEST) { + const clonedPayload = deepClone(payload); + + chunk(payload.tags, MAX_IMPS_PER_REQUEST).forEach(tags => { + clonedPayload.tags = tags; + const payloadString = JSON.stringify(clonedPayload); + request.push({ + method: 'POST', + url: endpointUrl, + data: payloadString, + bidderRequest, + options + }); + }); + } else { + const payloadString = JSON.stringify(payload); + request = { + method: 'POST', + url: endpointUrl, + data: payloadString, + bidderRequest, + options + }; + } + + return request; +} + +function newRenderer(adUnitCode, rtbBid, rendererOptions = {}) { + const renderer = Renderer.install({ + id: rtbBid.renderer_id, + url: rtbBid.renderer_url, + config: rendererOptions, + loaded: false, + adUnitCode + }); + + try { + renderer.setRender(outstreamRender); + } catch (err) { + logWarn('Prebid Error calling setRender on renderer', err); + } + + renderer.setEventHandlers({ + impression: () => logMessage('MediaFuse outstream video impression event'), + loaded: () => logMessage('MediaFuse outstream video loaded event'), + ended: () => { + logMessage('MediaFuse outstream renderer video event'); + document.querySelector(`#${adUnitCode}`).style.display = 'none'; + } + }); + return renderer; +} + +/** + * Unpack the Server's Bid into a Prebid-compatible one. + * @param serverBid + * @param rtbBid + * @param bidderRequest + * @return Bid + */ +function newBid(serverBid, rtbBid, bidderRequest) { + const bidRequest = getBidRequest(serverBid.uuid, [bidderRequest]); + const bid = { + requestId: serverBid.uuid, + cpm: rtbBid.cpm, + creativeId: rtbBid.creative_id, + dealId: rtbBid.deal_id, + currency: 'USD', + netRevenue: true, + ttl: 300, + adUnitCode: bidRequest.adUnitCode, + mediafuse: { + buyerMemberId: rtbBid.buyer_member_id, + dealPriority: rtbBid.deal_priority, + dealCode: rtbBid.deal_code + } + }; + + // WE DON'T FULLY SUPPORT THIS ATM - future spot for adomain code; creating a stub for 5.0 compliance + if (rtbBid.adomain) { + bid.meta = Object.assign({}, bid.meta, { advertiserDomains: [] }); + } + + if (rtbBid.advertiser_id) { + bid.meta = Object.assign({}, bid.meta, { advertiserId: rtbBid.advertiser_id }); + } + + // temporary function; may remove at later date if/when adserver fully supports dchain + function setupDChain(rtbBid) { + let dchain = { + ver: '1.0', + complete: 0, + nodes: [{ + bsid: rtbBid.buyer_member_id.toString() + }], + }; + + return dchain; + } + if (rtbBid.buyer_member_id) { + bid.meta = Object.assign({}, bid.meta, {dchain: setupDChain(rtbBid)}); + } + + if (rtbBid.brand_id) { + bid.meta = Object.assign({}, bid.meta, { brandId: rtbBid.brand_id }); + } + + if (rtbBid.rtb.video) { + // shared video properties used for all 3 contexts + Object.assign(bid, { + width: rtbBid.rtb.video.player_width, + height: rtbBid.rtb.video.player_height, + vastImpUrl: rtbBid.notify_url, + ttl: 3600 + }); + + const videoContext = deepAccess(bidRequest, 'mediaTypes.video.context'); + switch (videoContext) { + case ADPOD: + const primaryCatId = getIabSubCategory(bidRequest.bidder, rtbBid.brand_category_id); + bid.meta = Object.assign({}, bid.meta, { primaryCatId }); + const dealTier = rtbBid.deal_priority; + bid.video = { + context: ADPOD, + durationSeconds: Math.floor(rtbBid.rtb.video.duration_ms / 1000), + dealTier + }; + bid.vastUrl = rtbBid.rtb.video.asset_url; + break; + case OUTSTREAM: + bid.adResponse = serverBid; + bid.adResponse.ad = bid.adResponse.ads[0]; + bid.adResponse.ad.video = bid.adResponse.ad.rtb.video; + bid.vastXml = rtbBid.rtb.video.content; + + if (rtbBid.renderer_url) { + const videoBid = find(bidderRequest.bids, bid => bid.bidId === serverBid.uuid); + const rendererOptions = deepAccess(videoBid, 'renderer.options'); + bid.renderer = newRenderer(bid.adUnitCode, rtbBid, rendererOptions); + } + break; + case INSTREAM: + bid.vastUrl = rtbBid.notify_url + '&redir=' + encodeURIComponent(rtbBid.rtb.video.asset_url); + break; + } + } else if (rtbBid.rtb[NATIVE]) { + const nativeAd = rtbBid.rtb[NATIVE]; + + // setting up the jsTracker: + // we put it as a data-src attribute so that the tracker isn't called + // until we have the adId (see onBidWon) + let jsTrackerDisarmed = rtbBid.viewability.config.replace('src=', 'data-src='); + + let jsTrackers = nativeAd.javascript_trackers; + + if (jsTrackers == undefined) { + jsTrackers = jsTrackerDisarmed; + } else if (isStr(jsTrackers)) { + jsTrackers = [jsTrackers, jsTrackerDisarmed]; + } else { + jsTrackers.push(jsTrackerDisarmed); + } + + bid[NATIVE] = { + title: nativeAd.title, + body: nativeAd.desc, + body2: nativeAd.desc2, + cta: nativeAd.ctatext, + rating: nativeAd.rating, + sponsoredBy: nativeAd.sponsored, + privacyLink: nativeAd.privacy_link, + address: nativeAd.address, + downloads: nativeAd.downloads, + likes: nativeAd.likes, + phone: nativeAd.phone, + price: nativeAd.price, + salePrice: nativeAd.saleprice, + clickUrl: nativeAd.link.url, + displayUrl: nativeAd.displayurl, + clickTrackers: nativeAd.link.click_trackers, + impressionTrackers: nativeAd.impression_trackers, + javascriptTrackers: jsTrackers + }; + if (nativeAd.main_img) { + bid['native'].image = { + url: nativeAd.main_img.url, + height: nativeAd.main_img.height, + width: nativeAd.main_img.width, + }; + } + if (nativeAd.icon) { + bid['native'].icon = { + url: nativeAd.icon.url, + height: nativeAd.icon.height, + width: nativeAd.icon.width, + }; + } + } else { + Object.assign(bid, { + width: rtbBid.rtb.banner.width, + height: rtbBid.rtb.banner.height, + ad: rtbBid.rtb.banner.content + }); + try { + if (rtbBid.rtb.trackers) { + for (let i = 0; i < rtbBid.rtb.trackers[0].impression_urls.length; i++) { + const url = rtbBid.rtb.trackers[0].impression_urls[i]; + const tracker = createTrackPixelHtml(url); + bid.ad += tracker; + } + } + } catch (error) { + logError('Error appending tracking pixel', error); + } + } + + return bid; +} + +function bidToTag(bid) { + const tag = {}; + tag.sizes = transformSizes(bid.sizes); + tag.primary_size = tag.sizes[0]; + tag.ad_types = []; + tag.uuid = bid.bidId; + if (bid.params.placementId) { + tag.id = parseInt(bid.params.placementId, 10); + } else { + tag.code = bid.params.invCode; + } + tag.allow_smaller_sizes = bid.params.allowSmallerSizes || false; + tag.use_pmt_rule = bid.params.usePaymentRule || false + tag.prebid = true; + tag.disable_psa = true; + let bidFloor = getBidFloor(bid); + if (bidFloor) { + tag.reserve = bidFloor; + } + if (bid.params.position) { + tag.position = { 'above': 1, 'below': 2 }[bid.params.position] || 0; + } + if (bid.params.trafficSourceCode) { + tag.traffic_source_code = bid.params.trafficSourceCode; + } + if (bid.params.privateSizes) { + tag.private_sizes = transformSizes(bid.params.privateSizes); + } + if (bid.params.supplyType) { + tag.supply_type = bid.params.supplyType; + } + if (bid.params.pubClick) { + tag.pubclick = bid.params.pubClick; + } + if (bid.params.extInvCode) { + tag.ext_inv_code = bid.params.extInvCode; + } + if (bid.params.publisherId) { + tag.publisher_id = parseInt(bid.params.publisherId, 10); + } + if (bid.params.externalImpId) { + tag.external_imp_id = bid.params.externalImpId; + } + if (!isEmpty(bid.params.keywords)) { + let keywords = transformBidderParamKeywords(bid.params.keywords); + + if (keywords.length > 0) { + keywords.forEach(deleteValues); + } + tag.keywords = keywords; + } + + let gpid = deepAccess(bid, 'ortb2Imp.ext.data.pbadslot'); + if (gpid) { + tag.gpid = gpid; + } + + if (bid.mediaType === NATIVE || deepAccess(bid, `mediaTypes.${NATIVE}`)) { + tag.ad_types.push(NATIVE); + if (tag.sizes.length === 0) { + tag.sizes = transformSizes([1, 1]); + } + + if (bid.nativeParams) { + const nativeRequest = buildNativeRequest(bid.nativeParams); + tag[NATIVE] = { layouts: [nativeRequest] }; + } + } + + const videoMediaType = deepAccess(bid, `mediaTypes.${VIDEO}`); + const context = deepAccess(bid, 'mediaTypes.video.context'); + + if (videoMediaType && context === 'adpod') { + tag.hb_source = 7; + } else { + tag.hb_source = 1; + } + if (bid.mediaType === VIDEO || videoMediaType) { + tag.ad_types.push(VIDEO); + } + + // instream gets vastUrl, outstream gets vastXml + if (bid.mediaType === VIDEO || (videoMediaType && context !== 'outstream')) { + tag.require_asset_url = true; + } + + if (bid.params.video) { + tag.video = {}; + // place any valid video params on the tag + Object.keys(bid.params.video) + .filter(param => includes(VIDEO_TARGETING, param)) + .forEach(param => { + switch (param) { + case 'context': + case 'playback_method': + let type = bid.params.video[param]; + type = (isArray(type)) ? type[0] : type; + tag.video[param] = VIDEO_MAPPING[param][type]; + break; + // Deprecating tags[].video.frameworks in favor of tags[].video_frameworks + case 'frameworks': + break; + default: + tag.video[param] = bid.params.video[param]; + } + }); + + if (bid.params.video.frameworks && isArray(bid.params.video.frameworks)) { + tag['video_frameworks'] = bid.params.video.frameworks; + } + } + + // use IAB ORTB values if the corresponding values weren't already set by bid.params.video + if (videoMediaType) { + tag.video = tag.video || {}; + Object.keys(videoMediaType) + .filter(param => includes(VIDEO_RTB_TARGETING, param)) + .forEach(param => { + switch (param) { + case 'minduration': + case 'maxduration': + if (typeof tag.video[param] !== 'number') tag.video[param] = videoMediaType[param]; + break; + case 'skip': + if (typeof tag.video['skippable'] !== 'boolean') tag.video['skippable'] = (videoMediaType[param] === 1); + break; + case 'skipafter': + if (typeof tag.video['skipoffset'] !== 'number') tag.video['skippoffset'] = videoMediaType[param]; + break; + case 'playbackmethod': + if (typeof tag.video['playback_method'] !== 'number') { + let type = videoMediaType[param]; + type = (isArray(type)) ? type[0] : type; + + // we only support iab's options 1-4 at this time. + if (type >= 1 && type <= 4) { + tag.video['playback_method'] = type; + } + } + break; + case 'api': + if (!tag['video_frameworks'] && isArray(videoMediaType[param])) { + // need to read thru array; remove 6 (we don't support it), swap 4 <> 5 if found (to match our adserver mapping for these specific values) + let apiTmp = videoMediaType[param].map(val => { + let v = (val === 4) ? 5 : (val === 5) ? 4 : val; + + if (v >= 1 && v <= 5) { + return v; + } + }).filter(v => v); + tag['video_frameworks'] = apiTmp; + } + break; + } + }); + } + + if (bid.renderer) { + tag.video = Object.assign({}, tag.video, { custom_renderer_present: true }); + } + + if (bid.params.frameworks && isArray(bid.params.frameworks)) { + tag['banner_frameworks'] = bid.params.frameworks; + } + + let adUnit = find(auctionManager.getAdUnits(), au => bid.transactionId === au.transactionId); + if (adUnit && adUnit.mediaTypes && adUnit.mediaTypes.banner) { + tag.ad_types.push(BANNER); + } + + if (tag.ad_types.length === 0) { + delete tag.ad_types; + } + + return tag; +} + +/* Turn bid request sizes into ut-compatible format */ +function transformSizes(requestSizes) { + let sizes = []; + let sizeObj = {}; + + if (isArray(requestSizes) && requestSizes.length === 2 && + !isArray(requestSizes[0])) { + sizeObj.width = parseInt(requestSizes[0], 10); + sizeObj.height = parseInt(requestSizes[1], 10); + sizes.push(sizeObj); + } else if (typeof requestSizes === 'object') { + for (let i = 0; i < requestSizes.length; i++) { + let size = requestSizes[i]; + sizeObj = {}; + sizeObj.width = parseInt(size[0], 10); + sizeObj.height = parseInt(size[1], 10); + sizes.push(sizeObj); + } + } + + return sizes; +} + +function hasUserInfo(bid) { + return !!bid.params.user; +} + +function hasMemberId(bid) { + return !!parseInt(bid.params.member, 10); +} + +function hasAppDeviceInfo(bid) { + if (bid.params) { + return !!bid.params.app + } +} + +function hasAppId(bid) { + if (bid.params && bid.params.app) { + return !!bid.params.app.id + } + return !!bid.params.app +} + +function hasDebug(bid) { + return !!bid.debug +} + +function hasAdPod(bid) { + return ( + bid.mediaTypes && + bid.mediaTypes.video && + bid.mediaTypes.video.context === ADPOD + ); +} + +function hasOmidSupport(bid) { + let hasOmid = false; + const bidderParams = bid.params; + const videoParams = bid.params.video; + if (bidderParams.frameworks && isArray(bidderParams.frameworks)) { + hasOmid = includes(bid.params.frameworks, 6); + } + if (!hasOmid && videoParams && videoParams.frameworks && isArray(videoParams.frameworks)) { + hasOmid = includes(bid.params.video.frameworks, 6); + } + return hasOmid; +} + +/** + * Expand an adpod placement into a set of request objects according to the + * total adpod duration and the range of duration seconds. Sets minduration/ + * maxduration video property according to requireExactDuration configuration + */ +function createAdPodRequest(tags, adPodBid) { + const { durationRangeSec, requireExactDuration } = adPodBid.mediaTypes.video; + + const numberOfPlacements = getAdPodPlacementNumber(adPodBid.mediaTypes.video); + const maxDuration = getMaxValueFromArray(durationRangeSec); + + const tagToDuplicate = tags.filter(tag => tag.uuid === adPodBid.bidId); + let request = fill(...tagToDuplicate, numberOfPlacements); + + if (requireExactDuration) { + const divider = Math.ceil(numberOfPlacements / durationRangeSec.length); + const chunked = chunk(request, divider); + + // each configured duration is set as min/maxduration for a subset of requests + durationRangeSec.forEach((duration, index) => { + chunked[index].map(tag => { + setVideoProperty(tag, 'minduration', duration); + setVideoProperty(tag, 'maxduration', duration); + }); + }); + } else { + // all maxdurations should be the same + request.map(tag => setVideoProperty(tag, 'maxduration', maxDuration)); + } + + return request; +} + +function getAdPodPlacementNumber(videoParams) { + const { adPodDurationSec, durationRangeSec, requireExactDuration } = videoParams; + const minAllowedDuration = getMinValueFromArray(durationRangeSec); + const numberOfPlacements = Math.floor(adPodDurationSec / minAllowedDuration); + + return requireExactDuration + ? Math.max(numberOfPlacements, durationRangeSec.length) + : numberOfPlacements; +} + +function setVideoProperty(tag, key, value) { + if (isEmpty(tag.video)) { tag.video = {}; } + tag.video[key] = value; +} + +function getRtbBid(tag) { + return tag && tag.ads && tag.ads.length && find(tag.ads, ad => ad.rtb); +} + +function buildNativeRequest(params) { + const request = {}; + + // map standard prebid native asset identifier to /ut parameters + // e.g., tag specifies `body` but /ut only knows `description`. + // mapping may be in form {tag: ''} or + // {tag: {serverName: '', requiredParams: {...}}} + Object.keys(params).forEach(key => { + // check if one of the forms is used, otherwise + // a mapping wasn't specified so pass the key straight through + const requestKey = + (NATIVE_MAPPING[key] && NATIVE_MAPPING[key].serverName) || + NATIVE_MAPPING[key] || + key; + + // required params are always passed on request + const requiredParams = NATIVE_MAPPING[key] && NATIVE_MAPPING[key].requiredParams; + request[requestKey] = Object.assign({}, requiredParams, params[key]); + + // convert the sizes of image/icon assets to proper format (if needed) + const isImageAsset = !!(requestKey === NATIVE_MAPPING.image.serverName || requestKey === NATIVE_MAPPING.icon.serverName); + if (isImageAsset && request[requestKey].sizes) { + let sizes = request[requestKey].sizes; + if (isArrayOfNums(sizes) || (isArray(sizes) && sizes.length > 0 && sizes.every(sz => isArrayOfNums(sz)))) { + request[requestKey].sizes = transformSizes(request[requestKey].sizes); + } + } + + if (requestKey === NATIVE_MAPPING.privacyLink) { + request.privacy_supported = true; + } + }); + + return request; +} + +/** + * This function hides google div container for outstream bids to remove unwanted space on page. Mediafuse renderer creates a new iframe outside of google iframe to render the outstream creative. + * @param {string} elementId element id + */ +function hidedfpContainer(elementId) { + var el = document.getElementById(elementId).querySelectorAll("div[id^='google_ads']"); + if (el[0]) { + el[0].style.setProperty('display', 'none'); + } +} + +function hideSASIframe(elementId) { + try { + // find script tag with id 'sas_script'. This ensures it only works if you're using Smart Ad Server. + const el = document.getElementById(elementId).querySelectorAll("script[id^='sas_script']"); + if (el[0].nextSibling && el[0].nextSibling.localName === 'iframe') { + el[0].nextSibling.style.setProperty('display', 'none'); + } + } catch (e) { + // element not found! + } +} + +function outstreamRender(bid) { + hidedfpContainer(bid.adUnitCode); + hideSASIframe(bid.adUnitCode); + // push to render queue because ANOutstreamVideo may not be loaded yet + bid.renderer.push(() => { + window.ANOutstreamVideo.renderAd({ + tagId: bid.adResponse.tag_id, + sizes: [bid.getSize().split('x')], + targetId: bid.adUnitCode, // target div id to render video + uuid: bid.adResponse.uuid, + adResponse: bid.adResponse, + rendererOptions: bid.renderer.getConfig() + }, handleOutstreamRendererEvents.bind(null, bid)); + }); +} + +function handleOutstreamRendererEvents(bid, id, eventName) { + bid.renderer.handleVideoEvent({ id, eventName }); +} + +function parseMediaType(rtbBid) { + const adType = rtbBid.ad_type; + if (adType === VIDEO) { + return VIDEO; + } else if (adType === NATIVE) { + return NATIVE; + } else { + return BANNER; + } +} + +function addUserId(eids, id, source, rti) { + if (id) { + if (rti) { + eids.push({ source, id, rti_partner: rti }); + } else { + eids.push({ source, id }); + } + } + return eids; +} + +function getBidFloor(bid) { + if (!isFn(bid.getFloor)) { + return (bid.params.reserve) ? bid.params.reserve : null; + } + + let floor = bid.getFloor({ + currency: 'USD', + mediaType: '*', + size: '*' + }); + if (isPlainObject(floor) && !isNaN(floor.floor) && floor.currency === 'USD') { + return floor.floor; + } + return null; +} + +registerBidder(spec); diff --git a/modules/mediafuseBidAdapter.md b/modules/mediafuseBidAdapter.md new file mode 100644 index 00000000000..f9ed9835b94 --- /dev/null +++ b/modules/mediafuseBidAdapter.md @@ -0,0 +1,151 @@ +# Overview + +``` +Module Name: Mediafuse Bid Adapter +Module Type: Bidder Adapter +Maintainer: prebid-js@xandr.com +``` + +# Description + +Connects to Mediafuse exchange for bids. + +Mediafuse bid adapter supports Banner, Video (instream and outstream) and Native. + +# Test Parameters +``` +var adUnits = [ + // Banner adUnit + { + code: 'banner-div', + mediaTypes: { + banner: { + sizes: [[300, 250], [300,600]] + } + }, + bids: [{ + bidder: 'mediafuse', + params: { + placementId: 13144370 + } + }] + }, + // Native adUnit + { + code: 'native-div', + sizes: [[1, 1]], + mediaTypes: { + native: { + title: { + required: true + }, + body: { + required: true + }, + image: { + required: true + }, + sponsoredBy: { + required: true + }, + icon: { + required: false + } + } + }, + bids: [{ + bidder: 'mediafuse', + params: { + placementId: 13232354, + allowSmallerSizes: true + } + }] + }, + // Video instream adUnit + { + code: 'video-instream', + sizes: [[640, 480]], + mediaTypes: { + video: { + playerSize: [[640, 480]], + context: 'instream' + }, + }, + bids: [{ + bidder: 'mediafuse', + params: { + placementId: 13232361, + video: { + skippable: true, + playback_methods: ['auto_play_sound_off'] + } + } + }] + }, + // Video outstream adUnit + { + code: 'video-outstream', + sizes: [[300, 250]], + mediaTypes: { + video: { + playerSize: [[300, 250]], + context: 'outstream', + // Certain ORTB 2.5 video values can be read from the mediatypes object; below are examples of supported params. + // To note - mediafuse supports additional values for our system that are not part of the ORTB spec. If you want + // to use these values, they will have to be declared in the bids[].params.video object instead using the mediafuse syntax. + // Between the corresponding values of the mediaTypes.video and params.video objects, the properties in params.video will + // take precedence if declared; eg in the example below, the `skippable: true` setting will be used instead of the `skip: 0`. + minduration: 1, + maxduration: 60, + skip: 0, // 1 - true, 0 - false + skipafter: 5, + playbackmethod: [2], // note - we only support options 1-4 at this time + api: [1,2,3] // note - option 6 is not supported at this time + } + }, + bids: [ + { + bidder: 'mediafuse', + params: { + placementId: 13232385, + video: { + skippable: true, + playback_method: 'auto_play_sound_off' + } + } + } + ] + }, + // Banner adUnit in a App Webview + // Only use this for situations where prebid.js is in a webview of an App + // See Prebid Mobile for displaying ads via an SDK + { + code: 'banner-div', + mediaTypes: { + banner: { + sizes: [[300, 250], [300,600]] + } + } + bids: [{ + bidder: 'mediafuse', + params: { + placementId: 13144370, + app: { + id: "B1O2W3M4AN.com.prebid.webview", + geo: { + lat: 40.0964439, + lng: -75.3009142 + }, + device_id: { + idfa: "4D12078D-3246-4DA4-AD5E-7610481E7AE", // Apple advertising identifier + aaid: "38400000-8cf0-11bd-b23e-10b96e40000d", // Android advertising identifier + md5udid: "5756ae9022b2ea1e47d84fead75220c8", // MD5 hash of the ANDROID_ID + sha1udid: "4DFAA92388699AC6539885AEF1719293879985BF", // SHA1 hash of the ANDROID_ID + windowsadid: "750c6be243f1c4b5c9912b95a5742fc5" // Windows advertising identifier + } + } + } + }] + } +]; +``` diff --git a/modules/mediakeysBidAdapter.js b/modules/mediakeysBidAdapter.js index 72dca2f1add..5eb32a3f6e4 100644 --- a/modules/mediakeysBidAdapter.js +++ b/modules/mediakeysBidAdapter.js @@ -1,10 +1,28 @@ -import find from 'core-js-pure/features/array/find.js'; -import arrayFrom from 'core-js-pure/features/array/from'; -import { getWindowTop, isFn, logWarn, getDNT, deepAccess, isArray, inIframe, mergeDeep, isStr, isEmpty, deepSetValue, deepClone, parseUrl, cleanObj, logError, triggerPixel, isInteger, isNumber } from '../src/utils.js'; -import { registerBidder } from '../src/adapters/bidderFactory.js'; -import { config } from '../src/config.js'; -import { BANNER, NATIVE, VIDEO } from '../src/mediaTypes.js'; -import { createEidsArray } from './userId/eids.js'; +import {arrayFrom, find} from '../src/polyfill.js'; +import { + cleanObj, + deepAccess, + deepClone, + deepSetValue, + getDNT, + getWindowTop, + inIframe, + isArray, + isEmpty, + isFn, + isInteger, + isNumber, + isStr, + logError, + logWarn, + mergeDeep, + parseUrl, + triggerPixel +} from '../src/utils.js'; +import {registerBidder} from '../src/adapters/bidderFactory.js'; +import {config} from '../src/config.js'; +import {BANNER, NATIVE, VIDEO} from '../src/mediaTypes.js'; +import {createEidsArray} from './userId/eids.js'; const AUCTION_TYPE = 1; const BIDDER_CODE = 'mediakeys'; @@ -653,11 +671,6 @@ export const spec = { if (fpd.user) { mergeDeep(payload, { user: fpd.user }); } - // Here we can handle device.geo prop - const deviceGeo = deepAccess(fpd, 'device.geo'); - if (deviceGeo) { - mergeDeep(payload.device, { geo: deviceGeo }); - } const request = { method: 'POST', diff --git a/modules/medianetAnalyticsAdapter.js b/modules/medianetAnalyticsAdapter.js index e281dde8ad0..09ebbc9bc31 100644 --- a/modules/medianetAnalyticsAdapter.js +++ b/modules/medianetAnalyticsAdapter.js @@ -1,11 +1,23 @@ -import { triggerPixel, deepAccess, getWindowTop, uniques, groupBy, isEmpty, _map, isPlainObject, logInfo, logError } from '../src/utils.js'; +import { + _map, + deepAccess, + getWindowTop, + groupBy, + isEmpty, + isPlainObject, + logError, + logInfo, + triggerPixel, + uniques, + getHighestCpm +} from '../src/utils.js'; import adapter from '../src/AnalyticsAdapter.js'; import adapterManager from '../src/adapterManager.js'; import CONSTANTS from '../src/constants.json'; -import { ajax } from '../src/ajax.js'; -import { getRefererInfo } from '../src/refererDetection.js'; -import { AUCTION_COMPLETED, AUCTION_IN_PROGRESS, getPriceGranularity } from '../src/auction.js'; -import includes from 'core-js-pure/features/array/includes.js'; +import {ajax} from '../src/ajax.js'; +import {getRefererInfo} from '../src/refererDetection.js'; +import {AUCTION_COMPLETED, AUCTION_IN_PROGRESS, getPriceGranularity} from '../src/auction.js'; +import {includes} from '../src/polyfill.js'; const analyticsType = 'endpoint'; const ENDPOINT = 'https://pb-logs.media.net/log?logid=kfk&evtid=prebid_analytics_events_client'; @@ -490,6 +502,27 @@ function _getSizes(mediaTypes, sizes) { } } +/* + - The code is used to determine if the current bid is higher than the previous bid. + - If it is, then the code will return true and if not, it will return false. + */ +function canSelectCurrentBid(previousBid, currentBid) { + if (!(previousBid instanceof Bid)) return false; + + // For first bid response the previous bid will be containing bid request obj + // in which the cpm would be undefined so the current bid can directly be selected. + const isFirstBidResponse = previousBid.cpm === undefined && currentBid.cpm !== undefined; + if (isFirstBidResponse) return true; + + // if there are 2 bids, get the highest bid + const selectedBid = getHighestCpm(previousBid, currentBid); + + // Return true if selectedBid is currentBid, + // The timeToRespond field is used as an identifier for distinguishing + // between the current iterating bid and the previous bid. + return selectedBid.timeToRespond === currentBid.timeToRespond; +} + function bidResponseHandler(bid) { const { width, height, mediaType, cpm, requestId, timeToRespond, auctionId, dealId } = bid; const {originalCpm, bidderCode, creativeId, adId, currency} = bid; @@ -498,7 +531,7 @@ function bidResponseHandler(bid) { return; } let bidObj = auctions[auctionId].findBid('bidId', requestId); - if (!(bidObj instanceof Bid)) { + if (!canSelectCurrentBid(bidObj, bid)) { return; } Object.assign( @@ -511,7 +544,7 @@ function bidResponseHandler(bid) { bidObj.originalCpm = originalCpm || cpm; let dfpbd = deepAccess(bid, 'adserverTargeting.hb_pb'); if (!dfpbd) { - let priceGranularity = getPriceGranularity(mediaType, bid); + let priceGranularity = getPriceGranularity(bid); let priceGranularityKey = PRICE_GRANULARITY[priceGranularity]; dfpbd = bid[priceGranularityKey] || cpm; } diff --git a/modules/medianetRtdProvider.js b/modules/medianetRtdProvider.js index cd86bf891f3..07b1d66fbc5 100644 --- a/modules/medianetRtdProvider.js +++ b/modules/medianetRtdProvider.js @@ -1,7 +1,7 @@ -import { isStr, isEmptyStr, logError, mergeDeep, isFn, insertElement } from '../src/utils.js'; -import { submodule } from '../src/hook.js'; -import { getGlobal } from '../src/prebidGlobal.js'; -import includes from 'core-js-pure/features/array/includes.js'; +import {insertElement, isEmptyStr, isFn, isStr, logError, mergeDeep} from '../src/utils.js'; +import {submodule} from '../src/hook.js'; +import {getGlobal} from '../src/prebidGlobal.js'; +import {includes} from '../src/polyfill.js'; const MODULE_NAME = 'medianet'; const SOURCE = MODULE_NAME + 'rtd'; diff --git a/modules/mediasquareBidAdapter.js b/modules/mediasquareBidAdapter.js index 1043c7cc3e6..427a16f1341 100644 --- a/modules/mediasquareBidAdapter.js +++ b/modules/mediasquareBidAdapter.js @@ -11,6 +11,7 @@ const BIDDER_ENDPOINT_WINNING = 'winning'; export const spec = { code: BIDDER_CODE, + gvlid: 791, aliases: ['msq'], // short code supportedMediaTypes: [BANNER, NATIVE, VIDEO], /** @@ -31,10 +32,16 @@ export const spec = { buildRequests: function(validBidRequests, bidderRequest) { let codes = []; let endpoint = document.location.search.match(/msq_test=true/) ? BIDDER_URL_TEST : BIDDER_URL_PROD; + let floor = {}; const test = config.getConfig('debug') ? 1 : 0; let adunitValue = null; Object.keys(validBidRequests).forEach(key => { adunitValue = validBidRequests[key]; + if (typeof adunitValue.getFloor === 'function') { + floor = adunitValue.getFloor({currency: 'EUR', mediaType: '*', size: '*'}); + } else { + floor = {}; + } codes.push({ owner: adunitValue.params.owner, code: adunitValue.params.code, @@ -42,12 +49,14 @@ export const spec = { bidId: adunitValue.bidId, auctionId: adunitValue.auctionId, transactionId: adunitValue.transactionId, - mediatypes: adunitValue.mediaTypes + mediatypes: adunitValue.mediaTypes, + floor: floor }); }); const payload = { codes: codes, - referer: encodeURIComponent(bidderRequest.refererInfo.referer) + referer: encodeURIComponent(bidderRequest.refererInfo.referer), + pbjs: '$prebid.version$' }; if (bidderRequest) { // modules informations (gdpr, ccpa, schain, userId) if (bidderRequest.gdprConsent) { @@ -108,6 +117,9 @@ export const spec = { if ('match' in value) { bidResponse['mediasquare']['match'] = value['match']; } + if ('hasConsent' in value) { + bidResponse['mediasquare']['hasConsent'] = value['hasConsent']; + } if ('native' in value) { bidResponse['native'] = value['native']; bidResponse['mediaType'] = 'native'; @@ -145,19 +157,22 @@ export const spec = { */ onBidWon: function(bid) { // fires a pixel to confirm a winning bid - let params = []; + let params = {'pbjs': '$prebid.version$'}; let endpoint = document.location.search.match(/msq_test=true/) ? BIDDER_URL_TEST : BIDDER_URL_PROD; let paramsToSearchFor = ['cpm', 'size', 'mediaType', 'currency', 'creativeId', 'adUnitCode', 'timeToRespond', 'requestId', 'auctionId'] if (bid.hasOwnProperty('mediasquare')) { - if (bid['mediasquare'].hasOwnProperty('bidder')) { params.push('bidder=' + bid['mediasquare']['bidder']); } - if (bid['mediasquare'].hasOwnProperty('code')) { params.push('code=' + bid['mediasquare']['code']); } - if (bid['mediasquare'].hasOwnProperty('match')) { params.push('match=' + bid['mediasquare']['match']); } + if (bid['mediasquare'].hasOwnProperty('bidder')) { params['bidder'] = bid['mediasquare']['bidder']; } + if (bid['mediasquare'].hasOwnProperty('code')) { params['code'] = bid['mediasquare']['code']; } + if (bid['mediasquare'].hasOwnProperty('match')) { params['match'] = bid['mediasquare']['match']; } + if (bid['mediasquare'].hasOwnProperty('hasConsent')) { params['hasConsent'] = bid['mediasquare']['hasConsent']; } }; for (let i = 0; i < paramsToSearchFor.length; i++) { - if (bid.hasOwnProperty(paramsToSearchFor[i])) { params.push(paramsToSearchFor[i] + '=' + bid[paramsToSearchFor[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() } + } } - if (params.length > 0) { params = '?' + params.join('&'); } - ajax(endpoint + BIDDER_ENDPOINT_WINNING + params, null, undefined, {method: 'GET', withCredentials: true}); + ajax(endpoint + BIDDER_ENDPOINT_WINNING, null, JSON.stringify(params), {method: 'POST', withCredentials: true}); return true; } diff --git a/modules/mgidBidAdapter.js b/modules/mgidBidAdapter.js index c811a0b2981..51b713c8958 100644 --- a/modules/mgidBidAdapter.js +++ b/modules/mgidBidAdapter.js @@ -7,7 +7,7 @@ import { getStorageManager } from '../src/storageManager.js'; const GVLID = 358; const DEFAULT_CUR = 'USD'; const BIDDER_CODE = 'mgid'; -export const storage = getStorageManager(GVLID, BIDDER_CODE); +export const storage = getStorageManager({gvlid: GVLID, bidderCode: BIDDER_CODE}); const ENDPOINT_URL = 'https://prebid.mgid.com/prebid/'; const LOG_WARN_PREFIX = '[MGID warn]: '; const LOG_INFO_PREFIX = '[MGID info]: '; diff --git a/modules/minutemediaBidAdapter.js b/modules/minutemediaBidAdapter.js new file mode 100644 index 00000000000..1a9ccfdf824 --- /dev/null +++ b/modules/minutemediaBidAdapter.js @@ -0,0 +1,360 @@ +import { logWarn, isArray, isFn, deepAccess, isEmpty, contains, timestamp, getBidIdParameter } from '../src/utils.js'; +import {registerBidder} from '../src/adapters/bidderFactory.js'; +import {VIDEO} from '../src/mediaTypes.js'; +import {config} from '../src/config.js'; + +const SUPPORTED_AD_TYPES = [VIDEO]; +const BIDDER_CODE = 'minutemedia'; +const ADAPTER_VERSION = '5.0.1'; +const TTL = 360; +const CURRENCY = 'USD'; +const SELLER_ENDPOINT = 'https://hb.minutemedia-prebid.com/'; +const MODES = { + PRODUCTION: 'hb-mm', + TEST: 'hb-mm-test' +} +const SUPPORTED_SYNC_METHODS = { + IFRAME: 'iframe', + PIXEL: 'pixel' +} + +export const spec = { + code: BIDDER_CODE, + gvlid: 918, + version: ADAPTER_VERSION, + supportedMediaTypes: SUPPORTED_AD_TYPES, + isBidRequestValid: function(bidRequest) { + if (!bidRequest.params) { + logWarn('no params have been set to MinuteMedia adapter'); + return false; + } + + if (!bidRequest.params.org) { + logWarn('org is a mandatory param for MinuteMedia adapter'); + return false; + } + + return true; + }, + buildRequests: function (bidRequests, bidderRequest) { + if (bidRequests.length === 0) { + return []; + } + + const requests = []; + + bidRequests.forEach(bid => { + requests.push(buildVideoRequest(bid, bidderRequest)); + }); + + return requests; + }, + interpretResponse: function({body}) { + const bidResponses = []; + + const bidResponse = { + requestId: body.requestId, + cpm: body.cpm, + width: body.width, + height: body.height, + creativeId: body.requestId, + currency: body.currency, + netRevenue: body.netRevenue, + ttl: body.ttl || TTL, + vastXml: body.vastXml, + mediaType: VIDEO + }; + + if (body.adomain && body.adomain.length) { + bidResponse.meta = {}; + bidResponse.meta.advertiserDomains = body.adomain + } + bidResponses.push(bidResponse); + + return bidResponses; + }, + getUserSyncs: function(syncOptions, serverResponses) { + const syncs = []; + for (const response of serverResponses) { + if (syncOptions.iframeEnabled && response.body.userSyncURL) { + syncs.push({ + type: 'iframe', + url: response.body.userSyncURL + }); + } + if (syncOptions.pixelEnabled && isArray(response.body.userSyncPixels)) { + const pixels = response.body.userSyncPixels.map(pixel => { + return { + type: 'image', + url: pixel + } + }) + syncs.push(...pixels) + } + } + return syncs; + } +}; + +registerBidder(spec); + +/** + * Get floor price + * @param bid {bid} + * @returns {Number} + */ +function getFloor(bid) { + if (!isFn(bid.getFloor)) { + return 0; + } + let floorResult = bid.getFloor({ + currency: CURRENCY, + mediaType: VIDEO, + size: '*' + }); + return floorResult.currency === CURRENCY && floorResult.floor ? floorResult.floor : 0; +} + +/** + * Build the video request + * @param bid {bid} + * @param bidderRequest {bidderRequest} + * @returns {Object} + */ +function buildVideoRequest(bid, bidderRequest) { + const sellerParams = generateParameters(bid, bidderRequest); + const {params} = bid; + return { + method: 'GET', + url: getEndpoint(params.testMode), + data: sellerParams + }; +} + +/** + * Get the the ad size from the bid + * @param bid {bid} + * @returns {Array} + */ +function getSizes(bid) { + if (deepAccess(bid, 'mediaTypes.video.sizes')) { + return bid.mediaTypes.video.sizes[0]; + } else if (Array.isArray(bid.sizes) && bid.sizes.length > 0) { + return bid.sizes[0]; + } + return []; +} + +/** + * Get schain string value + * @param schainObject {Object} + * @returns {string} + */ +function getSupplyChain(schainObject) { + if (isEmpty(schainObject)) { + return ''; + } + let scStr = `${schainObject.ver},${schainObject.complete}`; + schainObject.nodes.forEach((node) => { + scStr += '!'; + scStr += `${getEncodedValIfNotEmpty(node.asi)},`; + scStr += `${getEncodedValIfNotEmpty(node.sid)},`; + scStr += `${node.hp ? encodeURIComponent(node.hp) : ''},`; + scStr += `${getEncodedValIfNotEmpty(node.rid)},`; + scStr += `${getEncodedValIfNotEmpty(node.name)},`; + scStr += `${getEncodedValIfNotEmpty(node.domain)}`; + }); + return scStr; +} + +/** + * Get encoded node value + * @param val {string} + * @returns {string} + */ +function getEncodedValIfNotEmpty(val) { + return !isEmpty(val) ? encodeURIComponent(val) : ''; +} + +/** + * Get preferred user-sync method based on publisher configuration + * @param bidderCode {string} + * @returns {string} + */ +function getAllowedSyncMethod(filterSettings, bidderCode) { + const iframeConfigsToCheck = ['all', 'iframe']; + const pixelConfigToCheck = 'image'; + if (filterSettings && iframeConfigsToCheck.some(config => isSyncMethodAllowed(filterSettings[config], bidderCode))) { + return SUPPORTED_SYNC_METHODS.IFRAME; + } + if (!filterSettings || !filterSettings[pixelConfigToCheck] || isSyncMethodAllowed(filterSettings[pixelConfigToCheck], bidderCode)) { + return SUPPORTED_SYNC_METHODS.PIXEL; + } +} + +/** + * Check if sync rule is supported + * @param syncRule {Object} + * @param bidderCode {string} + * @returns {boolean} + */ +function isSyncMethodAllowed(syncRule, bidderCode) { + if (!syncRule) { + return false; + } + const isInclude = syncRule.filter === 'include'; + const bidders = isArray(syncRule.bidders) ? syncRule.bidders : [bidderCode]; + return isInclude && contains(bidders, bidderCode); +} + +/** + * Get the seller endpoint + * @param testMode {boolean} + * @returns {string} + */ +function getEndpoint(testMode) { + return testMode + ? SELLER_ENDPOINT + MODES.TEST + : SELLER_ENDPOINT + MODES.PRODUCTION; +} + +/** + * get device type + * @param uad {ua} + * @returns {string} + */ +function getDeviceType(ua) { + if (/ipad|android 3.0|xoom|sch-i800|playbook|tablet|kindle/i + .test(ua.toLowerCase())) { + return '5'; + } + if (/iphone|ipod|android|blackberry|opera|mini|windows\sce|palm|smartphone|iemobile/i + .test(ua.toLowerCase())) { + return '4'; + } + if (/smart[-_\s]?tv|hbbtv|appletv|googletv|hdmi|netcast|viera|nettv|roku|\bdtv\b|sonydtv|inettvbrowser|\btv\b/i + .test(ua.toLowerCase())) { + return '3'; + } + return '1'; +} + +/** + * Generate query parameters for the request + * @param bid {bid} + * @param bidderRequest {bidderRequest} + * @returns {Object} + */ +function generateParameters(bid, bidderRequest) { + const {params} = bid; + const timeout = config.getConfig('bidderTimeout'); + const {syncEnabled, filterSettings} = config.getConfig('userSync') || {}; + const [width, height] = getSizes(bid); + const {bidderCode} = bidderRequest; + const domain = window.location.hostname; + + // fix floor price in case of NAN + if (isNaN(params.floorPrice)) { + params.floorPrice = 0; + } + + const requestParams = { + wrapper_type: 'prebidjs', + wrapper_vendor: '$$PREBID_GLOBAL$$', + wrapper_version: '$prebid.version$', + adapter_version: ADAPTER_VERSION, + auction_start: timestamp(), + ad_unit_code: getBidIdParameter('adUnitCode', bid), + tmax: timeout, + width: width, + height: height, + publisher_id: params.org, + floor_price: Math.max(getFloor(bid), params.floorPrice), + ua: navigator.userAgent, + bid_id: getBidIdParameter('bidId', bid), + bidder_request_id: getBidIdParameter('bidderRequestId', bid), + transaction_id: getBidIdParameter('transactionId', bid), + session_id: getBidIdParameter('auctionId', bid), + publisher_name: domain, + site_domain: domain, + dnt: (navigator.doNotTrack == 'yes' || navigator.doNotTrack == '1' || navigator.msDoNotTrack == '1') ? 1 : 0, + device_type: getDeviceType(navigator.userAgent) + }; + + const userIdsParam = getBidIdParameter('userId', bid); + if (userIdsParam) { + requestParams.userIds = JSON.stringify(userIdsParam); + } + + const ortb2Metadata = config.getConfig('ortb2') || {}; + if (ortb2Metadata.site) { + requestParams.site_metadata = JSON.stringify(ortb2Metadata.site); + } + if (ortb2Metadata.user) { + requestParams.user_metadata = JSON.stringify(ortb2Metadata.user); + } + + const playbackMethod = deepAccess(bid, 'mediaTypes.video.playbackmethod'); + if (playbackMethod) { + requestParams.playback_method = playbackMethod; + } + const placement = deepAccess(bid, 'mediaTypes.video.placement'); + if (placement) { + requestParams.placement = placement; + } + const pos = deepAccess(bid, 'mediaTypes.video.pos'); + if (pos) { + requestParams.pos = pos; + } + const minduration = deepAccess(bid, 'mediaTypes.video.minduration'); + if (minduration) { + requestParams.min_duration = minduration; + } + const maxduration = deepAccess(bid, 'mediaTypes.video.maxduration'); + if (maxduration) { + requestParams.max_duration = maxduration; + } + const skip = deepAccess(bid, 'mediaTypes.video.skip'); + if (skip) { + requestParams.skip = skip; + } + const linearity = deepAccess(bid, 'mediaTypes.video.linearity'); + if (linearity) { + requestParams.linearity = linearity; + } + + if (params.placementId) { + requestParams.placement_id = params.placementId; + } + + if (syncEnabled) { + const allowedSyncMethod = getAllowedSyncMethod(filterSettings, bidderCode); + if (allowedSyncMethod) { + requestParams.cs_method = allowedSyncMethod; + } + } + + if (bidderRequest.uspConsent) { + requestParams.us_privacy = bidderRequest.uspConsent; + } + + if (bidderRequest && bidderRequest.gdprConsent && bidderRequest.gdprConsent.gdprApplies) { + requestParams.gdpr = bidderRequest.gdprConsent.gdprApplies; + requestParams.gdpr_consent = bidderRequest.gdprConsent.consentString; + } + + if (params.ifa) { + requestParams.ifa = params.ifa; + } + + if (bid.schain) { + requestParams.schain = getSupplyChain(bid.schain); + } + + if (bidderRequest && bidderRequest.refererInfo) { + requestParams.referrer = deepAccess(bidderRequest, 'refererInfo.referer'); + requestParams.page_url = config.getConfig('pageUrl') || deepAccess(window, 'location.href'); + } + + return requestParams; +} diff --git a/modules/minutemediaBidAdapter.md b/modules/minutemediaBidAdapter.md new file mode 100644 index 00000000000..348cc586e08 --- /dev/null +++ b/modules/minutemediaBidAdapter.md @@ -0,0 +1,51 @@ +#Overview + +Module Name: MinuteMedia Bidder Adapter + +Module Type: Bidder Adapter + +Maintainer: hb@minutemedia.com + + +# Description + +Module that connects to MinuteMedia's demand sources. + +The MinuteMedia adapter requires setup and approval from the MinuteMedia. Please reach out to hb@minutemedia.com to create an MinuteMedia account. + +The adapter supports Video(instream). + +# Bid Parameters +## Video + +| Name | Scope | Type | Description | Example +| ---- | ----- | ---- | ----------- | ------- +| `org` | required | String | MinuteMedia publisher Id provided by your MinuteMedia representative | "56f91cd4d3e3660002000033" +| `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 + +# Test Parameters +```javascript +var adUnits = [ + { + code: 'dfp-video-div', + sizes: [[640, 480]], + mediaTypes: { + video: { + playerSize: [[640, 480]], + context: 'instream' + } + }, + bids: [{ + bidder: 'minutemedia', + params: { + org: '56f91cd4d3e3660002000033', // Required + floorPrice: 2.00, // Optional + placementId: '12345678', // Optional + testMode: false // Optional + } + }] + } + ]; +``` diff --git a/modules/missenaBidAdapter.js b/modules/missenaBidAdapter.js index 30749e977a8..41bae4d6568 100644 --- a/modules/missenaBidAdapter.js +++ b/modules/missenaBidAdapter.js @@ -43,10 +43,10 @@ export const spec = { payload.consent_string = bidderRequest.gdprConsent.consentString; payload.consent_required = bidderRequest.gdprConsent.gdprApplies; } - + const baseUrl = bidRequest.params.baseUrl || ENDPOINT_URL; return { method: 'POST', - url: ENDPOINT_URL + '?' + formatQS({ t: bidRequest.params.apiKey }), + url: baseUrl + '?' + formatQS({ t: bidRequest.params.apiKey }), data: JSON.stringify(payload), }; }); diff --git a/modules/multibid/index.js b/modules/multibid/index.js index ef0771e291f..8081e40ccb2 100644 --- a/modules/multibid/index.js +++ b/modules/multibid/index.js @@ -8,7 +8,7 @@ import {setupBeforeHookFnOnce, getHook} from '../../src/hook.js'; import { logWarn, deepAccess, getUniqueIdentifierStr, deepSetValue, groupBy } from '../../src/utils.js'; -import events from '../../src/events.js'; +import * as events from '../../src/events.js'; import CONSTANTS from '../../src/constants.json'; import {addBidderRequests} from '../../src/auction.js'; import {getHighestCpmBidsFromBidPool, sortByDealAndPriceBucketOrCpm} from '../../src/targeting.js'; diff --git a/modules/nativoBidAdapter.js b/modules/nativoBidAdapter.js index c9e6a1f659f..e07a124665f 100644 --- a/modules/nativoBidAdapter.js +++ b/modules/nativoBidAdapter.js @@ -12,8 +12,71 @@ const TIME_TO_LIVE = 360 const SUPPORTED_AD_TYPES = [BANNER] +/** + * Keep track of bid data by keys + * @returns {Object} - Map of bid data that can be referenced by multiple keys + */ +const BidDataMap = () => { + const referenceMap = {} + const bids = [] + + /** + * Add a refence to the index by key value + * @param {String} key - The key to store the index reference + * @param {Integer} index - The index value of the bidData + */ + function adKeyReference(key, index) { + if (!referenceMap.hasOwnProperty(key)) { + referenceMap[key] = index + } + } + + /** + * Adds a bid to the map + * @param {Object} bid - Bid data + * @param {Array/String} keys - Keys to reference the index value + */ + function addBidData(bid, keys) { + const index = bids.length + bids.push(bid) + + if (Array.isArray(keys)) { + keys.forEach((key) => { + adKeyReference(String(key), index) + }) + return + } + + adKeyReference(String(keys), index) + } + + /** + * Get's the bid data refrerenced by the key + * @param {String} key - The key value to find the bid data by + * @returns {Object} - The bid data + */ + function getBidData(key) { + const stringKey = String(key) + if (referenceMap.hasOwnProperty(stringKey)) { + return bids[referenceMap[stringKey]] + } + } + + // Return API + return { + addBidData, + getBidData, + } +} + const bidRequestMap = {} const adUnitsRequested = {} +const extData = {} + +// Filtering +const adsToFilter = new Set() +const advertisersToFilter = new Set() +const campaignsToFilter = new Set() // Prebid adapter referrence doc: https://docs.prebid.org/dev-docs/bidder-adaptor.html @@ -45,7 +108,7 @@ export const spec = { if (!bid.params) return bid.bidder === BIDDER_CODE // Check if any supplied parameters are invalid - const hasInvalidParameters = Object.keys(bid.params).some(key => { + const hasInvalidParameters = Object.keys(bid.params).some((key) => { const value = bid.params[key] const validityCheck = validParameter[key] @@ -69,8 +132,8 @@ export const spec = { */ buildRequests: function (validBidRequests, bidderRequest) { const placementIds = new Set() - const placmentBidIdMap = {} let placementId, pageUrl + const bidDataMap = BidDataMap() validBidRequests.forEach((request) => { pageUrl = deepAccess( request, @@ -83,13 +146,13 @@ export const spec = { placementIds.add(placementId) } - var key = placementId || request.adUnitCode - placmentBidIdMap[key] = { + const bidData = { bidId: request.bidId, size: getLargestSize(request.sizes), } + bidDataMap.addBidData(bidData, [placementId, request.adUnitCode]) }) - bidRequestMap[bidderRequest.bidderRequestId] = placmentBidIdMap + bidRequestMap[bidderRequest.bidderRequestId] = bidDataMap // Build adUnit data const adUnitData = { @@ -123,6 +186,20 @@ export const spec = { }, ] + // Add filtering + if (adsToFilter.size > 0) { + params.unshift({ key: 'ntv_atf', value: Array.from(adsToFilter).join(',') }) + } + + if (advertisersToFilter.size > 0) { + params.unshift({ key: 'ntv_avtf', value: Array.from(advertisersToFilter).join(',') }) + } + + if (campaignsToFilter.size > 0) { + params.unshift({ key: 'ntv_ctf', value: Array.from(campaignsToFilter).join(',') }) + } + + // Add placement IDs if (placementIds.size > 0) { // Convert Set to Array (IE 11 Safe) const placements = [] @@ -131,6 +208,7 @@ export const spec = { params.unshift({ key: 'ntv_ptd', value: placements.join(',') }) } + // Add GDPR params if (bidderRequest.gdprConsent) { // Put on the beginning of the qs param array params.unshift({ @@ -139,6 +217,7 @@ export const spec = { }) } + // Add USP params if (bidderRequest.uspConsent) { // Put on the beginning of the qs param array params.unshift({ key: 'us_privacy', value: bidderRequest.uspConsent }) @@ -195,6 +274,8 @@ export const spec = { }, } + if (bid.ext) extData[bid.id] = bid.ext + bidResponses.push(bidResponse) }) }) @@ -300,7 +381,15 @@ export const spec = { * Will be called when a bid from the adapter won the auction. * @param {Object} bid - The bid that won the auction */ - onBidWon: function (bid) {}, + onBidWon: function (bid) { + const ext = extData[bid.dealId] + + if (!ext) return + + appendFilterData(adsToFilter, ext.adsToFilter) + appendFilterData(advertisersToFilter, ext.advertisersToFilter) + appendFilterData(campaignsToFilter, ext.campaignsToFilter) + }, /** * Will be called when the adserver targeting has been set for a bid from the adapter. @@ -315,12 +404,14 @@ export const spec = { * @returns {String} - The bidId value associated with the corresponding placementId */ getAdUnitData: function (bidderRequestId, bid) { - var data = deepAccess(bidRequestMap, `${bidderRequestId}.${bid.impid}`) + const bidDataMap = bidRequestMap[bidderRequestId] - if (data) return data + const placementId = bid.impid + const adUnitCode = deepAccess(bid, 'ext.ad_unit_id') - var unitCode = deepAccess(bid, 'ext.ad_unit_id') - return deepAccess(bidRequestMap, `${bidderRequestId}.${unitCode}`) + return ( + bidDataMap.getBidData(adUnitCode) || bidDataMap.getBidData(placementId) + ) }, } registerBidder(spec) @@ -375,3 +466,14 @@ function getLargestSize(sizes, method = area) { * @returns The calculated area */ const area = (size) => size[0] * size[1] + +/** + * Save any filter data from winning bid requests for subsequent requests + * @param {Array} filter - The filter data bucket currently stored + * @param {Array} filterData - The filter data to add + */ +function appendFilterData(filter, filterData) { + if (filterData && Array.isArray(filterData) && filterData.length) { + filterData.forEach((ad) => filter.add(ad)) + } +} diff --git a/modules/nextMillenniumBidAdapter.js b/modules/nextMillenniumBidAdapter.js index 4791740bf2f..365d692e614 100644 --- a/modules/nextMillenniumBidAdapter.js +++ b/modules/nextMillenniumBidAdapter.js @@ -13,7 +13,7 @@ export const spec = { isBidRequestValid: function(bid) { return !!( - bid.params.placement_id && isStr(bid.params.placement_id) + (bid.params.placement_id && isStr(bid.params.placement_id)) || (bid.params.group_id && isStr(bid.params.group_id)) ); }, @@ -28,9 +28,10 @@ export const spec = { 'ext': { 'prebid': { 'storedrequest': { - 'id': getBidIdParameter('placement_id', bid.params) + 'id': getPlacementId(bid) } }, + 'nextMillennium': { 'refresh_count': window.nmmRefreshCounts[bid.adUnitCode]++, } @@ -46,10 +47,12 @@ export const spec = { 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 } @@ -91,6 +94,7 @@ export const spec = { meta: { advertiserDomains: bid.adomain || [] }, + ad: bid.adm }); }); @@ -109,6 +113,7 @@ export const spec = { let bidders = [] if (responses) { _each(responses, (response) => { + if (!(response && response.body && response.body.ext && response.body.ext.responsetimemillis)) return _each(Object.keys(response.body.ext.responsetimemillis), b => bidders.push(b)) }) } @@ -124,4 +129,34 @@ export const spec = { }, }; +function getPlacementId(bid) { + const groupId = getBidIdParameter('group_id', bid.params) + const placementId = getBidIdParameter('placement_id', bid.params) + if (!groupId) return placementId + + let windowTop = getTopWindow(window) + let size = [] + if (bid.mediaTypes) { + if (bid.mediaTypes.banner) size = bid.mediaTypes.banner.sizes && bid.mediaTypes.banner.sizes[0] + if (bid.mediaTypes.video) size = bid.mediaTypes.video.playerSize + } + + const host = (windowTop && windowTop.location && windowTop.location.host) || '' + return `g${groupId};${size.join('x')};${host}` +} + +function getTopWindow(curWindow, nesting = 0) { + if (nesting > 10) { + return curWindow + } + + try { + if (curWindow.parent.document) { + return getTopWindow(curWindow.parent.window, ++nesting) + } + } catch (err) { + return curWindow + } +} + registerBidder(spec); diff --git a/modules/nextMillenniumBidAdapter.md b/modules/nextMillenniumBidAdapter.md index 048fe907ac7..136f97d94d5 100644 --- a/modules/nextMillenniumBidAdapter.md +++ b/modules/nextMillenniumBidAdapter.md @@ -21,8 +21,9 @@ Currently module supports only banner mediaType. bids: [{ bidder: 'nextMillennium', params: { - placement_id: '-1' + placement_id: '-1', + group_id: '6731' } }] }]; -``` \ No newline at end of file +``` diff --git a/modules/nextrollBidAdapter.js b/modules/nextrollBidAdapter.js index b5af7ec1486..4e82bc1cbda 100644 --- a/modules/nextrollBidAdapter.js +++ b/modules/nextrollBidAdapter.js @@ -1,18 +1,18 @@ import { deepAccess, - parseUrl, - isNumber, getBidIdParameter, - isPlainObject, + isArray, isFn, + isNumber, + isPlainObject, isStr, + parseUrl, replaceAuctionPrice, - isArray, } from '../src/utils.js'; -import { registerBidder } from '../src/adapters/bidderFactory.js'; -import { BANNER, NATIVE } from '../src/mediaTypes.js'; +import {registerBidder} from '../src/adapters/bidderFactory.js'; +import {BANNER, NATIVE} from '../src/mediaTypes.js'; -import find from 'core-js-pure/features/array/find.js'; +import {find} from '../src/polyfill.js'; const BIDDER_CODE = 'nextroll'; const BIDDER_ENDPOINT = 'https://d.adroll.com/bid/prebid/'; @@ -244,8 +244,8 @@ function _buildResponse(bidResponse, bid) { return response; } -const privacyLink = 'https://info.evidon.com/pub_info/573'; -const privacyIcon = 'https://c.betrad.com/pub/icon1.png'; +const privacyLink = 'https://app.adroll.com/optout/personalized'; +const privacyIcon = 'https://s.adroll.com/j/ad-choices-small.png'; function _getNativeResponse(adm, price) { let baseResponse = { diff --git a/modules/nexx360BidAdapter.js b/modules/nexx360BidAdapter.js new file mode 100644 index 00000000000..a59bb635875 --- /dev/null +++ b/modules/nexx360BidAdapter.js @@ -0,0 +1,149 @@ +import {ajax} from '../src/ajax.js'; +import {config} from '../src/config.js'; +import {registerBidder} from '../src/adapters/bidderFactory.js'; +import {BANNER} from '../src/mediaTypes.js'; + +const BIDDER_CODE = 'nexx360'; +const BIDDER_URL = 'https://fast.nexx360.io/prebid' +const CACHE_URL = 'https://fast.nexx360.io/cache' +const METRICS_TRACKER_URL = 'https://fast.nexx360.io/track-imp' + +export const spec = { + code: BIDDER_CODE, + aliases: ['revenuemaker'], // short 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.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. + */ + buildRequests: function(validBidRequests, bidderRequest) { + const adUnits = []; + const test = config.getConfig('debug') ? 1 : 0; + let adunitValue = null; + let userEids = null; + Object.keys(validBidRequests).forEach(key => { + adunitValue = validBidRequests[key]; + adUnits.push({ + account: adunitValue.params.account, + tagId: adunitValue.params.tagId, + label: adunitValue.adUnitCode, + bidId: adunitValue.bidId, + auctionId: adunitValue.auctionId, + transactionId: adunitValue.transactionId, + mediatypes: adunitValue.mediaTypes + }); + if (adunitValue.userIdAsEids) userEids = adunitValue.userIdAsEids; + }); + const payload = { + adUnits, + href: encodeURIComponent(bidderRequest.refererInfo.referer) + }; + if (bidderRequest) { // modules informations (gdpr, ccpa, schain, userId) + if (bidderRequest.gdprConsent) { + payload.gdpr = bidderRequest.gdprConsent.gdprApplies ? 1 : 0; + payload.gdprConsent = bidderRequest.gdprConsent.consentString; + } else { + payload.gdpr = 0; + payload.gdprConsent = ''; + } + if (bidderRequest.uspConsent) { payload.uspConsent = bidderRequest.uspConsent; } + if (bidderRequest.schain) { payload.schain = bidderRequest.schain; } + if (userEids !== null) payload.userEids = userEids; + }; + if (test) payload.test = 1; + const payloadString = JSON.stringify(payload); + return { + method: 'POST', + url: BIDDER_URL, + data: payloadString, + }; + }, + /** + * Unpack the response from the server into a list of bids. + * + * @param {ServerResponse} serverResponse A successful response from the server. + * @return {Bid[]} An array of bids which were nested inside the server. + */ + interpretResponse: function(serverResponse, bidRequest) { + const serverBody = serverResponse.body; + // const headerValue = serverResponse.headers.get('some-response-header'); + const bidResponses = []; + let bidResponse = null; + let value = null; + if (serverBody.hasOwnProperty('responses')) { + Object.keys(serverBody['responses']).forEach(key => { + value = serverBody['responses'][key]; + bidResponse = { + requestId: value['bidId'], + cpm: value['cpm'], + currency: value['currency'], + width: value['width'], + height: value['height'], + adUrl: `${CACHE_URL}?uuid=${value['uuid']}`, + ttl: value['ttl'], + creativeId: value['creativeId'], + netRevenue: true, + nexx360: { + 'ssp': value['bidder'], + 'consent': value['consent'], + 'tagId': value['tagId'] + }, + /* + meta: { + 'advertiserDomains': value['adomain'] + } + */ + }; + bidResponses.push(bidResponse); + }); + } + return bidResponses; + }, + + /** + * Register the user sync pixels which should be dropped after the auction. + * + * @param {SyncOptions} syncOptions Which user syncs are allowed? + * @param {ServerResponse[]} serverResponses List of server's responses. + * @return {UserSync[]} The user syncs which should be dropped. + */ + getUserSyncs: function(syncOptions, serverResponses, gdprConsent, uspConsent) { + if (typeof serverResponses === 'object' && serverResponses != null && serverResponses.length > 0 && serverResponses[0].hasOwnProperty('body') && + serverResponses[0].body.hasOwnProperty('cookies') && typeof serverResponses[0].body.cookies === 'object') { + return serverResponses[0].body.cookies.slice(0, 5); + } else { + return []; + } + }, + + /** + * 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' }; + if (bid.hasOwnProperty('nexx360')) { + if (bid.nexx360.hasOwnProperty('ssp')) params.ssp = bid.nexx360.ssp; + if (bid.nexx360.hasOwnProperty('tagId')) params.tag_id = bid.nexx360.tagId; + if (bid.nexx360.hasOwnProperty('consent')) params.consent = bid.nexx360.consent; + }; + params.price = bid.cpm; + const url = `${METRICS_TRACKER_URL}?${new URLSearchParams(params).toString()}`; + ajax(url, null, undefined, {method: 'GET', withCredentials: true}); + return true; + } + +} +registerBidder(spec); diff --git a/modules/nexx360BidAdapter.md b/modules/nexx360BidAdapter.md new file mode 100644 index 00000000000..882d83cb24e --- /dev/null +++ b/modules/nexx360BidAdapter.md @@ -0,0 +1,35 @@ +# Overview + +``` +Module Name: Nexx360 Bid Adapter +Module Type: Bidder Adapter +Maintainer: gabriel@nexx360.io +``` + +# Description + +Connects to Nexx360 network for bids. + +Nexx360 bid adapter supports Banner only for the time being. + +# Test Parameters +``` +var adUnits = [ + // Banner adUnit + { + code: 'banner-div', + mediaTypes: { + banner: { + sizes: [[300, 250], [300,600]] + } + }, + bids: [{ + bidder: 'nexx360', + params: { + account: '1067', + tagId: 'luvxjvgn' + } + }] + }, +]; +``` diff --git a/modules/nobidBidAdapter.js b/modules/nobidBidAdapter.js index d10c1d0e430..f788093f833 100644 --- a/modules/nobidBidAdapter.js +++ b/modules/nobidBidAdapter.js @@ -6,7 +6,7 @@ import { getStorageManager } from '../src/storageManager.js'; const GVLID = 816; const BIDDER_CODE = 'nobid'; -const storage = getStorageManager(GVLID, BIDDER_CODE); +const storage = getStorageManager({gvlid: GVLID, bidderCode: BIDDER_CODE}); window.nobidVersion = '1.3.2'; window.nobid = window.nobid || {}; window.nobid.bidResponses = window.nobid.bidResponses || {}; diff --git a/modules/novatiqIdSystem.js b/modules/novatiqIdSystem.js index 4c3324d3fc0..ae9cc4c818f 100644 --- a/modules/novatiqIdSystem.js +++ b/modules/novatiqIdSystem.js @@ -5,24 +5,25 @@ * @requires module:modules/userId */ -import { logInfo } from '../src/utils.js'; +import { logInfo, getWindowLocation } from '../src/utils.js'; import { ajax } from '../src/ajax.js'; import { submodule } from '../src/hook.js'; +import { getStorageManager } from '../src/storageManager.js'; /** @type {Submodule} */ export const novatiqIdSubmodule = { -/** -* used to link submodule with config -* @type {string} -*/ + /** + * used to link submodule with config + * @type {string} + */ name: 'novatiq', /** -* 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: { @@ -33,42 +34,202 @@ 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) { - function snowflakeId(placeholder) { - return placeholder - ? (placeholder ^ Math.random() * 16 >> placeholder / 4).toString(16) - : ([1e7] + -1e3 + -4e3 + -8e3 + -1e11 + 1e3).replace(/[018]/g, snowflakeId); - } - const configParams = config.params || {}; - const srcId = this.getSrcId(configParams); + const urlParams = this.getUrlParams(configParams); + const srcId = this.getSrcId(configParams, urlParams); + const sharedId = this.getSharedId(configParams); + const useCallbacks = this.useCallbacks(configParams); + + logInfo('NOVATIQ config params: ' + JSON.stringify(configParams)); logInfo('NOVATIQ Sync request used sourceid param: ' + srcId); + logInfo('NOVATIQ Sync request Shared ID: ' + sharedId); + + return this.sendSyncRequest(useCallbacks, sharedId, srcId, urlParams); + }, + + sendSyncRequest(useCallbacks, sharedId, sspid, urlParams) { + const syncUrl = this.getSyncUrl(sharedId, sspid, urlParams); + const url = syncUrl.url; + const novatiqId = syncUrl.novatiqId; + + // for testing + const sharedStatus = (sharedId != undefined && sharedId != false) ? 'Found' : 'Not Found'; + + if (useCallbacks) { + let res = this.sendAsyncSyncRequest(novatiqId, url); ; + res.sharedStatus = sharedStatus; + + return res; + } else { + this.sendSimpleSyncRequest(novatiqId, url); + + return { 'id': novatiqId, + 'sharedStatus': sharedStatus } + } + }, - let partnerhost; - partnerhost = window.location.hostname; - logInfo('NOVATIQ partner hostname: ' + partnerhost); + sendAsyncSyncRequest(novatiqId, url) { + logInfo('NOVATIQ Setting up ASYNC sync request'); + + const resp = function (callback) { + logInfo('NOVATIQ *** Calling ASYNC sync request'); + + function onSuccess(response, responseObj) { + let syncrc; + var novatiqIdJson = { syncResponse: 0 }; + syncrc = responseObj.status; + logInfo('NOVATIQ Sync Response Code:' + syncrc); + logInfo('NOVATIQ *** ASYNC request returned ' + syncrc); + if (syncrc === 200) { + novatiqIdJson = { 'id': novatiqId, syncResponse: 1 }; + } else { + if (syncrc === 204) { + novatiqIdJson = { 'id': novatiqId, syncResponse: 2 }; + } + } + callback(novatiqIdJson); + } + + ajax(url, + { success: onSuccess }, + undefined, { method: 'GET', withCredentials: false }); + } + + return {callback: resp}; + }, + + sendSimpleSyncRequest(novatiqId, url) { + logInfo('NOVATIQ Sending SIMPLE sync request'); - const novatiqId = snowflakeId(); - const url = 'https://spadsync.com/sync?sptoken=' + novatiqId + '&sspid=' + srcId + '&ssphost=' + partnerhost; ajax(url, undefined, undefined, { method: 'GET', withCredentials: false }); logInfo('NOVATIQ snowflake: ' + novatiqId); - return { 'id': novatiqId } }, - getSrcId(configParams) { - logInfo('NOVATIQ Configured sourceid param: ' + configParams.sourceid); + getNovatiqId(urlParams) { + // standard uuid format + let uuidFormat = [1e7] + -1e3 + -4e3 + -8e3 + -1e11; + if (urlParams.useStandardUuid == false) { + // novatiq standard uuid(like) format + uuidFormat = uuidFormat + 1e3; + } + + return (uuidFormat).replace(/[018]/g, c => + (c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16) + ); + }, + + getSyncUrl(sharedId, sspid, urlParams) { + let novatiqId = this.getNovatiqId(urlParams); + + let url = 'https://spadsync.com/sync?' + urlParams.novatiqId + '=' + novatiqId; + + if (urlParams.useSspId) { + url = url + '&sspid=' + sspid; + } + + if (urlParams.useSspHost) { + let ssphost = getWindowLocation().hostname; + logInfo('NOVATIQ partner hostname: ' + ssphost); + + url = url + '&ssphost=' + ssphost; + } + + // append on the shared ID if we have one + if (sharedId != null) { + url = url + '&sharedId=' + sharedId; + } + + return { + url: url, + novatiqId: novatiqId + } + }, + + getUrlParams(configParams) { + let urlParams = { + novatiqId: 'snowflake', + useStandardUuid: false, + useSspId: true, + useSspHost: true + } + + if (typeof configParams.urlParams != 'undefined') { + if (configParams.urlParams.novatiqId != undefined) { + urlParams.novatiqId = configParams.urlParams.novatiqId; + } + if (configParams.urlParams.useStandardUuid != undefined) { + urlParams.useStandardUuid = configParams.urlParams.useStandardUuid; + } + if (configParams.urlParams.useSspId != undefined) { + urlParams.useSspId = configParams.urlParams.useSspId; + } + if (configParams.urlParams.useSspHost != undefined) { + urlParams.useSspHost = configParams.urlParams.useSspHost; + } + } + + return urlParams; + }, + + useCallbacks(configParams) { + return typeof configParams.useCallbacks != 'undefined' && configParams.useCallbacks === true; + }, - function isHex(str) { - var a = parseInt(str, 16); - return (a.toString(16) === str) + useSharedId(configParams) { + return typeof configParams.useSharedId != 'undefined' && configParams.useSharedId === true; + }, + + getCookieOrStorageID(configParams) { + let cookieOrStorageID = '_pubcid'; + + if (typeof configParams.sharedIdName != 'undefined' && configParams.sharedIdName != null && configParams.sharedIdName != '') { + cookieOrStorageID = configParams.sharedIdName; + logInfo('NOVATIQ sharedID name redefined: ' + cookieOrStorageID); + } + + return cookieOrStorageID; + }, + + // return null if we aren't supposed to use one or we are but there isn't one present + getSharedId(configParams) { + let sharedId = null; + if (this.useSharedId(configParams)) { + let cookieOrStorageID = this.getCookieOrStorageID(configParams); + const storage = getStorageManager({moduleName: 'pubCommonId'}); + + // first check local storage + if (storage.hasLocalStorage()) { + sharedId = storage.getDataFromLocalStorage(cookieOrStorageID); + logInfo('NOVATIQ sharedID retrieved from local storage:' + sharedId); + } + + // if nothing check the local cookies + if (sharedId == null) { + sharedId = storage.getCookie(cookieOrStorageID); + logInfo('NOVATIQ sharedID retrieved from cookies:' + sharedId); + } } + logInfo('NOVATIQ sharedID returning: ' + sharedId); + + return sharedId; + }, + + getSrcId(configParams, urlParams) { + if (urlParams.useSspId == false) { + logInfo('NOVATIQ Configured to NOT use sspid'); + return ''; + } + + logInfo('NOVATIQ Configured sourceid param: ' + configParams.sourceid); + let srcId; if (typeof configParams.sourceid === 'undefined' || configParams.sourceid === null || configParams.sourceid === '') { srcId = '000'; @@ -76,9 +237,6 @@ export const novatiqIdSubmodule = { } else if (configParams.sourceid.length < 3 || configParams.sourceid.length > 3) { srcId = '001'; logInfo('NOVATIQ sourceid param set to value 001 due to wrong size in config section 3 chars max e.g. 1ab'); - } else if (isHex(configParams.sourceid) == false) { - srcId = '002'; - logInfo('NOVATIQ sourceid param set to value 002 due to wrong format in config section expecting hex value only'); } else { srcId = configParams.sourceid; } diff --git a/modules/novatiqIdSystem.md b/modules/novatiqIdSystem.md index ce561a696e3..f33fc700311 100644 --- a/modules/novatiqIdSystem.md +++ b/modules/novatiqIdSystem.md @@ -1,8 +1,8 @@ -# Novatiq Snowflake ID +# Novatiq Hyper ID -Novatiq proprietary dynamic snowflake ID is a unique, non sequential and single use identifier for marketing activation. Our in network solution matches verification requests to telco network IDs, safely and securely inside telecom infrastructure. Novatiq snowflake ID can be used for identity validation and as a secured 1st party data delivery mechanism. +The Novatiq proprietary dynamic Hyper ID is a unique, non sequential and single use identifier for marketing activation. Our in network solution matches verification requests to telco network IDs safely and securely inside telecom infrastructure. The Novatiq Hyper ID can be used for identity validation and as a secured 1st party data delivery mechanism. -## Novatiq Snowflake ID Configuration +## Novatiq Hyper ID Configuration Enable by adding the Novatiq submodule to your Prebid.js package with: @@ -18,19 +18,80 @@ pbjs.setConfig({ userIds: [{ name: 'novatiq', params: { - sourceid '1a3', // change to the Partner Number you received from Novatiq + // change to the Partner Number you received from Novatiq + sourceid '1a3' } } }], - auctionDelay: 50 // 50ms maximum auction delay, applies to all userId modules + // 50ms maximum auction delay, applies to all userId modules + auctionDelay: 50 } }); ``` -| Param under userSync.userIds[] | Scope | Type | Description | Example | +### Parameters for the Novatiq Module +| Param | Scope | Type | Description | Example | | --- | --- | --- | --- | --- | | name | Required | String | Module identification: `"novatiq"` | `"novatiq"` | | params | Required | Object | Configuration specifications for the Novatiq module. | | -| params.sourceid | Required | String | This is the Novatiq Partner Number obtained via Novatiq registration. | `1a3` | +| params.sourceid | Required | String | The Novatiq Partner Number obtained via Novatiq | `1a3` | +| params.useSharedId | Optional | Boolean | Use the sharedID module if it's activated. | `true` | +| params.sharedIdName | Optional | String | Same as the SharedID "name" parameter
Defaults to "_pubcid" | `"demo_pubcid"` | +| params.useCallbacks | Optional | Boolean | Use callbacks for custom integrations | `false` | +| params.urlParams | Optional | Object | Sync URl configuration for custom integrations | | +| params.urlParams.novatiqId | Optional | String | The name of the parameter used to indicate the novatiq ID uuid | `snowflake` | +| params.urlParams.useStandardUuid | Optional | Boolean | Use a standard UUID format, or the Novatiq UUID format | `false` | +| params.urlParams.useSspId | Optional | Boolean | Send the sspid (sourceid) along with the sync request | `false` | +| params.urlParams.useSspHost | Optional | Boolean | Send the ssphost along with the sync request | `false` | + +# Novatiq Hyper ID with Prebid SharedID support +You can make use of the Prebid.js SharedId module as follows. + +## Novatiq Hyper ID Configuration + +Enable by adding the Novatiq and SharedId submodule to your Prebid.js package with: + +``` +gulp build --modules=novatiqIdSystem,userId,pubCommonId +``` + +Module activation and configuration: + +```javascript +pbjs.setConfig({ + userSync: { + userIds: [ + { + name: "pubCommonId", + storage: { + type: "cookie", + // optional: will default to _pubcid if left blank + name: "demo_pubcid", + + // expires in 1 years + expires: 365 + }, + bidders: [ 'adtarget' ] + }, + { + name: 'novatiq', + params: { + // change to the Partner Number you received from Novatiq + sourceid '1a3', + + // Use the sharedID module + useSharedId: true, + + // optional: will default to _pubcid if left blank. + // If not left blank must match "name" in the the module above + sharedIdName: 'demo_pubcid' + } + } + }], + // 50ms maximum auction delay, applies to all userId modules + auctionDelay: 50 + } +}); +``` If you have any questions, please reach out to us at prebid@novatiq.com. diff --git a/modules/oguryBidAdapter.js b/modules/oguryBidAdapter.js index c21eaffa48c..7d2989b2066 100644 --- a/modules/oguryBidAdapter.js +++ b/modules/oguryBidAdapter.js @@ -10,7 +10,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.2.7'; +const ADAPTER_VERSION = '1.2.10'; function isBidRequestValid(bid) { const adUnitSizes = getAdUnitSizes(bid); @@ -41,10 +41,10 @@ function buildRequests(validBidRequests, bidderRequest) { const openRtbBidRequestBanner = { id: bidderRequest.auctionId, tmax: DEFAULT_TIMEOUT, - at: 2, + at: 1, regs: { ext: { - gdpr: 1 + gdpr: bidderRequest.gdprConsent && bidderRequest.gdprConsent.gdprApplies ? 1 : 0 }, }, site: { @@ -63,14 +63,7 @@ function buildRequests(validBidRequests, bidderRequest) { } }; - if (bidderRequest.hasOwnProperty('gdprConsent') && - bidderRequest.gdprConsent.hasOwnProperty('gdprApplies')) { - openRtbBidRequestBanner.regs.ext.gdpr = bidderRequest.gdprConsent.gdprApplies ? 1 : 0 - } - - if (bidderRequest.hasOwnProperty('gdprConsent') && - bidderRequest.gdprConsent.hasOwnProperty('consentString') && - bidderRequest.gdprConsent.consentString.length > 0) { + if (bidderRequest.gdprConsent && bidderRequest.gdprConsent.consentString) { openRtbBidRequestBanner.user.ext.consent = bidderRequest.gdprConsent.consentString } @@ -78,7 +71,7 @@ function buildRequests(validBidRequests, bidderRequest) { const sizes = getAdUnitSizes(bidRequest) .map(size => ({ w: size[0], h: size[1] })); - if (bidRequest.hasOwnProperty('mediaTypes') && + if (bidRequest.mediaTypes && bidRequest.mediaTypes.hasOwnProperty('banner')) { openRtbBidRequestBanner.site.id = bidRequest.params.assetKey; @@ -166,11 +159,11 @@ function onBidWon(bid) { w.OG_PREBID_BID_OBJECT = { ...(bid && { ...bid }), } - if (bid && bid.hasOwnProperty('nurl') && bid.nurl.length > 0) ajax(bid['nurl'], null); + if (bid && bid.nurl) ajax(bid.nurl, null); } function onTimeout(timeoutData) { - ajax(`${TIMEOUT_MONITORING_HOST}/bid_timeout`, null, JSON.stringify(timeoutData[0]), { + ajax(`${TIMEOUT_MONITORING_HOST}/bid_timeout`, null, JSON.stringify({...timeoutData[0], location: window.location.href}), { method: 'POST', contentType: 'application/json' }); diff --git a/modules/oneVideoBidAdapter.js b/modules/oneVideoBidAdapter.js index e0db143dc0f..aeb19e7c32c 100644 --- a/modules/oneVideoBidAdapter.js +++ b/modules/oneVideoBidAdapter.js @@ -10,6 +10,7 @@ export const spec = { SYNC_ENDPOINT1: 'https://pixel.advertising.com/ups/57304/sync?gdpr=&gdpr_consent=&_origin=0&redir=true', SYNC_ENDPOINT2: 'https://match.adsrvr.org/track/cmf/generic?ttd_pid=adaptv&ttd_tpi=1', supportedMediaTypes: ['video', 'banner'], + gvlid: 25, /** * Determines whether or not the given bid request is valid. * diff --git a/modules/onetagBidAdapter.js b/modules/onetagBidAdapter.js index 5642dce9018..89c614dba23 100644 --- a/modules/onetagBidAdapter.js +++ b/modules/onetagBidAdapter.js @@ -1,20 +1,20 @@ 'use strict'; -import { BANNER, VIDEO } from '../src/mediaTypes.js'; -import { INSTREAM, OUTSTREAM } from '../src/video.js'; -import { Renderer } from '../src/Renderer.js'; -import find from 'core-js-pure/features/array/find.js'; -import { getStorageManager } from '../src/storageManager.js'; -import { registerBidder } from '../src/adapters/bidderFactory.js'; -import { createEidsArray } from './userId/eids.js'; -import { deepClone } from '../src/utils.js'; +import {BANNER, VIDEO} from '../src/mediaTypes.js'; +import {INSTREAM, OUTSTREAM} from '../src/video.js'; +import {Renderer} from '../src/Renderer.js'; +import {find} from '../src/polyfill.js'; +import {getStorageManager} from '../src/storageManager.js'; +import {registerBidder} from '../src/adapters/bidderFactory.js'; +import {createEidsArray} from './userId/eids.js'; +import {deepClone} from '../src/utils.js'; const ENDPOINT = 'https://onetag-sys.com/prebid-request'; const USER_SYNC_ENDPOINT = 'https://onetag-sys.com/usync/'; const BIDDER_CODE = 'onetag'; const GVLID = 241; -const storage = getStorageManager(GVLID); +const storage = getStorageManager({gvlid: GVLID, bidderCode: BIDDER_CODE}); /** * Determines whether or not the given bid request is valid. diff --git a/modules/open8BidAdapter.js b/modules/open8BidAdapter.js new file mode 100644 index 00000000000..7fa97235525 --- /dev/null +++ b/modules/open8BidAdapter.js @@ -0,0 +1,188 @@ +import { Renderer } from '../src/Renderer.js'; +import {ajax} from '../src/ajax.js'; +import { createTrackPixelHtml, getBidIdParameter, logError, logWarn, tryAppendQueryString } from '../src/utils.js'; +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { VIDEO, BANNER } from '../src/mediaTypes.js'; + +const BIDDER_CODE = 'open8'; +const URL = 'https://as.vt.open8.com/v1/control/prebid'; +const AD_TYPE = { + VIDEO: 1, + BANNER: 2 +}; + +export const spec = { + code: BIDDER_CODE, + supportedMediaTypes: [VIDEO, BANNER], + + isBidRequestValid: function(bid) { + return !!(bid.params.slotKey); + }, + + buildRequests: function(validBidRequests, bidderRequest) { + var requests = []; + for (var i = 0; i < validBidRequests.length; i++) { + var bid = validBidRequests[i]; + var queryString = ''; + var slotKey = getBidIdParameter('slotKey', bid.params); + queryString = tryAppendQueryString(queryString, 'slot_key', slotKey); + queryString = tryAppendQueryString(queryString, 'imp_id', generateImpId()); + queryString += ('bid_id=' + bid.bidId); + + requests.push({ + method: 'GET', + url: URL, + data: queryString + }); + } + return requests; + }, + + interpretResponse: function(serverResponse, request) { + var bidderResponse = serverResponse.body; + + if (!bidderResponse.isAdReturn) { + return []; + } + + var ad = bidderResponse.ad; + + const bid = { + slotKey: bidderResponse.slotKey, + userId: bidderResponse.userId, + impId: bidderResponse.impId, + media: bidderResponse.media, + ds: ad.ds, + spd: ad.spd, + fa: ad.fa, + pr: ad.pr, + mr: ad.mr, + nurl: ad.nurl, + requestId: ad.bidId, + cpm: ad.price, + creativeId: ad.creativeId, + dealId: ad.dealId, + currency: ad.currency || 'JPY', + netRevenue: true, + ttl: 360, // 6 minutes + meta: { + advertiserDomains: ad.adomain || [] + } + } + + if (ad.adType === AD_TYPE.VIDEO) { + const videoAd = bidderResponse.ad.video; + Object.assign(bid, { + vastXml: videoAd.vastXml, + width: videoAd.w, + height: videoAd.h, + renderer: newRenderer(bidderResponse), + adResponse: bidderResponse, + mediaType: VIDEO + }); + } else if (ad.adType === AD_TYPE.BANNER) { + const bannerAd = bidderResponse.ad.banner; + Object.assign(bid, { + width: bannerAd.w, + height: bannerAd.h, + ad: bannerAd.adm, + mediaType: BANNER + }); + if (bannerAd.imps) { + try { + bannerAd.imps.forEach(impTrackUrl => { + const tracker = createTrackPixelHtml(impTrackUrl); + bid.ad += tracker; + }); + } catch (error) { + logError('Error appending imp tracking pixel', error); + } + } + } + return [bid]; + }, + + getUserSyncs: function(syncOptions, serverResponses) { + const syncs = []; + if (syncOptions.iframeEnabled && serverResponses.length) { + const syncIFs = serverResponses[0].body.syncIFs; + if (syncIFs) { + syncIFs.forEach(sync => { + syncs.push({ + type: 'iframe', + url: sync + }); + }); + } + } + if (syncOptions.pixelEnabled && serverResponses.length) { + const syncPixs = serverResponses[0].body.syncPixels; + if (syncPixs) { + syncPixs.forEach(sync => { + syncs.push({ + type: 'image', + url: sync + }); + }); + } + } + return syncs; + }, + onBidWon: function(bid) { + if (!bid.nurl) { return; } + const winUrl = bid.nurl.replace( + /\$\{AUCTION_PRICE\}/, + bid.cpm + ); + ajax(winUrl, null); + } +} + +function generateImpId() { + var l = 16; + var c = 'abcdefghijklmnopqrstuvwsyz0123456789'; + var cl = c.length; + var r = ''; + for (var i = 0; i < l; i++) { + r += c[Math.floor(Math.random() * cl)]; + } + return r; +} + +function newRenderer(bidderResponse) { + const renderer = Renderer.install({ + id: bidderResponse.ad.bidId, + url: bidderResponse.ad.video.purl, + loaded: false, + }); + + try { + renderer.setRender(outstreamRender); + } catch (err) { + logWarn('Prebid Error calling setRender on newRenderer', err); + } + + return renderer; +} + +function outstreamRender(bid) { + bid.renderer.push(() => { + window.op8.renderPrebid({ + vastXml: bid.vastXml, + adUnitCode: bid.adUnitCode, + slotKey: bid.slotKey, + impId: bid.impId, + userId: bid.userId, + media: bid.media, + ds: bid.ds, + spd: bid.spd, + fa: bid.fa, + pr: bid.pr, + mr: bid.mr, + adResponse: bid.adResponse, + mediaType: bid.mediaType + }); + }); +} + +registerBidder(spec); diff --git a/modules/openwebBidAdapter.js b/modules/openwebBidAdapter.js index 9476d2d2914..f515eb14011 100644 --- a/modules/openwebBidAdapter.js +++ b/modules/openwebBidAdapter.js @@ -1,8 +1,8 @@ -import { isNumber, deepAccess, isArray, flatten, convertTypes, 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 'core-js-pure/features/array/find.js'; +import {convertTypes, 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'; const ENDPOINT = 'https://ghb.spotim.market/v2/auction'; const BIDDER_CODE = 'openweb'; diff --git a/modules/openxAnalyticsAdapter.js b/modules/openxAnalyticsAdapter.js index f67f8bd0c75..89140c0aacd 100644 --- a/modules/openxAnalyticsAdapter.js +++ b/modules/openxAnalyticsAdapter.js @@ -1,10 +1,23 @@ -import { logInfo, logError, getWindowLocation, parseQS, logMessage, _each, deepAccess, logWarn, _map, flatten, uniques, isEmpty, parseSizesInput } from '../src/utils.js'; +import { + _each, + _map, + deepAccess, + flatten, + getWindowLocation, + isEmpty, + logError, + logInfo, + logMessage, + logWarn, + parseQS, + parseSizesInput, + uniques +} from '../src/utils.js'; import adapter from '../src/AnalyticsAdapter.js'; import CONSTANTS from '../src/constants.json'; import adapterManager from '../src/adapterManager.js'; -import { ajax } from '../src/ajax.js'; -import find from 'core-js-pure/features/array/find.js'; -import includes from 'core-js-pure/features/array/includes.js'; +import {ajax} from '../src/ajax.js'; +import {find, includes} from '../src/polyfill.js'; export const AUCTION_STATES = { INIT: 'initialized', // auction has initialized diff --git a/modules/openxBidAdapter.js b/modules/openxBidAdapter.js index 60b441e2c10..85dcfbb3b47 100644 --- a/modules/openxBidAdapter.js +++ b/modules/openxBidAdapter.js @@ -1,8 +1,18 @@ -import { deepAccess, convertTypes, isArray, inIframe, _map, deepSetValue, _each, parseSizesInput, parseUrl } from '../src/utils.js'; +import { + _each, + _map, + convertTypes, + deepAccess, + deepSetValue, + inIframe, + isArray, + parseSizesInput, + parseUrl +} from '../src/utils.js'; import {config} from '../src/config.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import {BANNER, VIDEO} from '../src/mediaTypes.js'; -import includes from 'core-js-pure/features/array/includes.js' +import {includes} from '../src/polyfill.js'; const SUPPORTED_AD_TYPES = [BANNER, VIDEO]; const VIDEO_TARGETING = ['startdelay', 'mimes', 'minduration', 'maxduration', diff --git a/modules/optimeraRtdProvider.js b/modules/optimeraRtdProvider.js index 024a558baca..dfe8f1bfcf2 100644 --- a/modules/optimeraRtdProvider.js +++ b/modules/optimeraRtdProvider.js @@ -101,7 +101,7 @@ export function scoreFileRequest() { export function returnTargetingData(adUnits, config) { const targeting = {}; try { - adUnits.forEach(function(adUnit) { + adUnits.forEach((adUnit) => { if (optimeraTargeting[adUnit]) { targeting[adUnit] = {}; targeting[adUnit][optimeraKeyName] = [optimeraTargeting[adUnit]]; @@ -141,12 +141,11 @@ export function init(moduleConfig) { setScoresURL(); scoreFileRequest(); return true; - } else { - if (!_moduleParams.clientID) { - logError('Optimera clientID is missing in the Optimera RTD configuration.'); - } - return false; } + if (!_moduleParams.clientID) { + logError('Optimera clientID is missing in the Optimera RTD configuration.'); + } + return false; } /** @@ -163,7 +162,7 @@ export function init(moduleConfig) { export function setScoresURL() { const optimeraHost = window.location.host; const optimeraPathName = window.location.pathname; - let newScoresURL = `${scoresBaseURL}${clientID}/${optimeraHost}${optimeraPathName}.js`; + const newScoresURL = `${scoresBaseURL}${clientID}/${optimeraHost}${optimeraPathName}.js`; if (scoresURL !== newScoresURL) { scoresURL = newScoresURL; fetchScoreFile = true; @@ -174,7 +173,7 @@ export function setScoresURL() { /** * Set the scores for the device if given. - * Add any any insights to the winddow.optimeraInsights object. + * Add data and insights to the winddow.optimera object. * * @param {*} result * @returns {string} JSON string of Optimera Scores. @@ -187,9 +186,16 @@ export function setScores(result) { scores = scores.device[device]; } logInfo(scores); + window.optimera = window.optimera || {}; + window.optimera.data = window.optimera.data || {}; + window.optimera.insights = window.optimera.insights || {}; + Object.keys(scores).map((key) => { + if (key !== 'insights') { + window.optimera.data[key] = scores[key]; + } + }); if (scores.insights) { - window.optimeraInsights = window.optimeraInsights || {}; - window.optimeraInsights.data = scores.insights; + window.optimera.insights = scores.insights; } } catch (e) { logError('Optimera score file could not be parsed.'); diff --git a/modules/orbidderBidAdapter.js b/modules/orbidderBidAdapter.js index 111c1876e14..38af3a8d1d6 100644 --- a/modules/orbidderBidAdapter.js +++ b/modules/orbidderBidAdapter.js @@ -3,7 +3,7 @@ import {registerBidder} from '../src/adapters/bidderFactory.js'; import { getStorageManager } from '../src/storageManager.js'; import { BANNER, NATIVE } from '../src/mediaTypes.js'; -const storageManager = getStorageManager(); +const storageManager = getStorageManager({bidderCode: 'orbidder'}); /** * Determines whether or not the given bid response is valid. diff --git a/modules/outbrainBidAdapter.js b/modules/outbrainBidAdapter.js index 439570e976e..e903f053c7e 100644 --- a/modules/outbrainBidAdapter.js +++ b/modules/outbrainBidAdapter.js @@ -27,9 +27,28 @@ export const spec = { gvlid: GVLID, supportedMediaTypes: [ NATIVE, BANNER ], isBidRequestValid: (bid) => { + if (typeof bid.params !== 'object') { + return false; + } + + if (typeof deepAccess(bid, 'params.publisher.id') !== 'string') { + return false; + } + + if (!!bid.params.tagid && typeof bid.params.tagid !== 'string') { + return false; + } + + if (!!bid.params.bcat && (typeof bid.params.bcat !== 'object' || !bid.params.bcat.every(item => typeof item === 'string'))) { + return false; + } + + if (!!bid.params.badv && (typeof bid.params.badv !== 'object' || !bid.params.badv.every(item => typeof item === 'string'))) { + return false; + } + return ( !!config.getConfig('outbrain.bidderUrl') && - !!deepAccess(bid, 'params.publisher.id') && !!(bid.nativeParams || bid.sizes) ); }, @@ -67,6 +86,13 @@ export const spec = { } } + if (typeof bid.getFloor === 'function') { + const floor = _getFloor(bid, bid.nativeParams ? NATIVE : BANNER); + if (floor) { + imp.bidfloor = floor; + } + } + return imp; }); @@ -190,7 +216,7 @@ export const spec = { registerBidder(spec); function parseNative(bid) { - const { assets, link, eventtrackers } = JSON.parse(bid.adm); + const { assets, link, privacy, eventtrackers } = JSON.parse(bid.adm); const result = { clickUrl: link.url, clickTrackers: link.clicktrackers || undefined @@ -202,6 +228,9 @@ function parseNative(bid) { result[kind] = content.text || content.value || { url: content.url, width: content.w, height: content.h }; } }); + if (privacy) { + result.privacyLink = privacy; + } if (eventtrackers) { result.impressionTrackers = []; eventtrackers.forEach(tracker => { @@ -251,8 +280,8 @@ function getNativeAssets(bid) { if (bidParams.sizes) { const sizes = flatten(bidParams.sizes); - w = sizes[0]; - h = sizes[1]; + w = parseInt(sizes[0], 10); + h = parseInt(sizes[1], 10); } asset[props.name] = { @@ -291,3 +320,15 @@ function transformSizes(requestSizes) { return []; } + +function _getFloor(bid, type) { + const floorInfo = bid.getFloor({ + currency: CURRENCY, + mediaType: type, + size: '*' + }); + if (typeof floorInfo === 'object' && floorInfo.currency === CURRENCY && !isNaN(parseFloat(floorInfo.floor))) { + return parseFloat(floorInfo.floor); + } + return null; +} diff --git a/modules/parrableIdSystem.js b/modules/parrableIdSystem.js index b1553bcb134..04f36d0cb63 100644 --- a/modules/parrableIdSystem.js +++ b/modules/parrableIdSystem.js @@ -7,13 +7,13 @@ // ci trigger: 1 -import { timestamp, logError, logWarn, isEmpty, contains, inIframe, deepClone, isPlainObject } from '../src/utils.js'; -import find from 'core-js-pure/features/array/find.js'; -import { ajax } from '../src/ajax.js'; -import { submodule } from '../src/hook.js'; -import { getRefererInfo } from '../src/refererDetection.js'; -import { uspDataHandler } from '../src/adapterManager.js'; -import { getStorageManager } from '../src/storageManager.js'; +import {contains, deepClone, inIframe, isEmpty, isPlainObject, logError, logWarn, timestamp} from '../src/utils.js'; +import {find} from '../src/polyfill.js'; +import {ajax} from '../src/ajax.js'; +import {submodule} from '../src/hook.js'; +import {getRefererInfo} from '../src/refererDetection.js'; +import {uspDataHandler} from '../src/adapterManager.js'; +import {getStorageManager} from '../src/storageManager.js'; const PARRABLE_URL = 'https://h.parrable.com/prebid'; const PARRABLE_COOKIE_NAME = '_parrable_id'; @@ -23,7 +23,7 @@ const LEGACY_OPTOUT_COOKIE_NAME = '_parrable_optout'; const ONE_YEAR_MS = 364 * 24 * 60 * 60 * 1000; const EXPIRE_COOKIE_DATE = 'Thu, 01 Jan 1970 00:00:00 GMT'; -const storage = getStorageManager(PARRABLE_GVLID); +const storage = getStorageManager({gvlid: PARRABLE_GVLID}); function getExpirationDate() { const oneYearFromNow = new Date(timestamp() + ONE_YEAR_MS); diff --git a/modules/permutiveRtdProvider.js b/modules/permutiveRtdProvider.js index 40282567506..c9d22655a31 100644 --- a/modules/permutiveRtdProvider.js +++ b/modules/permutiveRtdProvider.js @@ -5,15 +5,16 @@ * @module modules/permutiveRtdProvider * @requires module:modules/realTimeData */ -import { getGlobal } from '../src/prebidGlobal.js' -import { submodule } from '../src/hook.js' -import { getStorageManager } from '../src/storageManager.js' -import { deepSetValue, deepAccess, isFn, mergeDeep, logError } from '../src/utils.js' -import { config } from '../src/config.js' -import includes from 'core-js-pure/features/array/includes.js' +import {getGlobal} from '../src/prebidGlobal.js'; +import {submodule} from '../src/hook.js'; +import {getStorageManager} from '../src/storageManager.js'; +import {deepAccess, deepSetValue, isFn, logError, mergeDeep} from '../src/utils.js'; +import {config} from '../src/config.js'; +import {includes} from '../src/polyfill.js'; + const MODULE_NAME = 'permutive' -export const storage = getStorageManager(null, MODULE_NAME) +export const storage = getStorageManager({gvlid: null, moduleName: MODULE_NAME}) function init (moduleConfig, userConsent) { return true diff --git a/modules/pilotxBidAdapter.js b/modules/pilotxBidAdapter.js new file mode 100644 index 00000000000..335c461e3d9 --- /dev/null +++ b/modules/pilotxBidAdapter.js @@ -0,0 +1,147 @@ +import { registerBidder } from '../src/adapters/bidderFactory.js'; +const BIDDER_CODE = 'pilotx'; +const ENDPOINT_URL = '//adn.pilotx.tv/hb' +export const spec = { + code: BIDDER_CODE, + 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. + */ + isBidRequestValid: function (bid) { + let sizesCheck = !!bid.sizes + let paramSizesCheck = !!bid.params.sizes + var sizeConfirmed = false + if (sizesCheck) { + if (bid.sizes.length < 1) { + return false + } else { + sizeConfirmed = true + } + } + if (paramSizesCheck) { + if (bid.params.sizes.length < 1 && !sizeConfirmed) { + return false + } else { + sizeConfirmed = true + } + } + if (!sizeConfirmed) { + return false + } + 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. + */ + buildRequests: function (validBidRequests, bidderRequest) { + let payloadItems = {}; + validBidRequests.forEach(bidRequest => { + let sizes = []; + let placementId = this.setPlacementID(bidRequest.params.placementId) + payloadItems[placementId] = {} + if (bidRequest.sizes.length > 0) { + if (Array.isArray(bidRequest.sizes[0])) { + for (let i = 0; i < bidRequest.sizes.length; i++) { + sizes[i] = [(bidRequest.sizes[i])[0], (bidRequest.sizes[i])[1]] + } + } else { + sizes[0] = [bidRequest.sizes[0], bidRequest.sizes[1]] + } + payloadItems[placementId]['sizes'] = sizes + } + if (bidRequest.mediaTypes != null) { + for (let i in bidRequest.mediaTypes) { + payloadItems[placementId][i] = { + ...bidRequest.mediaTypes[i] + } + } + } + let consentTemp = '' + let consentRequiredTemp = false + if (bidderRequest && bidderRequest.gdprConsent) { + consentTemp = bidderRequest.gdprConsent.consentString + // will check if the gdprApplies field was populated with a boolean value (ie from page config). If it's undefined, then default to true + consentRequiredTemp = (typeof bidderRequest.gdprConsent.gdprApplies === 'boolean') ? bidderRequest.gdprConsent.gdprApplies : true + } + + payloadItems[placementId]['gdprConsentString'] = consentTemp + payloadItems[placementId]['gdprConsentRequired'] = consentRequiredTemp + payloadItems[placementId]['bidId'] = bidRequest.bidId + }); + const payload = payloadItems; + const payloadString = JSON.stringify(payload); + return { + method: 'POST', + url: ENDPOINT_URL, + data: payloadString, + }; + }, + /** + * Unpack the response from the server into a list of bids. + * + * @param {ServerResponse} serverResponse A successful response from the server. + * @return {Bid[]} An array of bids which were nested inside the server. + */ + interpretResponse: function (serverResponse, bidRequest) { + const serverBody = serverResponse.body; + const bidResponses = []; + if (serverBody.mediaType == 'banner') { + const bidResponse = { + requestId: serverBody.requestId, + cpm: serverBody.cpm, + width: serverBody.width, + height: serverBody.height, + creativeId: serverBody.creativeId, + currency: serverBody.currency, + netRevenue: false, + ttl: serverBody.ttl, + ad: serverBody.ad, + mediaType: 'banner', + meta: { + mediaType: 'banner', + advertiserDomains: serverBody.advertiserDomains + } + } + bidResponses.push(bidResponse) + } else if (serverBody.mediaType == 'video') { + const bidResponse = { + requestId: serverBody.requestId, + cpm: serverBody.cpm, + width: serverBody.width, + height: serverBody.height, + creativeId: serverBody.creativeId, + currency: serverBody.currency, + netRevenue: false, + ttl: serverBody.ttl, + vastUrl: serverBody.vastUrl, + mediaType: 'video', + meta: { + mediaType: 'video', + advertiserDomains: serverBody.advertiserDomains + } + } + bidResponses.push(bidResponse) + } + + return bidResponses; + }, + + /** + * Formats placement ids for adserver ingestion purposes + * @param {string[]} The placement ID/s in an array + */ + setPlacementID: function (placementId) { + if (Array.isArray(placementId)) { + return placementId.join('#') + } + return placementId + }, +} +registerBidder(spec); diff --git a/modules/pilotxBidAdapter.md b/modules/pilotxBidAdapter.md new file mode 100644 index 00000000000..37489bda4a0 --- /dev/null +++ b/modules/pilotxBidAdapter.md @@ -0,0 +1,50 @@ +# Overview + +``` +Module Name: Pilotx Prebid Adapter +Module Type: Bidder Adapter +Maintainer: tony@pilotx.tv +``` + +# Description + +Connects to Pilotx + +Pilotx's bid adapter supports banner and video. + +# Test Parameters +``` +// Banner adUnit +var adUnits = [{ + code: 'div-gpt-ad-1460505748561-0', + mediaTypes: { + banner: { + sizes: [[300, 250], [300,600]], + } + }, + bids: [{ + bidder: 'pilotx', + params: { + placementId: ["1423"] + } + }] + +}]; + +// Video adUnit +var videoAdUnit = { + code: 'video1', + mediaTypes: { + video: { + context: 'instream', + playerSize: [640, 480], + } + }, + bids: [{ + bidder: 'pilotx', + params: { + placementId: '1422', + } + }] +}; +``` \ No newline at end of file diff --git a/modules/pixfutureBidAdapter.js b/modules/pixfutureBidAdapter.js index e9db875fc2f..29552ec796d 100644 --- a/modules/pixfutureBidAdapter.js +++ b/modules/pixfutureBidAdapter.js @@ -1,14 +1,22 @@ -import { registerBidder } from '../src/adapters/bidderFactory.js'; -import { getStorageManager } from '../src/storageManager.js'; -import { BANNER } from '../src/mediaTypes.js'; -import { config } from '../src/config.js'; -import includes from 'core-js-pure/features/array/includes.js'; -import { convertCamelToUnderscore, isArray, isNumber, isPlainObject, deepAccess, isEmpty, transformBidderParamKeywords, isFn } from '../src/utils.js'; -import { auctionManager } from '../src/auctionManager.js'; -import find from 'core-js-pure/features/array/find.js'; +import {registerBidder} from '../src/adapters/bidderFactory.js'; +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, + isEmpty, + isFn, + isNumber, + isPlainObject, + transformBidderParamKeywords +} from '../src/utils.js'; +import {auctionManager} from '../src/auctionManager.js'; const SOURCE = 'pbjs'; -const storageManager = getStorageManager(); +const storageManager = getStorageManager({bidderCode: 'pixfuture'}); const USER_PARAMS = ['age', 'externalUid', 'segments', 'gender', 'dnt', 'language']; export const spec = { code: 'pixfuture', diff --git a/modules/prebidServerBidAdapter/index.js b/modules/prebidServerBidAdapter/index.js index efe2576efd5..d30fd5bf810 100644 --- a/modules/prebidServerBidAdapter/index.js +++ b/modules/prebidServerBidAdapter/index.js @@ -1,22 +1,43 @@ import Adapter from '../../src/adapter.js'; -import { createBid } from '../../src/bidfactory.js'; +import {createBid} from '../../src/bidfactory.js'; import { - getPrebidInternal, logError, isStr, isPlainObject, logWarn, generateUUID, bind, logMessage, - triggerPixel, insertUserSyncIframe, deepAccess, mergeDeep, deepSetValue, cleanObj, parseSizesInput, - getBidRequest, getDefinedParams, createTrackPixelHtml, pick, deepClone, uniques, flatten, isNumber, - isEmpty, isArray, logInfo + bind, + cleanObj, + createTrackPixelHtml, + deepAccess, + deepClone, + deepSetValue, + flatten, + generateUUID, + getBidRequest, + getDefinedParams, + getPrebidInternal, + insertUserSyncIframe, + isArray, + isEmpty, + isNumber, + isPlainObject, + isStr, + logError, + logInfo, + logMessage, + logWarn, + mergeDeep, + parseSizesInput, + pick, timestamp, + triggerPixel, + uniques } from '../../src/utils.js'; import CONSTANTS from '../../src/constants.json'; import adapterManager from '../../src/adapterManager.js'; import { config } from '../../src/config.js'; import { VIDEO, NATIVE } from '../../src/mediaTypes.js'; -import { processNativeAdUnitParams } from '../../src/native.js'; import { isValid } from '../../src/adapters/bidderFactory.js'; -import events from '../../src/events.js'; -import includes from 'core-js-pure/features/array/includes.js'; +import * as events from '../../src/events.js'; +import {find, includes} from '../../src/polyfill.js'; import { S2S_VENDORS } from './config.js'; import { ajax } from '../../src/ajax.js'; -import find from 'core-js-pure/features/array/find.js'; +import {hook} from '../../src/hook.js'; const getConfig = config.getConfig; @@ -54,6 +75,7 @@ let eidPermissions; * @typedef {Object} S2SDefaultConfig * @summary Base config properties for server to server header bidding * @property {string} [adapter='prebidServer'] adapter code to use for S2S + * @property {boolean} [allowUnknownBidderCodes=false] allow bids from bidders that were not explicitly requested * @property {boolean} [enabled=false] enables S2S bidding * @property {number} [timeout=1000] timeout for S2S bidders - should be lower than `pbjs.requestBids({timeout})` * @property {number} [syncTimeout=1000] timeout for cookie sync iframe / image rendering @@ -77,10 +99,12 @@ let eidPermissions; * @type {S2SDefaultConfig} */ const s2sDefaultConfig = { + bidders: Object.freeze([]), timeout: 1000, syncTimeout: 1000, maxBids: 1, adapter: 'prebidServer', + allowUnknownBidderCodes: false, adapterOptions: {}, syncUrlModifier: {} }; @@ -120,7 +144,7 @@ function updateConfigDefaultVendor(option) { */ function validateConfigRequiredProps(option) { const keys = Object.keys(option); - if (['accountId', 'bidders', 'endpoint'].filter(key => { + if (['accountId', 'endpoint'].filter(key => { if (!includes(keys, key)) { logError(key + ' missing in server to server config'); return true; @@ -433,7 +457,6 @@ let nativeEventTrackerMethodMap = { * Protocol spec for OpenRTB endpoint * e.g., https:///v1/openrtb2/auction */ -let bidIdMap = {}; let nativeAssetCache = {}; // store processed native params to preserve /** @@ -491,8 +514,24 @@ export function resetWurlMap() { wurlMap = {}; } -const OPEN_RTB_PROTOCOL = { - buildRequest(s2sBidRequest, bidRequests, adUnits, s2sConfig, requestedBidders) { +function ORTB2(s2sBidRequest, bidderRequests, adUnits, requestedBidders) { + this.s2sBidRequest = s2sBidRequest; + this.bidderRequests = bidderRequests; + this.adUnits = adUnits; + this.s2sConfig = s2sBidRequest.s2sConfig; + this.requestedBidders = requestedBidders; + + this.bidIdMap = {}; + this.adUnitsByImp = {}; + this.impRequested = {}; + this.auctionId = bidderRequests.map(br => br.auctionId).reduce((l, r) => (l == null || l === r) && r); + this.requestTimestamp = timestamp(); +} + +Object.assign(ORTB2.prototype, { + buildRequest() { + const {s2sBidRequest, bidderRequests: bidRequests, adUnits, s2sConfig, requestedBidders} = this; + let imps = []; let aliases = {}; const firstBidRequest = bidRequests[0]; @@ -500,6 +539,14 @@ const OPEN_RTB_PROTOCOL = { // transform ad unit into array of OpenRTB impression objects let impIds = new Set(); adUnits.forEach(adUnit => { + // TODO: support labels / conditional bids + // for now, just warn about them + adUnit.bids.forEach((bid) => { + if (bid.mediaTypes != null) { + logWarn(`Prebid Server adapter does not (yet) support bidder-specific mediaTypes for the same adUnit. Size mapping configuration will be ignored for adUnit: ${adUnit.code}, bidder: ${bid.bidder}`); + } + }) + // in case there is a duplicate imp.id, add '-2' suffix to the second imp.id. // e.g. if there are 2 adUnits (case of twin adUnit codes) with code 'test', // first imp will have id 'test' and second imp will have id 'test-2' @@ -510,8 +557,9 @@ const OPEN_RTB_PROTOCOL = { impressionId = `${adUnit.code}-${i}`; } impIds.add(impressionId); + this.adUnitsByImp[impressionId] = adUnit; - const nativeParams = processNativeAdUnitParams(deepAccess(adUnit, 'mediaTypes.native')); + const nativeParams = adUnit.nativeParams; let nativeAssets; if (nativeParams) { try { @@ -584,9 +632,7 @@ const OPEN_RTB_PROTOCOL = { const bannerParams = deepAccess(adUnit, 'mediaTypes.banner'); adUnit.bids.forEach(bid => { - // OpenRTB response contains imp.id and bidder name. These are - // combined to create a unique key for each bid since an id isn't returned - bidIdMap[`${impressionId}${bid.bidder}`] = bid.bid_id; + this.setBidRequestId(impressionId, bid.bidder, bid.bid_id); // check for and store valid aliases to add to the request if (adapterManager.aliasRegistry[bid.bidder]) { const bidder = adapterManager.bidderRegistry[bid.bidder]; @@ -661,6 +707,7 @@ const OPEN_RTB_PROTOCOL = { // get bidder params in form { : {...params} } // initialize reduce function with the user defined `ext` properties on the ad unit const ext = adUnit.bids.reduce((acc, bid) => { + if (bid.bidder == null) return acc; const adapter = adapterManager.bidderRegistry[bid.bidder]; if (adapter && adapter.getSpec().transformBidParams) { bid.params = adapter.getSpec().transformBidParams(bid.params, true, adUnit, bidRequests); @@ -669,7 +716,7 @@ const OPEN_RTB_PROTOCOL = { return acc; }, {...deepAccess(adUnit, 'ortb2Imp.ext')}); - const imp = { id: impressionId, ext, secure: s2sConfig.secure }; + const imp = { ...adUnit.ortb2Imp, id: impressionId, ext, secure: s2sConfig.secure }; const ortb2 = {...deepAccess(adUnit, 'ortb2Imp.ext.data')}; Object.keys(ortb2).forEach(prop => { @@ -700,7 +747,7 @@ const OPEN_RTB_PROTOCOL = { } }); - Object.assign(imp, mediaTypes); + mergeDeep(imp, mediaTypes); // if storedAuctionResponse has been set, pass SRID const storedAuctionResponseBid = find(firstBidRequest.bids, bid => (bid.adUnitCode === adUnit.code && bid.storedAuctionResponse)); @@ -854,10 +901,12 @@ const OPEN_RTB_PROTOCOL = { addBidderFirstPartyDataToRequest(request); + request.imp.forEach((imp) => this.impRequested[imp.id] = imp); return request; }, - interpretResponse(response, bidderRequests, s2sConfig) { + interpretResponse(response) { + const {bidderRequests, s2sConfig} = this; const bids = []; [['errors', 'serverErrors'], ['responsetimemillis', 'serverResponseTimeMs']] @@ -867,22 +916,27 @@ const OPEN_RTB_PROTOCOL = { // a seatbid object contains a `bid` array and a `seat` string response.seatbid.forEach(seatbid => { (seatbid.bid || []).forEach(bid => { - let bidRequest; - let key = `${bid.impid}${seatbid.seat}`; - if (bidIdMap[key]) { - bidRequest = getBidRequest( - bidIdMap[key], - bidderRequests - ); + let bidRequest = this.getBidRequest(bid.impid, seatbid.seat); + if (bidRequest == null) { + if (!s2sConfig.allowUnknownBidderCodes) { + logWarn(`PBS adapter received bid from unknown bidder (${seatbid.seat}), but 's2sConfig.allowUnknownBidderCodes' is not set. Ignoring bid.`); + return; + } + // for stored impression, a request was made with bidder code `null`. Pick it up here so that NO_BID, BID_WON, etc events + // can work as expected (otherwise, the original request will always result in NO_BID). + bidRequest = this.getBidRequest(bid.impid, null); } const cpm = bid.price; const status = cpm !== 0 ? CONSTANTS.STATUS.GOOD : CONSTANTS.STATUS.NO_BID; - let bidObject = createBid(status, bidRequest || { + let bidObject = createBid(status, { bidder: seatbid.seat, - src: TYPE + src: TYPE, + bidId: bidRequest ? (bidRequest.bidId || bidRequest.bid_Id) : null, + transactionId: this.adUnitsByImp[bid.impid].transactionId, + auctionId: this.auctionId, }); - + bidObject.requestTimestamp = this.requestTimestamp; bidObject.cpm = cpm; // temporarily leaving attaching it to each bidResponse so no breaking change @@ -900,7 +954,7 @@ const OPEN_RTB_PROTOCOL = { // store wurl by auctionId and adId so it can be accessed from the BID_WON event handler if (isStr(deepAccess(bid, 'ext.prebid.events.win'))) { - addWurl(bidRequest.auctionId, bidObject.adId, deepAccess(bid, 'ext.prebid.events.win')); + addWurl(this.auctionId, bidObject.adId, deepAccess(bid, 'ext.prebid.events.win')); } let extPrebidTargeting = deepAccess(bid, 'ext.prebid.targeting'); @@ -921,9 +975,8 @@ const OPEN_RTB_PROTOCOL = { if (deepAccess(bid, 'ext.prebid.type') === VIDEO) { bidObject.mediaType = VIDEO; - let sizes = bidRequest.sizes && bidRequest.sizes[0]; - bidObject.playerWidth = sizes[0]; - bidObject.playerHeight = sizes[1]; + const impReq = this.impRequested[bid.impid]; + [bidObject.playerWidth, bidObject.playerHeight] = [impReq.video.w, impReq.video.h]; // try to get cache values from 'response.ext.prebid.cache.js' // else try 'bid.ext.prebid.targeting' as fallback @@ -1006,7 +1059,6 @@ const OPEN_RTB_PROTOCOL = { bidObject.width = bid.w; bidObject.height = bid.h; if (bid.dealid) { bidObject.dealId = bid.dealid; } - bidObject.requestId = bidRequest.bidId || bidRequest.bid_Id; bidObject.creative_id = bid.crid; bidObject.creativeId = bid.crid; if (bid.burl) { bidObject.burl = bid.burl; } @@ -1021,14 +1073,24 @@ const OPEN_RTB_PROTOCOL = { bidObject.ttl = (bid.exp) ? bid.exp : configTtl; bidObject.netRevenue = (bid.netRevenue) ? bid.netRevenue : DEFAULT_S2S_NETREVENUE; - bids.push({ adUnit: bidRequest.adUnitCode, bid: bidObject }); + bids.push({ adUnit: this.adUnitsByImp[bid.impid].code, bid: bidObject }); }); }); } return bids; + }, + setBidRequestId(impId, bidderCode, bidId) { + this.bidIdMap[this.impBidderKey(impId, bidderCode)] = bidId; + }, + getBidRequest(impId, bidderCode) { + const key = this.impBidderKey(impId, bidderCode); + return this.bidIdMap[key] && getBidRequest(this.bidIdMap[key], this.bidderRequests); + }, + impBidderKey(impId, bidderCode) { + return `${impId}${bidderCode}`; } -}; +}); /** * BID_WON event to request the wurl @@ -1076,20 +1138,8 @@ export function PrebidServer() { /* Prebid executes this function when the page asks to send out bid requests */ baseAdapter.callBids = function(s2sBidRequest, bidRequests, addBidResponse, done, ajax) { - const adUnits = deepClone(s2sBidRequest.ad_units); let { gdprConsent, uspConsent } = getConsentData(bidRequests); - // at this point ad units should have a size array either directly or mapped so filter for that - const validAdUnits = adUnits.filter(unit => - unit.mediaTypes && (unit.mediaTypes.native || (unit.mediaTypes.banner && unit.mediaTypes.banner.sizes) || (unit.mediaTypes.video && unit.mediaTypes.video.playerSize)) - ); - - // in case config.bidders contains invalid bidders, we only process those we sent requests for - const requestedBidders = validAdUnits - .map(adUnit => adUnit.bids.map(bid => bid.bidder).filter(uniques)) - .reduce(flatten) - .filter(uniques); - if (Array.isArray(_s2sConfigs)) { if (s2sBidRequest.s2sConfig && s2sBidRequest.s2sConfig.syncEndpoint && getMatchingConsentUrl(s2sBidRequest.s2sConfig.syncEndpoint, gdprConsent)) { let syncBidders = s2sBidRequest.s2sConfig.bidders @@ -1099,59 +1149,23 @@ export function PrebidServer() { queueSync(syncBidders, gdprConsent, uspConsent, s2sBidRequest.s2sConfig); } - const request = OPEN_RTB_PROTOCOL.buildRequest(s2sBidRequest, bidRequests, validAdUnits, s2sBidRequest.s2sConfig, requestedBidders); - const requestJson = request && JSON.stringify(request); - logInfo('BidRequest: ' + requestJson); - const endpointUrl = getMatchingConsentUrl(s2sBidRequest.s2sConfig.endpoint, gdprConsent); - if (request && requestJson && endpointUrl) { - ajax( - endpointUrl, - { - success: response => handleResponse(response, requestedBidders, bidRequests, addBidResponse, done, s2sBidRequest.s2sConfig), - error: done - }, - requestJson, - { contentType: 'text/plain', withCredentials: true } - ); - } else { - logError('PBS request not made. Check endpoints.'); - } - } - }; - - /* Notify Prebid of bid responses so bids can get in the auction */ - function handleResponse(response, requestedBidders, bidderRequests, addBidResponse, done, s2sConfig) { - let result; - let bids = []; - let { gdprConsent, uspConsent } = getConsentData(bidderRequests); - - try { - result = JSON.parse(response); - - bids = OPEN_RTB_PROTOCOL.interpretResponse( - result, - bidderRequests, - s2sConfig - ); - - bids.forEach(({adUnit, bid}) => { - if (isValid(adUnit, bid, bidderRequests)) { - addBidResponse(adUnit, bid); + processPBSRequest(s2sBidRequest, bidRequests, ajax, { + onResponse: function (isValid, requestedBidders) { + if (isValid) { + bidRequests.forEach(bidderRequest => events.emit(CONSTANTS.EVENTS.BIDDER_DONE, bidderRequest)); + } + done(); + doClientSideSyncs(requestedBidders, gdprConsent, uspConsent); + }, + onError: done, + onBid: function ({adUnit, bid}) { + if (isValid(adUnit, bid)) { + addBidResponse(adUnit, bid); + } } - }); - - bidderRequests.forEach(bidderRequest => events.emit(CONSTANTS.EVENTS.BIDDER_DONE, bidderRequest)); - } catch (error) { - logError(error); + }) } - - if (!result || (result.status && includes(result.status, 'Error'))) { - logError('error parsing response: ', result.status); - } - - done(); - doClientSideSyncs(requestedBidders, gdprConsent, uspConsent); - } + }; // Listen for bid won to call wurl events.on(CONSTANTS.EVENTS.BID_WON, bidWonHandler); @@ -1163,6 +1177,66 @@ export function PrebidServer() { }); } +/** + * Build and send the appropriate HTTP request over the network, then interpret the response. + * @param s2sBidRequest + * @param bidRequests + * @param ajax + * @param onResponse {function(boolean, Array[String])} invoked on a successful HTTP response - with a flag indicating whether it was successful, + * and a list of the unique bidder codes that were sent in the request + * @param onError {function(String, {})} invoked on HTTP failure - with status message and XHR error + * @param onBid {function({})} invoked once for each bid in the response - with the bid as returned by interpretResponse + */ +export const processPBSRequest = hook('sync', function (s2sBidRequest, bidRequests, ajax, {onResponse, onError, onBid}) { + let { gdprConsent } = getConsentData(bidRequests); + const adUnits = deepClone(s2sBidRequest.ad_units); + + // at this point ad units should have a size array either directly or mapped so filter for that + const validAdUnits = adUnits.filter(unit => + unit.mediaTypes && (unit.mediaTypes.native || (unit.mediaTypes.banner && unit.mediaTypes.banner.sizes) || (unit.mediaTypes.video && unit.mediaTypes.video.playerSize)) + ); + + // in case config.bidders contains invalid bidders, we only process those we sent requests for + const requestedBidders = validAdUnits + .map(adUnit => adUnit.bids.map(bid => bid.bidder).filter(uniques)) + .reduce(flatten) + .filter(uniques); + + const ortb2 = new ORTB2(s2sBidRequest, bidRequests, validAdUnits, requestedBidders); + const request = ortb2.buildRequest(); + const requestJson = request && JSON.stringify(request); + logInfo('BidRequest: ' + requestJson); + const endpointUrl = getMatchingConsentUrl(s2sBidRequest.s2sConfig.endpoint, gdprConsent); + if (request && requestJson && endpointUrl) { + ajax( + endpointUrl, + { + success: function (response) { + let result; + try { + result = JSON.parse(response); + const bids = ortb2.interpretResponse(result); + bids.forEach(onBid); + } catch (error) { + logError(error); + } + if (!result || (result.status && includes(result.status, 'Error'))) { + logError('error parsing response: ', result ? result.status : 'not valid JSON'); + onResponse(false, requestedBidders); + } else { + onResponse(true, requestedBidders); + } + }, + error: onError + }, + requestJson, + {contentType: 'text/plain', withCredentials: true} + ); + } else { + logError('PBS request not made. Check endpoints.'); + } +}, 'processPBSRequest'); + /** * Global setter that sets eids permissions for bidders * This setter is to be used by userId module when included diff --git a/modules/prebidmanagerAnalyticsAdapter.js b/modules/prebidmanagerAnalyticsAdapter.js index a1a0a636e3c..1ac7ba84916 100644 --- a/modules/prebidmanagerAnalyticsAdapter.js +++ b/modules/prebidmanagerAnalyticsAdapter.js @@ -7,7 +7,7 @@ import { getStorageManager } from '../src/storageManager.js'; /** * prebidmanagerAnalyticsAdapter.js - analytics adapter for prebidmanager */ -export const storage = getStorageManager(undefined, 'prebidmanager'); +export const storage = getStorageManager({gvlid: undefined, moduleName: 'prebidmanager'}); const DEFAULT_EVENT_URL = 'https://endpoint.prebidmanager.com/endpoint' const analyticsType = 'endpoint'; const analyticsName = 'Prebid Manager Analytics: '; diff --git a/modules/priceFloors.js b/modules/priceFloors.js index b55638c1a5c..ff4213f1330 100644 --- a/modules/priceFloors.js +++ b/modules/priceFloors.js @@ -1,13 +1,30 @@ -import { parseUrl, deepAccess, parseGPTSingleSizeArray, getGptSlotInfoForAdUnitCode, deepSetValue, logWarn, deepClone, getParameterByName, generateUUID, logError, logInfo, isNumber, pick, debugTurnedOn } from '../src/utils.js'; -import { getGlobal } from '../src/prebidGlobal.js'; -import { config } from '../src/config.js'; -import { ajaxBuilder } from '../src/ajax.js'; -import events from '../src/events.js'; +import { + debugTurnedOn, + deepAccess, + deepClone, + deepSetValue, + generateUUID, + getGptSlotInfoForAdUnitCode, + getParameterByName, + isNumber, + logError, + logInfo, + logWarn, + parseGPTSingleSizeArray, + parseUrl, + pick +} from '../src/utils.js'; +import {getGlobal} from '../src/prebidGlobal.js'; +import {config} from '../src/config.js'; +import {ajaxBuilder} from '../src/ajax.js'; +import * as events from '../src/events.js'; import CONSTANTS from '../src/constants.json'; -import { getHook } from '../src/hook.js'; -import { createBid } from '../src/bidfactory.js'; -import find from 'core-js-pure/features/array/find.js'; -import { getRefererInfo } from '../src/refererDetection.js'; +import {getHook} from '../src/hook.js'; +import {createBid} from '../src/bidfactory.js'; +import {find} from '../src/polyfill.js'; +import {getRefererInfo} from '../src/refererDetection.js'; +import {bidderSettings} from '../src/bidderSettings.js'; +import {auctionManager} from '../src/auctionManager.js'; /** * @summary This Module is intended to provide users with the ability to dynamically set and enforce price floors on a per auction basis. @@ -65,9 +82,14 @@ function getHostNameFromReferer(referer) { } // First look into bidRequest! -function getGptSlotFromBidRequest(bidRequest) { - const isGam = deepAccess(bidRequest, 'ortb2Imp.ext.data.adserver.name') === 'gam'; - return isGam && bidRequest.ortb2Imp.ext.data.adserver.adslot; +function getGptSlotFromAdUnit(transactionId, {index = auctionManager.index} = {}) { + const adUnit = index.getAdUnit({transactionId}); + const isGam = deepAccess(adUnit, 'ortb2Imp.ext.data.adserver.name') === 'gam'; + return isGam && adUnit.ortb2Imp.ext.data.adserver.adslot; +} + +function getAdUnitCode(request, response, {index = auctionManager.index} = {}) { + return request?.adUnitCode || index.getAdUnit(response).code; } /** @@ -76,9 +98,9 @@ function getGptSlotFromBidRequest(bidRequest) { export let fieldMatchingFunctions = { 'size': (bidRequest, bidResponse) => parseGPTSingleSizeArray(bidResponse.size) || '*', 'mediaType': (bidRequest, bidResponse) => bidResponse.mediaType || 'banner', - 'gptSlot': (bidRequest, bidResponse) => getGptSlotFromBidRequest(bidRequest) || getGptSlotInfoForAdUnitCode(bidRequest.adUnitCode).gptSlot, + 'gptSlot': (bidRequest, bidResponse) => getGptSlotFromAdUnit((bidRequest || bidResponse).transactionId) || getGptSlotInfoForAdUnitCode(getAdUnitCode(bidRequest, bidResponse)).gptSlot, 'domain': (bidRequest, bidResponse) => referrerHostname || getHostNameFromReferer(getRefererInfo().referer), - 'adUnitCode': (bidRequest, bidResponse) => bidRequest.adUnitCode + 'adUnitCode': (bidRequest, bidResponse) => getAdUnitCode(bidRequest, bidResponse) } /** @@ -116,7 +138,7 @@ export function getFirstMatchingFloor(floorData, bidObject, responseObject = {}) let matchingData = { floorMin: floorData.floorMin || 0, - floorRuleValue: floorData.values[matchingRule] || floorData.default, + floorRuleValue: isNaN(floorData.values[matchingRule]) ? floorData.default : floorData.values[matchingRule], matchingData: allPossibleMatches[0], // the first possible match is an "exact" so contains all data relevant for anlaytics adapters matchingRule }; @@ -147,7 +169,7 @@ function generatePossibleEnumerations(arrayOfFields, delimiter) { * @summary If a the input bidder has a registered cpmadjustment it returns the input CPM after being adjusted */ export function getBiddersCpmAdjustment(bidderName, inputCpm, bid = {}) { - const adjustmentFunction = deepAccess(getGlobal(), `bidderSettings.${bidderName}.bidCpmAdjustment`) || deepAccess(getGlobal(), 'bidderSettings.standard.bidCpmAdjustment'); + const adjustmentFunction = bidderSettings.get(bidderName, 'bidCpmAdjustment'); if (adjustmentFunction) { return parseFloat(adjustmentFunction(inputCpm, {...bid, cpm: inputCpm})); } @@ -664,15 +686,16 @@ function shouldFloorBid(floorData, floorInfo, bid) { * And if the rule we find determines a bid should be floored we will do so. */ export function addBidResponseHook(fn, adUnitCode, bid) { - let floorData = _floorDataForAuction[this.bidderRequest.auctionId]; - // if no floor data or associated bidRequest then bail - const matchingBidRequest = find(this.bidderRequest.bids, bidRequest => bidRequest.bidId && bidRequest.bidId === bid.requestId); - if (!floorData || !bid || floorData.skipped || !matchingBidRequest) { + let floorData = _floorDataForAuction[bid.auctionId]; + // if no floor data then bail + if (!floorData || !bid || floorData.skipped) { return fn.call(this, adUnitCode, bid); } + const matchingBidRequest = auctionManager.index.getBidRequest(bid) + // get the matching rule - let floorInfo = getFirstMatchingFloor(floorData.data, {...matchingBidRequest}, {...bid, size: [bid.width, bid.height]}); + 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); @@ -706,7 +729,7 @@ export function addBidResponseHook(fn, adUnitCode, bid) { if (shouldFloorBid(floorData, floorInfo, bid)) { // bid fails floor -> throw it out // create basic bid no-bid with necessary data fro analytics adapters - let flooredBid = createBid(CONSTANTS.STATUS.NO_BID, matchingBidRequest); + let flooredBid = createBid(CONSTANTS.STATUS.NO_BID, bid.getIdentifiers()); Object.assign(flooredBid, pick(bid, [ 'floorData', 'width', diff --git a/modules/pubCommonId.js b/modules/pubCommonId.js index 6ecf0723aae..faca59cce1c 100644 --- a/modules/pubCommonId.js +++ b/modules/pubCommonId.js @@ -5,7 +5,7 @@ */ import { logMessage, parseUrl, buildUrl, triggerPixel, generateUUID, isArray } from '../src/utils.js'; import { config } from '../src/config.js'; -import events from '../src/events.js'; +import * as events from '../src/events.js'; import CONSTANTS from '../src/constants.json'; import { getStorageManager } from '../src/storageManager.js'; diff --git a/modules/publinkIdSystem.js b/modules/publinkIdSystem.js index 990227e7cfe..9d5645a38cb 100644 --- a/modules/publinkIdSystem.js +++ b/modules/publinkIdSystem.js @@ -16,7 +16,7 @@ const GVLID = 24; const PUBLINK_COOKIE = '_publink'; const PUBLINK_S2S_COOKIE = '_publink_srv'; -export const storage = getStorageManager(GVLID); +export const storage = getStorageManager({gvlid: GVLID}); function isHex(s) { return /^[A-F0-9]+$/i.test(s); diff --git a/modules/pubmaticBidAdapter.js b/modules/pubmaticBidAdapter.js index 366a0326054..0667ac0fc74 100644 --- a/modules/pubmaticBidAdapter.js +++ b/modules/pubmaticBidAdapter.js @@ -12,6 +12,7 @@ const USER_SYNC_URL_IMAGE = 'https://image8.pubmatic.com/AdServer/ImgSync?p='; const DEFAULT_CURRENCY = 'USD'; const AUCTION_TYPE = 1; const GROUPM_ALIAS = {code: 'groupm', gvlid: 98}; +const MARKETPLACE_PARTNERS = ['groupm'] const UNDEFINED = undefined; const DEFAULT_WIDTH = 0; const DEFAULT_HEIGHT = 0; @@ -1065,6 +1066,12 @@ export const spec = { * @return ServerRequest Info describing the request to the server. */ buildRequests: (validBidRequests, bidderRequest) => { + if (bidderRequest && MARKETPLACE_PARTNERS.includes(bidderRequest.bidderCode)) { + // We have got the buildRequests function call for Marketplace Partners + logInfo('For all publishers using ' + bidderRequest.bidderCode + ' bidder, the PubMatic bidder will also be enabled so PubMatic server will respond back with the bids that needs to be submitted for PubMatic and ' + bidderRequest.bidderCode + ' in the network call sent by PubMatic bidder. Hence we do not want to create a network call for ' + bidderRequest.bidderCode + '. This way we are trying to save a network call from browser.'); + return; + } + var refererInfo; if (bidderRequest && bidderRequest.refererInfo) { refererInfo = bidderRequest.refererInfo; @@ -1080,7 +1087,7 @@ export const spec = { bid = deepClone(originalBid); bid.params.adSlot = bid.params.adSlot || ''; _parseAdSlot(bid); - if (bid.params.hasOwnProperty('video')) { + if ((bid.mediaTypes && bid.mediaTypes.hasOwnProperty('video')) || bid.params.hasOwnProperty('video')) { // Nothing to do } else { // If we have a native mediaType configured alongside banner, its ok if the banner size is not set in width and height @@ -1287,6 +1294,13 @@ export const spec = { }; } + // if from the server-response the bid.ext.marketplace is set then + // submit the bid to Prebid as marketplace name + if (bid.ext && !!bid.ext.marketplace && MARKETPLACE_PARTNERS.includes(bid.ext.marketplace)) { + newBid.bidderCode = bid.ext.marketplace; + newBid.bidder = bid.ext.marketplace; + } + bidResponses.push(newBid); }); }); diff --git a/modules/pubxaiAnalyticsAdapter.js b/modules/pubxaiAnalyticsAdapter.js index 0031ff5539b..669bd062206 100644 --- a/modules/pubxaiAnalyticsAdapter.js +++ b/modules/pubxaiAnalyticsAdapter.js @@ -142,13 +142,14 @@ function send(data, status) { let location = getWindowLocation(); const storage = getStorage(); data.initOptions = initOptions; + data.pageDetail = {}; + Object.assign(data.pageDetail, { + host: location.host, + path: location.pathname, + search: location.search + }); if (typeof data !== 'undefined' && typeof data.auctionInit !== 'undefined') { - Object.assign(data.pageDetail, { - host: location.host, - path: location.pathname, - search: location.search, - adUnitCount: data.auctionInit.adUnitCodes ? data.auctionInit.adUnitCodes.length : null - }); + data.pageDetail.adUnitCount = data.auctionInit.adUnitCodes ? data.auctionInit.adUnitCodes.length : null; data.initOptions.auctionId = data.auctionInit.auctionId; delete data.auctionInit; diff --git a/modules/quantcastBidAdapter.js b/modules/quantcastBidAdapter.js index e168339426d..449c7d12d6f 100644 --- a/modules/quantcastBidAdapter.js +++ b/modules/quantcastBidAdapter.js @@ -1,9 +1,9 @@ -import { deepAccess, logInfo, logError, isEmpty, isArray } from '../src/utils.js'; -import { ajax } from '../src/ajax.js'; -import { config } from '../src/config.js'; -import { getStorageManager } from '../src/storageManager.js'; -import { registerBidder } from '../src/adapters/bidderFactory.js'; -import find from 'core-js-pure/features/array/find.js'; +import {deepAccess, isArray, isEmpty, logError, logInfo} from '../src/utils.js'; +import {ajax} from '../src/ajax.js'; +import {config} from '../src/config.js'; +import {getStorageManager} from '../src/storageManager.js'; +import {registerBidder} from '../src/adapters/bidderFactory.js'; +import {find} from '../src/polyfill.js'; const BIDDER_CODE = 'quantcast'; const DEFAULT_BID_FLOOR = 0.0000000001; @@ -21,7 +21,7 @@ export const QUANTCAST_PROTOCOL = 'https'; export const QUANTCAST_PORT = '8443'; export const QUANTCAST_FPA = '__qca'; -export const storage = getStorageManager(QUANTCAST_VENDOR_ID, BIDDER_CODE); +export const storage = getStorageManager({gvlid: QUANTCAST_VENDOR_ID, bidderCode: BIDDER_CODE}); function makeVideoImp(bid) { const videoInMediaType = deepAccess(bid, 'mediaTypes.video') || {}; diff --git a/modules/readpeakBidAdapter.js b/modules/readpeakBidAdapter.js index 31e430d79f9..099e1fb6332 100644 --- a/modules/readpeakBidAdapter.js +++ b/modules/readpeakBidAdapter.js @@ -1,7 +1,7 @@ import { logError, replaceAuctionPrice, parseUrl } from '../src/utils.js'; import { registerBidder } from '../src/adapters/bidderFactory.js'; import { config } from '../src/config.js'; -import { NATIVE } from '../src/mediaTypes.js'; +import { NATIVE, BANNER } from '../src/mediaTypes.js'; export const ENDPOINT = 'https://app.readpeak.com/header/prebid'; @@ -19,10 +19,9 @@ const BIDDER_CODE = 'readpeak'; export const spec = { code: BIDDER_CODE, - supportedMediaTypes: [NATIVE], + supportedMediaTypes: [NATIVE, BANNER], - isBidRequestValid: bid => - !!(bid && bid.params && bid.params.publisherId && bid.nativeParams), + isBidRequestValid: bid => !!(bid && bid.params && bid.params.publisherId), buildRequests: (bidRequests, bidderRequest) => { const currencyObj = config.getConfig('currency'); @@ -31,8 +30,7 @@ export const spec = { const request = { id: bidRequests[0].bidderRequestId, imp: bidRequests - .map(slot => impression(slot)) - .filter(imp => imp.native != null), + .map(slot => impression(slot)), site: site(bidRequests, bidderRequest), app: app(bidRequests), device: device(), @@ -96,10 +94,16 @@ function bidResponseAvailable(bidRequest, bidResponse) { creativeId: idToBidMap[id].crid, ttl: 300, netRevenue: true, - mediaType: NATIVE, - currency: bidResponse.cur, - native: nativeResponse(idToImpMap[id], idToBidMap[id]) + mediaType: idToImpMap[id].native ? NATIVE : BANNER, + currency: bidResponse.cur }; + if (idToImpMap[id].native) { + bid.native = nativeResponse(idToImpMap[id], idToBidMap[id]); + } else if (idToImpMap[id].banner) { + bid.ad = idToBidMap[id].adm + bid.width = idToBidMap[id].w + bid.height = idToBidMap[id].h + } if (idToBidMap[id].adomain) { bid.meta = { advertiserDomains: idToBidMap[id].adomain @@ -121,13 +125,19 @@ function impression(slot) { }); bidFloorFromModule = floorInfo.currency === 'USD' ? floorInfo.floor : undefined; } - return { + const imp = { id: slot.bidId, - native: nativeImpression(slot), bidfloor: bidFloorFromModule || slot.params.bidfloor || 0, bidfloorcur: (bidFloorFromModule && 'USD') || slot.params.bidfloorcur || 'USD', tagId: slot.params.tagId || '0' }; + + if (slot.mediaTypes.native) { + imp.native = nativeImpression(slot); + } else if (slot.mediaTypes.banner) { + imp.banner = bannerImpression(slot); + } + return imp } function nativeImpression(slot) { @@ -218,6 +228,15 @@ function dataAsset(id, params, type, defaultLen) { : null; } +function bannerImpression(slot) { + var sizes = slot.mediaTypes.banner.sizes || slot.sizes; + return { + format: sizes.map((s) => ({ w: s[0], h: s[1] })), + w: sizes[0][0], + h: sizes[0][1], + } +} + function site(bidRequests, bidderRequest) { const url = config.getConfig('pageUrl') || diff --git a/modules/readpeakBidAdapter.md b/modules/readpeakBidAdapter.md index da250e7f77a..8f8e7369ea5 100644 --- a/modules/readpeakBidAdapter.md +++ b/modules/readpeakBidAdapter.md @@ -15,17 +15,48 @@ Please reach out to your account team or hello@readpeak.com for more information # Test Parameters ```javascript - var adUnits = [{ - code: '/19968336/prebid_native_example_2', - mediaTypes: { native: { type: 'image' } }, - bids: [{ - bidder: 'readpeak', - params: { - bidfloor: 5.00, - publisherId: 'test', - siteId: 'test', - tagId: 'test-tag-1' + var adUnits = [ + { + code: '/19968336/prebid_native_example_2', + mediaTypes: { + native: { + title: { + required: true + }, + image: { + required: true + }, + body: { + required: true + }, + } }, - }] - }]; + bids: [{ + bidder: 'readpeak', + params: { + bidfloor: 5.00, + publisherId: 'test', + siteId: 'test', + tagId: 'test-tag-1' + }, + }] + }, + { + code: '/19968336/prebid_banner_example_2', + mediaTypes: { + banner: { + sizes: [[640, 320], [300, 600]], + } + }, + bids: [{ + bidder: 'readpeak', + params: { + bidfloor: 5.00, + publisherId: 'test', + siteId: 'test', + tagId: 'test-tag-2' + }, + }] + } + ]; ``` diff --git a/modules/reconciliationRtdProvider.js b/modules/reconciliationRtdProvider.js index fc5f0ab621a..9b6a3d7aca3 100644 --- a/modules/reconciliationRtdProvider.js +++ b/modules/reconciliationRtdProvider.js @@ -16,10 +16,10 @@ * @property {?boolean} allowAccess */ -import { submodule } from '../src/hook.js'; -import { ajaxBuilder } from '../src/ajax.js'; -import { isGptPubadsDefined, timestamp, generateUUID, logError } from '../src/utils.js'; -import find from 'core-js-pure/features/array/find.js'; +import {submodule} from '../src/hook.js'; +import {ajaxBuilder} from '../src/ajax.js'; +import {generateUUID, isGptPubadsDefined, logError, timestamp} from '../src/utils.js'; +import {find} from '../src/polyfill.js'; /** @type {Object} */ const MessageType = { diff --git a/modules/relaidoBidAdapter.js b/modules/relaidoBidAdapter.js index 128fd72996b..db381555ef9 100644 --- a/modules/relaidoBidAdapter.js +++ b/modules/relaidoBidAdapter.js @@ -10,7 +10,7 @@ const ADAPTER_VERSION = '1.0.7'; const DEFAULT_TTL = 300; const UUID_KEY = 'relaido_uuid'; -const storage = getStorageManager(); +const storage = getStorageManager({bidderCode: BIDDER_CODE}); function isBidRequestValid(bid) { if (!deepAccess(bid, 'params.placementId')) { @@ -121,9 +121,8 @@ function interpretResponse(serverResponse, bidRequest) { return []; } - const playerUrl = bidRequest.player || body.playerUrl; - for (const res of body.ads) { + const playerUrl = res.playerUrl || bidRequest.player || body.playerUrl; let bidResponse = { requestId: res.bidId, width: res.width, @@ -131,6 +130,7 @@ function interpretResponse(serverResponse, bidRequest) { cpm: res.price, currency: res.currency, creativeId: res.creativeId, + playerUrl: playerUrl, dealId: body.dealId || '', ttl: body.ttl || DEFAULT_TTL, netRevenue: true, diff --git a/modules/rhythmoneBidAdapter.js b/modules/rhythmoneBidAdapter.js index d0e399ab7e3..9e378f2d2ed 100644 --- a/modules/rhythmoneBidAdapter.js +++ b/modules/rhythmoneBidAdapter.js @@ -8,6 +8,7 @@ import { BANNER, VIDEO } from '../src/mediaTypes.js'; function RhythmOneBidAdapter() { this.code = 'rhythmone'; this.supportedMediaTypes = [VIDEO, BANNER]; + this.gvlid = 36; let SUPPORTED_VIDEO_PROTOCOLS = [2, 3, 5, 6]; let SUPPORTED_VIDEO_MIMES = ['video/mp4']; diff --git a/modules/richaudienceBidAdapter.js b/modules/richaudienceBidAdapter.js index 0f62e988249..b49d7c5584c 100755 --- a/modules/richaudienceBidAdapter.js +++ b/modules/richaudienceBidAdapter.js @@ -59,10 +59,15 @@ export const spec = { REFERER = (typeof bidderRequest.refererInfo.referer != 'undefined' ? encodeURIComponent(bidderRequest.refererInfo.referer) : null) payload.gdpr_consent = ''; - payload.gdpr = bidderRequest.gdprConsent.gdprApplies; + payload.gdpr = false; if (bidderRequest && bidderRequest.gdprConsent) { - payload.gdpr_consent = bidderRequest.gdprConsent.consentString; + if (typeof bidderRequest.gdprConsent.gdprApplies != 'undefined') { + payload.gdpr = bidderRequest.gdprConsent.gdprApplies; + } + if (typeof bidderRequest.gdprConsent.consentString != 'undefined') { + payload.gdpr_consent = bidderRequest.gdprConsent.consentString; + } } var payloadString = JSON.stringify(payload); @@ -192,6 +197,13 @@ function raiGetSizes(bid) { function raiGetDemandType(bid) { let raiFormat = 'display'; + if (typeof bid.sizes != 'undefined') { + bid.sizes.forEach(function (sz) { + if ((sz[0] == '1800' && sz[1] == '1000') || (sz[0] == '1' && sz[1] == '1')) { + raiFormat = 'skin' + } + }) + } if (bid.mediaTypes != undefined) { if (bid.mediaTypes.video != undefined) { raiFormat = 'video'; @@ -295,7 +307,7 @@ function raiGetFloor(bid, config) { raiFloor = bid.params.bidfloor; } else if (typeof bid.getFloor == 'function') { let floorSpec = bid.getFloor({ - currency: config.getConfig('currency.adServerCurrency'), + currency: config.getConfig('floors.data.currency') != null ? config.getConfig('floors.data.currency') : 'USD', mediaType: typeof bid.mediaTypes['banner'] == 'object' ? 'banner' : 'video', size: '*' }) diff --git a/modules/riseBidAdapter.js b/modules/riseBidAdapter.js index 70777fe03f2..a8ea023d46a 100644 --- a/modules/riseBidAdapter.js +++ b/modules/riseBidAdapter.js @@ -1,17 +1,17 @@ -import { logWarn, isArray, isFn, deepAccess, isEmpty, contains, timestamp, getBidIdParameter } from '../src/utils.js'; +import { logWarn, logInfo, isArray, isFn, deepAccess, isEmpty, contains, timestamp, getBidIdParameter, triggerPixel, isInteger } from '../src/utils.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; -import {VIDEO} from '../src/mediaTypes.js'; +import {BANNER, VIDEO} from '../src/mediaTypes.js'; import {config} from '../src/config.js'; -const SUPPORTED_AD_TYPES = [VIDEO]; +const SUPPORTED_AD_TYPES = [BANNER, VIDEO]; const BIDDER_CODE = 'rise'; -const ADAPTER_VERSION = '5.0.0'; +const ADAPTER_VERSION = '6.0.0'; const TTL = 360; const CURRENCY = 'USD'; const SELLER_ENDPOINT = 'https://hb.yellowblue.io/'; const MODES = { - PRODUCTION: 'hb', - TEST: 'hb-test' + PRODUCTION: 'hb-multi', + TEST: 'hb-multi-test' } const SUPPORTED_SYNC_METHODS = { IFRAME: 'iframe', @@ -23,7 +23,7 @@ export const spec = { gvlid: 1043, version: ADAPTER_VERSION, supportedMediaTypes: SUPPORTED_AD_TYPES, - isBidRequestValid: function(bidRequest) { + isBidRequestValid: function (bidRequest) { if (!bidRequest.params) { logWarn('no params have been set to Rise adapter'); return false; @@ -36,54 +36,70 @@ export const spec = { return true; }, - buildRequests: function (bidRequests, bidderRequest) { - if (bidRequests.length === 0) { - return []; - } + buildRequests: function (validBidRequests, bidderRequest) { + const combinedRequestsObject = {}; - const requests = []; + // use data from the first bid, to create the general params for all bids + const generalObject = validBidRequests[0]; + const testMode = generalObject.params.testMode; - bidRequests.forEach(bid => { - requests.push(buildVideoRequest(bid, bidderRequest)); - }); + combinedRequestsObject.params = generateGeneralParams(generalObject, bidderRequest); + combinedRequestsObject.bids = generateBidsParams(validBidRequests, bidderRequest); - return requests; + return { + method: 'POST', + url: getEndpoint(testMode), + data: combinedRequestsObject + } }, - interpretResponse: function({body}) { + interpretResponse: function ({body}) { const bidResponses = []; - const bidResponse = { - requestId: body.requestId, - cpm: body.cpm, - width: body.width, - height: body.height, - creativeId: body.requestId, - currency: body.currency, - netRevenue: body.netRevenue, - ttl: body.ttl || TTL, - vastXml: body.vastXml, - mediaType: VIDEO - }; - - if (body.adomain && body.adomain.length) { - bidResponse.meta = {}; - bidResponse.meta.advertiserDomains = body.adomain + if (body.bids) { + body.bids.forEach(adUnit => { + const bidResponse = { + requestId: adUnit.requestId, + cpm: adUnit.cpm, + currency: adUnit.currency || CURRENCY, + width: adUnit.width, + height: adUnit.height, + ttl: adUnit.ttl || TTL, + creativeId: adUnit.requestId, + netRevenue: adUnit.netRevenue || true, + nurl: adUnit.nurl, + mediaType: adUnit.mediaType, + meta: { + mediaType: adUnit.mediaType + } + }; + + if (adUnit.mediaType === VIDEO) { + bidResponse.vastXml = adUnit.vastXml; + } else if (adUnit.mediaType === BANNER) { + bidResponse.ad = adUnit.ad; + } + + if (adUnit.adomain && adUnit.adomain.length) { + bidResponse.meta.advertiserDomains = adUnit.adomain; + } + + bidResponses.push(bidResponse); + }); } - bidResponses.push(bidResponse); return bidResponses; }, - getUserSyncs: function(syncOptions, serverResponses) { + getUserSyncs: function (syncOptions, serverResponses) { const syncs = []; for (const response of serverResponses) { - if (syncOptions.iframeEnabled && response.body.userSyncURL) { + if (syncOptions.iframeEnabled && response.body.params.userSyncURL) { syncs.push({ type: 'iframe', - url: response.body.userSyncURL + url: response.body.params.userSyncURL }); } - if (syncOptions.pixelEnabled && isArray(response.body.userSyncPixels)) { - const pixels = response.body.userSyncPixels.map(pixel => { + if (syncOptions.pixelEnabled && isArray(response.body.params.userSyncPixels)) { + const pixels = response.body.params.userSyncPixels.map(pixel => { return { type: 'image', url: pixel @@ -93,6 +109,16 @@ export const spec = { } } return syncs; + }, + onBidWon: function (bid) { + if (bid == null) { + return; + } + + logInfo('onBidWon:', bid); + if (bid.hasOwnProperty('nurl') && bid.nurl.length > 0) { + triggerPixel(bid.nurl); + } } }; @@ -103,46 +129,33 @@ registerBidder(spec); * @param bid {bid} * @returns {Number} */ -function getFloor(bid) { +function getFloor(bid, mediaType) { if (!isFn(bid.getFloor)) { return 0; } let floorResult = bid.getFloor({ currency: CURRENCY, - mediaType: VIDEO, + mediaType: mediaType, size: '*' }); return floorResult.currency === CURRENCY && floorResult.floor ? floorResult.floor : 0; } /** - * Build the video request - * @param bid {bid} - * @param bidderRequest {bidderRequest} - * @returns {Object} - */ -function buildVideoRequest(bid, bidderRequest) { - const sellerParams = generateParameters(bid, bidderRequest); - const {params} = bid; - return { - method: 'GET', - url: getEndpoint(params.testMode), - data: sellerParams - }; -} - -/** - * Get the the ad size from the bid + * Get the the ad sizes array from the bid * @param bid {bid} * @returns {Array} */ -function getSizes(bid) { - if (deepAccess(bid, 'mediaTypes.video.sizes')) { - return bid.mediaTypes.video.sizes[0]; +function getSizesArray(bid, mediaType) { + let sizesArray = [] + + if (deepAccess(bid, `mediaTypes.${mediaType}.sizes`)) { + sizesArray = bid.mediaTypes[mediaType].sizes; } else if (Array.isArray(bid.sizes) && bid.sizes.length > 0) { - return bid.sizes[0]; + sizesArray = bid.sizes; } - return []; + + return sizesArray; } /** @@ -239,122 +252,180 @@ function getDeviceType(ua) { return '1'; } +function generateBidsParams(validBidRequests, bidderRequest) { + const bidsArray = []; + + if (validBidRequests.length) { + validBidRequests.forEach(bid => { + bidsArray.push(generateBidParameters(bid, bidderRequest)); + }); + } + + return bidsArray; +} + /** - * Generate query parameters for the request - * @param bid {bid} - * @param bidderRequest {bidderRequest} - * @returns {Object} + * Generate bid specific parameters + * @param {bid} bid + * @param {bidderRequest} bidderRequest + * @returns {Object} bid specific params object */ -function generateParameters(bid, bidderRequest) { +function generateBidParameters(bid, bidderRequest) { const {params} = bid; - const timeout = config.getConfig('bidderTimeout'); - const {syncEnabled, filterSettings} = config.getConfig('userSync') || {}; - const [width, height] = getSizes(bid); - const {bidderCode} = bidderRequest; - const domain = window.location.hostname; + const mediaType = isBanner(bid) ? BANNER : VIDEO; + const sizesArray = getSizesArray(bid, mediaType); // fix floor price in case of NAN if (isNaN(params.floorPrice)) { params.floorPrice = 0; } - const requestParams = { + const bidObject = { + mediaType, + adUnitCode: getBidIdParameter('adUnitCode', bid), + sizes: sizesArray, + floorPrice: Math.max(getFloor(bid, mediaType), params.floorPrice), + bidId: getBidIdParameter('bidId', bid), + bidderRequestId: getBidIdParameter('bidderRequestId', bid), + transactionId: getBidIdParameter('transactionId', bid), + }; + + const pos = deepAccess(bid, `mediaTypes.${mediaType}.pos`); + if (pos) { + bidObject.pos = pos; + } + + const gpid = deepAccess(bid, `ortb2Imp.ext.gpid`); + if (gpid) { + bidObject.gpid = gpid; + } + + const placementId = params.placementId || deepAccess(bid, `mediaTypes.${mediaType}.name`); + if (placementId) { + bidObject.placementId = placementId; + } + + if (mediaType === VIDEO) { + const playbackMethod = deepAccess(bid, `mediaTypes.video.playbackmethod`); + let playbackMethodValue; + + // verify playbackMethod is of type integer array, or integer only. + if (Array.isArray(playbackMethod) && isInteger(playbackMethod[0])) { + // only the first playbackMethod in the array will be used, according to OpenRTB 2.5 recommendation + playbackMethodValue = playbackMethod[0]; + } else if (isInteger(playbackMethod)) { + playbackMethodValue = playbackMethod; + } + + if (playbackMethodValue) { + bidObject.playbackMethod = playbackMethodValue; + } + + const placement = deepAccess(bid, `mediaTypes.video.placement`); + if (placement) { + bidObject.placement = placement; + } + + const minDuration = deepAccess(bid, `mediaTypes.video.minduration`); + if (minDuration) { + bidObject.minDuration = minDuration; + } + + const maxDuration = deepAccess(bid, `mediaTypes.video.maxduration`); + if (maxDuration) { + bidObject.maxDuration = maxDuration; + } + + const skip = deepAccess(bid, `mediaTypes.video.skip`); + if (skip) { + bidObject.skip = skip; + } + + const linearity = deepAccess(bid, `mediaTypes.video.linearity`); + if (linearity) { + bidObject.linearity = linearity; + } + } + + return bidObject; +} + +function isBanner(bid) { + return bid.mediaTypes && bid.mediaTypes.banner; +} + +/** + * Generate params that are common between all bids + * @param {single bid object} generalObject + * @param {bidderRequest} bidderRequest + * @returns {object} the common params object + */ +function generateGeneralParams(generalObject, bidderRequest) { + const domain = window.location.hostname; + const {syncEnabled, filterSettings} = config.getConfig('userSync') || {}; + const {bidderCode} = bidderRequest; + const generalBidParams = generalObject.params; + const timeout = config.getConfig('bidderTimeout'); + + // these params are snake_case instead of camelCase to allow backwards compatability on the server. + // in the future, these will be converted to camelCase to match our convention. + const generalParams = { wrapper_type: 'prebidjs', wrapper_vendor: '$$PREBID_GLOBAL$$', wrapper_version: '$prebid.version$', adapter_version: ADAPTER_VERSION, auction_start: timestamp(), - ad_unit_code: getBidIdParameter('adUnitCode', bid), - tmax: timeout, - width: width, - height: height, - publisher_id: params.org, - floor_price: Math.max(getFloor(bid), params.floorPrice), - ua: navigator.userAgent, - bid_id: getBidIdParameter('bidId', bid), - bidder_request_id: getBidIdParameter('bidderRequestId', bid), - transaction_id: getBidIdParameter('transactionId', bid), - session_id: getBidIdParameter('auctionId', bid), + publisher_id: generalBidParams.org, publisher_name: domain, site_domain: domain, dnt: (navigator.doNotTrack == 'yes' || navigator.doNotTrack == '1' || navigator.msDoNotTrack == '1') ? 1 : 0, - device_type: getDeviceType(navigator.userAgent) - }; + device_type: getDeviceType(navigator.userAgent), + ua: navigator.userAgent, + session_id: getBidIdParameter('auctionId', generalObject), + tmax: timeout + } - const userIdsParam = getBidIdParameter('userId', bid); + const userIdsParam = getBidIdParameter('userId', generalObject); if (userIdsParam) { - requestParams.userIds = JSON.stringify(userIdsParam); + generalParams.userIds = JSON.stringify(userIdsParam); } const ortb2Metadata = config.getConfig('ortb2') || {}; if (ortb2Metadata.site) { - requestParams.site_metadata = JSON.stringify(ortb2Metadata.site); + generalParams.site_metadata = JSON.stringify(ortb2Metadata.site); } if (ortb2Metadata.user) { - requestParams.user_metadata = JSON.stringify(ortb2Metadata.user); - } - - const playbackMethod = deepAccess(bid, 'mediaTypes.video.playbackmethod'); - if (playbackMethod) { - requestParams.playback_method = playbackMethod; - } - const placement = deepAccess(bid, 'mediaTypes.video.placement'); - if (placement) { - requestParams.placement = placement; - } - const pos = deepAccess(bid, 'mediaTypes.video.pos'); - if (pos) { - requestParams.pos = pos; - } - const minduration = deepAccess(bid, 'mediaTypes.video.minduration'); - if (minduration) { - requestParams.min_duration = minduration; - } - const maxduration = deepAccess(bid, 'mediaTypes.video.maxduration'); - if (maxduration) { - requestParams.max_duration = maxduration; - } - const skip = deepAccess(bid, 'mediaTypes.video.skip'); - if (skip) { - requestParams.skip = skip; - } - const linearity = deepAccess(bid, 'mediaTypes.video.linearity'); - if (linearity) { - requestParams.linearity = linearity; - } - - if (params.placementId) { - requestParams.placement_id = params.placementId; + generalParams.user_metadata = JSON.stringify(ortb2Metadata.user); } if (syncEnabled) { const allowedSyncMethod = getAllowedSyncMethod(filterSettings, bidderCode); if (allowedSyncMethod) { - requestParams.cs_method = allowedSyncMethod; + generalParams.cs_method = allowedSyncMethod; } } if (bidderRequest.uspConsent) { - requestParams.us_privacy = bidderRequest.uspConsent; + generalParams.us_privacy = bidderRequest.uspConsent; } if (bidderRequest && bidderRequest.gdprConsent && bidderRequest.gdprConsent.gdprApplies) { - requestParams.gdpr = bidderRequest.gdprConsent.gdprApplies; - requestParams.gdpr_consent = bidderRequest.gdprConsent.consentString; + generalParams.gdpr = bidderRequest.gdprConsent.gdprApplies; + generalParams.gdpr_consent = bidderRequest.gdprConsent.consentString; } - if (params.ifa) { - requestParams.ifa = params.ifa; + if (generalBidParams.ifa) { + generalParams.ifa = generalBidParams.ifa; } - if (bid.schain) { - requestParams.schain = getSupplyChain(bid.schain); + if (generalObject.schain) { + generalParams.schain = getSupplyChain(generalObject.schain); } if (bidderRequest && bidderRequest.refererInfo) { - requestParams.referrer = deepAccess(bidderRequest, 'refererInfo.referer'); - requestParams.page_url = config.getConfig('pageUrl') || deepAccess(window, 'location.href'); + generalParams.referrer = deepAccess(bidderRequest, 'refererInfo.referer'); + generalParams.page_url = config.getConfig('pageUrl') || deepAccess(window, 'location.href'); } - return requestParams; + return generalParams } diff --git a/modules/roxotAnalyticsAdapter.js b/modules/roxotAnalyticsAdapter.js index c9245d4ae08..b11898b9ea8 100644 --- a/modules/roxotAnalyticsAdapter.js +++ b/modules/roxotAnalyticsAdapter.js @@ -1,10 +1,10 @@ -import { deepClone, getParameterByName, logInfo, logError } from '../src/utils.js'; +import {deepClone, getParameterByName, logError, logInfo} from '../src/utils.js'; import adapter from '../src/AnalyticsAdapter.js'; import CONSTANTS from '../src/constants.json'; import adapterManager from '../src/adapterManager.js'; -import includes from 'core-js-pure/features/array/includes.js'; +import {includes} from '../src/polyfill.js'; import {ajaxBuilder} from '../src/ajax.js'; -import { getStorageManager } from '../src/storageManager.js'; +import {getStorageManager} from '../src/storageManager.js'; const storage = getStorageManager(); diff --git a/modules/rtbhouseBidAdapter.js b/modules/rtbhouseBidAdapter.js index 40389b1e1e0..b8436179a30 100644 --- a/modules/rtbhouseBidAdapter.js +++ b/modules/rtbhouseBidAdapter.js @@ -1,7 +1,7 @@ -import { isArray, deepAccess, getOrigin, logError } from '../src/utils.js'; -import { BANNER, NATIVE } from '../src/mediaTypes.js'; -import { registerBidder } from '../src/adapters/bidderFactory.js'; -import includes from 'core-js-pure/features/array/includes.js'; +import {deepAccess, getOrigin, isArray, logError} from '../src/utils.js'; +import {BANNER, NATIVE} from '../src/mediaTypes.js'; +import {registerBidder} from '../src/adapters/bidderFactory.js'; +import {includes} from '../src/polyfill.js'; const BIDDER_CODE = 'rtbhouse'; const REGIONS = ['prebid-eu', 'prebid-us', 'prebid-asia']; @@ -9,6 +9,7 @@ const ENDPOINT_URL = 'creativecdn.com/bidder/prebid/bids'; 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; +const GVLID = 16; // Codes defined by OpenRTB Native Ads 1.1 specification export const OPENRTB = { @@ -36,6 +37,7 @@ export const OPENRTB = { export const spec = { code: BIDDER_CODE, supportedMediaTypes: SUPPORTED_MEDIA_TYPES, + gvlid: GVLID, isBidRequestValid: function (bid) { return !!(includes(REGIONS, bid.params.region) && bid.params.publisherId); diff --git a/modules/rtdModule/index.js b/modules/rtdModule/index.js index 63c6a8d580d..381059c68f7 100644 --- a/modules/rtdModule/index.js +++ b/modules/rtdModule/index.js @@ -36,6 +36,7 @@ * @param {string[]} adUnitsCodes * @param {SubmoduleConfig} config * @param {UserConsentData} userConsent + * @param {auction} auction */ /** @@ -152,11 +153,11 @@ import {config} from '../../src/config.js'; import {module} from '../../src/hook.js'; -import {logError, logWarn} from '../../src/utils.js'; -import events from '../../src/events.js'; +import {logError, logInfo, logWarn} from '../../src/utils.js'; +import * as events from '../../src/events.js'; import CONSTANTS from '../../src/constants.json'; import {gdprDataHandler, uspDataHandler} from '../../src/adapterManager.js'; -import find from 'core-js-pure/features/array/find.js'; +import {find} from '../../src/polyfill.js'; import {getGlobal} from '../../src/prebidGlobal.js'; /** @type {string} */ @@ -256,6 +257,7 @@ function initSubModules() { } }); subModules = subModulesByOrder; + logInfo(`Real time data module enabled, using submodules: ${subModules.map((m) => m.name).join(', ')}`); } /** @@ -290,18 +292,12 @@ export function setBidRequestsData(fn, reqBidsConfigObj) { return exitHook(); } - if (shouldDelayAuction) { - waitTimeout = setTimeout(exitHook, _moduleConfig.auctionDelay); - } + waitTimeout = setTimeout(exitHook, shouldDelayAuction ? _moduleConfig.auctionDelay : 0); relevantSubModules.forEach(sm => { sm.getBidRequestData(reqBidsConfigObj, onGetBidRequestDataCallback.bind(sm), sm.config, _userConsent) }); - if (!shouldDelayAuction) { - return exitHook(); - } - function onGetBidRequestDataCallback() { if (isDone) { return; @@ -309,12 +305,15 @@ export function setBidRequestsData(fn, reqBidsConfigObj) { if (this.config && this.config.waitForIt) { callbacksExpected--; } - if (callbacksExpected <= 0) { - return exitHook(); + if (callbacksExpected === 0) { + setTimeout(exitHook, 0); } } function exitHook() { + if (isDone) { + return; + } isDone = true; clearTimeout(waitTimeout); fn.call(this, reqBidsConfigObj); @@ -341,7 +340,7 @@ export function getAdUnitTargeting(auction) { } let targeting = []; for (let i = relevantSubModules.length - 1; i >= 0; i--) { - const smTargeting = relevantSubModules[i].getTargetingData(adUnitCodes, relevantSubModules[i].config, _userConsent); + const smTargeting = relevantSubModules[i].getTargetingData(adUnitCodes, relevantSubModules[i].config, _userConsent, auction); if (smTargeting && typeof smTargeting === 'object') { targeting.push(smTargeting); } else { @@ -355,6 +354,7 @@ export function getAdUnitTargeting(auction) { if (!kv) { return } + logInfo('RTD set ad unit targeting of', kv, 'for', adUnit); adUnit[CONSTANTS.JSON_MAPPING.ADSERVER_TARGETING] = Object.assign(adUnit[CONSTANTS.JSON_MAPPING.ADSERVER_TARGETING] || {}, kv); }); return auction.adUnits; diff --git a/modules/rubiconAnalyticsAdapter.js b/modules/rubiconAnalyticsAdapter.js index 5b17048cbd3..69335ff33a8 100644 --- a/modules/rubiconAnalyticsAdapter.js +++ b/modules/rubiconAnalyticsAdapter.js @@ -1,4 +1,4 @@ -import { generateUUID, mergeDeep, deepAccess, parseUrl, logError, pick, isEmpty, logWarn, debugTurnedOn, parseQS, getWindowLocation, isAdUnitCodeMatchingSlot, isNumber, isGptPubadsDefined, _each, deepSetValue } from '../src/utils.js'; +import { generateUUID, mergeDeep, deepAccess, parseUrl, logError, pick, isEmpty, logWarn, debugTurnedOn, parseQS, getWindowLocation, isAdUnitCodeMatchingSlot, isNumber, isGptPubadsDefined, _each, deepSetValue, deepClone, logInfo } from '../src/utils.js'; import adapter from '../src/AnalyticsAdapter.js'; import adapterManager from '../src/adapterManager.js'; import CONSTANTS from '../src/constants.json'; @@ -8,10 +8,11 @@ import { getGlobal } from '../src/prebidGlobal.js'; import { getStorageManager } from '../src/storageManager.js'; const RUBICON_GVL_ID = 52; -export const storage = getStorageManager(RUBICON_GVL_ID, 'rubicon'); +export const storage = getStorageManager({gvlid: RUBICON_GVL_ID, moduleName: 'rubicon'}); const COOKIE_NAME = 'rpaSession'; const LAST_SEEN_EXPIRE_TIME = 1800000; // 30 mins const END_EXPIRE_TIME = 21600000; // 6 hours +const MODULE_NAME = 'Rubicon Analytics'; const pbsErrorMap = { 1: 'timeout-error', @@ -31,7 +32,8 @@ const { BIDDER_DONE, BID_TIMEOUT, BID_WON, - SET_TARGETING + SET_TARGETING, + BILLABLE_EVENT }, STATUS: { GOOD, @@ -55,13 +57,19 @@ const cache = { targeting: {}, timeouts: {}, gpt: {}, + billing: {} }; const BID_REJECTED_IPF = 'rejected-ipf'; export let rubiConf = { pvid: generateUUID().slice(0, 8), - analyticsEventDelay: 0 + analyticsEventDelay: 0, + dmBilling: { + enabled: false, + vendors: [], + waitForAuction: true + } }; // 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 @@ -76,7 +84,7 @@ export function getHostNameFromReferer(referer) { try { rubiconAdapter.referrerHostname = parseUrl(referer, { noDecodeWholeURL: true }).hostname; } catch (e) { - logError('Rubicon Analytics: Unable to parse hostname from supplied url: ', referer, e); + logError(`${MODULE_NAME}: Unable to parse hostname from supplied url: `, referer, e); rubiconAdapter.referrerHostname = ''; } return rubiconAdapter.referrerHostname @@ -115,6 +123,55 @@ function formatSource(src) { return src.toLowerCase(); } +function getBillingPayload(event) { + // for now we are mapping all events to type "general", later we will expand support for specific types + let billingEvent = deepClone(event); + billingEvent.type = 'general'; + billingEvent.accountId = accountId; + // mark as sent + deepSetValue(cache.billing, `${event.vendor}.${event.billingId}`, true); + return billingEvent; +} + +function sendBillingEvent(event) { + let message = getBasicEventDetails(undefined, 'soloBilling'); + message.billableEvents = [getBillingPayload(event)]; + ajax( + rubiconAdapter.getUrl(), + null, + JSON.stringify(message), + { + contentType: 'application/json' + } + ); +} + +function getBasicEventDetails(auctionId, trigger) { + let auctionCache = cache.auctions[auctionId]; + let referrer = config.getConfig('pageUrl') || pageReferer || (auctionCache && auctionCache.referrer); + let message = { + timestamps: { + prebidLoaded: rubiconAdapter.MODULE_INITIALIZED_TIME, + auctionEnded: auctionCache ? auctionCache.endTs : undefined, + eventTime: Date.now() + }, + trigger, + integration: rubiConf.int_type || DEFAULT_INTEGRATION, + version: '$prebid.version$', + referrerUri: referrer, + referrerHostname: rubiconAdapter.referrerHostname || getHostNameFromReferer(referrer), + channel: 'web', + }; + if (rubiConf.wrapperName) { + message.wrapper = { + name: rubiConf.wrapperName, + family: rubiConf.wrapperFamily, + rule: rubiConf.rule_name + } + } + return message; +} + function sendMessage(auctionId, bidWonId, trigger) { function formatBid(bid) { return pick(bid, [ @@ -160,28 +217,8 @@ function sendMessage(auctionId, bidWonId, trigger) { samplingFactor }); } + let message = getBasicEventDetails(auctionId, trigger); let auctionCache = cache.auctions[auctionId]; - let referrer = config.getConfig('pageUrl') || (auctionCache && auctionCache.referrer); - let message = { - timestamps: { - prebidLoaded: rubiconAdapter.MODULE_INITIALIZED_TIME, - auctionEnded: auctionCache.endTs, - eventTime: Date.now() - }, - trigger, - integration: rubiConf.int_type || DEFAULT_INTEGRATION, - version: '$prebid.version$', - referrerUri: referrer, - referrerHostname: rubiconAdapter.referrerHostname || getHostNameFromReferer(referrer), - channel: 'web', - }; - if (rubiConf.wrapperName) { - message.wrapper = { - name: rubiConf.wrapperName, - family: rubiConf.wrapperFamily, - rule: rubiConf.rule_name - } - } if (auctionCache && !auctionCache.sent) { let adUnitMap = Object.keys(auctionCache.bids).reduce((adUnits, bidId) => { let bid = auctionCache.bids[bidId]; @@ -195,6 +232,7 @@ function sendMessage(auctionId, bidWonId, trigger) { 'adserverTargeting', () => !isEmpty(cache.targeting[bid.adUnit.adUnitCode]) ? stringProperties(cache.targeting[bid.adUnit.adUnitCode]) : undefined, 'gam', gam => !isEmpty(gam) ? gam : undefined, 'pbAdSlot', + 'gpid', 'pattern' ]); adUnit.bids = []; @@ -234,6 +272,9 @@ function sendMessage(auctionId, bidWonId, trigger) { let auction = { clientTimeoutMillis: auctionCache.timeout, + auctionStart: auctionCache.timestamp, + auctionEnd: auctionCache.endTs, + bidderOrder: auctionCache.bidderOrder, samplingFactor, accountId, adUnits: Object.keys(adUnitMap).map(i => adUnitMap[i]), @@ -318,6 +359,12 @@ function sendMessage(auctionId, bidWonId, trigger) { ]; } + // if we have not sent any billingEvents send them + const pendingBillingEvents = getPendingBillingEvents(auctionCache); + if (pendingBillingEvents && pendingBillingEvents.length) { + message.billableEvents = pendingBillingEvents; + } + ajax( this.getUrl(), null, @@ -328,6 +375,17 @@ function sendMessage(auctionId, bidWonId, trigger) { ); } +function getPendingBillingEvents(auctionCache) { + if (auctionCache && auctionCache.billing && auctionCache.billing.length) { + return auctionCache.billing.reduce((accum, billingEvent) => { + if (deepAccess(cache.billing, `${billingEvent.vendor}.${billingEvent.billingId}`) === false) { + accum.push(getBillingPayload(billingEvent)); + } + return accum; + }, []); + } +} + function adUnitIsOnlyInstream(adUnit) { return adUnit.mediaTypes && Object.keys(adUnit.mediaTypes).length === 1 && deepAccess(adUnit, 'mediaTypes.video.context') === 'instream'; } @@ -356,7 +414,7 @@ function getBidPrice(bid) { try { return Number(prebidGlobal.convertCurrency(cpm, currency, 'USD')); } catch (err) { - logWarn('Rubicon Analytics Adapter: Could not determine the bidPriceUSD of the bid ', bid); + logWarn(`${MODULE_NAME}: Could not determine the bidPriceUSD of the bid `, bid); } } @@ -384,8 +442,9 @@ export function parseBidResponse(bid, previousBidResponse, auctionFloorData) { 'floorRuleValue', () => deepAccess(bid, 'floorData.floorRuleValue'), 'floorRule', () => debugTurnedOn() ? deepAccess(bid, 'floorData.floorRule') : undefined, 'adomains', () => { - let adomains = deepAccess(bid, 'meta.advertiserDomains'); - return Array.isArray(adomains) && adomains.length > 0 ? adomains.slice(0, 10) : undefined + const adomains = deepAccess(bid, 'meta.advertiserDomains'); + const validAdomains = Array.isArray(adomains) && adomains.filter(domain => typeof domain === 'string'); + return validAdomains && validAdomains.length > 0 ? validAdomains.slice(0, 10) : undefined } ]); } @@ -445,7 +504,7 @@ function getRpaCookie() { try { return JSON.parse(window.atob(encodedCookie)); } catch (e) { - logError(`Rubicon Analytics: Unable to decode ${COOKIE_NAME} value: `, e); + logError(`${MODULE_NAME}: Unable to decode ${COOKIE_NAME} value: `, e); } } return {}; @@ -455,7 +514,7 @@ function setRpaCookie(decodedCookie) { try { storage.setDataInLocalStorage(COOKIE_NAME, window.btoa(JSON.stringify(decodedCookie))); } catch (e) { - logError(`Rubicon Analytics: Unable to encode ${COOKIE_NAME} value: `, e); + logError(`${MODULE_NAME}: Unable to encode ${COOKIE_NAME} value: `, e); } } @@ -487,14 +546,20 @@ function subscribeToGamSlots() { window.googletag.pubads().addEventListener('slotRenderEnded', event => { const isMatchingAdSlot = isAdUnitCodeMatchingSlot(event.slot); // loop through auctions and adUnits and mark the info - Object.keys(cache.auctions).forEach(auctionId => { + // only mark first auction which finds a match + let hasMatch = false; + Object.keys(cache.auctions).find(auctionId => { (Object.keys(cache.auctions[auctionId].bids) || []).forEach(bidId => { let bid = cache.auctions[auctionId].bids[bidId]; // if this slot matches this bids adUnit, add the adUnit info - if (isMatchingAdSlot(bid.adUnit.adUnitCode)) { + // only mark it if it already has not been marked + if (!bid.adUnit.gamRendered && isMatchingAdSlot(bid.adUnit.adUnitCode)) { // mark this adUnit as having been rendered by gam cache.auctions[auctionId].gamHasRendered[bid.adUnit.adUnitCode] = true; + // this current auction has an adunit that matched the slot, so mark it as matched so next auciton is skipped + hasMatch = true; + bid.adUnit.gam = pick(event, [ // these come in as `null` from Gpt, which when stringified does not get removed // so set explicitly to undefined when not a number @@ -504,6 +569,9 @@ function subscribeToGamSlots() { 'adSlot', () => event.slot.getAdUnitPath(), 'isSlotEmpty', () => event.isEmpty || undefined ]); + + // this lets us know next iteration not to check this bids adunit + bid.adUnit.gamRendered = true; } }); // Now if all adUnits have gam rendered, send the payload @@ -516,10 +584,35 @@ function subscribeToGamSlots() { sendMessage.call(rubiconAdapter, auctionId, undefined, 'gam') } } + return hasMatch; }); }); } +let pageReferer; + +const isBillingEventValid = event => { + // vendor is whitelisted + const isWhitelistedVendor = rubiConf.dmBilling.vendors.includes(event.vendor); + // event is not duplicated + const isNotDuplicate = typeof deepAccess(cache.billing, `${event.vendor}.${event.billingId}`) !== 'boolean'; + // billingId is defined and a string + return typeof event.billingId === 'string' && isWhitelistedVendor && isNotDuplicate; +} + +const sendOrAddEventToQueue = event => { + // if any auction is not sent yet, then add it to the auction queue + const pendingAuction = Object.keys(cache.auctions).find(auctionId => !cache.auctions[auctionId].sent); + + if (rubiConf.dmBilling.waitForAuction && pendingAuction) { + cache.auctions[pendingAuction].billing = cache.auctions[pendingAuction].billing || []; + cache.auctions[pendingAuction].billing.push(event); + } else { + // send it + sendBillingEvent(event); + } +} + let baseAdapter = adapter({ analyticsType: 'endpoint' }); let rubiconAdapter = Object.assign({}, baseAdapter, { MODULE_INITIALIZED_TIME: Date.now(), @@ -535,7 +628,7 @@ let rubiconAdapter = Object.assign({}, baseAdapter, { if (config.options.endpoint) { this.getUrl = () => config.options.endpoint; } else { - logError('required endpoint missing from rubicon analytics'); + logError(`${MODULE_NAME}: required endpoint missing`); error = true; } if (typeof config.options.sampling !== 'undefined') { @@ -543,7 +636,7 @@ let rubiconAdapter = Object.assign({}, baseAdapter, { } if (typeof config.options.samplingFactor !== 'undefined') { if (typeof config.options.sampling !== 'undefined') { - logWarn('Both options.samplingFactor and options.sampling enabled in rubicon analytics, defaulting to samplingFactor'); + logWarn(`${MODULE_NAME}: Both options.samplingFactor and options.sampling enabled defaulting to samplingFactor`); } samplingFactor = parseFloat(config.options.samplingFactor); config.options.sampling = 1 / samplingFactor; @@ -553,10 +646,10 @@ let rubiconAdapter = Object.assign({}, baseAdapter, { let validSamplingFactors = [1, 10, 20, 40, 100]; if (validSamplingFactors.indexOf(samplingFactor) === -1) { error = true; - logError('invalid samplingFactor for rubicon analytics: ' + samplingFactor + ', must be one of ' + validSamplingFactors.join(', ')); + logError(`${MODULE_NAME}: invalid samplingFactor ${samplingFactor} - must be one of ${validSamplingFactors.join(', ')}`); } else if (!accountId) { error = true; - logError('required accountId missing for rubicon analytics'); + logError(`${MODULE_NAME}: required accountId missing for rubicon analytics`); } if (!error) { @@ -568,6 +661,7 @@ let rubiconAdapter = Object.assign({}, baseAdapter, { accountId = undefined; rubiConf = {}; cache.gpt.registered = false; + cache.billing = {}; baseAdapter.disableAnalytics.apply(this, arguments); }, track({ eventType, args }) { @@ -582,7 +676,8 @@ let rubiconAdapter = Object.assign({}, baseAdapter, { cacheEntry.bids = {}; cacheEntry.bidsWon = {}; cacheEntry.gamHasRendered = {}; - cacheEntry.referrer = deepAccess(args, 'bidderRequests.0.refererInfo.referer'); + cacheEntry.referrer = pageReferer = deepAccess(args, 'bidderRequests.0.refererInfo.referer'); + cacheEntry.bidderOrder = []; const floorData = deepAccess(args, 'bidderRequests.0.bids.0.floorData'); if (floorData) { cacheEntry.floorData = { ...floorData }; @@ -607,6 +702,7 @@ let rubiconAdapter = Object.assign({}, baseAdapter, { } break; case BID_REQUESTED: + cache.auctions[args.auctionId].bidderOrder.push(args.bidderCode); Object.assign(cache.auctions[args.auctionId].bids, args.bids.reduce((memo, bid) => { // mark adUnits we expect bidWon events for cache.auctions[args.auctionId].bidsWon[bid.adUnitCode] = false; @@ -685,7 +781,8 @@ let rubiconAdapter = Object.assign({}, baseAdapter, { } }, 'pbAdSlot', () => deepAccess(bid, 'ortb2Imp.ext.data.pbadslot'), - 'pattern', () => deepAccess(bid, 'ortb2Imp.ext.data.aupname') + 'pattern', () => deepAccess(bid, 'ortb2Imp.ext.data.aupname'), + 'gpid', () => deepAccess(bid, 'ortb2Imp.ext.gpid') ]) ]); return memo; @@ -710,7 +807,7 @@ let rubiconAdapter = Object.assign({}, baseAdapter, { auctionEntry.floorData.enforcements = { ...args.floorData.enforcements }; } if (!bid) { - logError('Rubicon Anlytics Adapter Error: Could not find associated bid request for bid response with requestId: ', args.requestId); + logError(`${MODULE_NAME}: Could not find associated bid request for bid response with requestId: `, args.requestId); break; } bid.source = formatSource(bid.source || args.source); @@ -781,7 +878,12 @@ let rubiconAdapter = Object.assign({}, baseAdapter, { break; case AUCTION_END: // see how long it takes for the payload to come fire - cache.auctions[args.auctionId].endTs = Date.now(); + let auctionData = cache.auctions[args.auctionId]; + // if for some reason the auction did not do its normal thing, this could be undefied so bail + if (!auctionData) { + break; + } + auctionData.endTs = Date.now(); const isOnlyInstreamAuction = args.adUnits && args.adUnits.every(adUnit => adUnitIsOnlyInstream(adUnit)); // If only instream, do not wait around, just send payload @@ -808,6 +910,14 @@ let rubiconAdapter = Object.assign({}, baseAdapter, { } }); break; + case BILLABLE_EVENT: + if (rubiConf.dmBilling.enabled && isBillingEventValid(args)) { + // add to the map indicating it has not been sent yet + deepSetValue(cache.billing, `${args.vendor}.${args.billingId}`, false); + sendOrAddEventToQueue(args); + } else { + logInfo(`${MODULE_NAME}: Billing event ignored`, args); + } } } }); diff --git a/modules/rubiconBidAdapter.js b/modules/rubiconBidAdapter.js index 28e491900f8..afb95d56d69 100644 --- a/modules/rubiconBidAdapter.js +++ b/modules/rubiconBidAdapter.js @@ -1,14 +1,28 @@ -import { mergeDeep, _each, logError, deepAccess, deepSetValue, isStr, isNumber, logWarn, convertTypes, isArray, parseSizesInput, logMessage, formatQS } from '../src/utils.js'; +import { + _each, + convertTypes, + deepAccess, + deepSetValue, + formatQS, + isArray, + isNumber, + isStr, + logError, + logMessage, + logWarn, + mergeDeep, + 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 find from 'core-js-pure/features/array/find.js'; -import { Renderer } from '../src/Renderer.js'; -import { getGlobal } from '../src/prebidGlobal.js'; +import {find} from '../src/polyfill.js'; +import {Renderer} from '../src/Renderer.js'; +import {getGlobal} from '../src/prebidGlobal.js'; const DEFAULT_INTEGRATION = 'pbjs_lite'; const DEFAULT_PBS_INTEGRATION = 'pbjs'; -const DEFAULT_RENDERER_URL = 'https://video-outstream.rubiconproject.com/apex-2.0.0.js'; +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 = {}; @@ -393,6 +407,7 @@ export const spec = { .concat([ 'tk_flint', 'x_source.tid', + 'l_pb_bid_id', 'x_source.pchain', 'p_screen_res', 'rp_floor', @@ -466,6 +481,7 @@ export const spec = { 'rp_secure': '1', 'tk_flint': `${rubiConf.int_type || DEFAULT_INTEGRATION}_v$prebid.version$`, 'x_source.tid': bidRequest.transactionId, + 'l_pb_bid_id': bidRequest.bidId, 'x_source.pchain': params.pchain, 'p_screen_res': _getScreenResolution(), 'tk_user_key': params.userId, @@ -864,7 +880,7 @@ function renderBid(bid) { height: bid.height, vastUrl: bid.vastUrl, placement: { - attachTo: `#${bid.adUnitCode}`, + attachTo: adUnitElement, align: config.align || 'center', position: config.position || 'append' }, diff --git a/modules/s2sTesting.js b/modules/s2sTesting.js index 1f2bb473174..8e9628c8810 100644 --- a/modules/s2sTesting.js +++ b/modules/s2sTesting.js @@ -1,12 +1,12 @@ -import { setS2STestingModule } from '../src/adapterManager.js'; +import {PARTITIONS, partitionBidders, filterBidsForAdUnit, getS2SBidderSet} from '../src/adapterManager.js'; +import {find} from '../src/polyfill.js'; +import {getBidderCodes, logWarn} from '../src/utils.js'; -let s2sTesting = {}; - -const SERVER = 'server'; -const CLIENT = 'client'; - -s2sTesting.SERVER = SERVER; -s2sTesting.CLIENT = CLIENT; +const {CLIENT, SERVER} = PARTITIONS; +export const s2sTesting = { + ...PARTITIONS, + clientTestBidders: new Set() +}; s2sTesting.bidSource = {}; // store bidder sources determined from s2sConfig bidderControl s2sTesting.globalRand = Math.random(); // if 10% of bidderA and 10% of bidderB should be server-side, make it the same 10% @@ -40,7 +40,7 @@ s2sTesting.getSourceBidderMap = function(adUnits = [], allS2SBidders = []) { [SERVER]: Object.keys(sourceBidders[SERVER]), [CLIENT]: Object.keys(sourceBidders[CLIENT]) }; -}; +} /** * @function calculateBidSources determines the source for each s2s bidder based on bidderControl weightings. these can be overridden at the adUnit level @@ -53,7 +53,7 @@ s2sTesting.calculateBidSources = function(s2sConfig = {}) { (s2sConfig.bidders || []).forEach((bidder) => { s2sTesting.bidSource[bidder] = s2sTesting.getSource(bidderControl[bidder] && bidderControl[bidder].bidSource) || SERVER; // default to server }); -}; +} /** * @function getSource() gets a random source based on the given sourceWeights (export just for testing) @@ -76,10 +76,59 @@ s2sTesting.getSource = function(sourceWeights = {}, bidSources = [SERVER, CLIENT // choose the first source with an incremental weight > random weight if (rndWeight < srcIncWeight[source]) return source; } -}; +} + +function doingS2STesting(s2sConfig) { + return s2sConfig && s2sConfig.enabled && s2sConfig.testing; +} -// inject the s2sTesting module into the adapterManager rather than importing it -// importing it causes the packager to include it even when it's not explicitly included in the build -setS2STestingModule(s2sTesting); +function isTestingServerOnly(s2sConfig) { + return Boolean(doingS2STesting(s2sConfig) && s2sConfig.testServerOnly); +} + +const adUnitsContainServerRequests = (adUnits, s2sConfig) => Boolean( + find(adUnits, adUnit => find(adUnit.bids, bid => ( + bid.bidSource || + (s2sConfig.bidderControl && s2sConfig.bidderControl[bid.bidder]) + ) && bid.finalSource === SERVER)) +); + +partitionBidders.before(function (next, adUnits, s2sConfigs) { + const serverBidders = getS2SBidderSet(s2sConfigs); + let serverOnly = false; + + s2sConfigs.forEach((s2sConfig) => { + if (doingS2STesting(s2sConfig)) { + s2sTesting.calculateBidSources(s2sConfig); + const bidderMap = s2sTesting.getSourceBidderMap(adUnits, [...serverBidders]); + // get all adapters doing client testing + bidderMap[CLIENT].forEach((bidder) => s2sTesting.clientTestBidders.add(bidder)) + } + if (isTestingServerOnly(s2sConfig) && adUnitsContainServerRequests(adUnits, s2sConfig)) { + logWarn('testServerOnly: True. All client requests will be suppressed.'); + serverOnly = true; + } + }); + + next.bail(getBidderCodes(adUnits).reduce((memo, bidder) => { + if (serverBidders.has(bidder)) { + memo[SERVER].push(bidder); + } + if (!serverOnly && (!serverBidders.has(bidder) || s2sTesting.clientTestBidders.has(bidder))) { + memo[CLIENT].push(bidder); + } + return memo; + }, {[CLIENT]: [], [SERVER]: []})); +}); + +filterBidsForAdUnit.before(function(next, bids, s2sConfig) { + if (s2sConfig == null) { + next.bail(bids.filter((bid) => !s2sTesting.clientTestBidders.size || bid.finalSource !== SERVER)); + } else { + const serverBidders = getS2SBidderSet(s2sConfig); + next.bail(bids.filter((bid) => serverBidders.has(bid.bidder) && + (!doingS2STesting(s2sConfig) || bid.finalSource !== CLIENT))); + } +}); export default s2sTesting; diff --git a/modules/saambaaBidAdapter.js b/modules/saambaaBidAdapter.js index 2810853532d..36ab50bfddd 100644 --- a/modules/saambaaBidAdapter.js +++ b/modules/saambaaBidAdapter.js @@ -1,420 +1,419 @@ -import { deepAccess, isFn, generateUUID, parseUrl, isEmpty, parseSizesInput } from '../src/utils.js'; -import { config } from '../src/config.js'; -import { registerBidder } from '../src/adapters/bidderFactory.js'; -import { VIDEO, BANNER } from '../src/mediaTypes.js'; -import find from 'core-js-pure/features/array/find.js'; -import includes from 'core-js-pure/features/array/includes.js'; - -const ADAPTER_VERSION = '1.0'; -const BIDDER_CODE = 'saambaa'; - -export const VIDEO_ENDPOINT = 'https://nep.advangelists.com/xp/get?pubid='; -export const BANNER_ENDPOINT = 'https://nep.advangelists.com/xp/get?pubid='; -export const OUTSTREAM_SRC = 'https://player-cdn.beachfrontmedia.com/playerapi/loader/outstream.js'; -export const VIDEO_TARGETING = ['mimes', 'playbackmethod', 'maxduration', 'skip', 'playerSize', 'context']; -export const DEFAULT_MIMES = ['video/mp4', 'application/javascript']; - -let pubid = ''; - -export const spec = { - code: BIDDER_CODE, - supportedMediaTypes: [BANNER, VIDEO], - - isBidRequestValid(bidRequest) { - if (typeof bidRequest != 'undefined') { - if (bidRequest.bidder !== BIDDER_CODE && typeof bidRequest.params === 'undefined') { return false; } - if (bidRequest === '' || bidRequest.params.placement === '' || bidRequest.params.pubid === '') { return false; } - return true; - } else { return false; } - }, - - buildRequests(bids, bidderRequest) { - let requests = []; - let videoBids = bids.filter(bid => isVideoBidValid(bid)); - let bannerBids = bids.filter(bid => isBannerBidValid(bid)); - videoBids.forEach(bid => { - pubid = getVideoBidParam(bid, 'pubid'); - requests.push({ - method: 'POST', - url: VIDEO_ENDPOINT + pubid, - data: createVideoRequestData(bid, bidderRequest), - bidRequest: bid - }); - }); - - bannerBids.forEach(bid => { - pubid = getBannerBidParam(bid, 'pubid'); - - requests.push({ - method: 'POST', - url: BANNER_ENDPOINT + pubid, - data: createBannerRequestData(bid, bidderRequest), - bidRequest: bid - }); - }); - return requests; - }, - - interpretResponse(serverResponse, {bidRequest}) { - let response = serverResponse.body; - if (response !== null && isEmpty(response) == false) { - if (isVideoBid(bidRequest)) { - let bidResponse = { - requestId: response.id, - bidderCode: BIDDER_CODE, - cpm: response.seatbid[0].bid[0].price, - width: response.seatbid[0].bid[0].w, - height: response.seatbid[0].bid[0].h, - ttl: response.seatbid[0].bid[0].ttl || 60, - creativeId: response.seatbid[0].bid[0].crid, - currency: response.cur, - meta: { 'advertiserDomains': response.seatbid[0].bid[0].adomain }, - mediaType: VIDEO, - netRevenue: true - } - - if (response.seatbid[0].bid[0].adm) { - bidResponse.vastXml = response.seatbid[0].bid[0].adm; - bidResponse.adResponse = { - content: response.seatbid[0].bid[0].adm - }; - } else { - bidResponse.vastUrl = response.seatbid[0].bid[0].nurl; - } - - return bidResponse; - } else { - return { - requestId: response.id, - bidderCode: BIDDER_CODE, - cpm: response.seatbid[0].bid[0].price, - width: response.seatbid[0].bid[0].w, - height: response.seatbid[0].bid[0].h, - ad: response.seatbid[0].bid[0].adm, - ttl: response.seatbid[0].bid[0].ttl || 60, - creativeId: response.seatbid[0].bid[0].crid, - currency: response.cur, - meta: { 'advertiserDomains': response.seatbid[0].bid[0].adomain }, - mediaType: BANNER, - netRevenue: true - } - } - } - } -}; - -function isBannerBid(bid) { - return deepAccess(bid, 'mediaTypes.banner') || !isVideoBid(bid); -} - -function isVideoBid(bid) { - return deepAccess(bid, 'mediaTypes.video'); -} - -function getBannerBidFloor(bid) { - let floorInfo = isFn(bid.getFloor) ? bid.getFloor({ currency: 'USD', mediaType: 'banner', size: '*' }) : {}; - return floorInfo.floor || getBannerBidParam(bid, 'bidfloor'); -} - -function getVideoBidFloor(bid) { - let floorInfo = isFn(bid.getFloor) ? bid.getFloor({ currency: 'USD', mediaType: 'video', size: '*' }) : {}; - return floorInfo.floor || getVideoBidParam(bid, 'bidfloor'); -} - -function isVideoBidValid(bid) { - return isVideoBid(bid) && getVideoBidParam(bid, 'pubid') && getVideoBidParam(bid, 'placement'); -} - -function isBannerBidValid(bid) { - return isBannerBid(bid) && getBannerBidParam(bid, 'pubid') && getBannerBidParam(bid, 'placement'); -} - -function getVideoBidParam(bid, key) { - return deepAccess(bid, 'params.video.' + key) || deepAccess(bid, 'params.' + key); -} - -function getBannerBidParam(bid, key) { - return deepAccess(bid, 'params.banner.' + key) || deepAccess(bid, 'params.' + key); -} - -function isMobile() { - return (/(ios|ipod|ipad|iphone|android)/i).test(navigator.userAgent); -} - -function isConnectedTV() { - return (/(smart[-]?tv|hbbtv|appletv|googletv|hdmi|netcast\.tv|viera|nettv|roku|\bdtv\b|sonydtv|inettvbrowser|\btv\b)/i).test(navigator.userAgent); -} - -function getDoNotTrack() { - return navigator.doNotTrack === '1' || window.doNotTrack === '1' || navigator.msDoNoTrack === '1' || navigator.doNotTrack === 'yes'; -} - -function findAndFillParam(o, key, value) { - try { - if (typeof value === 'function') { - o[key] = value(); - } else { - o[key] = value; - } - } catch (ex) {} -} - -function getOsVersion() { - let clientStrings = [ - { s: 'Android', r: /Android/ }, - { s: 'iOS', r: /(iPhone|iPad|iPod)/ }, - { s: 'Mac OS X', r: /Mac OS X/ }, - { s: 'Mac OS', r: /(MacPPC|MacIntel|Mac_PowerPC|Macintosh)/ }, - { s: 'Linux', r: /(Linux|X11)/ }, - { s: 'Windows 10', r: /(Windows 10.0|Windows NT 10.0)/ }, - { s: 'Windows 8.1', r: /(Windows 8.1|Windows NT 6.3)/ }, - { s: 'Windows 8', r: /(Windows 8|Windows NT 6.2)/ }, - { s: 'Windows 7', r: /(Windows 7|Windows NT 6.1)/ }, - { s: 'Windows Vista', r: /Windows NT 6.0/ }, - { s: 'Windows Server 2003', r: /Windows NT 5.2/ }, - { s: 'Windows XP', r: /(Windows NT 5.1|Windows XP)/ }, - { s: 'UNIX', r: /UNIX/ }, - { s: 'Search Bot', r: /(nuhk|Googlebot|Yammybot|Openbot|Slurp|MSNBot|Ask Jeeves\/Teoma|ia_archiver)/ } - ]; - let cs = find(clientStrings, cs => cs.r.test(navigator.userAgent)); - return cs ? cs.s : 'unknown'; -} - -function getFirstSize(sizes) { - return (sizes && sizes.length) ? sizes[0] : { w: undefined, h: undefined }; -} - -function parseSizes(sizes) { - return parseSizesInput(sizes).map(size => { - let [ width, height ] = size.split('x'); - return { - w: parseInt(width, 10) || undefined, - h: parseInt(height, 10) || undefined - }; - }); -} - -function getVideoSizes(bid) { - return parseSizes(deepAccess(bid, 'mediaTypes.video.playerSize') || bid.sizes); -} - -function getBannerSizes(bid) { - return parseSizes(deepAccess(bid, 'mediaTypes.banner.sizes') || bid.sizes); -} - -function getTopWindowReferrer() { - try { - return window.top.document.referrer; - } catch (e) { - return ''; - } -} - -function getVideoTargetingParams(bid) { - const result = {}; - const excludeProps = ['playerSize', 'context', 'w', 'h']; - Object.keys(Object(bid.mediaTypes.video)) - .filter(key => !includes(excludeProps, key)) - .forEach(key => { - result[ key ] = bid.mediaTypes.video[ key ]; - }); - Object.keys(Object(bid.params.video)) - .filter(key => includes(VIDEO_TARGETING, key)) - .forEach(key => { - result[ key ] = bid.params.video[ key ]; - }); - return result; -} - -function createVideoRequestData(bid, bidderRequest) { - let topLocation = getTopWindowLocation(bidderRequest); - let topReferrer = getTopWindowReferrer(); - - // if size is explicitly given via adapter params - let paramSize = getVideoBidParam(bid, 'size'); - let sizes = []; - let coppa = config.getConfig('coppa'); - - if (typeof paramSize !== 'undefined' && paramSize != '') { - sizes = parseSizes(paramSize); - } else { - sizes = getVideoSizes(bid); - } - const firstSize = getFirstSize(sizes); - let floor = (getVideoBidFloor(bid) == null || typeof getVideoBidFloor(bid) == 'undefined') ? 0.5 : getVideoBidFloor(bid); - let video = getVideoTargetingParams(bid); - const o = { - 'device': { - 'langauge': (global.navigator.language).split('-')[0], - 'dnt': (global.navigator.doNotTrack === 1 ? 1 : 0), - 'devicetype': isMobile() ? 4 : isConnectedTV() ? 3 : 2, - 'js': 1, - 'os': getOsVersion() - }, - 'at': 2, - 'site': {}, - 'tmax': 3000, - 'cur': ['USD'], - 'id': bid.bidId, - 'imp': [], - 'regs': { - 'ext': { - } - }, - 'user': { - 'ext': { - } - } - }; - - o.site['page'] = topLocation.href; - o.site['domain'] = topLocation.hostname; - o.site['search'] = topLocation.search; - o.site['domain'] = topLocation.hostname; - o.site['ref'] = topReferrer; - o.site['mobile'] = isMobile() ? 1 : 0; - const secure = topLocation.protocol.indexOf('https') === 0 ? 1 : 0; - - o.device['dnt'] = getDoNotTrack() ? 1 : 0; - - findAndFillParam(o.site, 'name', function() { - return global.top.document.title; - }); - - findAndFillParam(o.device, 'h', function() { - return global.screen.height; - }); - findAndFillParam(o.device, 'w', function() { - return global.screen.width; - }); - - let placement = getVideoBidParam(bid, 'placement'); - - for (let j = 0; j < sizes.length; j++) { - o.imp.push({ - 'id': '' + j, - 'displaymanager': '' + BIDDER_CODE, - 'displaymanagerver': '' + ADAPTER_VERSION, - 'tagId': placement, - 'bidfloor': floor, - 'bidfloorcur': 'USD', - 'secure': secure, - 'video': Object.assign({ - 'id': generateUUID(), - 'pos': 0, - 'w': firstSize.w, - 'h': firstSize.h, - 'mimes': DEFAULT_MIMES - }, video) - - }); - } - if (coppa) { - o.regs.ext = {'coppa': 1}; - } - if (bidderRequest && bidderRequest.gdprConsent) { - let { gdprApplies, consentString } = bidderRequest.gdprConsent; - o.regs.ext = {'gdpr': gdprApplies ? 1 : 0}; - o.user.ext = {'consent': consentString}; - } - - return o; -} - -function getTopWindowLocation(bidderRequest) { - let url = bidderRequest && bidderRequest.refererInfo && bidderRequest.refererInfo.referer; - return parseUrl(config.getConfig('pageUrl') || url, { decodeSearchAsString: true }); -} - -function createBannerRequestData(bid, bidderRequest) { - let topLocation = getTopWindowLocation(bidderRequest); - let topReferrer = getTopWindowReferrer(); - - // if size is explicitly given via adapter params - - let paramSize = getBannerBidParam(bid, 'size'); - let sizes = []; - let coppa = config.getConfig('coppa'); - if (typeof paramSize !== 'undefined' && paramSize != '') { - sizes = parseSizes(paramSize); - } else { - sizes = getBannerSizes(bid); - } - - let floor = (getBannerBidFloor(bid) == null || typeof getBannerBidFloor(bid) == 'undefined') ? 0.1 : getBannerBidFloor(bid); - const o = { - 'device': { - 'langauge': (global.navigator.language).split('-')[0], - 'dnt': (global.navigator.doNotTrack === 1 ? 1 : 0), - 'devicetype': isMobile() ? 4 : isConnectedTV() ? 3 : 2, - 'js': 1 - }, - 'at': 2, - 'site': {}, - 'tmax': 3000, - 'cur': ['USD'], - 'id': bid.bidId, - 'imp': [], - 'regs': { - 'ext': { - } - }, - 'user': { - 'ext': { - } - } - }; - - o.site['page'] = topLocation.href; - o.site['domain'] = topLocation.hostname; - o.site['search'] = topLocation.search; - o.site['domain'] = topLocation.hostname; - o.site['ref'] = topReferrer; - o.site['mobile'] = isMobile() ? 1 : 0; - const secure = topLocation.protocol.indexOf('https') === 0 ? 1 : 0; - - o.device['dnt'] = getDoNotTrack() ? 1 : 0; - - findAndFillParam(o.site, 'name', function() { - return global.top.document.title; - }); - - findAndFillParam(o.device, 'h', function() { - return global.screen.height; - }); - findAndFillParam(o.device, 'w', function() { - return global.screen.width; - }); - - let placement = getBannerBidParam(bid, 'placement'); - for (let j = 0; j < sizes.length; j++) { - let size = sizes[j]; - - o.imp.push({ - 'id': '' + j, - 'displaymanager': '' + BIDDER_CODE, - 'displaymanagerver': '' + ADAPTER_VERSION, - 'tagId': placement, - 'bidfloor': floor, - 'bidfloorcur': 'USD', - 'secure': secure, - 'banner': { - 'id': generateUUID(), - 'pos': 0, - 'w': size['w'], - 'h': size['h'] - } - }); - } - if (coppa) { - o.regs.ext = {'coppa': 1}; - } - if (bidderRequest && bidderRequest.gdprConsent) { - let { gdprApplies, consentString } = bidderRequest.gdprConsent; - o.regs.ext = {'gdpr': gdprApplies ? 1 : 0}; - o.user.ext = {'consent': consentString}; - } - - return o; -} -registerBidder(spec); +import {deepAccess, generateUUID, isEmpty, isFn, parseSizesInput, parseUrl} from '../src/utils.js'; +import {config} from '../src/config.js'; +import {registerBidder} from '../src/adapters/bidderFactory.js'; +import {BANNER, VIDEO} from '../src/mediaTypes.js'; +import {find, includes} from '../src/polyfill.js'; + +const ADAPTER_VERSION = '1.0'; +const BIDDER_CODE = 'saambaa'; + +export const VIDEO_ENDPOINT = 'https://nep.advangelists.com/xp/get?pubid='; +export const BANNER_ENDPOINT = 'https://nep.advangelists.com/xp/get?pubid='; +export const OUTSTREAM_SRC = 'https://player-cdn.beachfrontmedia.com/playerapi/loader/outstream.js'; +export const VIDEO_TARGETING = ['mimes', 'playbackmethod', 'maxduration', 'skip', 'playerSize', 'context']; +export const DEFAULT_MIMES = ['video/mp4', 'application/javascript']; + +let pubid = ''; + +export const spec = { + code: BIDDER_CODE, + supportedMediaTypes: [BANNER, VIDEO], + + isBidRequestValid(bidRequest) { + if (typeof bidRequest != 'undefined') { + if (bidRequest.bidder !== BIDDER_CODE && typeof bidRequest.params === 'undefined') { return false; } + if (bidRequest === '' || bidRequest.params.placement === '' || bidRequest.params.pubid === '') { return false; } + return true; + } else { return false; } + }, + + buildRequests(bids, bidderRequest) { + let requests = []; + let videoBids = bids.filter(bid => isVideoBidValid(bid)); + let bannerBids = bids.filter(bid => isBannerBidValid(bid)); + videoBids.forEach(bid => { + pubid = getVideoBidParam(bid, 'pubid'); + requests.push({ + method: 'POST', + url: VIDEO_ENDPOINT + pubid, + data: createVideoRequestData(bid, bidderRequest), + bidRequest: bid + }); + }); + + bannerBids.forEach(bid => { + pubid = getBannerBidParam(bid, 'pubid'); + + requests.push({ + method: 'POST', + url: BANNER_ENDPOINT + pubid, + data: createBannerRequestData(bid, bidderRequest), + bidRequest: bid + }); + }); + return requests; + }, + + interpretResponse(serverResponse, {bidRequest}) { + let response = serverResponse.body; + if (response !== null && isEmpty(response) == false) { + if (isVideoBid(bidRequest)) { + let bidResponse = { + requestId: response.id, + bidderCode: BIDDER_CODE, + cpm: response.seatbid[0].bid[0].price, + width: response.seatbid[0].bid[0].w, + height: response.seatbid[0].bid[0].h, + ttl: response.seatbid[0].bid[0].ttl || 60, + creativeId: response.seatbid[0].bid[0].crid, + currency: response.cur, + meta: { 'advertiserDomains': response.seatbid[0].bid[0].adomain }, + mediaType: VIDEO, + netRevenue: true + } + + if (response.seatbid[0].bid[0].adm) { + bidResponse.vastXml = response.seatbid[0].bid[0].adm; + bidResponse.adResponse = { + content: response.seatbid[0].bid[0].adm + }; + } else { + bidResponse.vastUrl = response.seatbid[0].bid[0].nurl; + } + + return bidResponse; + } else { + return { + requestId: response.id, + bidderCode: BIDDER_CODE, + cpm: response.seatbid[0].bid[0].price, + width: response.seatbid[0].bid[0].w, + height: response.seatbid[0].bid[0].h, + ad: response.seatbid[0].bid[0].adm, + ttl: response.seatbid[0].bid[0].ttl || 60, + creativeId: response.seatbid[0].bid[0].crid, + currency: response.cur, + meta: { 'advertiserDomains': response.seatbid[0].bid[0].adomain }, + mediaType: BANNER, + netRevenue: true + } + } + } + } +}; + +function isBannerBid(bid) { + return deepAccess(bid, 'mediaTypes.banner') || !isVideoBid(bid); +} + +function isVideoBid(bid) { + return deepAccess(bid, 'mediaTypes.video'); +} + +function getBannerBidFloor(bid) { + let floorInfo = isFn(bid.getFloor) ? bid.getFloor({ currency: 'USD', mediaType: 'banner', size: '*' }) : {}; + return floorInfo.floor || getBannerBidParam(bid, 'bidfloor'); +} + +function getVideoBidFloor(bid) { + let floorInfo = isFn(bid.getFloor) ? bid.getFloor({ currency: 'USD', mediaType: 'video', size: '*' }) : {}; + return floorInfo.floor || getVideoBidParam(bid, 'bidfloor'); +} + +function isVideoBidValid(bid) { + return isVideoBid(bid) && getVideoBidParam(bid, 'pubid') && getVideoBidParam(bid, 'placement'); +} + +function isBannerBidValid(bid) { + return isBannerBid(bid) && getBannerBidParam(bid, 'pubid') && getBannerBidParam(bid, 'placement'); +} + +function getVideoBidParam(bid, key) { + return deepAccess(bid, 'params.video.' + key) || deepAccess(bid, 'params.' + key); +} + +function getBannerBidParam(bid, key) { + return deepAccess(bid, 'params.banner.' + key) || deepAccess(bid, 'params.' + key); +} + +function isMobile() { + return (/(ios|ipod|ipad|iphone|android)/i).test(navigator.userAgent); +} + +function isConnectedTV() { + return (/(smart[-]?tv|hbbtv|appletv|googletv|hdmi|netcast\.tv|viera|nettv|roku|\bdtv\b|sonydtv|inettvbrowser|\btv\b)/i).test(navigator.userAgent); +} + +function getDoNotTrack() { + return navigator.doNotTrack === '1' || window.doNotTrack === '1' || navigator.msDoNoTrack === '1' || navigator.doNotTrack === 'yes'; +} + +function findAndFillParam(o, key, value) { + try { + if (typeof value === 'function') { + o[key] = value(); + } else { + o[key] = value; + } + } catch (ex) {} +} + +function getOsVersion() { + let clientStrings = [ + { s: 'Android', r: /Android/ }, + { s: 'iOS', r: /(iPhone|iPad|iPod)/ }, + { s: 'Mac OS X', r: /Mac OS X/ }, + { s: 'Mac OS', r: /(MacPPC|MacIntel|Mac_PowerPC|Macintosh)/ }, + { s: 'Linux', r: /(Linux|X11)/ }, + { s: 'Windows 10', r: /(Windows 10.0|Windows NT 10.0)/ }, + { s: 'Windows 8.1', r: /(Windows 8.1|Windows NT 6.3)/ }, + { s: 'Windows 8', r: /(Windows 8|Windows NT 6.2)/ }, + { s: 'Windows 7', r: /(Windows 7|Windows NT 6.1)/ }, + { s: 'Windows Vista', r: /Windows NT 6.0/ }, + { s: 'Windows Server 2003', r: /Windows NT 5.2/ }, + { s: 'Windows XP', r: /(Windows NT 5.1|Windows XP)/ }, + { s: 'UNIX', r: /UNIX/ }, + { s: 'Search Bot', r: /(nuhk|Googlebot|Yammybot|Openbot|Slurp|MSNBot|Ask Jeeves\/Teoma|ia_archiver)/ } + ]; + let cs = find(clientStrings, cs => cs.r.test(navigator.userAgent)); + return cs ? cs.s : 'unknown'; +} + +function getFirstSize(sizes) { + return (sizes && sizes.length) ? sizes[0] : { w: undefined, h: undefined }; +} + +function parseSizes(sizes) { + return parseSizesInput(sizes).map(size => { + let [ width, height ] = size.split('x'); + return { + w: parseInt(width, 10) || undefined, + h: parseInt(height, 10) || undefined + }; + }); +} + +function getVideoSizes(bid) { + return parseSizes(deepAccess(bid, 'mediaTypes.video.playerSize') || bid.sizes); +} + +function getBannerSizes(bid) { + return parseSizes(deepAccess(bid, 'mediaTypes.banner.sizes') || bid.sizes); +} + +function getTopWindowReferrer() { + try { + return window.top.document.referrer; + } catch (e) { + return ''; + } +} + +function getVideoTargetingParams(bid) { + const result = {}; + const excludeProps = ['playerSize', 'context', 'w', 'h']; + Object.keys(Object(bid.mediaTypes.video)) + .filter(key => !includes(excludeProps, key)) + .forEach(key => { + result[ key ] = bid.mediaTypes.video[ key ]; + }); + Object.keys(Object(bid.params.video)) + .filter(key => includes(VIDEO_TARGETING, key)) + .forEach(key => { + result[ key ] = bid.params.video[ key ]; + }); + return result; +} + +function createVideoRequestData(bid, bidderRequest) { + let topLocation = getTopWindowLocation(bidderRequest); + let topReferrer = getTopWindowReferrer(); + + // if size is explicitly given via adapter params + let paramSize = getVideoBidParam(bid, 'size'); + let sizes = []; + let coppa = config.getConfig('coppa'); + + if (typeof paramSize !== 'undefined' && paramSize != '') { + sizes = parseSizes(paramSize); + } else { + sizes = getVideoSizes(bid); + } + const firstSize = getFirstSize(sizes); + let floor = (getVideoBidFloor(bid) == null || typeof getVideoBidFloor(bid) == 'undefined') ? 0.5 : getVideoBidFloor(bid); + let video = getVideoTargetingParams(bid); + const o = { + 'device': { + 'langauge': (global.navigator.language).split('-')[0], + 'dnt': (global.navigator.doNotTrack === 1 ? 1 : 0), + 'devicetype': isMobile() ? 4 : isConnectedTV() ? 3 : 2, + 'js': 1, + 'os': getOsVersion() + }, + 'at': 2, + 'site': {}, + 'tmax': 3000, + 'cur': ['USD'], + 'id': bid.bidId, + 'imp': [], + 'regs': { + 'ext': { + } + }, + 'user': { + 'ext': { + } + } + }; + + o.site['page'] = topLocation.href; + o.site['domain'] = topLocation.hostname; + o.site['search'] = topLocation.search; + o.site['domain'] = topLocation.hostname; + o.site['ref'] = topReferrer; + o.site['mobile'] = isMobile() ? 1 : 0; + const secure = topLocation.protocol.indexOf('https') === 0 ? 1 : 0; + + o.device['dnt'] = getDoNotTrack() ? 1 : 0; + + findAndFillParam(o.site, 'name', function() { + return global.top.document.title; + }); + + findAndFillParam(o.device, 'h', function() { + return global.screen.height; + }); + findAndFillParam(o.device, 'w', function() { + return global.screen.width; + }); + + let placement = getVideoBidParam(bid, 'placement'); + + for (let j = 0; j < sizes.length; j++) { + o.imp.push({ + 'id': '' + j, + 'displaymanager': '' + BIDDER_CODE, + 'displaymanagerver': '' + ADAPTER_VERSION, + 'tagId': placement, + 'bidfloor': floor, + 'bidfloorcur': 'USD', + 'secure': secure, + 'video': Object.assign({ + 'id': generateUUID(), + 'pos': 0, + 'w': firstSize.w, + 'h': firstSize.h, + 'mimes': DEFAULT_MIMES + }, video) + + }); + } + if (coppa) { + o.regs.ext = {'coppa': 1}; + } + if (bidderRequest && bidderRequest.gdprConsent) { + let { gdprApplies, consentString } = bidderRequest.gdprConsent; + o.regs.ext = {'gdpr': gdprApplies ? 1 : 0}; + o.user.ext = {'consent': consentString}; + } + + return o; +} + +function getTopWindowLocation(bidderRequest) { + let url = bidderRequest && bidderRequest.refererInfo && bidderRequest.refererInfo.referer; + return parseUrl(config.getConfig('pageUrl') || url, { decodeSearchAsString: true }); +} + +function createBannerRequestData(bid, bidderRequest) { + let topLocation = getTopWindowLocation(bidderRequest); + let topReferrer = getTopWindowReferrer(); + + // if size is explicitly given via adapter params + + let paramSize = getBannerBidParam(bid, 'size'); + let sizes = []; + let coppa = config.getConfig('coppa'); + if (typeof paramSize !== 'undefined' && paramSize != '') { + sizes = parseSizes(paramSize); + } else { + sizes = getBannerSizes(bid); + } + + let floor = (getBannerBidFloor(bid) == null || typeof getBannerBidFloor(bid) == 'undefined') ? 0.1 : getBannerBidFloor(bid); + const o = { + 'device': { + 'langauge': (global.navigator.language).split('-')[0], + 'dnt': (global.navigator.doNotTrack === 1 ? 1 : 0), + 'devicetype': isMobile() ? 4 : isConnectedTV() ? 3 : 2, + 'js': 1 + }, + 'at': 2, + 'site': {}, + 'tmax': 3000, + 'cur': ['USD'], + 'id': bid.bidId, + 'imp': [], + 'regs': { + 'ext': { + } + }, + 'user': { + 'ext': { + } + } + }; + + o.site['page'] = topLocation.href; + o.site['domain'] = topLocation.hostname; + o.site['search'] = topLocation.search; + o.site['domain'] = topLocation.hostname; + o.site['ref'] = topReferrer; + o.site['mobile'] = isMobile() ? 1 : 0; + const secure = topLocation.protocol.indexOf('https') === 0 ? 1 : 0; + + o.device['dnt'] = getDoNotTrack() ? 1 : 0; + + findAndFillParam(o.site, 'name', function() { + return global.top.document.title; + }); + + findAndFillParam(o.device, 'h', function() { + return global.screen.height; + }); + findAndFillParam(o.device, 'w', function() { + return global.screen.width; + }); + + let placement = getBannerBidParam(bid, 'placement'); + for (let j = 0; j < sizes.length; j++) { + let size = sizes[j]; + + o.imp.push({ + 'id': '' + j, + 'displaymanager': '' + BIDDER_CODE, + 'displaymanagerver': '' + ADAPTER_VERSION, + 'tagId': placement, + 'bidfloor': floor, + 'bidfloorcur': 'USD', + 'secure': secure, + 'banner': { + 'id': generateUUID(), + 'pos': 0, + 'w': size['w'], + 'h': size['h'] + } + }); + } + if (coppa) { + o.regs.ext = {'coppa': 1}; + } + if (bidderRequest && bidderRequest.gdprConsent) { + let { gdprApplies, consentString } = bidderRequest.gdprConsent; + o.regs.ext = {'gdpr': gdprApplies ? 1 : 0}; + o.user.ext = {'consent': consentString}; + } + + return o; +} +registerBidder(spec); diff --git a/modules/seedingAllianceBidAdapter.js b/modules/seedingAllianceBidAdapter.js index 00f3b64fb44..b7aec0f8881 100755 --- a/modules/seedingAllianceBidAdapter.js +++ b/modules/seedingAllianceBidAdapter.js @@ -152,10 +152,10 @@ export const spec = { const { seatbid, cur } = serverResponse.body; - const bidResponses = flatten(seatbid.map(seat => seat.bid)).reduce((result, bid) => { + const bidResponses = (typeof seatbid != 'undefined') ? flatten(seatbid.map(seat => seat.bid)).reduce((result, bid) => { result[bid.impid - 1] = bid; return result; - }, []); + }, []) : []; return bids .map((bid, id) => { @@ -167,7 +167,7 @@ export const spec = { cpm: bidResponse.price, creativeId: bidResponse.crid, ttl: 1000, - netRevenue: bid.netRevenue === 'net', + netRevenue: (!bid.netRevenue || bid.netRevenue === 'net'), currency: cur, mediaType: NATIVE, bidderCode: BIDDER_CODE, diff --git a/modules/seedtagBidAdapter.js b/modules/seedtagBidAdapter.js index bae27d41028..2f61e0bc56a 100644 --- a/modules/seedtagBidAdapter.js +++ b/modules/seedtagBidAdapter.js @@ -154,9 +154,12 @@ export function getTimeoutUrl (data) { isArray(data[0].params) && data[0].params[0] ) { const params = data[0].params[0]; + const timeout = data[0].timeout + queryParams = '?publisherToken=' + params.publisherId + - '&adUnitId=' + params.adUnitId; + '&adUnitId=' + params.adUnitId + + '&timeout=' + timeout; } return SEEDTAG_SSP_ONTIMEOUT_ENDPOINT + queryParams; } diff --git a/modules/sharedIdSystem.js b/modules/sharedIdSystem.js index 32a96100d43..656b62815c7 100644 --- a/modules/sharedIdSystem.js +++ b/modules/sharedIdSystem.js @@ -11,7 +11,7 @@ import { coppaDataHandler } from '../src/adapterManager.js'; import {getStorageManager} from '../src/storageManager.js'; const GVLID = 887; -export const storage = getStorageManager(GVLID, 'pubCommonId'); +export const storage = getStorageManager({gvlid: GVLID, moduleName: 'pubCommonId'}); const COOKIE = 'cookie'; const LOCAL_STORAGE = 'html5'; const OPTOUT_NAME = '_pubcid_optout'; diff --git a/modules/sharethroughBidAdapter.js b/modules/sharethroughBidAdapter.js index 06cc81324cf..1dd95812e12 100644 --- a/modules/sharethroughBidAdapter.js +++ b/modules/sharethroughBidAdapter.js @@ -18,7 +18,7 @@ export const sharethroughInternal = { export const sharethroughAdapterSpec = { code: BIDDER_CODE, supportedMediaTypes: [VIDEO, BANNER], - + gvlid: 80, isBidRequestValid: bid => !!bid.params.pkey && bid.bidder === BIDDER_CODE, buildRequests: (bidRequests, bidderRequest) => { diff --git a/modules/sigmoidAnalyticsAdapter.js b/modules/sigmoidAnalyticsAdapter.js index da0ca9e38e5..a0521bd5297 100644 --- a/modules/sigmoidAnalyticsAdapter.js +++ b/modules/sigmoidAnalyticsAdapter.js @@ -1,11 +1,11 @@ /* Sigmoid Analytics Adapter for prebid.js v1.1.0-pre Updated : 2018-03-28 */ -import includes from 'core-js-pure/features/array/includes.js'; +import {includes} from '../src/polyfill.js'; import adapter from '../src/AnalyticsAdapter.js'; import CONSTANTS from '../src/constants.json'; import adapterManager from '../src/adapterManager.js'; -import { getStorageManager } from '../src/storageManager.js'; -import { generateUUID, logInfo, logError } from '../src/utils.js'; +import {getStorageManager} from '../src/storageManager.js'; +import {generateUUID, logError, logInfo} from '../src/utils.js'; const storage = getStorageManager(); diff --git a/modules/sirdataRtdProvider.js b/modules/sirdataRtdProvider.js index 344357bcb62..182ff384fef 100644 --- a/modules/sirdataRtdProvider.js +++ b/modules/sirdataRtdProvider.js @@ -7,12 +7,12 @@ * @requires module:modules/realTimeData */ import {getGlobal} from '../src/prebidGlobal.js'; -import { deepAccess, logError, deepEqual, deepSetValue, isEmpty, mergeDeep } from '../src/utils.js'; +import {deepAccess, deepEqual, deepSetValue, isEmpty, logError, mergeDeep} from '../src/utils.js'; import {submodule} from '../src/hook.js'; import {ajax} from '../src/ajax.js'; -import findIndex from 'core-js-pure/features/array/find-index.js'; -import { getRefererInfo } from '../src/refererDetection.js'; -import { config } from '../src/config.js'; +import {findIndex} from '../src/polyfill.js'; +import {getRefererInfo} from '../src/refererDetection.js'; +import {config} from '../src/config.js'; /** @type {string} */ const MODULE_NAME = 'realTimeData'; diff --git a/modules/sizeMappingV2.js b/modules/sizeMappingV2.js index 95f0eea4075..405799813eb 100644 --- a/modules/sizeMappingV2.js +++ b/modules/sizeMappingV2.js @@ -4,12 +4,19 @@ * rendering. Read full API documentation on Prebid.org, http://prebid.org/dev-docs/modules/sizeMappingV2.html */ -import { isArray, logError, isArrayOfNums, deepClone, logWarn, getWindowTop, deepEqual, logInfo, isValidMediaTypes, deepAccess, getDefinedParams, getUniqueIdentifierStr, flatten } from '../src/utils.js'; -import { processNativeAdUnitParams } from '../src/native.js'; -import { adunitCounter } from '../src/adUnits.js'; -import includes from 'core-js-pure/features/array/includes.js'; -import { getHook } from '../src/hook.js'; -import { adUnitSetupChecks } from '../src/prebid.js'; +import { + deepClone, + getWindowTop, + isArray, + isArrayOfNums, + isValidMediaTypes, + logError, + logInfo, + logWarn +} from '../src/utils.js'; +import {includes} from '../src/polyfill.js'; +import {getHook} from '../src/hook.js'; +import {adUnitSetupChecks} from '../src/prebid.js'; // Allows for stubbing of these functions while writing unit tests. export const internal = { @@ -21,61 +28,33 @@ export const internal = { isLabelActivated }; -/* - 'sizeMappingInternalStore' contains information on, whether a particular auction is using size mapping V2 (the new size mapping spec), - and it also contains additional information on each adUnit, such as, mediaTypes, activeViewport, etc. This information is required by - the 'getBids' function. -*/ - -export const sizeMappingInternalStore = createSizeMappingInternalStore(); - -function createSizeMappingInternalStore() { - const sizeMappingInternalStore = {}; - - return { - initializeStore: function (auctionId, isUsingSizeMappingBool) { - sizeMappingInternalStore[auctionId] = { - usingSizeMappingV2: isUsingSizeMappingBool, - adUnits: [] - }; - }, - getAuctionDetail: function (auctionId) { - return sizeMappingInternalStore[auctionId]; - }, - setAuctionDetail: function (auctionId, adUnitDetail) { - sizeMappingInternalStore[auctionId].adUnits.push(adUnitDetail); - } - } -} +const V2_ADUNITS = new WeakMap(); /* Returns "true" if at least one of the adUnits in the adUnits array is using an Ad Unit and/or Bidder level sizeConfig, otherwise, returns "false." */ export function isUsingNewSizeMapping(adUnits) { - let isUsingSizeMappingBool = false; - adUnits.forEach(adUnit => { + return !!adUnits.find(adUnit => { + if (V2_ADUNITS.has(adUnit)) return V2_ADUNITS.get(adUnit); if (adUnit.mediaTypes) { // checks for the presence of sizeConfig property at the adUnit.mediaTypes object - Object.keys(adUnit.mediaTypes).forEach(mediaType => { + for (let mediaType of Object.keys(adUnit.mediaTypes)) { if (adUnit.mediaTypes[mediaType].sizeConfig) { - if (isUsingSizeMappingBool === false) { - isUsingSizeMappingBool = true; - } + V2_ADUNITS.set(adUnit, true); + return true; } - }); - - // checks for the presence of sizeConfig property at the adUnit.bids[].bidder object - adUnit.bids && isArray(adUnit.bids) && adUnit.bids.forEach(bidder => { - if (bidder.sizeConfig) { - if (isUsingSizeMappingBool === false) { - isUsingSizeMappingBool = true; - } + } + for (let bid of adUnit.bids && isArray(adUnit.bids) ? adUnit.bids : []) { + if (bid.sizeConfig) { + V2_ADUNITS.set(adUnit, true); + return true; } - }); + } + V2_ADUNITS.set(adUnit, false); + return false; } }); - return isUsingSizeMappingBool; } /** @@ -168,19 +147,12 @@ export function checkAdUnitSetupHook(adUnits) { } const validatedAdUnits = []; adUnits.forEach(adUnit => { - const bids = adUnit.bids; + adUnit = adUnitSetupChecks.validateAdUnit(adUnit); + if (adUnit == null) return; + const mediaTypes = adUnit.mediaTypes; let validatedBanner, validatedVideo, validatedNative; - if (!bids || !isArray(bids)) { - logError(`Detected adUnit.code '${adUnit.code}' did not have 'adUnit.bids' defined or 'adUnit.bids' is not an array. Removing adUnit from auction.`); - return; - } - - if (!mediaTypes || Object.keys(mediaTypes).length === 0) { - logError(`Detected adUnit.code '${adUnit.code}' did not have a 'mediaTypes' object defined. This is a required field for the auction, so this adUnit has been removed.`); - return; - } if (mediaTypes.banner) { if (mediaTypes.banner.sizes) { // Ad unit is using 'mediaTypes.banner.sizes' instead of the new property 'sizeConfig'. Apply the old checks! @@ -290,23 +262,11 @@ export function checkBidderSizeConfigFormat(sizeConfig) { return didCheckPass; } -getHook('getBids').before(function (fn, bidderInfo) { - // check if the adUnit is using sizeMappingV2 specs and store the result in _sizeMappingUsageMap. - if (typeof sizeMappingInternalStore.getAuctionDetail(bidderInfo.auctionId) === 'undefined') { - const isUsingSizeMappingBool = isUsingNewSizeMapping(bidderInfo.adUnits); - - // initialize sizeMappingInternalStore for the first time for a particular auction - sizeMappingInternalStore.initializeStore(bidderInfo.auctionId, isUsingSizeMappingBool); - } - if (sizeMappingInternalStore.getAuctionDetail(bidderInfo.auctionId).usingSizeMappingV2) { - // if adUnit is found using sizeMappingV2 specs, run the getBids function which processes the sizeConfig object - // and returns the bids array for a particular bidder. - - const bids = getBids(bidderInfo); - return fn.bail(bids); +getHook('setupAdUnitMediaTypes').before(function (fn, adUnits, labels) { + if (isUsingNewSizeMapping(adUnits)) { + return fn.bail(setupAdUnitMediaTypes(adUnits, labels)); } else { - // if not using sizeMappingV2, default back to the getBids function defined in adapterManager. - return fn.call(this, bidderInfo); + return fn.call(this, adUnits, labels); } }); @@ -424,8 +384,8 @@ export function getFilteredMediaTypes(mediaTypes) { return sizeBucketToSizeMap; }, {}); - return { mediaTypes, sizeBucketToSizeMap, activeViewport, transformedMediaTypes }; -}; + return { sizeBucketToSizeMap, activeViewport, transformedMediaTypes }; +} /** * Evaluates the given sizeConfig object and checks for various properties to determine if the sizeConfig is active or not. For example, @@ -476,126 +436,87 @@ export function getActiveSizeBucket(sizeConfig, activeViewport) { } export function getRelevantMediaTypesForBidder(sizeConfig, activeViewport) { + const mediaTypes = new Set(); if (internal.checkBidderSizeConfigFormat(sizeConfig)) { const activeSizeBucket = internal.getActiveSizeBucket(sizeConfig, activeViewport); - return sizeConfig.filter(config => config.minViewPort === activeSizeBucket)[0]['relevantMediaTypes']; + sizeConfig.filter(config => config.minViewPort === activeSizeBucket)[0]['relevantMediaTypes'].forEach((mt) => mediaTypes.add(mt)); } - return []; + return mediaTypes; } -// sets sizeMappingInternalStore for a given auctionId with relevant adUnit information returned from the call to 'getFilteredMediaTypes' function -// returns adUnit details object. -export function getAdUnitDetail(auctionId, adUnit, labels) { - // fetch all adUnits for an auction from the sizeMappingInternalStore - const adUnitsForAuction = sizeMappingInternalStore.getAuctionDetail(auctionId).adUnits; - - // check if the adUnit exists already in the sizeMappingInterStore (check for equivalence of 'code' && 'mediaTypes' properties) - const adUnitDetail = adUnitsForAuction.filter(adUnitDetail => adUnitDetail.adUnitCode === adUnit.code && deepEqual(adUnitDetail.mediaTypes, adUnit.mediaTypes)); - - if (adUnitDetail.length > 0) { - adUnitDetail[0].cacheHits++; - return adUnitDetail[0]; - } else { - const identicalAdUnit = adUnitsForAuction.filter(adUnitDetail => adUnitDetail.adUnitCode === adUnit.code); - const adUnitInstance = identicalAdUnit.length > 0 && typeof identicalAdUnit[0].instance === 'number' ? identicalAdUnit[identicalAdUnit.length - 1].instance + 1 : 1; - const isLabelActivated = internal.isLabelActivated(adUnit, labels, adUnit.code, adUnitInstance); - const { mediaTypes = adUnit.mediaTypes, sizeBucketToSizeMap, activeViewport, transformedMediaTypes } = isLabelActivated && internal.getFilteredMediaTypes(adUnit.mediaTypes); - - const adUnitDetail = { - adUnitCode: adUnit.code, - mediaTypes, - sizeBucketToSizeMap, - activeViewport, - transformedMediaTypes, - instance: adUnitInstance, - isLabelActivated, - cacheHits: 0 - }; - - // set adUnitDetail in sizeMappingInternalStore against the correct 'auctionId'. - sizeMappingInternalStore.setAuctionDetail(auctionId, adUnitDetail); - isLabelActivated && logInfo(`Size Mapping V2:: Ad Unit: ${adUnit.code}(${adUnitInstance}) => Active size buckets after filtration: `, sizeBucketToSizeMap); - - return adUnitDetail; - } +export function getAdUnitDetail(adUnit, labels, adUnitInstance) { + const isLabelActivated = internal.isLabelActivated(adUnit, labels, adUnit.code, adUnitInstance); + const { sizeBucketToSizeMap, activeViewport, transformedMediaTypes } = isLabelActivated && internal.getFilteredMediaTypes(adUnit.mediaTypes); + isLabelActivated && logInfo(`Size Mapping V2:: Ad Unit: ${adUnit.code}(${adUnitInstance}) => Active size buckets after filtration: `, sizeBucketToSizeMap); + return { + activeViewport, + transformedMediaTypes, + isLabelActivated, + }; } -export function getBids({ bidderCode, auctionId, bidderRequestId, adUnits, labels, src }) { +export function setupAdUnitMediaTypes(adUnits, labels) { + const duplCounter = {}; return adUnits.reduce((result, adUnit) => { + const instance = (() => { + if (!duplCounter.hasOwnProperty(adUnit.code)) { + duplCounter[adUnit.code] = 1; + } + return duplCounter[adUnit.code]++; + })(); if (adUnit.mediaTypes && isValidMediaTypes(adUnit.mediaTypes)) { - const { activeViewport, transformedMediaTypes, instance: adUnitInstance, isLabelActivated, cacheHits } = internal.getAdUnitDetail(auctionId, adUnit, labels); + const { activeViewport, transformedMediaTypes, isLabelActivated } = internal.getAdUnitDetail(adUnit, labels, instance); if (isLabelActivated) { - // check if adUnit has any active media types remaining, if not drop the adUnit from auction, - // else proceed to evaluate the bids object. if (Object.keys(transformedMediaTypes).length === 0) { - cacheHits === 0 && logInfo(`Size Mapping V2:: Ad Unit: ${adUnit.code}(${adUnitInstance}) => Ad unit disabled since there are no active media types after sizeConfig filtration.`); - return result; - } - result - .push(adUnit.bids.filter(bid => bid.bidder === bidderCode) - .reduce((bids, bid) => { - if (internal.isLabelActivated(bid, labels, adUnit.code, adUnitInstance)) { - // handle native params - const nativeParams = adUnit.nativeParams || deepAccess(adUnit, 'mediaTypes.native'); - if (nativeParams) { - bid = Object.assign({}, bid, { - nativeParams: processNativeAdUnitParams(nativeParams) - }); - } - - bid = Object.assign({}, bid, getDefinedParams(adUnit, ['mediaType', 'renderer'])); - - if (bid.sizeConfig) { - const relevantMediaTypes = internal.getRelevantMediaTypesForBidder(bid.sizeConfig, activeViewport); - if (relevantMediaTypes.length === 0) { - logError(`Size Mapping V2:: Ad Unit: ${adUnit.code}(${adUnitInstance}), Bidder: ${bidderCode} => 'sizeConfig' is not configured properly. This bidder won't be eligible for sizeConfig checks and will remail active.`); - bid = Object.assign({}, bid); - } else if (relevantMediaTypes[0] !== 'none') { - const bidderMediaTypes = Object - .keys(transformedMediaTypes) - .filter(mt => relevantMediaTypes.indexOf(mt) > -1) - .reduce((mediaTypes, mediaType) => { - mediaTypes[mediaType] = transformedMediaTypes[mediaType]; - return mediaTypes; - }, {}); - - if (Object.keys(bidderMediaTypes).length > 0) { - bid = Object.assign({}, bid, { mediaTypes: bidderMediaTypes }); - } else { - logInfo(`Size Mapping V2:: Ad Unit: ${adUnit.code}(${adUnitInstance}), Bidder: ${bid.bidder} => 'relevantMediaTypes' does not match with any of the active mediaTypes at the Ad Unit level. This bidder is disabled.`); - return bids; + logInfo(`Size Mapping V2:: Ad Unit: ${adUnit.code}(${instance}) => Ad unit disabled since there are no active media types after sizeConfig filtration.`); + } else { + adUnit.mediaTypes = transformedMediaTypes; + adUnit.bids = adUnit.bids.reduce((bids, bid) => { + if (internal.isLabelActivated(bid, labels, adUnit.code, instance)) { + if (bid.sizeConfig) { + const relevantMediaTypes = internal.getRelevantMediaTypesForBidder(bid.sizeConfig, activeViewport); + if (relevantMediaTypes.size === 0) { + logError(`Size Mapping V2:: Ad Unit: ${adUnit.code}(${instance}), Bidder: ${bid.bidder} => 'sizeConfig' is not configured properly. This bidder won't be eligible for sizeConfig checks and will remain active.`); + bids.push(bid); + } else if (!relevantMediaTypes.has('none')) { + let modified = false; + const bidderMediaTypes = Object.fromEntries( + Object.entries(transformedMediaTypes) + .filter(([key, val]) => { + if (!relevantMediaTypes.has(key)) { + modified = true; + return false; + } + return true; + }) + ); + if (Object.keys(bidderMediaTypes).length > 0) { + if (modified) { + bid.mediaTypes = bidderMediaTypes; } + bids.push(bid); } else { - logInfo(`Size Mapping V2:: Ad Unit: ${adUnit.code}(${adUnitInstance}), Bidder: ${bid.bidder} => 'relevantMediaTypes' is set to 'none' in sizeConfig for current viewport size. This bidder is disabled.`); - return bids; + logInfo(`Size Mapping V2:: Ad Unit: ${adUnit.code}(${instance}), Bidder: ${bid.bidder} => 'relevantMediaTypes' does not match with any of the active mediaTypes at the Ad Unit level. This bidder is disabled.`); } + } else { + logInfo(`Size Mapping V2:: Ad Unit: ${adUnit.code}(${instance}), Bidder: ${bid.bidder} => 'relevantMediaTypes' is set to 'none' in sizeConfig for current viewport size. This bidder is disabled.`); } - bids.push(Object.assign({}, bid, { - adUnitCode: adUnit.code, - transactionId: adUnit.transactionId, - sizes: deepAccess(transformedMediaTypes, 'banner.sizes') || deepAccess(transformedMediaTypes, 'video.playerSize') || [], - mediaTypes: bid.mediaTypes || transformedMediaTypes, - bidId: bid.bid_id || getUniqueIdentifierStr(), - bidderRequestId, - auctionId, - src, - bidRequestsCount: adunitCounter.getRequestsCounter(adUnit.code), - bidderRequestsCount: adunitCounter.getBidderRequestsCounter(adUnit.code, bid.bidder), - bidderWinsCount: adunitCounter.getBidderWinsCounter(adUnit.code, bid.bidder) - })); - return bids; } else { - logInfo(`Size Mapping V2:: Ad Unit: ${adUnit.code}(${adUnitInstance}), Bidder: ${bid.bidder} => Label check for this bidder has failed. This bidder is disabled.`); - return bids; + bids.push(bid); } - }, [])); + } else { + logInfo(`Size Mapping V2:: Ad Unit: ${adUnit.code}(${instance}), Bidder: ${bid.bidder} => Label check for this bidder has failed. This bidder is disabled.`); + } + return bids; + }, []); + result.push(adUnit); + } } else { - cacheHits === 0 && logInfo(`Size Mapping V2:: Ad Unit: ${adUnit.code}(${adUnitInstance}) => Ad unit is disabled due to failing label check.`); + logInfo(`Size Mapping V2:: Ad Unit: ${adUnit.code}(${instance}) => Ad unit is disabled due to failing label check.`); } } else { logWarn(`Size Mapping V2:: Ad Unit: ${adUnit.code} => Ad unit has declared invalid 'mediaTypes' or has not declared a 'mediaTypes' property`); - return result; } return result; - }, []).reduce(flatten, []).filter(val => val !== ''); + }, []) } diff --git a/modules/slimcutBidAdapter.js b/modules/slimcutBidAdapter.js index c2592137fd8..2d35e09d777 100644 --- a/modules/slimcutBidAdapter.js +++ b/modules/slimcutBidAdapter.js @@ -9,7 +9,8 @@ const BIDDER_CODE = 'slimcut'; const ENDPOINT_URL = 'https://sb.freeskreen.com/pbr'; export const spec = { code: BIDDER_CODE, - aliases: ['scm'], + gvlid: 52, + aliases: [{ code: 'scm', gvlid: 52 }], supportedMediaTypes: ['video', 'banner'], /** * Determines whether or not the given bid request is valid. diff --git a/modules/smaatoBidAdapter.js b/modules/smaatoBidAdapter.js index 68334aed0ab..b792983534d 100644 --- a/modules/smaatoBidAdapter.js +++ b/modules/smaatoBidAdapter.js @@ -16,9 +16,6 @@ const buildOpenRtbBidRequest = (bidRequest, bidderRequest) => { tmax: bidderRequest.timeout, site: { id: window.location.hostname, - publisher: { - id: deepAccess(bidRequest, 'params.publisherId') - }, domain: window.location.hostname, page: window.location.href, ref: bidderRequest.refererInfo.referer @@ -51,6 +48,8 @@ const buildOpenRtbBidRequest = (bidRequest, bidderRequest) => { Object.assign(requestTemplate.user, ortb2.user); Object.assign(requestTemplate.site, ortb2.site); + deepSetValue(requestTemplate, 'site.publisher.id', deepAccess(bidRequest, 'params.publisherId')); + if (bidderRequest.gdprConsent && bidderRequest.gdprConsent.gdprApplies === true) { deepSetValue(requestTemplate, 'regs.ext.gdpr', bidderRequest.gdprConsent.gdprApplies ? 1 : 0); deepSetValue(requestTemplate, 'user.ext.consent', bidderRequest.gdprConsent.consentString); @@ -110,6 +109,7 @@ const buildServerRequest = (validBidRequest, data) => { export const spec = { code: BIDDER_CODE, supportedMediaTypes: [BANNER, VIDEO], + gvlid: 82, /** * Determines whether or not the given bid request is valid. diff --git a/modules/smarthubBidAdapter.js b/modules/smarthubBidAdapter.js new file mode 100644 index 00000000000..a94ed972b2e --- /dev/null +++ b/modules/smarthubBidAdapter.js @@ -0,0 +1,187 @@ +import {deepAccess, isFn, logError, logMessage} 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'; + +const BIDDER_CODE = 'smarthub'; + +function isBidResponseValid(bid) { + if (!bid.requestId || !bid.cpm || !bid.creativeId || !bid.ttl || !bid.currency || !bid.hasOwnProperty('netRevenue')) { + return false; + } + switch (bid.mediaType) { + case BANNER: + return Boolean(bid.width && bid.height && bid.ad); + case VIDEO: + return Boolean(bid.width && bid.height && (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 { partnerName, seat, token, iabCat, minBidfloor, pos } = params; + const bidfloor = getBidFloor(bid); + + const placement = { + partnerName: partnerName.toLowerCase(), + seat, + token, + iabCat, + minBidfloor, + pos, + bidId, + schain, + bidfloor + }; + + 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 (e) { + logError(e); + return 0; + } +} + +function buildRequestParams(bidderRequest = {}, placements = []) { + 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.referer; + 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; + return { + deviceWidth, + deviceHeight, + language, + secure, + host, + page, + placements, + coppa: config.getConfig('coppa') === true ? 1 : 0, + ccpa: bidderRequest.uspConsent || undefined, + gdpr: bidderRequest.gdprConsent || undefined, + tmax: config.getConfig('bidderTimeout') + }; +} + +export const spec = { + code: BIDDER_CODE, + supportedMediaTypes: [BANNER, VIDEO, NATIVE], + + isBidRequestValid: (bid = {}) => { + const { params, bidId, mediaTypes } = bid; + let valid = Boolean(bidId && params && params.partnerName && params.seat && 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 = {}) => { + const tempObj = {}; + + const len = validBidRequests.length; + for (let i = 0; i < len; i++) { + const bid = validBidRequests[i]; + const data = getPlacementReqData(bid); + tempObj[data.partnerName] = tempObj[data.partnerName] || []; + tempObj[data.partnerName].push(data); + } + + return Object.keys(tempObj).map(key => { + const request = buildRequestParams(bidderRequest, tempObj[key]); + return { + method: 'POST', + url: `https://${key}-prebid.smart-hub.io/pbjs`, + 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/smarthubBidAdapter.md b/modules/smarthubBidAdapter.md new file mode 100644 index 00000000000..c09855303e2 --- /dev/null +++ b/modules/smarthubBidAdapter.md @@ -0,0 +1,94 @@ +# Overview + +``` +Module Name: SmartHub Bidder Adapter +Module Type: SmartHub Bidder Adapter +Maintainer: support@smart-hub.io +``` + +# Description + +Connects to SmartHub exchange for bids. + +SmartHub 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: 'smarthub', + params: { + partnerName: 'pbjstest', + seat: 'testSeat', + token: 'testBanner', + iabCat: ['IAB1-1', 'IAB3-1', 'IAB4-3'], + minBidfloor: 10, + pos: 1, + } + } + ] + }, + { + code: 'addunit2', + mediaTypes: { + video: { + playerSize: [ [640, 480] ], + minduration: 5, + maxduration: 60, + } + }, + bids: [ + { + bidder: 'smarthub', + params: { + partnerName: 'pbjstest', + seat: 'testSeat', + token: 'testVideo', + iabCat: ['IAB1-1', 'IAB3-1', 'IAB4-3'], + minBidfloor: 10, + pos: 1, + } + } + ] + }, + { + code: 'addunit3', + mediaTypes: { + native: { + title: { + required: true + }, + body: { + required: true + }, + icon: { + required: true, + size: [64, 64] + } + } + }, + bids: [ + { + bidder: 'smarthub', + params: { + partnerName: 'pbjstest', + seat: 'testSeat', + token: 'testNative', + iabCat: ['IAB1-1', 'IAB3-1', 'IAB4-3'], + minBidfloor: 10, + pos: 1, + } + } + ] + } + ]; +``` diff --git a/modules/smarticoBidAdapter.js b/modules/smarticoBidAdapter.js index 2399a12f932..edb774f812f 100644 --- a/modules/smarticoBidAdapter.js +++ b/modules/smarticoBidAdapter.js @@ -1,6 +1,6 @@ -import { registerBidder } from '../src/adapters/bidderFactory.js'; -import { BANNER } from '../src/mediaTypes.js'; -import find from 'core-js-pure/features/array/find.js'; +import {registerBidder} from '../src/adapters/bidderFactory.js'; +import {BANNER} from '../src/mediaTypes.js'; +import {find} from '../src/polyfill.js'; const SMARTICO_CONFIG = { bidRequestUrl: 'https://trmads.eu/preBidRequest', diff --git a/modules/sortableAnalyticsAdapter.js b/modules/sortableAnalyticsAdapter.js index 76d3ca63d69..4580ce6dbb8 100644 --- a/modules/sortableAnalyticsAdapter.js +++ b/modules/sortableAnalyticsAdapter.js @@ -5,6 +5,7 @@ import adapterManager from '../src/adapterManager.js'; import {ajax} from '../src/ajax.js'; import {getGlobal} from '../src/prebidGlobal.js'; import { config } from '../src/config.js'; +import {bidderSettings} from '../src/bidderSettings.js'; const DEFAULT_PROTOCOL = 'https'; const DEFAULT_HOST = 'pa.deployads.com'; @@ -182,11 +183,11 @@ function getFactor(bidder) { } function getBiddersFactors() { - const pb = getGlobal(); const result = {}; - if (pb && pb.bidderSettings) { - Object.keys(pb.bidderSettings).forEach(bidderKey => { - const bidder = pb.bidderSettings[bidderKey]; + const settings = bidderSettings.getSettings(); + if (settings) { + Object.keys(settings).forEach(bidderKey => { + const bidder = settings[bidderKey]; const factor = getFactor(bidder); if (factor !== null) { result[bidderKey] = factor; diff --git a/modules/sovrnAnalyticsAdapter.js b/modules/sovrnAnalyticsAdapter.js index aee7ddd2690..065cfaa58bc 100644 --- a/modules/sovrnAnalyticsAdapter.js +++ b/modules/sovrnAnalyticsAdapter.js @@ -1,11 +1,10 @@ -import { logError, timestamp } from '../src/utils.js'; -import adapter from '../src/AnalyticsAdapter.js' -import adaptermanager from '../src/adapterManager.js' -import CONSTANTS from '../src/constants.json' -import {ajaxBuilder} from '../src/ajax.js' -import {config} from '../src/config.js' -import find from 'core-js-pure/features/array/find.js' -import includes from 'core-js-pure/features/array/includes.js' +import {logError, timestamp} from '../src/utils.js'; +import adapter from '../src/AnalyticsAdapter.js'; +import adaptermanager from '../src/adapterManager.js'; +import CONSTANTS from '../src/constants.json'; +import {ajaxBuilder} from '../src/ajax.js'; +import {config} from '../src/config.js'; +import {find, includes} from '../src/polyfill.js'; const ajax = ajaxBuilder(0) diff --git a/modules/sovrnBidAdapter.js b/modules/sovrnBidAdapter.js index 38788036ce0..4ca8f03c6b4 100644 --- a/modules/sovrnBidAdapter.js +++ b/modules/sovrnBidAdapter.js @@ -1,12 +1,38 @@ -import { _each, getBidIdParameter, isArray, deepClone, parseUrl, getUniqueIdentifierStr, deepSetValue, logError, deepAccess } from '../src/utils.js'; +import { _each, getBidIdParameter, isArray, deepClone, parseUrl, getUniqueIdentifierStr, deepSetValue, logError, deepAccess, isInteger, logWarn } from '../src/utils.js'; import { registerBidder } from '../src/adapters/bidderFactory.js' -import { BANNER } from '../src/mediaTypes.js' -import { createEidsArray } from './userId/eids.js'; -import {config} from '../src/config.js'; +import { ADPOD, BANNER, VIDEO } from '../src/mediaTypes.js' +import { createEidsArray } from './userId/eids.js' +import {config} from '../src/config.js' + +const ORTB_VIDEO_PARAMS = { + 'mimes': (value) => Array.isArray(value) && value.length > 0 && value.every(v => typeof v === 'string'), + 'minduration': (value) => isInteger(value), + 'maxduration': (value) => isInteger(value), + 'protocols': (value) => Array.isArray(value) && value.every(v => v >= 1 && v <= 10), + 'w': (value) => isInteger(value), + 'h': (value) => isInteger(value), + 'startdelay': (value) => isInteger(value), + 'placement': (value) => isInteger(value) && value >= 1 && value <= 5, + 'linearity': (value) => [1, 2].indexOf(value) !== -1, + 'skip': (value) => [0, 1].indexOf(value) !== -1, + 'skipmin': (value) => isInteger(value), + 'skipafter': (value) => isInteger(value), + 'sequence': (value) => isInteger(value), + 'battr': (value) => Array.isArray(value) && value.every(v => v >= 1 && v <= 17), + 'maxextended': (value) => isInteger(value), + 'minbitrate': (value) => isInteger(value), + 'maxbitrate': (value) => isInteger(value), + 'boxingallowed': (value) => [0, 1].indexOf(value) !== -1, + 'playbackmethod': (value) => Array.isArray(value) && value.every(v => v >= 1 && v <= 6), + 'playbackend': (value) => [1, 2, 3].indexOf(value) !== -1, + 'delivery': (value) => Array.isArray(value) && value.every(v => v >= 1 && v <= 3), + 'pos': (value) => isInteger(value) && value >= 1 && value <= 7, + 'api': (value) => Array.isArray(value) && value.every(v => v >= 1 && v <= 6) +} export const spec = { code: 'sovrn', - supportedMediaTypes: [BANNER], + supportedMediaTypes: [BANNER, VIDEO], gvlid: 13, /** @@ -15,7 +41,12 @@ export const spec = { * @return boolean for whether or not a bid is valid */ isBidRequestValid: function(bid) { - return !!(bid.params.tagid && !isNaN(parseFloat(bid.params.tagid)) && isFinite(bid.params.tagid)) + return !!( + bid.params.tagid && + !isNaN(parseFloat(bid.params.tagid)) && + isFinite(bid.params.tagid) && + deepAccess(bid, 'mediaTypes.video.context') !== ADPOD + ) }, /** @@ -50,13 +81,9 @@ export const spec = { } iv = iv || getBidIdParameter('iv', bid.params) - let bidSizes = (bid.mediaTypes && bid.mediaTypes.banner && bid.mediaTypes.banner.sizes) || bid.sizes - bidSizes = ((isArray(bidSizes) && isArray(bidSizes[0])) ? bidSizes : [bidSizes]) - bidSizes = bidSizes.filter(size => isArray(size)) - const processedSizes = bidSizes.map(size => ({w: parseInt(size[0], 10), h: parseInt(size[1], 10)})) const floorInfo = (bid.getFloor && typeof bid.getFloor === 'function') ? bid.getFloor({ currency: 'USD', - mediaType: 'banner', + mediaType: bid.mediaTypes && bid.mediaTypes.banner ? 'banner' : 'video', size: '*' }) : {} floorInfo.floor = floorInfo.floor || getBidIdParameter('bidfloor', bid.params) @@ -64,13 +91,24 @@ export const spec = { const imp = { adunitcode: bid.adUnitCode, id: bid.bidId, - banner: { + tagid: String(getBidIdParameter('tagid', bid.params)), + bidfloor: floorInfo.floor + } + + if (deepAccess(bid, 'mediaTypes.banner')) { + let bidSizes = deepAccess(bid, 'mediaTypes.banner.sizes') || bid.sizes + bidSizes = (isArray(bidSizes) && isArray(bidSizes[0])) ? bidSizes : [bidSizes] + bidSizes = bidSizes.filter(size => isArray(size)) + const processedSizes = bidSizes.map(size => ({w: parseInt(size[0], 10), h: parseInt(size[1], 10)})) + + imp.banner = { format: processedSizes, w: 1, h: 1, - }, - tagid: String(getBidIdParameter('tagid', bid.params)), - bidfloor: floorInfo.floor + }; + } + if (deepAccess(bid, 'mediaTypes.video')) { + imp.video = _buildVideoRequestObj(bid); } imp.ext = getBidIdParameter('ext', bid.ortb2Imp) || undefined @@ -149,7 +187,7 @@ export const spec = { seatbid[0].bid && seatbid[0].bid.length > 0) { seatbid[0].bid.map(sovrnBid => { - sovrnBidResponses.push({ + const bid = { requestId: sovrnBid.impid, cpm: parseFloat(sovrnBid.price), width: parseInt(sovrnBid.w), @@ -158,11 +196,18 @@ export const spec = { dealId: sovrnBid.dealid || null, currency: 'USD', netRevenue: true, - mediaType: BANNER, - ad: decodeURIComponent(`${sovrnBid.adm}`), ttl: sovrnBid.ext ? (sovrnBid.ext.ttl || 90) : 90, meta: { advertiserDomains: sovrnBid && sovrnBid.adomain ? sovrnBid.adomain : [] } - }); + } + + if (!sovrnBid.nurl) { + bid.mediaType = VIDEO + bid.vastXml = decodeURIComponent(sovrnBid.adm) + } else { + bid.mediaType = BANNER + bid.ad = decodeURIComponent(`${sovrnBid.adm}`) + } + sovrnBidResponses.push(bid); }); } return sovrnBidResponses @@ -209,4 +254,34 @@ export const spec = { }, } -registerBidder(spec); +function _buildVideoRequestObj(bid) { + const videoObj = {} + const videoAdUnitParams = deepAccess(bid, 'mediaTypes.video', {}) + const videoBidderParams = deepAccess(bid, 'params.video', {}) + const computedParams = {} + + if (Array.isArray(videoAdUnitParams.playerSize)) { + const sizes = (Array.isArray(videoAdUnitParams.playerSize[0])) ? videoAdUnitParams.playerSize[0] : videoAdUnitParams.playerSize + computedParams.w = sizes[0] + computedParams.h = sizes[1] + } + + const videoParams = { + ...computedParams, + ...videoAdUnitParams, + ...videoBidderParams + }; + + Object.keys(ORTB_VIDEO_PARAMS).forEach(paramName => { + if (videoParams.hasOwnProperty(paramName)) { + if (ORTB_VIDEO_PARAMS[paramName](videoParams[paramName])) { + videoObj[paramName] = videoParams[paramName] + } else { + logWarn(`The OpenRTB video param ${paramName} has been skipped due to misformating. Please refer to OpenRTB 2.5 spec.`); + } + } + }) + return videoObj +} + +registerBidder(spec) diff --git a/modules/sovrnBidAdapter.md b/modules/sovrnBidAdapter.md index 2b5d21d5515..53e3158024d 100644 --- a/modules/sovrnBidAdapter.md +++ b/modules/sovrnBidAdapter.md @@ -10,9 +10,9 @@ Maintainer: jrosendahl@sovrn.com Sovrn's adapter integration to the Prebid library. Posts plain-text JSON to the /rtb/bid endpoint. -# Test Parameters +# Banner Test Parameters -``` +```js var adUnits = [ { code: 'test-leaderboard', @@ -45,3 +45,81 @@ var adUnits = [ } ] ``` + +# Video Test Parameters +### Instream +```js +var videoAdUnit = { + code: 'video1', + sizes: [640, 480], + mediaTypes: { + video: { + context: 'instream', + playerSize: [640, 480], + mimes: ['video/mp4'], + protocols: [1, 2, 3, 4, 5, 6, 7, 8], + playbackmethod: [2], + skip: 1, + }, + }, + bids: [ + { + bidder: 'sovrn', + // Prebid Server Bidder Params https://docs.prebid.org/dev-docs/pbs-bidders.html#sovrn + params: { + tagid: '315045', + bidfloor: '0.04', + }, + }, + ], +} +``` +### Outstream +```js +var adUnits = [ + { + code: videoId, + mediaTypes: { + video: { + context: 'outstream', + playerSize: [640, 360], + mimes: ['video/mp4'], + protocols: [1, 2, 3, 4, 5, 6, 7, 8], + playbackmethod: [2], + skip: 1, + }, + }, + bids: [ + { + bidder: 'sovrn', + // Prebid Server Bidder Params https://docs.prebid.org/dev-docs/pbs-bidders.html#sovrn + params: { + tagid: '315045', + bidfloor: '0.04', + }, + }, + ], + renderer: { + url: 'https://acdn.adnxs.com/video/outstream/ANOutstreamVideo.js', + render: function (bid) { + adResponse = { + ad: { + video: { + content: bid.vastXml, + player_height: bid.height, + player_width: bid.width, + }, + }, + } + // push to render queue because ANOutstreamVideo may not be loaded yet. + bid.renderer.push(() => { + ANOutstreamVideo.renderAd({ + targetId: bid.adUnitCode, + adResponse: adResponse, + }) + }) + }, + }, + }, +] +``` diff --git a/modules/spotxBidAdapter.js b/modules/spotxBidAdapter.js index 7d5865684a7..2fd403058d1 100644 --- a/modules/spotxBidAdapter.js +++ b/modules/spotxBidAdapter.js @@ -11,8 +11,7 @@ export const GOOGLE_CONSENT = { consented_providers: ['3', '7', '11', '12', '15' export const spec = { code: BIDDER_CODE, - gvlid: 165, - aliases: ['spotx'], + gvlid: 52, supportedMediaTypes: [VIDEO], /** diff --git a/modules/sspBCBidAdapter.js b/modules/sspBCBidAdapter.js index c8f3c138ce4..e46073a63ba 100644 --- a/modules/sspBCBidAdapter.js +++ b/modules/sspBCBidAdapter.js @@ -1,15 +1,17 @@ -import { isArray, deepAccess, logWarn, parseUrl } from '../src/utils.js'; -import { ajax } from '../src/ajax.js'; -import { registerBidder } from '../src/adapters/bidderFactory.js'; -import { BANNER, NATIVE, VIDEO } from '../src/mediaTypes.js'; -import strIncludes from 'core-js-pure/features/string/includes.js'; +import {deepAccess, isArray, logWarn, parseUrl} from '../src/utils.js'; +import {ajax} from '../src/ajax.js'; +import {registerBidder} from '../src/adapters/bidderFactory.js'; +import {BANNER, NATIVE, VIDEO} from '../src/mediaTypes.js'; +import {includes as strIncludes} from '../src/polyfill.js'; const BIDDER_CODE = 'sspBC'; const BIDDER_URL = 'https://ssp.wp.pl/bidder/'; const SYNC_URL = 'https://ssp.wp.pl/bidder/usersync'; const NOTIFY_URL = 'https://ssp.wp.pl/bidder/notify'; +const TRACKER_URL = 'https://bdr.wpcdn.pl/tag/jstracker.js'; +const GVLID = 676; const TMAX = 450; -const BIDDER_VERSION = '5.3'; +const BIDDER_VERSION = '5.41'; const W = window; const { navigator } = W; const oneCodeDetection = {}; @@ -29,8 +31,8 @@ const getNotificationPayload = bidData => { const result = { requestId: undefined, siteId: [], - adUnit: [], slotId: [], + tagid: [], } bids.forEach(bid => { let params = isArray(bid.params) ? bid.params[0] : bid.params; @@ -55,7 +57,7 @@ const getNotificationPayload = bidData => { result.adomain = meta.advertiserDomains && meta.advertiserDomains[0]; result.networkName = meta.networkName; } - result.adUnit.push(bid.adUnitCode) + result.tagid.push(bid.adUnitCode); result.requestId = bid.auctionId || result.requestId; result.timeout = bid.timeout || result.timeout; }) @@ -123,7 +125,16 @@ const applyClientHints = ortbRequest => { ] }]; - ortbRequest.user = Object.assign(ortbRequest.user, { data }); + const ch = { data }; + ortbRequest.user = { ...ortbRequest.user, ...ch }; +}; + +const applyUserIds = (validBidRequest, ortbRequest) => { + const eids = validBidRequest.userIdAsEids + if (eids && eids.length) { + const ids = { eids }; + ortbRequest.user = { ...ortbRequest.user, ...ids }; + } }; /** @@ -135,8 +146,8 @@ const applyGdpr = (bidderRequest, ortbRequest) => { if (gdprConsent) { const { apiVersion, gdprApplies, consentString } = gdprConsent; consentApiVersion = apiVersion; - ortbRequest.regs = Object.assign(ortbRequest.regs, { '[ortb_extensions.gdpr]': gdprApplies ? 1 : 0 }); - ortbRequest.user = Object.assign(ortbRequest.user, { '[ortb_extensions.consent]': consentString }); + ortbRequest.regs = Object.assign(ortbRequest.regs, { 'gdpr': gdprApplies ? 1 : 0 }); + ortbRequest.user = Object.assign(ortbRequest.user, { 'consent': consentString }); } } @@ -299,7 +310,7 @@ const mapImpression = slot => { ext.data = Object.assign({ pbsize: adUnitsCalled[adUnitCode] }, ext.data); const imp = { - id: id && siteId ? id : 'bidid-' + bidId, + id: id && siteId ? id.padStart(3, '0') : 'bidid-' + bidId, banner: mapBanner(slot), native: mapNative(slot), video: mapVideo(slot), @@ -342,7 +353,7 @@ const isVideoAd = bid => { const isNativeAd = bid => { const xmlTester = new RegExp(/^{['"]native['"]/); - return bid.adm && bid.adm.match(xmlTester); + return bid.admNative || (bid.adm && bid.adm.match(xmlTester)); } const parseNative = nativeData => { @@ -450,12 +461,10 @@ const renderCreative = (site, auctionId, bid, seat, request) => { window.gdpr = ${JSON.stringify(request.gdprConsent)}; window.page = "${site.page}"; window.ref = "${site.ref}"; + window.adlabel = "${site.adLabel ? site.adLabel : ''}"; + window.pubid = "${site.publisherId ? site.publisherId : ''}"; `; - if (gam) { - adcode += `window.gam = ${JSON.stringify(gam)};`; - } - adcode += ` @@ -469,6 +478,7 @@ const renderCreative = (site, auctionId, bid, seat, request) => { const spec = { code: BIDDER_CODE, + gvlid: GVLID, aliases: [], supportedMediaTypes: [BANNER, NATIVE, VIDEO], isBidRequestValid(bid) { @@ -513,6 +523,7 @@ const spec = { applyGdpr(bidderRequest, payload); applyClientHints(payload); + applyUserIds(validBidRequests[0], payload); return { method: 'POST', @@ -558,10 +569,14 @@ const spec = { /* bid response might include ext object containing siteId / slotId, as detected by OneCode update site / slot data in this case + + ext also might contain publisherId and custom ad label */ - const { siteid, slotid } = ext; + const { siteid, slotid, pubid, adlabel } = ext; site.id = siteid || site.id; site.slot = slotid || site.slot; + site.publisherId = pubid; + site.adLabel = adlabel; } if (bidRequest && site.id && !strIncludes(site.id, 'bidid')) { @@ -598,10 +613,28 @@ const spec = { bid.mediaType = 'native'; // check native object try { - const nativeData = JSON.parse(serverBid.adm).native; + const nativeData = serverBid.admNative || JSON.parse(serverBid.adm).native; bid.native = parseNative(nativeData); bid.width = 1; bid.height = 1; + + // append viewability tracker + const jsData = { + rid: bidRequest.auctionId, + crid: bid.creativeId, + adunit: bidRequest.adUnitCode, + url: bid.native.clickUrl, + vendor: seat, + site: site.id, + slot: site.slot, + cpm: bid.cpm.toPrecision(4), + } + const jsTracker = '`); + break; + case 'callback': + tracker.value(element); + break; + } +} + +function viewabilityCriteriaMet(observer, vid, element, tracker) { + stopObserving(observer, vid, element); + fireViewabilityTracker(element, tracker); +} + +/** + * Start measuring viewability of an element + * @typedef {{ method: string='img','js','callback', value: string|function }} ViewabilityTracker { method: 'img', value: 'http://my.tracker/123' } + * @typedef {{ inViewThreshold: number, timeInView: number }} ViewabilityCriteria { inViewThreshold: 0.5, timeInView: 1000 } + * @param {string} vid unique viewability identifier + * @param {HTMLElement} element + * @param {ViewabilityTracker} tracker + * @param {ViewabilityCriteria} criteria + */ +export function startMeasurement(vid, element, tracker, criteria) { + if (!isValid(vid, element, tracker, criteria)) { + return; + } + + const options = { + root: null, + rootMargin: '0px', + threshold: criteria.inViewThreshold, + }; + + let observer; + let viewable = false; + let stateChange = (entries) => { + viewable = entries[0].isIntersecting; + + if (viewable) { + observers[vid].timeoutId = window.setTimeout(() => { + viewabilityCriteriaMet(observer, vid, element, tracker); + }, criteria.timeInView); + } else if (observers[vid].timeoutId) { + window.clearTimeout(observers[vid].timeoutId); + } + }; + + observer = new IntersectionObserver(stateChange, options); + observers[vid] = { + observer: observer, + element: element, + timeoutId: null, + done: false, + }; + + observer.observe(element); + + logInfo(`${MODULE_NAME}: startMeasurement called with:`, arguments); +} + +/** + * Stop measuring viewability of an element + * @param {string} vid unique viewability identifier + */ +export function stopMeasurement(vid) { + if (!vid || !observers[vid]) { + logWarn(`${MODULE_NAME}: must provide a registered vid`, vid); + return; + } + + observers[vid].observer.unobserve(observers[vid].element); + if (observers[vid].timeoutId) { + window.clearTimeout(observers[vid].timeoutId); + } + + // allow the observer under this vid to be created again + if (!observers[vid].done) { + delete observers[vid]; + } +} + +function listenMessagesFromCreative() { + window.addEventListener('message', receiveMessage, false); +} + +/** + * Recieve messages from creatives + * @param {MessageEvent} evt + */ +export function receiveMessage(evt) { + var key = evt.message ? 'message' : 'data'; + var data = {}; + try { + data = JSON.parse(evt[key]); + } catch (e) { + return; + } + + if (!data || data.message !== 'Prebid Viewability') { + return; + } + + switch (data.action) { + case 'startMeasurement': + let element = data.elementId && document.getElementById(data.elementId); + if (!element) { + element = find(document.getElementsByTagName('IFRAME'), iframe => (iframe.contentWindow || iframe.contentDocument.defaultView) == evt.source); + } + + startMeasurement(data.vid, element, data.tracker, data.criteria); + break; + case 'stopMeasurement': + stopMeasurement(data.vid); + break; + } +} + +init(); diff --git a/modules/viewability.md b/modules/viewability.md new file mode 100644 index 00000000000..df93b5c40db --- /dev/null +++ b/modules/viewability.md @@ -0,0 +1,87 @@ +# Overview + +Module Name: Viewability + +Purpose: Track when a given HTML element becomes viewable + +Maintainer: atrajkovic@magnite.com + +# Configuration + +Module does not need any configuration, as long as you include it in your PBJS bundle. +Viewability module has only two functions `startMeasurement` and `stopMeasurement` which can be used to enable more complex viewability measurements. Since it allows tracking from within creative (possibly inside a safe frame) this module registers a message listener, for messages with a format that is described bellow. + +## `startMeasurement` + +| startMeasurement Arg Object | Scope | Type | Description | Example | +| --------------------- | -------- | ------------ | -------------------------------------------------------------------------------- | --------- | +| vid | Required | String | Unique viewability identifier, used to reference particular observer | `"ae0f9"` | +| element | Required | HTMLElement | Reference to an HTML element that needs to be tracked | `document.getElementById('test_div')` | +| tracker | Required | ViewabilityTracker | How viewaility event is communicated back to the parties of interest | `{ method: 'img', value: 'http://my.tracker/123' }` | +| criteria | Required | ViewabilityCriteria| Defines custom viewability criteria using the threshold and duration provided | `{ inViewThreshold: 0.5, timeInView: 1000 }` | + +| ViewabilityTracker | Scope | Type | Description | Example | +| --------------------- | -------- | ------------ | -------------------------------------------------------------------------------- | --------- | +| method | Required | String | Type of method for Tracker | `'img' OR 'js' OR 'callback'` | +| value | Required | String | URL string for 'img' and 'js' Trackers, or a function for 'callback' Tracker | `'http://my.tracker/123'` | + +| ViewabilityCriteria | Scope | Type | Description | Example | +| --------------------- | -------- | ------------ | -------------------------------------------------------------------------------- | --------- | +| inViewThreshold | Required | Number | Represents a percentage threshold for the Element to be registered as in view | `0.5` | +| timeInView | Required | Number | Number of milliseconds that a given element needs to be in view continuously, above the threshold | `1000` | + +## Please Note: +- `vid` allows for multiple trackers, with different criteria to be registered for a given HTML element, independently. It's not autogenerated by `startMeasurement()`, it needs to be provided by the caller so that it doesn't have to be posted back to the source iframe (in case viewability is started from within the creative). +- In case of 'callback' method, HTML element is being passed back to the callback function. +- When a tracker needs to be started, without direct access to pbjs, postMessage mechanism can be used to invoke `startMeasurement`, with a following payload: `vid`, `tracker` and `criteria` as described above, but also with `message: 'Prebid Viewability'` and `action: 'startMeasurement'`. Optionally payload can provide `elementId`, if available at that time (for ad servers where name of the iframe is known, or adservers that render outside an iframe). If `elementId` is not provided, viewability module will try to find the iframe that corresponds to the message source. + + +## `stopMeasurement` + +| stopMeasurement Arg Object | Scope | Type | Description | Example | +| --------------------- | -------- | ------------ | -------------------------------------------------------------------------------- | --------- | +| vid | Required | String | Unique viewability identifier, referencing an already started viewability tracker. | `"ae0f9"` | + +## Please Note: +- When a tracker needs to be stopped, without direct access to pbjs, postMessage mechanism can be used here as well. To invoke `stopMeasurement`, you provide the payload with `vid`, `message: 'Prebid Viewability'` and `action: 'stopMeasurement`. Check the example bellow. + +# Examples + +## Example of starting a viewability measurement, when you have direct access to pbjs +``` +pbjs.viewability.startMeasurement( + 'ae0f9', + document.getElementById('test_div'), + { method: 'img', value: 'http://my.tracker/123' }, + { inViewThreshold: 0.5, timeInView: 1000 } +); +``` + +## Example of starting a viewability measurement from within a rendered creative +``` +let viewabilityRecord = { + vid: 'ae0f9', + tracker: { method: 'img', value: 'http://my.tracker/123'}, + criteria: { inViewThreshold: 0.5, timeInView: 1000 }, + message: 'Prebid Viewability', + action: 'startMeasurement' +} + +window.parent.postMessage(JSON.stringify(viewabilityRecord), '*'); +``` + +## Example of stopping the viewability measurement, when you have direct access to pbjs +``` +pbjs.viewability.stopMeasurement('ae0f9'); +``` + +## Example of stopping the viewability measurement from within a rendered creative +``` +let viewabilityRecord = { + vid: 'ae0f9', + message: 'Prebid Viewability', + action: 'stopMeasurement' +} + +window.parent.postMessage(JSON.stringify(viewabilityRecord), '*'); +``` diff --git a/modules/viewdeosDXBidAdapter.js b/modules/viewdeosDXBidAdapter.js index e3d02938c5b..9e0cb91af9b 100644 --- a/modules/viewdeosDXBidAdapter.js +++ b/modules/viewdeosDXBidAdapter.js @@ -1,8 +1,8 @@ -import { deepAccess, isArray, flatten, logError, parseSizesInput } from '../src/utils.js'; +import {deepAccess, flatten, isArray, logError, parseSizesInput} from '../src/utils.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; -import {VIDEO, BANNER} from '../src/mediaTypes.js'; +import {BANNER, VIDEO} from '../src/mediaTypes.js'; import {Renderer} from '../src/Renderer.js'; -import findIndex from 'core-js-pure/features/array/find-index.js'; +import {findIndex} from '../src/polyfill.js'; const URL = 'https://ghb.sync.viewdeos.com/auction/'; const OUTSTREAM_SRC = 'https://player.sync.viewdeos.com/outstream-unit/2.01/outstream.min.js'; diff --git a/modules/visxBidAdapter.js b/modules/visxBidAdapter.js index af8672ea233..696d54e4b52 100644 --- a/modules/visxBidAdapter.js +++ b/modules/visxBidAdapter.js @@ -42,7 +42,7 @@ export const spec = { } } } - return !!bid.params.uid; + return !!bid.params.uid && !isNaN(parseInt(bid.params.uid)); }, buildRequests: function(validBidRequests, bidderRequest) { const auids = []; @@ -203,6 +203,15 @@ export const spec = { }, onTimeout: function(timeoutData) { // Call '/track/bid_timeout' with timeout data + timeoutData.forEach(({ params }) => { + if (params) { + params.forEach((item) => { + if (item && item.uid) { + item.uid = parseInt(item.uid); + } + }); + } + }); triggerPixel(buildUrl(TRACK_TIMEOUT_PATH) + '//' + JSON.stringify(timeoutData)); } }; @@ -249,7 +258,7 @@ function buildImpObject(bid) { ...(banner && { banner }), ...(video && { video }), ext: { - bidder: { uid: Number(uid) }, + bidder: { uid: parseInt(uid) }, } }; diff --git a/modules/voxBidAdapter.js b/modules/voxBidAdapter.js index 8db97800630..25dbbda90cf 100644 --- a/modules/voxBidAdapter.js +++ b/modules/voxBidAdapter.js @@ -1,7 +1,7 @@ -import { _map, logWarn, deepAccess, isArray } from '../src/utils.js'; -import { registerBidder } from '../src/adapters/bidderFactory.js' -import {BANNER, VIDEO} from '../src/mediaTypes.js' -import find from 'core-js-pure/features/array/find.js'; +import {_map, deepAccess, isArray, 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'; import {auctionManager} from '../src/auctionManager.js'; import {Renderer} from '../src/Renderer.js'; diff --git a/modules/waardexBidAdapter.js b/modules/waardexBidAdapter.js index ee17a71dd35..1a97e3bd351 100644 --- a/modules/waardexBidAdapter.js +++ b/modules/waardexBidAdapter.js @@ -1,8 +1,8 @@ -import { logError, isArray, deepAccess, getBidIdParameter } from '../src/utils.js'; +import {deepAccess, getBidIdParameter, isArray, logError} 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 'core-js-pure/features/array/find.js'; +import {find} from '../src/polyfill.js'; const ENDPOINT = `https://hb.justbidit.xyz:8843/prebid`; const BIDDER_CODE = 'waardex'; diff --git a/modules/weboramaRtdProvider.js b/modules/weboramaRtdProvider.js index 70f258e1698..0885df02f05 100644 --- a/modules/weboramaRtdProvider.js +++ b/modules/weboramaRtdProvider.js @@ -7,27 +7,42 @@ * @requires module:modules/realTimeData */ +/** onData callback type + * @callback dataCallback + * @param {Object} data profile data + * @param {Boolean} site true if site, else it is user + * @returns {void} + */ + /** * @typedef {Object} ModuleParams - * @property {WeboCtxConf} weboCtxConf - * @property {WeboUserDataConf} weboUserDataConf + * @property {?Boolean} setPrebidTargeting if true, will set the GAM targeting (default undefined) + * @property {?Boolean} sendToBidders if true, will send the contextual profile to all bidders (default undefined) + * @property {?dataCallback} onData callback + * @property {?WeboCtxConf} weboCtxConf + * @property {?WeboUserDataConf} weboUserDataConf */ /** * @typedef {Object} WeboCtxConf * @property {string} token required token to be used on bigsea contextual API requests * @property {?string} targetURL specify the target url instead use the referer - * @property {?Boolean} setPrebidTargeting if true will set the GAM targeting (default true) - * @property {?Boolean} sendToBidders if true, will send the contextual profile to all bidders (default true) + * @property {?Boolean} setPrebidTargeting if true, will set the GAM targeting (default params.setPrebidTargeting or true) + * @property {?Boolean} sendToBidders if true, will send the contextual profile to all bidders (default params.sendToBidders or true) + * @property {?dataCallback} onData callback * @property {?object} defaultProfile to be used if the profile is not found + * @property {?Boolean} enabled if false, will ignore this configuration */ /** * @typedef {Object} WeboUserDataConf - * @property {?string} localStorageProfileKey can be used to customize the local storage key (default is 'webo_wam2gam_entry') - * @property {?Boolean} setPrebidTargeting if true will set the GAM targeting (default true) - * @property {?Boolean} sendToBidders if true, will send the contextual profile to all bidders (default true) + * @property {?number} accountId wam account id + * @property {?Boolean} setPrebidTargeting if true, will set the GAM targeting (default params.setPrebidTargeting or true) + * @property {?Boolean} sendToBidders if true, will send the user-centric profile to all bidders (default params.sendToBidders or true) * @property {?object} defaultProfile to be used if the profile is not found + * @property {?dataCallback} onData callback + * @property {?string} localStorageProfileKey can be used to customize the local storage key (default is 'webo_wam2gam_entry') + * @property {?Boolean} enabled if false, will ignore this configuration */ import { @@ -39,8 +54,10 @@ import { isEmpty, mergeDeep, logError, + logWarn, tryAppendQueryString, - logMessage + logMessage, + isFn } from '../src/utils.js'; import { submodule @@ -65,7 +82,7 @@ const LOCAL_STORAGE_USER_TARGETING_SECTION = 'targeting'; /** @type {number} */ const GVLID = 284; /** @type {object} */ -export const storage = getStorageManager(GVLID, SUBMODULE_NAME); +export const storage = getStorageManager({gvlid: GVLID, moduleName: SUBMODULE_NAME}); /** @type {null|Object} */ let _weboContextualProfile = null; @@ -86,40 +103,88 @@ let _weboUserDataInitialized = false; function init(moduleConfig) { moduleConfig = moduleConfig || {}; const moduleParams = moduleConfig.params || {}; - const weboCtxConf = moduleParams.weboCtxConf || {}; + const weboCtxConf = moduleParams.weboCtxConf; const weboUserDataConf = moduleParams.weboUserDataConf; - _weboCtxInitialized = initWeboCtx(weboCtxConf); - _weboUserDataInitialized = initWeboUserData(weboUserDataConf); + _weboCtxInitialized = initWeboCtx(moduleParams, weboCtxConf); + _weboUserDataInitialized = initWeboUserData(moduleParams, weboUserDataConf); return _weboCtxInitialized || _weboUserDataInitialized; } /** Initialize contextual sub module + * @param {ModuleParams} moduleParams * @param {WeboCtxConf} weboCtxConf * @return {Boolean} true if sub module was initialized with success */ -function initWeboCtx(weboCtxConf) { +function initWeboCtx(moduleParams, weboCtxConf) { + if (!weboCtxConf || weboCtxConf.enabled === false) { + moduleParams.weboCtxConf = null; + + return false + } + + normalizeConf(moduleParams, weboCtxConf); + _weboCtxInitialized = false; _weboContextualProfile = null; if (!weboCtxConf.token) { - logError('missing param "token" for weborama contextual sub module initialization'); + logWarn('missing param "token" for weborama contextual sub module initialization'); return false; } + logMessage('weborama contextual intialized with success'); + return true; } /** Initialize weboUserData sub module + * @param {ModuleParams} moduleParams * @param {WeboUserDataConf} weboUserDataConf * @return {Boolean} true if sub module was initialized with success */ -function initWeboUserData(weboUserDataConf) { +function initWeboUserData(moduleParams, weboUserDataConf) { + if (!weboUserDataConf || weboUserDataConf.enabled === false) { + moduleParams.weboUserDataConf = null; + + return false; + } + + normalizeConf(moduleParams, weboUserDataConf); + _weboUserDataInitialized = false; _weboUserDataUserProfile = null; - return !!weboUserDataConf; + let message = 'weborama user-centric intialized with success'; + if (weboUserDataConf.hasOwnProperty('accountId')) { + message = `weborama user-centric intialized with success for account: ${weboUserDataConf.accountId}`; + } + + logMessage(message); + + return true; +} + +/** @type {Object} */ +const globalDefaults = { + setPrebidTargeting: true, + sendToBidders: true, + onData: (data, kind, def) => logMessage('onData(data,kind,default)', data, kind, def), +} + +/** normalize submodule configuration + * @param {ModuleParams} moduleParams + * @param {WeboCtxConf|WeboUserDataConf} submoduleParams + * @return {void} + */ +function normalizeConf(moduleParams, submoduleParams) { + Object.entries(globalDefaults).forEach(([propertyName, globalDefaultValue]) => { + if (!submoduleParams.hasOwnProperty(propertyName)) { + const hasModuleParam = moduleParams.hasOwnProperty(propertyName); + submoduleParams[propertyName] = (hasModuleParam) ? moduleParams[propertyName] : globalDefaultValue; + } + }) } /** function that provides ad server targeting data to RTD-core @@ -132,8 +197,8 @@ function getTargetingData(adUnitsCodes, moduleConfig) { const moduleParams = moduleConfig.params || {}; const weboCtxConf = moduleParams.weboCtxConf || {}; const weboUserDataConf = moduleParams.weboUserDataConf || {}; - const weboCtxConfTargeting = weboCtxConf.setPrebidTargeting !== false; - const weboUserDataConfTargeting = weboUserDataConf.setPrebidTargeting !== false; + const weboCtxConfTargeting = weboCtxConf.setPrebidTargeting; + const weboUserDataConfTargeting = weboUserDataConf.setPrebidTargeting; try { const profile = getCompleteProfile(moduleParams, weboCtxConfTargeting, weboUserDataConfTargeting); @@ -250,21 +315,79 @@ export function getBidRequestData(reqBidsConfigObj, onDone, moduleConfig) { function handleBidRequestData(adUnits, moduleParams) { const weboCtxConf = moduleParams.weboCtxConf || {}; const weboUserDataConf = moduleParams.weboUserDataConf || {}; - const weboCtxConfTargeting = weboCtxConf.sendToBidders !== false; - const weboUserDataConfTargeting = weboUserDataConf.sendToBidders !== false; - const profile = getCompleteProfile(moduleParams, weboCtxConfTargeting, weboUserDataConfTargeting); + const weboCtxConfTargeting = weboCtxConf.sendToBidders; + const weboUserDataConfTargeting = weboUserDataConf.sendToBidders; - if (isEmpty(profile)) { - return; + if (weboCtxConfTargeting) { + const contextualProfile = getContextualProfile(weboCtxConf); + if (!isEmpty(contextualProfile)) { + setBidRequestProfile(adUnits, contextualProfile, true); + } + } + + if (weboUserDataConfTargeting) { + const weboUserDataProfile = getWeboUserDataProfile(weboUserDataConf); + if (!isEmpty(weboUserDataProfile)) { + setBidRequestProfile(adUnits, weboUserDataProfile, false); + } } + handleOnData(weboCtxConf, weboUserDataConf); +} + +/** function that handle with onData callbacks + * @param {WeboCtxConf} weboCtxConf + * @param {WeboUserDataConf} weboUserDataConf + */ + +function handleOnData(weboCtxConf, weboUserDataConf) { + const callbacks = [{ + onData: weboCtxConf.onData, + fetchData: () => getContextualProfile(weboCtxConf), + site: true, + }, { + onData: weboUserDataConf.onData, + fetchData: () => getWeboUserDataProfile(weboUserDataConf), + site: false, + }]; + + callbacks.filter(obj => isFn(obj.onData)).forEach(obj => { + try { + const data = obj.fetchData(); + obj.onData(data, obj.site); + } catch (e) { + const kind = (obj.site) ? 'site' : 'user'; + logError(`error while executure onData callback with ${kind}-based data:`, e); + } + }); +} + +/** function that set bid request data on each segment (site or user centric) + * @param {Object[]} adUnits + * @param {Object} profile + * @param {Boolean} site true if site centric, else it is user centric + * @returns {void} + */ +function setBidRequestProfile(adUnits, profile, site) { + setGlobalOrtb2(profile, site); + adUnits.forEach(adUnit => { if (adUnit.hasOwnProperty('bids')) { - adUnit.bids.forEach(bid => handleBid(adUnit, profile, bid)); + const adUnitCode = adUnit.code || 'no code'; + adUnit.bids.forEach(bid => handleBid(adUnitCode, profile, site, bid)); } }); } +/** @type {string} */ +const APPNEXUS = 'appnexus'; + +/** @type {string} */ +const PUBMATIC = 'pubmatic'; + +/** @type {string} */ +const RUBICON = 'rubicon'; + /** @type {string} */ const SMARTADSERVER = 'smartadserver'; @@ -272,35 +395,143 @@ const SMARTADSERVER = 'smartadserver'; const bidderAliasRegistry = adapterManager.aliasRegistry || {}; /** handle individual bid - * @param {Object} adUnit + * @param {string} adUnitCode * @param {Object} profile + * @param {Boolean} site true if site centric, else it is user centric * @param {Object} bid * @returns {void} */ -function handleBid(adUnit, profile, bid) { +function handleBid(adUnitCode, profile, site, bid) { const bidder = bidderAliasRegistry[bid.bidder] || bid.bidder; - logMessage('handle bidder', bidder, bid); + logMessage(`handling on adunit '${adUnitCode}', bidder '${bidder}' and bid`, bid); switch (bidder) { + case APPNEXUS: + handleAppnexusBid(profile, bid); + + break; + + case PUBMATIC: + handlePubmaticBid(profile, bid); + + break; + case SMARTADSERVER: - handleSmartadserverBid(adUnit, profile, bid); + handleSmartadserverBid(profile, bid); break; + case RUBICON: + handleRubiconBid(profile, site, bid); + + break; + default: + logMessage(`unsupported bidder '${bidder}', trying via bidder ortb2 fpd`); + const section = ((site) ? 'site' : 'user'); + const base = `ortb2.${section}.ext.data`; + + assignProfileToObject(bid, base, profile); + } +} + +/** + * set ortb2 global data + * @param {Object} profile + * @param {Boolean} site + * @returns {void} + */ +function setGlobalOrtb2(profile, site) { + const section = ((site) ? 'site' : 'user'); + const base = `${section}.ext.data`; + const addOrtb2 = {}; + + assignProfileToObject(addOrtb2, base, profile); + + if (!isEmpty(addOrtb2)) { + const testGlobal = getGlobal().getConfig('ortb2') || {}; + const ortb2 = { + ortb2: mergeDeep({}, testGlobal, addOrtb2) + }; + getGlobal().setConfig(ortb2); + } +} + +/** + * assign profile to object + * @param {Object} destination + * @param {string} base + * @param {Object} profile + * @returns {void} + */ +function assignProfileToObject(destination, base, profile) { + Object.keys(profile).forEach(key => { + const path = `${base}.${key}`; + deepSetValue(destination, path, profile[key]) + }) +} + +/** handle rubicon bid + * @param {Object} profile + * @param {Boolean} site + * @param {Object} bid + * @returns {void} + */ +function handleRubiconBid(profile, site, bid) { + const section = (site) ? 'inventory' : 'visitor'; + const base = `params.${section}`; + assignProfileToObject(bid, base, profile); +} + +/** handle appnexus/xandr bid + * @param {Object} profile + * @param {Object} bid + * @returns {void} + */ +function handleAppnexusBid(profile, bid) { + const base = 'params.keywords'; + assignProfileToObject(bid, base, profile); +} + +/** handle pubmatic bid + * @param {Object} profile + * @param {Object} bid + * @returns {void} + */ +function handlePubmaticBid(profile, bid) { + const sep = '|'; + const subsep = ','; + const bidKey = 'params.dctr'; + const target = []; + + const data = deepAccess(bid, bidKey); + if (data) { + data.split(sep).forEach(t => target.push(t)); } + + Object.keys(profile).forEach(key => { + const value = profile[key].join(subsep); + const keyword = `${key}=${value}`; + if (target.indexOf(keyword) === -1) { + target.push(keyword); + } + }); + + deepSetValue(bid, bidKey, target.join(sep)); } /** handle smartadserver bid - * @param {Object} adUnit * @param {Object} profile * @param {Object} bid * @returns {void} */ -function handleSmartadserverBid(adUnit, profile, bid) { +function handleSmartadserverBid(profile, bid) { + const sep = ';'; + const bidKey = 'params.target'; const target = []; - if (deepAccess(bid, 'params.target')) { - target.push(bid.params.target.split(';')); + const data = deepAccess(bid, bidKey); + if (data) { + data.split(sep).forEach(t => target.push(t)); } Object.keys(profile).forEach(key => { @@ -311,8 +542,7 @@ function handleSmartadserverBid(adUnit, profile, bid) { } }); }); - - deepSetValue(bid, 'params.target', target.join(';')); + deepSetValue(bid, bidKey, target.join(sep)); } /** set bigsea contextual profile on module state @@ -350,7 +580,7 @@ function fetchContextualProfile(weboCtxConf, onSuccess, onDone) { queryString = tryAppendQueryString(queryString, 'token', token); queryString = tryAppendQueryString(queryString, 'url', targetURL); - const url = 'https://ctx.weborama.com/api/profile?' + queryString; + const url = `https://ctx.weborama.com/api/profile?${queryString}`; ajax(url, { success: function(response, req) { diff --git a/modules/weboramaRtdProvider.md b/modules/weboramaRtdProvider.md index 06e5b4fb43b..732944c6e1c 100644 --- a/modules/weboramaRtdProvider.md +++ b/modules/weboramaRtdProvider.md @@ -21,40 +21,51 @@ Compile the Weborama RTD module into your Prebid build: Add the Weborama RTD provider to your Prebid config. ```javascript -pbjs.setConfig( - ... - realTimeData: { - auctionDelay: 1000, - dataProviders: [ - { +var pbjs = pbjs || {}; +pbjs.que = pbjs.que || []; + +pbjs.que.push(function () { + pbjs.setConfig({ + debug: true, + realTimeData: { + auctionDelay: 1000, + dataProviders: [{ name: "weborama", waitForIt: true, params: { - weboCtxConf: { // contextual configuration - token: "<>", // mandatory - targetURL: "...", // default is document.URL - setPrebidTargeting: true, // default - sendToBidders: true, // default - defaultProfile: { // optional, default is none - webo_ctx: ['foo'], - webo_ds: ['bar'] - } + setPrebidTargeting: true, // optional + sendToBidders: true, // optional + onData: function(data, site){ // optional + var kind = (site)? 'site' : 'user'; + console.log('onData', kind, data); }, - weboUserDataConf: { // user-centric configuration - setPrebidTargeting: true, // default - sendToBidders: true, // default - defaultProfile: { // optional, default is none - webo_cs: ['baz'], - webo_audiences: ['bam'] - }, - localStorageProfileKey: 'webo_wam2gam_entry' // default + weboCtxConf: { + token: "to-be-defined", // mandatory + targetURL: "https://prebid.org", // default is document.URL + setPrebidTargeting: true, // override param.setPrebidTargeting or default true + sendToBidders: true, // override param.sendToBidders or default true + defaultProfile: { // optional + webo_ctx: ['moon'], + webo_ds: ['bar'] + } + //, onData: function (data, ...) { ...} + }, + weboUserDataConf: { + accountId: 12345, // optional, used for logging + setPrebidTargeting: true, // override param.setPrebidTargeting or default true + sendToBidders: true, // override param.sendToBidders or default true + defaultProfile: { // optional + webo_cs: ['Red'], + webo_audiences: ['bam'] + }, + localStorageProfileKey: 'webo_wam2gam_entry' // default + //, onData: function (data, ...) { ...} } } - } - ] - } - ... -); + }] + } + }); +}); ``` ### Parameter Descriptions for the Weborama Configuration Section @@ -64,23 +75,82 @@ pbjs.setConfig( | name | String | Real time data module name | Mandatory. Always 'Weborama' | | waitForIt | Boolean | Mandatory. Required to ensure that the auction is delayed until prefetch is complete | Optional. Defaults to false but recommended to true | | params | Object | | Optional | -| params.weboCtxConf | Object | Weborama Contextual Configuration | Optional | -| params.weboCtxConf.token | String | Security Token provided by Weborama, unique per client | Mandatory | -| params.weboCtxConf.targetURL | String | Url to be profiled in the contextual api | Optional. Defaults to `document.URL` | -| params.weboCtxConf.setPrebidTargeting|Boolean|If true, will use the contextual profile to set the prebid (GPT/GAM or AST) targeting of all adunits managed by prebid.js| Optional. Default is *true*.| -| params.weboCtxConf.sendToBidders|Boolean|If true, will send the contextual profile to all bidders (only smartadserver is supported now)| Optional. Default is *true*.| -| params.weboCtxConf.defaultProfile | Object | default value of the profile to be used when there are no response from contextual api (such as timeout)| Optional. Default is `{}` | -| params.weboUserDataConf | Object | WeboUserData Configuration | Optional | -| params.weboUserDataConf.setPrebidTargeting|Boolean|If true, will use the contextual profile to set the prebid (GPT/GAM or AST) targeting of all adunits managed by prebid.js| Optional. Default is *true*.| -| params.weboUserDataConf.sendToBidders|Boolean|If true, will send the contextual profile to all bidders (only smartadserver is supported now)| Optional. Default is *true*.| -| params.weboUserDataConf.defaultProfile | Object | default value of the profile to be used when there are no response from contextual api (such as timeout)| Optional. Default is `{}` | -| params.weboUserDataConf.localStorageProfileKey| String | can be used to customize the local storage key | Optional | +| params.setPrebidTargeting | Boolean | If true, may use the profile to set the prebid (GPT/GAM or AST) targeting of all adunits managed by prebid.js | Optional. Affects the `weboCtxConf` and `weboUserDataConf` sections | +| params.sendToBidders | Boolean | If true, may send the profile to all bidders | Optional. Affects the `weboCtxConf` and `weboUserDataConf` sections | +| params.weboCtxConf | Object | Weborama Contextual Configuration | Optional +| params.weboUserDataConf | Object | Weborama User-Centric Configuration | Optional | +| params.onData | Callback | If set, will receive the profile and site flag | Optional. Affects the `weboCtxConf` and `weboUserDataConf` sections | + +#### Contextual Configuration + +| Name |Type | Description | Notes | +| :------------ | :------------ | :------------ |:------------ | +| token | String | Security Token provided by Weborama, unique per client | Mandatory | +| targetURL | String | Url to be profiled in the contextual api | Optional. Defaults to `document.URL` | +| setPrebidTargeting|Boolean|If true, will use the contextual profile to set the prebid (GPT/GAM or AST) targeting of all adunits managed by prebid.js| Optional. Default is `params.setPrebidTargeting` (if any) or **true**.| +| sendToBidders|Boolean|If true, will send the contextual profile to all bidders| Optional. Default is `params.sendToBidders` (if any) or **true**.| +| defaultProfile | Object | default value of the profile to be used when there are no response from contextual api (such as timeout)| Optional. Default is `{}` | +| onData | Callback | If set, will receive the profile and site flag | Optional. Default is `params.onData` (if any) or log via prebid debug | +| enabled | Boolean| if false, will ignore this configuration| default true| + +#### User-Centric Configuration + +| Name |Type | Description | Notes | +| :------------ | :------------ | :------------ |:------------ | +| accountId|Number|WAM account id. If present, will be used on logging and statistics| Optional.| +| setPrebidTargeting|Boolean|If true, will use the user profile to set the prebid (GPT/GAM or AST) targeting of all adunits managed by prebid.js| Optional. Default is `params.setPrebidTargeting` (if any) or **true**.| +| sendToBidders|Boolean|If true, will send the user profile to all bidders| Optional. Default is `params.sendToBidders` (if any) or **true**.| +| onData | Callback | If set, will receive the profile and site flag | Optional. Default is `params.onData` (if any) or log via prebid debug | +| defaultProfile | Object | default value of the profile to be used when there are no response from contextual api (such as timeout)| Optional. Default is `{}` | +| localStorageProfileKey| String | can be used to customize the local storage key | Optional | +| enabled | Boolean| if false, will ignore this configuration| default true| + +### Supported Bidders + +We currently support the following bidder adapters: +* SmartADServer SSP +* PubMatic SSP +* AppNexus SSP +* Rubicon SSP + +We also set the bidder and global ortb2 `site` and `user` sections. The following bidders may support it, to be sure, check the `First Party Data Support` on the feature list for the particular bidder from here: https://docs.prebid.org/dev-docs/bidders + +* Adagio +* AdformOpenRTB +* AdKernel +* AdMixer +* Adnuntius +* Adrelevantis +* adxcg +* AMX RTB +* Avocet +* BeOp +* Criteo +* Etarget +* Inmar +* Index Exchange +* Livewrapped +* Mediakeys +* NoBid +* OpenX +* Opt Out Advertising +* Ozone Project +* Proxistore +* Rise +* Smaato +* Sonobi +* TheMediaGrid +* TripleLift +* TrustX +* Yahoo SSP +* Yieldlab +* Zeta Global Ssp ### Testing To view an example of available segments returned by Weborama's backends: -`gulp serve --modules=rtdModule,weboramaRtdProvider,smartadserverBidAdapter` +`gulp serve --notest --nolint --modules=rtdModule,weboramaRtdProvider,smartadserverBidAdapter,pubmaticBidAdapter,appnexusBidAdapter,rubiconBidAdapter,criteoBidAdapter` and then point your browser at: diff --git a/modules/widespaceBidAdapter.js b/modules/widespaceBidAdapter.js index 7890628f94b..ba94f90f9c9 100644 --- a/modules/widespaceBidAdapter.js +++ b/modules/widespaceBidAdapter.js @@ -1,14 +1,8 @@ import {config} from '../src/config.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; -import { - parseQueryStringParameters, - parseSizesInput -} from '../src/utils.js'; -import includes from 'core-js-pure/features/array/includes.js'; -import find from 'core-js-pure/features/array/find.js'; -import { getStorageManager } from '../src/storageManager.js'; - -export const storage = getStorageManager(); +import {parseQueryStringParameters, parseSizesInput} from '../src/utils.js'; +import {find, includes} from '../src/polyfill.js'; +import {getStorageManager} from '../src/storageManager.js'; const BIDDER_CODE = 'widespace'; const WS_ADAPTER_VERSION = '2.0.1'; @@ -17,6 +11,7 @@ const LS_KEYS = { LC_UID: 'wsLcuid', CUST_DATA: 'wsCustomData' }; +export const storage = getStorageManager({bidderCode: BIDDER_CODE}); let preReqTime = 0; diff --git a/modules/winrBidAdapter.js b/modules/winrBidAdapter.js index 9213c113460..124aba57866 100644 --- a/modules/winrBidAdapter.js +++ b/modules/winrBidAdapter.js @@ -1,12 +1,22 @@ -import { convertCamelToUnderscore, isArray, isNumber, isPlainObject, deepAccess, logError, convertTypes, getParameterByName, getBidRequest, isEmpty, transformBidderParamKeywords, isFn } from '../src/utils.js'; -import { config } from '../src/config.js'; -import { registerBidder } from '../src/adapters/bidderFactory.js'; -import { BANNER } from '../src/mediaTypes.js'; -import find from 'core-js-pure/features/array/find.js'; -import includes from 'core-js-pure/features/array/includes.js'; -import { getStorageManager } from '../src/storageManager.js'; - -export const storage = getStorageManager(); +import { + convertCamelToUnderscore, + convertTypes, + deepAccess, + getBidRequest, + getParameterByName, + isArray, + isEmpty, + isFn, + isNumber, + isPlainObject, + logError, + transformBidderParamKeywords +} from '../src/utils.js'; +import {config} from '../src/config.js'; +import {registerBidder} from '../src/adapters/bidderFactory.js'; +import {BANNER} from '../src/mediaTypes.js'; +import {find, includes} from '../src/polyfill.js'; +import {getStorageManager} from '../src/storageManager.js'; const BIDDER_CODE = 'winr'; const URL = 'https://ib.adnxs.com/ut/v3/prebid'; @@ -17,6 +27,8 @@ const SOURCE = 'pbjs'; const DEFAULT_CURRENCY = 'USD'; const GATE_COOKIE_NAME = 'wnr_gate'; +export const storage = getStorageManager({bidderCode: BIDDER_CODE}); + function buildBid(bidData) { const bid = bidData; const position = { @@ -39,9 +51,9 @@ function wrapAd(bid, position) { - + ' + }, + 'rtb': { + 'banner': { + 'content': '', + 'width': 300, + 'height': 250 + }, + 'trackers': [ + { + 'impression_urls': [ + 'https://lax1-ib.adnxs.com/impression', + 'https://www.test.com/tracker' + ], + 'video_events': {} + } + ] + } + } + ] + } + ] + }; + + it('should get correct bid response', function () { + const expectedResponse = [ + { + 'requestId': '3db3773286ee59', + 'cpm': 0.5, + 'creativeId': 29681110, + 'dealId': undefined, + 'width': 300, + 'height': 250, + 'ad': '', + 'mediaType': 'banner', + 'currency': 'USD', + 'ttl': 300, + 'netRevenue': true, + 'adUnitCode': 'code', + 'appnexus': { + 'buyerMemberId': 958 + }, + 'meta': { + 'dchain': { + 'ver': '1.0', + 'complete': 0, + 'nodes': [{ + 'bsid': '958' + }] + } + } + } + ]; + const bidderRequest = { + bids: [{ + bidId: '3db3773286ee59', + adUnitCode: 'code' + }] + }; + const result = spec.interpretResponse({ body: response }, {bidderRequest}); + expect(Object.keys(result[0])).to.have.members(Object.keys(expectedResponse[0])); + }); + + it('handles outstream video responses', function () { + const response = { + 'tags': [{ + 'uuid': '84ab500420319d', + 'ads': [{ + 'ad_type': 'video', + 'cpm': 0.500000, + 'notify_url': 'imptracker.com', + 'rtb': { + 'video': { + 'content': '' + } + }, + 'javascriptTrackers': '' + }] + }] + }; + const bidderRequest = { + bids: [{ + bidId: '84ab500420319d', + adUnitCode: 'code', + mediaTypes: { + video: { + context: 'outstream' + } + } + }] + } + + const result = spec.interpretResponse({ body: response }, {bidderRequest}); + expect(result[0]).not.to.have.property('vastXml'); + expect(result[0]).not.to.have.property('vastUrl'); + expect(result[0]).to.have.property('width', 1); + expect(result[0]).to.have.property('height', 1); + expect(result[0]).to.have.property('mediaType', 'banner'); + }); + }); + + describe('getUserSyncs', function() { + const syncOptions = { + syncEnabled: false + }; + + it('should not return sync', function() { + const serverResponse = [{ body: '' }]; + const result = spec.getUserSyncs(syncOptions, serverResponse); + expect(result).to.be.undefined; + }); + }); + + describe('transformBidParams', function() { + it('cast placementId to number', function() { + const adUnit = { + code: 'adunit-code', + params: { + placementId: '456' + } + }; + const bid = { + params: { + placementId: '456' + }, + sizes: [[300, 250]], + mediaTypes: { + banner: { sizes: [[300, 250]] } + } + }; + + const params = spec.transformBidParams({ placementId: '456' }, true, adUnit, [{ bidderCode: 'bigRichmedia', auctionId: bid.auctionId, bids: [bid] }]); + + expect(params.placement_id).to.exist; + expect(params.placement_id).to.be.a('number'); + }); + }); + + describe('onBidWon', function() { + it('Should not have any error', function() { + const result = spec.onBidWon({}); + expect(true).to.be.true; + }); + }); +}); diff --git a/test/spec/modules/bizzclickBidAdapter_spec.js b/test/spec/modules/bizzclickBidAdapter_spec.js index 500f45e0573..f80051b0a50 100644 --- a/test/spec/modules/bizzclickBidAdapter_spec.js +++ b/test/spec/modules/bizzclickBidAdapter_spec.js @@ -48,6 +48,17 @@ const BANNER_BID_REQUEST = { sizes: [[300, 250], [300, 600]] } }, + schain: { + ver: '1.0', + complete: 1, + nodes: [ + { + asi: 'example.com', + sid: '164', + hp: 1 + } + ] + }, bidder: 'bizzclick', params: { placementId: 'hash', @@ -252,6 +263,11 @@ describe('BizzclickAdapter', function() { 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'); }); diff --git a/test/spec/modules/brandmetricsRtdProvider_spec.js b/test/spec/modules/brandmetricsRtdProvider_spec.js new file mode 100644 index 00000000000..3cac5a3d559 --- /dev/null +++ b/test/spec/modules/brandmetricsRtdProvider_spec.js @@ -0,0 +1,191 @@ +import * as brandmetricsRTD from '../../../modules/brandmetricsRtdProvider.js'; +import {config} from 'src/config.js'; + +const VALID_CONFIG = { + name: 'brandmetrics', + waitForIt: true, + params: { + scriptId: '00000000-0000-0000-0000-000000000000', + bidders: ['ozone'] + } +}; + +const NO_BIDDERS_CONFIG = { + name: 'brandmetrics', + waitForIt: true, + params: { + scriptId: '00000000-0000-0000-0000-000000000000' + } +}; + +const NO_SCRIPTID_CONFIG = { + name: 'brandmetrics', + waitForIt: true +}; + +const USER_CONSENT = { + gdpr: { + vendorData: { + vendor: { + consents: { + 422: true + } + }, + purpose: { + consents: { + 1: true, + 7: true + } + } + }, + gdprApplies: true + } +}; + +const NO_TCF_CONSENT = { + gdpr: { + vendorData: { + vendor: { + consents: { + 422: false + } + }, + purpose: { + consents: { + 1: false, + 7: false + } + } + }, + gdprApplies: true + } +}; + +const NO_USP_CONSENT = { + usp: '1NYY' +}; + +function mockSurveyLoaded(surveyConf) { + const commands = window._brandmetrics || []; + commands.forEach(command => { + if (command.cmd === '_addeventlistener') { + const conf = command.val; + if (conf.event === 'surveyloaded') { + conf.handler(surveyConf); + } + } + }); +} + +function scriptTagExists(url) { + const tags = document.getElementsByTagName('script'); + for (let i = 0; i < tags.length; i++) { + if (tags[i].src === url) { + return true; + } + } + return false; +} + +describe('BrandmetricsRTD module', () => { + beforeEach(function () { + const scriptTags = document.getElementsByTagName('script'); + for (let i = 0; i < scriptTags.length; i++) { + if (scriptTags[i].src.indexOf('brandmetrics') !== -1) { + scriptTags[i].remove(); + } + } + }); + + it('should init and return true', () => { + expect(brandmetricsRTD.brandmetricsSubmodule.init(VALID_CONFIG, USER_CONSENT)).to.equal(true); + }); + + it('should init and return true even if bidders is not included', () => { + expect(brandmetricsRTD.brandmetricsSubmodule.init(NO_BIDDERS_CONFIG, USER_CONSENT)).to.equal(true); + }); + + it('should init even if script- id is not configured', () => { + expect(brandmetricsRTD.brandmetricsSubmodule.init(NO_SCRIPTID_CONFIG, USER_CONSENT)).to.equal(true); + }); + + it('should not init when there is no TCF- consent', () => { + expect(brandmetricsRTD.brandmetricsSubmodule.init(VALID_CONFIG, NO_TCF_CONSENT)).to.equal(false); + }); + + it('should not init when there is no usp- consent', () => { + expect(brandmetricsRTD.brandmetricsSubmodule.init(VALID_CONFIG, NO_USP_CONSENT)).to.equal(false); + }); +}); + +describe('getBidRequestData', () => { + beforeEach(function () { + config.resetConfig() + }) + + it('should set targeting keys for specified bidders', () => { + brandmetricsRTD.brandmetricsSubmodule.getBidRequestData({}, () => { + const bidderConfig = config.getBidderConfig() + const expected = VALID_CONFIG.params.bidders + + expected.forEach(exp => { + expect(bidderConfig[exp].ortb2.user.ext.data.mockTargetKey).to.equal('mockMeasurementId') + }) + }, VALID_CONFIG); + + mockSurveyLoaded({ + available: true, + conf: { + displayOption: { + type: 'pbjs', + targetKey: 'mockTargetKey' + } + }, + survey: { + measurementId: 'mockMeasurementId' + } + }); + }); + + it('should only set targeting keys when the brandmetrics survey- type is "pbjs"', () => { + mockSurveyLoaded({ + available: true, + conf: { + displayOption: { + type: 'dfp', + targetKey: 'mockTargetKey' + } + }, + survey: { + measurementId: 'mockMeasurementId' + } + }); + + brandmetricsRTD.brandmetricsSubmodule.getBidRequestData({}, () => {}, VALID_CONFIG); + const bidderConfig = config.getBidderConfig() + expect(Object.keys(bidderConfig).length).to.equal(0) + }); + + it('should use a default targeting key name if the brandmetrics- configuration does not include one', () => { + mockSurveyLoaded({ + available: true, + conf: { + displayOption: { + type: 'pbjs', + } + }, + survey: { + measurementId: 'mockMeasurementId' + } + }); + + brandmetricsRTD.brandmetricsSubmodule.getBidRequestData({}, () => {}, VALID_CONFIG); + + const bidderConfig = config.getBidderConfig() + const expected = VALID_CONFIG.params.bidders + + expected.forEach(exp => { + expect(bidderConfig[exp].ortb2.user.ext.data.brandmetrics_survey).to.equal('mockMeasurementId') + }) + }); +}); diff --git a/test/spec/modules/browsiRtdProvider_spec.js b/test/spec/modules/browsiRtdProvider_spec.js index 32e1c7fe795..c36b48c5105 100644 --- a/test/spec/modules/browsiRtdProvider_spec.js +++ b/test/spec/modules/browsiRtdProvider_spec.js @@ -1,6 +1,8 @@ import * as browsiRTD from '../../../modules/browsiRtdProvider.js'; import {makeSlot} from '../integration/faker/googletag.js'; import * as utils from '../../../src/utils' +import * as events from '../../../src/events'; +import * as sinon from 'sinon'; describe('browsi Real time data sub module', function () { const conf = { @@ -15,6 +17,28 @@ describe('browsi Real time data sub module', function () { } }] }; + const auction = {adUnits: [ + { + code: 'adMock', + transactionId: 1 + }, + { + code: 'hasPrediction', + transactionId: 1 + } + ]}; + + let sandbox; + let eventsEmitSpy; + + before(() => { + sandbox = sinon.sandbox.create(); + eventsEmitSpy = sandbox.spy(events, ['emit']); + }); + + after(() => { + sandbox.restore(); + }); it('should init and return true', function () { browsiRTD.collectData(); @@ -61,13 +85,13 @@ describe('browsi Real time data sub module', function () { describe('should return data to RTD module', function () { it('should return empty if no ad units defined', function () { browsiRTD.setData({}); - expect(browsiRTD.browsiSubmodule.getTargetingData([])).to.eql({}); + 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'])).to.eql({adMock: {bv: 'NA'}}); + expect(browsiRTD.browsiSubmodule.getTargetingData(['adMock'], null, null, auction)).to.eql({adMock: {bv: 'NA'}}); }); it('should return prediction from server', function () { @@ -78,7 +102,7 @@ describe('browsi Real time data sub module', function () { pmd: undefined }; browsiRTD.setData(data); - expect(browsiRTD.browsiSubmodule.getTargetingData(['hasPrediction'])).to.eql({hasPrediction: {bv: '0.20'}}); + expect(browsiRTD.browsiSubmodule.getTargetingData(['hasPrediction'], null, null, auction)).to.eql({hasPrediction: {bv: '0.20'}}); }) }) @@ -135,4 +159,77 @@ describe('browsi Real time data sub module', function () { expect(utils.deepAccess(fakeAdUnits[1], 'ortb2Imp.ext.data.browsi')).to.eql({bv: '0.10'}); }) }) + + describe('should emit billable event', function () { + beforeEach(() => { + eventsEmitSpy.resetHistory(); + }) + it('should send one event per ad unit code', function () { + const auction = {adUnits: [ + { + code: 'a', + transactionId: 1 + }, + { + code: 'b', + transactionId: 2 + }, + { + code: 'a', + transactionId: 3 + }, + ]}; + + browsiRTD.browsiSubmodule.getTargetingData(['a', 'b'], null, null, auction); + expect(eventsEmitSpy.callCount).to.equal(2); + }) + it('should send events only for received ad unit codes', function () { + const auction = {adUnits: [ + { + code: 'a', + transactionId: 1 + }, + { + code: 'b', + transactionId: 2 + }, + { + code: 'c', + transactionId: 3 + }, + ]}; + + browsiRTD.browsiSubmodule.getTargetingData(['a'], null, null, auction); + expect(eventsEmitSpy.callCount).to.equal(1); + browsiRTD.browsiSubmodule.getTargetingData(['b'], null, null, auction); + expect(eventsEmitSpy.callCount).to.equal(2); + }) + it('should use 1st transaction ID in case of twin ad unit codes', function () { + const auction = { + auctionId: '123', + adUnits: [ + { + code: 'a', + transactionId: 1 + }, + { + code: 'a', + transactionId: 3 + }, + ]}; + + const expectedCall = { + vendor: 'browsi', + type: 'adRequest', + transactionId: 1, + auctionId: '123' + } + + browsiRTD.browsiSubmodule.getTargetingData(['a'], null, null, auction); + const callArguments = eventsEmitSpy.getCalls()[0].args[1]; + // billing id is random, we can't check its value + delete callArguments['billingId']; + expect(callArguments).to.eql(expectedCall); + }) + }) }); diff --git a/test/spec/modules/colossussspBidAdapter_spec.js b/test/spec/modules/colossussspBidAdapter_spec.js index e453bcbc997..32f02def27e 100644 --- a/test/spec/modules/colossussspBidAdapter_spec.js +++ b/test/spec/modules/colossussspBidAdapter_spec.js @@ -61,6 +61,7 @@ describe('ColossussspAdapter', function () { }); it('Should return false when placement_id is not a number', function () { bid.params.placement_id = 'aaa'; + delete bid.params.group_id; expect(spec.isBidRequestValid(bid)).to.be.false; }); }); @@ -108,7 +109,7 @@ describe('ColossussspAdapter', function () { } }); it('Returns empty data if no valid requests are passed', function () { - serverRequest = spec.buildRequests([]); + serverRequest = spec.buildRequests([], bidderRequest); let data = serverRequest.data; expect(data.placements).to.be.an('array').that.is.empty; }); @@ -198,13 +199,13 @@ describe('ColossussspAdapter', function () { }) describe('getUserSyncs', function () { - let userSync = spec.getUserSyncs(); + let userSync = spec.getUserSyncs({}, {}, {}, {}); 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[0].type).to.be.equal('image'); - expect(userSync[0].url).to.be.equal('https://colossusssp.com/?c=o&m=cookie'); + expect(userSync[0].type).to.be.equal('hms.gif'); + expect(userSync[0].url).to.be.equal('https://sync.colossusssp.com/hms.gif?pbjs=1&coppa=0'); }); }); }); diff --git a/test/spec/modules/consentManagementUsp_spec.js b/test/spec/modules/consentManagementUsp_spec.js index 7d3cd48a8e4..32fd2ddb2e2 100644 --- a/test/spec/modules/consentManagementUsp_spec.js +++ b/test/spec/modules/consentManagementUsp_spec.js @@ -8,9 +8,9 @@ import { } from 'modules/consentManagementUsp.js'; import * as utils from 'src/utils.js'; import { config } from 'src/config.js'; -import { uspDataHandler } from 'src/adapterManager.js'; +import {uspDataHandler} from 'src/adapterManager.js'; +import 'src/prebid.js'; -let assert = require('chai').assert; let expect = require('chai').expect; function createIFrameMarker() { @@ -58,6 +58,12 @@ describe('consentManagement', function () { sinon.assert.notCalled(utils.logInfo); }); + it('should not produce any USP metadata', function() { + setConsentConfig({}); + let consentMeta = uspDataHandler.getConsentMeta(); + expect(consentMeta).to.be.undefined; + }); + it('should exit the consent manager if only config.gdpr is an object', function() { setConsentConfig({ gdpr: { cmpApi: 'iab' } }); expect(consentAPI).to.be.undefined; @@ -71,6 +77,11 @@ describe('consentManagement', function () { sinon.assert.calledOnce(utils.logWarn); sinon.assert.notCalled(utils.logInfo); }); + + it('should immediately start looking up consent data', () => { + setConsentConfig({usp: {cmpApi: 'invalid'}}); + expect(uspDataHandler.ready).to.be.true; + }); }); describe('valid setConsentConfig value', function () { @@ -91,6 +102,21 @@ describe('consentManagement', function () { expect(consentAPI).to.be.equal('daa'); expect(consentTimeout).to.be.equal(7500); }); + + it('should enable uspDataHandler', () => { + setConsentConfig({usp: {cmpApi: 'daa', timeout: 7500}}); + expect(uspDataHandler.enabled).to.be.true; + }); + + it('should call setConsentData(null) on invalid CMP api', () => { + setConsentConfig({usp: {cmpApi: 'invalid'}}); + let hookRan = false; + requestBidsHook(() => { + hookRan = true; + }, {}); + expect(hookRan).to.be.true; + expect(uspDataHandler.ready).to.be.true; + }); }); describe('static consent string setConsentConfig value', () => { @@ -220,6 +246,32 @@ describe('consentManagement', function () { expect(consent).to.equal(testConsentData.uspString); sinon.assert.called(uspStub); }); + + it('should call uspDataHandler.setConsentData(null) on error', () => { + let hookRan = false; + uspStub = sinon.stub(window, '__uspapi').callsFake((...args) => { + args[2](null, false); + }); + requestBidsHook(() => { + hookRan = true; + }, {}); + expect(hookRan).to.be.true; + expect(uspDataHandler.ready).to.be.true; + expect(uspDataHandler.getConsentData()).to.equal(null); + }); + + it('should call uspDataHandler.setConsentData(null) on timeout', (done) => { + setConsentConfig({usp: {timeout: 10}}); + let hookRan = false; + uspStub = sinon.stub(window, '__uspapi').callsFake(() => {}); + requestBidsHook(() => { hookRan = true; }, {}); + setTimeout(() => { + expect(hookRan).to.be.true; + expect(uspDataHandler.ready).to.be.true; + expect(uspDataHandler.getConsentData()).to.equal(null); + done(); + }, 20) + }); }); describe('USPAPI workflow for iframed page', function () { @@ -366,6 +418,27 @@ describe('consentManagement', function () { expect(didHookReturn).to.be.true; expect(consent).to.equal(testConsentData.uspString); }); + + it('returns USP consent metadata', function () { + let testConsentData = { + uspString: '1NY' + }; + + uspapiStub = sinon.stub(window, '__uspapi').callsFake((...args) => { + args[2](testConsentData, true); + }); + + setConsentConfig(goodConfig); + requestBidsHook(() => { didHookReturn = true; }, {}); + + let consentMeta = uspDataHandler.getConsentMeta(); + + sinon.assert.notCalled(utils.logWarn); + sinon.assert.notCalled(utils.logError); + + expect(consentMeta.usp).to.equal(testConsentData.uspString); + expect(consentMeta.generatedAt).to.be.above(1644367751709); + }); }); }); }); diff --git a/test/spec/modules/consentManagement_spec.js b/test/spec/modules/consentManagement_spec.js index d7ae3c58a85..712e311e433 100644 --- a/test/spec/modules/consentManagement_spec.js +++ b/test/spec/modules/consentManagement_spec.js @@ -2,6 +2,7 @@ import { setConsentConfig, requestBidsHook, resetConsentData, userCMP, consentTi import { gdprDataHandler } from 'src/adapterManager.js'; import * as utils from 'src/utils.js'; import { config } from 'src/config.js'; +import 'src/prebid.js'; let expect = require('chai').expect; @@ -45,6 +46,18 @@ describe('consentManagement', function () { expect(userCMP).to.be.undefined; sinon.assert.calledOnce(utils.logWarn); }); + + it('should not produce any consent metadata', function() { + setConsentConfig(undefined) + let consentMetadata = gdprDataHandler.getConsentMeta(); + expect(consentMetadata).to.be.undefined; + sinon.assert.calledOnce(utils.logWarn); + }) + + it('should immediately look up consent data', () => { + setConsentConfig({gdpr: {cmpApi: 'invalid'}}); + expect(gdprDataHandler.ready).to.be.true; + }) }); describe('valid setConsentConfig value', function () { @@ -124,6 +137,11 @@ describe('consentManagement', function () { }); expect(gdprScope).to.be.equal(false); }); + + it('should enable gdprDataHandler', () => { + setConsentConfig({gdpr: {}}); + expect(gdprDataHandler.enabled).to.be.true; + }); }); describe('static consent string setConsentConfig value', () => { @@ -318,6 +336,14 @@ describe('consentManagement', function () { expect(consent).to.be.null; }); + it('should call gpdrDataHandler.setConsentData() when unknown CMP api is used', () => { + setConsentConfig({gdpr: {cmpApi: 'invalid'}}); + let hookRan = false; + requestBidsHook(() => { hookRan = true; }, {}); + expect(hookRan).to.be.true; + expect(gdprDataHandler.ready).to.be.true; + }) + it('should throw proper errors when CMP is not found', function () { setConsentConfig(goodConfigWithCancelAuction); @@ -329,6 +355,7 @@ describe('consentManagement', function () { sinon.assert.calledTwice(utils.logError); expect(didHookReturn).to.be.false; expect(consent).to.be.null; + expect(gdprDataHandler.ready).to.be.true; }); }); @@ -667,6 +694,33 @@ describe('consentManagement', function () { expect(consent.apiVersion).to.equal(2); }); + it('produces gdpr metadata', function () { + let testConsentData = { + tcString: 'abc12345234', + gdprApplies: true, + purposeOneTreatment: false, + eventStatus: 'tcloaded', + vendorData: { + tcString: 'abc12345234' + } + }; + cmpStub = sinon.stub(window, '__tcfapi').callsFake((...args) => { + args[2](testConsentData, true); + }); + + setConsentConfig(goodConfigWithAllowAuction); + + requestBidsHook(() => { + didHookReturn = true; + }, {}); + let consentMeta = gdprDataHandler.getConsentMeta(); + sinon.assert.notCalled(utils.logError); + expect(consentMeta.consentStringSize).to.be.above(0) + expect(consentMeta.gdprApplies).to.be.true; + expect(consentMeta.apiVersion).to.equal(2); + expect(consentMeta.generatedAt).to.be.above(1644367751709); + }); + it('performs lookup check and stores consentData for a valid existing user with additional consent', function () { let testConsentData = { tcString: 'abc12345234', @@ -703,6 +757,11 @@ describe('consentManagement', function () { setConsentConfig(goodConfigWithAllowAuction); + sinon.assert.calledOnce(utils.logWarn); + sinon.assert.notCalled(utils.logError); + + [utils.logWarn, utils.logError].forEach((stub) => stub.reset()); + requestBidsHook(() => { didHookReturn = true; }, { bidsBackHandler: () => bidsBackHandlerReturn = true }); @@ -713,6 +772,7 @@ describe('consentManagement', function () { expect(didHookReturn).to.be.false; expect(bidsBackHandlerReturn).to.be.true; expect(consent).to.be.null; + expect(gdprDataHandler.ready).to.be.true; }); it('allows the auction when CMP is unresponsive', (done) => { @@ -731,6 +791,7 @@ describe('consentManagement', function () { const consent = gdprDataHandler.getConsentData(); expect(consent.gdprApplies).to.be.true; expect(consent.consentString).to.be.undefined; + expect(gdprDataHandler.ready).to.be.true; done(); }, 20); }); diff --git a/test/spec/modules/conversantBidAdapter_spec.js b/test/spec/modules/conversantBidAdapter_spec.js index e871ab3f9c6..53169326d3b 100644 --- a/test/spec/modules/conversantBidAdapter_spec.js +++ b/test/spec/modules/conversantBidAdapter_spec.js @@ -2,6 +2,8 @@ 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'; describe('Conversant adapter tests', function() { const siteId = '108060'; @@ -119,7 +121,34 @@ describe('Conversant adapter tests', function() { bidId: 'bid005', bidderRequestId: '117d765b87bed38', auctionId: 'req000' - }]; + }, + // video with first party data + { + bidder: 'conversant', + params: { + site_id: siteId + }, + mediaTypes: { + video: { + context: 'instream', + mimes: ['video/mp4', 'video/x-flv'] + } + }, + ortb2Imp: { + instl: 1, + ext: { + data: { + pbadslot: 'homepage-top-rect' + } + } + }, + placementCode: 'pcode006', + transactionId: 'tx006', + bidId: 'bid006', + bidderRequestId: '117d765b87bed38', + auctionId: 'req000' + } + ]; const bidResponses = { body: { @@ -216,7 +245,7 @@ describe('Conversant adapter tests', function() { expect(payload).to.have.property('id', 'req000'); expect(payload).to.have.property('at', 1); expect(payload).to.have.property('imp'); - expect(payload.imp).to.be.an('array').with.lengthOf(6); + expect(payload.imp).to.be.an('array').with.lengthOf(7); expect(payload.imp[0]).to.have.property('id', 'bid000'); expect(payload.imp[0]).to.have.property('secure', 1); @@ -306,6 +335,16 @@ describe('Conversant adapter tests', function() { 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).to.have.property('site'); expect(payload.site).to.have.property('id', siteId); expect(payload.site).to.have.property('mobile').that.is.oneOf([0, 1]); @@ -321,6 +360,34 @@ describe('Conversant adapter tests', function() { expect(payload).to.not.have.property('user'); // there should be no user by default }); + it('Verify first party data', () => { + const bidderRequest = {refererInfo: {referer: 'http://test.com?a=b&c=123'}}; + const cfg = {ortb2: {site: {content: {series: 'MySeries', season: 'MySeason', episode: 3, title: 'MyTitle'}}}}; + config.setConfig(cfg); + const request = spec.buildRequests(bidRequests, bidderRequest); + const payload = request.data; + expect(payload.site).to.have.property('content'); + expect(payload.site.content).to.have.property('series'); + expect(payload.site.content).to.have.property('season'); + expect(payload.site.content).to.have.property('episode'); + expect(payload.site.content).to.have.property('title'); + config.resetConfig(); + }); + + it('Verify supply chain data', () => { + const bidderRequest = {refererInfo: {referer: 'http://test.com?a=b&c=123'}}; + const schain = {complete: 1, ver: '1.0', nodes: [{asi: 'bidderA.com', sid: '00001', hp: 1}]}; + const bidsWithSchain = bidRequests.map((bid) => { + return Object.assign({ + schain: schain + }, bid); + }); + const request = spec.buildRequests(bidsWithSchain, bidderRequest); + const payload = request.data; + expect(deepAccess(payload, 'source.ext.schain.nodes')).to.exist; + expect(payload.source.ext.schain.nodes[0].asi).equals(schain.nodes[0].asi); + }); + it('Verify override url', function() { const testUrl = 'https://someurl?name=value'; const request = spec.buildRequests([{params: {white_label_url: testUrl}}]); diff --git a/test/spec/modules/criteoBidAdapter_spec.js b/test/spec/modules/criteoBidAdapter_spec.js index aa995b3c9a0..8793d9351d4 100755 --- a/test/spec/modules/criteoBidAdapter_spec.js +++ b/test/spec/modules/criteoBidAdapter_spec.js @@ -430,7 +430,11 @@ describe('The Criteo bidding adapter', function () { bidder: 'criteo', adUnitCode: 'bid-123', transactionId: 'transaction-123', - sizes: [[728, 90]], + mediaTypes: { + banner: { + sizes: [[728, 90]] + } + }, params: {} }, ]; @@ -445,7 +449,11 @@ describe('The Criteo bidding adapter', function () { bidder: 'criteo', adUnitCode: 'bid-123', transactionId: 'transaction-123', - sizes: [[728, 90]], + mediaTypes: { + banner: { + sizes: [[728, 90]] + } + }, params: { zoneId: 123, publisherSubId: '123', @@ -473,7 +481,11 @@ describe('The Criteo bidding adapter', function () { it('should keep undefined sizes for non native banner', function () { const bidRequests = [ { - sizes: [[undefined, undefined]], + mediaTypes: { + banner: { + sizes: [[undefined, undefined]] + } + }, params: {}, }, ]; @@ -486,7 +498,11 @@ describe('The Criteo bidding adapter', function () { it('should keep undefined size for non native banner', function () { const bidRequests = [ { - sizes: [undefined, undefined], + mediaTypes: { + banner: { + sizes: [undefined, undefined] + } + }, params: {}, }, ]; @@ -499,7 +515,11 @@ describe('The Criteo bidding adapter', function () { it('should properly detect and get sizes of native sizeless banner', function () { const bidRequests = [ { - sizes: [[undefined, undefined]], + mediaTypes: { + banner: { + sizes: [[undefined, undefined]] + } + }, params: { nativeCallback: function() {} }, @@ -514,7 +534,11 @@ describe('The Criteo bidding adapter', function () { it('should properly detect and get size of native sizeless banner', function () { const bidRequests = [ { - sizes: [undefined, undefined], + mediaTypes: { + banner: { + sizes: [undefined, undefined] + } + }, params: { nativeCallback: function() {} }, @@ -585,7 +609,11 @@ describe('The Criteo bidding adapter', function () { bidder: 'criteo', adUnitCode: 'bid-123', transactionId: 'transaction-123', - sizes: [[728, 90]], + mediaTypes: { + banner: { + sizes: [[728, 90]] + } + }, params: { zoneId: 123, }, @@ -594,7 +622,11 @@ describe('The Criteo bidding adapter', function () { bidder: 'criteo', adUnitCode: 'bid-234', transactionId: 'transaction-234', - sizes: [[300, 250], [728, 90]], + mediaTypes: { + banner: { + sizes: [[300, 250], [728, 90]] + } + }, params: { networkId: 456, }, @@ -625,7 +657,11 @@ describe('The Criteo bidding adapter', function () { bidder: 'criteo', adUnitCode: 'bid-123', transactionId: 'transaction-123', - sizes: [[728, 90]], + mediaTypes: { + banner: { + sizes: [[728, 90]] + } + }, params: { zoneId: 123, }, @@ -647,7 +683,11 @@ describe('The Criteo bidding adapter', function () { bidder: 'criteo', adUnitCode: 'bid-123', transactionId: 'transaction-123', - sizes: [[728, 90]], + mediaTypes: { + banner: { + sizes: [[728, 90]] + } + }, params: { zoneId: 123, }, @@ -663,13 +703,42 @@ describe('The Criteo bidding adapter', function () { expect(request.data.user.uspIab).to.equal('1YNY'); }); + it('should properly build a request with schain object', function () { + const expectedSchain = { + someProperty: 'someValue' + }; + const bidRequests = [ + { + bidder: 'criteo', + schain: expectedSchain, + adUnitCode: 'bid-123', + transactionId: 'transaction-123', + mediaTypes: { + banner: { + sizes: [[728, 90]] + } + }, + params: { + zoneId: 123, + }, + }, + ]; + + const request = spec.buildRequests(bidRequests, bidderRequest); + expect(request.data.source.ext.schain).to.equal(expectedSchain); + }); + it('should properly build a request with if ccpa consent field is not provided', function () { const bidRequests = [ { bidder: 'criteo', adUnitCode: 'bid-123', transactionId: 'transaction-123', - sizes: [[728, 90]], + mediaTypes: { + banner: { + sizes: [[728, 90]] + } + }, params: { zoneId: 123, }, @@ -690,7 +759,7 @@ describe('The Criteo bidding adapter', function () { bidder: 'criteo', adUnitCode: 'bid-123', transactionId: 'transaction-123', - sizes: [[728, 90]], + sizes: [[640, 480]], mediaTypes: { video: { playerSize: [640, 480], @@ -717,6 +786,7 @@ describe('The Criteo bidding adapter', function () { expect(request.method).to.equal('POST'); const ortbRequest = request.data; 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']); expect(ortbRequest.slots[0].video.maxduration).to.equal(30); expect(ortbRequest.slots[0].video.api).to.deep.equal([1, 2]); @@ -734,7 +804,7 @@ describe('The Criteo bidding adapter', function () { bidder: 'criteo', adUnitCode: 'bid-123', transactionId: 'transaction-123', - sizes: [[728, 90]], + sizes: [[640, 480], [800, 600]], mediaTypes: { video: { playerSize: [[640, 480], [800, 600]], @@ -760,6 +830,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].sizes).to.deep.equal([]); expect(ortbRequest.slots[0].video.mimes).to.deep.equal(['video/mp4', 'video/x-flv']); expect(ortbRequest.slots[0].video.playersizes).to.deep.equal(['640x480', '800x600']); expect(ortbRequest.slots[0].video.maxduration).to.equal(30); @@ -778,7 +849,7 @@ describe('The Criteo bidding adapter', function () { bidder: 'criteo', adUnitCode: 'bid-123', transactionId: 'transaction-123', - sizes: [[728, 90]], + sizes: [[300, 250]], mediaTypes: { video: { playerSize: [ [300, 250] ], @@ -800,6 +871,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].sizes).to.deep.equal([]); expect(ortbRequest.slots[0].video.playersizes).to.deep.equal(['300x250']); expect(ortbRequest.slots[0].video.mimes).to.deep.equal(['video/mp4', 'video/MPV', 'video/H264', 'video/webm', 'video/ogg']); expect(ortbRequest.slots[0].video.minduration).to.equal(1); @@ -816,7 +888,11 @@ describe('The Criteo bidding adapter', function () { bidder: 'criteo', adUnitCode: 'bid-123', transactionId: 'transaction-123', - sizes: [[728, 90]], + mediaTypes: { + banner: { + sizes: [[728, 90]] + } + }, params: { zoneId: 123, }, @@ -838,7 +914,11 @@ describe('The Criteo bidding adapter', function () { bidder: 'criteo', adUnitCode: 'bid-123', transactionId: 'transaction-123', - sizes: [[728, 90]], + mediaTypes: { + banner: { + sizes: [[728, 90]] + } + }, params: { zoneId: 123 } @@ -863,7 +943,11 @@ describe('The Criteo bidding adapter', function () { bidder: 'criteo', adUnitCode: 'bid-123', transactionId: 'transaction-123', - sizes: [[728, 90]], + mediaTypes: { + banner: { + sizes: [[728, 90]] + } + }, params: { zoneId: 123, ext: { @@ -907,7 +991,11 @@ describe('The Criteo bidding adapter', function () { bidder: 'criteo', adUnitCode: 'bid-123', transactionId: 'transaction-123', - sizes: [[728, 90]], + mediaTypes: { + banner: { + sizes: [[728, 90]] + } + }, params: { zoneId: 123, ext: { diff --git a/test/spec/modules/currency_spec.js b/test/spec/modules/currency_spec.js index ccd205964a9..928c252943c 100644 --- a/test/spec/modules/currency_spec.js +++ b/test/spec/modules/currency_spec.js @@ -9,8 +9,11 @@ import { setConfig, addBidResponseHook, currencySupportEnabled, - currencyRates + currencyRates, + ready } from 'modules/currency.js'; +import {createBid} from '../../../src/bidfactory.js'; +import CONSTANTS from '../../../src/constants.json'; var assert = require('chai').assert; var expect = require('chai').expect; @@ -22,8 +25,13 @@ describe('currency', function () { let fn = sinon.spy(); + function makeBid(bidProps) { + return Object.assign(createBid(CONSTANTS.STATUS.GOOD), bidProps); + } + beforeEach(function () { fakeCurrencyFileServer = sinon.fakeServer.create(); + ready.reset(); }); afterEach(function () { @@ -286,7 +294,7 @@ describe('currency', function () { }); describe('currency.addBidResponseDecorator bidResponseQueue', function () { - it('not run until currency rates file is loaded', function () { + it('not run until currency rates file is loaded', function (done) { setConfig({}); fakeCurrencyFileServer.respondWith(JSON.stringify(getCurrencyRates())); @@ -296,21 +304,34 @@ describe('currency', function () { setConfig({ 'adServerCurrency': 'JPY' }); var marker = false; - addBidResponseHook(function() { + let promiseResolved = false; + addBidResponseHook(Object.assign(function() { marker = true; - }, 'elementId', bid); + }, { + bail: function (promise) { + promise.then(() => promiseResolved = true); + } + }), 'elementId', bid); expect(marker).to.equal(false); - fakeCurrencyFileServer.respond(); - expect(marker).to.equal(true); + setTimeout(() => { + expect(promiseResolved).to.be.false; + fakeCurrencyFileServer.respond(); + + setTimeout(() => { + expect(marker).to.equal(true); + expect(promiseResolved).to.be.true; + done(); + }); + }); }); }); describe('currency.addBidResponseDecorator', function () { it('should leave bid at 1 when currency support is not enabled and fromCurrency is USD', function () { setConfig({}); - var bid = { 'cpm': 1, 'currency': 'USD' }; + var bid = makeBid({ 'cpm': 1, 'currency': 'USD' }); var innerBid; addBidResponseHook(function(adCodeId, bid) { innerBid = bid; @@ -321,7 +342,7 @@ describe('currency', function () { it('should result in NO_BID when currency support is not enabled and fromCurrency is not USD', function () { setConfig({}); - var bid = { 'cpm': 1, 'currency': 'GBP' }; + var bid = makeBid({ 'cpm': 1, 'currency': 'GBP' }); var innerBid; addBidResponseHook(function(adCodeId, bid) { innerBid = bid; @@ -333,7 +354,7 @@ describe('currency', function () { setConfig({ 'adServerCurrency': 'USD' }); - var bid = { 'cpm': 1, 'currency': 'USD' }; + var bid = makeBid({ 'cpm': 1, 'currency': 'USD' }); var innerBid; addBidResponseHook(function(adCodeId, bid) { innerBid = bid; @@ -348,7 +369,7 @@ describe('currency', function () { fakeCurrencyFileServer.respondWith(JSON.stringify(getCurrencyRates())); setConfig({ 'adServerCurrency': 'JPY' }); fakeCurrencyFileServer.respond(); - var bid = { 'cpm': 1, 'currency': 'ABC' }; + var bid = makeBid({ 'cpm': 1, 'currency': 'ABC' }); var innerBid; addBidResponseHook(function(adCodeId, bid) { innerBid = bid; @@ -360,7 +381,7 @@ describe('currency', function () { fakeCurrencyFileServer.respondWith(JSON.stringify(getCurrencyRates())); setConfig({ 'adServerCurrency': 'ABC' }); fakeCurrencyFileServer.respond(); - var bid = { 'cpm': 1, 'currency': 'GBP' }; + var bid = makeBid({ 'cpm': 1, 'currency': 'GBP' }); var innerBid; addBidResponseHook(function(adCodeId, bid) { innerBid = bid; @@ -372,7 +393,7 @@ describe('currency', function () { fakeCurrencyFileServer.respondWith(JSON.stringify(getCurrencyRates())); setConfig({ 'adServerCurrency': 'JPY' }); fakeCurrencyFileServer.respond(); - var bid = { 'cpm': 1, 'currency': 'JPY' }; + var bid = makeBid({ 'cpm': 1, 'currency': 'JPY' }); var innerBid; addBidResponseHook(function(adCodeId, bid) { innerBid = bid; @@ -385,7 +406,7 @@ describe('currency', function () { fakeCurrencyFileServer.respondWith(JSON.stringify(getCurrencyRates())); setConfig({ 'adServerCurrency': 'GBP' }); fakeCurrencyFileServer.respond(); - var bid = { 'cpm': 1, 'currency': 'USD' }; + var bid = makeBid({ 'cpm': 1, 'currency': 'USD' }); var innerBid; addBidResponseHook(function(adCodeId, bid) { innerBid = bid; @@ -398,7 +419,7 @@ describe('currency', function () { fakeCurrencyFileServer.respondWith(JSON.stringify(getCurrencyRates())); setConfig({ 'adServerCurrency': 'GBP' }); fakeCurrencyFileServer.respond(); - var bid = { 'cpm': 1, 'currency': 'CNY' }; + var bid = makeBid({ 'cpm': 1, 'currency': 'CNY' }); var innerBid; addBidResponseHook(function(adCodeId, bid) { innerBid = bid; @@ -411,7 +432,7 @@ describe('currency', function () { fakeCurrencyFileServer.respondWith(JSON.stringify(getCurrencyRates())); setConfig({ 'adServerCurrency': 'CNY' }); fakeCurrencyFileServer.respond(); - var bid = { 'cpm': 1, 'currency': 'JPY' }; + var bid = makeBid({ 'cpm': 1, 'currency': 'JPY' }); var innerBid; addBidResponseHook(function(adCodeId, bid) { innerBid = bid; diff --git a/test/spec/modules/cwireBidAdapter_spec.js b/test/spec/modules/cwireBidAdapter_spec.js index 3f922fc1471..b21e73e1561 100644 --- a/test/spec/modules/cwireBidAdapter_spec.js +++ b/test/spec/modules/cwireBidAdapter_spec.js @@ -1,5 +1,6 @@ import { expect } from 'chai'; import * as utils from '../../../src/utils.js'; +import { config } from '../../../src/config.js'; import { spec, CW_PAGE_VIEW_ID, @@ -24,7 +25,6 @@ const BID_DEFAULTS = { params: { placementId: 123456, pageId: 777, - adUnitElementId: 'target-div' }, sizes: [[300, 250], [1, 1]], }; @@ -85,6 +85,7 @@ describe('C-WIRE bid adapter', () => { afterEach(() => { sandbox.restore(); + config.resetConfig(); }); // START TESTING @@ -119,31 +120,149 @@ describe('C-WIRE bid adapter', () => { bid01.params.pageId = '3320'; expect(spec.isBidRequestValid(bid01)).to.equal(false); }); + }); + + describe('C-WIRE - buildRequests()', function () { + it('creates a valid request', function () { + const bid01 = new BidRequestBuilder({ + mediaTypes: { + banner: { + sizes: [[1, 1]], + } + } + }).withParams({ + cwcreative: 54321, + cwapikey: 'xxx-xxx-yyy-zzz-uuid', + refgroups: 'group_1', + }).build(); + + const bidderRequest01 = new BidderRequestBuilder().build(); + + const requests = spec.buildRequests([bid01], bidderRequest01); - it('should use params.adUnitElementId if provided', function () { + expect(requests.data.slots.length).to.equal(1); + expect(requests.data.cwid).to.be.null; + expect(requests.data.cwid).to.be.null; + expect(requests.data.slots[0].sizes[0]).to.equal('1x1'); + expect(requests.data.cwcreative).to.equal(54321); + expect(requests.data.cwapikey).to.equal('xxx-xxx-yyy-zzz-uuid'); + expect(requests.data.refgroups[0]).to.equal('group_1'); + }); + + it('creates a valid request - read debug params from second bid', function () { const bid01 = new BidRequestBuilder().withParams().build(); - expect(spec.isBidRequestValid(bid01)).to.equal(true); - expect(bid01.params.adUnitElementId).to.exist; - expect(bid01.params.adUnitElementId).to.equal('target-div'); + const bid02 = new BidRequestBuilder({ + mediaTypes: { + banner: { + sizes: [[1, 1]], + } + } + }).withParams({ + cwcreative: 1234, + cwapikey: 'api_key_5', + refgroups: 'group_5', + }).build(); + + const bidderRequest01 = new BidderRequestBuilder().build(); + + const requests = spec.buildRequests([bid01, bid02], bidderRequest01); + + expect(requests.data.slots.length).to.equal(2); + expect(requests.data.cwid).to.be.null; + expect(requests.data.cwid).to.be.null; + expect(requests.data.cwcreative).to.equal(1234); + expect(requests.data.cwapikey).to.equal('api_key_5'); + expect(requests.data.refgroups[0]).to.equal('group_5'); }); - it('should use default adUnitCode if no adUnitElementId provided', function () { - const bid01 = new BidRequestBuilder().withParams({}, ['adUnitElementId']).build(); - expect(spec.isBidRequestValid(bid01)).to.equal(true); - expect(bid01.params.adUnitElementId).to.exist; - expect(bid01.params.adUnitElementId).to.equal('original-div'); + it('creates a valid request - read debug params from first bid, ignore second', function () { + const bid01 = new BidRequestBuilder() + .withParams({ + cwcreative: 33, + cwapikey: 'api_key_33', + refgroups: 'group_33', + }).build(); + + const bid02 = new BidRequestBuilder() + .withParams({ + cwcreative: 1234, + cwapikey: 'api_key_5', + refgroups: 'group_5', + }).build(); + + const bidderRequest01 = new BidderRequestBuilder().build(); + + const requests = spec.buildRequests([bid01, bid02], bidderRequest01); + + expect(requests.data.slots.length).to.equal(2); + expect(requests.data.cwid).to.be.null; + expect(requests.data.cwid).to.be.null; + expect(requests.data.cwcreative).to.equal(33); + expect(requests.data.cwapikey).to.equal('api_key_33'); + expect(requests.data.refgroups[0]).to.equal('group_33'); }); - }); - describe('C-WIRE - buildRequests()', function () { - it('creates a valid request', function () { + it('creates a valid request - read debug params from 3 different slots', function () { + const bid01 = new BidRequestBuilder() + .withParams({ + cwcreative: 33, + }).build(); + + const bid02 = new BidRequestBuilder() + .withParams({ + cwapikey: 'api_key_5', + }).build(); + + const bid03 = new BidRequestBuilder() + .withParams({ + refgroups: 'group_5', + }).build(); + const bidderRequest01 = new BidderRequestBuilder().build(); + + const requests = spec.buildRequests([bid01, bid02, bid03], bidderRequest01); + + expect(requests.data.slots.length).to.equal(3); + expect(requests.data.cwid).to.be.null; + expect(requests.data.cwid).to.be.null; + expect(requests.data.cwcreative).to.equal(33); + expect(requests.data.cwapikey).to.equal('api_key_5'); + expect(requests.data.refgroups[0]).to.equal('group_5'); + }); + + it('creates a valid request - config is overriden by URL params', function () { // for whatever reason stub for getWindowLocation does not work // so this was the closest way to test for get params const params = sandbox.stub(utils, 'getParameterByName'); - params.withArgs('cwgroups').returns('group_1'); - params.withArgs('cwcreative').returns('54321'); + params.withArgs('cwgroups').returns('group_2'); + params.withArgs('cwcreative').returns('654321'); + + const bid01 = new BidRequestBuilder({ + mediaTypes: { + banner: { + sizes: [[1, 1]], + } + } + }).withParams({ + cwcreative: 54321, + cwapikey: 'xxx-xxx-yyy-zzz', + refgroups: 'group_1', + }).build(); + + const bidderRequest01 = new BidderRequestBuilder().build(); + + const requests = spec.buildRequests([bid01], bidderRequest01); + expect(requests.data.slots.length).to.equal(1); + expect(requests.data.cwid).to.be.null; + expect(requests.data.cwid).to.be.null; + expect(requests.data.slots[0].sizes[0]).to.equal('1x1'); + expect(requests.data.cwcreative).to.equal(654321); + expect(requests.data.cwapikey).to.equal('xxx-xxx-yyy-zzz'); + expect(requests.data.refgroups[0]).to.equal('group_2'); + }); + + it('creates a valid request - if params are not set, null or empty array are sent to the API', function () { const bid01 = new BidRequestBuilder({ mediaTypes: { banner: { @@ -158,9 +277,11 @@ describe('C-WIRE bid adapter', () => { expect(requests.data.slots.length).to.equal(1); expect(requests.data.cwid).to.be.null; + expect(requests.data.cwid).to.be.null; expect(requests.data.slots[0].sizes[0]).to.equal('1x1'); - expect(requests.data.cwcreative).to.equal('54321'); - expect(requests.data.refgroups[0]).to.equal('group_1'); + expect(requests.data.cwcreative).to.equal(null); + expect(requests.data.cwapikey).to.equal(null); + expect(requests.data.refgroups.length).to.equal(0); }); }); diff --git a/test/spec/modules/dacIdSystem_spec.js b/test/spec/modules/dacIdSystem_spec.js new file mode 100644 index 00000000000..d78b4a69000 --- /dev/null +++ b/test/spec/modules/dacIdSystem_spec.js @@ -0,0 +1,51 @@ +import { dacIdSystemSubmodule, storage, cookieKey } from 'modules/dacIdSystem.js'; + +const DACID_DUMMY_VALUE = 'dacIdTest'; +const DACID_DUMMY_OBJ = { + dacId: DACID_DUMMY_VALUE +}; + +describe('dacId module', function () { + let getCookieStub; + + beforeEach(function (done) { + getCookieStub = sinon.stub(storage, 'getCookie'); + done(); + }); + + afterEach(function () { + getCookieStub.restore(); + }); + + const cookieTestCasesForEmpty = [ + undefined, + null, + '' + ] + + describe('getId()', function () { + it('should return the uid when it exists in cookie', function () { + getCookieStub.withArgs(cookieKey).returns(DACID_DUMMY_VALUE); + const id = dacIdSystemSubmodule.getId(); + expect(id).to.be.deep.equal({id: {dacId: DACID_DUMMY_VALUE}}); + }); + + cookieTestCasesForEmpty.forEach(testCase => it('should return the uid when it not exists in cookie', function () { + getCookieStub.withArgs(cookieKey).returns(testCase); + const id = dacIdSystemSubmodule.getId(); + expect(id).to.be.deep.equal(undefined); + })); + }); + + describe('decode()', function () { + it('should return the uid when it exists in cookie', function () { + const decoded = dacIdSystemSubmodule.decode(DACID_DUMMY_OBJ); + expect(decoded).to.be.deep.equal({dacId: {id: DACID_DUMMY_VALUE}}); + }); + + it('should return the undefined when decode id is not "string"', function () { + const decoded = dacIdSystemSubmodule.decode(1); + expect(decoded).to.equal(undefined); + }); + }); +}); diff --git a/test/spec/modules/debugging_mod_spec.js b/test/spec/modules/debugging_mod_spec.js new file mode 100644 index 00000000000..79866d023e9 --- /dev/null +++ b/test/spec/modules/debugging_mod_spec.js @@ -0,0 +1,451 @@ +import {expect} from 'chai'; +import {BidInterceptor} from '../../../modules/debugging/bidInterceptor.js'; +import {bidderBidInterceptor} from '../../../modules/debugging/index.js'; +import {pbsBidInterceptor} from '../../../modules/debugging/pbsInterceptor.js'; + +describe('bid interceptor', () => { + let interceptor, mockSetTimeout; + beforeEach(() => { + mockSetTimeout = sinon.stub().callsFake((fn) => fn()); + interceptor = new BidInterceptor({setTimeout: mockSetTimeout}); + }); + + function setRules(...rules) { + interceptor.updateConfig({ + intercept: rules + }); + } + + describe('serializeConfig', () => { + Object.entries({ + regexes: /pat/, + functions: () => ({}) + }).forEach(([test, arg]) => { + it(`should filter out ${test}`, () => { + const valid = [{key1: 'value'}, {key2: 'value'}]; + const ser = interceptor.serializeConfig([...valid, {outer: {inner: arg}}]); + expect(ser).to.eql(valid); + }); + }); + }); + + describe('match()', () => { + Object.entries({ + value: {key: 'value'}, + regex: {key: /^value$/}, + 'function': (o) => o.key === 'value' + }).forEach(([test, matcher]) => { + describe(`by ${test}`, () => { + it('should work on matching top-level properties', () => { + setRules({when: matcher}); + const rule = interceptor.match({key: 'value'}); + expect(rule).to.not.eql(null); + }); + + it('should work on matching nested properties', () => { + setRules({when: {outer: {inner: matcher}}}); + const rule = interceptor.match({outer: {inner: {key: 'value'}}}); + expect(rule).to.not.eql(null); + }); + + it('should not work on non-matching inputs', () => { + setRules({when: matcher}); + expect(interceptor.match({key: 'different-value'})).to.not.be.ok; + expect(interceptor.match({differentKey: 'value'})).to.not.be.ok; + }); + }); + }); + + it('should respect rule order', () => { + setRules({when: {key: 'value'}}, {when: {}}, {when: {}}); + const rule = interceptor.match({}); + expect(rule.no).to.equal(2); + }); + + it('should pass extra arguments to property function matchers', () => { + let matchDef = { + key: sinon.stub(), + outer: {inner: {key: sinon.stub()}} + }; + const extraArgs = [{}, {}]; + setRules({when: matchDef}); + interceptor.match({key: {}, outer: {inner: {key: {}}}}, ...extraArgs); + [matchDef.key, matchDef.outer.inner.key].forEach((fn) => { + expect(fn.calledOnceWith(sinon.match.any, ...extraArgs.map(sinon.match.same))).to.be.true; + }); + }); + + it('should pass extra arguments to single-function matcher', () => { + let matchDef = sinon.stub(); + setRules({when: matchDef}); + const args = [{}, {}, {}]; + interceptor.match(...args); + expect(matchDef.calledOnceWith(...args.map(sinon.match.same))).to.be.true; + }); + }); + + describe('rule', () => { + function matchingRule({replace, options}) { + setRules({when: {}, then: replace, options: options}); + return interceptor.match({}); + } + + describe('.replace()', () => { + const REQUIRED_KEYS = [ + // https://docs.prebid.org/dev-docs/bidder-adaptor.html#bidder-adaptor-Interpreting-the-Response + 'requestId', 'cpm', 'currency', 'width', 'height', 'ttl', + 'creativeId', 'netRevenue', 'meta', 'ad' + ]; + it('should include required bid response keys by default', () => { + expect(matchingRule({}).replace({})).to.include.keys(REQUIRED_KEYS); + }); + + Object.entries({ + value: {key: 'value'}, + 'function': () => ({key: 'value'}) + }).forEach(([test, replDef]) => { + describe(`by ${test}`, () => { + it('should merge top-level properties with replace definition', () => { + const result = matchingRule({replace: replDef}).replace({}); + expect(result).to.include.keys(REQUIRED_KEYS); + expect(result.key).to.equal('value'); + }); + + it('should merge nested properties with replace definition', () => { + const result = matchingRule({replace: {outer: {inner: replDef}}}).replace({}); + expect(result).to.include.keys(REQUIRED_KEYS); + expect(result.outer.inner).to.eql({key: 'value'}); + }); + }); + }); + + it('should pass extra arguments to single function replacer', () => { + const replDef = sinon.stub(); + const args = [{}, {}, {}]; + matchingRule({replace: replDef}).replace(...args); + expect(replDef.calledOnceWith(...args.map(sinon.match.same))).to.be.true; + }); + + it('should pass extra arguments to function property replacers', () => { + const replDef = { + key: sinon.stub(), + outer: {inner: {key: sinon.stub()}} + }; + const args = [{}, {}, {}]; + matchingRule({replace: replDef}).replace(...args); + [replDef.key, replDef.outer.inner.key].forEach((repl) => { + expect(repl.calledOnceWith(...args.map(sinon.match.same))).to.be.true; + }); + }); + }); + + describe('.options', () => { + it('should include default rule options', () => { + const optDef = {someOption: 'value'}; + const ruleOptions = matchingRule({options: optDef}).options; + expect(ruleOptions).to.include(optDef); + expect(ruleOptions).to.include(interceptor.DEFAULT_RULE_OPTIONS); + }); + + it('should override defaults', () => { + const optDef = {delay: 123}; + const ruleOptions = matchingRule({options: optDef}).options; + expect(ruleOptions).to.eql(optDef); + }); + }); + }); + + describe('intercept()', () => { + let done, addBid; + + function intercept(args = {}) { + const bidRequest = {bids: args.bids || []}; + return interceptor.intercept(Object.assign({bidRequest, done, addBid}, args)); + } + + beforeEach(() => { + done = sinon.spy(); + addBid = sinon.spy(); + }); + + describe('on no match', () => { + it('should return untouched bids and bidRequest', () => { + const bids = [{}, {}]; + const bidRequest = {}; + const result = intercept({bids, bidRequest}); + expect(result.bids).to.equal(bids); + expect(result.bidRequest).to.equal(bidRequest); + }); + + it('should call done() immediately', () => { + intercept(); + expect(done.calledOnce).to.be.true; + expect(mockSetTimeout.args[0][1]).to.equal(0); + }); + + it('should not call addBid', () => { + intercept(); + expect(addBid.called).to.not.be.ok; + }); + }); + + describe('on match', () => { + let match1, match2, repl1, repl2; + const DELAY_1 = 123; + const DELAY_2 = 321; + const REQUEST = { + bids: [ + {id: 1, match: false}, + {id: 2, match: 1}, + {id: 3, match: 2} + ] + }; + + beforeEach(() => { + match1 = sinon.stub().callsFake((bid) => bid.match === 1); + match2 = sinon.stub().callsFake((bid) => bid.match === 2); + repl1 = sinon.stub().returns({replace: 1}); + repl2 = sinon.stub().returns({replace: 2}); + setRules( + {when: match1, then: repl1, options: {delay: DELAY_1}}, + {when: match2, then: repl2, options: {delay: DELAY_2}}, + ); + }); + + it('should return only non-matching bids', () => { + const {bids, bidRequest} = intercept({bidRequest: REQUEST}); + expect(bids).to.eql([REQUEST.bids[0]]); + expect(bidRequest.bids).to.eql([REQUEST.bids[0]]); + }); + + it('should call addBid for each matching bid', () => { + intercept({bidRequest: REQUEST}); + expect(addBid.callCount).to.equal(2); + expect(addBid.calledWith(sinon.match({replace: 1, isDebug: true}), REQUEST.bids[1])).to.be.true; + expect(addBid.calledWith(sinon.match({replace: 2, isDebug: true}), REQUEST.bids[2])).to.be.true; + [DELAY_1, DELAY_2].forEach((delay) => { + expect(mockSetTimeout.calledWith(sinon.match.any, delay)).to.be.true; + }); + }); + + it('should call done()', () => { + intercept({bidRequest: REQUEST}); + expect(done.calledOnce).to.be.true; + }); + + it('should pass bid and bidRequest to match and replace functions', () => { + intercept({bidRequest: REQUEST}); + Object.entries({ + 1: [match1, repl1], + 2: [match2, repl2] + }).forEach(([index, fns]) => { + fns.forEach((fn) => { + expect(fn.calledWith(REQUEST.bids[index], REQUEST)).to.be.true; + }); + }); + }); + }); + }); +}); + +describe('bidderBidInterceptor', () => { + let next, interceptBids, onCompletion, interceptResult, done, addBid; + + function interceptorArgs({spec = {}, bids = [], bidRequest = {}, ajax = {}, wrapCallback = {}, cbs = {}} = {}) { + return [next, interceptBids, spec, bids, bidRequest, ajax, wrapCallback, Object.assign({onCompletion}, cbs)]; + } + + beforeEach(() => { + next = sinon.spy(); + interceptBids = sinon.stub().callsFake((opts) => { + done = opts.done; + addBid = opts.addBid; + return interceptResult; + }); + onCompletion = sinon.spy(); + interceptResult = {bids: [], bidRequest: {}}; + }); + + it('should pass to interceptBid an addBid that triggers onBid', () => { + const onBid = sinon.spy(); + bidderBidInterceptor(...interceptorArgs({cbs: {onBid}})); + const bid = {}; + addBid(bid); + expect(onBid.calledWith(sinon.match.same(bid))).to.be.true; + }); + + describe('with no remaining bids', () => { + it('should pass a done callback that triggers onCompletion', () => { + bidderBidInterceptor(...interceptorArgs()); + expect(onCompletion.calledOnce).to.be.false; + interceptBids.args[0][0].done(); + expect(onCompletion.calledOnce).to.be.true; + }); + + it('should not call next()', () => { + bidderBidInterceptor(...interceptorArgs()); + expect(next.called).to.be.false; + }); + }); + + describe('with remaining bids', () => { + const REMAINING_BIDS = [{id: 1}, {id: 2}]; + beforeEach(() => { + interceptResult = {bids: REMAINING_BIDS, bidRequest: {bids: REMAINING_BIDS}}; + }); + + it('should call next', () => { + const callbacks = { + onResponse: {}, + onRequest: {}, + onBid: {} + }; + const args = interceptorArgs({cbs: callbacks}); + const expectedNextArgs = [ + args[2], + interceptResult.bids, + interceptResult.bidRequest, + ...args.slice(5, args.length - 1), + ].map(sinon.match.same) + .concat([sinon.match({ + onResponse: sinon.match.same(callbacks.onResponse), + onRequest: sinon.match.same(callbacks.onRequest), + onBid: sinon.match.same(callbacks.onBid) + })]); + bidderBidInterceptor(...args); + expect(next.calledOnceWith(...expectedNextArgs)).to.be.true; + }); + + it('should trigger onCompletion once both interceptBids.done and next.cbs.onCompletion are called ', () => { + bidderBidInterceptor(...interceptorArgs()); + expect(onCompletion.calledOnce).to.be.false; + next.args[0][next.args[0].length - 1].onCompletion(); + expect(onCompletion.calledOnce).to.be.false; + done(); + expect(onCompletion.calledOnce).to.be.true; + }); + }); +}); + +describe('pbsBidInterceptor', () => { + const EMPTY_INT_RES = {bids: [], bidRequest: {bids: []}}; + let next, interceptBids, s2sBidRequest, bidRequests, ajax, onResponse, onError, onBid, interceptResults, + addBids, dones, reqIdx; + + beforeEach(() => { + reqIdx = 0; + [addBids, dones] = [[], []]; + next = sinon.spy(); + ajax = sinon.spy(); + onResponse = sinon.spy(); + onError = sinon.spy(); + onBid = sinon.spy(); + interceptBids = sinon.stub().callsFake((opts) => { + addBids.push(opts.addBid); + dones.push(opts.done); + return interceptResults[reqIdx++]; + }); + s2sBidRequest = {}; + bidRequests = [{bids: []}, {bids: []}]; + interceptResults = [EMPTY_INT_RES, EMPTY_INT_RES]; + }); + + function callInterceptor() { + return pbsBidInterceptor(next, interceptBids, s2sBidRequest, bidRequests, ajax, {onResponse, onError, onBid}); + } + + it('passes addBids that trigger onBid', () => { + callInterceptor(); + bidRequests.forEach((_, i) => { + const bid = {adUnitCode: i, prop: i}; + const bidRequest = {req: i}; + addBids[i](bid, bidRequest); + expect(onBid.calledWith({adUnit: i, bid: sinon.match(bid)})); + }); + }); + + describe('on no match', () => { + it('should not call next', () => { + callInterceptor(); + expect(next.called).to.be.false; + }); + + it('should pass done callbacks that trigger a dummy onResponse once they all run', () => { + callInterceptor(); + expect(onResponse.called).to.be.false; + bidRequests.forEach((_, i) => { + dones[i](); + expect(onResponse.called).to.equal(i === bidRequests.length - 1); + }); + expect(onResponse.calledWith(true, [])).to.be.true; + }); + }); + + describe('on match', () => { + let matchingBids; + beforeEach(() => { + matchingBids = [ + [{bidId: 1, matching: true}, {bidId: 2, matching: true}], + [], + [{bidId: 3, matching: true}] + ]; + interceptResults = matchingBids.map((bids) => ({bids, bidRequest: {bids}})); + s2sBidRequest = { + ad_units: [ + {bids: [{bid_id: 1, matching: true}, {bid_id: 3, matching: true}, {bid_id: 100}, {bid_id: 101}]}, + {bids: [{bid_id: 2, matching: true}, {bid_id: 110}, {bid_id: 111}]}, + {bids: [{bid_id: 120}]} + ] + }; + bidRequests = matchingBids.map((mBids, i) => [ + {bidId: 100 + (i * 10)}, + {bidId: 101 + (i * 10)}, + ...mBids + ]); + }); + + it('should call next', () => { + callInterceptor(); + expect(next.calledOnceWith( + sinon.match.any, + sinon.match.any, + ajax, + sinon.match({ + onError, + onBid + }) + )).to.be.true; + }); + + it('should filter out intercepted bids from s2sBidRequest', () => { + callInterceptor(); + const interceptedS2SReq = next.args[0][0]; + const allMatching = interceptedS2SReq.ad_units.every((u) => u.bids.length > 0 && u.bids.every((b) => b.matching)); + expect(allMatching).to.be.true; + }); + + it('should pass bidRequests as returned by interceptBids', () => { + callInterceptor(); + const passedBidReqs = next.args[0][1]; + interceptResults + .filter((r) => r.bids.length > 0) + .forEach(({bidRequest}, i) => { + expect(passedBidReqs[i]).to.equal(bidRequest); + }); + }); + + it('should pass an onResponse that triggers original onResponse only once all intercept dones are called', () => { + callInterceptor(); + const interceptedOnResponse = next.args[0][next.args[0].length - 1].onResponse; + expect(onResponse.called).to.be.false; + const responseArgs = ['dummy', 'args']; + interceptedOnResponse(...responseArgs); + expect(onResponse.called).to.be.false; + dones.forEach((f, i) => { + f(); + expect(onResponse.called).to.equal(i === dones.length - 1); + }); + expect(onResponse.calledOnceWith(...responseArgs)).to.be.true; + }); + }); +}); diff --git a/test/spec/modules/deepintentDpesIdsystem_spec.js b/test/spec/modules/deepintentDpesIdsystem_spec.js index 7ea5553393c..4c26b118a98 100644 --- a/test/spec/modules/deepintentDpesIdsystem_spec.js +++ b/test/spec/modules/deepintentDpesIdsystem_spec.js @@ -1,5 +1,5 @@ import { expect } from 'chai'; -import find from 'core-js-pure/features/array/find.js'; +import {find} from 'src/polyfill.js'; import { storage, deepintentDpesSubmodule } from 'modules/deepintentDpesIdSystem.js'; import { init, requestBidsHook, setSubmoduleRegistry } from 'modules/userId/index.js'; import { config } from 'src/config.js'; diff --git a/test/spec/modules/dfpAdServerVideo_spec.js b/test/spec/modules/dfpAdServerVideo_spec.js index eaffca01e06..300e2104fae 100644 --- a/test/spec/modules/dfpAdServerVideo_spec.js +++ b/test/spec/modules/dfpAdServerVideo_spec.js @@ -410,6 +410,59 @@ describe('The DFP video support module', function () { expect(customParams).to.have.property('hb_cache_id', 'def'); }); + it('should keep the url protocol, host, and pathname when using url and params', function () { + const url = parse(buildDfpVideoUrl({ + adUnit: adUnit, + bid: bid, + url: 'http://video.adserver.example/ads?sz=640x480&iu=/123/aduniturl&impl=s', + params: { + cust_params: { + hb_rand: 'random' + } + } + })); + + expect(url.protocol).to.equal('http:'); + expect(url.host).to.equal('video.adserver.example'); + expect(url.pathname).to.equal('/ads'); + }); + + it('should append to the url size param', () => { + const url = parse(buildDfpVideoUrl({ + adUnit: adUnit, + bid: bid, + url: 'http://video.adserver.example/ads?sz=360x240&iu=/123/aduniturl&impl=s', + params: { + cust_params: { + hb_rand: 'random' + } + } + })); + + const queryObject = utils.parseQS(url.query); + expect(queryObject.sz).to.equal('360x240|640x480'); + }); + + it('should append to the existing url cust params', () => { + const url = parse(buildDfpVideoUrl({ + adUnit: adUnit, + bid: bid, + url: 'http://video.adserver.example/ads?sz=360x240&iu=/123/aduniturl&impl=s&cust_params=existing_key%3Dexisting_value%26other_key%3Dother_value', + params: { + cust_params: { + hb_rand: 'random' + } + } + })); + + const queryObject = utils.parseQS(url.query); + const customParams = utils.parseQS('?' + decodeURIComponent(queryObject.cust_params)); + + expect(customParams).to.have.property('existing_key', 'existing_value'); + expect(customParams).to.have.property('other_key', 'other_value'); + expect(customParams).to.have.property('hb_rand', 'random'); + }); + describe('adpod unit tests', function () { let amStub; let amGetAdUnitsStub; diff --git a/test/spec/modules/displayioBidAdapter_spec.js b/test/spec/modules/displayioBidAdapter_spec.js new file mode 100644 index 00000000000..dfeb67fb467 --- /dev/null +++ b/test/spec/modules/displayioBidAdapter_spec.js @@ -0,0 +1,239 @@ +import { expect } from 'chai' +import {spec} from 'modules/displayioBidAdapter.js' + +describe('Displayio adapter', function () { + const BIDDER = 'displayio' + const bidRequests = [{ + bidId: 'bidId_001', + bidder: BIDDER, + adUnitCode: 'adUnit_001', + auctionId: 'auctionId_001', + bidderRequestId: 'bidderRequestId_001', + mediaTypes: { + banner: { + sizes: [[320, 480]] + }, + video: { + sizes: [[360, 640]] + }, + }, + params: { + siteId: 1, + placementId: 1, + adsSrvDomain: 'adsSrvDomain', + cdnDomain: 'cdnDomain', + } + }] + const bidderRequest = { + refererInfo: { + referer: 'testprebid.com' + } + } + + describe('isBidRequestValid', function () { + it('should return true when required params found', function() { + const validBid = spec.isBidRequestValid(bidRequests[0]) + expect(validBid).to.be.true + }) + + const bidRequestsNoParams = [{ + bidder: BIDDER, + }] + it('should not validate without params', function () { + const request = spec.isBidRequestValid(bidRequestsNoParams, bidderRequest) + expect(request).to.be.false + }) + + const noSiteId = { + bidder: BIDDER, + params: { + placementId: 1, + adsSrvDomain: 'adsSrvDomain', + cdnDomain: 'cdnDomain', + } + } + it('should not validate without siteId', function() { + const invalidBid = spec.isBidRequestValid(noSiteId) + expect(invalidBid).to.be.false + }) + + const noPlacementId = { + bidder: BIDDER, + params: { + siteId: 1, + adsSrvDomain: 'adsSrvDomain', + cdnDomain: 'cdnDomain', + } + } + it('should not validate without placementId', function() { + const invalidBid = spec.isBidRequestValid(noPlacementId) + expect(invalidBid).to.be.false + }) + + const noAdsSrvDomain = { + bidder: BIDDER, + params: { + siteId: 1, + placementId: 1, + cdnDomain: 'cdnDomain', + } + } + it('should not validate without adsSrvDomain', function() { + const invalidBid = spec.isBidRequestValid(noAdsSrvDomain) + expect(invalidBid).to.be.false + }) + + const noCdnDomain = { + bidder: BIDDER, + params: { + siteId: 1, + placementId: 1, + adsSrvDomain: 'adsSrvDomain', + } + } + it('should not validate without cdnDomain', function() { + const invalidBid = spec.isBidRequestValid(noCdnDomain) + expect(invalidBid).to.be.false + }) + }) + + describe('buildRequests', function () { + it('should build request', function() { + const request = spec.buildRequests(bidRequests, bidderRequest) + expect(request).to.not.be.empty + }) + + it('sends bid request to the endpoint via POST', function () { + const request = spec.buildRequests(bidRequests, bidderRequest) + expect(request[0].method).to.equal('POST') + }) + + it('sends all bid parameters', function () { + const request = spec.buildRequests(bidRequests, bidderRequest) + expect(request[0]).to.have.keys(['headers', 'data', 'method', 'url']) + }) + + it('should not crash when there is no media types', function () { + const bidRequestsNoMediaTypes = [{ + bidder: BIDDER, + params: { + siteId: 1, + placementId: 1, + adsSrvDomain: 'adsSrvDomain', + cdnDomain: 'cdnDomain', + } + }] + const request = spec.buildRequests(bidRequestsNoMediaTypes, bidderRequest) + expect(request[0]).to.have.keys(['headers', 'data', 'method', 'url']) + }) + }) + + describe('_getPayload', function () { + const payload = spec._getPayload(bidRequests[0], bidderRequest) + it('should not be empty', function() { + expect(payload).to.not.be.empty + }) + + it('should have userSession', function() { + expect(payload.userSession).to.be.a('string') + }) + + it('should have data object', function() { + expect(payload.data).to.be.a('object') + }) + + it('should have complianceData object', function() { + expect(payload.data.complianceData).to.be.a('object') + }) + + it('should have device object', function() { + expect(payload.data.device).to.be.a('object') + }) + + it('should have omidpn', function() { + expect(payload.data.omidpn).to.be.a('string') + }) + + it('should have integration', function() { + expect(payload.data.integration).to.be.a('string') + }) + + it('should have bidId', function() { + expect(payload.data.id).to.not.be.empty + }) + + it('should have action getPlacement', function() { + expect(payload.data.action).to.be.equal('getPlacement') + }) + + it('should have app parameter', function() { + expect(payload.data.app).to.be.a('number') + }) + + it('should have placement parameter', function() { + expect(payload.data.placement).to.be.a('number') + }) + }) + + describe('interpretResponse', function () { + const response = { + body: { + status: 'ok', + data: { + ads: [{ + ad: { + data: { + id: '001', + ecpm: 100, + w: 32, + h: 480, + markup: 'test ad' + } + }, + subtype: 'html' + }], + } + } + } + const serverRequest = { + data: { + data: { + id: 'id_001', + data: { + ref: 'testprebid.com' + } + } + } + } + + let ir = spec.interpretResponse(response, serverRequest) + + expect(ir.length).to.equal(1) + + ir = ir[0] + + it('should have requestId', function() { + expect(ir.requestId).to.be.a('string') + }) + + it('should have cpm', function() { + expect(ir.cpm).to.be.a('number') + }) + + it('should have width', function() { + expect(ir.width).to.be.a('number') + }) + + it('should have height', function() { + expect(ir.height).to.be.a('number') + }) + + it('should have creativeId', function() { + expect(ir.creativeId).to.be.a('number') + }) + + it('should have ad', function() { + expect(ir.ad).to.be.a('string') + }) + }) +}) diff --git a/test/spec/modules/dspxBidAdapter_spec.js b/test/spec/modules/dspxBidAdapter_spec.js index 09f40895ec9..2869385d7e7 100644 --- a/test/spec/modules/dspxBidAdapter_spec.js +++ b/test/spec/modules/dspxBidAdapter_spec.js @@ -62,6 +62,7 @@ describe('dspxAdapter', function () { 'bidId': '30b31c1838de1e1', 'bidderRequestId': '22edbae2733bf61', 'auctionId': '1d1a030790a475', + 'adUnitCode': 'testDiv1', 'userId': { 'netId': '123', 'uid2': '456' @@ -98,7 +99,8 @@ describe('dspxAdapter', function () { ], 'bidId': '30b31c1838de1e3', 'bidderRequestId': '22edbae2733bf69', - 'auctionId': '1d1a030790a477' + 'auctionId': '1d1a030790a477', + 'adUnitCode': 'testDiv2' }, { 'bidder': 'dspx', @@ -120,13 +122,15 @@ describe('dspxAdapter', function () { 'bidId': '30b31c1838de1e4', 'bidderRequestId': '22edbae2733bf67', - 'auctionId': '1d1a030790a478' + 'auctionId': '1d1a030790a478', + 'adUnitCode': 'testDiv3' }, { 'bidder': 'dspx', 'params': { 'placement': '101', - 'devMode': true + 'devMode': true, + 'vastFormat': 'vast4' }, 'mediaTypes': { 'video': { @@ -136,7 +140,8 @@ describe('dspxAdapter', function () { }, 'bidId': '30b31c1838de1e41', 'bidderRequestId': '22edbae2733bf67', - 'auctionId': '1d1a030790a478' + 'auctionId': '1d1a030790a478', + 'adUnitCode': 'testDiv4' } ]; @@ -157,16 +162,16 @@ describe('dspxAdapter', function () { it('sends bid request to our endpoint via GET', function () { expect(request1.method).to.equal('GET'); expect(request1.url).to.equal(ENDPOINT_URL); - let data = request1.data.replace(/rnd=\d+\&/g, '').replace(/ref=.*\&bid/g, 'bid'); - expect(data).to.equal('_f=html&alternative=prebid_js&inventory_item_id=6682&srw=300&srh=250&idt=100&bid_id=30b31c1838de1e1&pfilter%5Bfloorprice%5D=1000000&pfilter%5Bprivate_auction%5D=0&pfilter%5Bgeo%5D%5Bcountry%5D=DE&pfilter%5Bgdpr_consent%5D=BOJ%2FP2HOJ%2FP2HABABMAAAAAZ%2BA%3D%3D&pfilter%5Bgdpr%5D=true&bcat=IAB2%2CIAB4&dvt=desktop&did_netid=123&did_uid2=456'); + let data = request1.data.replace(/rnd=\d+\&/g, '').replace(/ref=.*\&bid/g, 'bid').replace(/pbver=.*?&/g, 'pbver=test&'); + expect(data).to.equal('_f=auto&alternative=prebid_js&inventory_item_id=6682&srw=300&srh=250&idt=100&bid_id=30b31c1838de1e1&pbver=test&pfilter%5Bfloorprice%5D=1000000&pfilter%5Bprivate_auction%5D=0&pfilter%5Bgeo%5D%5Bcountry%5D=DE&pfilter%5Bgdpr_consent%5D=BOJ%2FP2HOJ%2FP2HABABMAAAAAZ%2BA%3D%3D&pfilter%5Bgdpr%5D=true&bcat=IAB2%2CIAB4&dvt=desktop&did_netid=123&did_uid2=456&auctionId=1d1a030790a475&pbcode=testDiv1&media_types%5Bbanner%5D=300x250'); }); var request2 = spec.buildRequests([bidRequests[1]], bidderRequest)[0]; it('sends bid request to our DEV endpoint via GET', function () { expect(request2.method).to.equal('GET'); expect(request2.url).to.equal(ENDPOINT_URL_DEV); - let data = request2.data.replace(/rnd=\d+\&/g, '').replace(/ref=.*\&bid/g, 'bid'); - expect(data).to.equal('_f=html&alternative=prebid_js&inventory_item_id=101&srw=300&srh=250&idt=100&bid_id=30b31c1838de1e2&pfilter%5Bgdpr_consent%5D=BOJ%2FP2HOJ%2FP2HABABMAAAAAZ%2BA%3D%3D&pfilter%5Bgdpr%5D=true&prebidDevMode=1'); + let data = request2.data.replace(/rnd=\d+\&/g, '').replace(/ref=.*\&bid/g, 'bid').replace(/pbver=.*?&/g, 'pbver=test&'); + expect(data).to.equal('_f=auto&alternative=prebid_js&inventory_item_id=101&srw=300&srh=250&idt=100&bid_id=30b31c1838de1e2&pbver=test&pfilter%5Bgdpr_consent%5D=BOJ%2FP2HOJ%2FP2HABABMAAAAAZ%2BA%3D%3D&pfilter%5Bgdpr%5D=true&prebidDevMode=1&auctionId=1d1a030790a476&media_types%5Bbanner%5D=300x250'); }); // Without gdprConsent @@ -179,23 +184,23 @@ describe('dspxAdapter', function () { it('sends bid request without gdprConsent to our endpoint via GET', function () { expect(request3.method).to.equal('GET'); expect(request3.url).to.equal(ENDPOINT_URL); - let data = request3.data.replace(/rnd=\d+\&/g, '').replace(/ref=.*\&bid/g, 'bid'); - expect(data).to.equal('_f=html&alternative=prebid_js&inventory_item_id=6682&srw=300&srh=250&idt=100&bid_id=30b31c1838de1e3&pfilter%5Bfloorprice%5D=1000000&pfilter%5Bprivate_auction%5D=0&pfilter%5Bgeo%5D%5Bcountry%5D=DE&bcat=IAB2%2CIAB4&dvt=desktop'); + let data = request3.data.replace(/rnd=\d+\&/g, '').replace(/ref=.*\&bid/g, 'bid').replace(/pbver=.*?&/g, 'pbver=test&'); + expect(data).to.equal('_f=auto&alternative=prebid_js&inventory_item_id=6682&srw=300&srh=250&idt=100&bid_id=30b31c1838de1e3&pbver=test&pfilter%5Bfloorprice%5D=1000000&pfilter%5Bprivate_auction%5D=0&pfilter%5Bgeo%5D%5Bcountry%5D=DE&bcat=IAB2%2CIAB4&dvt=desktop&auctionId=1d1a030790a477&pbcode=testDiv2&media_types%5Bbanner%5D=300x250'); }); var request4 = spec.buildRequests([bidRequests[3]], bidderRequestWithoutGdpr)[0]; it('sends bid request without gdprConsent to our DEV endpoint via GET', function () { expect(request4.method).to.equal('GET'); expect(request4.url).to.equal(ENDPOINT_URL_DEV); - let data = request4.data.replace(/rnd=\d+\&/g, '').replace(/ref=.*\&bid/g, 'bid'); - expect(data).to.equal('_f=html&alternative=prebid_js&inventory_item_id=101&srw=300&srh=250&idt=100&bid_id=30b31c1838de1e4&prebidDevMode=1'); + let data = request4.data.replace(/rnd=\d+\&/g, '').replace(/ref=.*\&bid/g, 'bid').replace(/pbver=.*?&/g, 'pbver=test&'); + expect(data).to.equal('_f=auto&alternative=prebid_js&inventory_item_id=101&srw=300&srh=250&idt=100&bid_id=30b31c1838de1e4&pbver=test&prebidDevMode=1&auctionId=1d1a030790a478&pbcode=testDiv3&media_types%5Bvideo%5D=640x480&media_types%5Bbanner%5D=300x250'); }); var request5 = spec.buildRequests([bidRequests[4]], bidderRequestWithoutGdpr)[0]; - it('sends bid video request to our rads endpoint via GET', function () { + it('sends bid video request to our endpoint via GET', function () { expect(request5.method).to.equal('GET'); - let data = request5.data.replace(/rnd=\d+\&/g, '').replace(/ref=.*\&bid/g, 'bid'); - expect(data).to.equal('_f=vast2&alternative=prebid_js&inventory_item_id=101&srw=640&srh=480&idt=100&bid_id=30b31c1838de1e41&prebidDevMode=1'); + let data = request5.data.replace(/rnd=\d+\&/g, '').replace(/ref=.*\&bid/g, 'bid').replace(/pbver=.*?&/g, 'pbver=test&'); + expect(data).to.equal('_f=auto&alternative=prebid_js&inventory_item_id=101&srw=640&srh=480&idt=100&bid_id=30b31c1838de1e41&pbver=test&vf=vast4&prebidDevMode=1&auctionId=1d1a030790a478&pbcode=testDiv4&media_types%5Bvideo%5D=640x480'); }); }); diff --git a/test/spec/modules/e_volutionBidAdapter_spec.js b/test/spec/modules/e_volutionBidAdapter_spec.js new file mode 100644 index 00000000000..1f60edda0ef --- /dev/null +++ b/test/spec/modules/e_volutionBidAdapter_spec.js @@ -0,0 +1,242 @@ +import {expect} from 'chai'; +import {spec} from '../../../modules/e_volutionBidAdapter.js'; + +describe('EvolutionTechBidAdapter', function () { + let bid = { + bidId: '23fhj33i987f', + bidder: 'e_volution', + params: { + placementId: 0 + }, + mediaTypes: { + banner: { + sizes: [[300, 250]], + } + } + }; + + describe('isBidRequestValid', function () { + it('Should return true if there are bidId, params and placementId 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]); + 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://service.e-volution.ai/?c=o&m=multi'); + }); + it('Returns valid data if array of bids is valid', function () { + let data = serverRequest.data; + expect(data).to.be.an('object'); + expect(data).to.have.all.keys('deviceWidth', 'deviceHeight', 'language', 'secure', 'host', 'page', 'placements'); + expect(data.deviceWidth).to.be.a('number'); + expect(data.deviceHeight).to.be.a('number'); + expect(data.language).to.be.a('string'); + expect(data.secure).to.be.within(0, 1); + expect(data.host).to.be.a('string'); + expect(data.page).to.be.a('string'); + let placement = data['placements'][0]; + expect(placement).to.have.keys('placementId', 'bidId', 'traffic', 'sizes', 'bidfloor'); + expect(placement.placementId).to.equal(0); + expect(placement.bidId).to.equal('23fhj33i987f'); + expect(placement.traffic).to.equal('banner'); + }); + it('Returns empty data if no valid requests are passed', function () { + serverRequest = spec.buildRequests([]); + let data = serverRequest.data; + expect(data.placements).to.be.an('array').that.is.empty; + }); + }); + describe('interpretResponse', function () { + it('Should interpret banner response', function () { + const banner = { + body: [{ + mediaType: 'banner', + width: 300, + height: 250, + cpm: 0.4, + ad: 'Test', + requestId: '23fhj33i987f', + ttl: 120, + creativeId: '2', + netRevenue: true, + currency: 'USD', + dealId: '1', + 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('23fhj33i987f'); + expect(dataItem.cpm).to.equal(0.4); + expect(dataItem.width).to.equal(300); + expect(dataItem.height).to.equal(250); + expect(dataItem.ad).to.equal('Test'); + expect(dataItem.ttl).to.equal(120); + expect(dataItem.creativeId).to.equal('2'); + expect(dataItem.netRevenue).to.be.true; + expect(dataItem.currency).to.equal('USD'); + }); + it('Should interpret video response', function () { + const video = { + body: [{ + vastUrl: 'test.com', + mediaType: 'video', + cpm: 0.5, + requestId: '23fhj33i987f', + ttl: 120, + creativeId: '2', + netRevenue: true, + currency: 'USD', + dealId: '1', + meta: {} + }] + }; + 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'); + }); + 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: {} + }] + }; + 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'); + }); + 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 () { + let userSync = spec.getUserSyncs(); + it('Returns valid URL and type', function () { + if (spec.noSync) { + expect(userSync).to.be.equal(false); + } else { + expect(userSync).to.be.an('array').with.lengthOf(1); + expect(userSync[0].type).to.exist; + expect(userSync[0].url).to.exist; + expect(userSync[0].type).to.be.equal('image'); + expect(userSync[0].url).to.be.equal('https://service.e-volution.ai/?c=o&m=sync'); + } + }); + }); +}); diff --git a/test/spec/modules/eids_spec.js b/test/spec/modules/eids_spec.js index 83924b38a7e..9edda3c9e95 100644 --- a/test/spec/modules/eids_spec.js +++ b/test/spec/modules/eids_spec.js @@ -240,9 +240,9 @@ describe('eids array generation for known sub-modules', function() { }); }); - it('haloId', function() { + it('hadronId', function() { const userId = { - haloId: 'some-random-id-value' + hadronId: 'some-random-id-value' }; const newEids = createEidsArray(userId); expect(newEids.length).to.equal(1); diff --git a/test/spec/modules/engageyaBidAdapter_spec.js b/test/spec/modules/engageyaBidAdapter_spec.js index 7f07e4b9e4a..283f0148402 100644 --- a/test/spec/modules/engageyaBidAdapter_spec.js +++ b/test/spec/modules/engageyaBidAdapter_spec.js @@ -45,6 +45,108 @@ describe('Engageya adapter', function () { ] }) + describe('isValidSize', function () { + const bid = { + bidder: 'engageya', + params: { + widgetId: 85610, + websiteId: 91140, + pageUrl: '[PAGE_URL]' + } + }; + it('Exact match, 236X202', function () { + bid.sizes = [[236, 202]]; + const isValid = spec.isBidRequestValid(bid); + expect(isValid).to.be.true; + }); + it('Exact match, 300X200', function () { + bid.sizes = [[300, 200]]; + const isValid = spec.isBidRequestValid(bid); + expect(isValid).to.be.true; + }); + it('Exact match, 600X500', function () { + bid.sizes = [[600, 500]]; + const isValid = spec.isBidRequestValid(bid); + expect(isValid).to.be.true; + }); + + it('Ratio max limit, 236X212', function () { + bid.sizes = [[236, 212]]; + const isValid = spec.isBidRequestValid(bid); + expect(isValid).to.be.true; + }); + it('Ratio max limit, 300X209', function () { + bid.sizes = [[300, 209]]; + const isValid = spec.isBidRequestValid(bid); + expect(isValid).to.be.true; + }); + it('Ratio max limit, 600X524', function () { + bid.sizes = [[600, 524]]; + const isValid = spec.isBidRequestValid(bid); + expect(isValid).to.be.true; + }); + + it('Ratio & width max limit, 248X222', function () { + bid.sizes = [[248, 222]]; + const isValid = spec.isBidRequestValid(bid); + expect(isValid).to.be.true; + }); + it('Ratio & width max limit, 315X220', function () { + bid.sizes = [[315, 220]]; + const isValid = spec.isBidRequestValid(bid); + expect(isValid).to.be.true; + }); + it('Ratio & width max limit, 631X551', function () { + bid.sizes = [[631, 551]]; + const isValid = spec.isBidRequestValid(bid); + expect(isValid).to.be.true; + }); + + it('Width too big, 320X285', function () { + bid.sizes = [[320, 285]]; + const isValid = spec.isBidRequestValid(bid); + expect(isValid).to.be.false; + }); + it('Width too big, 316X220', function () { + bid.sizes = [[316, 220]]; + const isValid = spec.isBidRequestValid(bid); + expect(isValid).to.be.false; + }); + it('Width too big, 632X551', function () { + bid.sizes = [[632, 551]]; + const isValid = spec.isBidRequestValid(bid); + expect(isValid).to.be.false; + }); + + it('Ratio too big, 600X525', function () { + bid.sizes = [[600, 525]]; + const isValid = spec.isBidRequestValid(bid); + expect(isValid).to.be.false; + }); + + it('Ratio min limit, 236X192', function () { + bid.sizes = [[236, 192]]; + const isValid = spec.isBidRequestValid(bid); + expect(isValid).to.be.true; + }); + it('Ratio min limit, 300X190', function () { + bid.sizes = [[300, 190]]; + const isValid = spec.isBidRequestValid(bid); + expect(isValid).to.be.true; + }); + it('Ratio min limit, 600X475', function () { + bid.sizes = [[600, 475]]; + const isValid = spec.isBidRequestValid(bid); + expect(isValid).to.be.true; + }); + + it('Ratio too small, 600X474', function () { + bid.sizes = [[600, 474]]; + const isValid = spec.isBidRequestValid(bid); + expect(isValid).to.be.false; + }); + }) + describe('isBidRequestValid', function () { it('Valid bid case', function () { let validBid = { diff --git a/test/spec/modules/eplanningAnalyticsAdapter_spec.js b/test/spec/modules/eplanningAnalyticsAdapter_spec.js index 255d116a0ff..872518f2f27 100644 --- a/test/spec/modules/eplanningAnalyticsAdapter_spec.js +++ b/test/spec/modules/eplanningAnalyticsAdapter_spec.js @@ -1,5 +1,5 @@ import eplAnalyticsAdapter from 'modules/eplanningAnalyticsAdapter.js'; -import includes from 'core-js-pure/features/array/includes.js'; +import {includes} from 'src/polyfill.js'; import { expect } from 'chai'; import { parseUrl } from 'src/utils.js'; import { server } from 'test/mocks/xhr.js'; diff --git a/test/spec/modules/fintezaAnalyticsAdapter_spec.js b/test/spec/modules/fintezaAnalyticsAdapter_spec.js index 54d1a7e7976..407ceb305a2 100644 --- a/test/spec/modules/fintezaAnalyticsAdapter_spec.js +++ b/test/spec/modules/fintezaAnalyticsAdapter_spec.js @@ -1,5 +1,5 @@ import fntzAnalyticsAdapter from 'modules/fintezaAnalyticsAdapter.js'; -import includes from 'core-js-pure/features/array/includes.js'; +import {includes} from 'src/polyfill.js'; import { expect } from 'chai'; import { parseUrl } from 'src/utils.js'; import { server } from 'test/mocks/xhr.js'; diff --git a/test/spec/modules/fluctBidAdapter_spec.js b/test/spec/modules/fluctBidAdapter_spec.js index 70666532442..8ef99727ce7 100644 --- a/test/spec/modules/fluctBidAdapter_spec.js +++ b/test/spec/modules/fluctBidAdapter_spec.js @@ -83,6 +83,98 @@ describe('fluctAdapter', function () { const request = spec.buildRequests(bidRequests, bidderRequest)[0]; expect(request.url).to.equal('https://hb.adingo.jp/prebid?dfpUnitCode=%2F100000%2Funit_code&tagId=10000%3A100000001&groupId=1000000002'); }); + + it('includes data.user.eids = [] by default', function () { + const request = spec.buildRequests(bidRequests, bidderRequest)[0]; + expect(request.data.user.eids).to.eql([]); + }); + + it('includes no data.params.kv by default', function () { + const request = spec.buildRequests(bidRequests, bidderRequest)[0]; + expect(request.data.params.kv).to.eql(undefined); + }); + + it('includes filtered user.eids if any exists', function () { + const bidRequests2 = bidRequests.map( + (bidReq) => Object.assign(bidReq, { + userIdAsEids: [ + { + source: 'foobar.com', + uids: [ + { id: 'foobar-id' } + ], + }, + { + source: 'adserver.org', + uids: [ + { id: 'tdid' } + ], + }, + { + source: 'criteo.com', + uids: [ + { id: 'criteo-id' } + ], + }, + { + source: 'intimatemerger.com', + uids: [ + { id: 'imuid' } + ], + }, + { + source: 'liveramp.com', + uids: [ + { id: 'idl-env' } + ], + }, + ], + }) + ); + const request = spec.buildRequests(bidRequests2, bidderRequest)[0]; + expect(request.data.user.eids).to.eql([ + { + source: 'adserver.org', + uids: [ + { id: 'tdid' } + ], + }, + { + source: 'criteo.com', + uids: [ + { id: 'criteo-id' } + ], + }, + { + source: 'intimatemerger.com', + uids: [ + { id: 'imuid' } + ], + }, + { + source: 'liveramp.com', + uids: [ + { id: 'idl-env' } + ], + }, + ]); + }); + + it('includes data.params.kv if any exists', function () { + const bidRequests2 = bidRequests.map( + (bidReq) => Object.assign(bidReq, { + params: { + kv: { + imsids: ['imsid1', 'imsid2'] + } + } + }) + ); + const request = spec.buildRequests(bidRequests2, bidderRequest)[0]; + expect(request.data.params.kv).to.eql({ + imsids: ['imsid1', 'imsid2'] + }); + }); }); describe('interpretResponse', function() { diff --git a/test/spec/modules/ftrackIdSystem_spec.js b/test/spec/modules/ftrackIdSystem_spec.js new file mode 100644 index 00000000000..69d66d75bb1 --- /dev/null +++ b/test/spec/modules/ftrackIdSystem_spec.js @@ -0,0 +1,238 @@ +import { ftrackIdSubmodule } from 'modules/ftrackIdSystem.js'; +import * as utils from 'src/utils.js'; +import { uspDataHandler } from 'src/adapterManager.js'; +let expect = require('chai').expect; + +let server; + +let configMock = { + name: 'ftrack', + params: { + url: 'https://d9.flashtalking.com/d9core' + }, + storage: { + name: 'ftrackId', + type: 'html5', + expires: 90, + refreshInSeconds: 8 * 3600 + }, + debug: true +}; + +let consentDataMock = { + gdprApplies: 0, + consentString: '' +}; + +describe('FTRACK ID System', () => { + describe(`Global Module Rules`, () => { + it(`should not use the "PREBID_GLOBAL" variable nor otherwise obtain a pointer to the global PBJS object`, () => { + expect((/PREBID_GLOBAL/gi).test(JSON.stringify(ftrackIdSubmodule))).to.not.be.ok; + }); + }); + + describe('ftrackIdSubmodule.isConfigOk():', () => { + let logWarnStub; + let logErrorStub; + + beforeEach(() => { + logWarnStub = sinon.stub(utils, 'logWarn'); + logErrorStub = sinon.stub(utils, 'logError'); + }); + + afterEach(() => { + logWarnStub.restore(); + logErrorStub.restore(); + }); + + it(`should be rejected if 'config.storage' property is missing`, () => { + let configMock1 = JSON.parse(JSON.stringify(configMock)); + delete configMock1.storage; + delete configMock1.params; + + ftrackIdSubmodule.isConfigOk(configMock1); + expect(logErrorStub.args[0][0]).to.equal(`FTRACK - config.storage required to be set.`); + }); + + it(`should be rejected if 'config.storage.name' property is missing`, () => { + let configMock1 = JSON.parse(JSON.stringify(configMock)); + delete configMock1.storage.name; + + ftrackIdSubmodule.isConfigOk(configMock1); + expect(logErrorStub.args[0][0]).to.equal(`FTRACK - config.storage required to be set.`); + }); + + it(`should be rejected if 'config.storage.name' is not 'ftrackId'`, () => { + let configMock1 = JSON.parse(JSON.stringify(configMock)); + configMock1.storage.name = 'not-ftrack'; + + ftrackIdSubmodule.isConfigOk(configMock1); + expect(logWarnStub.args[0][0]).to.equal(`FTRACK - config.storage.name recommended to be "ftrackId".`); + }); + + it(`should be rejected if 'congig.storage.type' property is missing`, () => { + let configMock1 = JSON.parse(JSON.stringify(configMock)); + delete configMock1.storage.type; + + ftrackIdSubmodule.isConfigOk(configMock1); + expect(logErrorStub.args[0][0]).to.equal(`FTRACK - config.storage required to be set.`); + }); + + it(`should be rejected if 'config.storage.type' is not 'html5'`, () => { + let configMock1 = JSON.parse(JSON.stringify(configMock)); + configMock1.storage.type = 'not-html5'; + + ftrackIdSubmodule.isConfigOk(configMock1); + expect(logWarnStub.args[0][0]).to.equal(`FTRACK - config.storage.type recommended to be "html5".`); + }); + + it(`should be rejected if 'config.params.url' does not exist`, () => { + let configMock1 = JSON.parse(JSON.stringify(configMock)); + delete configMock1.params.url; + + ftrackIdSubmodule.isConfigOk(configMock1); + expect(logWarnStub.args[0][0]).to.equal(`FTRACK - config.params.url is required for ftrack to run. Url should be "https://d9.flashtalking.com/d9core".`); + }); + + it(`should be rejected if 'storage.param.url' does not exist or is not 'https://d9.flashtalking.com/d9core'`, () => { + let configMock1 = JSON.parse(JSON.stringify(configMock)); + configMock1.params.url = 'https://d9.NOT.flashtalking.com/d9core'; + + ftrackIdSubmodule.isConfigOk(configMock1); + expect(logWarnStub.args[0][0]).to.equal(`FTRACK - config.params.url is required for ftrack to run. Url should be "https://d9.flashtalking.com/d9core".`); + }); + }); + + describe(`ftrackIdSubmodule.isThereConsent():`, () => { + let uspDataHandlerStub; + beforeEach(() => { + uspDataHandlerStub = sinon.stub(uspDataHandler, 'getConsentData'); + }); + + afterEach(() => { + uspDataHandlerStub.restore(); + }); + + describe(`returns 'false' if:`, () => { + it(`GDPR: if gdprApplies is truthy`, () => { + expect(ftrackIdSubmodule.isThereConsent({gdprApplies: 1})).to.not.be.ok; + expect(ftrackIdSubmodule.isThereConsent({gdprApplies: true})).to.not.be.ok; + }); + + it(`US_PRIVACY version 1: if 'Opt Out Sale' is 'Y'`, () => { + uspDataHandlerStub.returns('1YYY'); + expect(ftrackIdSubmodule.isThereConsent({})).to.not.be.ok; + }); + }); + + describe(`returns 'true' if`, () => { + it(`GDPR: if gdprApplies is undefined, false or 0`, () => { + expect(ftrackIdSubmodule.isThereConsent({gdprApplies: 0})).to.be.ok; + expect(ftrackIdSubmodule.isThereConsent({gdprApplies: false})).to.be.ok; + expect(ftrackIdSubmodule.isThereConsent({gdprApplies: null})).to.be.ok; + expect(ftrackIdSubmodule.isThereConsent({})).to.be.ok; + }); + + it(`US_PRIVACY version 1: if 'Opt Out Sale' is not 'Y' ('N','-')`, () => { + uspDataHandlerStub.returns('1NNN'); + expect(ftrackIdSubmodule.isThereConsent(null)).to.be.ok; + + uspDataHandlerStub.returns('1---'); + expect(ftrackIdSubmodule.isThereConsent(null)).to.be.ok; + }); + }); + }); + + describe('getId() method', () => { + it(`should be using the StorageManager to set cookies or localstorage, as opposed to doing it directly`, () => { + expect((/localStorage/gi).test(JSON.stringify(ftrackIdSubmodule))).to.not.be.ok; + expect((/cookie/gi).test(JSON.stringify(ftrackIdSubmodule))).to.not.be.ok; + }); + + it(`should be the only method that gets a new ID aka hits the D9 endpoint`, () => { + let appendChildStub = sinon.stub(window.document.body, 'appendChild'); + + ftrackIdSubmodule.getId(configMock, null, null).callback(); + expect(window.document.body.appendChild.called).to.be.ok; + let actualScriptTag = window.document.body.appendChild.args[0][0]; + expect(actualScriptTag.tagName.toLowerCase()).to.equal('script'); + expect(actualScriptTag.getAttribute('src')).to.equal('https://d9.flashtalking.com/d9core'); + appendChildStub.resetHistory(); + + ftrackIdSubmodule.decode('value', configMock); + expect(window.document.body.appendChild.called).to.not.be.ok; + expect(window.document.body.appendChild.args).to.deep.equal([]); + appendChildStub.resetHistory(); + + ftrackIdSubmodule.extendId(configMock, null, {cache: {id: ''}}); + expect(window.document.body.appendChild.called).to.not.be.ok; + expect(window.document.body.appendChild.args).to.deep.equal([]); + + appendChildStub.restore(); + }); + + it(`should populate localstorage and return the IDS (end-to-end test)`, () => { + let ftrackId, + ftrackIdExp, + forceCallback = false; + + // Confirm that our item is not in localStorage yet + expect(window.localStorage.getItem('ftrack-rtd')).to.not.be.ok; + expect(window.localStorage.getItem('ftrack-rtd_exp')).to.not.be.ok; + + ftrackIdSubmodule.getId(configMock, consentDataMock, null).callback(); + return new Promise(function(resolve, reject) { + window.testTimer = function () { + // Sinon fake server is NOT changing the readyState to 4, so instead + // we are forcing the callback to run and just passing in the expected Object + if (!forceCallback && window.hasOwnProperty('D9r')) { + window.D9r.callback({ 'DeviceID': [''], 'SingleDeviceID': [''] }); + forceCallback = true; + } + + ftrackId = window.localStorage.getItem('ftrackId'); + ftrackIdExp = window.localStorage.getItem('ftrackId_exp'); + + if (!!ftrackId && !!ftrackIdExp) { + expect(window.localStorage.getItem('ftrackId')).to.be.ok; + expect(window.localStorage.getItem('ftrackId_exp')).to.be.ok; + resolve(); + } else { + window.setTimeout(window.testTimer, 25); + } + }; + window.testTimer(); + }); + }); + }); + + describe(`decode() method`, () => { + it(`should respond with an object with the key 'ftrackId'`, () => { + expect(ftrackIdSubmodule.decode('value', configMock)).to.deep.equal({ftrackId: 'value'}); + }); + + it(`should not be making requests to retrieve a new ID, it should just be decoding a response`, () => { + server = sinon.createFakeServer(); + ftrackIdSubmodule.decode('value', configMock); + + expect(server.requests).to.have.length(0); + + server.restore(); + }) + }); + + describe(`extendId() method`, () => { + it(`should not be making requests to retrieve a new ID, it should just be adding additional data to the id object`, () => { + server = sinon.createFakeServer(); + ftrackIdSubmodule.extendId(configMock, null, {cache: {id: ''}}); + + expect(server.requests).to.have.length(0); + + server.restore(); + }); + + it(`should return cacheIdObj`, () => { + expect(ftrackIdSubmodule.extendId(configMock, null, {cache: {id: ''}})).to.deep.equal({cache: {id: ''}}); + }); + }); +}); diff --git a/test/spec/modules/gdprEnforcement_spec.js b/test/spec/modules/gdprEnforcement_spec.js index 82cb70f42be..a78f5ac948e 100644 --- a/test/spec/modules/gdprEnforcement_spec.js +++ b/test/spec/modules/gdprEnforcement_spec.js @@ -16,7 +16,7 @@ import { config } from 'src/config.js'; import adapterManager, { gdprDataHandler } from 'src/adapterManager.js'; import * as utils from 'src/utils.js'; import { validateStorageEnforcement } from 'src/storageManager.js'; -import events from 'src/events.js'; +import * as events from 'src/events.js'; describe('gdpr enforcement', function () { let nextFnSpy; diff --git a/test/spec/modules/gmosspBidAdapter_spec.js b/test/spec/modules/gmosspBidAdapter_spec.js index c22badb9b4c..87c87600b97 100644 --- a/test/spec/modules/gmosspBidAdapter_spec.js +++ b/test/spec/modules/gmosspBidAdapter_spec.js @@ -1,7 +1,6 @@ import { expect } from 'chai'; import { spec } from 'modules/gmosspBidAdapter.js'; import { newBidder } from 'src/adapters/bidderFactory.js'; -import {getStorageManager} from 'src/storageManager'; import * as utils from 'src/utils.js'; const ENDPOINT = 'https://sp.gmossp-sp.jp/hb/prebid/query.ad'; @@ -36,8 +35,7 @@ describe('GmosspAdapter', function () { }); describe('buildRequests', function () { - const storage = getStorageManager(); - const bidRequests = [ + let bidRequests = [ { bidder: 'gmossp', params: { @@ -52,7 +50,12 @@ describe('GmosspAdapter', function () { bidId: '2b84475b5b636e', bidderRequestId: '1f4001782ac16c', auctionId: 'aba03555-4802-4c45-9f15-05ffa8594cff', - transactionId: '791e9d84-af92-4903-94da-24c7426d9d0c' + transactionId: '791e9d84-af92-4903-94da-24c7426d9d0c', + userId: { + imuid: 'h.0a4749e7ffe09fa6', + pubcid: '1111', + idl_env: '1111', + } } ]; @@ -62,24 +65,24 @@ describe('GmosspAdapter', function () { referer: 'https://hoge.com' } }; - storage.setCookie('_im_uid.1000283', 'h.0a4749e7ffe09fa6'); - 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('tid=791e9d84-af92-4903-94da-24c7426d9d0c&bid=2b84475b5b636e&ver=$prebid.version$&sid=123456&im_uid=h.0a4749e7ffe09fa6&url=https%3A%2F%2Fhoge.com&cur=JPY&dnt=0&'); + expect(requests[0].data).to.equal('tid=791e9d84-af92-4903-94da-24c7426d9d0c&bid=2b84475b5b636e&ver=$prebid.version$&sid=123456&im_uid=h.0a4749e7ffe09fa6&shared_id=1111&idl_env=1111&url=https%3A%2F%2Fhoge.com' + '&ref=' + encodeURIComponent(document.referrer) + '&cur=JPY&dnt=0&'); }); - it('should use fallback if refererInfo.referer in bid request is empty and _im_uid.1000283 cookie is empty', function () { + it('should use fallback if refererInfo.referer in bid request is empty and im_uid ,shared_id, idl_env cookie is empty', function () { const bidderRequest = { refererInfo: { referer: '' - } + }, }; - storage.setCookie('_im_uid.1000283', ''); + bidRequests[0].userId.imuid = ''; + bidRequests[0].userId.pubcid = ''; + bidRequests[0].userId.idl_env = ''; const requests = spec.buildRequests(bidRequests, bidderRequest); - const result = 'tid=791e9d84-af92-4903-94da-24c7426d9d0c&bid=2b84475b5b636e&ver=$prebid.version$&sid=123456&url=' + encodeURIComponent(window.top.location.href) + '&cur=JPY&dnt=0&'; + const result = 'tid=791e9d84-af92-4903-94da-24c7426d9d0c&bid=2b84475b5b636e&ver=$prebid.version$&sid=123456&ref=' + encodeURIComponent(document.referrer) + '&cur=JPY&dnt=0&'; expect(requests[0].data).to.equal(result); }); }); @@ -104,7 +107,7 @@ describe('GmosspAdapter', function () { } ]; - it('should get correct banner bid response', function() { + it('should get correct banner bid response', function () { const response = { bid: '2b84475b5b636e', price: 20, diff --git a/test/spec/modules/gnetBidAdapter_spec.js b/test/spec/modules/gnetBidAdapter_spec.js index eeb33418a82..a69b196bc5c 100644 --- a/test/spec/modules/gnetBidAdapter_spec.js +++ b/test/spec/modules/gnetBidAdapter_spec.js @@ -8,7 +8,7 @@ import { newBidder } from 'src/adapters/bidderFactory.js'; -const ENDPOINT = 'https://adserver.gnetproject.com/prebid.php'; +const ENDPOINT = 'https://service.gnetrtb.com/api/adrequest'; describe('gnetAdapter', function () { const adapter = newBidder(spec); @@ -23,7 +23,7 @@ describe('gnetAdapter', function () { let bid = { bidder: 'gnet', params: { - websiteId: '4' + websiteId: '1', adunitId: '1' } }; @@ -39,11 +39,22 @@ describe('gnetAdapter', function () { }); }); + describe('onBidWon', function () { + const bid = { + requestId: '29d5b1d3a520f8' + }; + + it('return success adserver won bid endpoint', () => { + const result = spec.onBidWon(bid); + assert.ok(result); + }); + }); + describe('buildRequests', function () { const bidRequests = [{ bidder: 'gnet', params: { - websiteId: '4' + websiteId: '1', adunitId: '1' }, adUnitCode: '/150790500/4_ZONA_IAB_300x250_5', sizes: [ @@ -52,12 +63,13 @@ describe('gnetAdapter', function () { bidId: '2a19afd5173318', bidderRequestId: '1f4001782ac16c', auctionId: 'aba03555-4802-4c45-9f15-05ffa8594cff', - transactionId: '894bdff6-61ec-4bec-a5a9-f36a5bfccef5' + transactionId: '894bdff6-61ec-4bec-a5a9-f36a5bfccef5', + gftuid: null }]; const bidderRequest = { refererInfo: { - referer: 'https://gnetproject.com/' + referer: 'https://gnetrtb.com' } }; @@ -66,13 +78,14 @@ describe('gnetAdapter', function () { expect(requests[0].url).to.equal(ENDPOINT); expect(requests[0].method).to.equal('POST'); expect(requests[0].data).to.equal(JSON.stringify({ - 'referer': 'https://gnetproject.com/', + 'referer': 'https://gnetrtb.com', 'adUnitCode': '/150790500/4_ZONA_IAB_300x250_5', 'bidId': '2a19afd5173318', 'transactionId': '894bdff6-61ec-4bec-a5a9-f36a5bfccef5', + 'gftuid': null, 'sizes': ['300x250'], 'params': { - 'websiteId': '4' + 'websiteId': '1', 'adunitId': '1' } })); }); diff --git a/test/spec/modules/gridBidAdapter_spec.js b/test/spec/modules/gridBidAdapter_spec.js index f31b8f16ef7..9e393a5823d 100644 --- a/test/spec/modules/gridBidAdapter_spec.js +++ b/test/spec/modules/gridBidAdapter_spec.js @@ -118,6 +118,24 @@ describe('TheMediaGrid Adapter', function () { } ]; + it('should be content categories and genre', function () { + const site = { + cat: ['IAB2'], + pagecat: ['IAB2-2'], + content: { + genre: 'Adventure' + } + }; + + const getConfigStub = sinon.stub(config, 'getConfig').callsFake( + arg => arg === 'ortb2.site' ? site : null); + const request = spec.buildRequests([bidRequests[0]], bidderRequest); + const payload = parseRequest(request.data); + expect(payload.site.cat).to.deep.equal([...site.cat, ...site.pagecat]); + expect(payload.site.content.genre).to.deep.equal(site.content.genre); + getConfigStub.restore(); + }); + it('should attach valid params to the tag', function () { const fpdUserIdVal = '0b0f84a1-1596-4165-9742-2e1a7dfac57f'; const getDataFromLocalStorageStub = sinon.stub(storage, 'getDataFromLocalStorage').callsFake( @@ -381,6 +399,24 @@ describe('TheMediaGrid Adapter', function () { expect(payload.user.ext.eids).to.deep.equal(eids); }); + it('if userId is present payload must have user.ext param with right keys', function () { + const ortb2UserExtDevice = { + screenWidth: 1200, + screenHeight: 800, + language: 'ru' + }; + const getConfigStub = sinon.stub(config, 'getConfig').callsFake( + arg => arg === 'ortb2.user.ext.device' ? ortb2UserExtDevice : null); + + const request = spec.buildRequests(bidRequests, bidderRequest); + expect(request.data).to.be.an('string'); + const payload = parseRequest(request.data); + expect(payload).to.have.property('user'); + expect(payload.user).to.have.property('ext'); + expect(payload.user.ext.device).to.deep.equal(ortb2UserExtDevice); + getConfigStub.restore(); + }); + it('if schain is present payload must have source.ext.schain param', function () { const schain = { complete: 1, @@ -510,7 +546,71 @@ describe('TheMediaGrid Adapter', function () { getConfigStub.restore(); }); - it('shold be right tmax when timeout in config is less then timeout in bidderRequest', function() { + it('should have user.data filled from config ortb2.user.data', function () { + const userData = [ + { + name: 'someName', + segment: [1, 2, { anyKey: 'anyVal' }, 'segVal', { id: 'segId' }, { value: 'segValue' }, { id: 'segId2', name: 'segName' }] + }, + { + name: 'permutive.com', + segment: [1, 2, 'segVal', { id: 'segId' }, { anyKey: 'anyVal' }, { value: 'segValue' }, { id: 'segId2', name: 'segName' }] + }, + { + someKey: 'another data' + } + ]; + + const getConfigStub = sinon.stub(config, 'getConfig').callsFake( + arg => arg === 'ortb2.user.data' ? userData : null); + const request = spec.buildRequests([bidRequests[0]], bidderRequest); + const payload = parseRequest(request.data); + expect(payload.user.data).to.deep.equal(userData); + getConfigStub.restore(); + }); + + it('should have right value in user.data when jwpsegments are present', function () { + const userData = [ + { + name: 'someName', + segment: [1, 2, { anyKey: 'anyVal' }, 'segVal', { id: 'segId' }, { value: 'segValue' }, { id: 'segId2', name: 'segName' }] + }, + { + name: 'permutive.com', + segment: [1, 2, 'segVal', { id: 'segId' }, { anyKey: 'anyVal' }, { value: 'segValue' }, { id: 'segId2', name: 'segName' }] + }, + { + someKey: 'another data' + } + ]; + const getConfigStub = sinon.stub(config, 'getConfig').callsFake( + arg => arg === 'ortb2.user.data' ? userData : null); + + const jsContent = {id: 'test_jw_content_id'}; + const jsSegments = ['test_seg_1', 'test_seg_2']; + const bidRequestsWithJwTargeting = Object.assign({}, bidRequests[0], { + rtd: { + jwplayer: { + targeting: { + segments: jsSegments, + content: jsContent + } + } + } + }); + const request = spec.buildRequests([bidRequestsWithJwTargeting], bidderRequest); + const payload = parseRequest(request.data); + expect(payload.user.data).to.deep.equal([{ + name: 'iow_labs_pub_data', + segment: [ + {name: 'jwpseg', value: jsSegments[0]}, + {name: 'jwpseg', value: jsSegments[1]} + ] + }, ...userData]); + getConfigStub.restore(); + }); + + it('should be right tmax when timeout in config is less then timeout in bidderRequest', function() { const getConfigStub = sinon.stub(config, 'getConfig').callsFake( arg => arg === 'bidderTimeout' ? 2000 : null); const request = spec.buildRequests([bidRequests[0]], bidderRequest); @@ -519,7 +619,7 @@ describe('TheMediaGrid Adapter', function () { expect(payload.tmax).to.equal(2000); getConfigStub.restore(); }); - it('shold be right tmax when timeout in bidderRequest is less then timeout in config', function() { + it('should be right tmax when timeout in bidderRequest is less then timeout in config', function() { const getConfigStub = sinon.stub(config, 'getConfig').callsFake( arg => arg === 'bidderTimeout' ? 5000 : null); const request = spec.buildRequests([bidRequests[0]], bidderRequest); @@ -581,6 +681,32 @@ describe('TheMediaGrid Adapter', function () { }); }); + it('should contain imp[].instl if available', function() { + const ortb2Imp = [{ + instl: 1 + }, { + instl: 2, + ext: { + data: { + adserver: { + name: 'ad_server_name', + adslot: '/222222/slot' + }, + pbadslot: '/222222/slot' + } + } + }]; + const bidRequestsWithOrtb2Imp = bidRequests.slice(0, 3).map((bid, ind) => { + return Object.assign(ortb2Imp[ind] ? { ortb2Imp: ortb2Imp[ind] } : {}, bid); + }); + const request = spec.buildRequests(bidRequestsWithOrtb2Imp, bidderRequest); + expect(request.data).to.be.an('string'); + const payload = parseRequest(request.data); + expect(payload.imp[0].instl).to.equal(1); + expect(payload.imp[1].instl).to.equal(2); + expect(payload.imp[2].instl).to.be.undefined; + }); + it('all id must be a string', function() { const fpdUserIdNumVal = 2345543345; const getDataFromLocalStorageStub = sinon.stub(storage, 'getDataFromLocalStorage').callsFake( diff --git a/test/spec/modules/gumgumBidAdapter_spec.js b/test/spec/modules/gumgumBidAdapter_spec.js index 93b863bf116..ebbc1c230f1 100644 --- a/test/spec/modules/gumgumBidAdapter_spec.js +++ b/test/spec/modules/gumgumBidAdapter_spec.js @@ -198,8 +198,8 @@ describe('gumgumAdapter', function () { slotRequest.params.slot = invalidSlotId; legacySlotRequest.params.inSlot = invalidSlotId; - req = spec.buildRequests([ slotRequest ])[0]; - legReq = spec.buildRequests([ legacySlotRequest ])[0]; + req = spec.buildRequests([slotRequest])[0]; + legReq = spec.buildRequests([legacySlotRequest])[0]; expect(req.data.si).to.equal(invalidSlotId); expect(legReq.data.si).to.equal(invalidSlotId); @@ -544,6 +544,52 @@ describe('gumgumAdapter', function () { const bidRequest = spec.buildRequests(bidRequests)[0]; expect(!!bidRequest.data.lt).to.be.true; }); + + it('should handle no gg params', function () { + const bidRequest = spec.buildRequests(bidRequests, { refererInfo: { referer: 'https://www.prebid.org/?param1=foo¶m2=bar¶m3=baz' } })[0]; + + // no params are in object + expect(bidRequest.data.hasOwnProperty('eAdBuyId')).to.be.false; + expect(bidRequest.data.hasOwnProperty('adBuyId')).to.be.false; + expect(bidRequest.data.hasOwnProperty('ggdeal')).to.be.false; + }); + + it('should handle encrypted ad buy id', function () { + const bidRequest = spec.buildRequests(bidRequests, { refererInfo: { referer: 'https://www.prebid.org/?param1=foo&ggad=bar¶m3=baz' } })[0]; + + // correct params are in object + expect(bidRequest.data.hasOwnProperty('eAdBuyId')).to.be.true; + expect(bidRequest.data.hasOwnProperty('adBuyId')).to.be.false; + expect(bidRequest.data.hasOwnProperty('ggdeal')).to.be.false; + + // params are stripped from pu property + expect(bidRequest.data.pu.includes('ggad')).to.be.false; + }); + + it('should handle unencrypted ad buy id', function () { + const bidRequest = spec.buildRequests(bidRequests, { refererInfo: { referer: 'https://www.prebid.org/?param1=foo&ggad=123¶m3=baz' } })[0]; + + // correct params are in object + expect(bidRequest.data.hasOwnProperty('eAdBuyId')).to.be.false; + expect(bidRequest.data.hasOwnProperty('adBuyId')).to.be.true; + expect(bidRequest.data.hasOwnProperty('ggdeal')).to.be.false; + + // params are stripped from pu property + expect(bidRequest.data.pu.includes('ggad')).to.be.false; + }); + + it('should handle multiple gg params', function () { + const bidRequest = spec.buildRequests(bidRequests, { refererInfo: { referer: 'https://www.prebid.org/?ggdeal=foo&ggad=bar¶m3=baz' } })[0]; + + // correct params are in object + expect(bidRequest.data.hasOwnProperty('eAdBuyId')).to.be.true; + expect(bidRequest.data.hasOwnProperty('adBuyId')).to.be.false; + expect(bidRequest.data.hasOwnProperty('ggdeal')).to.be.true; + + // params are stripped from pu property + expect(bidRequest.data.pu.includes('ggad')).to.be.false; + expect(bidRequest.data.pu.includes('ggdeal')).to.be.false; + }); }) describe('interpretResponse', function () { @@ -696,7 +742,7 @@ describe('gumgumAdapter', function () { it('uses request size that nearest matches response size for in-screen', function () { const request = { ...bidRequest }; const body = { ...serverResponse }; - const expectedSize = [ 300, 50 ]; + const expectedSize = [300, 50]; let result; request.pi = 2; diff --git a/test/spec/modules/hadronIdSystem_spec.js b/test/spec/modules/hadronIdSystem_spec.js new file mode 100644 index 00000000000..77eeb70326b --- /dev/null +++ b/test/spec/modules/hadronIdSystem_spec.js @@ -0,0 +1,57 @@ +import { hadronIdSubmodule, storage } from 'modules/hadronIdSystem.js'; +import { server } from 'test/mocks/xhr.js'; +import * as utils from 'src/utils.js'; + +describe('HadronIdSystem', function () { + describe('getId', function() { + let getDataFromLocalStorageStub; + + beforeEach(function() { + getDataFromLocalStorageStub = sinon.stub(storage, 'getDataFromLocalStorage'); + }); + + afterEach(function () { + getDataFromLocalStorageStub.restore(); + }); + + it('gets a hadronId', function() { + const config = { + params: {} + }; + const callbackSpy = sinon.spy(); + const callback = hadronIdSubmodule.getId(config).callback; + callback(callbackSpy); + const request = server.requests[0]; + expect(request.url).to.eq(`https://id.hadron.ad.gt/api/v1/pbhid`); + request.respond(200, { 'Content-Type': 'application/json' }, JSON.stringify({ hadronId: 'testHadronId1' })); + expect(callbackSpy.lastCall.lastArg).to.deep.equal({hadronId: 'testHadronId1'}); + }); + + it('gets a cached hadronid', function() { + const config = { + params: {} + }; + getDataFromLocalStorageStub.withArgs('auHadronId').returns('tstCachedHadronId1'); + + const callbackSpy = sinon.spy(); + const callback = hadronIdSubmodule.getId(config).callback; + callback(callbackSpy); + expect(callbackSpy.lastCall.lastArg).to.deep.equal({hadronId: 'tstCachedHadronId1'}); + }); + + it('allows configurable id url', function() { + const config = { + params: { + url: 'https://hadronid.publync.com' + } + }; + const callbackSpy = sinon.spy(); + const callback = hadronIdSubmodule.getId(config).callback; + callback(callbackSpy); + const request = server.requests[0]; + expect(request.url).to.eq('https://hadronid.publync.com'); + request.respond(200, { 'Content-Type': 'application/json' }, JSON.stringify({ hadronId: 'testHadronId1' })); + expect(callbackSpy.lastCall.lastArg).to.deep.equal({hadronId: 'testHadronId1'}); + }); + }); +}); diff --git a/test/spec/modules/hadronRtdProvider_spec.js b/test/spec/modules/hadronRtdProvider_spec.js new file mode 100644 index 00000000000..30e4947566f --- /dev/null +++ b/test/spec/modules/hadronRtdProvider_spec.js @@ -0,0 +1,762 @@ +import {config} from 'src/config.js'; +import {HALOID_LOCAL_NAME, RTD_LOCAL_NAME, addRealTimeData, getRealTimeData, hadronSubmodule, storage} from 'modules/hadronRtdProvider.js'; +import {server} from 'test/mocks/xhr.js'; + +const responseHeader = {'Content-Type': 'application/json'}; + +describe('hadronRtdProvider', function() { + let getDataFromLocalStorageStub; + + beforeEach(function() { + config.resetConfig(); + getDataFromLocalStorageStub = sinon.stub(storage, 'getDataFromLocalStorage'); + }); + + afterEach(function () { + getDataFromLocalStorageStub.restore(); + }); + + describe('hadronSubmodule', function() { + it('successfully instantiates', function () { + expect(hadronSubmodule.init()).to.equal(true); + }); + }); + + describe('Add Real-Time Data', function() { + it('merges ortb2 data', function() { + let rtdConfig = {}; + let bidConfig = {}; + + const setConfigUserObj1 = { + name: 'www.dataprovider1.com', + ext: { taxonomyname: 'iab_audience_taxonomy' }, + segment: [{ + id: '1776' + }] + }; + + const setConfigUserObj2 = { + name: 'www.dataprovider2.com', + ext: { taxonomyname: 'iab_audience_taxonomy' }, + segment: [{ + id: '1914' + }] + }; + + const setConfigSiteObj1 = { + name: 'www.dataprovider3.com', + ext: { + taxonomyname: 'iab_audience_taxonomy' + }, + segment: [ + { + id: '1812' + }, + { + id: '1955' + } + ] + } + + config.setConfig({ + ortb2: { + user: { + data: [setConfigUserObj1, setConfigUserObj2] + }, + site: { + content: { + data: [setConfigSiteObj1] + } + } + } + }); + + const rtdUserObj1 = { + name: 'www.dataprovider4.com', + ext: { + taxonomyname: 'iab_audience_taxonomy' + }, + segment: [ + { + id: '1918' + }, + { + id: '1939' + } + ] + }; + + const rtdSiteObj1 = { + name: 'www.dataprovider5.com', + ext: { + taxonomyname: 'iab_audience_taxonomy' + }, + segment: [ + { + id: '1945' + }, + { + id: '2003' + } + ] + }; + + const rtd = { + ortb2: { + user: { + data: [rtdUserObj1] + }, + site: { + content: { + data: [rtdSiteObj1] + } + } + } + }; + + addRealTimeData(bidConfig, rtd, rtdConfig); + + let ortb2Config = config.getConfig().ortb2; + + expect(ortb2Config.user.data).to.deep.include.members([setConfigUserObj1, setConfigUserObj2, rtdUserObj1]); + expect(ortb2Config.site.content.data).to.deep.include.members([setConfigSiteObj1, rtdSiteObj1]); + }); + + it('merges ortb2 data without duplication', function() { + let rtdConfig = {}; + let bidConfig = {}; + + const userObj1 = { + name: 'www.dataprovider1.com', + ext: { taxonomyname: 'iab_audience_taxonomy' }, + segment: [{ + id: '1776' + }] + }; + + const userObj2 = { + name: 'www.dataprovider2.com', + ext: { taxonomyname: 'iab_audience_taxonomy' }, + segment: [{ + id: '1914' + }] + }; + + const siteObj1 = { + name: 'www.dataprovider3.com', + ext: { + taxonomyname: 'iab_audience_taxonomy' + }, + segment: [ + { + id: '1812' + }, + { + id: '1955' + } + ] + } + + config.setConfig({ + ortb2: { + user: { + data: [userObj1, userObj2] + }, + site: { + content: { + data: [siteObj1] + } + } + } + }); + + const rtd = { + ortb2: { + user: { + data: [userObj1] + }, + site: { + content: { + data: [siteObj1] + } + } + } + }; + + addRealTimeData(bidConfig, rtd, rtdConfig); + + let ortb2Config = config.getConfig().ortb2; + + expect(ortb2Config.user.data).to.deep.include.members([userObj1, userObj2]); + expect(ortb2Config.site.content.data).to.deep.include.members([siteObj1]); + expect(ortb2Config.user.data).to.have.lengthOf(2); + expect(ortb2Config.site.content.data).to.have.lengthOf(1); + }); + + it('merges bidder-specific ortb2 data', function() { + let rtdConfig = {}; + let bidConfig = {}; + + const configUserObj1 = { + name: 'www.dataprovider1.com', + ext: { segtax: 3 }, + segment: [{ + id: '1776' + }] + }; + + const configUserObj2 = { + name: 'www.dataprovider2.com', + ext: { segtax: 3 }, + segment: [{ + id: '1914' + }] + }; + + const configUserObj3 = { + name: 'www.dataprovider1.com', + ext: { segtax: 3 }, + segment: [{ + id: '2003' + }] + }; + + const configSiteObj1 = { + name: 'www.dataprovider3.com', + ext: { + segtax: 1 + }, + segment: [ + { + id: '1812' + }, + { + id: '1955' + } + ] + }; + + const configSiteObj2 = { + name: 'www.dataprovider3.com', + ext: { + segtax: 1 + }, + segment: [ + { + id: '1812' + } + ] + }; + + config.setBidderConfig({ + bidders: ['adbuzz'], + config: { + ortb2: { + user: { + data: [configUserObj1, configUserObj2] + }, + site: { + content: { + data: [configSiteObj1] + } + } + } + } + }); + + config.setBidderConfig({ + bidders: ['pubvisage'], + config: { + ortb2: { + user: { + data: [configUserObj3] + }, + site: { + content: { + data: [configSiteObj2] + } + } + } + } + }); + + const rtdUserObj1 = { + name: 'www.dataprovider4.com', + ext: { + segtax: 501 + }, + segment: [ + { + id: '1918' + }, + { + id: '1939' + } + ] + }; + + const rtdUserObj2 = { + name: 'www.dataprovider2.com', + ext: { + segtax: 502 + }, + segment: [ + { + id: '1939' + } + ] + }; + + const rtdSiteObj1 = { + name: 'www.dataprovider5.com', + ext: { + segtax: 1 + }, + segment: [ + { + id: '441' + }, + { + id: '442' + } + ] + }; + + const rtdSiteObj2 = { + name: 'www.dataprovider6.com', + ext: { + segtax: 2 + }, + segment: [ + { + id: '676' + } + ] + }; + + const rtd = { + ortb2b: { + adbuzz: { + ortb2: { + user: { + data: [rtdUserObj1] + }, + site: { + content: { + data: [rtdSiteObj1] + } + } + } + }, + pubvisage: { + ortb2: { + user: { + data: [rtdUserObj2] + }, + site: { + content: { + data: [rtdSiteObj2] + } + } + } + } + } + }; + + addRealTimeData(bidConfig, rtd, rtdConfig); + + let ortb2Config = config.getBidderConfig().adbuzz.ortb2; + + expect(ortb2Config.user.data).to.deep.include.members([configUserObj1, configUserObj2, rtdUserObj1]); + expect(ortb2Config.site.content.data).to.deep.include.members([configSiteObj1, rtdSiteObj1]); + + ortb2Config = config.getBidderConfig().pubvisage.ortb2; + + expect(ortb2Config.user.data).to.deep.include.members([configUserObj3, rtdUserObj2]); + expect(ortb2Config.site.content.data).to.deep.include.members([configSiteObj2, rtdSiteObj2]); + }); + + it('merges bidder-specific ortb2 data without duplication', function() { + let rtdConfig = {}; + let bidConfig = {}; + + const userObj1 = { + name: 'www.dataprovider1.com', + ext: { segtax: 3 }, + segment: [{ + id: '1776' + }] + }; + + const userObj2 = { + name: 'www.dataprovider2.com', + ext: { segtax: 3 }, + segment: [{ + id: '1914' + }] + }; + + const userObj3 = { + name: 'www.dataprovider1.com', + ext: { segtax: 3 }, + segment: [{ + id: '2003' + }] + }; + + const siteObj1 = { + name: 'www.dataprovider3.com', + ext: { + segtax: 1 + }, + segment: [ + { + id: '1812' + }, + { + id: '1955' + } + ] + }; + + const siteObj2 = { + name: 'www.dataprovider3.com', + ext: { + segtax: 1 + }, + segment: [ + { + id: '1812' + } + ] + }; + + config.setBidderConfig({ + bidders: ['adbuzz'], + config: { + ortb2: { + user: { + data: [userObj1, userObj2] + }, + site: { + content: { + data: [siteObj1] + } + } + } + } + }); + + config.setBidderConfig({ + bidders: ['pubvisage'], + config: { + ortb2: { + user: { + data: [userObj3] + }, + site: { + content: { + data: [siteObj2] + } + } + } + } + }); + + const rtd = { + ortb2b: { + adbuzz: { + ortb2: { + user: { + data: [userObj1] + }, + site: { + content: { + data: [siteObj1] + } + } + } + }, + pubvisage: { + ortb2: { + user: { + data: [userObj2, userObj3] + }, + site: { + content: { + data: [siteObj1, siteObj2] + } + } + } + } + } + }; + + addRealTimeData(bidConfig, rtd, rtdConfig); + + let ortb2Config = config.getBidderConfig().adbuzz.ortb2; + + expect(ortb2Config.user.data).to.deep.include.members([userObj1]); + expect(ortb2Config.site.content.data).to.deep.include.members([siteObj1]); + + expect(ortb2Config.user.data).to.have.lengthOf(2); + expect(ortb2Config.site.content.data).to.have.lengthOf(1); + + ortb2Config = config.getBidderConfig().pubvisage.ortb2; + + expect(ortb2Config.user.data).to.deep.include.members([userObj3, userObj3]); + expect(ortb2Config.site.content.data).to.deep.include.members([siteObj1, siteObj2]); + + expect(ortb2Config.user.data).to.have.lengthOf(2); + expect(ortb2Config.site.content.data).to.have.lengthOf(2); + }); + + it('allows publisher defined rtd ortb2 logic', function() { + const rtdConfig = { + params: { + handleRtd: function(bidConfig, rtd, rtdConfig, pbConfig) { + if (rtd.ortb2.user.data[0].segment[0].id == '1776') { + pbConfig.setConfig({ortb2: rtd.ortb2}); + } else { + pbConfig.setConfig({ortb2: {}}); + } + } + } + }; + + let bidConfig = {}; + + const rtdUserObj1 = { + name: 'www.dataprovider.com', + ext: { taxonomyname: 'iab_audience_taxonomy' }, + segment: [{ + id: '1776' + }] + }; + + let rtd = { + ortb2: { + user: { + data: [rtdUserObj1] + } + } + }; + + config.resetConfig(); + + let pbConfig = config.getConfig(); + addRealTimeData(bidConfig, rtd, rtdConfig); + expect(config.getConfig().ortb2.user.data).to.deep.include.members([rtdUserObj1]); + + const rtdUserObj2 = { + name: 'www.audigent.com', + ext: { + segtax: '1', + taxprovider: '1' + }, + segment: [{ + id: 'pubseg1' + }] + }; + + rtd = { + ortb2: { + user: { + data: [rtdUserObj2] + } + } + }; + + config.resetConfig(); + + pbConfig = config.getConfig(); + addRealTimeData(bidConfig, rtd, rtdConfig); + expect(config.getConfig().ortb2).to.deep.equal({}); + }); + + it('allows publisher defined adunit logic', function() { + const rtdConfig = { + params: { + handleRtd: function(bidConfig, rtd, rtdConfig, pbConfig) { + var adUnits = bidConfig.adUnits; + for (var i = 0; i < adUnits.length; i++) { + var adUnit = adUnits[i]; + for (var j = 0; j < adUnit.bids.length; j++) { + var bid = adUnit.bids[j]; + if (bid.bidder == 'adBuzz') { + for (var k = 0; k < rtd.adBuzz.length; k++) { + bid.adBuzzData.segments.adBuzz.push(rtd.adBuzz[k]); + } + } else if (bid.bidder == 'trueBid') { + for (var k = 0; k < rtd.trueBid.length; k++) { + bid.trueBidSegments.push(rtd.trueBid[k]); + } + } + } + } + } + } + }; + + let bidConfig = { + adUnits: [ + { + bids: [ + { + bidder: 'adBuzz', + adBuzzData: { + segments: { + adBuzz: [ + { + id: 'adBuzzSeg1' + } + ] + } + } + }, + { + bidder: 'trueBid', + trueBidSegments: [] + } + ] + } + ] + }; + + const rtd = { + adBuzz: [{id: 'adBuzzSeg2'}, {id: 'adBuzzSeg3'}], + trueBid: [{id: 'truebidSeg1'}, {id: 'truebidSeg2'}, {id: 'truebidSeg3'}] + }; + + addRealTimeData(bidConfig, rtd, rtdConfig); + + expect(bidConfig.adUnits[0].bids[0].adBuzzData.segments.adBuzz[0].id).to.equal('adBuzzSeg1'); + expect(bidConfig.adUnits[0].bids[0].adBuzzData.segments.adBuzz[1].id).to.equal('adBuzzSeg2'); + expect(bidConfig.adUnits[0].bids[0].adBuzzData.segments.adBuzz[2].id).to.equal('adBuzzSeg3'); + expect(bidConfig.adUnits[0].bids[1].trueBidSegments[0].id).to.equal('truebidSeg1'); + expect(bidConfig.adUnits[0].bids[1].trueBidSegments[1].id).to.equal('truebidSeg2'); + expect(bidConfig.adUnits[0].bids[1].trueBidSegments[2].id).to.equal('truebidSeg3'); + }); + }); + + describe('Get Real-Time Data', function() { + it('gets rtd from local storage cache', function() { + const rtdConfig = { + params: { + segmentCache: true + } + }; + + const bidConfig = {}; + + const rtdUserObj1 = { + name: 'www.dataprovider3.com', + ext: { + taxonomyname: 'iab_audience_taxonomy' + }, + segment: [ + { + id: '1918' + }, + { + id: '1939' + } + ] + }; + + const cachedRtd = { + rtd: { + ortb2: { + user: { + data: [rtdUserObj1] + } + } + } + }; + + getDataFromLocalStorageStub.withArgs(RTD_LOCAL_NAME).returns(JSON.stringify(cachedRtd)); + + expect(config.getConfig().ortb2).to.be.undefined; + getRealTimeData(bidConfig, () => {}, rtdConfig, {}); + expect(config.getConfig().ortb2.user.data).to.deep.include.members([rtdUserObj1]); + }); + + it('gets real-time data via async request', function() { + const setConfigSiteObj1 = { + name: 'www.audigent.com', + ext: { + segtax: '1', + taxprovider: '1' + }, + segment: [ + { + id: 'pubseg1' + }, + { + id: 'pubseg2' + } + ] + } + + config.setConfig({ + ortb2: { + site: { + content: { + data: [setConfigSiteObj1] + } + } + } + }); + + const rtdConfig = { + params: { + segmentCache: false, + usePubHadron: true, + requestParams: { + publisherId: 'testPub1' + } + } + }; + + let bidConfig = {}; + + const rtdUserObj1 = { + name: 'www.audigent.com', + ext: { + segtax: '1', + taxprovider: '1' + }, + segment: [ + { + id: 'pubseg1' + }, + { + id: 'pubseg2' + } + ] + }; + + const data = { + rtd: { + ortb2: { + user: { + data: [rtdUserObj1] + } + } + } + }; + + getDataFromLocalStorageStub.withArgs(HALOID_LOCAL_NAME).returns('testHadronId1'); + getRealTimeData(bidConfig, () => {}, rtdConfig, {}); + + let request = server.requests[0]; + let postData = JSON.parse(request.requestBody); + expect(postData.config).to.have.deep.property('publisherId', 'testPub1'); + expect(postData.userIds).to.have.deep.property('hadronId', 'testHadronId1'); + + request.respond(200, responseHeader, JSON.stringify(data)); + + expect(config.getConfig().ortb2.user.data).to.deep.include.members([rtdUserObj1]); + }); + }); +}); diff --git a/test/spec/modules/iasRtdProvider_spec.js b/test/spec/modules/iasRtdProvider_spec.js index 192b2c6e3c3..0d52c594fb5 100644 --- a/test/spec/modules/iasRtdProvider_spec.js +++ b/test/spec/modules/iasRtdProvider_spec.js @@ -29,7 +29,7 @@ describe('iasRtdProvider is a RTD provider that', function () { const value = iasSubModule.init(config); expect(value).to.equal(false); }); - it('returns false missing pubId param', function () { + it('returns true with only the pubId param', function () { const config = { name: 'ias', waitForIt: true, @@ -40,6 +40,20 @@ describe('iasRtdProvider is a RTD provider that', function () { const value = iasSubModule.init(config); expect(value).to.equal(true); }); + it('returns true with the pubId and keyMappings params', function () { + const config = { + name: 'ias', + waitForIt: true, + params: { + pubId: '123456', + keyMappings: { + 'id': 'ias_id' + } + } + }; + const value = iasSubModule.init(config); + expect(value).to.equal(true); + }); }); describe('has a method `getBidRequestData` that', function () { it('exists', function () { @@ -75,34 +89,43 @@ describe('iasRtdProvider is a RTD provider that', function () { it('exists', function () { expect(iasSubModule.getTargetingData).to.be.a('function'); }); - it('invoke method', function () { - const targeting = iasSubModule.getTargetingData(adUnitsCode, config); - expect(adUnitsCode).to.length(2); - expect(targeting).to.be.not.null; - expect(targeting).to.be.not.empty; - expect(targeting['one-div-id']).to.be.not.null; - const targetingKeys = Object.keys(targeting['one-div-id']); - expect(targetingKeys.length).to.equal(10); - expect(targetingKeys['adt']).to.be.not.null; - expect(targetingKeys['alc']).to.be.not.null; - expect(targetingKeys['dlm']).to.be.not.null; - expect(targetingKeys['drg']).to.be.not.null; - expect(targetingKeys['hat']).to.be.not.null; - expect(targetingKeys['off']).to.be.not.null; - expect(targetingKeys['vio']).to.be.not.null; - expect(targetingKeys['fr']).to.be.not.null; - expect(targetingKeys['ias-kw']).to.be.not.null; - expect(targetingKeys['id']).to.be.not.null; - expect(targeting['one-div-id']['adt']).to.be.eq('veryLow'); - expect(targeting['one-div-id']['alc']).to.be.eq('veryLow'); - expect(targeting['one-div-id']['dlm']).to.be.eq('veryLow'); - expect(targeting['one-div-id']['drg']).to.be.eq('veryLow'); - expect(targeting['one-div-id']['hat']).to.be.eq('veryLow'); - expect(targeting['one-div-id']['off']).to.be.eq('veryLow'); - expect(targeting['one-div-id']['vio']).to.be.eq('veryLow'); - expect(targeting['one-div-id']['fr']).to.be.eq('false'); - expect(targeting['one-div-id']['id']).to.be.eq('4813f7a2-1f22-11ec-9bfd-0a1107f94461'); - }); + describe('invoke method', function () { + it('returns a targeting object with the right shape', function () { + const targeting = iasSubModule.getTargetingData(adUnitsCode, config); + expect(adUnitsCode).to.length(2); + expect(targeting).to.be.not.null; + expect(targeting).to.be.not.empty; + expect(targeting['one-div-id']).to.be.not.null; + }); + it('returns the right keys', function () { + const targeting = iasSubModule.getTargetingData(adUnitsCode, config); + const targetingKeys = Object.keys(targeting['one-div-id']); + expect(targetingKeys.length).to.equal(10); + expect(targetingKeys).to.include('adt', 'adt key missing from the targeting object'); + expect(targetingKeys).to.include('alc', 'alc key missing from the targeting object'); + expect(targetingKeys).to.include('dlm', 'dlm key missing from the targeting object'); + expect(targetingKeys).to.include('drg', 'drg key missing from the targeting object'); + expect(targetingKeys).to.include('hat', 'hat key missing from the targeting object'); + expect(targetingKeys).to.include('off', 'off key missing from the targeting object'); + expect(targetingKeys).to.include('vio', 'vio key missing from the targeting object'); + expect(targetingKeys).to.include('fr', 'fr key missing from the targeting object'); + expect(targetingKeys).to.include('ias-kw', 'ias-kw key missing from the targeting object'); + expect(targetingKeys).to.not.include('id', 'id key present in the targeting object, should have been renamed to ias_id'); + expect(targetingKeys).to.include('ias_id', 'ias_id key missing from the targeting object'); + }); + it('returns the right values', function () { + const targeting = iasSubModule.getTargetingData(adUnitsCode, config); + expect(targeting['one-div-id']['adt']).to.be.eq('veryLow'); + expect(targeting['one-div-id']['alc']).to.be.eq('veryLow'); + expect(targeting['one-div-id']['dlm']).to.be.eq('veryLow'); + expect(targeting['one-div-id']['drg']).to.be.eq('veryLow'); + expect(targeting['one-div-id']['hat']).to.be.eq('veryLow'); + expect(targeting['one-div-id']['off']).to.be.eq('veryLow'); + expect(targeting['one-div-id']['vio']).to.be.eq('veryLow'); + expect(targeting['one-div-id']['fr']).to.be.eq('false'); + expect(targeting['one-div-id']['ias_id']).to.be.eq('4813f7a2-1f22-11ec-9bfd-0a1107f94461'); + }); + }) }); }); @@ -110,7 +133,10 @@ const config = { name: 'ias', waitForIt: true, params: { - pubId: 1234 + pubId: 1234, + keyMappings: { + 'id': 'ias_id' + } } }; diff --git a/test/spec/modules/id5AnalyticsAdapter_spec.js b/test/spec/modules/id5AnalyticsAdapter_spec.js index be5998967c9..83951c3a6e9 100644 --- a/test/spec/modules/id5AnalyticsAdapter_spec.js +++ b/test/spec/modules/id5AnalyticsAdapter_spec.js @@ -2,7 +2,7 @@ import adapterManager from '../../../src/adapterManager.js'; import id5AnalyticsAdapter from '../../../modules/id5AnalyticsAdapter.js'; import { expect } from 'chai'; import sinon from 'sinon'; -import events from '../../../src/events.js'; +import * as events from '../../../src/events.js'; import constants from '../../../src/constants.json'; import { generateUUID } from '../../../src/utils.js'; diff --git a/test/spec/modules/id5IdSystem_spec.js b/test/spec/modules/id5IdSystem_spec.js index debde20e4c0..02020fc2d3a 100644 --- a/test/spec/modules/id5IdSystem_spec.js +++ b/test/spec/modules/id5IdSystem_spec.js @@ -13,7 +13,7 @@ import { import { init, requestBidsHook, setSubmoduleRegistry, coreStorage } from 'modules/userId/index.js'; import { config } from 'src/config.js'; import { server } from 'test/mocks/xhr.js'; -import events from 'src/events.js'; +import * as events from 'src/events.js'; import CONSTANTS from 'src/constants.json'; import * as utils from 'src/utils.js'; diff --git a/test/spec/modules/idWardRtdProvider_spec.js b/test/spec/modules/idWardRtdProvider_spec.js new file mode 100644 index 00000000000..949365baec6 --- /dev/null +++ b/test/spec/modules/idWardRtdProvider_spec.js @@ -0,0 +1,113 @@ +import {config} from 'src/config.js'; +import {getRealTimeData, idWardRtdSubmodule, storage} from 'modules/idWardRtdProvider.js'; + +describe('idWardRtdProvider', function() { + let getDataFromLocalStorageStub; + + const testReqBidsConfigObj = { + adUnits: [ + { + bids: ['bid1', 'bid2'] + } + ] + }; + + const onDone = function() { return true }; + + const cmoduleConfig = { + 'name': 'idWard', + 'params': { + 'cohortStorageKey': 'cohort_ids' + } + } + + beforeEach(function() { + config.resetConfig(); + getDataFromLocalStorageStub = sinon.stub(storage, 'getDataFromLocalStorage') + }); + + afterEach(function () { + getDataFromLocalStorageStub.restore(); + }); + + describe('idWardRtdSubmodule', function() { + it('successfully instantiates', function () { + expect(idWardRtdSubmodule.init()).to.equal(true); + }); + }); + + describe('Get Real-Time Data', function() { + it('gets rtd from local storage', function() { + const rtdConfig = { + params: { + cohortStorageKey: 'cohort_ids', + segtax: 503 + } + }; + + const bidConfig = {}; + + const rtdUserObj1 = { + name: 'id-ward.com', + ext: { + segtax: 503 + }, + segment: [ + { + id: 'TCZPQOWPEJG3MJOTUQUF793A' + }, + { + id: '93SUG3H540WBJMYNT03KX8N3' + } + ] + }; + + getDataFromLocalStorageStub.withArgs('cohort_ids') + .returns(JSON.stringify(['TCZPQOWPEJG3MJOTUQUF793A', '93SUG3H540WBJMYNT03KX8N3'])); + + expect(config.getConfig().ortb2).to.be.undefined; + getRealTimeData(bidConfig, () => {}, rtdConfig, {}); + expect(config.getConfig().ortb2.user.data).to.deep.include.members([rtdUserObj1]); + }); + + it('do not set rtd if local storage empty', function() { + const rtdConfig = { + params: { + cohortStorageKey: 'cohort_ids', + segtax: 503 + } + }; + + const bidConfig = {}; + + getDataFromLocalStorageStub.withArgs('cohort_ids') + .returns(null); + + expect(config.getConfig().ortb2).to.be.undefined; + getRealTimeData(bidConfig, () => {}, rtdConfig, {}); + expect(config.getConfig().ortb2).to.be.undefined; + }); + + it('do not set rtd if local storage has incorrect value', function() { + const rtdConfig = { + params: { + cohortStorageKey: 'cohort_ids', + segtax: 503 + } + }; + + const bidConfig = {}; + + getDataFromLocalStorageStub.withArgs('cohort_ids') + .returns('wrong cohort ids value'); + + expect(config.getConfig().ortb2).to.be.undefined; + getRealTimeData(bidConfig, () => {}, rtdConfig, {}); + expect(config.getConfig().ortb2).to.be.undefined; + }); + + it('should initalise and return with config', function () { + expect(getRealTimeData(testReqBidsConfigObj, onDone, cmoduleConfig)).to.equal(undefined) + }); + }); +}); diff --git a/test/spec/modules/idxIdSystem_spec.js b/test/spec/modules/idxIdSystem_spec.js index 14cd9a88d13..5cecc12c253 100644 --- a/test/spec/modules/idxIdSystem_spec.js +++ b/test/spec/modules/idxIdSystem_spec.js @@ -1,5 +1,5 @@ import { expect } from 'chai'; -import find from 'core-js-pure/features/array/find.js'; +import {find} from 'src/polyfill.js'; import { config } from 'src/config.js'; import { init, requestBidsHook, setSubmoduleRegistry } from 'modules/userId/index.js'; import { storage, idxIdSubmodule } from 'modules/idxIdSystem.js'; diff --git a/test/spec/modules/imRtdProvider_spec.js b/test/spec/modules/imRtdProvider_spec.js index e3365e8eaf2..6d92440a144 100644 --- a/test/spec/modules/imRtdProvider_spec.js +++ b/test/spec/modules/imRtdProvider_spec.js @@ -51,7 +51,8 @@ describe('imRtdProvider', function () { describe('getBidderFunction', function () { const assumedBidder = [ 'ix', - 'pubmatic' + 'pubmatic', + 'fluct' ]; assumedBidder.forEach(bidderName => { it(`should return bidderFunction with assumed bidder: ${bidderName}`, function () { @@ -70,6 +71,34 @@ describe('imRtdProvider', function () { it(`should return null with unexpected bidder`, function () { expect(getBidderFunction('test')).to.equal(null); }); + describe('fluct bidder function', function () { + it('should return a bid w/o im_segments if not any exists', function () { + const bid = {bidder: 'fluct'}; + expect(getBidderFunction('fluct')(bid, '')).to.eql(bid); + }); + it('should return a bid w/ im_segments if any exists', function () { + const bid = { + bidder: 'fluct', + params: { + kv: { + foo: ['foo1'] + } + } + }; + expect(getBidderFunction('fluct')(bid, {im_segments: ['12345', '67890']})) + .to.eql( + { + bidder: 'fluct', + params: { + kv: { + foo: ['foo1'], + imsids: ['12345', '67890'] + } + } + } + ); + }); + }); }) describe('getCustomBidderFunction', function () { diff --git a/test/spec/modules/improvedigitalBidAdapter_spec.js b/test/spec/modules/improvedigitalBidAdapter_spec.js index 7d6099e0de6..9dcc11f5aa1 100644 --- a/test/spec/modules/improvedigitalBidAdapter_spec.js +++ b/test/spec/modules/improvedigitalBidAdapter_spec.js @@ -1,14 +1,14 @@ import { expect } from 'chai'; -import { ImproveDigitalAdServerJSClient, spec } from 'modules/improvedigitalBidAdapter.js'; +import { spec } from 'modules/improvedigitalBidAdapter.js'; import { config } from 'src/config.js'; import * as utils from 'src/utils.js'; +import {BANNER, VIDEO} from '../../../src/mediaTypes'; describe('Improve Digital Adapter Tests', function () { - const idClient = new ImproveDigitalAdServerJSClient('hb'); - - const METHOD = 'GET'; - const URL = 'https://ice.360yield.com/hb'; - const PARAM_PREFIX = 'jsonp='; + const METHOD = 'POST'; + const URL = 'https://ad.360yield.com/pb'; + const INSTREAM_TYPE = 1; + const OUTSTREAM_TYPE = 3; const simpleBidRequest = { bidder: 'improvedigital', @@ -22,10 +22,10 @@ describe('Improve Digital Adapter Tests', function () { auctionId: '192721e36a0239', mediaTypes: { banner: { - sizes: [[300, 250], [160, 600], ['blah', 150], [-1, 300], [300, -5]] + sizes: [[300, 250], [160, 600]] } }, - sizes: [[300, 250], [160, 600], ['blah', 150], [-1, 300], [300, -5]] + sizes: [[300, 250], [160, 600]] }; const videoParams = { @@ -53,7 +53,7 @@ describe('Improve Digital Adapter Tests', function () { const multiFormatBidRequest = utils.deepClone(simpleBidRequest); multiFormatBidRequest.mediaTypes = { banner: { - sizes: [[300, 250], [160, 600], ['blah', 150], [-1, 300], [300, -5]] + sizes: [[300, 250], [160, 600]] }, video: { context: 'outstream', @@ -145,66 +145,85 @@ describe('Improve Digital Adapter Tests', function () { describe('buildRequests', function () { it('should make a well-formed request objects', function () { - const requests = spec.buildRequests([simpleBidRequest], bidderRequest); - expect(requests).to.be.an('array'); - expect(requests.length).to.equal(1); - - const request = requests[0]; + const getConfigStub = sinon.stub(config, 'getConfig'); + getConfigStub.withArgs('improvedigital.usePrebidSizes').returns(true); + const request = spec.buildRequests([simpleBidRequest], bidderRequest)[0]; + expect(request).to.be.an('object'); expect(request.method).to.equal(METHOD); expect(request.url).to.equal(URL); expect(request.bidderRequest).to.deep.equal(bidderRequest); - expect(request.data.substring(0, PARAM_PREFIX.length)).to.equal(PARAM_PREFIX); - - const params = JSON.parse(decodeURIComponent(request.data.substring(PARAM_PREFIX.length))); - expect(params.bid_request).to.be.an('object'); - expect(params.bid_request.id).to.be.a('string'); - expect(params.bid_request.version).to.equal(`${spec.version}-${idClient.CONSTANTS.CLIENT_VERSION}`); - expect(params.bid_request.gdpr).to.not.exist; - expect(params.bid_request.us_privacy).to.not.exist; - expect(params.bid_request.schain).to.not.exist; - expect(params.bid_request.user).to.not.exist; - expect(params.bid_request.imp).to.deep.equal([ + + const payload = JSON.parse(request.data); + expect(payload).to.be.an('object'); + expect(payload.id).to.be.a('string'); + expect(payload.tmax).not.to.exist; + expect(payload.cur).to.be.an('array'); + expect(payload.regs).to.not.exist; + expect(payload.schain).to.not.exist; + expect(payload.source).to.be.an('object'); + expect(payload.device).to.be.an('object'); + expect(payload.user).to.not.exist; + expect(payload.imp).to.deep.equal([ { id: '33e9500b21129f', - pid: 1053688, - tid: 'f183e871-fbed-45f0-a427-c8a63c4c01eb', - banner: {} + secure: 0, + ext: { + bidder: { + placementId: 1053688, + } + }, + banner: { + format: [ + {w: 300, h: 250}, + {w: 160, h: 600}, + ] + } } ]); + getConfigStub.restore(); }); it('should make a well-formed request object for multi-format ad unit', function () { - const requests = spec.buildRequests([multiFormatBidRequest], multiFormatBidderRequest); - expect(requests).to.be.an('array'); - expect(requests.length).to.equal(1); - - const request = requests[0]; + const getConfigStub = sinon.stub(config, 'getConfig'); + getConfigStub.withArgs('improvedigital.usePrebidSizes').returns(true); + const request = spec.buildRequests([multiFormatBidRequest], multiFormatBidderRequest)[0]; + expect(request).to.be.an('object'); expect(request.method).to.equal(METHOD); expect(request.url).to.equal(URL); expect(request.bidderRequest).to.deep.equal(multiFormatBidderRequest); - expect(request.data.substring(0, PARAM_PREFIX.length)).to.equal(PARAM_PREFIX); - - const params = JSON.parse(decodeURIComponent(request.data.substring(PARAM_PREFIX.length))); - expect(params.bid_request).to.be.an('object'); - expect(params.bid_request.id).to.be.a('string'); - expect(params.bid_request.version).to.equal(`${spec.version}-${idClient.CONSTANTS.CLIENT_VERSION}`); - expect(params.bid_request.gdpr).to.not.exist; - expect(params.bid_request.us_privacy).to.not.exist; - expect(params.bid_request.imp).to.deep.equal([ + + const payload = JSON.parse(request.data); + expect(payload).to.be.an('object'); + expect(payload.imp).to.deep.equal([ { id: '33e9500b21129f', - pid: 1053688, - tid: 'f183e871-fbed-45f0-a427-c8a63c4c01eb', - banner: {} + secure: 0, + ext: { + bidder: { + placementId: 1053688, + } + }, + video: { + placement: OUTSTREAM_TYPE, + w: 640, + h: 480, + mimes: ['video/mp4'], + }, + banner: { + format: [ + {w: 300, h: 250}, + {w: 160, h: 600}, + ] + } } ]); + getConfigStub.restore(); }); it('should set placementKey and publisherId for smart tags', function () { - const requests = spec.buildRequests([simpleSmartTagBidRequest], bidderRequest); - const params = JSON.parse(decodeURIComponent(requests[0].data.substring(PARAM_PREFIX.length))); - expect(params.bid_request.imp[0].pubid).to.equal(1032); - expect(params.bid_request.imp[0].pkey).to.equal('data_team_test_hb_smoke_test'); + const payload = JSON.parse(spec.buildRequests([simpleSmartTagBidRequest], bidderRequest)[0].data); + expect(payload.imp[0].ext.bidder.publisherId).to.equal(1032); + expect(payload.imp[0].ext.bidder.placementKey).to.equal('data_team_test_hb_smoke_test'); }); it('should add keyValues', function () { @@ -215,102 +234,92 @@ describe('Improve Digital Adapter Tests', function () { ] }; bidRequest.params.keyValues = keyValues; - const request = spec.buildRequests([bidRequest], bidderRequest)[0]; - const params = JSON.parse(decodeURIComponent(request.data.substring(PARAM_PREFIX.length))); - expect(params.bid_request.imp[0].kvw).to.deep.equal(keyValues); + const payload = JSON.parse(spec.buildRequests([bidRequest], bidderRequest)[0].data); + expect(payload.imp[0].ext.bidder.keyValues).to.deep.equal(keyValues); }); - it('should add single size filter', function () { - const bidRequest = Object.assign({}, simpleBidRequest); - const size = { - w: 800, - h: 600 - }; - bidRequest.params.size = size; - const request = spec.buildRequests([bidRequest], bidderRequest)[0]; - const params = JSON.parse(decodeURIComponent(request.data.substring(PARAM_PREFIX.length))); - expect(params.bid_request.imp[0].banner).to.deep.equal(size); - // When single size filter is set, format shouldn't be populated. This - // is to maintain backward compatibily - expect(params.bid_request.imp[0].banner.format).to.not.exist; - }); + // it('should add single size filter', function () { + // const bidRequest = Object.assign({}, simpleBidRequest); + // const size = { + // w: 800, + // h: 600 + // }; + // bidRequest.params.size = size; + // const payload = JSON.parse(spec.buildRequests([bidRequest], bidderRequest).data); + // expect(payload.imp[0].banner).to.deep.equal(size); + // // When single size filter is set, format shouldn't be populated. This + // // is to maintain backward compatibily + // expect(payload.imp[0].banner.format).to.not.exist; + // }); it('should add currency', function () { const bidRequest = Object.assign({}, simpleBidRequest); const getConfigStub = sinon.stub(config, 'getConfig').returns('JPY'); - const request = spec.buildRequests([bidRequest], bidderRequest)[0]; - const params = JSON.parse(decodeURIComponent(request.data.substring(PARAM_PREFIX.length))); - expect(params.bid_request.imp[0].currency).to.equal('JPY'); + const payload = JSON.parse(spec.buildRequests([bidRequest], bidderRequest)[0].data); + expect(payload.cur).to.deep.equal(['JPY']); getConfigStub.restore(); }); it('should add bid floor', function () { const bidRequest = Object.assign({}, simpleBidRequest); - let request = spec.buildRequests([bidRequest], bidderRequest)[0]; - let params = JSON.parse(decodeURIComponent(request.data.substring(PARAM_PREFIX.length))); + let payload = JSON.parse(spec.buildRequests([bidRequest], bidderRequest)[0].data); // Floor price currency shouldn't be populated without a floor price - expect(params.bid_request.imp[0].bidfloorcur).to.not.exist; + expect(payload.imp[0].bidfloorcur).to.not.exist; // Default floor price currency bidRequest.params.bidFloor = 0.05; - request = spec.buildRequests([bidRequest], bidderRequest)[0]; - params = JSON.parse(decodeURIComponent(request.data.substring(PARAM_PREFIX.length))); - expect(params.bid_request.imp[0].bidfloor).to.equal(0.05); - expect(params.bid_request.imp[0].bidfloorcur).to.equal('USD'); + payload = JSON.parse(spec.buildRequests([bidRequest], bidderRequest)[0].data); + expect(payload.imp[0].bidfloor).to.equal(0.05); + expect(payload.imp[0].bidfloorcur).to.equal('USD'); // Floor price currency bidRequest.params.bidFloorCur = 'eUR'; - request = spec.buildRequests([bidRequest])[0]; - params = JSON.parse(decodeURIComponent(request.data.substring(PARAM_PREFIX.length))); - expect(params.bid_request.imp[0].bidfloor).to.equal(0.05); - expect(params.bid_request.imp[0].bidfloorcur).to.equal('EUR'); + payload = JSON.parse(spec.buildRequests([bidRequest], bidderRequest)[0].data); + expect(payload.imp[0].bidfloor).to.equal(0.05); + expect(payload.imp[0].bidfloorcur).to.equal('EUR'); // getFloor defined -> use it over bidFloor let getFloorResponse = { currency: 'USD', floor: 3 }; bidRequest.getFloor = () => getFloorResponse; - request = spec.buildRequests([bidRequest])[0]; - params = JSON.parse(decodeURIComponent(request.data.substring(PARAM_PREFIX.length))); - expect(params.bid_request.imp[0].bidfloor).to.equal(3); - expect(params.bid_request.imp[0].bidfloorcur).to.equal('USD'); + payload = JSON.parse(spec.buildRequests([bidRequest], bidderRequest)[0].data); + expect(payload.imp[0].bidfloor).to.equal(3); + // expect(payload.imp[0].bidfloorcur).to.equal('USD'); }); it('should add GDPR consent string', function () { const bidRequest = Object.assign({}, simpleBidRequest); - const request = spec.buildRequests([bidRequest], bidderRequestGdpr)[0]; - const params = JSON.parse(decodeURIComponent(request.data.substring(PARAM_PREFIX.length))); - expect(params.bid_request.gdpr).to.equal('BOJ/P2HOJ/P2HABABMAAAAAZ+A=='); - expect(params.bid_request.user.ext.consented_providers_settings.consented_providers).to.exist.and.to.deep.equal([1, 35, 41, 101]); + const payload = JSON.parse(spec.buildRequests([bidRequest], bidderRequestGdpr)[0].data); + expect(payload.regs.ext.gdpr).to.exist.and.to.equal(1); + expect(payload.user.ext.consent).to.equal('BOJ/P2HOJ/P2HABABMAAAAAZ+A=='); + expect(payload.user.ext.consented_providers_settings.consented_providers).to.exist.and.to.deep.equal([1, 35, 41, 101]); }); it('should add CCPA consent string', function () { const bidRequest = Object.assign({}, simpleBidRequest); - const request = spec.buildRequests([bidRequest], { uspConsent: '1YYY' })[0]; - const params = JSON.parse(decodeURIComponent(request.data.substring(PARAM_PREFIX.length))); - expect(params.bid_request.us_privacy).to.equal('1YYY'); + const request = spec.buildRequests([bidRequest], {...bidderRequest, ...{ uspConsent: '1YYY' }}); + const payload = JSON.parse(request[0].data); + expect(payload.regs.ext.us_privacy).to.equal('1YYY'); }); it('should add referrer', function () { const bidRequest = Object.assign({}, simpleBidRequest); const request = spec.buildRequests([bidRequest], bidderRequestReferrer)[0]; - const params = JSON.parse(decodeURIComponent(request.data.substring(PARAM_PREFIX.length))); - expect(params.bid_request.referrer).to.equal('https://blah.com/test.html'); + const payload = JSON.parse(request.data); + expect(payload.site.page).to.equal('https://blah.com/test.html'); }); it('should not add video params for banner', function () { const bidRequest = JSON.parse(JSON.stringify(simpleBidRequest)); bidRequest.params.video = videoParams; const request = spec.buildRequests([bidRequest], bidderRequest)[0]; - const params = JSON.parse(decodeURIComponent(request.data.substring(PARAM_PREFIX.length))); - expect(params.bid_request.imp[0].video).to.not.exist; + const payload = JSON.parse(request.data); + expect(payload.imp[0].video).to.not.exist; }); - it('should add ad type for instream video', function () { + it('should add correct placement value for instream and outstream video', function () { let bidRequest = JSON.parse(JSON.stringify(simpleBidRequest)); - bidRequest.mediaType = 'video'; - let request = spec.buildRequests([bidRequest], bidderRequest)[0]; - let params = JSON.parse(decodeURIComponent(request.data.substring(PARAM_PREFIX.length))); - expect(params.bid_request.imp[0].ad_types).to.deep.equal(['video']); - expect(params.bid_request.imp[0].video).to.not.exist; + let payload = JSON.parse(spec.buildRequests([bidRequest], bidderRequest)[0].data); + expect(payload.imp[0].video).to.not.exist; bidRequest = JSON.parse(JSON.stringify(simpleBidRequest)); bidRequest.mediaTypes = { @@ -319,32 +328,45 @@ describe('Improve Digital Adapter Tests', function () { playerSize: [640, 480] } }; - request = spec.buildRequests([bidRequest], bidderRequest)[0]; - params = JSON.parse(decodeURIComponent(request.data.substring(PARAM_PREFIX.length))); - expect(params.bid_request.imp[0].ad_types).to.deep.equal(['video']); - expect(params.bid_request.imp[0].video).to.not.exist; - }); - - it('should not set ad type for outstream video', function() { - const request = spec.buildRequests([outstreamBidRequest])[0]; - const params = JSON.parse(decodeURIComponent(request.data.substring(PARAM_PREFIX.length))); - expect(params.bid_request.imp[0].ad_types).to.not.exist; - expect(params.bid_request.imp[0].video).to.not.exist; - }); - - it('should not set ad type for multi-format bids', function() { - const request = spec.buildRequests([multiFormatBidRequest], bidderRequest)[0]; - const params = JSON.parse(decodeURIComponent(request.data.substring(PARAM_PREFIX.length))); - expect(params.bid_request.imp[0].ad_types).to.not.exist; - expect(params.bid_request.imp[0].video).to.not.exist; + payload = JSON.parse(spec.buildRequests([bidRequest], bidderRequest)[0].data); + expect(payload.imp[0].video.placement).to.exist.and.equal(1); + bidRequest.mediaTypes.video.context = 'outstream'; + payload = JSON.parse(spec.buildRequests([bidRequest], bidderRequest)[0].data); + expect(payload.imp[0].video.placement).to.exist.and.equal(3); }); it('should set video params for instream', function() { const bidRequest = JSON.parse(JSON.stringify(instreamBidRequest)); + delete bidRequest.mediaTypes.video.playerSize; + const videoParams = { + mimes: ['video/mp4'], + skip: 1, + skipmin: 5, + skipafter: 30, + minduration: 15, + maxduration: 60, + startdelay: 5, + minbitrate: 500, + maxbitrate: 2000, + w: 1024, + h: 640, + placement: INSTREAM_TYPE, + }; bidRequest.params.video = videoParams; - const request = spec.buildRequests([bidRequest])[0]; - const params = JSON.parse(decodeURIComponent(request.data.substring(PARAM_PREFIX.length))); - expect(params.bid_request.imp[0].video).to.deep.equal(videoParams); + const request = spec.buildRequests([bidRequest], bidderRequest)[0]; + const payload = JSON.parse(request.data); + expect(payload.imp[0].video).to.deep.equal(videoParams); + }); + + it('should set video playerSize over video params', () => { + const bidRequest = JSON.parse(JSON.stringify(instreamBidRequest)); + bidRequest.params.video = { + w: 1024, h: 640 + } + const request = spec.buildRequests([bidRequest], bidderRequest)[0]; + const payload = JSON.parse(request.data); + expect(payload.imp[0].video.h).equal(480); + expect(payload.imp[0].video.w).equal(640); }); it('should set skip params only if skip=1', function() { @@ -357,22 +379,28 @@ describe('Improve Digital Adapter Tests', function () { } bidRequest.params.video = videoTest; let request = spec.buildRequests([bidRequest])[0]; - let params = JSON.parse(decodeURIComponent(request.data.substring(PARAM_PREFIX.length))); - expect(params.bid_request.imp[0].video).to.deep.equal(videoTest); + let payload = JSON.parse(request.data); + expect(payload.imp[0].video.skip).to.equal(1); + expect(payload.imp[0].video.skipmin).to.equal(5); + expect(payload.imp[0].video.skipafter).to.equal(30); // 0 - leave out skipmin and skipafter videoTest.skip = 0; bidRequest.params.video = videoTest; request = spec.buildRequests([bidRequest])[0]; - params = JSON.parse(decodeURIComponent(request.data.substring(PARAM_PREFIX.length))); - expect(params.bid_request.imp[0].video).to.deep.equal({ skip: 0 }); + payload = JSON.parse(request.data); + expect(payload.imp[0].video.skip).to.equal(0); + expect(payload.imp[0].video.skipmin).to.not.exist; + expect(payload.imp[0].video.skipafter).to.not.exist; // other videoTest.skip = 'blah'; bidRequest.params.video = videoTest; request = spec.buildRequests([bidRequest])[0]; - params = JSON.parse(decodeURIComponent(request.data.substring(PARAM_PREFIX.length))); - expect(params.bid_request.imp[0].video).to.not.exist; + payload = JSON.parse(request.data); + expect(payload.imp[0].video.skip).to.not.exist; + expect(payload.imp[0].video.skipmin).to.not.exist; + expect(payload.imp[0].video.skipafter).to.not.exist; }); it('should ignore invalid/unexpected video params', function() { @@ -387,51 +415,36 @@ describe('Improve Digital Adapter Tests', function () { videoTestInvParam.blah = 1; bidRequest.params.video = videoTestInvParam; let request = spec.buildRequests([bidRequest])[0]; - let params = JSON.parse(decodeURIComponent(request.data.substring(PARAM_PREFIX.length))); - expect(params.bid_request.imp[0].video).to.deep.equal(videoTest); + let payload = JSON.parse(request.data); + expect(payload.imp[0].video.blah).not.to.exist; }); it('should set video params for outstream', function() { const bidRequest = JSON.parse(JSON.stringify(outstreamBidRequest)); bidRequest.params.video = videoParams; const request = spec.buildRequests([bidRequest])[0]; - const params = JSON.parse(decodeURIComponent(request.data.substring(PARAM_PREFIX.length))); - expect(params.bid_request.imp[0].video).to.deep.equal(videoParams); + const payload = JSON.parse(request.data); + expect(payload.imp[0].video).to.deep.equal({...{ + mimes: ['video/mp4'], + placement: OUTSTREAM_TYPE, + w: bidRequest.mediaTypes.video.playerSize[0], + h: bidRequest.mediaTypes.video.playerSize[1], + }, + ...videoParams}); }); - + // it('should set video params for multi-format', function() { const bidRequest = JSON.parse(JSON.stringify(multiFormatBidRequest)); bidRequest.params.video = videoParams; const request = spec.buildRequests([bidRequest])[0]; - const params = JSON.parse(decodeURIComponent(request.data.substring(PARAM_PREFIX.length))); - expect(params.bid_request.imp[0].video).to.deep.equal(videoParams); - }); - - it('should not set Prebid sizes in bid request for instream video', function () { - const getConfigStub = sinon.stub(config, 'getConfig'); - getConfigStub.withArgs('improvedigital.usePrebidSizes').returns(true); - const request = spec.buildRequests([instreamBidRequest], bidderRequest)[0]; - const params = JSON.parse(decodeURIComponent(request.data.substring(PARAM_PREFIX.length))); - expect(params.bid_request.imp[0].banner.format).to.not.exist; - getConfigStub.restore(); - }); - - it('should not set Prebid sizes in bid request for outstream video', function () { - const getConfigStub = sinon.stub(config, 'getConfig'); - getConfigStub.withArgs('improvedigital.usePrebidSizes').returns(true); - const request = spec.buildRequests([outstreamBidRequest], bidderRequest)[0]; - const params = JSON.parse(decodeURIComponent(request.data.substring(PARAM_PREFIX.length))); - expect(params.bid_request.imp[0].banner.format).to.not.exist; - getConfigStub.restore(); - }); - - it('should not set Prebid sizes in multi-format bid request', function () { - const getConfigStub = sinon.stub(config, 'getConfig'); - getConfigStub.withArgs('improvedigital.usePrebidSizes').returns(true); - const request = spec.buildRequests([multiFormatBidRequest], bidderRequest)[0]; - const params = JSON.parse(decodeURIComponent(request.data.substring(PARAM_PREFIX.length))); - expect(params.bid_request.imp[0].banner.format).to.not.exist; - getConfigStub.restore(); + const payload = JSON.parse(request.data); + const testVideoParams = Object.assign({ + placement: OUTSTREAM_TYPE, + w: 640, + h: 480, + mimes: ['video/mp4'], + }, videoParams); + expect(payload.imp[0].video).to.deep.equal(testVideoParams); }); it('should add schain', function () { @@ -439,8 +452,8 @@ describe('Improve Digital Adapter Tests', function () { const bidRequest = Object.assign({}, simpleBidRequest); bidRequest.schain = schain; const request = spec.buildRequests([bidRequest], bidderRequestReferrer)[0]; - const params = JSON.parse(decodeURIComponent(request.data.substring(PARAM_PREFIX.length))); - expect(params.bid_request.schain).to.equal(schain); + const payload = JSON.parse(request.data); + expect(payload.source.ext.schain).to.equal(schain); }); it('should add eids', function () { @@ -455,8 +468,8 @@ describe('Improve Digital Adapter Tests', function () { const bidRequest = Object.assign({}, simpleBidRequest); bidRequest.userId = userId; const request = spec.buildRequests([bidRequest], bidderRequestReferrer)[0]; - const params = JSON.parse(decodeURIComponent(request.data.substring(PARAM_PREFIX.length))); - expect(params.bid_request.user).to.deep.equal(expectedUserObject); + const payload = JSON.parse(request.data); + expect(payload.user).to.deep.equal(expectedUserObject); }); it('should return 2 requests', function () { @@ -486,8 +499,8 @@ describe('Improve Digital Adapter Tests', function () { const getConfigStub = sinon.stub(config, 'getConfig'); getConfigStub.withArgs('improvedigital.usePrebidSizes').returns(true); const request = spec.buildRequests([simpleBidRequest], bidderRequest)[0]; - const params = JSON.parse(decodeURIComponent(request.data.substring(PARAM_PREFIX.length))); - expect(params.bid_request.imp[0].banner).to.deep.equal({ + const payload = JSON.parse(request.data); + expect(payload.imp[0].banner).to.deep.equal({ format: [ { w: 300, h: 250 }, { w: 160, h: 600 } @@ -506,8 +519,8 @@ describe('Improve Digital Adapter Tests', function () { }; bidRequest.params.size = size; const request = spec.buildRequests([bidRequest], bidderRequest)[0]; - const params = JSON.parse(decodeURIComponent(request.data.substring(PARAM_PREFIX.length))); - expect(params.bid_request.imp[0].banner).to.deep.equal({ + const payload = JSON.parse(request.data); + expect(payload.imp[0].banner).to.deep.equal({ format: [ { w: 300, h: 250 }, { w: 160, h: 600 } @@ -516,388 +529,396 @@ describe('Improve Digital Adapter Tests', function () { getConfigStub.restore(); }); - it('should set pagecat and genre ➞ fpd:ortb2.site', function() { - config.setConfig(JSON.parse('{"ortb2":{"site":{"cat":["IAB2"],"pagecat":["IAB2-2"],"content":{"genre":"Adventure"}}}}')); - const bidRequest = Object.assign({}, simpleBidRequest); - const request = spec.buildRequests([bidRequest], bidderRequestReferrer)[0]; - const params = JSON.parse(decodeURIComponent(request.data.substring(PARAM_PREFIX.length))); - expect(params.bid_request.pagecat).to.be.an('array'); - expect(params.bid_request.pagecat).to.deep.equal(['IAB2-2']); - expect(params.bid_request.genre).to.be.a('string'); - expect(params.bid_request.genre).be.equal('Adventure'); + it('should set GPID and Instl Signal', function () { + const bidRequest = Object.assign({ + ortb2Imp: { + instl: true, + ext: { + gpid: '/123/ID-FORMAT', + data: { + pbadslot: '/123/ID-FORMAT-PBADSLOT', + adserver: { + adslot: '/123/ID-FORMAT-ADSERVER-PB-ADSLOT', + } + } + }, + } + }, simpleBidRequest); + let request = spec.buildRequests([bidRequest], bidderRequest)[0]; + let payload = JSON.parse(request.data); + expect(payload.imp[0].ext.gpid).to.equal('/123/ID-FORMAT'); + expect(payload.imp[0].instl).to.equal(1); + + delete bidRequest.ortb2Imp.ext.gpid; + request = spec.buildRequests([bidRequest], bidderRequest)[0]; + payload = JSON.parse(request.data); + expect(payload.imp[0].ext.gpid).to.equal('/123/ID-FORMAT-PBADSLOT'); + + delete bidRequest.ortb2Imp.ext.data.pbadslot; + request = spec.buildRequests([bidRequest], bidderRequest)[0]; + payload = JSON.parse(request.data); + expect(payload.imp[0].ext.gpid).to.equal('/123/ID-FORMAT-ADSERVER-PB-ADSLOT'); + + delete bidRequest.ortb2Imp.ext.data.adserver; + delete bidRequest.ortb2Imp.instl; + request = spec.buildRequests([bidRequest], bidderRequest)[0]; + payload = JSON.parse(request.data); + expect(payload.imp[0].ext.gpid).to.not.exist; + expect(payload.imp[0].instl).to.not.exist; }); - it('should not set pagecat and genre when malformed data provided ➞ fpd:ortb2.site', function() { - config.setConfig(JSON.parse('{"ortb2":{"site":{"pagecat":"IAB2-2","content":{"genre":["Adventure"]}}}}')); - const bidRequest = Object.assign({}, simpleBidRequest); - const request = spec.buildRequests([bidRequest], bidderRequestReferrer)[0]; - const params = JSON.parse(decodeURIComponent(request.data.substring(PARAM_PREFIX.length))); - expect(params.bid_request.pagecat).does.not.exist; - expect(params.bid_request.genre).does.not.exist; + it('should not set site when app is defined in FPD', function () { + const getConfigStub = sinon.stub(config, 'getConfig'); + getConfigStub.withArgs('ortb2.app').returns({ content: 'XYZ' }); + let request = spec.buildRequests([simpleBidRequest], bidderRequest)[0]; + let payload = JSON.parse(request.data); + expect(payload.site).does.not.exist; + expect(payload.app).does.exist; + expect(payload.app.content).does.exist.and.equal('XYZ'); + getConfigStub.restore(); }); - it('should use cat when pagecat not available ➞ fpd:ortb2.site', function() { - config.setConfig(JSON.parse('{"ortb2":{"site":{"cat":["IAB2"]}}}')); - const bidRequest = Object.assign({}, simpleBidRequest); - const request = spec.buildRequests([bidRequest], bidderRequestReferrer)[0]; - const params = JSON.parse(decodeURIComponent(request.data.substring(PARAM_PREFIX.length))); - expect(params.bid_request.pagecat).to.be.an('array'); - expect(params.bid_request.pagecat).to.deep.equal(['IAB2']); + it('should not set site when app is defined in CONFIG', function () { + const getConfigStub = sinon.stub(config, 'getConfig'); + getConfigStub.withArgs('app').returns({ content: 'XYZ' }); + let request = spec.buildRequests([simpleBidRequest], bidderRequest)[0]; + let payload = JSON.parse(request.data); + expect(payload.site).does.not.exist; + expect(payload.app).does.exist; + expect(payload.app.content).does.exist.and.equal('XYZ'); + getConfigStub.restore(); }); - it('should format pagecat correctly ➞ fpd:ortb2.site', function() { - config.setConfig(JSON.parse('{"ortb2":{"site":{"cat":["IAB2", ["IAB-1"], "IAB3", 123, ""]}}}')); - const bidRequest = Object.assign({}, simpleBidRequest); - const request = spec.buildRequests([bidRequest], bidderRequestReferrer)[0]; - const params = JSON.parse(decodeURIComponent(request.data.substring(PARAM_PREFIX.length))); - expect(params.bid_request.pagecat).to.be.an('array'); - expect(params.bid_request.pagecat).to.deep.equal([ - 'IAB2', - 'IAB3' - ] - ); + it('should set correct site params', function () { + let getConfigStub = sinon.stub(config, 'getConfig'); + getConfigStub.withArgs('site').returns({ + content: 'XYZ', + page: 'https://improveditigal.com/', + domain: 'improveditigal.com' + }); + let request = spec.buildRequests([simpleBidRequest], bidderRequestReferrer)[0]; + let payload = JSON.parse(request.data); + expect(payload.site.content).does.exist.and.equal('XYZ'); + expect(payload.site.page).does.exist.and.equal('https://improveditigal.com/'); + expect(payload.site.domain).does.exist.and.equal('improveditigal.com'); + getConfigStub.reset(); + + request = spec.buildRequests([simpleBidRequest], bidderRequestReferrer)[0]; + payload = JSON.parse(request.data); + expect(payload.site.content).does.not.exist; + expect(payload.site.page).does.exist.and.equal('https://blah.com/test.html'); + expect(payload.site.domain).does.exist.and.equal('blah.com'); + + getConfigStub.withArgs('ortb2.site').returns({ + content: 'ZZZ', + }); + request = spec.buildRequests([simpleBidRequest], bidderRequestReferrer)[0]; + payload = JSON.parse(request.data); + expect(payload.site.content).does.exist.and.equal('ZZZ'); + expect(payload.site.page).does.exist.and.equal('https://blah.com/test.html'); + expect(payload.site.domain).does.exist.and.equal('blah.com'); + getConfigStub.restore(); + }); + + it('should set pageUrl as site param', function () { + let getConfigStub = sinon.stub(config, 'getConfig'); + getConfigStub.withArgs('pageUrl').returns('https://improvidigital.com/test-page'); + let request = spec.buildRequests([simpleBidRequest], bidderRequestReferrer)[0]; + let payload = JSON.parse(request.data); + expect(payload.site.page).does.exist.and.equal('https://improvidigital.com/test-page'); + expect(payload.site.domain).does.exist.and.equal('improvidigital.com'); + getConfigStub.reset(); + + getConfigStub.withArgs('pageUrl').returns(undefined); + request = spec.buildRequests([simpleBidRequest], bidderRequestReferrer)[0]; + payload = JSON.parse(request.data); + expect(payload.site.page).does.exist.and.equal('https://blah.com/test.html'); + expect(payload.site.domain).does.exist.and.equal('blah.com'); + getConfigStub.restore(); + }); + + it('should set site when app not available', function () { + const getConfigStub = sinon.stub(config, 'getConfig'); + getConfigStub.withArgs('app').returns(undefined); + let request = spec.buildRequests([simpleBidRequest], bidderRequest)[0]; + let payload = JSON.parse(request.data); + expect(payload.site).does.exist; + expect(payload.app).does.not.exist; + getConfigStub.restore(); }); }); const serverResponse = { 'body': { - 'id': '687a06c541d8d1', - 'site_id': 191642, - 'bid': [ + 'id': '99f9a9e6-5126-425b-822c-8b4edad2a719', + 'cur': 'EUR', + 'seatbid': [ { - 'isNet': false, - 'id': '33e9500b21129f', - 'advid': '5279', - 'price': 1.45888594164456, - 'nurl': 'https://ice.360yield.com/imp_pixel?ic=wVmhKI07hCVyGC1sNdFp.6buOSiGYOw8jPyZLlcMY2RCwD4ek3Fy6.xUI7U002skGBs3objMBoNU-Frpvmb9js3NKIG0YZJgWaNdcpXY9gOXE9hY4-wxybCjVSNzhOQB-zic73hzcnJnKeoGgcfvt8fMy18-yD0aVdYWt4zbqdoITOkKNCPBEgbPFu1rcje-o7a64yZ7H3dKvtnIixXQYc1Ep86xGSBGXY6xW2KfUOMT6vnkemxO72divMkMdhR8cAuqIubbx-ZID8-xf5c9k7p6DseeBW0I8ionrlTHx.rGosgxhiFaMqtr7HiA7PBzKvPdeEYN0hQ8RYo8JzYL82hA91A3V2m9Ij6y0DfIJnnrKN8YORffhxmJ6DzwEl1zjrVFbD01bqB3Vdww8w8PQJSkKQkd313tr-atU8LS26fnBmOngEkVHwAr2WCKxuUvxHmuVBTA-Lgz7wKwMoOJCA3hFxMavVb0ZFB7CK0BUTVU6z0De92Q.FJKNCHLMbjX3vcAQ90=', - 'h': 290, - 'pid': 1053688, - 'sync': [ - 'https://link1', - 'https://link2' + 'bid': [ + { + 'ext': { + 'improvedigital': { + 'line_item_id': 320896, + 'bidder_id': 0, + 'brand_name': '', + 'buying_type': 'classic', + 'agency_id': '0' + } + }, + 'exp': 120, + 'crid': '510265', + 'price': 1.9200543539802946, + 'id': '35adfe19-d6e9-46b9-9f7d-20da7026b965', + 'w': 728, + 'impid': '33e9500b21129f', + 'h': 90, + 'adm': '  ', + 'cid': '123159' + } ], - 'crid': '422031', - 'w': 600, - 'cid': '99006', - 'adm': 'document.writeln(\"\\\"\\\"\\/<\\/a>\");document.writeln(\"<\\/improvedigital_ad_output_information>\");' + 'seat': 'improvedigital' } - ], - 'debug': '' + ] } }; const serverResponseTwoBids = { 'body': { - 'id': '687a06c541d8d1', - 'site_id': 191642, - 'bid': [ - serverResponse.body.bid[0], + 'id': '99f9a9e6-5126-425b-822c-8b4edad2a719', + 'cur': 'EUR', + 'seatbid': [ { - 'isNet': true, - 'id': '1234', - 'advid': '5280', - 'price': 1.23, - 'nurl': 'https://link/imp_pixel?ic=wVmhKI07hCVyGC1sNdFp.6buOSiGYOw8jPyZLlcMY2RCwD4ek3Fy6.xUI7U002skGBs3objMBoNU-Frpvmb9js3NKIG0YZJgWaNdcpXY9gOXE9hY4-wxybCjVSNzhOQB-zic73hzcnJnKeoGgcfvt8fMy18-yD0aVdYWt4zbqdoITOkKNCPBEgbPFu1rcje-o7a64yZ7H3dKvtnIixXQYc1Ep86xGSBGXY6xW2KfUOMT6vnkemxO72divMkMdhR8cAuqIubbx-ZID8-xf5c9k7p6DseeBW0I8ionrlTHx.rGosgxhiFaMqtr7HiA7PBzKvPdeEYN0hQ8RYo8JzYL82hA91A3V2m9Ij6y0DfIJnnrKN8YORffhxmJ6DzwEl1zjrVFbD01bqB3Vdww8w8PQJSkKQkd313tr-atU8LS26fnBmOngEkVHwAr2WCKxuUvxHmuVBTA-Lgz7wKwMoOJCA3hFxMavVb0ZFB7CK0BUTVU6z0De92Q.FJKNCHLMbjX3vcAQ90=', - 'h': 400, - 'pid': 1053688, - 'sync': [ - 'https://link3' + 'bid': [ + { + 'ext': { + 'improvedigital': { + 'line_item_id': 320896, + 'bidder_id': 0, + 'brand_name': '', + 'buying_type': 'classic', + 'agency_id': '0' + } + }, + 'exp': 120, + 'crid': '510265', + 'price': 1.9200543539802946, + 'id': '35adfe19-d6e9-46b9-9f7d-20da7026b965', + 'w': 728, + 'impid': '33e9500b21129f', + 'h': 90, + 'adm': '  ', + 'cid': '123159' + }, + { + 'ext': { + 'improvedigital': { + 'line_item_id': 320896, + 'bidder_id': 0, + 'brand_name': '', + 'buying_type': 'classic', + 'agency_id': '0' + } + }, + 'exp': 120, + 'crid': '479163', + 'price': 1.9200543539802946, + 'id': '83c8d524-0955-4d0c-b558-4c9f3600e09b', + 'w': 300, + 'impid': '33e9500b21129f', + 'h': 250, + 'adm': '  ', + 'cid': '123159' + } ], - 'crid': '422033', - 'w': 700, - 'cid': '99006', - 'adm': 'document.writeln(\"\\\"\\\"\\/<\\/a>\");document.writeln(\"<\\/improvedigital_ad_output_information>\");' + 'seat': 'improvedigital_improvedigital' } ], - 'debug': '' + ext: { + improvedigital: { + sync: [ + 'https://link1', + 'https://link2', + 'https://link3', + ] + } + } } }; const serverResponseNative = { body: { - id: '687a06c541d8d1', - site_id: 191642, - bid: [ + 'id': '8201e669-bbbf-4f61-b9a2-4cb854033c82', + 'cur': 'EUR', + 'seatbid': [ { - isNet: false, - id: '33e9500b21129f', - advid: '5279', - price: 1.45888594164456, - nurl: 'https://ice.360yield.com/imp_pixel?ic=wVm', - h: 290, - pid: 1053688, - sync: [ - 'https://link1', - 'https://link2' - ], - crid: '422031', - w: 600, - cid: '99006', - native: { - assets: [ - { - title: { - text: 'Native title' - } - }, - { - data: { - type: 1, - value: 'Improve Digital' - } - }, - { - data: { - type: 2, - value: 'Native body' - } - }, - { - data: { - type: 3, - value: '4' // rating - } - }, - { - data: { - type: 4, - value: '10105' // likes + 'bid': [ + { + 'ext': { + 'improvedigital': { + 'line_item_id': 411331, + 'bidder_id': 0, + 'brand_name': '', + 'buying_type': 'classic', + 'agency_id': '0', + 'is_net': true } }, - { - data: { - type: 5, - value: '150000' // downloads - } - }, - { - data: { - type: 6, - value: '3.99' // price - } - }, - { - data: { - type: 7, - value: '4.49' // salePrice - } - }, - { - data: { - type: 8, - value: '(123) 456-7890' // phone - } - }, - { - data: { - type: 9, - value: '123 Main Street, Anywhere USA' // address - } - }, - { - data: { - type: 10, - value: 'body2' - } - }, - { - data: { - type: 11, - value: 'https://myurl.com' // displayUrl - } - }, - { - data: { - type: 12, - value: 'Do it' // cta - } - }, - { - img: { - type: 1, - url: 'Should get ignored', - h: 300, - w: 400 - } - }, - { - img: { - type: 2, - url: 'https://blah.com/icon.jpg', - h: 30, - w: 40 - } - - }, - { - img: { - type: 3, - url: 'https://blah.com/image.jpg', - h: 200, - w: 800 - } - } - ], - link: { - url: 'https://advertiser.com', - clicktrackers: [ - 'https://click.tracker.com/click?impid=123' - ] - }, - imptrackers: [ - 'https://imptrack1.com', - 'https://imptrack2.com' - ], - jstracker: '', - privacy: 'https://www.myprivacyurl.com' - } + 'crid': '544456', + 'exp': 120, + 'id': '52098fad-20c1-476b-a4fa-41e275e5a4a5', + 'price': 1.8600000000000003, + 'adm': "{\"ver\":\"1.1\",\"imptrackers\":[\"https://secure.adnxs.com/imptr?id=52311&t=2\",\"https://euw-ice.360yield.com/imp_pixel?ic=hcUBlCANx1FabHBf6FR2gC7UO4xEyXahdZAn0-B5qL-bb3A74BJ1smyWIyW7IWcC0SOjSXzVpevTHXxTqJ.sf.Qhahyy6tSo.0j1QWfXlH8sM4-8vKWjMjw-x.IrJJNlwkQ0s1CdwcwTefcLXm5l2E-W19VhACuV7f3mgrZMNjiSw.SjJAfyPC3SIyAMRjYfj53UmjriQ46T7lhmkqxK8wHmksYCdbZc3PZESk8NWl28sxdjNvnYYCFMcJbeav.LOLabyTXfwy-1cEPbQs.IKMRZIKaqccTDPV3wOtzbNv0jQzatd3Nnv-PGFQcjQ-GW3i27W04Fws4kodpFSn-B6VwZAjzLzoyd5gBncyRnAyCplEbgHU5sZ1IyKHWjgCl3ZtRIK5vqrRD5D-xqgSnOi7-phG.CqZWDZ4bMDSfQg2ZnbvUTyGKcEl0WR59dW5izTMV4Fjizcrvr5T-t.zMbGwz.hGnmLIyhTqh.IcwW.GiDLVExlDlix5S1LXIWVsSyrQ==\"],\"assets\":[{\"id\":1,\"data\":{\"value\":\"ImproveDigital\",\"type\":1}},{\"id\":3,\"data\":{\"value\":\"Test content.\",\"type\":2}},{\"id\":0,\"title\":{\"text\":\"Sample Prebid Test Title\"}}],\"link\":{\"url\":\"https://euw-ice.360yield.com/click/hcUBlHOV7YhVse8RyBa0ajjyPa9Vt17e4g-1m3cRj3E67vq-RYux.SiUeAmBfNBcoOqkUc6A15AWmi4yFu5K-BdkaYjildyyk7fNLyR6hWr411kv4vrFwm5jrIBceuHS6K8oN69f.uCo8zGTdR2TbSlldwcpahQPlufZU.6VaMsu4IC53uEiUT5vb7kAw6TTlxuGBNq6zaGryiWEV2.N3YYJDTyYPh8tv-ZFyeFZFm0Gnjv.xWbC.70JcRUVU9UelQaPsTpTWYTXBhJt84YJUw1-GNtaLNVLSjjZbVoA2fsMti5p6OBmF.7u39on2OPgvseIkSmge7Pqg63pRqdP75hp.DAEk6OkcN1jGnwP2DSbvpaSbin5lVqjfO0B-wnQgfQTCUtM5v4JmkNweLhUf9Q-x.nPKLW5SccEk9ZFXzY2-1wpT3PWm8Tix3NRscLPZub9wHzL..pl6ip8cQ9hp16UjwT4H6RMAxL0R7bl-h2pAicGAzYmuO7ntRESKUoIWA==//http%3A%2F%2Fquantum-advertising.com%2Ffr%2F\"},\"jstracker\":\"\"}", + 'impid': '2d7a7db325c6f', + 'cid': '196108' + } + ], + 'seat': 'improvedigital' } - ], - debug: '' + ] } }; const serverResponseVideo = { 'body': { - 'id': '687a06c541d8d1', - 'site_id': 191642, - 'bid': [ + 'id': '8ed20675-8934-430c-b645-1ccd17b35839', + 'cur': 'EUR', + 'seatbid': [ { - 'isNet': false, - 'id': '33e9500b21129f', - 'advid': '5279', - 'price': 1.45888594164456, - 'nurl': 'http://ice.360yield.com/imp_pixel?ic=wVmhKI07hCVyGC1sNdFp.6buOSiGYOw8jPyZLlcMY2RCwD4ek3Fy6.xUI7U002skGBs3objMBoNU-Frpvmb9js3NKIG0YZJgWaNdcpXY9gOXE9hY4-wxybCjVSNzhOQB-zic73hzcnJnKeoGgcfvt8fMy18-yD0aVdYWt4zbqdoITOkKNCPBEgbPFu1rcje-o7a64yZ7H3dKvtnIixXQYc1Ep86xGSBGXY6xW2KfUOMT6vnkemxO72divMkMdhR8cAuqIubbx-ZID8-xf5c9k7p6DseeBW0I8ionrlTHx.rGosgxhiFaMqtr7HiA7PBzKvPdeEYN0hQ8RYo8JzYL82hA91A3V2m9Ij6y0DfIJnnrKN8YORffhxmJ6DzwEl1zjrVFbD01bqB3Vdww8w8PQJSkKQkd313tr-atU8LS26fnBmOngEkVHwAr2WCKxuUvxHmuVBTA-Lgz7wKwMoOJCA3hFxMavVb0ZFB7CK0BUTVU6z0De92Q.FJKNCHLMbjX3vcAQ90=', - 'h': 290, - 'pid': 1053688, - 'sync': [ - 'http://link1', - 'http://link2' + 'bid': [ + { + 'ext': { + 'improvedigital': { + 'line_item_id': 321329, + 'bidder_id': 0, + 'brand_name': '', + 'buying_type': 'classic', + 'agency_id': '0' + } + }, + 'exp': 120, + 'crid': '484367', + 'price': 9.600271769901472, + 'id': 'b131fd7b-5759-4b72-800e-60e69291e7d9', + 'adomain': [ + 'improvedigital.com' + ], + 'impid': '33e9500b21129f', + 'adm': '', + 'w': 640, + 'h': 480, + 'cid': '123159' + } ], - 'crid': '422031', - 'w': 600, - 'cid': '99006', - 'adm': '', - 'ad_type': 'video' + 'seat': 'improvedigital' } ], - 'debug': '' } }; - const nativeEventtrackers = [ - { - event: 1, - method: 1, - url: 'https://www.mytracker.com/imptracker' - }, - { - event: 1, - method: 2, - url: 'https://www.mytracker.com/tracker.js' + const serverResponseRazr = { + body: { + 'id': '2adac6a5fe04df', + 'cur': 'EUR', + 'ext': { + 'improvedigital': { + 'sync': [ + 'https://d5p.de17a.com/getuid/improve_digital?publisher_user_id=ce26f11e-567a-4eb7-bf94-51752e293ca5&publisher_dsp_id=61&publisher_call_type=redirect&gdpr=1&gdpr_consent=CPU22FrPU22FrAcABBENCDCsAP_AAH_AAChQIltf_X__b3_j-_5_f_t0eY1P9_7_v-0zjhfdt-8N3f_X_L8X42M7vF36pq4KuR4Eu3LBIQdlHOHcTUmw6okVrzPsbk2cr7NKJ7PEmnMbO2dYGH9_n93TuZKY7______z_v-v_v____f_7-3_3__5_3---_e_V_99zLv9____39nP___9v-_9_____4IhgEmGpeQBdmWODJtGlUKIEYVhIdAKACigGFoisIHVwU7K4CfUELABCagJwIgQYgowYBAAIJAEhEQEgB4IBEARAIAAQAqwEIACNgEFgBYGAQACgGhYgRQBCBIQZHBUcpgQFSLRQT2ViCUHexphCGWeBFAo_oqEBGs0QLAyEhYOY4AkBLxZIHmKF8gAAAAA.f_gAD_gAAAAA&publisher_redirecturl=https://euw-ice.360yield.com/match' + ] + } + }, + 'seatbid': [ + { + 'bid': [ + { + 'ext': { + 'improvedigital': { + 'line_item_id': 410573, + 'bidder_id': 0, + 'brand_name': '', + 'buying_type': 'classic', + 'agency_id': '0' + } + }, + 'exp': 120, + 'crid': '544063', + 'price': 1.9199364935359489, + 'id': '1fcf4dd8-a783-48ed-b59c-8fc8eeccb024', + 'adomain': [ + 'improvedigital.com' + ], + 'w': 970, + 'impid': '33e9500b21129f', + 'h': 250, + 'adm': '\n\n\n\n\n\n\n\n ', + 'cid': '187354' + } + ], + 'seat': 'improvedigital' + } + ] } - ]; + }; describe('interpretResponse', function () { const expectedBid = [ { - 'ad': '', - 'creativeId': '422031', - 'cpm': 1.45888594164456, - 'currency': 'USD', - 'height': 290, - 'mediaType': 'banner', - 'netRevenue': false, - 'requestId': '33e9500b21129f', - 'ttl': 300, - 'width': 600 + requestId: '33e9500b21129f', + cpm: 1.9200543539802946, + currency: 'EUR', + width: 728, + height: 90, + ttl: 300, + ad: '  ', + creativeId: '510265', + dealId: 320896, + netRevenue: false, + mediaType: BANNER, + meta: { + advertiserDomains: [] + } } ]; const expectedTwoBids = [ expectedBid[0], { - 'ad': '', - 'creativeId': '422033', - 'cpm': 1.23, - 'currency': 'USD', - 'height': 400, - 'mediaType': 'banner', - 'netRevenue': true, - 'requestId': '1234', - 'ttl': 300, - 'width': 700 - } - ]; - - const expectedBidNative = [ - { - mediaType: 'native', - creativeId: '422031', - cpm: 1.45888594164456, - currency: 'USD', - height: 290, - netRevenue: false, requestId: '33e9500b21129f', + cpm: 1.9200543539802946, + currency: 'EUR', + width: 300, + height: 250, ttl: 300, - width: 600, - native: { - title: 'Native title', - body: 'Native body', - body2: 'body2', - cta: 'Do it', - sponsoredBy: 'Improve Digital', - rating: '4', - likes: '10105', - downloads: '150000', - price: '3.99', - salePrice: '4.49', - phone: '(123) 456-7890', - address: '123 Main Street, Anywhere USA', - displayUrl: 'https://myurl.com', - icon: { - url: 'https://blah.com/icon.jpg', - height: 30, - width: 40 - }, - image: { - url: 'https://blah.com/image.jpg', - height: 200, - width: 800 - }, - clickUrl: 'https://advertiser.com', - clickTrackers: ['https://click.tracker.com/click?impid=123'], - impressionTrackers: [ - 'https://ice.360yield.com/imp_pixel?ic=wVm', - 'https://imptrack1.com', - 'https://imptrack2.com' - ], - javascriptTrackers: '', - privacyLink: 'https://www.myprivacyurl.com' + ad: '  ', + creativeId: '479163', + dealId: 320896, + netRevenue: false, + mediaType: BANNER, + meta: { + advertiserDomains: [] } } ]; const expectedBidInstreamVideo = [ { - 'vastXml': '', - 'creativeId': '422031', - 'cpm': 1.45888594164456, - 'currency': 'USD', - 'height': 290, - 'mediaType': 'video', - 'netRevenue': false, - 'requestId': '33e9500b21129f', - 'ttl': 300, - 'width': 600 + requestId: '33e9500b21129f', + cpm: 9.600271769901472, + currency: 'EUR', + ttl: 300, + vastXml: '', + creativeId: '484367', + dealId: 321329, + netRevenue: false, + mediaType: VIDEO, + meta: { + advertiserDomains: ['improvedigital.com'], + } } ]; const expectedBidOutstreamVideo = utils.deepClone(expectedBidInstreamVideo); expectedBidOutstreamVideo[0].adResponse = { - content: expectedBidOutstreamVideo[0].vastXml, - height: expectedBidOutstreamVideo[0].height, - width: expectedBidOutstreamVideo[0].width + content: expectedBidOutstreamVideo[0].vastXml }; it('should return a well-formed display bid', function () { @@ -919,50 +940,35 @@ describe('Improve Digital Adapter Tests', function () { const response = JSON.parse(JSON.stringify(serverResponse)); let bids; - delete response.body.bid[0].lid; - response.body.bid[0].buying_type = 'deal_id'; + delete response.body.seatbid[0].bid[0].ext.improvedigital.line_item_id; + response.body.seatbid[0].bid[0].ext.improvedigital.buying_type = 'deal_id'; bids = spec.interpretResponse(response, {bidderRequest}); expect(bids[0].dealId).to.not.exist; - response.body.bid[0].lid = 268515; - delete response.body.bid[0].buying_type; + response.body.seatbid[0].bid[0].ext.improvedigital.line_item_id = 268515; + delete response.body.seatbid[0].bid[0].ext.improvedigital.buying_type; bids = spec.interpretResponse(response, {bidderRequest}); expect(bids[0].dealId).to.not.exist; - response.body.bid[0].lid = 268515; - response.body.bid[0].buying_type = 'rtb'; + response.body.seatbid[0].bid[0].ext.improvedigital.line_item_id = 268515; + response.body.seatbid[0].bid[0].ext.improvedigital.buying_type = 'rtb'; bids = spec.interpretResponse(response, {bidderRequest}); expect(bids[0].dealId).to.not.exist; - response.body.bid[0].lid = 268515; - response.body.bid[0].buying_type = 'classic'; + response.body.seatbid[0].bid[0].ext.improvedigital.line_item_id = 268515; + response.body.seatbid[0].bid[0].ext.improvedigital.buying_type = 'classic'; bids = spec.interpretResponse(response, {bidderRequest}); expect(bids[0].dealId).to.equal(268515); - response.body.bid[0].lid = 268515; - response.body.bid[0].buying_type = 'deal_id'; + response.body.seatbid[0].bid[0].ext.improvedigital.line_item_id = 268515; + response.body.seatbid[0].bid[0].ext.improvedigital.buying_type = 'deal_id'; bids = spec.interpretResponse(response, {bidderRequest}); expect(bids[0].dealId).to.equal(268515); - - response.body.bid[0].lid = [ 268515, 12456, 34567 ]; - response.body.bid[0].buying_type = 'deal_id'; - bids = spec.interpretResponse(response, {bidderRequest}); - expect(bids[0].dealId).to.not.exist; - - response.body.bid[0].lid = [ 268515, 12456, 34567 ]; - response.body.bid[0].buying_type = [ 'deal_id', 'classic' ]; - bids = spec.interpretResponse(response, {bidderRequest}); - expect(bids[0].dealId).to.not.exist; - - response.body.bid[0].lid = [ 268515, 12456, 34567 ]; - response.body.bid[0].buying_type = [ 'rtb', 'deal_id', 'deal_id' ]; - bids = spec.interpretResponse(response, {bidderRequest}); - expect(bids[0].dealId).to.equal(12456); }); it('should set currency', function () { const response = JSON.parse(JSON.stringify(serverResponse)); - response.body.bid[0].currency = 'eur'; + response.body.cur = 'eur'; const bids = spec.interpretResponse(response, {bidderRequest}); expect(bids[0].currency).to.equal('EUR'); }); @@ -972,35 +978,35 @@ describe('Improve Digital Adapter Tests', function () { let bids; // Price missing or 0 - response.body.bid[0].price = 0; + response.body.seatbid[0].bid[0].price = 0; bids = spec.interpretResponse(response, {bidderRequest}); expect(bids).to.deep.equal([]); - delete response.body.bid[0].price; + delete response.body.seatbid[0].bid[0]; bids = spec.interpretResponse(response, {bidderRequest}); expect(bids).to.deep.equal([]); - response.body.bid[0].price = null; + response.body.seatbid[0].bid[0] = []; bids = spec.interpretResponse(response, {bidderRequest}); expect(bids).to.deep.equal([]); // errorCode present response = JSON.parse(JSON.stringify(serverResponse)); - response.body.bid[0].errorCode = undefined; + response.body.seatbid[0].bid[0].errorCode = undefined; bids = spec.interpretResponse(response, {bidderRequest}); expect(bids).to.deep.equal([]); // adm and native missing response = JSON.parse(JSON.stringify(serverResponse)); - delete response.body.bid[0].adm; + delete response.body.seatbid[0].bid[0].adm; bids = spec.interpretResponse(response, {bidderRequest}); expect(bids).to.deep.equal([]); - response.body.bid[0].adm = null; + response.body.seatbid[0].bid[0].adm = null; bids = spec.interpretResponse(response, {bidderRequest}); expect(bids).to.deep.equal([]); }); it('should set netRevenue', function () { const response = JSON.parse(JSON.stringify(serverResponse)); - response.body.bid[0].isNet = true; + response.body.seatbid[0].bid[0].ext.improvedigital.is_net = true; const bids = spec.interpretResponse(response, {bidderRequest}); expect(bids[0].netRevenue).to.equal(true); }); @@ -1008,30 +1014,31 @@ describe('Improve Digital Adapter Tests', function () { it('should set advertiserDomains', function () { const adomain = ['domain.com']; const response = JSON.parse(JSON.stringify(serverResponse)); - response.body.bid[0].adomain = adomain; + response.body.seatbid[0].bid[0].adomain = adomain; const bids = spec.interpretResponse(response, {bidderRequest}); expect(bids[0].meta.advertiserDomains).to.equal(adomain); }); - + // // Native ads it('should return a well-formed native ad bid', function () { - let bids = spec.interpretResponse(serverResponseNative, {bidderRequest}); - expect(bids[0].ortbNative).to.deep.equal(serverResponseNative.body.bid[0].native); - delete bids[0].ortbNative; - expect(bids).to.deep.equal(expectedBidNative); - - // eventtrackers - const response = JSON.parse(JSON.stringify(serverResponseNative)); - const expectedBids = JSON.parse(JSON.stringify(expectedBidNative)); - response.body.bid[0].native.eventtrackers = nativeEventtrackers; - expectedBids[0].native.impressionTrackers = [ - 'https://ice.360yield.com/imp_pixel?ic=wVm', - 'https://www.mytracker.com/imptracker' - ]; - expectedBids[0].native.javascriptTrackers = ''; - bids = spec.interpretResponse(response, {bidderRequest}); - delete bids[0].ortbNative; - expect(bids).to.deep.equal(expectedBids); + const nativeBidderRequest = JSON.parse(JSON.stringify(bidderRequest)); + nativeBidderRequest.bids[0].bidId = '2d7a7db325c6f'; + delete nativeBidderRequest.bids[0].mediaTypes.banner; + nativeBidderRequest.bids[0].mediaTypes.native = {}; + const bids = spec.interpretResponse(serverResponseNative, {bidderRequest: nativeBidderRequest}); + // Verify Native Response + expect(bids[0].native).to.exist; + const nativeBid = bids[0].native; + const nativeResp = JSON.parse(serverResponseNative.body.seatbid[0].bid[0].adm); + // Verify Native Response + expect(nativeBid.clickUrl).to.exist.and.equal(nativeResp.link.url); + expect(nativeBid.impressionTrackers).to.exist.and.deep.equal(nativeResp.imptrackers); + expect(nativeBid.javascriptTrackers).to.exist.and.deep.equal(nativeResp.jstracker); + + // Verify Assets + expect(nativeBid.title).to.exist.and.equal('Sample Prebid Test Title'); + expect(nativeBid.sponsoredBy).to.exist.and.equal('ImproveDigital'); + expect(nativeBid.body).to.exist.and.equal('Test content.'); }); // Video @@ -1053,6 +1060,16 @@ describe('Improve Digital Adapter Tests', function () { delete (bids[0].renderer); expect(bids).to.deep.equal(expectedBidOutstreamVideo); }); + + it('should not affect non-RAZR bids', function () { + const bids = spec.interpretResponse(serverResponse, {bidderRequest}); + expect(bids[0].renderer).to.not.exist; + }); + + it('should detect RAZR bids', function () { + const bids = spec.interpretResponse(serverResponseRazr, {bidderRequest}); + expect(bids[0].renderer).to.exist; + }); }); describe('getUserSyncs', function () { diff --git a/test/spec/modules/insticatorBidAdapter_spec.js b/test/spec/modules/insticatorBidAdapter_spec.js index 7764117dbae..e14207ba3e0 100644 --- a/test/spec/modules/insticatorBidAdapter_spec.js +++ b/test/spec/modules/insticatorBidAdapter_spec.js @@ -12,11 +12,12 @@ let utils = require('src/utils.js'); describe('InsticatorBidAdapter', function () { const adapter = newBidder(spec); + const bidderRequestId = '22edbae2733bf6'; let bidRequest = { bidder: 'insticator', adUnitCode: 'adunit-code', params: { - adUnitId: '1a2b3c4d5e6f1a2b3c4d' + adUnitId: '1a2b3c4d5e6f1a2b3c4d', }, sizes: [[300, 250], [300, 600]], mediaTypes: { @@ -25,10 +26,38 @@ describe('InsticatorBidAdapter', function () { } }, bidId: '30b31c1838de1e', + ortb2Imp: { + ext: { + gpid: '1111/homepage' + } + }, + schain: { + ver: '1.0', + complete: 1, + nodes: [ + { + asi: 'insticator.com', + sid: '00001', + hp: 1, + rid: bidderRequestId + } + ] + }, + userIdAsEids: [ + { + source: 'criteo.com', + uids: [ + { + id: '123', + atype: 1 + } + ] + } + ], }; let bidderRequest = { - bidderRequestId: '22edbae2733bf6', + bidderRequestId, auctionId: '74f78609-a92d-4cf1-869f-1b244bbfb5d2', timeout: 300, gdprConsent: { @@ -144,12 +173,23 @@ describe('InsticatorBidAdapter', function () { const data = JSON.parse(requests[0].data); expect(data).to.be.an('object'); - expect(data).to.have.all.keys('id', 'tmax', 'source', 'site', 'device', 'regs', 'user', 'imp'); + expect(data).to.have.all.keys('id', 'tmax', 'source', 'site', 'device', 'regs', 'user', 'imp', 'ext'); expect(data.id).to.equal(bidderRequest.bidderRequestId); expect(data.tmax).to.equal(bidderRequest.timeout); - expect(data.source).to.eql({ - fd: 1, - tid: bidderRequest.auctionId, + expect(data.source).to.have.all.keys('fd', 'tid', 'ext'); + expect(data.source.fd).to.equal(1); + expect(data.source.tid).to.equal(bidderRequest.auctionId); + expect(data.source.ext).to.have.property('schain').to.deep.equal({ + ver: '1.0', + complete: 1, + nodes: [ + { + asi: 'insticator.com', + sid: '00001', + hp: 1, + rid: bidderRequest.bidderRequestId + } + ] }); expect(data.site).to.be.an('object'); expect(data.site.domain).not.to.be.empty; @@ -167,6 +207,18 @@ describe('InsticatorBidAdapter', function () { expect(data.regs.ext.gdprConsentString).to.equal(bidderRequest.gdprConsent.consentString); expect(data.user).to.be.an('object'); expect(data.user.id).to.equal(USER_ID_DUMMY_VALUE); + expect(data.user.ext).to.have.property('eids'); + expect(data.user.ext.eids).to.deep.equal([ + { + source: 'criteo.com', + uids: [ + { + id: '123', + atype: 1 + } + ] + } + ]); expect(data.imp).to.be.an('array').that.have.lengthOf(1); expect(data.imp).to.deep.equal([{ id: bidRequest.bidId, @@ -178,11 +230,20 @@ describe('InsticatorBidAdapter', function () { ] }, ext: { + gpid: bidRequest.ortb2Imp.ext.gpid, insticator: { adUnitId: bidRequest.params.adUnitId, }, } }]); + expect(data.ext).to.be.an('object'); + expect(data.ext.insticator).to.be.an('object') + expect(data.ext.insticator).to.deep.equal({ + adapter: { + vendor: 'prebid', + prebid: '$prebid.version$' + } + }); }); it('should generate new userId if not valid user is stored', function () { @@ -281,7 +342,12 @@ describe('InsticatorBidAdapter', function () { h: 200, adm: 'adm1', exp: 60, - bidADomain: ['test1.com'], + adomain: ['test1.com'], + ext: { + meta: { + test: 1 + } + } }, { impid: 'bid2', @@ -290,7 +356,7 @@ describe('InsticatorBidAdapter', function () { w: 600, h: 200, adm: 'adm2', - bidADomain: ['test2.com'], + adomain: ['test2.com'], }, { impid: 'bid3', @@ -299,7 +365,7 @@ describe('InsticatorBidAdapter', function () { w: 300, h: 200, adm: 'adm3', - bidADomain: ['test3.com'], + adomain: ['test3.com'], } ], }, @@ -321,7 +387,8 @@ describe('InsticatorBidAdapter', function () { meta: { advertiserDomains: [ 'test1.com' - ] + ], + test: 1 }, ad: 'adm1', adUnitCode: 'adunit-code-1', diff --git a/test/spec/modules/instreamTracking_spec.js b/test/spec/modules/instreamTracking_spec.js index 8d795fec88b..8c49da76ab6 100644 --- a/test/spec/modules/instreamTracking_spec.js +++ b/test/spec/modules/instreamTracking_spec.js @@ -1,7 +1,7 @@ import { assert } from 'chai'; import { trackInstreamDeliveredImpressions } from 'modules/instreamTracking.js'; import { config } from 'src/config.js'; -import events from 'src/events.js'; +import * as events from 'src/events.js'; import * as utils from 'src/utils.js'; import * as sinon from 'sinon'; import { INSTREAM, OUTSTREAM } from 'src/video.js'; diff --git a/test/spec/modules/intersectionRtdProvider_spec.js b/test/spec/modules/intersectionRtdProvider_spec.js index eb223d81d8f..0621c4f67e0 100644 --- a/test/spec/modules/intersectionRtdProvider_spec.js +++ b/test/spec/modules/intersectionRtdProvider_spec.js @@ -1,6 +1,6 @@ import {config as _config, config} from 'src/config.js'; import { expect } from 'chai'; -import events from 'src/events.js'; +import * as events from 'src/events.js'; import * as prebidGlobal from 'src/prebidGlobal.js'; import { intersectionSubmodule } from 'modules/intersectionRtdProvider.js'; import * as utils from 'src/utils.js'; diff --git a/test/spec/modules/iqzoneBidAdapter_spec.js b/test/spec/modules/iqzoneBidAdapter_spec.js index 3c7da783728..de0459f4714 100644 --- a/test/spec/modules/iqzoneBidAdapter_spec.js +++ b/test/spec/modules/iqzoneBidAdapter_spec.js @@ -143,6 +143,7 @@ describe('IQZoneBidAdapter', function () { 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'); @@ -368,4 +369,29 @@ describe('IQZoneBidAdapter', function () { 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.smartssp.iqzone.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.smartssp.iqzone.com/image?pbjs=1&ccpa_consent=1---&coppa=0') + }); + }); }); diff --git a/test/spec/modules/ixBidAdapter_spec.js b/test/spec/modules/ixBidAdapter_spec.js index cdf70a799e3..0e1a854b67c 100644 --- a/test/spec/modules/ixBidAdapter_spec.js +++ b/test/spec/modules/ixBidAdapter_spec.js @@ -173,6 +173,26 @@ describe('IndexexchangeAdapter', function () { } ]; + const DEFAULT_BANNER_VALID_BID_PARAM_NO_SIZE = [ + { + bidder: 'ix', + params: { + siteId: '123' + }, + mediaTypes: { + banner: { + sizes: [[300, 250], [300, 600]] + } + }, + adUnitCode: 'div-gpt-ad-1460505748561-0', + transactionId: '173f49a8-7549-4218-a23c-e7ba59b47229', + bidId: '1a2b3c4d', + bidderRequestId: '11a22b33c44d', + auctionId: '1aa2bb3cc4dd', + schain: SAMPLE_SCHAIN + } + ]; + const DEFAULT_VIDEO_VALID_BID = [ { bidder: 'ix', @@ -675,6 +695,22 @@ describe('IndexexchangeAdapter', function () { expect(spec.isBidRequestValid(bid)).to.equal(true); }); + it('should set bid[].renderer if renderer not defined at mediaType.video level', function () { + const bid = spec.interpretResponse({ body: DEFAULT_VIDEO_BID_RESPONSE }, { + data: DEFAULT_BIDDER_REQUEST_DATA, validBidRequests: DEFAULT_MULTIFORMAT_BANNER_VALID_BID + }); + expect(bid[0].renderer).to.be.exist; + }); + + it('should not set bid[].renderer if renderer defined at mediaType.video level', function () { + const outstreamAdUnit = DEFAULT_MULTIFORMAT_BANNER_VALID_BID; + outstreamAdUnit[0].mediaTypes.video.renderer = {} + const bid = spec.interpretResponse({ body: DEFAULT_VIDEO_BID_RESPONSE }, { + data: DEFAULT_BIDDER_REQUEST_DATA, validBidRequests: DEFAULT_MULTIFORMAT_BANNER_VALID_BID + }); + expect(bid[0].renderer).to.be.undefined; + }); + it('should return false when there is only bidFloor', function () { const bid = utils.deepClone(DEFAULT_BANNER_VALID_BID[0]); bid.params.bidFloor = 50; @@ -1230,7 +1266,7 @@ describe('IndexexchangeAdapter', function () { const r = JSON.parse(request.data.r); expect(r.ext.ixdiag.userIds).to.be.an('array'); - expect(r.ext.ixdiag.userIds.should.include('lotamePanoramaId')); + expect(r.ext.ixdiag.userIds.should.not.include('lotamePanoramaId')); expect(r.ext.ixdiag.userIds.should.not.include('merkleId')); expect(r.ext.ixdiag.userIds.should.not.include('parrableId')); }); @@ -1375,6 +1411,7 @@ describe('IndexexchangeAdapter', function () { delete bidWithoutSchain[0].schain; const requestWithoutSchain = spec.buildRequests(bidWithoutSchain, DEFAULT_OPTION)[0]; const queryWithoutSchain = requestWithoutSchain.data; + const GPID = '/19968336/some-adunit-path'; it('request should be made to IX endpoint with GET method', function () { expect(requestMethod).to.equal('GET'); @@ -1414,7 +1451,6 @@ describe('IndexexchangeAdapter', function () { }); it('should send gpid in request if ortb2Imp.ext.gpid exists', function () { - const GPID = '/19968336/some-adunit-path'; const validBids = utils.deepClone(DEFAULT_BANNER_VALID_BID); validBids[0].ortb2Imp = { ext: { @@ -1426,6 +1462,18 @@ describe('IndexexchangeAdapter', function () { expect(gpid).to.equal(GPID); }); + it('should send gpid in request if ortb2Imp.ext.gpid exists when no size present', function () { + const validBids = utils.deepClone(DEFAULT_BANNER_VALID_BID_PARAM_NO_SIZE); + validBids[0].ortb2Imp = { + ext: { + gpid: GPID + } + }; + const requests = spec.buildRequests(validBids, DEFAULT_OPTION); + const { ext: { gpid } } = JSON.parse(requests[0].data.r).imp[0]; + expect(gpid).to.equal(GPID); + }); + it('should not send dfp_adunit_code in request if ortb2Imp.ext.data.adserver.adslot does not exists', function () { const GPID = '/19968336/some-adunit-path'; const validBids = utils.deepClone(DEFAULT_BANNER_VALID_BID); @@ -1490,6 +1538,14 @@ describe('IndexexchangeAdapter', function () { expect(payload.imp).to.have.lengthOf(1); }); + it('payload should set site.page to pageUrl when it exists in config object', function () { + const url = 'https://example.com/index.html'; + config.setConfig({ pageUrl: url }); + const request = spec.buildRequests(DEFAULT_BANNER_VALID_BID, DEFAULT_OPTION)[0].data; + const payload = JSON.parse(request.r); + expect(payload.site.page).to.contains(url); + }); + it('payload should have correct format and value for r.id when bidderRequestId is a number ', function () { const bidWithIntId = utils.deepClone(DEFAULT_BANNER_VALID_BID); bidWithIntId[0].bidderRequestId = 123456; @@ -1699,6 +1755,10 @@ describe('IndexexchangeAdapter', function () { }); describe('first party data', () => { + beforeEach(() => { + config.resetConfig(); + }); + it('should add first party data to page url in bid request if it exists in config', function () { config.setConfig({ ix: { @@ -2104,6 +2164,19 @@ describe('IndexexchangeAdapter', function () { expect(impression.video.api).to.equal(2); expect(impression.video.mimes[0]).to.equal('video/mp4'); }); + + it('should send gpid in request if ortb2Imp.ext.gpid exists', function () { + const GPID = '/19968336/some-adunit-path'; + const validBids = utils.deepClone(DEFAULT_VIDEO_VALID_BID); + validBids[0].ortb2Imp = { + ext: { + gpid: GPID + } + }; + const requests = spec.buildRequests(validBids, DEFAULT_OPTION); + const { ext: { gpid } } = JSON.parse(requests[0].data.r).imp[0]; + expect(gpid).to.equal(GPID); + }); }); describe('buildRequestMultiFormat', function () { diff --git a/test/spec/modules/justIdSystem_spec.js b/test/spec/modules/justIdSystem_spec.js new file mode 100644 index 00000000000..b6a8cd2d310 --- /dev/null +++ b/test/spec/modules/justIdSystem_spec.js @@ -0,0 +1,216 @@ +import { justIdSubmodule, ConfigWrapper, jtUtils, EX_URL_REQUIRED, EX_INVALID_MODE } from 'modules/justIdSystem.js'; +import { loadExternalScriptStub } from 'test/mocks/adloaderStub.js'; +import * as utils from 'src/utils.js'; + +const DEFAULT_PARTNER = 'pbjs-just-id-module'; + +const url = 'https://example.com/getId.js'; + +describe('JustIdSystem', function () { + describe('configWrapper', function() { + it('invalid mode', function() { + expect(() => new ConfigWrapper({ params: { mode: 'invalidmode' } })).to.throw(EX_INVALID_MODE); + }) + + it('url is required', function() { + expect(() => new ConfigWrapper(configModeCombined())).to.throw(EX_URL_REQUIRED); + }) + + it('defaultPartner', function() { + expect(new ConfigWrapper(configModeCombined(url)).getUrl()).to.eq(expectedUrl(url, DEFAULT_PARTNER)); + }) + + it('customPartner', function() { + const partner = 'abc'; + expect(new ConfigWrapper(configModeCombined(url, partner)).getUrl()).to.eq(expectedUrl(url, partner)); + }) + }); + + describe('decode', function() { + it('decode justId', function() { + const justId = 'aaa'; + expect(justIdSubmodule.decode({uid: justId})).to.deep.eq({justId: justId}); + }) + }); + + describe('getId basic', function() { + var atmMock = (cmd, param) => { + switch (cmd) { + case 'getReadyState': + param('ready') + return; + case 'getVersion': + return Promise.resolve('1.0'); + case 'getUid': + param('user123'); + } + } + + var currentAtm; + + var getAtmStub = sinon.stub(jtUtils, 'getAtm').callsFake(() => currentAtm); + + var logErrorStub; + + beforeEach(function() { + logErrorStub = sinon.spy(utils, 'logError'); + }); + + afterEach(function() { + logErrorStub.restore(); + }); + + it('all ok', function(done) { + currentAtm = atmMock; + const callbackSpy = sinon.stub(); + + callbackSpy.callsFake(idObj => { + try { + expect(idObj.uid).to.equal('user123'); + done(); + } catch (err) { + done(err); + } + }) + + const atmVarName = '__fakeAtm'; + + justIdSubmodule.getId({params: {atmVarName: atmVarName}}).callback(callbackSpy); + + expect(getAtmStub.lastCall.lastArg).to.equal(atmVarName); + }); + + it('unsuported version', function(done) { + currentAtm = (cmd, param) => { + switch (cmd) { + case 'getReadyState': + param('ready') + } + } + + const callbackSpy = sinon.stub(); + + callbackSpy.callsFake(idObj => { + try { + expect(logErrorStub.calledOnce).to.be.true; + expect(idObj).to.be.undefined + done(); + } catch (err) { + done(err); + } + }) + + justIdSubmodule.getId({}).callback(callbackSpy); + }); + + it('work with stub', function(done) { + var calls = []; + currentAtm = (cmd, param) => { + calls.push({cmd: cmd, param: param}); + } + + const callbackSpy = sinon.stub(); + + callbackSpy.callsFake(idObj => { + try { + expect(idObj.uid).to.equal('user123'); + done(); + } catch (err) { + done(err); + } + }) + + justIdSubmodule.getId({}).callback(callbackSpy); + + currentAtm = atmMock; + expect(calls.length).to.equal(1); + expect(calls[0].cmd).to.equal('getReadyState'); + calls[0].param('ready') + }); + }); + + describe('getId combined', function() { + const scriptTag = document.createElement('script'); + + const onPrebidGetId = sinon.stub().callsFake(event => { + var cacheIdObj = event.detail && event.detail.cacheIdObj; + var justId = (cacheIdObj && cacheIdObj.uid && cacheIdObj.uid + '-x') || 'user123'; + scriptTag.dispatchEvent(new CustomEvent('justIdReady', { detail: { justId: justId } })); + }); + + scriptTag.addEventListener('prebidGetId', onPrebidGetId) + + var scriptTagCallback; + + beforeEach(() => { + loadExternalScriptStub.callsFake((url, moduleCode, callback) => { + scriptTagCallback = callback; + return scriptTag; + }); + }) + + var logErrorStub; + + beforeEach(() => { + logErrorStub = sinon.spy(utils, 'logError'); + }); + + afterEach(() => { + logErrorStub.restore(); + }); + + it('url is required', function() { + expect(justIdSubmodule.getId(configModeCombined())).to.be.undefined; + expect(logErrorStub.calledOnce).to.be.true; + }); + + it('without cachedIdObj', function() { + const callbackSpy = sinon.spy(); + justIdSubmodule.getId(configModeCombined(url)).callback(callbackSpy); + + scriptTagCallback(); + + expect(callbackSpy.lastCall.lastArg.uid).to.equal('user123'); + }); + + it('with cachedIdObj', function() { + const callbackSpy = sinon.spy(); + + justIdSubmodule.getId(configModeCombined(url), undefined, { uid: 'userABC' }).callback(callbackSpy); + + scriptTagCallback(); + + expect(callbackSpy.lastCall.lastArg.uid).to.equal('userABC-x'); + }); + + it('check if getId arguments are passed to prebidGetId event', function() { + const callbackSpy = sinon.spy(); + + const a = configModeCombined(url); + const b = { y: 'y' } + const c = { z: 'z' } + + justIdSubmodule.getId(a, b, c).callback(callbackSpy); + + scriptTagCallback(); + + expect(onPrebidGetId.lastCall.lastArg.detail).to.deep.eq({ config: a, consentData: b, cacheIdObj: c }); + }); + }); +}); + +function expectedUrl(url, srcId) { + return `${url}?sourceId=${srcId}` +} + +function configModeCombined(url, partner) { + var conf = { + params: { + mode: 'COMBINED' + } + } + url && (conf.params.url = url); + partner && (conf.params.partner = partner); + + return conf; +} diff --git a/test/spec/modules/justpremiumBidAdapter_spec.js b/test/spec/modules/justpremiumBidAdapter_spec.js index edc5325def3..3686418a991 100644 --- a/test/spec/modules/justpremiumBidAdapter_spec.js +++ b/test/spec/modules/justpremiumBidAdapter_spec.js @@ -97,12 +97,13 @@ describe('justpremium adapter', function () { expect(jpxRequest.id).to.equal(adUnits[0].params.zone) expect(jpxRequest.mediaTypes && jpxRequest.mediaTypes.banner && jpxRequest.mediaTypes.banner.sizes).to.not.equal('undefined') expect(jpxRequest.version.prebid).to.equal('$prebid.version$') - expect(jpxRequest.version.jp_adapter).to.equal('1.8.1') + expect(jpxRequest.version.jp_adapter).to.equal('1.8.2') expect(jpxRequest.pubcid).to.equal('0000000') expect(jpxRequest.uids.tdid).to.equal('1111111') expect(jpxRequest.uids.id5id.uid).to.equal('2222222') expect(jpxRequest.uids.digitrustid.data.id).to.equal('3333333') expect(jpxRequest.us_privacy).to.equal('1YYN') + expect(jpxRequest.ggExt).to.be.null }) }) diff --git a/test/spec/modules/jwplayerRtdProvider_spec.js b/test/spec/modules/jwplayerRtdProvider_spec.js index 458e45e8ae7..db17d18864b 100644 --- a/test/spec/modules/jwplayerRtdProvider_spec.js +++ b/test/spec/modules/jwplayerRtdProvider_spec.js @@ -1,7 +1,8 @@ import { fetchTargetingForMediaId, getVatFromCache, extractPublisherParams, formatTargetingResponse, getVatFromPlayer, enrichAdUnits, addTargetingToBid, - fetchTargetingInformation, jwplayerSubmodule } from 'modules/jwplayerRtdProvider.js'; + fetchTargetingInformation, jwplayerSubmodule, getContentId, getContentData } from 'modules/jwplayerRtdProvider.js'; import { server } from 'test/mocks/xhr.js'; +import {addOrtbSiteContent} from '../../../modules/jwplayerRtdProvider'; describe('jwplayerRtdProvider', function() { const testIdForSuccess = 'test_id_for_success'; @@ -412,7 +413,7 @@ describe('jwplayerRtdProvider', function() { }); }); - describe(' Extract Publisher Params', function () { + describe('Extract Publisher Params', function () { const config = { mediaID: 'test' }; it('should exclude adUnits that do not support instream video and do not specify jwTargeting', function () { @@ -480,6 +481,198 @@ describe('jwplayerRtdProvider', function() { }) }); + describe('Get content id', function() { + it('prefixes jw_ to the media id', function () { + const mediaId = 'mediaId'; + const contentId = getContentId(mediaId); + expect(contentId).to.equal('jw_mediaId'); + }); + + it('returns undefined when media id is empty', function () { + let contentId = getContentId(); + expect(contentId).to.be.undefined; + contentId = getContentId(''); + expect(contentId).to.be.undefined; + contentId = getContentId(null); + expect(contentId).to.be.undefined; + }); + }); + + describe('Get Content Data', function () { + it('returns undefined when segments are empty', function () { + let data = getContentData(null); + expect(data).to.be.undefined; + data = getContentData(undefined); + expect(data).to.be.undefined; + data = getContentData([]); + expect(data).to.be.undefined; + }); + + it('returns proper format', function () { + const segment1 = 'segment1'; + const segment2 = 'segment2'; + const segment3 = 'segment3'; + const data = getContentData([segment1, segment2, segment3]); + expect(data).to.have.property('name', 'jwplayer'); + expect(data.ext).to.have.property('segtax', 502); + expect(data.segment[0]).to.deep.equal({ id: segment1, value: segment1 }); + expect(data.segment[1]).to.deep.equal({ id: segment2, value: segment2 }); + expect(data.segment[2]).to.deep.equal({ id: segment3, value: segment3 }); + }); + }); + + describe(' Add Ortb Site Content', function () { + it('should maintain object structure when id and data params are empty', function () { + const bid = { + ortb2: { + site: { + content: { + id: 'randomId' + }, + random: { + random_sub: 'randomSub' + } + }, + app: { + content: { + id: 'appId' + } + } + } + }; + addOrtbSiteContent(bid); + expect(bid).to.have.nested.property('ortb2.site.content.id', 'randomId'); + expect(bid).to.have.nested.property('ortb2.site.random.random_sub', 'randomSub'); + expect(bid).to.have.nested.property('ortb2.app.content.id', 'appId'); + }); + + it('should create a structure compliant with the oRTB 2 spec', function() { + const bid = {}; + const expectedId = 'expectedId'; + const expectedData = { datum: 'datum' }; + addOrtbSiteContent(bid, expectedId, expectedData); + expect(bid).to.have.nested.property('ortb2.site.content.id', expectedId); + expect(bid).to.have.nested.property('ortb2.site.content.data'); + expect(bid.ortb2.site.content.data[0]).to.be.deep.equal(expectedData); + }); + + it('should respect existing structure when adding adding fields', function () { + const bid = { + ortb2: { + site: { + content: { + id: 'oldId' + }, + random: { + random_sub: 'randomSub' + } + }, + app: { + content: { + id: 'appId' + } + } + } + }; + + const expectedId = 'expectedId'; + const expectedData = { datum: 'datum' }; + addOrtbSiteContent(bid, expectedId, expectedData); + expect(bid).to.have.nested.property('ortb2.site.random.random_sub', 'randomSub'); + expect(bid).to.have.nested.property('ortb2.app.content.id', 'appId'); + expect(bid).to.have.nested.property('ortb2.site.content.id', expectedId); + expect(bid).to.have.nested.property('ortb2.site.content.data'); + expect(bid.ortb2.site.content.data[0]).to.be.deep.equal(expectedData); + }); + + it('should set content id', function () { + const bid = {}; + const expectedId = 'expectedId'; + addOrtbSiteContent(bid, expectedId); + expect(bid).to.have.nested.property('ortb2.site.content.id', expectedId); + }); + + it('should override content id', function () { + const bid = { + ortb2: { + site: { + content: { + id: 'oldId' + } + } + } + }; + + const expectedId = 'expectedId'; + addOrtbSiteContent(bid, expectedId); + expect(bid).to.have.nested.property('ortb2.site.content.id', expectedId); + }); + + it('should keep previous content id when not set', function () { + const previousId = 'oldId'; + const bid = { + ortb2: { + site: { + content: { + id: previousId, + data: [{ datum: 'first_datum' }] + } + } + } + }; + + addOrtbSiteContent(bid, null, { datum: 'new_datum' }); + expect(bid).to.have.nested.property('ortb2.site.content.id', previousId); + }); + + it('should set content data', function () { + const bid = {}; + const expectedData = { datum: 'datum' }; + addOrtbSiteContent(bid, null, expectedData); + expect(bid).to.have.nested.property('ortb2.site.content.data'); + expect(bid.ortb2.site.content.data).to.have.length(1); + expect(bid.ortb2.site.content.data[0]).to.be.deep.equal(expectedData); + }); + + it('should append content data', function () { + const bid = { + ortb2: { + site: { + content: { + data: [{ datum: 'first_datum' }] + } + } + } + }; + + const expectedData = { datum: 'datum' }; + addOrtbSiteContent(bid, null, expectedData); + expect(bid).to.have.nested.property('ortb2.site.content.data'); + expect(bid.ortb2.site.content.data).to.have.length(2); + expect(bid.ortb2.site.content.data.pop()).to.be.deep.equal(expectedData); + }); + + it('should keep previous data when not set', function () { + const expectedId = 'expectedId'; + const expectedData = { datum: 'first_datum' }; + const bid = { + ortb2: { + site: { + content: { + data: [expectedData] + } + } + } + }; + + addOrtbSiteContent(bid, expectedId); + expect(bid).to.have.nested.property('ortb2.site.content.data'); + expect(bid.ortb2.site.content.data).to.have.length(1); + expect(bid.ortb2.site.content.data[0]).to.be.deep.equal(expectedData); + expect(bid).to.have.nested.property('ortb2.site.content.id', expectedId); + }); + }); + describe('Add Targeting to Bid', function () { const targeting = {foo: 'bar'}; diff --git a/test/spec/modules/kargoBidAdapter_spec.js b/test/spec/modules/kargoBidAdapter_spec.js index 1eb514e87d2..6f5a0008783 100644 --- a/test/spec/modules/kargoBidAdapter_spec.js +++ b/test/spec/modules/kargoBidAdapter_spec.js @@ -149,20 +149,12 @@ describe('kargo adapter tests', function () { }; } - function initializeKruxUser() { - setLocalStorageItem('kxkar_user', 'rsgr9pnij'); - } - - function initializeKruxSegments() { - setLocalStorageItem('kxkar_segs', 'qv9v984dy,rpx2gy365,qrd5u4axv,rnub9nmtd,reha00jnu'); - } - function getKrgCrb() { - return 'eyJzeW5jSWRzIjp7IjIiOiI4MmZhMjU1NS01OTY5LTQ2MTQtYjRjZS00ZGNmMTA4MGU5ZjkiLCIxNiI6IlZveElrOEFvSnowQUFFZENleUFBQUFDMiY1MDIiLCIyMyI6ImQyYTg1NWE1LTFiMWMtNDMwMC05NDBlLWE3MDhmYTFmMWJkZSIsIjI0IjoiVm94SWs4QW9KejBBQUVkQ2V5QUFBQUMyJjUwMiIsIjI1IjoiNWVlMjQxMzgtNWUwMy00YjlkLWE5NTMtMzhlODMzZjI4NDlmIiwiMl84MCI6ImQyYTg1NWE1LTFiMWMtNDMwMC05NDBlLWE3MDhmYTFmMWJkZSIsIjJfOTMiOiI1ZWUyNDEzOC01ZTAzLTRiOWQtYTk1My0zOGU4MzNmMjg0OWYifSwidXNlcklkIjoiNWYxMDg4MzEtMzAyZC0xMWU3LWJmNmItNDU5NWFjZDNiZjZjIiwiY2xpZW50SWQiOiIyNDEwZDhmMi1jMTExLTQ4MTEtODhhNS03YjVlMTkwZTQ3NWYiLCJvcHRPdXQiOmZhbHNlLCJleHBpcmVUaW1lIjoxNDk3NDQ5MzgyNjY4LCJsYXN0U3luY2VkQXQiOjE0OTczNjI5NzkwMTJ9'; + return 'eyJzeW5jSWRzIjp7IjIiOiI4MmZhMjU1NS01OTY5LTQ2MTQtYjRjZS00ZGNmMTA4MGU5ZjkiLCIxNiI6IlZveElrOEFvSnowQUFFZENleUFBQUFDMiY1MDIiLCIyMyI6ImQyYTg1NWE1LTFiMWMtNDMwMC05NDBlLWE3MDhmYTFmMWJkZSIsIjI0IjoiVm94SWs4QW9KejBBQUVkQ2V5QUFBQUMyJjUwMiIsIjI1IjoiNWVlMjQxMzgtNWUwMy00YjlkLWE5NTMtMzhlODMzZjI4NDlmIiwiMl84MCI6ImQyYTg1NWE1LTFiMWMtNDMwMC05NDBlLWE3MDhmYTFmMWJkZSIsIjJfOTMiOiI1ZWUyNDEzOC01ZTAzLTRiOWQtYTk1My0zOGU4MzNmMjg0OWYifSwibGV4SWQiOiI1ZjEwODgzMS0zMDJkLTExZTctYmY2Yi00NTk1YWNkM2JmNmMiLCJjbGllbnRJZCI6IjI0MTBkOGYyLWMxMTEtNDgxMS04OGE1LTdiNWUxOTBlNDc1ZiIsIm9wdE91dCI6ZmFsc2UsImV4cGlyZVRpbWUiOjE0OTc0NDkzODI2NjgsImxhc3RTeW5jZWRBdCI6MTQ5NzM2Mjk3OTAxMn0='; } function getKrgCrbOldStyle() { - return '%7B%22v%22%3A%22eyJzeW5jSWRzIjp7IjIiOiI4MmZhMjU1NS01OTY5LTQ2MTQtYjRjZS00ZGNmMTA4MGU5ZjkiLCIxNiI6IlZveElrOEFvSnowQUFFZENleUFBQUFDMiY1MDIiLCIyMyI6ImQyYTg1NWE1LTFiMWMtNDMwMC05NDBlLWE3MDhmYTFmMWJkZSIsIjI0IjoiVm94SWs4QW9KejBBQUVkQ2V5QUFBQUMyJjUwMiIsIjI1IjoiNWVlMjQxMzgtNWUwMy00YjlkLWE5NTMtMzhlODMzZjI4NDlmIiwiMl84MCI6ImQyYTg1NWE1LTFiMWMtNDMwMC05NDBlLWE3MDhmYTFmMWJkZSIsIjJfOTMiOiI1ZWUyNDEzOC01ZTAzLTRiOWQtYTk1My0zOGU4MzNmMjg0OWYifSwidXNlcklkIjoiNWYxMDg4MzEtMzAyZC0xMWU3LWJmNmItNDU5NWFjZDNiZjZjIiwiY2xpZW50SWQiOiIyNDEwZDhmMi1jMTExLTQ4MTEtODhhNS03YjVlMTkwZTQ3NWYiLCJvcHRPdXQiOmZhbHNlLCJleHBpcmVUaW1lIjoxNDk3NDQ5MzgyNjY4LCJsYXN0U3luY2VkQXQiOjE0OTczNjI5NzkwMTJ9%22%7D'; + return '%7B%22v%22%3A%22eyJzeW5jSWRzIjp7IjIiOiI4MmZhMjU1NS01OTY5LTQ2MTQtYjRjZS00ZGNmMTA4MGU5ZjkiLCIxNiI6IlZveElrOEFvSnowQUFFZENleUFBQUFDMiY1MDIiLCIyMyI6ImQyYTg1NWE1LTFiMWMtNDMwMC05NDBlLWE3MDhmYTFmMWJkZSIsIjI0IjoiVm94SWs4QW9KejBBQUVkQ2V5QUFBQUMyJjUwMiIsIjI1IjoiNWVlMjQxMzgtNWUwMy00YjlkLWE5NTMtMzhlODMzZjI4NDlmIiwiMl84MCI6ImQyYTg1NWE1LTFiMWMtNDMwMC05NDBlLWE3MDhmYTFmMWJkZSIsIjJfOTMiOiI1ZWUyNDEzOC01ZTAzLTRiOWQtYTk1My0zOGU4MzNmMjg0OWYifSwibGV4SWQiOiI1ZjEwODgzMS0zMDJkLTExZTctYmY2Yi00NTk1YWNkM2JmNmMiLCJjbGllbnRJZCI6IjI0MTBkOGYyLWMxMTEtNDgxMS04OGE1LTdiNWUxOTBlNDc1ZiIsIm9wdE91dCI6ZmFsc2UsImV4cGlyZVRpbWUiOjE0OTc0NDkzODI2NjgsImxhc3RTeW5jZWRBdCI6MTQ5NzM2Mjk3OTAxMn0=%22%7D'; } function initializeKrgCrb(cookieOnly) { @@ -236,7 +228,7 @@ describe('kargo adapter tests', function () { return spec._getSessionId(); } - function getExpectedKrakenParams(excludeUserIds, excludeKrux, expectedRawCRB, expectedRawCRBCookie, expectedGDPR) { + function getExpectedKrakenParams(excludeUserIds, expectedRawCRB, expectedRawCRBCookie, expectedGDPR) { var base = { timeout: 200, requestCount: requestCount++, @@ -273,16 +265,6 @@ describe('kargo adapter tests', function () { optOut: false, usp: '1---' }, - krux: { - userID: 'rsgr9pnij', - segments: [ - 'qv9v984dy', - 'rpx2gy365', - 'qrd5u4axv', - 'rnub9nmtd', - 'reha00jnu' - ] - }, pageURL: window.location.href, prebidRawBidRequests: [ { @@ -326,13 +308,6 @@ describe('kargo adapter tests', function () { delete base.prebidRawBidRequests[0].userId.tdid; } - if (excludeKrux) { - base.krux = { - userID: null, - segments: [] - }; - } - return base; } @@ -367,101 +342,77 @@ describe('kargo adapter tests', function () { } it('works when all params and localstorage and cookies are correctly set', function() { - initializeKruxUser(); - initializeKruxSegments(); initializeKrgCrb(); - testBuildRequests(false, getExpectedKrakenParams(undefined, undefined, getKrgCrb(), getKrgCrbOldStyle())); + testBuildRequests(false, getExpectedKrakenParams(undefined, getKrgCrb(), getKrgCrbOldStyle())); }); it('works when all params and cookies are correctly set but no localstorage', function() { - initializeKruxUser(); - initializeKruxSegments(); initializeKrgCrb(true); - testBuildRequests(false, getExpectedKrakenParams(undefined, undefined, null, getKrgCrbOldStyle())); + testBuildRequests(false, getExpectedKrakenParams(undefined, null, getKrgCrbOldStyle())); }); it('gracefully handles nothing being set', function() { - testBuildRequests(true, getExpectedKrakenParams(true, true, null, null)); + testBuildRequests(true, getExpectedKrakenParams(true, null, null)); }); it('gracefully handles browsers without localStorage', function() { simulateNoLocalStorage(); - testBuildRequests(true, getExpectedKrakenParams(true, true, null, null)); + testBuildRequests(true, getExpectedKrakenParams(true, null, null)); }); it('handles empty yet valid Kargo CRB', function() { - initializeKruxUser(); - initializeKruxSegments(); initializeEmptyKrgCrb(); initializeEmptyKrgCrbCookie(); - testBuildRequests(true, getExpectedKrakenParams(true, undefined, getEmptyKrgCrb(), getEmptyKrgCrbOldStyle())); + testBuildRequests(true, getExpectedKrakenParams(true, getEmptyKrgCrb(), getEmptyKrgCrbOldStyle())); }); it('handles broken Kargo CRBs where base64 encoding is invalid', function() { - initializeKruxUser(); - initializeKruxSegments(); initializeInvalidKrgCrbType1(); - testBuildRequests(true, getExpectedKrakenParams(true, undefined, getInvalidKrgCrbType1(), null)); + testBuildRequests(true, getExpectedKrakenParams(true, getInvalidKrgCrbType1(), null)); }); it('handles broken Kargo CRBs where top level JSON is invalid on cookie', function() { - initializeKruxUser(); - initializeKruxSegments(); initializeInvalidKrgCrbType1Cookie(); - testBuildRequests(true, getExpectedKrakenParams(true, undefined, null, getInvalidKrgCrbType1())); + testBuildRequests(true, getExpectedKrakenParams(true, null, getInvalidKrgCrbType1())); }); it('handles broken Kargo CRBs where decoded JSON is invalid', function() { - initializeKruxUser(); - initializeKruxSegments(); initializeInvalidKrgCrbType2(); - testBuildRequests(true, getExpectedKrakenParams(true, undefined, getInvalidKrgCrbType2(), null)); + testBuildRequests(true, getExpectedKrakenParams(true, getInvalidKrgCrbType2(), null)); }); it('handles broken Kargo CRBs where inner base 64 is invalid on cookie', function() { - initializeKruxUser(); - initializeKruxSegments(); initializeInvalidKrgCrbType2Cookie(); - testBuildRequests(true, getExpectedKrakenParams(true, undefined, null, getInvalidKrgCrbType2OldStyle())); + testBuildRequests(true, getExpectedKrakenParams(true, null, getInvalidKrgCrbType2OldStyle())); }); it('handles broken Kargo CRBs where inner JSON is invalid on cookie', function() { - initializeKruxUser(); - initializeKruxSegments(); initializeInvalidKrgCrbType3Cookie(); - testBuildRequests(true, getExpectedKrakenParams(true, undefined, null, getInvalidKrgCrbType3OldStyle())); + testBuildRequests(true, getExpectedKrakenParams(true, null, getInvalidKrgCrbType3OldStyle())); }); it('handles broken Kargo CRBs where inner JSON is falsey', function() { - initializeKruxUser(); - initializeKruxSegments(); initializeInvalidKrgCrbType4Cookie(); - testBuildRequests(true, getExpectedKrakenParams(true, undefined, null, getInvalidKrgCrbType4OldStyle())); + testBuildRequests(true, getExpectedKrakenParams(true, null, getInvalidKrgCrbType4OldStyle())); }); it('handles a non-existant currency object on the config', function() { simulateNoCurrencyObject(); - initializeKruxUser(); - initializeKruxSegments(); initializeKrgCrb(); - testBuildRequests(false, getExpectedKrakenParams(undefined, undefined, getKrgCrb(), getKrgCrbOldStyle())); + testBuildRequests(false, getExpectedKrakenParams(undefined, getKrgCrb(), getKrgCrbOldStyle())); }); it('handles no ad server currency being set on the currency object in the config', function() { simulateNoAdServerCurrency(); - initializeKruxUser(); - initializeKruxSegments(); initializeKrgCrb(); - testBuildRequests(false, getExpectedKrakenParams(undefined, undefined, getKrgCrb(), getKrgCrbOldStyle())); + testBuildRequests(false, getExpectedKrakenParams(undefined, getKrgCrb(), getKrgCrbOldStyle())); }); it('sends gdpr consent', function () { - initializeKruxUser(); - initializeKruxSegments(); initializeKrgCrb(); - testBuildRequests(false, getExpectedKrakenParams(undefined, undefined, getKrgCrb(), getKrgCrbOldStyle(), generateGDPRExpect(true, true)), generateGDPR(true, true)); - testBuildRequests(false, getExpectedKrakenParams(undefined, undefined, getKrgCrb(), getKrgCrbOldStyle(), generateGDPRExpect(false, true)), generateGDPR(false, true)); - testBuildRequests(false, getExpectedKrakenParams(undefined, undefined, getKrgCrb(), getKrgCrbOldStyle(), generateGDPRExpect(false, false)), generateGDPR(false, false)); + testBuildRequests(false, getExpectedKrakenParams(undefined, getKrgCrb(), getKrgCrbOldStyle(), generateGDPRExpect(true, true)), generateGDPR(true, true)); + testBuildRequests(false, getExpectedKrakenParams(undefined, getKrgCrb(), getKrgCrbOldStyle(), generateGDPRExpect(false, true)), generateGDPR(false, true)); + testBuildRequests(false, getExpectedKrakenParams(undefined, getKrgCrb(), getKrgCrbOldStyle(), generateGDPRExpect(false, false)), generateGDPR(false, false)); }); }); @@ -493,6 +444,15 @@ describe('kargo adapter tests', function () { adm: '
', width: 300, height: 250 + }, + 4: { + id: 'bar', + cpm: 2.5, + adm: '
', + width: 300, + height: 250, + metadata: {}, + currency: 'EUR' } }}, { currency: 'USD', @@ -511,6 +471,11 @@ describe('kargo adapter tests', function () { params: { placementId: 'bar' } + }, { + bidId: 4, + params: { + placementId: 'bar' + } }] }); var expectation = [{ @@ -552,6 +517,18 @@ describe('kargo adapter tests', function () { netRevenue: true, currency: 'USD', meta: undefined + }, { + requestId: '4', + cpm: 2.5, + width: 300, + height: 250, + ad: '
', + ttl: 300, + creativeId: 'bar', + dealId: undefined, + netRevenue: true, + currency: 'EUR', + meta: undefined }]; expect(resp).to.deep.equal(expectation); }); diff --git a/test/spec/modules/kubientBidAdapter_spec.js b/test/spec/modules/kubientBidAdapter_spec.js index 5449de0c4de..f7afc709564 100644 --- a/test/spec/modules/kubientBidAdapter_spec.js +++ b/test/spec/modules/kubientBidAdapter_spec.js @@ -1,6 +1,13 @@ import { expect, assert } from 'chai'; import { spec } from 'modules/kubientBidAdapter.js'; import { BANNER, VIDEO } from '../../../src/mediaTypes.js'; +import {config} from '../../../src/config'; + +function encodeQueryData(data) { + return Object.keys(data).map(function(key) { + return [key, data[key]].map(encodeURIComponent).join('='); + }).join('&'); +} describe('KubientAdapter', function () { let bidBanner = { @@ -12,7 +19,7 @@ describe('KubientAdapter', function () { }, getFloor: function(params) { return { - floor: 0.05, + floor: 0, currency: 'USD' }; }, @@ -94,24 +101,20 @@ describe('KubientAdapter', function () { uspConsent: uspConsentData }; describe('buildRequestBanner', function () { - let serverRequests = spec.buildRequests([bidBanner], Object.assign({}, bidderRequest, {bids: [bidBanner]})); - it('Creates a ServerRequest object with method, URL and data', function () { - expect(serverRequests).to.be.an('array'); + beforeEach(function () { + config.resetConfig(); }); - for (let i = 0; i < serverRequests.length; i++) { - let serverRequest = serverRequests[i]; - it('Creates a ServerRequest object with method, URL and data', function () { + it('Creates Banner 1 ServerRequest object with method, URL and data', function () { + config.setConfig({'coppa': false}); + let serverRequests = spec.buildRequests([bidBanner], Object.assign({}, bidderRequest, {bids: [bidBanner]})); + expect(serverRequests).to.be.an('array'); + for (let i = 0; i < serverRequests.length; i++) { + let serverRequest = serverRequests[i]; expect(serverRequest.method).to.be.a('string'); expect(serverRequest.url).to.be.a('string'); expect(serverRequest.data).to.be.a('string'); - }); - it('Returns POST method', function () { expect(serverRequest.method).to.equal('POST'); - }); - it('Returns valid URL', function () { expect(serverRequest.url).to.equal('https://kssp.kbntx.ch/kubprebidjs'); - }); - it('Returns valid data if array of bids is valid', function () { let data = JSON.parse(serverRequest.data); expect(data).to.be.an('object'); expect(data).to.have.all.keys('v', 'requestId', 'adSlots', 'gdpr', 'referer', 'tmax', 'consent', 'consentGiven', 'uspConsent'); @@ -124,35 +127,30 @@ describe('KubientAdapter', function () { expect(data.uspConsent).to.exist.and.to.equal(uspConsentData); for (let j = 0; j < data['adSlots'].length; j++) { let adSlot = data['adSlots'][i]; - expect(adSlot).to.have.all.keys('bidId', 'zoneId', 'floor', 'banner', 'schain'); + expect(adSlot).to.have.all.keys('bidId', 'zoneId', 'banner', 'schain'); expect(adSlot.bidId).to.be.a('string').and.to.equal(bidBanner.bidId); expect(adSlot.zoneId).to.be.a('string').and.to.equal(bidBanner.params.zoneid); - expect(adSlot.floor).to.be.a('number'); expect(adSlot.schain).to.be.an('object'); expect(adSlot.banner).to.be.an('object'); } - }); - } + } + }); }); describe('buildRequestVideo', function () { - let serverRequests = spec.buildRequests([bidVideo], Object.assign({}, bidderRequest, {bids: [bidVideo]})); - it('Creates a ServerRequest object with method, URL and data', function () { - expect(serverRequests).to.be.an('array'); + beforeEach(function () { + config.resetConfig(); }); - for (let i = 0; i < serverRequests.length; i++) { - let serverRequest = serverRequests[i]; - it('Creates a ServerRequest object with method, URL and data', function () { + it('Creates Video 1 ServerRequest object with method, URL and data', function () { + config.setConfig({'coppa': false}); + let serverRequests = spec.buildRequests([bidVideo], Object.assign({}, bidderRequest, {bids: [bidVideo]})); + expect(serverRequests).to.be.an('array'); + for (let i = 0; i < serverRequests.length; i++) { + let serverRequest = serverRequests[i]; expect(serverRequest.method).to.be.a('string'); expect(serverRequest.url).to.be.a('string'); expect(serverRequest.data).to.be.a('string'); - }); - it('Returns POST method', function () { expect(serverRequest.method).to.equal('POST'); - }); - it('Returns valid URL', function () { expect(serverRequest.url).to.equal('https://kssp.kbntx.ch/kubprebidjs'); - }); - it('Returns valid data if array of bids is valid', function () { let data = JSON.parse(serverRequest.data); expect(data).to.be.an('object'); expect(data).to.have.all.keys('v', 'requestId', 'adSlots', 'gdpr', 'referer', 'tmax', 'consent', 'consentGiven', 'uspConsent'); @@ -172,11 +170,88 @@ describe('KubientAdapter', function () { expect(adSlot.schain).to.be.an('object'); expect(adSlot.video).to.be.an('object'); } - }); - } + } + }); + }); + describe('buildRequestBanner', function () { + beforeEach(function () { + config.resetConfig(); + }); + it('Creates Banner 2 ServerRequest object with method, URL and data with bidBanner', function () { + config.setConfig({'coppa': true}); + let serverRequests = spec.buildRequests([bidBanner], Object.assign({}, bidderRequest, {bids: [bidBanner]})); + expect(serverRequests).to.be.an('array'); + for (let i = 0; i < serverRequests.length; i++) { + let serverRequest = serverRequests[i]; + expect(serverRequest.method).to.be.a('string'); + expect(serverRequest.url).to.be.a('string'); + expect(serverRequest.data).to.be.a('string'); + expect(serverRequest.method).to.equal('POST'); + expect(serverRequest.url).to.equal('https://kssp.kbntx.ch/kubprebidjs'); + let data = JSON.parse(serverRequest.data); + expect(data).to.be.an('object'); + expect(data).to.have.all.keys('v', 'requestId', 'adSlots', 'gdpr', 'coppa', 'referer', 'tmax', 'consent', 'consentGiven', 'uspConsent'); + expect(data.v).to.exist.and.to.be.a('string'); + expect(data.requestId).to.exist.and.to.be.a('string'); + expect(data.coppa).to.be.a('number').and.to.equal(1); + expect(data.referer).to.be.a('string'); + expect(data.tmax).to.exist.and.to.be.a('number'); + expect(data.gdpr).to.exist.and.to.be.within(0, 1); + expect(data.consent).to.equal(consentString); + expect(data.uspConsent).to.exist.and.to.equal(uspConsentData); + for (let j = 0; j < data['adSlots'].length; j++) { + let adSlot = data['adSlots'][i]; + expect(adSlot).to.have.all.keys('bidId', 'zoneId', 'banner', 'schain'); + expect(adSlot.bidId).to.be.a('string').and.to.equal(bidBanner.bidId); + expect(adSlot.zoneId).to.be.a('string').and.to.equal(bidBanner.params.zoneid); + expect(adSlot.schain).to.be.an('object'); + expect(adSlot.banner).to.be.an('object'); + } + } + }); + }); + describe('buildRequestVideo', function () { + beforeEach(function () { + config.resetConfig(); + }); + it('Creates Video 2 ServerRequest object with method, URL and data', function () { + config.setConfig({'coppa': true}); + let serverRequests = spec.buildRequests([bidVideo], Object.assign({}, bidderRequest, {bids: [bidVideo]})); + expect(serverRequests).to.be.an('array'); + for (let i = 0; i < serverRequests.length; i++) { + let serverRequest = serverRequests[i]; + expect(serverRequest.method).to.be.a('string'); + expect(serverRequest.url).to.be.a('string'); + expect(serverRequest.data).to.be.a('string'); + expect(serverRequest.method).to.equal('POST'); + expect(serverRequest.url).to.equal('https://kssp.kbntx.ch/kubprebidjs'); + let data = JSON.parse(serverRequest.data); + expect(data).to.be.an('object'); + expect(data).to.have.all.keys('v', 'requestId', 'adSlots', 'gdpr', 'coppa', 'referer', 'tmax', 'consent', 'consentGiven', 'uspConsent'); + expect(data.v).to.exist.and.to.be.a('string'); + expect(data.requestId).to.exist.and.to.be.a('string'); + expect(data.coppa).to.be.a('number').and.to.equal(1); + expect(data.referer).to.be.a('string'); + expect(data.tmax).to.exist.and.to.be.a('number'); + expect(data.gdpr).to.exist.and.to.be.within(0, 1); + expect(data.consent).to.equal(consentString); + expect(data.uspConsent).to.exist.and.to.equal(uspConsentData); + for (let j = 0; j < data['adSlots'].length; j++) { + let adSlot = data['adSlots'][i]; + expect(adSlot).to.have.all.keys('bidId', 'zoneId', 'floor', 'video', 'schain'); + expect(adSlot.bidId).to.be.a('string').and.to.equal(bidVideo.bidId); + expect(adSlot.zoneId).to.be.a('string').and.to.equal(bidVideo.params.zoneid); + expect(adSlot.floor).to.be.a('number'); + expect(adSlot.schain).to.be.an('object'); + expect(adSlot.video).to.be.an('object'); + } + } + }); }); - describe('isBidRequestValid', function () { + beforeEach(function () { + config.resetConfig(); + }); it('Should return true when required params are found', function () { expect(spec.isBidRequestValid(bidBanner)).to.be.true; expect(spec.isBidRequestValid(bidVideo)).to.be.true; @@ -192,8 +267,10 @@ describe('KubientAdapter', function () { expect(spec.isBidRequestValid(bidVideo)).to.be.false; }); }); - describe('interpretResponse', function () { + beforeEach(function () { + config.resetConfig(); + }); it('Should interpret response', function () { const serverResponse = { body: @@ -234,7 +311,6 @@ describe('KubientAdapter', function () { expect(dataItem.meta).to.exist.and.to.be.a('object'); expect(dataItem.meta.advertiserDomains).to.exist.and.to.be.a('array').and.to.equal(serverResponse.body.seatbid[0].bid[0].meta.adomain); }); - it('Should return no ad when not given a server response', function () { const ads = spec.interpretResponse(null); expect(ads).to.be.an('array').and.to.have.length(0); @@ -242,6 +318,9 @@ describe('KubientAdapter', function () { }); describe('interpretResponse Video', function () { + beforeEach(function () { + config.resetConfig(); + }); it('Should interpret response', function () { const serverResponse = { body: @@ -285,7 +364,6 @@ describe('KubientAdapter', function () { expect(dataItem.mediaType).to.exist.and.to.equal(VIDEO); expect(dataItem.vastXml).to.exist.and.to.be.a('string').and.to.equal(serverResponse.body.seatbid[0].bid[0].adm); }); - it('Should return no ad when not given a server response', function () { const ads = spec.interpretResponse(null); expect(ads).to.be.an('array').and.to.have.length(0); @@ -293,89 +371,68 @@ describe('KubientAdapter', function () { }); describe('getUserSyncs', function () { - it('should register the sync iframe without gdpr', function () { - let syncOptions = { - iframeEnabled: true - }; - let serverResponses = null; - let gdprConsent = { - consentString: consentString - }; - let uspConsent = null; - let syncs = spec.getUserSyncs(syncOptions, serverResponses, gdprConsent, uspConsent); - expect(syncs).to.be.an('array').and.to.have.length(1); - expect(syncs[0].type).to.equal('iframe'); - expect(syncs[0].url).to.equal('https://kdmp.kbntx.ch/init.html?consent_str=' + consentString + '&consent_given=0'); - }); - it('should register the sync iframe with gdpr', function () { - let syncOptions = { - iframeEnabled: true - }; - let serverResponses = null; - let gdprConsent = { - gdprApplies: true, - consentString: consentString - }; - let uspConsent = null; - let syncs = spec.getUserSyncs(syncOptions, serverResponses, gdprConsent, uspConsent); - expect(syncs).to.be.an('array').and.to.have.length(1); - expect(syncs[0].type).to.equal('iframe'); - expect(syncs[0].url).to.equal('https://kdmp.kbntx.ch/init.html?consent_str=' + consentString + '&gdpr=1&consent_given=0'); - }); - it('should register the sync iframe with gdpr vendor', function () { - let syncOptions = { - iframeEnabled: true - }; - let serverResponses = null; - let gdprConsent = { - gdprApplies: true, - consentString: consentString, - apiVersion: 1, - vendorData: { - vendorConsents: { - 794: 1 - } - } - }; - let uspConsent = null; - let syncs = spec.getUserSyncs(syncOptions, serverResponses, gdprConsent, uspConsent); - expect(syncs).to.be.an('array').and.to.have.length(1); - expect(syncs[0].type).to.equal('iframe'); - expect(syncs[0].url).to.equal('https://kdmp.kbntx.ch/init.html?consent_str=' + consentString + '&gdpr=1&consent_given=1'); + beforeEach(function () { + config.resetConfig(); }); it('should register the sync image without gdpr', function () { let syncOptions = { pixelEnabled: true }; + let values = {}; let serverResponses = null; let gdprConsent = { consentString: consentString }; let uspConsent = null; + config.setConfig({ + userSync: { + filterSettings: { + image: { + bidders: '*', + filter: 'include' + } + } + } + }); let syncs = spec.getUserSyncs(syncOptions, serverResponses, gdprConsent, uspConsent); + values['consent'] = consentString; expect(syncs).to.be.an('array').and.to.have.length(1); expect(syncs[0].type).to.equal('image'); - expect(syncs[0].url).to.equal('https://kdmp.kbntx.ch/init.png?consent_str=' + consentString + '&consent_given=0'); + expect(syncs[0].url).to.equal('https://matching.kubient.net/match/sp?' + encodeQueryData(values)); }); it('should register the sync image with gdpr', function () { let syncOptions = { pixelEnabled: true }; + let values = {}; let serverResponses = null; let gdprConsent = { gdprApplies: true, consentString: consentString }; let uspConsent = null; + config.setConfig({ + userSync: { + filterSettings: { + image: { + bidders: '*', + filter: 'include' + } + } + } + }); let syncs = spec.getUserSyncs(syncOptions, serverResponses, gdprConsent, uspConsent); + values['gdpr'] = 1; + values['consent'] = consentString; expect(syncs).to.be.an('array').and.to.have.length(1); expect(syncs[0].type).to.equal('image'); - expect(syncs[0].url).to.equal('https://kdmp.kbntx.ch/init.png?consent_str=' + consentString + '&gdpr=1&consent_given=0'); + expect(syncs[0].url).to.equal('https://matching.kubient.net/match/sp?' + encodeQueryData(values)); }); it('should register the sync image with gdpr vendor', function () { let syncOptions = { pixelEnabled: true }; + let values = {}; let serverResponses = null; let gdprConsent = { gdprApplies: true, @@ -390,10 +447,49 @@ describe('KubientAdapter', function () { } }; let uspConsent = null; + config.setConfig({ + userSync: { + filterSettings: { + image: { + bidders: '*', + filter: 'include' + } + } + } + }); + let syncs = spec.getUserSyncs(syncOptions, serverResponses, gdprConsent, uspConsent); + values['gdpr'] = 1; + values['consent'] = consentString; + expect(syncs).to.be.an('array').and.to.have.length(1); + expect(syncs[0].type).to.equal('image'); + expect(syncs[0].url).to.equal('https://matching.kubient.net/match/sp?' + encodeQueryData(values)); + }); + it('should register the sync image without gdpr and with uspConsent', function () { + let syncOptions = { + pixelEnabled: true + }; + let values = {}; + let serverResponses = null; + let gdprConsent = { + consentString: consentString + }; + let uspConsent = '1YNN'; + config.setConfig({ + userSync: { + filterSettings: { + image: { + bidders: '*', + filter: 'include' + } + } + } + }); let syncs = spec.getUserSyncs(syncOptions, serverResponses, gdprConsent, uspConsent); + values['consent'] = consentString; + values['usp'] = uspConsent; expect(syncs).to.be.an('array').and.to.have.length(1); expect(syncs[0].type).to.equal('image'); - expect(syncs[0].url).to.equal('https://kdmp.kbntx.ch/init.png?consent_str=' + consentString + '&gdpr=1&consent_given=1'); + expect(syncs[0].url).to.equal('https://matching.kubient.net/match/sp?' + encodeQueryData(values)); }); }) }); diff --git a/test/spec/modules/limelightDigitalBidAdapter_spec.js b/test/spec/modules/limelightDigitalBidAdapter_spec.js index 191c4759f76..115acda722f 100644 --- a/test/spec/modules/limelightDigitalBidAdapter_spec.js +++ b/test/spec/modules/limelightDigitalBidAdapter_spec.js @@ -359,107 +359,124 @@ describe('limelightDigitalAdapter', function () { }); }); describe('getUserSyncs', function () { - const serverResponses = [ - { - body: [ - { - ext: { - sync: { - iframe: 'iframeUrl', - } - } - }, - { - ext: { - sync: { - pixel: 'pixelUrl' - } - } - }, - {}, - { - ext: {} - }, - { - ext: { - sync: {} - } - }, - { - ext: { - sync: { - iframe: 'iframeUrl2', - pixel: 'pixelUrl3' + it('should return trackers for lm(only iframe) if server responses contain lm user sync header and iframe and image enabled', function () { + const serverResponses = [ + { + headers: { + get: function (header) { + if (header === 'X-PLL-UserSync-Image') { + return 'https://tracker-lm.ortb.net/sync'; } - } - } - ] - }, - { - body: [ - { - ext: { - sync: { - iframe: 'iframeUrl2', - pixel: 'pixelUrl2' + if (header === 'X-PLL-UserSync-Iframe') { + return 'https://tracker-lm.ortb.net/sync.html'; } } }, - { - ext: { - sync: { - iframe: 'iframeUrl3', - pixel: 'pixelUrl3' - } - } - } - ] - } - ]; - it('should return empty array if server responses do not contain sync urls', function () { + body: [] + } + ]; const syncOptions = { iframeEnabled: true, pixelEnabled: true }; - const serverResponsesWithoutSyncUrls = serverResponses.map(serverResponse => { - const serverResponseWithoutSyncUrls = Object.assign({}, serverResponse); - serverResponseWithoutSyncUrls.body = serverResponse.body.map(serverResponseBody => { - const serverResponseBodyWithoutSyncUrls = Object.assign({}, serverResponseBody); - delete serverResponseBodyWithoutSyncUrls.ext; - return serverResponseBodyWithoutSyncUrls; - }); - return serverResponseWithoutSyncUrls; - }); - expect(spec.getUserSyncs(syncOptions, serverResponsesWithoutSyncUrls)).to.be.an('array').that.is.empty; + expect(spec.getUserSyncs(syncOptions, serverResponses)).to.deep.equal([ + { + type: 'iframe', + url: 'https://tracker-lm.ortb.net/sync.html' + } + ]); }); it('should return empty array if all sync types are disabled', function () { + const serverResponses = [ + { + headers: { + get: function (header) { + if (header === 'X-PLL-UserSync-Image') { + return 'https://tracker-1.ortb.net/sync'; + } + if (header === 'X-PLL-UserSync-Iframe') { + return 'https://tracker-1.ortb.net/sync.html'; + } + } + }, + body: [] + } + ]; const syncOptions = { iframeEnabled: false, pixelEnabled: false }; expect(spec.getUserSyncs(syncOptions, serverResponses)).to.be.an('array').that.is.empty; }); - it('should return iframe sync urls if iframe sync is enabled', function () { + it('should return no pixels if iframe sync is enabled and headers are blank', function () { + const serverResponses = [ + { + headers: null, + body: [] + } + ]; const syncOptions = { iframeEnabled: true, pixelEnabled: false }; + expect(spec.getUserSyncs(syncOptions, serverResponses)).to.be.an('array').that.is.empty; + }); + it('should return image sync urls for lm if pixel sync is enabled and headers have lm pixel', function () { + const serverResponses = [ + { + headers: { + get: function (header) { + if (header === 'X-PLL-UserSync-Image') { + return 'https://tracker-lm.ortb.net/sync'; + } + if (header === 'X-PLL-UserSync-Iframe') { + return 'https://tracker-lm.ortb.net/sync.html'; + } + } + }, + body: [] + } + ]; + const syncOptions = { + iframeEnabled: false, + pixelEnabled: true + }; expect(spec.getUserSyncs(syncOptions, serverResponses)).to.deep.equal([ { - type: 'iframe', - url: 'iframeUrl' - }, + type: 'image', + url: 'https://tracker-lm.ortb.net/sync' + } + ]); + }); + it('should return image sync urls for client1 and clien2 if pixel sync is enabled and two responses and headers have two pixels', function () { + const serverResponses = [ { - type: 'iframe', - url: 'iframeUrl2' + headers: { + get: function (header) { + if (header === 'X-PLL-UserSync-Image') { + return 'https://tracker-1.ortb.net/sync'; + } + if (header === 'X-PLL-UserSync-Iframe') { + return 'https://tracker-1.ortb.net/sync.html'; + } + } + }, + body: [] }, { - type: 'iframe', - url: 'iframeUrl3' + headers: { + get: function (header) { + if (header === 'X-PLL-UserSync-Image') { + return 'https://tracker-2.ortb.net/sync'; + } + if (header === 'X-PLL-UserSync-Iframe') { + return 'https://tracker-2.ortb.net/sync.html'; + } + } + }, + body: [] } - ]); - }); - it('should return image sync urls if pixel sync is enabled', function () { + ]; const syncOptions = { iframeEnabled: false, pixelEnabled: true @@ -467,47 +484,78 @@ describe('limelightDigitalAdapter', function () { expect(spec.getUserSyncs(syncOptions, serverResponses)).to.deep.equal([ { type: 'image', - url: 'pixelUrl' + url: 'https://tracker-1.ortb.net/sync' }, { type: 'image', - url: 'pixelUrl3' + url: 'https://tracker-2.ortb.net/sync' + } + ]); + }); + it('should return image sync url for pll if pixel sync is enabled and two responses and headers have two same pixels', function () { + const serverResponses = [ + { + headers: { + get: function (header) { + if (header === 'X-PLL-UserSync-Image') { + return 'https://tracker-lm.ortb.net/sync'; + } + if (header === 'X-PLL-UserSync-Iframe') { + return 'https://tracker-lm.ortb.net/sync.html'; + } + } + }, + body: [] }, + { + headers: { + get: function (header) { + if (header === 'X-PLL-UserSync-Image') { + return 'https://tracker-lm.ortb.net/sync'; + } + if (header === 'X-PLL-UserSync-Iframe') { + return 'https://tracker-lm.ortb.net/sync.html'; + } + } + }, + body: [] + } + ]; + const syncOptions = { + iframeEnabled: false, + pixelEnabled: true + }; + expect(spec.getUserSyncs(syncOptions, serverResponses)).to.deep.equal([ { type: 'image', - url: 'pixelUrl2' + url: 'https://tracker-lm.ortb.net/sync' } ]); }); - it('should return all sync urls if all sync types are enabled', function () { + it('should return iframe sync url for pll if pixel sync is enabled and iframe is enables and headers have both iframe and img pixels', function () { + const serverResponses = [ + { + headers: { + get: function (header) { + if (header === 'X-PLL-UserSync-Image') { + return 'https://tracker-lm.ortb.net/sync'; + } + if (header === 'X-PLL-UserSync-Iframe') { + return 'https://tracker-lm.ortb.net/sync.html'; + } + } + }, + body: [] + } + ]; const syncOptions = { iframeEnabled: true, pixelEnabled: true - } + }; expect(spec.getUserSyncs(syncOptions, serverResponses)).to.deep.equal([ { type: 'iframe', - url: 'iframeUrl' - }, - { - type: 'iframe', - url: 'iframeUrl2' - }, - { - type: 'iframe', - url: 'iframeUrl3' - }, - { - type: 'image', - url: 'pixelUrl' - }, - { - type: 'image', - url: 'pixelUrl3' - }, - { - type: 'image', - url: 'pixelUrl2' + url: 'https://tracker-lm.ortb.net/sync.html' } ]); }); diff --git a/test/spec/modules/liveIntentIdSystem_spec.js b/test/spec/modules/liveIntentIdSystem_spec.js index f1de2f3bf93..4893f7cbb30 100644 --- a/test/spec/modules/liveIntentIdSystem_spec.js +++ b/test/spec/modules/liveIntentIdSystem_spec.js @@ -66,7 +66,7 @@ describe('LiveIntentId', function() { consentString: 'consentDataString' }) liveIntentIdSubmodule.getId(defaultConfigParams); - expect(server.requests[0].url).to.match(/https:\/\/rp.liadm.com\/j\?wpn=prebid.*us_privacy=1YNY.*&gdpr=1&gdpr_consent=consentDataString.*/); + expect(server.requests[0].url).to.match(/https:\/\/rp.liadm.com\/j\?.*&us_privacy=1YNY.*&wpn=prebid.*&gdpr=1&gdpr_consent=consentDataString.*/); }); it('should fire an event when getId and a hash is provided', function() { @@ -88,7 +88,7 @@ describe('LiveIntentId', function() { } } }}); - expect(server.requests[0].url).to.match(/https:\/\/collector.liveintent.com\/j\?aid=a-0001&wpn=prebid.*/); + expect(server.requests[0].url).to.match(/https:\/\/collector.liveintent.com\/j\?.*aid=a-0001.*&wpn=prebid.*/); }); it('should initialize LiveConnect and emit an event with a privacy string when decode', function() { @@ -195,7 +195,7 @@ describe('LiveIntentId', function() { it('should include the LiveConnect identifier when calling the LiveIntent Identity Exchange endpoint', function() { const oldCookie = 'a-xxxx--123e4567-e89b-12d3-a456-426655440000' - getDataFromLocalStorageStub.withArgs('_li_duid').returns(oldCookie); + getCookieStub.withArgs('_lc2_fpi').returns(oldCookie) let callBackSpy = sinon.spy(); let submoduleCallback = liveIntentIdSubmodule.getId(defaultConfigParams).callback; submoduleCallback(callBackSpy); @@ -211,7 +211,7 @@ describe('LiveIntentId', function() { it('should include the LiveConnect identifier and additional Identifiers to resolve', function() { const oldCookie = 'a-xxxx--123e4567-e89b-12d3-a456-426655440000' - getDataFromLocalStorageStub.withArgs('_li_duid').returns(oldCookie); + getCookieStub.withArgs('_lc2_fpi').returns(oldCookie); getDataFromLocalStorageStub.withArgs('_thirdPC').returns('third-pc'); const configParams = { params: { ...defaultConfigParams.params, diff --git a/test/spec/modules/livewrappedAnalyticsAdapter_spec.js b/test/spec/modules/livewrappedAnalyticsAdapter_spec.js index bd6f361572b..bab7a68287d 100644 --- a/test/spec/modules/livewrappedAnalyticsAdapter_spec.js +++ b/test/spec/modules/livewrappedAnalyticsAdapter_spec.js @@ -38,6 +38,10 @@ const BID1 = { adId: '2ecff0db240757', auctionId: '25c6d7f5-699a-4bfc-87c9-996f915341fa', mediaType: 'banner', + meta: { + data: 'value1' + }, + dealId: 'dealid', getStatusCode() { return CONSTANTS.STATUS.GOOD; } @@ -54,6 +58,10 @@ const BID2 = Object.assign({}, BID1, { bidId: '3ecff0db240757', requestId: '3ecff0db240757', adId: '3ecff0db240757', + meta: { + data: 'value2' + }, + dealId: undefined }); const BID3 = { @@ -190,7 +198,10 @@ const ANALYTICS_MESSAGE = { IsBid: true, mediaType: 1, gdpr: 0, - auctionId: 0 + auctionId: 0, + meta: { + data: 'value1' + } }, { timeStamp: 1519149562216, @@ -205,7 +216,10 @@ const ANALYTICS_MESSAGE = { IsBid: true, mediaType: 1, gdpr: 0, - auctionId: 0 + auctionId: 0, + meta: { + data: 'value2' + } }, { timeStamp: 1519149562216, @@ -231,7 +245,11 @@ const ANALYTICS_MESSAGE = { orgCpm: 120, mediaType: 1, gdpr: 0, - auctionId: 0 + auctionId: 0, + meta: { + data: 'value1' + }, + dealId: 'dealid' }, { timeStamp: 1519149562216, @@ -244,7 +262,10 @@ const ANALYTICS_MESSAGE = { orgCpm: 230, mediaType: 1, gdpr: 0, - auctionId: 0 + auctionId: 0, + meta: { + data: 'value2' + } } ], rf: [ diff --git a/test/spec/modules/lkqdBidAdapter_spec.js b/test/spec/modules/lkqdBidAdapter_spec.js new file mode 100644 index 00000000000..6e2c7fe8e2c --- /dev/null +++ b/test/spec/modules/lkqdBidAdapter_spec.js @@ -0,0 +1,323 @@ +import { spec } from 'modules/lkqdBidAdapter.js'; +import { newBidder } from 'src/adapters/bidderFactory.js'; +import { config } from 'src/config.js'; +import { expect } from 'chai'; + +describe('lkqdBidAdapter', () => { + const BIDDER_CODE = 'lkqd'; + const SITE_ID = '662921'; + const PUBLISHER_ID = '263'; + const END_POINT = new URL('https://rtb.lkqd.net/ad'); + const ADAPTER = newBidder(spec); + + let sandbox; + + beforeEach(() => { + sandbox = sinon.createSandbox(); + }); + + afterEach(() => { + sandbox.restore(); + }); + + context('inherited functions', () => { + it('exists and is a function', () => { + expect(ADAPTER.callBids).to.exist.and.to.be.a('function'); + }); + }); + + context('isBidRequestValid', () => { + const bid = { + bidder: BIDDER_CODE, + params: { + 'siteId': SITE_ID, + 'placementId': PUBLISHER_ID + }, + adUnitCode: 'video1', + sizes: [[300, 250], [640, 480]], + bidId: '30b31c1838de1e', + bidderRequestId: '22edbae2733bf6', + requestId: 'a09c66c3-53e3-4428-b296-38fc08e7cd2a', + transactionId: 'd6f6b392-54a9-454c-85fb-a2fd882c4a2d', + }; + + it('should return true when required params found', () => { + expect(spec.isBidRequestValid(bid)).to.equal(true); + }); + + it('should return false when required params are not passed', () => { + let bid = Object.assign({}, bid); + delete bid.params; + bid.params = { + wrong: 'missing zone id' + }; + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + }); + + context('buildRequests', () => { + const bidRequests = [ + { + 'bidder': BIDDER_CODE, + 'params': { + 'siteId': SITE_ID, + 'placementId': PUBLISHER_ID, + 'c1': 'newWindow', + 'c20': 'lkqdCustom' + }, + 'adUnitCode': BIDDER_CODE, + 'sizes': [[300, 250], [640, 480]], + 'bidId': '30b31c1838de1e', + 'bidderRequestId': '22edbae2733bf6', + 'requestId': 'a09c66c3-53e3-4428-b296-38fc08e7cd2a', + 'transactionId': 'd6f6b392-54a9-454c-85fb-a2fd882c4a2d', + } + ]; + const bidRequest = [ + { + 'bidder': BIDDER_CODE, + 'params': { + 'siteId': SITE_ID, + 'placementId': PUBLISHER_ID, + 'schain': '1.0,1!exchange1.com,1234%21abcd,1,bid-request-1,publisher%2c%20Inc.,publisher.com' + }, + 'adUnitCode': BIDDER_CODE, + 'sizes': [640, 480], + 'bidId': '30b31c1838de1e', + 'bidderRequestId': '22edbae2733bf6', + 'requestId': 'a09c66c3-53e3-4428-b296-38fc08e7cd2a', + 'transactionId': 'd6f6b392-54a9-454c-85fb-a2fd882c4a2d', + } + ]; + + it('should have correct url params, method', () => { + const requests = spec.buildRequests(bidRequests, {}); + expect(requests[0].method).to.eq('POST'); + + const url1 = new URL(requests[0].url); + expect(url1.origin).to.eq(END_POINT.origin); + + const params1 = new URLSearchParams(url1.search); + const object1 = Object.fromEntries(params1.entries()); + expect(object1.pid).to.eq(PUBLISHER_ID); + expect(object1.sid).to.eq(SITE_ID); + expect(object1.output).to.eq('rtb'); + expect(object1.prebid).to.eq('true'); + }); + + it('should populate height, width, c1, c20, coppa with 2 imp', () => { + sandbox.stub(config, 'getConfig') + .withArgs('coppa') + .returns(true); + + const requests = spec.buildRequests(bidRequests, {}); + expect(requests.length).to.equal(1); + + const serverRequestObject = requests[0]; + expect(serverRequestObject.data.imp.length).to.equal(2); + expect(serverRequestObject.data.regs.coppa).to.be.a('number'); + expect(serverRequestObject.data.regs.coppa).to.eq(1); + + const imp1 = serverRequestObject.data.imp[0]; + expect(imp1.video.w).to.be.a('number'); + expect(imp1.video.w).to.eq(300); + expect(imp1.video.h).to.be.a('number'); + expect(imp1.video.h).to.eq(250); + expect(imp1.video.ext.lkqdcustomparameters.c1).to.be.a('string'); + expect(imp1.video.ext.lkqdcustomparameters.c1).to.eq('newWindow'); + expect(imp1.video.ext.lkqdcustomparameters.c20).to.be.a('string'); + expect(imp1.video.ext.lkqdcustomparameters.c20).to.eq('lkqdCustom'); + + const imp2 = serverRequestObject.data.imp[1]; + expect(imp2.video.w).to.be.a('number'); + expect(imp2.video.w).to.eq(640); + expect(imp2.video.h).to.be.a('number'); + expect(imp2.video.h).to.eq(480); + expect(imp2.video.ext.lkqdcustomparameters.c1).to.be.a('string'); + expect(imp2.video.ext.lkqdcustomparameters.c1).to.eq('newWindow'); + expect(imp2.video.ext.lkqdcustomparameters.c20).to.be.a('string'); + expect(imp2.video.ext.lkqdcustomparameters.c20).to.eq('lkqdCustom'); + }); + + it('should not populate unspecified parameters', () => { + const requests = spec.buildRequests(bidRequests); + + const serverRequestObject = requests[0]; + expect(serverRequestObject.data.device.dnt).to.be.a('undefined'); + expect(serverRequestObject.data.content).to.be.a('undefined'); + expect(serverRequestObject.data.regs.coppa).to.be.a('undefined'); + expect(serverRequestObject.data.source).to.be.a('undefined'); + + const imp1 = serverRequestObject.data.imp[0]; + expect(imp1.video.ext.lkqdcustomparameters.c10).to.be.a('undefined'); + + const imp2 = serverRequestObject.data.imp[1]; + expect(imp2.video.ext.lkqdcustomparameters.c39).to.be.a('undefined'); + }); + + it('should handle single size request', () => { + const requests = spec.buildRequests(bidRequest, {}); + const serverRequestObject = requests[0]; + expect(serverRequestObject.data.imp.length).to.equal(1); + expect(serverRequestObject.data.source).to.not.be.a('undefined'); + expect(serverRequestObject.data.source.ext).to.not.be.a('undefined'); + expect(serverRequestObject.data.source.ext.schain).to.not.be.a('undefined'); + + const imp1 = serverRequestObject.data.imp[0]; + expect(imp1.video.w).to.be.a('number'); + expect(imp1.video.w).to.eq(640); + expect(imp1.video.h).to.be.a('number'); + expect(imp1.video.h).to.eq(480); + + const schain = serverRequestObject.data.source.ext.schain; + expect(schain.validation).to.have.string('strict'); + expect(schain.config.ver).to.have.string('1.0'); + expect(schain.config.complete).to.eq(1); + expect(schain.config.nodes[0].asi).to.have.string('exchange1.com'); + expect(schain.config.nodes[0].sid).to.have.string('1234!abcd'); + expect(schain.config.nodes[0].hp).to.eq(1); + expect(schain.config.nodes[0].rid).to.have.string('bid-request-1'); + expect(schain.config.nodes[0].name).to.have.string('publisher, Inc.'); + expect(schain.config.nodes[0].domain).to.have.string('publisher.com'); + }); + }); + + context('interpretResponse', () => { + const bidRequest = { + 'method': 'POST', + 'url': `https://rtb.lkqd.net/ad?pid=${PUBLISHER_ID}&sid=${SITE_ID}&output=rtb&prebid=true`, + 'id': '5511262729333416592', + 'imp': [{ + 'id': '5511262729333416592', + 'displaymanager': 'LKQD SDK', + 'video': { + 'mimes': ['application/x-mpegURL', 'video/mp4', 'video/H264'], + 'protocols': [1, 2, 3, 4, 5, 6, 7, 8], + 'w': 1920, + 'h': 1080, + 'startdelay': 0, + 'placement': 1, + 'playbackmethod': [1], + 'ext': { + 'lkqdcustomparameters': { + 'custom1': 'cus1', + 'custom4': 'cus4', + 'custom7': 'cus7' + } + } + }, + 'bidfloorcur': 'USD', + 'secure': 1 + }], + 'app': { + 'id': '1112444-5742940365329809425', + 'name': 'lkqdappfortesting', + 'bundle': 'com.lkqdbundleid.fortesting', + 'content': { + 'id': '123456', + 'title': 'MyLkqdContent', + 'url': 'https://lkqd.com', + 'len': 600 + } + }, + 'device': { + 'ua': 'Mozilla/5.0 (SMART-TV; Linux; Tizen 4.0) AppleWebKit/538.1 (KHTML, like Gecko) Version/4.0 TV Safari/538.1', + 'geo': { + 'utcoffset': -420, + }, + 'dnt': 0, + 'ip': '184.103.177.205', + 'ifa': 'f4254ada-4174-6cfd-83c7-2f999c457c1d' + }, + 'user': { + 'ext': {} + }, + 'test': 0, + 'at': 2, + 'tmax': 100, + 'cur': ['USD'], + 'source': { + 'ext': { + 'schain': { + 'ver': '1.0', + 'complete': 0, + 'nodes': [{ + 'asi': 'lkqd.net', + 'sid': '604', + 'hp': 1 + }] + } + } + }, + 'regs': { + 'coppa': 1, + 'ext': { + 'gdpr': 0, + 'us_privacy': '1NNN' + } + } + }; + + const serverResponse = { + body: { + 'id': '5511262729333416592', + 'seatbid': [{ + 'bid': [{ + 'id': '281063413403658952', + 'impid': '5511262729333416592', + 'price': 5.409, + 'adm': 'LKQD00:00:15', + 'adomain': ['lkqd.com'], + 'crid': 'lkqd-rtb-79-1030666', + 'protocol': 3, + 'w': 1920, + 'h': 1080, + 'ext': { + 'imptrackers': [] + } + }] + }], + 'cur': 'USD' + } + }; + + it('should correctly parse valid bid response', () => { + const bidResponses = spec.interpretResponse(serverResponse, bidRequest); + expect(bidResponses.length).to.equal(1); + + const bidResponse = bidResponses[0]; + expect(bidResponse.requestId).to.equal('5511262729333416592'); + expect(bidResponse.ad).to.equal(serverResponse.body.seatbid[0].bid[0].adm); + expect(bidResponse.cpm).to.equal(5.409); + expect(bidResponse.width).to.equal(1920); + expect(bidResponse.height).to.equal(1080); + expect(bidResponse.ttl).to.equal(300); + expect(bidResponse.creativeId).to.equal('lkqd-rtb-79-1030666'); + expect(bidResponse.currency).to.equal('USD'); + expect(bidResponse.netRevenue).to.equal(true); + expect(bidResponse.meta.mediaType).to.equal('video'); + }); + + it('safely handles invalid bid response', () => { + let invalidServerResponse = {}; + invalidServerResponse.body = ''; + + let result = spec.interpretResponse(invalidServerResponse, bidRequest); + expect(result.length).to.equal(0); + }); + + it('handles nobid responses', () => { + let nobidResponse = {}; + nobidResponse.body = { + seatbid: [ + { + bid: [] + } + ] + }; + + let result = spec.interpretResponse(nobidResponse, bidRequest); + expect(result.length).to.equal(0); + }); + }); +}); diff --git a/test/spec/modules/lunamediahbBidAdapter_spec.js b/test/spec/modules/lunamediahbBidAdapter_spec.js index e9f88935ed5..181b6e75fe7 100644 --- a/test/spec/modules/lunamediahbBidAdapter_spec.js +++ b/test/spec/modules/lunamediahbBidAdapter_spec.js @@ -80,7 +80,7 @@ describe('LunamediaHBBidAdapter', function () { expect(data).to.be.an('object'); let placement = data['placements'][0]; expect(placement).to.be.an('object'); - expect(placement).to.have.keys('placementId', 'bidId', 'traffic', 'wPlayer', 'hPlayer', 'schain'); + expect(placement).to.have.keys('placementId', 'bidId', 'traffic', 'wPlayer', 'hPlayer', 'schain', 'videoContext'); expect(placement.traffic).to.equal(VIDEO); expect(placement.wPlayer).to.equal(playerSize[0]); expect(placement.hPlayer).to.equal(playerSize[1]); @@ -301,4 +301,30 @@ describe('LunamediaHBBidAdapter', function () { 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://cookie.lmgssp.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://cookie.lmgssp.com/image?pbjs=1&ccpa_consent=1---&coppa=0') + }); + }); }); diff --git a/test/spec/modules/malltvAnalyticsAdapter_spec.js b/test/spec/modules/malltvAnalyticsAdapter_spec.js index 599ac6e4256..c96069df0f9 100644 --- a/test/spec/modules/malltvAnalyticsAdapter_spec.js +++ b/test/spec/modules/malltvAnalyticsAdapter_spec.js @@ -4,7 +4,7 @@ import { } from 'modules/malltvAnalyticsAdapter.js' import { expect } from 'chai' import { getCpmInEur } from '../../../modules/malltvAnalyticsAdapter' -import events from 'src/events' +import * as events from 'src/events' import constants from 'src/constants.json' const auctionId = 'b0b39610-b941-4659-a87c-de9f62d3e13e' diff --git a/test/spec/modules/mass_spec.js b/test/spec/modules/mass_spec.js index a4a87ce113f..3b5d89b0a8c 100644 --- a/test/spec/modules/mass_spec.js +++ b/test/spec/modules/mass_spec.js @@ -60,17 +60,9 @@ const mockedNonMassBids = [ }, ]; -// mock bidder request: -const mockedBidderRequest = { - bidderCode: 'ix', - bidderRequestId: 'bidder-request-id-1' -}; - const noop = function() {}; describe('MASS Module', function() { - let bidderRequest = Object.assign({}, mockedBidderRequest); - it('should be enabled by default', function() { expect(isEnabled).to.equal(true); }); @@ -86,9 +78,7 @@ describe('MASS Module', function() { const originalBid = Object.assign({}, mockedBid); const bid = Object.assign({}, originalBid); - bidderRequest.bids = [bid]; - - addBidResponseHook.call({bidderRequest}, noop, 'ad-code-id', bid); + addBidResponseHook(noop, 'ad-code-id', bid); expect(bid).to.deep.equal(originalBid); }); @@ -100,9 +90,7 @@ describe('MASS Module', function() { const originalBid = Object.assign({}, mockedBid); const bid = Object.assign({}, originalBid); - bidderRequest.bids = [bid]; - - addBidResponseHook.call({bidderRequest}, noop, 'ad-code-id', bid); + addBidResponseHook(noop, 'ad-code-id', bid); expect(bid.ad).to.not.equal(originalBid.ad); diff --git a/test/spec/modules/mediafuseBidAdapter_spec.js b/test/spec/modules/mediafuseBidAdapter_spec.js new file mode 100644 index 00000000000..f4defa293e1 --- /dev/null +++ b/test/spec/modules/mediafuseBidAdapter_spec.js @@ -0,0 +1,1442 @@ +import { expect } from 'chai'; +import { spec } from 'modules/mediafuseBidAdapter.js'; +import { newBidder } from 'src/adapters/bidderFactory.js'; +import * as bidderFactory from 'src/adapters/bidderFactory.js'; +import { auctionManager } from 'src/auctionManager.js'; +import { deepClone } from 'src/utils.js'; +import { config } from 'src/config.js'; + +const ENDPOINT = 'https://ib.adnxs.com/ut/v3/prebid'; + +describe('MediaFuseAdapter', 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 () { + let bid = { + 'bidder': 'mediafuse', + 'params': { + 'placementId': '10433394' + }, + '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 required params found', function () { + let bid = Object.assign({}, bid); + delete bid.params; + bid.params = { + 'member': '1234', + 'invCode': 'ABCD' + }; + + expect(spec.isBidRequestValid(bid)).to.equal(true); + }); + + it('should return false when required params are not passed', function () { + let bid = Object.assign({}, bid); + delete bid.params; + bid.params = { + 'placementId': 0 + }; + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + }); + + describe('buildRequests', function () { + let getAdUnitsStub; + let bidRequests = [ + { + 'bidder': 'mediafuse', + 'params': { + 'placementId': '10433394' + }, + 'adUnitCode': 'adunit-code', + 'sizes': [[300, 250], [300, 600]], + 'bidId': '30b31c1838de1e', + 'bidderRequestId': '22edbae2733bf6', + 'auctionId': '1d1a030790a475', + 'transactionId': '04f2659e-c005-4eb1-a57c-fa93145e3843' + } + ]; + + beforeEach(function() { + getAdUnitsStub = sinon.stub(auctionManager, 'getAdUnits').callsFake(function() { + return []; + }); + }); + + afterEach(function() { + getAdUnitsStub.restore(); + }); + + it('should parse out private sizes', function () { + let bidRequest = Object.assign({}, + bidRequests[0], + { + params: { + placementId: '10433394', + privateSizes: [300, 250] + } + } + ); + + const request = spec.buildRequests([bidRequest]); + const payload = JSON.parse(request.data); + + expect(payload.tags[0].private_sizes).to.exist; + expect(payload.tags[0].private_sizes).to.deep.equal([{width: 300, height: 250}]); + }); + + it('should add publisher_id in request', function() { + let bidRequest = Object.assign({}, + bidRequests[0], + { + params: { + placementId: '10433394', + publisherId: '1231234' + } + }); + const request = spec.buildRequests([bidRequest]); + const payload = JSON.parse(request.data); + + expect(payload.tags[0].publisher_id).to.exist; + expect(payload.tags[0].publisher_id).to.deep.equal(1231234); + expect(payload.publisher_id).to.exist; + expect(payload.publisher_id).to.deep.equal(1231234); + }) + + it('should add source and verison to the tag', function () { + const request = spec.buildRequests(bidRequests); + const payload = JSON.parse(request.data); + expect(payload.sdk).to.exist; + expect(payload.sdk).to.deep.equal({ + source: 'pbjs', + version: '$prebid.version$' + }); + }); + + it('should populate the ad_types array on all requests', function () { + let adUnits = [{ + code: 'adunit-code', + mediaTypes: { + banner: { + sizes: [[300, 250], [300, 600]] + } + }, + bids: [{ + bidder: 'mediafuse', + params: { + placementId: '10433394' + } + }], + transactionId: '04f2659e-c005-4eb1-a57c-fa93145e3843' + }]; + + ['banner', 'video', 'native'].forEach(type => { + getAdUnitsStub.callsFake(function(...args) { + return adUnits; + }); + + const bidRequest = Object.assign({}, bidRequests[0]); + bidRequest.mediaTypes = {}; + bidRequest.mediaTypes[type] = {}; + + const request = spec.buildRequests([bidRequest]); + const payload = JSON.parse(request.data); + + expect(payload.tags[0].ad_types).to.deep.equal([type]); + + if (type === 'banner') { + delete adUnits[0].mediaTypes; + } + }); + }); + + it('should not populate the ad_types array when adUnit.mediaTypes is undefined', function() { + const bidRequest = Object.assign({}, bidRequests[0]); + const request = spec.buildRequests([bidRequest]); + const payload = JSON.parse(request.data); + + expect(payload.tags[0].ad_types).to.not.exist; + }); + + it('should populate the ad_types array on outstream requests', function () { + const bidRequest = Object.assign({}, bidRequests[0]); + bidRequest.mediaTypes = {}; + bidRequest.mediaTypes.video = {context: 'outstream'}; + + const request = spec.buildRequests([bidRequest]); + const payload = JSON.parse(request.data); + + expect(payload.tags[0].ad_types).to.deep.equal(['video']); + expect(payload.tags[0].hb_source).to.deep.equal(1); + }); + + it('sends bid request to ENDPOINT via POST', function () { + const request = spec.buildRequests(bidRequests); + expect(request.url).to.equal(ENDPOINT); + expect(request.method).to.equal('POST'); + }); + + it('should attach valid video params to the tag', function () { + let bidRequest = Object.assign({}, + bidRequests[0], + { + params: { + placementId: '10433394', + video: { + id: 123, + minduration: 100, + foobar: 'invalid' + } + } + } + ); + + const request = spec.buildRequests([bidRequest]); + const payload = JSON.parse(request.data); + expect(payload.tags[0].video).to.deep.equal({ + id: 123, + minduration: 100 + }); + expect(payload.tags[0].hb_source).to.deep.equal(1); + }); + + it('should include ORTB video values when video params were not set', function() { + let bidRequest = deepClone(bidRequests[0]); + bidRequest.params = { + placementId: '1234235', + video: { + skippable: true, + playback_method: ['auto_play_sound_off', 'auto_play_sound_unknown'], + context: 'outstream' + } + }; + bidRequest.mediaTypes = { + video: { + playerSize: [640, 480], + context: 'outstream', + mimes: ['video/mp4'], + skip: 0, + minduration: 5, + api: [1, 5, 6], + playbackmethod: [2, 4] + } + }; + + const request = spec.buildRequests([bidRequest]); + const payload = JSON.parse(request.data); + + expect(payload.tags[0].video).to.deep.equal({ + minduration: 5, + playback_method: 2, + skippable: true, + context: 4 + }); + expect(payload.tags[0].video_frameworks).to.deep.equal([1, 4]) + }); + + it('should add video property when adUnit includes a renderer', function () { + const videoData = { + mediaTypes: { + video: { + context: 'outstream', + mimes: ['video/mp4'] + } + }, + params: { + placementId: '10433394', + video: { + skippable: true, + playback_method: ['auto_play_sound_off'] + } + } + }; + + let bidRequest1 = deepClone(bidRequests[0]); + bidRequest1 = Object.assign({}, bidRequest1, videoData, { + renderer: { + url: 'https://test.renderer.url', + render: function () {} + } + }); + + let bidRequest2 = deepClone(bidRequests[0]); + bidRequest2.adUnitCode = 'adUnit_code_2'; + bidRequest2 = Object.assign({}, bidRequest2, videoData); + + const request = spec.buildRequests([bidRequest1, bidRequest2]); + const payload = JSON.parse(request.data); + expect(payload.tags[0].video).to.deep.equal({ + skippable: true, + playback_method: 2, + custom_renderer_present: true + }); + expect(payload.tags[1].video).to.deep.equal({ + skippable: true, + playback_method: 2 + }); + }); + + it('should attach valid user params to the tag', function () { + let bidRequest = Object.assign({}, + bidRequests[0], + { + params: { + placementId: '10433394', + user: { + externalUid: '123', + segments: [123, { id: 987, value: 876 }], + foobar: 'invalid' + } + } + } + ); + + const request = spec.buildRequests([bidRequest]); + const payload = JSON.parse(request.data); + + expect(payload.user).to.exist; + expect(payload.user).to.deep.equal({ + external_uid: '123', + segments: [{id: 123}, {id: 987, value: 876}] + }); + }); + + it('should attach reserve param when either bid param or getFloor function exists', function () { + let getFloorResponse = { currency: 'USD', floor: 3 }; + let request, payload = null; + let bidRequest = deepClone(bidRequests[0]); + + // 1 -> reserve not defined, getFloor not defined > empty + request = spec.buildRequests([bidRequest]); + payload = JSON.parse(request.data); + + expect(payload.tags[0].reserve).to.not.exist; + + // 2 -> reserve is defined, getFloor not defined > reserve is used + bidRequest.params = { + 'placementId': '10433394', + 'reserve': 0.5 + }; + request = spec.buildRequests([bidRequest]); + payload = JSON.parse(request.data); + + expect(payload.tags[0].reserve).to.exist.and.to.equal(0.5); + + // 3 -> reserve is defined, getFloor is defined > getFloor is used + bidRequest.getFloor = () => getFloorResponse; + + request = spec.buildRequests([bidRequest]); + payload = JSON.parse(request.data); + + expect(payload.tags[0].reserve).to.exist.and.to.equal(3); + }); + + it('should duplicate adpod placements into batches and set correct maxduration', function() { + let bidRequest = Object.assign({}, + bidRequests[0], + { + params: { placementId: '14542875' } + }, + { + mediaTypes: { + video: { + context: 'adpod', + playerSize: [640, 480], + adPodDurationSec: 300, + durationRangeSec: [15, 30], + } + } + } + ); + + const request = spec.buildRequests([bidRequest]); + const payload1 = JSON.parse(request[0].data); + const payload2 = JSON.parse(request[1].data); + + // 300 / 15 = 20 total + expect(payload1.tags.length).to.equal(15); + expect(payload2.tags.length).to.equal(5); + + expect(payload1.tags[0]).to.deep.equal(payload1.tags[1]); + expect(payload1.tags[0].video.maxduration).to.equal(30); + + expect(payload2.tags[0]).to.deep.equal(payload1.tags[1]); + expect(payload2.tags[0].video.maxduration).to.equal(30); + }); + + it('should round down adpod placements when numbers are uneven', function() { + let bidRequest = Object.assign({}, + bidRequests[0], + { + params: { placementId: '14542875' } + }, + { + mediaTypes: { + video: { + context: 'adpod', + playerSize: [640, 480], + adPodDurationSec: 123, + durationRangeSec: [45], + } + } + } + ); + + const request = spec.buildRequests([bidRequest]); + const payload = JSON.parse(request.data); + expect(payload.tags.length).to.equal(2); + }); + + it('should duplicate adpod placements when requireExactDuration is set', function() { + let bidRequest = Object.assign({}, + bidRequests[0], + { + params: { placementId: '14542875' } + }, + { + mediaTypes: { + video: { + context: 'adpod', + playerSize: [640, 480], + adPodDurationSec: 300, + durationRangeSec: [15, 30], + requireExactDuration: true, + } + } + } + ); + + // 20 total placements with 15 max impressions = 2 requests + const request = spec.buildRequests([bidRequest]); + expect(request.length).to.equal(2); + + // 20 spread over 2 requests = 15 in first request, 5 in second + const payload1 = JSON.parse(request[0].data); + const payload2 = JSON.parse(request[1].data); + expect(payload1.tags.length).to.equal(15); + expect(payload2.tags.length).to.equal(5); + + // 10 placements should have max/min at 15 + // 10 placemenst should have max/min at 30 + const payload1tagsWith15 = payload1.tags.filter(tag => tag.video.maxduration === 15); + const payload1tagsWith30 = payload1.tags.filter(tag => tag.video.maxduration === 30); + expect(payload1tagsWith15.length).to.equal(10); + expect(payload1tagsWith30.length).to.equal(5); + + // 5 placemenst with min/max at 30 were in the first request + // so 5 remaining should be in the second + const payload2tagsWith30 = payload2.tags.filter(tag => tag.video.maxduration === 30); + expect(payload2tagsWith30.length).to.equal(5); + }); + + it('should set durations for placements when requireExactDuration is set and numbers are uneven', function() { + let bidRequest = Object.assign({}, + bidRequests[0], + { + params: { placementId: '14542875' } + }, + { + mediaTypes: { + video: { + context: 'adpod', + playerSize: [640, 480], + adPodDurationSec: 105, + durationRangeSec: [15, 30, 60], + requireExactDuration: true, + } + } + } + ); + + const request = spec.buildRequests([bidRequest]); + const payload = JSON.parse(request.data); + expect(payload.tags.length).to.equal(7); + + const tagsWith15 = payload.tags.filter(tag => tag.video.maxduration === 15); + const tagsWith30 = payload.tags.filter(tag => tag.video.maxduration === 30); + const tagsWith60 = payload.tags.filter(tag => tag.video.maxduration === 60); + expect(tagsWith15.length).to.equal(3); + expect(tagsWith30.length).to.equal(3); + expect(tagsWith60.length).to.equal(1); + }); + + it('should break adpod request into batches', function() { + let bidRequest = Object.assign({}, + bidRequests[0], + { + params: { placementId: '14542875' } + }, + { + mediaTypes: { + video: { + context: 'adpod', + playerSize: [640, 480], + adPodDurationSec: 225, + durationRangeSec: [5], + } + } + } + ); + + const request = spec.buildRequests([bidRequest]); + const payload1 = JSON.parse(request[0].data); + const payload2 = JSON.parse(request[1].data); + const payload3 = JSON.parse(request[2].data); + + expect(payload1.tags.length).to.equal(15); + expect(payload2.tags.length).to.equal(15); + expect(payload3.tags.length).to.equal(15); + }); + + it('should contain hb_source value for adpod', function() { + let bidRequest = Object.assign({}, + bidRequests[0], + { + params: { placementId: '14542875' } + }, + { + mediaTypes: { + video: { + context: 'adpod', + playerSize: [640, 480], + adPodDurationSec: 300, + durationRangeSec: [15, 30], + } + } + } + ); + const request = spec.buildRequests([bidRequest])[0]; + const payload = JSON.parse(request.data); + expect(payload.tags[0].hb_source).to.deep.equal(7); + }); + + it('should contain hb_source value for other media', function() { + let bidRequest = Object.assign({}, + bidRequests[0], + { + mediaType: 'banner', + params: { + sizes: [[300, 250], [300, 600]], + placementId: 13144370 + } + } + ); + const request = spec.buildRequests([bidRequest]); + const payload = JSON.parse(request.data); + expect(payload.tags[0].hb_source).to.deep.equal(1); + }); + + it('adds brand_category_exclusion to request when set', function() { + let bidRequest = Object.assign({}, bidRequests[0]); + sinon + .stub(config, 'getConfig') + .withArgs('adpod.brandCategoryExclusion') + .returns(true); + + const request = spec.buildRequests([bidRequest]); + const payload = JSON.parse(request.data); + + expect(payload.brand_category_uniqueness).to.equal(true); + + config.getConfig.restore(); + }); + + it('adds auction level keywords to request when set', function() { + let bidRequest = Object.assign({}, bidRequests[0]); + sinon + .stub(config, 'getConfig') + .withArgs('mediafuseAuctionKeywords') + .returns({ + gender: 'm', + music: ['rock', 'pop'], + test: '' + }); + + const request = spec.buildRequests([bidRequest]); + const payload = JSON.parse(request.data); + + expect(payload.keywords).to.deep.equal([{ + 'key': 'gender', + 'value': ['m'] + }, { + 'key': 'music', + 'value': ['rock', 'pop'] + }, { + 'key': 'test' + }]); + + config.getConfig.restore(); + }); + + it('should attach native params to the request', function () { + let bidRequest = Object.assign({}, + bidRequests[0], + { + mediaType: 'native', + nativeParams: { + title: {required: true}, + body: {required: true}, + body2: {required: true}, + image: {required: true, sizes: [100, 100]}, + icon: {required: true}, + cta: {required: false}, + rating: {required: true}, + sponsoredBy: {required: true}, + privacyLink: {required: true}, + displayUrl: {required: true}, + address: {required: true}, + downloads: {required: true}, + likes: {required: true}, + phone: {required: true}, + price: {required: true}, + salePrice: {required: true} + } + } + ); + + const request = spec.buildRequests([bidRequest]); + const payload = JSON.parse(request.data); + + expect(payload.tags[0].native.layouts[0]).to.deep.equal({ + title: {required: true}, + description: {required: true}, + desc2: {required: true}, + main_image: {required: true, sizes: [{ width: 100, height: 100 }]}, + icon: {required: true}, + ctatext: {required: false}, + rating: {required: true}, + sponsored_by: {required: true}, + privacy_link: {required: true}, + displayurl: {required: true}, + address: {required: true}, + downloads: {required: true}, + likes: {required: true}, + phone: {required: true}, + price: {required: true}, + saleprice: {required: true}, + privacy_supported: true + }); + expect(payload.tags[0].hb_source).to.equal(1); + }); + + it('should always populated tags[].sizes with 1,1 for native if otherwise not defined', function () { + let bidRequest = Object.assign({}, + bidRequests[0], + { + mediaType: 'native', + nativeParams: { + image: { required: true } + } + } + ); + bidRequest.sizes = [[150, 100], [300, 250]]; + + let request = spec.buildRequests([bidRequest]); + let payload = JSON.parse(request.data); + expect(payload.tags[0].sizes).to.deep.equal([{width: 150, height: 100}, {width: 300, height: 250}]); + + delete bidRequest.sizes; + + request = spec.buildRequests([bidRequest]); + payload = JSON.parse(request.data); + + expect(payload.tags[0].sizes).to.deep.equal([{width: 1, height: 1}]); + }); + + it('should convert keyword params to proper form and attaches to request', function () { + let bidRequest = Object.assign({}, + bidRequests[0], + { + params: { + placementId: '10433394', + keywords: { + single: 'val', + singleArr: ['val'], + singleArrNum: [5], + multiValMixed: ['value1', 2, 'value3'], + singleValNum: 123, + emptyStr: '', + emptyArr: [''], + badValue: {'foo': 'bar'} // should be dropped + } + } + } + ); + + const request = spec.buildRequests([bidRequest]); + const payload = JSON.parse(request.data); + + expect(payload.tags[0].keywords).to.deep.equal([{ + 'key': 'single', + 'value': ['val'] + }, { + 'key': 'singleArr', + 'value': ['val'] + }, { + 'key': 'singleArrNum', + 'value': ['5'] + }, { + 'key': 'multiValMixed', + 'value': ['value1', '2', 'value3'] + }, { + 'key': 'singleValNum', + 'value': ['123'] + }, { + 'key': 'emptyStr' + }, { + 'key': 'emptyArr' + }]); + }); + + it('should add payment rules to the request', function () { + let bidRequest = Object.assign({}, + bidRequests[0], + { + params: { + placementId: '10433394', + usePaymentRule: true + } + } + ); + + const request = spec.buildRequests([bidRequest]); + const payload = JSON.parse(request.data); + + expect(payload.tags[0].use_pmt_rule).to.equal(true); + }); + + it('should add gpid to the request', function () { + let testGpid = '/12345/my-gpt-tag-0'; + let bidRequest = deepClone(bidRequests[0]); + bidRequest.ortb2Imp = { ext: { data: { pbadslot: 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 gdpr consent information to the request', function () { + let consentString = 'BOJ8RZsOJ8RZsABAB8AAAAAZ+A=='; + let bidderRequest = { + 'bidderCode': 'mediafuse', + 'auctionId': '1d1a030790a475', + 'bidderRequestId': '22edbae2733bf6', + 'timeout': 3000, + 'gdprConsent': { + consentString: consentString, + gdprApplies: true, + addtlConsent: '1~7.12.35.62.66.70.89.93.108' + } + }; + bidderRequest.bids = bidRequests; + + const request = spec.buildRequests(bidRequests, bidderRequest); + expect(request.options).to.deep.equal({withCredentials: true}); + const payload = JSON.parse(request.data); + + expect(payload.gdpr_consent).to.exist; + expect(payload.gdpr_consent.consent_string).to.exist.and.to.equal(consentString); + expect(payload.gdpr_consent.consent_required).to.exist.and.to.be.true; + expect(payload.gdpr_consent.addtl_consent).to.exist.and.to.deep.equal([7, 12, 35, 62, 66, 70, 89, 93, 108]); + }); + + it('should add us privacy string to payload', function() { + let consentString = '1YA-'; + let bidderRequest = { + 'bidderCode': 'mediafuse', + 'auctionId': '1d1a030790a475', + 'bidderRequestId': '22edbae2733bf6', + 'timeout': 3000, + 'uspConsent': consentString + }; + bidderRequest.bids = bidRequests; + + const request = spec.buildRequests(bidRequests, bidderRequest); + const payload = JSON.parse(request.data); + + expect(payload.us_privacy).to.exist; + expect(payload.us_privacy).to.exist.and.to.equal(consentString); + }); + + it('supports sending hybrid mobile app parameters', function () { + let appRequest = Object.assign({}, + bidRequests[0], + { + params: { + placementId: '10433394', + app: { + id: 'B1O2W3M4AN.com.prebid.webview', + geo: { + lat: 40.0964439, + lng: -75.3009142 + }, + device_id: { + idfa: '4D12078D-3246-4DA4-AD5E-7610481E7AE', // Apple advertising identifier + aaid: '38400000-8cf0-11bd-b23e-10b96e40000d', // Android advertising identifier + md5udid: '5756ae9022b2ea1e47d84fead75220c8', // MD5 hash of the ANDROID_ID + sha1udid: '4DFAA92388699AC6539885AEF1719293879985BF', // SHA1 hash of the ANDROID_ID + windowsadid: '750c6be243f1c4b5c9912b95a5742fc5' // Windows advertising identifier + } + } + } + } + ); + const request = spec.buildRequests([appRequest]); + const payload = JSON.parse(request.data); + expect(payload.app).to.exist; + expect(payload.app).to.deep.equal({ + appid: 'B1O2W3M4AN.com.prebid.webview' + }); + expect(payload.device.device_id).to.exist; + expect(payload.device.device_id).to.deep.equal({ + aaid: '38400000-8cf0-11bd-b23e-10b96e40000d', + idfa: '4D12078D-3246-4DA4-AD5E-7610481E7AE', + md5udid: '5756ae9022b2ea1e47d84fead75220c8', + sha1udid: '4DFAA92388699AC6539885AEF1719293879985BF', + windowsadid: '750c6be243f1c4b5c9912b95a5742fc5' + }); + expect(payload.device.geo).to.exist; + expect(payload.device.geo).to.deep.equal({ + lat: 40.0964439, + lng: -75.3009142 + }); + }); + + it('should add referer info to payload', function () { + const bidRequest = Object.assign({}, bidRequests[0]) + const bidderRequest = { + refererInfo: { + referer: 'https://example.com/page.html', + reachedTop: true, + numIframes: 2, + stack: [ + 'https://example.com/page.html', + 'https://example.com/iframe1.html', + 'https://example.com/iframe2.html' + ] + } + } + const request = spec.buildRequests([bidRequest], bidderRequest); + const payload = JSON.parse(request.data); + + expect(payload.referrer_detection).to.exist; + expect(payload.referrer_detection).to.deep.equal({ + rd_ref: 'https%3A%2F%2Fexample.com%2Fpage.html', + rd_top: true, + rd_ifs: 2, + rd_stk: bidderRequest.refererInfo.stack.map((url) => encodeURIComponent(url)).join(',') + }); + }); + + it('should populate schain if available', function () { + const bidRequest = Object.assign({}, bidRequests[0], { + schain: { + ver: '1.0', + complete: 1, + nodes: [ + { + 'asi': 'blob.com', + 'sid': '001', + 'hp': 1 + } + ] + } + }); + + const request = spec.buildRequests([bidRequest]); + const payload = JSON.parse(request.data); + expect(payload.schain).to.deep.equal({ + ver: '1.0', + complete: 1, + nodes: [ + { + 'asi': 'blob.com', + 'sid': '001', + 'hp': 1 + } + ] + }); + }); + + it('should populate coppa if set in config', function () { + let bidRequest = Object.assign({}, bidRequests[0]); + sinon.stub(config, 'getConfig') + .withArgs('coppa') + .returns(true); + + const request = spec.buildRequests([bidRequest]); + const payload = JSON.parse(request.data); + + expect(payload.user.coppa).to.equal(true); + + config.getConfig.restore(); + }); + + it('should set the X-Is-Test customHeader if test flag is enabled', function () { + let bidRequest = Object.assign({}, bidRequests[0]); + sinon.stub(config, 'getConfig') + .withArgs('apn_test') + .returns(true); + + const request = spec.buildRequests([bidRequest]); + expect(request.options.customHeaders).to.deep.equal({'X-Is-Test': 1}); + + config.getConfig.restore(); + }); + + it('should always set withCredentials: true on the request.options', function () { + let bidRequest = Object.assign({}, bidRequests[0]); + const request = spec.buildRequests([bidRequest]); + expect(request.options.withCredentials).to.equal(true); + }); + + it('should set simple domain variant if purpose 1 consent is not given', function () { + let consentString = 'BOJ8RZsOJ8RZsABAB8AAAAAZ+A=='; + let bidderRequest = { + 'bidderCode': 'mediafuse', + 'auctionId': '1d1a030790a475', + 'bidderRequestId': '22edbae2733bf6', + 'timeout': 3000, + 'gdprConsent': { + consentString: consentString, + gdprApplies: true, + apiVersion: 2, + vendorData: { + purpose: { + consents: { + 1: false + } + } + } + } + }; + bidderRequest.bids = bidRequests; + + const request = spec.buildRequests(bidRequests, bidderRequest); + expect(request.url).to.equal('https://ib.adnxs-simple.com/ut/v3/prebid'); + }); + + it('should populate eids when supported userIds are available', function () { + const bidRequest = Object.assign({}, bidRequests[0], { + userId: { + tdid: 'sample-userid', + uid2: { id: 'sample-uid2-value' }, + criteoId: 'sample-criteo-userid', + netId: 'sample-netId-userid', + idl_env: 'sample-idl-userid', + flocId: { + id: 'sample-flocid-value', + version: 'chrome.1.0' + } + } + }); + + const request = spec.buildRequests([bidRequest]); + const payload = JSON.parse(request.data); + expect(payload.eids).to.deep.include({ + source: 'adserver.org', + id: 'sample-userid', + rti_partner: 'TDID' + }); + + expect(payload.eids).to.deep.include({ + source: 'criteo.com', + id: 'sample-criteo-userid', + }); + + expect(payload.eids).to.deep.include({ + source: 'chrome.com', + id: 'sample-flocid-value' + }); + + expect(payload.eids).to.deep.include({ + source: 'netid.de', + id: 'sample-netId-userid', + }); + + expect(payload.eids).to.deep.include({ + source: 'liveramp.com', + id: 'sample-idl-userid' + }); + + expect(payload.eids).to.deep.include({ + source: 'uidapi.com', + id: 'sample-uid2-value', + rti_partner: 'UID2' + }); + }); + + it('should populate iab_support object at the root level if omid support is detected', function () { + // with bid.params.frameworks + let bidRequest_A = Object.assign({}, bidRequests[0], { + params: { + frameworks: [1, 2, 5, 6], + video: { + frameworks: [1, 2, 5, 6] + } + } + }); + let request = spec.buildRequests([bidRequest_A]); + let payload = JSON.parse(request.data); + expect(payload.iab_support).to.be.an('object'); + expect(payload.iab_support).to.deep.equal({ + omidpn: 'Mediafuse', + omidpv: '$prebid.version$' + }); + expect(payload.tags[0].banner_frameworks).to.be.an('array'); + expect(payload.tags[0].banner_frameworks).to.deep.equal([1, 2, 5, 6]); + expect(payload.tags[0].video_frameworks).to.be.an('array'); + expect(payload.tags[0].video_frameworks).to.deep.equal([1, 2, 5, 6]); + expect(payload.tags[0].video.frameworks).to.not.exist; + + // without bid.params.frameworks + const bidRequest_B = Object.assign({}, bidRequests[0]); + request = spec.buildRequests([bidRequest_B]); + payload = JSON.parse(request.data); + expect(payload.iab_support).to.not.exist; + expect(payload.tags[0].banner_frameworks).to.not.exist; + expect(payload.tags[0].video_frameworks).to.not.exist; + + // with video.frameworks but it is not an array + const bidRequest_C = Object.assign({}, bidRequests[0], { + params: { + video: { + frameworks: "'1', '2', '3', '6'" + } + } + }); + request = spec.buildRequests([bidRequest_C]); + payload = JSON.parse(request.data); + expect(payload.iab_support).to.not.exist; + expect(payload.tags[0].banner_frameworks).to.not.exist; + expect(payload.tags[0].video_frameworks).to.not.exist; + }); + }) + + describe('interpretResponse', function () { + let bfStub; + let bidderSettingsStorage; + + before(function() { + bfStub = sinon.stub(bidderFactory, 'getIabSubCategory'); + bidderSettingsStorage = $$PREBID_GLOBAL$$.bidderSettings; + }); + + after(function() { + bfStub.restore(); + $$PREBID_GLOBAL$$.bidderSettings = bidderSettingsStorage; + }); + + let response = { + 'version': '3.0.0', + 'tags': [ + { + 'uuid': '3db3773286ee59', + 'tag_id': 10433394, + 'auction_id': '4534722592064951574', + 'nobid': false, + 'no_ad_url': 'https://lax1-ib.adnxs.com/no-ad', + 'timeout_ms': 10000, + 'ad_profile_id': 27079, + 'ads': [ + { + 'content_source': 'rtb', + 'ad_type': 'banner', + 'buyer_member_id': 958, + 'creative_id': 29681110, + 'media_type_id': 1, + 'media_subtype_id': 1, + 'cpm': 0.5, + 'cpm_publisher_currency': 0.5, + 'publisher_currency_code': '$', + 'client_initiated_ad_counting': true, + 'viewability': { + 'config': '' + }, + 'rtb': { + 'banner': { + 'content': '', + 'width': 300, + 'height': 250 + }, + 'trackers': [ + { + 'impression_urls': [ + 'https://lax1-ib.adnxs.com/impression', + 'https://www.test.com/tracker' + ], + 'video_events': {} + } + ] + } + } + ] + } + ] + }; + + it('should get correct bid response', function () { + let expectedResponse = [ + { + 'requestId': '3db3773286ee59', + 'cpm': 0.5, + 'creativeId': 29681110, + 'dealId': undefined, + 'width': 300, + 'height': 250, + 'ad': '', + 'mediaType': 'banner', + 'currency': 'USD', + 'ttl': 300, + 'netRevenue': true, + 'adUnitCode': 'code', + 'mediafuse': { + 'buyerMemberId': 958 + }, + 'meta': { + 'dchain': { + 'ver': '1.0', + 'complete': 0, + 'nodes': [{ + 'bsid': '958' + }] + } + } + } + ]; + let bidderRequest = { + bids: [{ + bidId: '3db3773286ee59', + adUnitCode: 'code' + }] + }; + let result = spec.interpretResponse({ body: response }, {bidderRequest}); + expect(Object.keys(result[0])).to.have.members(Object.keys(expectedResponse[0])); + }); + + it('should reject 0 cpm bids', function () { + let zeroCpmResponse = deepClone(response); + zeroCpmResponse.tags[0].ads[0].cpm = 0; + + let bidderRequest = { + bidderCode: 'mediafuse' + }; + + let result = spec.interpretResponse({ body: zeroCpmResponse }, { bidderRequest }); + expect(result.length).to.equal(0); + }); + + it('should allow 0 cpm bids if allowZeroCpmBids setConfig is true', function () { + $$PREBID_GLOBAL$$.bidderSettings = { + mediafuse: { + allowZeroCpmBids: true + } + }; + + let zeroCpmResponse = deepClone(response); + zeroCpmResponse.tags[0].ads[0].cpm = 0; + + let bidderRequest = { + bidderCode: 'mediafuse', + bids: [{ + bidId: '3db3773286ee59', + adUnitCode: 'code' + }] + }; + + let result = spec.interpretResponse({ body: zeroCpmResponse }, { bidderRequest }); + expect(result.length).to.equal(1); + expect(result[0].cpm).to.equal(0); + }); + + it('handles nobid responses', function () { + let response = { + 'version': '0.0.1', + 'tags': [{ + 'uuid': '84ab500420319d', + 'tag_id': 5976557, + 'auction_id': '297492697822162468', + 'nobid': true + }] + }; + let bidderRequest; + + let result = spec.interpretResponse({ body: response }, {bidderRequest}); + expect(result.length).to.equal(0); + }); + + it('handles outstream video responses', function () { + let response = { + 'tags': [{ + 'uuid': '84ab500420319d', + 'ads': [{ + 'ad_type': 'video', + 'cpm': 0.500000, + 'notify_url': 'imptracker.com', + 'rtb': { + 'video': { + 'content': '' + } + }, + 'javascriptTrackers': '' + }] + }] + }; + let bidderRequest = { + bids: [{ + bidId: '84ab500420319d', + adUnitCode: 'code', + mediaTypes: { + video: { + context: 'outstream' + } + } + }] + } + + let result = spec.interpretResponse({ body: response }, {bidderRequest}); + expect(result[0]).to.have.property('vastXml'); + expect(result[0]).to.have.property('vastImpUrl'); + expect(result[0]).to.have.property('mediaType', 'video'); + }); + + it('handles instream video responses', function () { + let response = { + 'tags': [{ + 'uuid': '84ab500420319d', + 'ads': [{ + 'ad_type': 'video', + 'cpm': 0.500000, + 'notify_url': 'imptracker.com', + 'rtb': { + 'video': { + 'asset_url': 'https://sample.vastURL.com/here/vid' + } + }, + 'javascriptTrackers': '' + }] + }] + }; + let bidderRequest = { + bids: [{ + bidId: '84ab500420319d', + adUnitCode: 'code', + mediaTypes: { + video: { + context: 'instream' + } + } + }] + } + + let result = spec.interpretResponse({ body: response }, {bidderRequest}); + expect(result[0]).to.have.property('vastUrl'); + expect(result[0]).to.have.property('vastImpUrl'); + expect(result[0]).to.have.property('mediaType', 'video'); + }); + + it('handles adpod responses', function () { + let response = { + 'tags': [{ + 'uuid': '84ab500420319d', + 'ads': [{ + 'ad_type': 'video', + 'brand_category_id': 10, + 'cpm': 0.500000, + 'notify_url': 'imptracker.com', + 'rtb': { + 'video': { + 'asset_url': 'https://sample.vastURL.com/here/adpod', + 'duration_ms': 30000, + } + }, + 'viewability': { + 'config': '' + } + }] + }] + }; + + let bidderRequest = { + bids: [{ + bidId: '84ab500420319d', + adUnitCode: 'code', + mediaTypes: { + video: { + context: 'adpod' + } + } + }] + }; + bfStub.returns('1'); + + let result = spec.interpretResponse({ body: response }, {bidderRequest}); + expect(result[0]).to.have.property('vastUrl'); + expect(result[0].video.context).to.equal('adpod'); + expect(result[0].video.durationSeconds).to.equal(30); + }); + + it('handles native responses', function () { + let response1 = deepClone(response); + response1.tags[0].ads[0].ad_type = 'native'; + response1.tags[0].ads[0].rtb.native = { + 'title': 'Native Creative', + 'desc': 'Cool description great stuff', + 'desc2': 'Additional body text', + 'ctatext': 'Do it', + 'sponsored': 'MediaFuse', + 'icon': { + 'width': 0, + 'height': 0, + 'url': 'https://cdn.adnxs.com/icon.png' + }, + 'main_img': { + 'width': 2352, + 'height': 1516, + 'url': 'https://cdn.adnxs.com/img.png' + }, + 'link': { + 'url': 'https://www.mediafuse.com', + 'fallback_url': '', + 'click_trackers': ['https://nym1-ib.adnxs.com/click'] + }, + 'impression_trackers': ['https://example.com'], + 'rating': '5', + 'displayurl': 'https://mediafuse.com/?url=display_url', + 'likes': '38908320', + 'downloads': '874983', + 'price': '9.99', + 'saleprice': 'FREE', + 'phone': '1234567890', + 'address': '28 W 23rd St, New York, NY 10010', + 'privacy_link': 'https://www.mediafuse.com/privacy-policy-agreement/', + 'javascriptTrackers': '' + }; + let bidderRequest = { + bids: [{ + bidId: '3db3773286ee59', + adUnitCode: 'code' + }] + } + + let result = spec.interpretResponse({ body: response1 }, {bidderRequest}); + expect(result[0].native.title).to.equal('Native Creative'); + expect(result[0].native.body).to.equal('Cool description great stuff'); + expect(result[0].native.cta).to.equal('Do it'); + expect(result[0].native.image.url).to.equal('https://cdn.adnxs.com/img.png'); + }); + + it('supports configuring outstream renderers', function () { + const outstreamResponse = deepClone(response); + outstreamResponse.tags[0].ads[0].rtb.video = {}; + outstreamResponse.tags[0].ads[0].renderer_url = 'renderer.js'; + + const bidderRequest = { + bids: [{ + bidId: '3db3773286ee59', + renderer: { + options: { + adText: 'configured' + } + }, + mediaTypes: { + video: { + context: 'outstream' + } + } + }] + }; + + const result = spec.interpretResponse({ body: outstreamResponse }, {bidderRequest}); + expect(result[0].renderer.config).to.deep.equal( + bidderRequest.bids[0].renderer.options + ); + }); + + it('should add deal_priority and deal_code', function() { + let responseWithDeal = deepClone(response); + responseWithDeal.tags[0].ads[0].ad_type = 'video'; + responseWithDeal.tags[0].ads[0].deal_priority = 5; + responseWithDeal.tags[0].ads[0].deal_code = '123'; + responseWithDeal.tags[0].ads[0].rtb.video = { + duration_ms: 1500, + player_width: 640, + player_height: 340, + }; + + let bidderRequest = { + bids: [{ + bidId: '3db3773286ee59', + adUnitCode: 'code', + mediaTypes: { + video: { + context: 'adpod' + } + } + }] + } + let result = spec.interpretResponse({ body: responseWithDeal }, {bidderRequest}); + expect(Object.keys(result[0].mediafuse)).to.include.members(['buyerMemberId', 'dealPriority', 'dealCode']); + expect(result[0].video.dealTier).to.equal(5); + }); + + it('should add advertiser id', function() { + let responseAdvertiserId = deepClone(response); + responseAdvertiserId.tags[0].ads[0].advertiser_id = '123'; + + let bidderRequest = { + bids: [{ + bidId: '3db3773286ee59', + adUnitCode: 'code' + }] + } + let result = spec.interpretResponse({ body: responseAdvertiserId }, {bidderRequest}); + expect(Object.keys(result[0].meta)).to.include.members(['advertiserId']); + }); + + it('should add brand id', function() { + let responseBrandId = deepClone(response); + responseBrandId.tags[0].ads[0].brand_id = 123; + + let bidderRequest = { + bids: [{ + bidId: '3db3773286ee59', + adUnitCode: 'code' + }] + } + let result = spec.interpretResponse({ body: responseBrandId }, {bidderRequest}); + expect(Object.keys(result[0].meta)).to.include.members(['brandId']); + }); + + it('should add advertiserDomains', function() { + let responseAdvertiserId = deepClone(response); + responseAdvertiserId.tags[0].ads[0].adomain = ['123']; + + let bidderRequest = { + bids: [{ + bidId: '3db3773286ee59', + adUnitCode: 'code' + }] + } + let result = spec.interpretResponse({ body: responseAdvertiserId }, {bidderRequest}); + expect(Object.keys(result[0].meta)).to.include.members(['advertiserDomains']); + expect(Object.keys(result[0].meta.advertiserDomains)).to.deep.equal([]); + }); + }); +}); diff --git a/test/spec/modules/mediakeysBidAdapter_spec.js b/test/spec/modules/mediakeysBidAdapter_spec.js index 602524e6eb3..38f2a3f9de3 100644 --- a/test/spec/modules/mediakeysBidAdapter_spec.js +++ b/test/spec/modules/mediakeysBidAdapter_spec.js @@ -1,5 +1,5 @@ import { expect } from 'chai'; -import find from 'core-js-pure/features/array/find.js'; +import {find} from 'src/polyfill.js'; import { spec } from 'modules/mediakeysBidAdapter.js'; import { newBidder } from 'src/adapters/bidderFactory.js'; import * as utils from 'src/utils.js'; @@ -575,12 +575,15 @@ describe('mediakeysBidAdapter', function () { }, user: { yob: 1985, - gender: 'm' - }, - device: { + gender: 'm', geo: { country: 'FR', city: 'Marseille' + }, + ext: { + data: { + registered: true + } } } } @@ -596,8 +599,9 @@ describe('mediakeysBidAdapter', function () { expect(data.site.ext.data.category).to.equal('sport'); expect(data.user.yob).to.equal(1985); expect(data.user.gender).to.equal('m'); - expect(data.device.geo.country).to.equal('FR'); - expect(data.device.geo.city).to.equal('Marseille'); + expect(data.user.geo.country).to.equal('FR'); + expect(data.user.geo.city).to.equal('Marseille'); + expect(data.user.ext.data.registered).to.be.true; }); }); diff --git a/test/spec/modules/medianetAnalyticsAdapter_spec.js b/test/spec/modules/medianetAnalyticsAdapter_spec.js index a0a62710a56..0ce26ab4863 100644 --- a/test/spec/modules/medianetAnalyticsAdapter_spec.js +++ b/test/spec/modules/medianetAnalyticsAdapter_spec.js @@ -2,7 +2,7 @@ import { expect } from 'chai'; import medianetAnalytics from 'modules/medianetAnalyticsAdapter.js'; import * as utils from 'src/utils.js'; import CONSTANTS from 'src/constants.json'; -import events from 'src/events.js'; +import * as events from 'src/events.js'; const { EVENTS: { AUCTION_INIT, BID_REQUESTED, BID_RESPONSE, NO_BID, BID_TIMEOUT, AUCTION_END, SET_TARGETING, BID_WON } @@ -23,7 +23,9 @@ const MOCK = { NO_BID_SET_TARGETING: {'div-gpt-ad-1460505748561-0': {}}, BID_WON: {'bidderCode': 'medianet', 'width': 300, 'height': 250, 'statusMessage': 'Bid available', 'adId': '3e6e4bce5c8fb3', 'requestId': '28248b0e6aece2', 'mediaType': 'banner', 'source': 'client', 'no_bid': false, 'cpm': 2.299, 'ad': 'AD_CODE', 'ttl': 180, 'creativeId': 'Test1', 'netRevenue': true, 'currency': 'USD', 'dfp_id': 'div-gpt-ad-1460505748561-0', 'originalCpm': 1.1495, 'originalCurrency': 'USD', 'auctionId': '8e0d5245-deb3-406c-96ca-9b609e077ff7', 'responseTimestamp': 1584563606009, 'requestTimestamp': 1584563605743, 'bidder': 'medianet', 'adUnitCode': 'div-gpt-ad-1460505748561-0', 'timeToRespond': 266, 'pbLg': '2.00', 'pbMg': '2.20', 'pbHg': '2.29', 'pbAg': '2.25', 'pbDg': '2.29', 'pbCg': '2.00', 'size': '300x250', 'adserverTargeting': {'hb_bidder': 'medianet', 'hb_adid': '3e6e4bce5c8fb3', 'hb_pb': '2.00', 'hb_size': '300x250', 'hb_source': 'client', 'hb_format': 'banner', 'prebid_test': 1}, 'status': 'rendered', 'params': [{'cid': 'test123', 'crid': '451466393'}]}, NO_BID: {'bidder': 'medianet', 'params': {'cid': 'test123', 'crid': '451466393', 'site': {}}, 'mediaTypes': {'banner': {'sizes': [[300, 250]], 'ext': ['asdads']}}, 'adUnitCode': 'div-gpt-ad-1460505748561-0', 'transactionId': '303fa0c6-682f-4aea-8e4a-dc68f0d5c7d5', 'sizes': [[300, 250], [300, 600]], 'bidId': '28248b0e6aece2', 'bidderRequestId': '13fccf3809fe43', 'auctionId': '8e0d5245-deb3-406c-96ca-9b609e077ff7', 'src': 'client'}, - BID_TIMEOUT: [{'bidId': '28248b0e6aece2', 'bidder': 'medianet', 'adUnitCode': 'div-gpt-ad-1460505748561-0', 'auctionId': '8e0d5245-deb3-406c-96ca-9b609e077ff7', 'params': [{'cid': 'test123', 'crid': '451466393', 'site': {}}, {'cid': '8CUX0H51P', 'crid': '451466393', 'site': {}}], 'timeout': 6}] + BID_TIMEOUT: [{'bidId': '28248b0e6aece2', 'bidder': 'medianet', 'adUnitCode': 'div-gpt-ad-1460505748561-0', 'auctionId': '8e0d5245-deb3-406c-96ca-9b609e077ff7', 'params': [{'cid': 'test123', 'crid': '451466393', 'site': {}}, {'cid': '8CUX0H51P', 'crid': '451466393', 'site': {}}], 'timeout': 6}], + BIDS_SAME_REQ_DIFF_CPM: [{'bidderCode': 'medianet', 'width': 300, 'height': 250, 'adId': '3e6e4bce5c8fgg', 'requestId': '28248b0e6aece2', 'mediaType': 'banner', 'source': 'client', 'ext': {'pvid': 123, 'crid': '321'}, 'no_bid': false, 'cpm': 2.299, 'ad': 'AD_CODE', 'ttl': 180, 'creativeId': 'Test1', 'netRevenue': true, 'currency': 'USD', 'dfp_id': 'div-gpt-ad-1460505748561-0', 'originalCpm': 1.1495, 'originalCurrency': 'USD', 'floorData': {'floorValue': 1.10, 'floorRule': 'banner'}, 'auctionId': '8e0d5245-deb3-406c-96ca-9b609e077ff7', 'responseTimestamp': 1584563606009, 'requestTimestamp': 1584563605743, 'bidder': 'medianet', 'adUnitCode': 'div-gpt-ad-1460505748561-0', 'timeToRespond': 266, 'pbLg': '2.00', 'pbMg': '2.20', 'pbHg': '2.29', 'pbAg': '2.25', 'pbDg': '2.29', 'pbCg': '2.00', 'size': '300x250', 'adserverTargeting': {'hb_bidder': 'medianet', 'hb_adid': '3e6e4bce5c8fb3', 'hb_pb': '2.00', 'hb_size': '300x250', 'hb_source': 'client', 'hb_format': 'banner', 'prebid_test': 1}, 'status': 'rendered', 'params': [{'cid': 'test123', 'crid': '451466393'}]}, {'bidderCode': 'medianet', 'width': 300, 'height': 250, 'adId': '3e6e4bce5c8fb4', 'requestId': '28248b0e6aece2', 'mediaType': 'banner', 'source': 'client', 'ext': {'pvid': 123, 'crid': '321'}, 'no_bid': false, 'cpm': 1.299, 'ad': 'AD_CODE', 'ttl': 180, 'creativeId': 'Test1', 'netRevenue': true, 'currency': 'USD', 'dfp_id': 'div-gpt-ad-1460505748561-0', 'originalCpm': 1.1495, 'originalCurrency': 'USD', 'floorData': {'floorValue': 1.10, 'floorRule': 'banner'}, 'auctionId': '8e0d5245-deb3-406c-96ca-9b609e077ff7', 'responseTimestamp': 1584563606009, 'requestTimestamp': 1584563605743, 'bidder': 'medianet', 'adUnitCode': 'div-gpt-ad-1460505748561-0', 'timeToRespond': 278, 'pbLg': '1.00', 'pbMg': '1.20', 'pbHg': '1.29', 'pbAg': '1.25', 'pbDg': '1.29', 'pbCg': '1.00', 'size': '300x250', 'adserverTargeting': {'hb_bidder': 'medianet', 'hb_adid': '3e6e4bce5c8fb3', 'hb_pb': '1.00', 'hb_size': '300x250', 'hb_source': 'client', 'hb_format': 'banner', 'prebid_test': 1}, 'status': 'rendered', 'params': [{'cid': 'test123', 'crid': '451466393'}]}], + BIDS_SAME_REQ_EQUAL_CPM: [{'bidderCode': 'medianet', 'width': 300, 'height': 250, 'adId': '3e6e4bce5c8fgg', 'requestId': '28248b0e6aece2', 'mediaType': 'banner', 'source': 'client', 'ext': {'pvid': 123, 'crid': '321'}, 'no_bid': false, 'cpm': 2.299, 'ad': 'AD_CODE', 'ttl': 180, 'creativeId': 'Test1', 'netRevenue': true, 'currency': 'USD', 'dfp_id': 'div-gpt-ad-1460505748561-0', 'originalCpm': 1.1495, 'originalCurrency': 'USD', 'floorData': {'floorValue': 1.1, 'floorRule': 'banner'}, 'auctionId': '8e0d5245-deb3-406c-96ca-9b609e077ff7', 'responseTimestamp': 1584563606009, 'requestTimestamp': 1584563605743, 'bidder': 'medianet', 'adUnitCode': 'div-gpt-ad-1460505748561-0', 'timeToRespond': 266, 'pbLg': '2.00', 'pbMg': '2.20', 'pbHg': '2.29', 'pbAg': '2.25', 'pbDg': '2.29', 'pbCg': '2.00', 'size': '300x250', 'adserverTargeting': {'hb_bidder': 'medianet', 'hb_adid': '3e6e4bce5c8fb3', 'hb_pb': '2.00', 'hb_size': '300x250', 'hb_source': 'client', 'hb_format': 'banner', 'prebid_test': 1}, 'status': 'rendered', 'params': [{'cid': 'test123', 'crid': '451466393'}]}, {'bidderCode': 'medianet', 'width': 300, 'height': 250, 'adId': '3e6e4bce5c8fb4', 'requestId': '28248b0e6aece2', 'mediaType': 'banner', 'source': 'client', 'ext': {'pvid': 123, 'crid': '321'}, 'no_bid': false, 'cpm': 2.299, 'ad': 'AD_CODE', 'ttl': 180, 'creativeId': 'Test1', 'netRevenue': true, 'currency': 'USD', 'dfp_id': 'div-gpt-ad-1460505748561-0', 'originalCpm': 1.1495, 'originalCurrency': 'USD', 'floorData': {'floorValue': 1.1, 'floorRule': 'banner'}, 'auctionId': '8e0d5245-deb3-406c-96ca-9b609e077ff7', 'responseTimestamp': 1584563606009, 'requestTimestamp': 1584563605743, 'bidder': 'medianet', 'adUnitCode': 'div-gpt-ad-1460505748561-0', 'timeToRespond': 286, 'pbLg': '2.00', 'pbMg': '2.20', 'pbHg': '2.29', 'pbAg': '2.25', 'pbDg': '2.29', 'pbCg': '2.00', 'size': '300x250', 'adserverTargeting': {'hb_bidder': 'medianet', 'hb_adid': '3e6e4bce5c8fb3', 'hb_pb': '2.00', 'hb_size': '300x250', 'hb_source': 'client', 'hb_format': 'banner', 'prebid_test': 1}, 'status': 'rendered', 'params': [{'cid': 'test123', 'crid': '451466393'}]}] } function performAuctionWithFloorConfig() { @@ -76,6 +78,15 @@ function performStandardAuctionWithTimeout() { events.emit(SET_TARGETING, MOCK.NO_BID_SET_TARGETING); } +function performStandardAuctionMultiBidWithSameRequestId(bidRespArray) { + events.emit(AUCTION_INIT, Object.assign({}, MOCK.AUCTION_INIT, {adUnits: MOCK.Ad_Units})); + events.emit(BID_REQUESTED, MOCK.BID_REQUESTED); + bidRespArray.forEach(bidResp => events.emit(BID_RESPONSE, bidResp)); + events.emit(AUCTION_END, MOCK.AUCTION_END); + events.emit(SET_TARGETING, MOCK.SET_TARGETING); + events.emit(BID_WON, MOCK.BID_WON); +} + function getQueryData(url, decode = false) { const queryArgs = url.split('?')[1].split('&'); return queryArgs.reduce((data, arg) => { @@ -265,5 +276,29 @@ describe('Media.net Analytics Adapter', function() { expect(timeoutLog.mpvid).to.have.ordered.members(['', '']); expect(timeoutLog.crid).to.have.ordered.members(['', '451466393']); }); + + it('should pick winning bid if multibids with same request id', function() { + performStandardAuctionMultiBidWithSameRequestId(MOCK.BIDS_SAME_REQ_DIFF_CPM); + let winningBid = medianetAnalytics.getlogsQueue().map((log) => getQueryData(log)).filter(log => log.winner)[0]; + expect(winningBid.adid).equals('3e6e4bce5c8fgg'); + medianetAnalytics.clearlogsQueue(); + + const reversedResponseArray = [].concat(MOCK.BIDS_SAME_REQ_DIFF_CPM).reverse(); + performStandardAuctionMultiBidWithSameRequestId(reversedResponseArray); + winningBid = medianetAnalytics.getlogsQueue().map((log) => getQueryData(log)).filter(log => log.winner)[0]; + expect(winningBid.adid).equals('3e6e4bce5c8fgg'); + }); + + it('should pick winning bid if multibids with same request id and equal cpm', function() { + performStandardAuctionMultiBidWithSameRequestId(MOCK.BIDS_SAME_REQ_EQUAL_CPM); + let winningBid = medianetAnalytics.getlogsQueue().map((log) => getQueryData(log)).filter(log => log.winner)[0]; + expect(winningBid.adid).equals('3e6e4bce5c8fgg'); + medianetAnalytics.clearlogsQueue(); + + const reversedResponseArray = [].concat(MOCK.BIDS_SAME_REQ_EQUAL_CPM).reverse(); + performStandardAuctionMultiBidWithSameRequestId(reversedResponseArray); + winningBid = medianetAnalytics.getlogsQueue().map((log) => getQueryData(log)).filter(log => log.winner)[0]; + expect(winningBid.adid).equals('3e6e4bce5c8fgg'); + }); }); }); diff --git a/test/spec/modules/mediasquareBidAdapter_spec.js b/test/spec/modules/mediasquareBidAdapter_spec.js index 862b74f4f97..40d0c44e4f9 100644 --- a/test/spec/modules/mediasquareBidAdapter_spec.js +++ b/test/spec/modules/mediasquareBidAdapter_spec.js @@ -61,7 +61,25 @@ describe('MediaSquare bid adapter tests', function () { code: 'publishername_atf_desktop_rg_pave' }, }]; - + var FLOORS_PARAMS = [{ + adUnitCode: 'banner-div', + bidId: 'aaaa1234', + auctionId: 'bbbb1234', + transactionId: 'cccc1234', + mediaTypes: { + banner: { + sizes: [ + [300, 250] + ] + } + }, + bidder: 'mediasquare', + params: { + owner: 'test', + code: 'publishername_atf_desktop_rg_pave' + }, + getFloor: function (a) { return { currency: 'EUR', floor: 1.0 }; }, + }]; var BID_RESPONSE = {'body': { 'responses': [{ 'transaction_id': 'cccc1234', @@ -117,6 +135,12 @@ describe('MediaSquare bid adapter tests', function () { expect(requestContent.codes[0]).to.have.property('auctionId').and.to.equal('bbbb1234'); expect(requestContent.codes[0]).to.have.property('transactionId').and.to.equal('cccc1234'); expect(requestContent.codes[0]).to.have.property('mediatypes').exist; + expect(requestContent.codes[0]).to.have.property('floor').exist; + expect(requestContent.codes[0].floor).to.deep.equal({}); + const requestfloor = spec.buildRequests(FLOORS_PARAMS, DEFAULT_OPTIONS); + const responsefloor = JSON.parse(requestfloor.data); + expect(responsefloor.codes[0]).to.have.property('floor').exist; + expect(responsefloor.codes[0].floor).to.have.property('floor').and.to.equal(1.0); }); it('Verify parse response', function () { @@ -148,6 +172,14 @@ describe('MediaSquare bid adapter tests', function () { expect(bid.mediasquare.match).to.exist; expect(bid.mediasquare.match).to.equal(true); }); + it('Verifies hasConsent', function () { + const request = spec.buildRequests(DEFAULT_PARAMS, DEFAULT_OPTIONS); + BID_RESPONSE.body.responses[0].hasConsent = true; + const response = spec.interpretResponse(BID_RESPONSE, request); + const bid = response[0]; + expect(bid.mediasquare.hasConsent).to.exist; + expect(bid.mediasquare.hasConsent).to.equal(true); + }); it('Verifies bidder code', function () { expect(spec.code).to.equal('mediasquare'); }); @@ -161,6 +193,8 @@ describe('MediaSquare bid adapter tests', function () { }); it('Verifies bid won', function () { const request = spec.buildRequests(DEFAULT_PARAMS, DEFAULT_OPTIONS); + BID_RESPONSE.body.responses[0].match = true + BID_RESPONSE.body.responses[0].hasConsent = true; const response = spec.interpretResponse(BID_RESPONSE, request); const won = spec.onBidWon(response[0]); expect(won).to.equal(true); diff --git a/test/spec/modules/minutemediaBidAdapter_spec.js b/test/spec/modules/minutemediaBidAdapter_spec.js new file mode 100644 index 00000000000..b1ad7f96bc4 --- /dev/null +++ b/test/spec/modules/minutemediaBidAdapter_spec.js @@ -0,0 +1,405 @@ +import { expect } from 'chai'; +import { spec } from 'modules/minutemediaBidAdapter.js'; +import { newBidder } from 'src/adapters/bidderFactory.js'; +import { config } from 'src/config.js'; +import { VIDEO } from '../../../src/mediaTypes.js'; +import { deepClone } from 'src/utils.js'; + +const ENDPOINT = 'https://hb.minutemedia-prebid.com/hb-mm'; +const TEST_ENDPOINT = 'https://hb.minutemedia-prebid.com/hb-mm-test'; +const TTL = 360; + +describe('minutemediaAdapter', function () { + const adapter = newBidder(spec); + + describe('inherited functions', function () { + it('exists and is a function', function () { + expect(adapter.callBids).to.exist.and.to.be.a('function'); + }); + }); + + describe('isBidRequestValid', function () { + const bid = { + 'bidder': spec.code, + 'adUnitCode': 'adunit-code', + 'sizes': [['640', '480']], + 'params': { + 'org': 'jdye8weeyirk00000001' + } + }; + + it('should return true when required params are passed', function () { + expect(spec.isBidRequestValid(bid)).to.equal(true); + }); + + it('should return false when required params are not found', function () { + const newBid = Object.assign({}, bid); + delete newBid.params; + newBid.params = { + 'org': null + }; + expect(spec.isBidRequestValid(newBid)).to.equal(false); + }); + }); + + describe('buildRequests', function () { + const bidRequests = [ + { + 'bidder': spec.code, + 'adUnitCode': 'adunit-code', + 'sizes': [[640, 480]], + 'params': { + 'org': 'jdye8weeyirk00000001' + }, + 'bidId': '299ffc8cca0b87', + 'bidderRequestId': '1144f487e563f9', + 'auctionId': 'bfc420c3-8577-4568-9766-a8a935fb620d', + } + ]; + + const testModeBidRequests = [ + { + 'bidder': spec.code, + 'adUnitCode': 'adunit-code', + 'sizes': [[640, 480]], + 'params': { + 'org': 'jdye8weeyirk00000001', + 'testMode': true + }, + 'bidId': '299ffc8cca0b87', + 'bidderRequestId': '1144f487e563f9', + 'auctionId': 'bfc420c3-8577-4568-9766-a8a935fb620d', + } + ]; + + const bidderRequest = { + bidderCode: 'minutemedia', + } + const placementId = '12345678'; + + it('sends the placementId as a query param', function () { + bidRequests[0].params.placementId = placementId; + const requests = spec.buildRequests(bidRequests, bidderRequest); + for (const request of requests) { + expect(request.data.placement_id).to.equal(placementId); + } + }); + + it('sends bid request to ENDPOINT via GET', function () { + const requests = spec.buildRequests(bidRequests, bidderRequest); + for (const request of requests) { + expect(request.url).to.equal(ENDPOINT); + expect(request.method).to.equal('GET'); + } + }); + + it('sends bid request to test ENDPOINT via GET', function () { + const requests = spec.buildRequests(testModeBidRequests, bidderRequest); + for (const request of requests) { + expect(request.url).to.equal(TEST_ENDPOINT); + expect(request.method).to.equal('GET'); + } + }); + + it('should send the correct bid Id', function () { + const requests = spec.buildRequests(bidRequests, bidderRequest); + for (const request of requests) { + expect(request.data.bid_id).to.equal('299ffc8cca0b87'); + } + }); + + it('should send the correct width and height', function () { + const requests = spec.buildRequests(bidRequests, bidderRequest); + for (const request of requests) { + expect(request.data).to.be.an('object'); + expect(request.data).to.have.property('width', 640); + expect(request.data).to.have.property('height', 480); + } + }); + + it('should respect syncEnabled option', function() { + config.setConfig({ + userSync: { + syncEnabled: false, + filterSettings: { + all: { + bidders: '*', + filter: 'include' + } + } + } + }); + const requests = spec.buildRequests(bidRequests, bidderRequest); + for (const request of requests) { + expect(request.data).to.be.an('object'); + expect(request.data).to.not.have.property('cs_method'); + } + }); + + it('should respect "iframe" filter settings', function () { + config.setConfig({ + userSync: { + syncEnabled: true, + filterSettings: { + iframe: { + bidders: [spec.code], + filter: 'include' + } + } + } + }); + const requests = spec.buildRequests(bidRequests, bidderRequest); + for (const request of requests) { + expect(request.data).to.be.an('object'); + expect(request.data).to.have.property('cs_method', 'iframe'); + } + }); + + it('should respect "all" filter settings', function () { + config.setConfig({ + userSync: { + syncEnabled: true, + filterSettings: { + all: { + bidders: [spec.code], + filter: 'include' + } + } + } + }); + const requests = spec.buildRequests(bidRequests, bidderRequest); + for (const request of requests) { + expect(request.data).to.be.an('object'); + expect(request.data).to.have.property('cs_method', 'iframe'); + } + }); + + it('should send the pixel user sync param if userSync is enabled and no "iframe" or "all" configs are present', function () { + config.setConfig({ + userSync: { + syncEnabled: true + } + }); + const requests = spec.buildRequests(bidRequests, bidderRequest); + for (const request of requests) { + expect(request.data).to.be.an('object'); + expect(request.data).to.have.property('cs_method', 'pixel'); + } + }); + + it('should respect total exclusion', function() { + config.setConfig({ + userSync: { + syncEnabled: true, + filterSettings: { + image: { + bidders: [spec.code], + filter: 'exclude' + }, + iframe: { + bidders: [spec.code], + filter: 'exclude' + } + } + } + }); + const requests = spec.buildRequests(bidRequests, bidderRequest); + for (const request of requests) { + expect(request.data).to.be.an('object'); + expect(request.data).to.not.have.property('cs_method'); + } + }); + + it('should have us_privacy param if usPrivacy is available in the bidRequest', function () { + const bidderRequestWithUSP = Object.assign({uspConsent: '1YNN'}, bidderRequest); + const requests = spec.buildRequests(bidRequests, bidderRequestWithUSP); + for (const request of requests) { + expect(request.data).to.be.an('object'); + expect(request.data).to.have.property('us_privacy', '1YNN'); + } + }); + + it('should have an empty us_privacy param if usPrivacy is missing in the bidRequest', function () { + const requests = spec.buildRequests(bidRequests, bidderRequest); + for (const request of requests) { + expect(request.data).to.be.an('object'); + expect(request.data).to.not.have.property('us_privacy'); + } + }); + + it('should not send the gdpr param if gdprApplies is false in the bidRequest', function () { + const bidderRequestWithGDPR = Object.assign({gdprConsent: {gdprApplies: false}}, bidderRequest); + const requests = spec.buildRequests(bidRequests, bidderRequestWithGDPR); + for (const request of requests) { + expect(request.data).to.be.an('object'); + expect(request.data).to.not.have.property('gdpr'); + expect(request.data).to.not.have.property('gdpr_consent'); + } + }); + + it('should send the gdpr param if gdprApplies is true in the bidRequest', function () { + const bidderRequestWithGDPR = Object.assign({gdprConsent: {gdprApplies: true, consentString: 'test-consent-string'}}, bidderRequest); + const requests = spec.buildRequests(bidRequests, bidderRequestWithGDPR); + for (const request of requests) { + expect(request.data).to.be.an('object'); + expect(request.data).to.have.property('gdpr', true); + expect(request.data).to.have.property('gdpr_consent', 'test-consent-string'); + } + }); + + it('should have schain param if it is available in the bidRequest', () => { + const schain = { + ver: '1.0', + complete: 1, + nodes: [{ asi: 'indirectseller.com', sid: '00001', hp: 1 }], + }; + bidRequests[0].schain = schain; + const requests = spec.buildRequests(bidRequests, bidderRequest); + for (const request of requests) { + expect(request.data).to.be.an('object'); + expect(request.data).to.have.property('schain', '1.0,1!indirectseller.com,00001,1,,,'); + } + }); + + it('should set floor_price to getFloor.floor value if it is greater than params.floorPrice', function() { + const bid = deepClone(bidRequests[0]); + bid.getFloor = () => { + return { + currency: 'USD', + floor: 3.32 + } + } + bid.params.floorPrice = 0.64; + const request = spec.buildRequests([bid], bidderRequest)[0]; + expect(request.data).to.be.an('object'); + expect(request.data).to.have.property('floor_price', 3.32); + }); + + it('should set floor_price to params.floorPrice value if it is greater than getFloor.floor', function() { + const bid = deepClone(bidRequests[0]); + bid.getFloor = () => { + return { + currency: 'USD', + floor: 0.8 + } + } + bid.params.floorPrice = 1.5; + const request = spec.buildRequests([bid], bidderRequest)[0]; + expect(request.data).to.be.an('object'); + expect(request.data).to.have.property('floor_price', 1.5); + }); + }); + + describe('interpretResponse', function () { + const response = { + cpm: 12.5, + vastXml: '', + width: 640, + height: 480, + requestId: '21e12606d47ba7', + netRevenue: true, + currency: 'USD', + adomain: ['abc.com'] + }; + + it('should get correct bid response', function () { + let expectedResponse = [ + { + requestId: '21e12606d47ba7', + cpm: 12.5, + width: 640, + height: 480, + creativeId: '21e12606d47ba7', + currency: 'USD', + netRevenue: true, + ttl: TTL, + vastXml: '', + mediaType: VIDEO, + meta: { + advertiserDomains: ['abc.com'] + } + } + ]; + const result = spec.interpretResponse({ body: response }); + expect(Object.keys(result[0])).to.have.members(Object.keys(expectedResponse[0])); + }); + }) + + describe('getUserSyncs', function() { + const imageSyncResponse = { + body: { + userSyncPixels: [ + 'https://image-sync-url.test/1', + 'https://image-sync-url.test/2', + 'https://image-sync-url.test/3' + ] + } + }; + + const iframeSyncResponse = { + body: { + userSyncURL: 'https://iframe-sync-url.test' + } + }; + + it('should register all img urls from the response', function() { + const syncs = spec.getUserSyncs({ pixelEnabled: true }, [imageSyncResponse]); + expect(syncs).to.deep.equal([ + { + type: 'image', + url: 'https://image-sync-url.test/1' + }, + { + type: 'image', + url: 'https://image-sync-url.test/2' + }, + { + type: 'image', + url: 'https://image-sync-url.test/3' + } + ]); + }); + + it('should register the iframe url from the response', function() { + const syncs = spec.getUserSyncs({ iframeEnabled: true }, [iframeSyncResponse]); + expect(syncs).to.deep.equal([ + { + type: 'iframe', + url: 'https://iframe-sync-url.test' + } + ]); + }); + + it('should register both image and iframe urls from the responses', function() { + const syncs = spec.getUserSyncs({ pixelEnabled: true, iframeEnabled: true }, [iframeSyncResponse, imageSyncResponse]); + expect(syncs).to.deep.equal([ + { + type: 'iframe', + url: 'https://iframe-sync-url.test' + }, + { + type: 'image', + url: 'https://image-sync-url.test/1' + }, + { + type: 'image', + url: 'https://image-sync-url.test/2' + }, + { + type: 'image', + url: 'https://image-sync-url.test/3' + } + ]); + }); + + it('should handle an empty response', function() { + const syncs = spec.getUserSyncs({ iframeEnabled: true }, []); + expect(syncs).to.deep.equal([]); + }); + + it('should handle when user syncs are disabled', function() { + const syncs = spec.getUserSyncs({ pixelEnabled: false }, [imageSyncResponse]); + expect(syncs).to.deep.equal([]); + }); + }) +}); diff --git a/test/spec/modules/multibid_spec.js b/test/spec/modules/multibid_spec.js index 86365eb520f..eaf8fa33a66 100644 --- a/test/spec/modules/multibid_spec.js +++ b/test/spec/modules/multibid_spec.js @@ -11,7 +11,6 @@ import { import {parse as parseQuery} from 'querystring'; import {config} from 'src/config.js'; import * as utils from 'src/utils.js'; -import find from 'core-js-pure/features/array/find.js'; describe('multibid adapter', function () { let bidArray = [{ diff --git a/test/spec/modules/nativoBidAdapter_spec.js b/test/spec/modules/nativoBidAdapter_spec.js index 23f48f3661a..c552090cf6e 100644 --- a/test/spec/modules/nativoBidAdapter_spec.js +++ b/test/spec/modules/nativoBidAdapter_spec.js @@ -1,14 +1,10 @@ import { expect } from 'chai' import { spec } from 'modules/nativoBidAdapter.js' -// import { newBidder } from 'src/adapters/bidderFactory.js' -// import * as bidderFactory from 'src/adapters/bidderFactory.js' -// import { deepClone } from 'src/utils.js' -// import { config } from 'src/config.js' describe('nativoBidAdapterTests', function () { describe('isBidRequestValid', function () { let bid = { - bidder: 'nativo' + bidder: 'nativo', } it('should return true if no params found', function () { @@ -273,9 +269,7 @@ describe('getAdUnitData', () => { } const data = spec.getAdUnitData(9876543, { impid: 12345 }) - expect(Object.keys(data)).to.have.deep.members( - Object.keys(adUnitData) - ) + expect(Object.keys(data)).to.have.deep.members(Object.keys(adUnitData)) }) it('Falls back to ad unit code value', () => { @@ -290,9 +284,158 @@ describe('getAdUnitData', () => { }, } - const data = spec.getAdUnitData(9876543, { impid: 12345, ext: { ad_unit_code: '#test-code' } }) - expect(Object.keys(data)).to.have.deep.members( - Object.keys(adUnitData) - ) + const data = spec.getAdUnitData(9876543, { + impid: 12345, + ext: { ad_unit_code: '#test-code' }, + }) + expect(Object.keys(data)).to.have.deep.members(Object.keys(adUnitData)) + }) +}) + +describe('Response to Request Filter Flow', () => { + let bidRequests = [ + { + bidder: 'nativo', + params: { + placementId: '10433394', + }, + adUnitCode: 'adunit-code', + sizes: [ + [300, 250], + [300, 600], + ], + bidId: '27b02036ccfa6e', + bidderRequestId: '1372cd8bd8d6a8', + auctionId: 'cfc467e4-2707-48da-becb-bcaab0b2c114', + transactionId: '3b36e7e0-0c3e-4006-a279-a741239154ff', + }, + ] + + let response + + beforeEach(() => { + response = { + id: '126456', + seatbid: [ + { + seat: 'seat_0', + bid: [ + { + id: 'f70362ac-f3cf-4225-82a5-948b690927a6', + impid: '1', + price: 3.569, + adm: '', + h: 300, + w: 250, + cat: [], + adomain: ['test.com'], + crid: '1060_72_6760217', + }, + ], + }, + ], + cur: 'USD', + } + }) + + let bidderRequest = { + id: 123456, + bids: [ + { + params: { + placementId: 1, + }, + }, + ], + } + + // mock + spec.getAdUnitData = () => { + return { + bidId: 123456, + size: [300, 250], + } + } + + it('Appends NO filter based on previous response', () => { + // Getting the mock response + let result = spec.interpretResponse({ body: response }, { bidderRequest }) + + // Winning the bid + spec.onBidWon(result[0]) + + // Making another request + const request = spec.buildRequests(bidRequests, { + bidderRequestId: 123456, + refererInfo: { + referer: 'https://www.test.com', + }, + }) + expect(request.url).to.not.include('ntv_aft') + expect(request.url).to.not.include('ntv_avtf') + expect(request.url).to.not.include('ntv_ctf') + }) + + it('Appends Ads filter based on previous response', () => { + response.seatbid[0].bid[0].ext = { adsToFilter: ['12345'] } + + // Getting the mock response + let result = spec.interpretResponse({ body: response }, { bidderRequest }) + + // Winning the bid + spec.onBidWon(result[0]) + + // Making another request + const request = spec.buildRequests(bidRequests, { + bidderRequestId: 123456, + refererInfo: { + referer: 'https://www.test.com', + }, + }) + expect(request.url).to.include(`ntv_atf=12345`) + expect(request.url).to.not.include('ntv_avtf') + expect(request.url).to.not.include('ntv_ctf') + }) + + it('Appends Advertiser filter based on previous response', () => { + response.seatbid[0].bid[0].ext = { advertisersToFilter: ['1'] } + + // Getting the mock response + let result = spec.interpretResponse({ body: response }, { bidderRequest }) + + // Winning the bid + spec.onBidWon(result[0]) + + // Making another request + const request = spec.buildRequests(bidRequests, { + bidderRequestId: 123456, + refererInfo: { + referer: 'https://www.test.com', + }, + }) + expect(request.url).to.include(`ntv_atf=12345`) + expect(request.url).to.include('ntv_avtf=1') + expect(request.url).to.not.include('ntv_ctf') + }) + + it('Appends Campaign filter based on previous response', () => { + response.seatbid[0].bid[0].ext = { campaignsToFilter: ['234'] } + + // Getting the mock response + let result = spec.interpretResponse({ body: response }, { bidderRequest }) + + // Winning the bid + spec.onBidWon(result[0]) + + // Making another request + const request = spec.buildRequests(bidRequests, { + bidderRequestId: 123456, + refererInfo: { + referer: 'https://www.test.com', + }, + }) + expect(request.url).to.include(`ntv_atf=12345`) + expect(request.url).to.include('ntv_avtf=1') + expect(request.url).to.include('ntv_ctf=234') }) }) diff --git a/test/spec/modules/nextMillenniumBidAdapter_spec.js b/test/spec/modules/nextMillenniumBidAdapter_spec.js index 0dfee96d0ea..a8aa62f24d1 100644 --- a/test/spec/modules/nextMillenniumBidAdapter_spec.js +++ b/test/spec/modules/nextMillenniumBidAdapter_spec.js @@ -18,6 +18,47 @@ describe('nextMillenniumBidAdapterTests', function() { } ]; + const bidRequestDataGI = [ + { + adUnitCode: 'test-banner-gi', + bidId: 'bid1234', + auctionId: 'b06c5141-fe8f-4cdf-9d7d-54415490a917', + bidder: 'nextMillennium', + params: { group_id: '1234' }, + mediaTypes: { + banner: { + sizes: [[300, 250]] + } + }, + + sizes: [[300, 250]], + uspConsent: '1---', + gdprConsent: { + consentString: 'kjfdniwjnifwenrif3', + gdprApplies: true + } + }, + + { + adUnitCode: 'test-video-gi', + bidId: 'bid1234', + auctionId: 'b06c5141-fe8f-4cdf-9d7d-54415490a917', + bidder: 'nextMillennium', + params: { group_id: '1234' }, + mediaTypes: { + video: { + playerSize: [640, 480], + } + }, + + uspConsent: '1---', + gdprConsent: { + consentString: 'kjfdniwjnifwenrif3', + gdprApplies: true + } + }, + ]; + 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'); @@ -38,6 +79,16 @@ describe('nextMillenniumBidAdapterTests', function() { expect(JSON.parse(request[0].data).id).to.equal('b06c5141-fe8f-4cdf-9d7d-54415490a917'); }); + it('use parameters group_id', function() { + for (let test of bidRequestDataGI) { + const request = spec.buildRequests([test]); + const requestData = JSON.parse(request[0].data); + const storeRequestId = requestData.ext.prebid.storedrequest.id; + const templateRE = /^g\d+;\d+x\d+;/; + expect(templateRE.test(storeRequestId)).to.be.true; + }; + }); + 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); diff --git a/test/spec/modules/nextrollBidAdapter_spec.js b/test/spec/modules/nextrollBidAdapter_spec.js index 4699fbc6e08..d4779120248 100644 --- a/test/spec/modules/nextrollBidAdapter_spec.js +++ b/test/spec/modules/nextrollBidAdapter_spec.js @@ -244,8 +244,8 @@ describe('nextrollBidAdapter', function() { let expectedResponse = { clickUrl: clickUrl, impressionTrackers: [impUrl], - privacyLink: 'https://info.evidon.com/pub_info/573', - privacyIcon: 'https://c.betrad.com/pub/icon1.png', + privacyLink: 'https://app.adroll.com/optout/personalized', + privacyIcon: 'https://s.adroll.com/j/ad-choices-small.png', title: titleText, image: {url: imgUrl, width: imgW, height: imgH}, sponsoredBy: brandText, @@ -274,8 +274,8 @@ describe('nextrollBidAdapter', function() { impressionTrackers: [impUrl], jstracker: [], clickTrackers: [], - privacyLink: 'https://info.evidon.com/pub_info/573', - privacyIcon: 'https://c.betrad.com/pub/icon1.png', + privacyLink: 'https://app.adroll.com/optout/personalized', + privacyIcon: 'https://s.adroll.com/j/ad-choices-small.png', title: titleText, image: {url: imgUrl, width: imgW, height: imgH}, icon: {url: iconUrl, width: iconW, height: iconH}, diff --git a/test/spec/modules/nexx360BidAdapter_spec.js b/test/spec/modules/nexx360BidAdapter_spec.js new file mode 100644 index 00000000000..7501391cbfc --- /dev/null +++ b/test/spec/modules/nexx360BidAdapter_spec.js @@ -0,0 +1,140 @@ +import {expect} from 'chai'; +import {spec} from 'modules/nexx360BidAdapter.js'; +import {newBidder} from 'src/adapters/bidderFactory.js'; +import {config} from 'src/config.js'; +import * as utils from 'src/utils.js'; +import { requestBidsHook } from 'modules/consentManagement.js'; + +describe('Nexx360 bid adapter tests', function () { + var DEFAULT_PARAMS = [{ + adUnitCode: 'banner-div', + bidId: 'aaaa1234', + auctionId: 'bbbb1234', + transactionId: 'cccc1234', + mediaTypes: { + banner: { + sizes: [ + [300, 250] + ] + } + }, + bidder: 'nexx360', + params: { + account: '1067', + tagId: 'luvxjvgn' + }, + }]; + + var BID_RESPONSE = {'body': { + 'responses': [ + { + 'bidId': '49a705a42610a', + 'cpm': 0.437245, + 'width': 300, + 'height': 250, + 'creativeId': '98493581', + 'currency': 'EUR', + 'netRevenue': true, + 'ttl': 360, + 'uuid': 'ce6d1ee3-2a05-4d7c-b97a-9e62097798ec', + 'bidder': 'appnexus', + 'consent': 1, + 'tagId': 'luvxjvgn' + } + ], + }}; + + const DEFAULT_OPTIONS = { + gdprConsent: { + gdprApplies: true, + consentString: 'BOzZdA0OzZdA0AGABBENDJ-AAAAvh7_______9______9uz_Ov_v_f__33e8__9v_l_7_-___u_-33d4-_1vf99yfm1-7ftr3tp_87ues2_Xur__79__3z3_9pxP78k89r7337Mw_v-_v-b7JCPN_Y3v-8Kg', + vendorData: {} + }, + refererInfo: { + referer: 'https://www.prebid.org', + canonicalUrl: 'https://www.prebid.org/the/link/to/the/page' + }, + uspConsent: '111222333', + userId: { 'id5id': { uid: '1111' } }, + schain: { + 'ver': '1.0', + 'complete': 1, + 'nodes': [{ + 'asi': 'exchange1.com', + 'sid': '1234', + 'hp': 1, + 'rid': 'bid-request-1', + 'name': 'publisher', + 'domain': 'publisher.com' + }] + }, + }; + it('Verify build request', function () { + const request = spec.buildRequests(DEFAULT_PARAMS, DEFAULT_OPTIONS); + expect(request).to.have.property('url').and.to.equal('https://fast.nexx360.io/prebid'); + expect(request).to.have.property('method').and.to.equal('POST'); + const requestContent = JSON.parse(request.data); + expect(requestContent.adUnits[0]).to.have.property('account').and.to.equal('1067'); + expect(requestContent.adUnits[0]).to.have.property('tagId').and.to.equal('luvxjvgn'); + expect(requestContent.adUnits[0]).to.have.property('label').and.to.equal('banner-div'); + expect(requestContent.adUnits[0]).to.have.property('bidId').and.to.equal('aaaa1234'); + expect(requestContent.adUnits[0]).to.have.property('auctionId').and.to.equal('bbbb1234'); + expect(requestContent.adUnits[0]).to.have.property('mediatypes').exist; + }); + + it('Verify parse response', function () { + const request = spec.buildRequests(DEFAULT_PARAMS, DEFAULT_OPTIONS); + const response = spec.interpretResponse(BID_RESPONSE, request); + expect(response).to.have.lengthOf(1); + const bid = response[0]; + expect(bid.cpm).to.equal(0.437245); + expect(bid.adUrl).to.equal('https://fast.nexx360.io/cache?uuid=ce6d1ee3-2a05-4d7c-b97a-9e62097798ec'); + expect(bid.width).to.equal(300); + expect(bid.height).to.equal(250); + expect(bid.creativeId).to.equal('98493581'); + expect(bid.currency).to.equal('EUR'); + expect(bid.netRevenue).to.equal(true); + expect(bid.ttl).to.equal(360); + expect(bid.requestId).to.equal('49a705a42610a'); + expect(bid.nexx360).to.exist; + expect(bid.nexx360.ssp).to.equal('appnexus'); + }); + it('Verifies bidder code', function () { + expect(spec.code).to.equal('nexx360'); + }); + + it('Verifies bidder aliases', function () { + expect(spec.aliases).to.have.lengthOf(1); + expect(spec.aliases[0]).to.equal('revenuemaker'); + }); + it('Verifies if bid request valid', function () { + expect(spec.isBidRequestValid(DEFAULT_PARAMS[0])).to.equal(true); + }); + it('Verifies bid won', function () { + const request = spec.buildRequests(DEFAULT_PARAMS, DEFAULT_OPTIONS); + const response = spec.interpretResponse(BID_RESPONSE, request); + const won = spec.onBidWon(response[0]); + expect(won).to.equal(true); + }); + it('Verifies user sync without cookie in bid response', function () { + var syncs = spec.getUserSyncs({}, [BID_RESPONSE], DEFAULT_OPTIONS.gdprConsent, DEFAULT_OPTIONS.uspConsent); + expect(syncs).to.have.lengthOf(0); + }); + it('Verifies user sync with cookies in bid response', function () { + BID_RESPONSE.body.cookies = [{'type': 'image', 'url': 'http://www.cookie.sync.org/'}]; + var syncs = spec.getUserSyncs({}, [BID_RESPONSE], DEFAULT_OPTIONS.gdprConsent); + expect(syncs).to.have.lengthOf(1); + expect(syncs[0]).to.have.property('type').and.to.equal('image'); + expect(syncs[0]).to.have.property('url').and.to.equal('http://www.cookie.sync.org/'); + }); + it('Verifies user sync with no bid response', function() { + var syncs = spec.getUserSyncs({}, null, DEFAULT_OPTIONS.gdprConsent, DEFAULT_OPTIONS.uspConsent); + expect(syncs).to.have.lengthOf(0); + }); + it('Verifies user sync with no bid body response', function() { + var syncs = spec.getUserSyncs({}, [], DEFAULT_OPTIONS.gdprConsent, DEFAULT_OPTIONS.uspConsent); + expect(syncs).to.have.lengthOf(0); + var syncs = spec.getUserSyncs({}, [{}], DEFAULT_OPTIONS.gdprConsent, DEFAULT_OPTIONS.uspConsent); + expect(syncs).to.have.lengthOf(0); + }); +}); diff --git a/test/spec/modules/novatiqIdSystem_spec.js b/test/spec/modules/novatiqIdSystem_spec.js index 60c82626450..b92fb0d219a 100644 --- a/test/spec/modules/novatiqIdSystem_spec.js +++ b/test/spec/modules/novatiqIdSystem_spec.js @@ -3,41 +3,41 @@ import * as utils from 'src/utils.js'; import { server } from 'test/mocks/xhr.js'; describe('novatiqIdSystem', function () { + let urlParams = { + novatiqId: 'snowflake', + useStandardUuid: false, + useSspId: true, + useSspHost: true + } + describe('getSrcId', function() { it('getSrcId should set srcId value to 000 due to undefined parameter in config section', function() { const config = { params: { } }; const configParams = config.params || {}; - const response = novatiqIdSubmodule.getSrcId(configParams); + const response = novatiqIdSubmodule.getSrcId(configParams, urlParams); expect(response).to.eq('000'); }); it('getSrcId should set srcId value to 000 due to missing value in config section', function() { const config = { params: { sourceid: '' } }; const configParams = config.params || {}; - const response = novatiqIdSubmodule.getSrcId(configParams); + const response = novatiqIdSubmodule.getSrcId(configParams, urlParams); expect(response).to.eq('000'); }); it('getSrcId should set value to 000 due to null value in config section', function() { const config = { params: { sourceid: null } }; const configParams = config.params || {}; - const response = novatiqIdSubmodule.getSrcId(configParams); + const response = novatiqIdSubmodule.getSrcId(configParams, urlParams); expect(response).to.eq('000'); }); it('getSrcId should set value to 001 due to wrong length in config section max 3 chars', function() { const config = { params: { sourceid: '1234' } }; const configParams = config.params || {}; - const response = novatiqIdSubmodule.getSrcId(configParams); + const response = novatiqIdSubmodule.getSrcId(configParams, urlParams); expect(response).to.eq('001'); }); - - it('getSrcId should set value to 002 due to wrong format in config section', function() { - const config = { params: { sourceid: '1xc' } }; - const configParams = config.params || {}; - const response = novatiqIdSubmodule.getSrcId(configParams); - expect(response).to.eq('002'); - }); }); describe('getId', function() { @@ -52,6 +52,89 @@ describe('novatiqIdSystem', function () { const response = novatiqIdSubmodule.getId(config); expect(response.id).should.be.not.empty; }); + + it('should set sharedStatus if sharedID is configured but returned null', function() { + const config = { params: { sourceid: '123', useSharedId: true } }; + const response = novatiqIdSubmodule.getId(config); + expect(response.sharedStatus).to.equal('Not Found'); + }); + + it('should set sharedStatus if sharedID is configured and is valid', function() { + const config = { params: { sourceid: '123', useSharedId: true } }; + + let stub = sinon.stub(novatiqIdSubmodule, 'getSharedId').returns('fakeId'); + + const response = novatiqIdSubmodule.getId(config); + + stub.restore(); + + expect(response.sharedStatus).to.equal('Found'); + }); + + it('should set sharedStatus if sharedID is configured and is valid when making an async call', function() { + const config = { params: { sourceid: '123', useSharedId: true, useCallbacks: true } }; + + let stub = sinon.stub(novatiqIdSubmodule, 'getSharedId').returns('fakeId'); + + const response = novatiqIdSubmodule.getId(config); + + stub.restore(); + + expect(response.sharedStatus).to.equal('Found'); + }); + }); + + describe('getUrlParams', function() { + it('should return default url parameters when none set', function() { + const defaultUrlParams = { + novatiqId: 'snowflake', + useStandardUuid: false, + useSspId: true, + useSspHost: true + } + + const config = { params: { sourceid: '123' } }; + const response = novatiqIdSubmodule.getUrlParams(config); + + expect(response).to.deep.equal(defaultUrlParams); + }); + + it('should return custom url parameters when set', function() { + let customUrlParams = { + novatiqId: 'hyperid', + useStandardUuid: true, + useSspId: false, + useSspHost: false + } + + const config = { + sourceid: '123', + urlParams: { + novatiqId: 'hyperid', + useStandardUuid: true, + useSspId: false, + useSspHost: false + } + }; + const response = novatiqIdSubmodule.getUrlParams(config); + + expect(response).to.deep.equal(customUrlParams); + }); + }); + + describe('sendAsyncSyncRequest', function() { + it('should return an async function when called asynchronously', function() { + const defaultUrlParams = { + novatiqId: 'snowflake', + useStandardUuid: false, + useSspId: true, + useSspHost: true + } + + const url = novatiqIdSubmodule.getSyncUrl(false, '', defaultUrlParams); + const response = novatiqIdSubmodule.sendAsyncSyncRequest('testuuid', url); + expect(response.callback).should.not.be.empty; + }); }); describe('decode', function() { diff --git a/test/spec/modules/oguryBidAdapter_spec.js b/test/spec/modules/oguryBidAdapter_spec.js index 225d0cd30ef..acf62bf5a7b 100644 --- a/test/spec/modules/oguryBidAdapter_spec.js +++ b/test/spec/modules/oguryBidAdapter_spec.js @@ -229,7 +229,7 @@ describe('OguryBidAdapter', function () { const defaultTimeout = 1000; const expectedRequestObject = { id: bidRequests[0].auctionId, - at: 2, + at: 1, tmax: defaultTimeout, imp: [{ id: bidRequests[0].bidId, @@ -271,7 +271,7 @@ describe('OguryBidAdapter', function () { }, ext: { prebidversion: '$prebid.version$', - adapterversion: '1.2.7' + adapterversion: '1.2.10' } }; @@ -300,7 +300,59 @@ describe('OguryBidAdapter', function () { ...expectedRequestObject, regs: { ext: { - gdpr: 1 + gdpr: 0 + }, + }, + user: { + ext: { + consent: '' + }, + } + }; + + const validBidRequests = bidRequests + + const request = spec.buildRequests(validBidRequests, bidderRequestWithoutGdpr); + expect(request.data).to.deep.equal(expectedRequestObjectWithoutGdpr); + expect(request.data.regs.ext.gdpr).to.be.a('number'); + }); + + it('should not add gdpr infos if gdprConsent is undefined', () => { + const bidderRequestWithoutGdpr = { + ...bidderRequest, + gdprConsent: undefined, + } + const expectedRequestObjectWithoutGdpr = { + ...expectedRequestObject, + regs: { + ext: { + gdpr: 0 + }, + }, + user: { + ext: { + consent: '' + }, + } + }; + + const validBidRequests = bidRequests + + const request = spec.buildRequests(validBidRequests, bidderRequestWithoutGdpr); + expect(request.data).to.deep.equal(expectedRequestObjectWithoutGdpr); + expect(request.data.regs.ext.gdpr).to.be.a('number'); + }); + + it('should not add tcString and turn off gdpr-applies if consentString and gdprApplies are undefined', () => { + const bidderRequestWithoutGdpr = { + ...bidderRequest, + gdprConsent: { consentString: undefined, gdprApplies: undefined }, + } + const expectedRequestObjectWithoutGdpr = { + ...expectedRequestObject, + regs: { + ext: { + gdpr: 0 }, }, user: { @@ -430,7 +482,7 @@ describe('OguryBidAdapter', function () { advertiserDomains: openRtbBidResponse.body.seatbid[0].bid[0].adomain }, nurl: openRtbBidResponse.body.seatbid[0].bid[0].nurl, - adapterVersion: '1.2.7', + adapterVersion: '1.2.10', prebidVersion: '$prebid.version$' }, { requestId: openRtbBidResponse.body.seatbid[0].bid[1].impid, @@ -447,7 +499,7 @@ describe('OguryBidAdapter', function () { advertiserDomains: openRtbBidResponse.body.seatbid[0].bid[1].adomain }, nurl: openRtbBidResponse.body.seatbid[0].bid[1].nurl, - adapterVersion: '1.2.7', + adapterVersion: '1.2.10', prebidVersion: '$prebid.version$' }] @@ -494,6 +546,11 @@ describe('OguryBidAdapter', function () { expect(requests.length).to.equal(0); }) + it('Should not create nurl request if bid contains undefined nurl', function() { + spec.onBidWon({ nurl: undefined }) + expect(requests.length).to.equal(0); + }) + it('Should create nurl request if bid nurl', function() { spec.onBidWon({ nurl }) expect(requests.length).to.equal(1); @@ -569,6 +626,7 @@ describe('OguryBidAdapter', function () { expect(requests.length).to.equal(1); expect(requests[0].url).to.equal(TIMEOUT_URL); expect(requests[0].method).to.equal('POST'); + expect(JSON.parse(requests[0].requestBody).location).to.equal(window.location.href); }) }); }); diff --git a/test/spec/modules/onetagBidAdapter_spec.js b/test/spec/modules/onetagBidAdapter_spec.js index e873597ca15..f335f2ec62a 100644 --- a/test/spec/modules/onetagBidAdapter_spec.js +++ b/test/spec/modules/onetagBidAdapter_spec.js @@ -1,6 +1,6 @@ import { spec, isValid, hasTypeVideo } from 'modules/onetagBidAdapter.js'; import { expect } from 'chai'; -import find from 'core-js-pure/features/array/find.js'; +import {find} from 'src/polyfill.js'; import { BANNER, VIDEO } from 'src/mediaTypes.js'; import {INSTREAM, OUTSTREAM} from 'src/video.js'; diff --git a/test/spec/modules/ooloAnalyticsAdapter_spec.js b/test/spec/modules/ooloAnalyticsAdapter_spec.js index f82f7856fb2..a6030e972ce 100644 --- a/test/spec/modules/ooloAnalyticsAdapter_spec.js +++ b/test/spec/modules/ooloAnalyticsAdapter_spec.js @@ -2,7 +2,7 @@ import ooloAnalytics, { PAGEVIEW_ID } from 'modules/ooloAnalyticsAdapter.js'; import {expect} from 'chai'; import {server} from 'test/mocks/xhr.js'; import constants from 'src/constants.json' -import events from 'src/events' +import * as events from 'src/events' import { config } from 'src/config'; import { buildAuctionData, generatePageViewId } from 'modules/ooloAnalyticsAdapter'; diff --git a/test/spec/modules/open8BidAdapter_spec.js b/test/spec/modules/open8BidAdapter_spec.js new file mode 100644 index 00000000000..27e460bad9d --- /dev/null +++ b/test/spec/modules/open8BidAdapter_spec.js @@ -0,0 +1,258 @@ +import { spec } from 'modules/open8BidAdapter'; +import { newBidder } from 'src/adapters/bidderFactory'; + +const ENDPOINT = 'https://as.vt.open8.com/v1/control/prebid'; + +describe('Open8Adapter', function() { + const adapter = newBidder(spec); + + describe('isBidRequestValid', function() { + let bid = { + 'bidder': 'open8', + 'params': { + 'slotKey': 'slotkey1234' + }, + 'adUnitCode': 'adunit', + 'sizes': [[300, 250]], + 'bidId': 'bidid1234', + 'bidderRequestId': 'requestid1234', + 'auctionId': 'auctionid1234', + }; + + 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() { + bid.params = { + ' slotKey': 0 + }; + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + }); + + describe('buildRequests', function() { + let bidRequests = [ + { + 'bidder': 'open8', + 'params': { + 'slotKey': 'slotkey1234' + }, + 'adUnitCode': 'adunit', + 'sizes': [[300, 250]], + 'bidId': 'bidid1234', + 'bidderRequestId': 'requestid1234', + 'auctionId': 'auctionid1234', + } + ]; + + it('sends bid request to ENDPOINT via GET', function() { + const requests = spec.buildRequests(bidRequests); + expect(requests[0].url).to.equal(ENDPOINT); + expect(requests[0].method).to.equal('GET'); + }); + }); + describe('interpretResponse', function() { + const adomin = ['example.com'] + const bannerResponse = { + slotKey: 'slotkey1234', + userId: 'userid1234', + impId: 'impid1234', + media: 'TEST_MEDIA', + nurl: '//example/win', + isAdReturn: true, + syncPixels: ['//example/sync/pixel.gif'], + syncIFs: [], + ad: { + bidId: 'TEST_BID_ID', + price: 1234.56, + creativeId: 'creativeid1234', + dealId: 'TEST_DEAL_ID', + currency: 'JPY', + ds: 876, + spd: 1234, + fa: 5678, + pr: 'pr1234', + mr: 'mr1234', + nurl: '//example/win', + adType: 2, + banner: { + w: 300, + h: 250, + adm: '
', + imps: ['//example.com/imp'] + }, + adomain: adomin + } + }; + const videoResponse = { + slotKey: 'slotkey1234', + userId: 'userid1234', + impId: 'impid1234', + media: 'TEST_MEDIA', + isAdReturn: true, + syncPixels: ['//example/sync/pixel.gif'], + syncIFs: [], + ad: { + bidId: 'TEST_BID_ID', + price: 1234.56, + creativeId: 'creativeid1234', + dealId: 'TEST_DEAL_ID', + currency: 'JPY', + ds: 876, + spd: 1234, + fa: 5678, + pr: 'pr1234', + mr: 'mr1234', + nurl: '//example/win', + adType: 1, + video: { + purl: '//playerexample.js', + vastXml: '', + w: 320, + h: 180 + }, + adomain: adomin + } + }; + + it('should get correct banner bid response', function() { + let expectedResponse = [{ + 'slotKey': 'slotkey1234', + 'userId': 'userid1234', + 'impId': 'impid1234', + 'media': 'TEST_MEDIA', + 'ds': 876, + 'spd': 1234, + 'fa': 5678, + 'pr': 'pr1234', + 'mr': 'mr1234', + 'nurl': '//example/win', + 'requestId': 'requestid1234', + 'cpm': 1234.56, + 'creativeId': 'creativeid1234', + 'dealId': 'TEST_DEAL_ID', + 'width': 300, + 'height': 250, + 'ad': "
", + 'mediaType': 'banner', + 'currency': 'JPY', + 'ttl': 360, + 'netRevenue': true, + 'meta': { } + }]; + + let bidderRequest; + let result = spec.interpretResponse({ body: bannerResponse }, { bidderRequest }); + expect(Object.keys(result[0])).to.have.members(Object.keys(expectedResponse[0])); + expect(result[0]).to.nested.contain.property('meta.advertiserDomains', adomin); + }); + + it('handles video responses', function() { + let expectedResponse = [{ + 'slotKey': 'slotkey1234', + 'userId': 'userid1234', + 'impId': 'impid1234', + 'media': 'TEST_MEDIA', + 'ds': 876, + 'spd': 1234, + 'fa': 5678, + 'pr': 'pr1234', + 'mr': 'mr1234', + 'nurl': '//example/win', + 'requestId': 'requestid1234', + 'cpm': 1234.56, + 'creativeId': 'creativeid1234', + 'dealId': 'TEST_DEAL_ID', + 'width': 320, + 'height': 180, + 'vastXml': '', + 'mediaType': 'video', + 'renderer': {}, + 'adResponse': {}, + 'currency': 'JPY', + 'ttl': 360, + 'netRevenue': true, + 'meta': { } + }]; + + let bidderRequest; + let result = spec.interpretResponse({ body: videoResponse }, { bidderRequest }); + expect(Object.keys(result[0])).to.have.members(Object.keys(expectedResponse[0])); + expect(result[0]).to.nested.contain.property('meta.advertiserDomains', adomin); + }); + + it('handles nobid responses', function() { + let response = { + isAdReturn: false, + 'ad': {} + }; + + let bidderRequest; + let result = spec.interpretResponse({ body: response }, { bidderRequest }); + expect(result.length).to.equal(0); + }); + }); + + describe('getUserSyncs', function() { + const imgResponse1 = { + body: { + 'isAdReturn': true, + 'ad': { /* ad body */ }, + 'syncPixels': [ + 'https://example.test/1' + ] + } + }; + + const imgResponse2 = { + body: { + 'isAdReturn': true, + 'ad': { /* ad body */ }, + 'syncPixels': [ + 'https://example.test/2' + ] + } + }; + + const ifResponse = { + body: { + 'isAdReturn': true, + 'ad': { /* ad body */ }, + 'syncIFs': [ + 'https://example.test/3' + ] + } + }; + + it('should use a sync img url from first response', function() { + const syncs = spec.getUserSyncs({ pixelEnabled: true }, [imgResponse1, imgResponse2, ifResponse]); + expect(syncs).to.deep.equal([ + { + type: 'image', + url: 'https://example.test/1' + } + ]); + }); + + it('handle ifs response', function() { + const syncs = spec.getUserSyncs({ iframeEnabled: true }, [ifResponse]); + expect(syncs).to.deep.equal([ + { + type: 'iframe', + url: 'https://example.test/3' + } + ]); + }); + + it('handle empty response (e.g. timeout)', function() { + const syncs = spec.getUserSyncs({ pixelEnabled: true }, []); + expect(syncs).to.deep.equal([]); + }); + + it('returns empty syncs when not enabled', function() { + const syncs = spec.getUserSyncs({ pixelEnabled: false }, [imgResponse1]); + expect(syncs).to.deep.equal([]); + }); + }); +}); diff --git a/test/spec/modules/openxAnalyticsAdapter_spec.js b/test/spec/modules/openxAnalyticsAdapter_spec.js index d7d2d31669c..47663a41f47 100644 --- a/test/spec/modules/openxAnalyticsAdapter_spec.js +++ b/test/spec/modules/openxAnalyticsAdapter_spec.js @@ -1,10 +1,10 @@ import { expect } from 'chai'; import openxAdapter, {AUCTION_STATES} from 'modules/openxAnalyticsAdapter.js'; -import events from 'src/events.js'; +import * as events from 'src/events.js'; import CONSTANTS from 'src/constants.json'; import * as utils from 'src/utils.js'; import { server } from 'test/mocks/xhr.js'; -import find from 'core-js-pure/features/array/find.js'; +import {find} from 'src/polyfill.js'; const { EVENTS: { AUCTION_INIT, BID_REQUESTED, BID_RESPONSE, BID_TIMEOUT, BID_WON, AUCTION_END } diff --git a/test/spec/modules/optimeraRtdProvider_spec.js b/test/spec/modules/optimeraRtdProvider_spec.js index 7883412ab70..aec8b79045e 100644 --- a/test/spec/modules/optimeraRtdProvider_spec.js +++ b/test/spec/modules/optimeraRtdProvider_spec.js @@ -1,5 +1,6 @@ import * as optimeraRTD from '../../../modules/optimeraRtdProvider.js'; -let utils = require('src/utils.js'); + +const utils = require('src/utils.js'); describe('Optimera RTD sub module', () => { it('should init, return true, and set the params', () => { @@ -31,34 +32,67 @@ describe('Optimera RTD score file properly sets targeting values', () => { const scores = { 'div-0': ['A1', 'A2'], 'div-1': ['A3', 'A4'], - 'device': { - 'de': { + device: { + de: { 'div-0': ['A5', 'A6'], 'div-1': ['A7', 'A8'], - 'insights': { - 'ilv': ['div-0'], - 'miv': ['div-4'], + insights: { + ilv: ['div-0'], + miv: ['div-4'], } }, - 'mo': { + mo: { 'div-0': ['A9', 'B0'], 'div-1': ['B1', 'B2'], - 'insights': { - 'ilv': ['div-1'], - 'miv': ['div-2'], + insights: { + ilv: ['div-1'], + miv: ['div-2'], } } }, - 'insights': { - 'ilv': ['div-5'], - 'miv': ['div-6'], + insights: { + ilv: ['div-5'], + miv: ['div-6'], } }; it('Properly set the score file url and scores', () => { optimeraRTD.setScores(JSON.stringify(scores)); expect(optimeraRTD.optimeraTargeting['div-0']).to.include.ordered.members(['A5', 'A6']); expect(optimeraRTD.optimeraTargeting['div-1']).to.include.ordered.members(['A7', 'A8']); - expect(window.optimeraInsights.data['ilv']).to.include.ordered.members(['div-0']); + }); +}); + +describe('Optimera RTD propery sets the window.optimera object', () => { + const scores = { + 'div-0': ['A1', 'A2'], + 'div-1': ['A3', 'A4'], + device: { + de: { + 'div-0': ['A5', 'A6'], + 'div-1': ['A7', 'A8'], + insights: { + ilv: ['div-0'], + miv: ['div-4'], + } + }, + mo: { + 'div-0': ['A9', 'B0'], + 'div-1': ['B1', 'B2'], + insights: { + ilv: ['div-1'], + miv: ['div-2'], + } + } + }, + insights: { + ilv: ['div-5'], + miv: ['div-6'], + } + }; + it('Properly set the score file url and scores', () => { + optimeraRTD.setScores(JSON.stringify(scores)); + expect(window.optimera.data['div-1']).to.include.ordered.members(['A7', 'A8']); + expect(window.optimera.insights.ilv).to.include.ordered.members(['div-0']); }); }); @@ -66,18 +100,18 @@ describe('Optimera RTD targeting object is properly formed', () => { const adDivs = ['div-0', 'div-1']; it('applyTargeting properly created the targeting object', () => { const targeting = optimeraRTD.returnTargetingData(adDivs); - expect(targeting).to.deep.include({'div-0': {'optimera': [['A5', 'A6']]}}); - expect(targeting).to.deep.include({'div-1': {'optimera': [['A7', 'A8']]}}); + expect(targeting).to.deep.include({ 'div-0': { optimera: [['A5', 'A6']] } }); + expect(targeting).to.deep.include({ 'div-1': { optimera: [['A7', 'A8']] } }); }); }); describe('Optimera RTD error logging', () => { let utilsLogErrorStub; - beforeEach(function () { + beforeEach(() => { utilsLogErrorStub = sinon.stub(utils, 'logError'); }); - afterEach(function () { + afterEach(() => { utilsLogErrorStub.restore(); }); diff --git a/test/spec/modules/optimonAnalyticsAdapter_spec.js b/test/spec/modules/optimonAnalyticsAdapter_spec.js index b5b76ce3fde..c50bfcb170f 100644 --- a/test/spec/modules/optimonAnalyticsAdapter_spec.js +++ b/test/spec/modules/optimonAnalyticsAdapter_spec.js @@ -2,7 +2,7 @@ import * as utils from 'src/utils.js'; import { expect } from 'chai'; import optimonAnalyticsAdapter from '../../../modules/optimonAnalyticsAdapter.js'; import adapterManager from 'src/adapterManager'; -import events from 'src/events'; +import * as events from 'src/events'; import constants from 'src/constants.json' const AD_UNIT_CODE = 'demo-adunit-1'; diff --git a/test/spec/modules/orbidderBidAdapter_spec.js b/test/spec/modules/orbidderBidAdapter_spec.js index 0a18799ad4b..750524cf47f 100644 --- a/test/spec/modules/orbidderBidAdapter_spec.js +++ b/test/spec/modules/orbidderBidAdapter_spec.js @@ -283,6 +283,27 @@ describe('orbidderBidAdapter', () => { }); }); + describe('buildRequests with price floor module', () => { + const bidRequest = deepClone(defaultBidRequestBanner); + bidRequest.params.bidfloor = 1; + bidRequest.getFloor = (floorObj) => { + return { + floor: bidRequest.floors.values['banner|640x480'], + currency: floorObj.currency, + mediaType: floorObj.mediaType + } + }; + + bidRequest.floors = { + currency: 'EUR', + values: { + 'banner|640x480': 15.07 + } + }; + const request = buildRequest(bidRequest); + expect(request.data.params.bidfloor).to.equal(15.07); + }); + describe('interpretResponse', () => { it('banner: should get correct bid response', () => { const serverResponse = [ diff --git a/test/spec/modules/outbrainBidAdapter_spec.js b/test/spec/modules/outbrainBidAdapter_spec.js index 4bc163aefe6..5dbdd049d82 100644 --- a/test/spec/modules/outbrainBidAdapter_spec.js +++ b/test/spec/modules/outbrainBidAdapter_spec.js @@ -100,6 +100,38 @@ describe('Outbrain Adapter', function () { } expect(spec.isBidRequestValid(bid)).to.equal(false) }) + it('should fail if tag id is not string', function () { + const bid = { + bidder: 'outbrain', + params: { + tagid: 123 + }, + ...nativeBidRequestParams, + } + expect(spec.isBidRequestValid(bid)).to.equal(false) + }) + it('should fail if badv does not include strings', function () { + const bid = { + bidder: 'outbrain', + params: { + tagid: 123, + badv: ['a', 2, 'c'] + }, + ...nativeBidRequestParams, + } + expect(spec.isBidRequestValid(bid)).to.equal(false) + }) + it('should fail if bcat does not include strings', function () { + const bid = { + bidder: 'outbrain', + params: { + tagid: 123, + bcat: ['a', 2, 'c'] + }, + ...nativeBidRequestParams, + } + expect(spec.isBidRequestValid(bid)).to.equal(false) + }) it('should succeed with outbrain config', function () { const bid = { bidder: 'outbrain', @@ -362,6 +394,63 @@ describe('Outbrain Adapter', function () { {source: 'liveramp.com', uids: [{id: 'id-value', atype: 3}]} ]); }); + + it('should pass bidfloor', function () { + const bidRequest = { + ...commonBidRequest, + ...nativeBidRequestParams, + } + bidRequest.getFloor = function() { + return { + currency: 'USD', + floor: 1.23, + } + } + + const res = spec.buildRequests([bidRequest], commonBidderRequest) + const resData = JSON.parse(res.data) + expect(resData.imp[0].bidfloor).to.equal(1.23) + }); + + it('should transform string sizes to numbers', function () { + let bidRequest = { + bidId: 'bidId', + params: {}, + ...commonBidRequest, + ...nativeBidRequestParams, + }; + bidRequest.nativeParams.image.sizes = ['120', '100'] + + const expectedNativeAssets = { + assets: [ + { + required: 1, + id: 3, + img: { + type: 3, + w: 120, + h: 100 + } + }, + { + required: 1, + id: 0, + title: {} + }, + { + required: 0, + id: 5, + data: { + type: 1 + } + } + ] + } + + let res = spec.buildRequests([bidRequest], commonBidderRequest); + const resData = JSON.parse(res.data) + expect(resData.imp[0].native.request).to.equal(JSON.stringify(expectedNativeAssets)); + }); }) describe('interpretResponse', function () { @@ -382,7 +471,7 @@ describe('Outbrain Adapter', function () { impid: '1', price: 1.1, nurl: 'http://example.com/win/${AUCTION_PRICE}', - adm: '{"ver":"1.2","assets":[{"id":3,"required":1,"img":{"url":"http://example.com/img/url","w":120,"h":100}},{"id":0,"required":1,"title":{"text":"Test title"}},{"id":5,"data":{"value":"Test sponsor"}}],"link":{"url":"http://example.com/click/url"},"eventtrackers":[{"event":1,"method":1,"url":"http://example.com/impression"}]}', + adm: '{"ver":"1.2","assets":[{"id":3,"required":1,"img":{"url":"http://example.com/img/url","w":120,"h":100}},{"id":0,"required":1,"title":{"text":"Test title"}},{"id":5,"data":{"value":"Test sponsor"}}],"privacy":"http://example.com/privacy","link":{"url":"http://example.com/click/url"},"eventtrackers":[{"event":1,"method":1,"url":"http://example.com/impression"}]}', adomain: [ 'example.com' ], @@ -435,7 +524,8 @@ describe('Outbrain Adapter', function () { sponsoredBy: 'Test sponsor', impressionTrackers: [ 'http://example.com/impression', - ] + ], + privacyLink: 'http://example.com/privacy' } } ] diff --git a/test/spec/modules/parrableIdSystem_spec.js b/test/spec/modules/parrableIdSystem_spec.js index 44118fb50de..176ba70fbcb 100644 --- a/test/spec/modules/parrableIdSystem_spec.js +++ b/test/spec/modules/parrableIdSystem_spec.js @@ -1,5 +1,5 @@ import { expect } from 'chai'; -import find from 'core-js-pure/features/array/find.js'; +import {find} from 'src/polyfill.js'; import { config } from 'src/config.js'; import * as utils from 'src/utils.js'; import { newStorageManager } from 'src/storageManager.js'; diff --git a/test/spec/modules/pilotxBidAdapter_spec.js b/test/spec/modules/pilotxBidAdapter_spec.js new file mode 100644 index 00000000000..2ef31c0a8f5 --- /dev/null +++ b/test/spec/modules/pilotxBidAdapter_spec.js @@ -0,0 +1,244 @@ +// import or require modules necessary for the test, e.g.: +import { expect } from 'chai'; // may prefer 'assert' in place of 'expect' +import { spec } from '../../../modules/pilotxBidAdapter.js'; + +describe('pilotxAdapter', function () { + describe('isBidRequestValid', function () { + let banner; + beforeEach(function () { + banner = { + bidder: 'pilotx', + adUnitCode: 'adunit-test', + mediaTypes: { banner: {} }, + sizes: [[300, 250], [468, 60]], + bidId: '2de8c82e30665a', + params: { + placementId: '1' + } + }; + }); + + it('should return false if sizes is empty', function () { + banner.sizes = [] + expect(spec.isBidRequestValid(banner)).to.equal(false); + }); + it('should return true if all is valid/ is not empty', function () { + expect(spec.isBidRequestValid(banner)).to.equal(true); + }); + it('should return false if there is no placement id found', function () { + banner.params = {} + expect(spec.isBidRequestValid(banner)).to.equal(false); + }); + it('should return false if sizes is empty', function () { + banner.sizes = [] + expect(spec.isBidRequestValid(banner)).to.equal(false); + }); + it('should return false for no size and empty params', function() { + const emptySizes = { + bidder: 'pilotx', + adUnitCode: 'adunit-test', + mediaTypes: { banner: {} }, + bidId: '2de8c82e30665a', + params: { + placementId: '1', + sizes: [] + } + }; + expect(spec.isBidRequestValid(emptySizes)).to.equal(false); + }) + it('should return true for no size and valid size params', function() { + const emptySizes = { + bidder: 'pilotx', + adUnitCode: 'adunit-test', + mediaTypes: { banner: {} }, + bidId: '2de8c82e30665a', + params: { + placementId: '1', + sizes: [[300, 250], [468, 60]] + } + }; + expect(spec.isBidRequestValid(emptySizes)).to.equal(true); + }) + it('should return false for no size items', function() { + const emptySizes = { + bidder: 'pilotx', + adUnitCode: 'adunit-test', + mediaTypes: { banner: {} }, + bidId: '2de8c82e30665a', + params: { + placementId: '1' + } + }; + expect(spec.isBidRequestValid(emptySizes)).to.equal(false); + }) + }); + + describe('buildRequests', function () { + const mockRequest = { refererInfo: {} }; + const mockRequestGDPR = { + refererInfo: {}, + gdprConsent: { + consentString: 'BOJ/P2HOJ/P2HABABMAAAAAZ+A==', + gdprApplies: true + } + + } + const mockVideo1 = [{ + adUnitCode: 'video1', + auctionId: '01618029-7ae9-4e98-a73a-1ed0c817f414', + bidId: '2a59588c0114fa', + bidRequestsCount: 1, + bidder: 'pilotx', + bidderRequestId: '1f6b4ba2039726', + bidderRequestsCount: 1, + bidderWinsCount: 0, + crumbs: { pubcid: 'de5240ef-ff80-4b55-8837-26a11cfbf64c' }, + mediaTypes: { + video: { + context: 'instream', + mimes: ['video/mp4'], + playbackmethod: [2], + playerSize: [[640, 480]], + protocols: [1, 2, 3, 4, 5, 6, 7, 8], + skip: 1 + } + }, + ortb2Imp: { + ext: { + data: { + pbadslot: 'video1' + } + } + }, + params: { placementId: '379' }, + sizes: [[640, 480]], + src: 'client', + transactionId: 'fec9f2ff-da13-4921-8437-8d679c2be7fe', + }]; + const mockVideo2 = [{ + adUnitCode: 'video1', + auctionId: '01618029-7ae9-4e98-a73a-1ed0c817f414', + bidId: '2a59588c0114fa', + bidRequestsCount: 1, + bidder: 'pilotx', + bidderRequestId: '1f6b4ba2039726', + bidderRequestsCount: 1, + bidderWinsCount: 0, + crumbs: { pubcid: 'de5240ef-ff80-4b55-8837-26a11cfbf64c' }, + mediaTypes: { + video: { + context: 'instream', + mimes: ['video/mp4'], + playbackmethod: [2], + playerSize: [[640, 480]], + protocols: [1, 2, 3, 4, 5, 6, 7, 8], + skip: 1 + } + }, + ortb2Imp: { + ext: { + data: { + pbadslot: 'video1' + } + } + }, + params: { placementId: '379' }, + sizes: [640, 480], + src: 'client', + transactionId: 'fec9f2ff-da13-4921-8437-8d679c2be7fe', + }]; + it('should return correct response', function () { + const builtRequest = spec.buildRequests(mockVideo1, mockRequest) + let builtRequestData = builtRequest.data + let data = JSON.parse(builtRequestData) + expect(data['379'].bidId).to.equal(mockVideo1[0].bidId) + }); + it('should return correct response for only array of size', function () { + const builtRequest = spec.buildRequests(mockVideo2, mockRequest) + let builtRequestData = builtRequest.data + let data = JSON.parse(builtRequestData) + expect(data['379'].sizes[0][0]).to.equal(mockVideo2[0].sizes[0]) + expect(data['379'].sizes[0][1]).to.equal(mockVideo2[0].sizes[1]) + }); + it('should be valid and pass gdpr items correctly', function () { + const builtRequest = spec.buildRequests(mockVideo2, mockRequestGDPR) + let builtRequestData = builtRequest.data + let data = JSON.parse(builtRequestData) + expect(data['379'].gdprConsentString).to.equal(mockRequestGDPR.gdprConsent.consentString) + expect(data['379'].gdprConsentRequired).to.equal(mockRequestGDPR.gdprConsent.gdprApplies) + }); + }); + describe('interpretResponse', function () { + const bidRequest = {} + const serverResponse = { + cpm: 2.5, + creativeId: 'V9060', + currency: 'US', + height: 480, + mediaType: 'video', + netRevenue: false, + requestId: '273b39c74069cb', + ttl: 3000, + vastUrl: 'http://testadserver.com/ads?&k=60cd901ad8ab70c9cedf373cb17b93b8&pid=379&tid=91342717', + width: 640 + } + const serverResponseVideo = { + body: serverResponse + } + const serverResponse2 = { + cpm: 2.5, + creativeId: 'V9060', + currency: 'US', + height: 480, + mediaType: 'banner', + netRevenue: false, + requestId: '273b39c74069cb', + ttl: 3000, + vastUrl: 'http://testadserver.com/ads?&k=60cd901ad8ab70c9cedf373cb17b93b8&pid=379&tid=91342717', + width: 640 + } + const serverResponseBanner = { + body: serverResponse2 + } + it('should be valid from bidRequest for video', function () { + const bidResponses = spec.interpretResponse(serverResponseVideo, bidRequest) + expect(bidResponses[0].requestId).to.equal(serverResponse.requestId) + expect(bidResponses[0].cpm).to.equal(serverResponse.cpm) + expect(bidResponses[0].width).to.equal(serverResponse.width) + expect(bidResponses[0].height).to.equal(serverResponse.height) + expect(bidResponses[0].creativeId).to.equal(serverResponse.creativeId) + expect(bidResponses[0].currency).to.equal(serverResponse.currency) + expect(bidResponses[0].netRevenue).to.equal(serverResponse.netRevenue) + expect(bidResponses[0].ttl).to.equal(serverResponse.ttl) + expect(bidResponses[0].vastUrl).to.equal(serverResponse.vastUrl) + expect(bidResponses[0].mediaType).to.equal(serverResponse.mediaType) + expect(bidResponses[0].meta.mediaType).to.equal(serverResponse.mediaType) + }); + it('should be valid from bidRequest for banner', function () { + const bidResponses = spec.interpretResponse(serverResponseBanner, bidRequest) + expect(bidResponses[0].requestId).to.equal(serverResponse2.requestId) + expect(bidResponses[0].cpm).to.equal(serverResponse2.cpm) + expect(bidResponses[0].width).to.equal(serverResponse2.width) + expect(bidResponses[0].height).to.equal(serverResponse2.height) + expect(bidResponses[0].creativeId).to.equal(serverResponse2.creativeId) + expect(bidResponses[0].currency).to.equal(serverResponse2.currency) + expect(bidResponses[0].netRevenue).to.equal(serverResponse2.netRevenue) + expect(bidResponses[0].ttl).to.equal(serverResponse2.ttl) + expect(bidResponses[0].ad).to.equal(serverResponse2.ad) + expect(bidResponses[0].mediaType).to.equal(serverResponse2.mediaType) + expect(bidResponses[0].meta.mediaType).to.equal(serverResponse2.mediaType) + }); + }); + describe('setPlacementID', function () { + const multiplePlacementIds = ['380', '381'] + it('should be valid with an array of placement ids passed', function () { + const placementID = spec.setPlacementID(multiplePlacementIds) + expect(placementID).to.equal('380#381') + }); + it('should be valid with single placement ID passed', function () { + const placementID = spec.setPlacementID('381') + expect(placementID).to.equal('381') + }); + }); + // Add other `describe` or `it` blocks as necessary +}); diff --git a/test/spec/modules/prebidServerBidAdapter_spec.js b/test/spec/modules/prebidServerBidAdapter_spec.js index 73a531b30bf..7b297aa4c5a 100644 --- a/test/spec/modules/prebidServerBidAdapter_spec.js +++ b/test/spec/modules/prebidServerBidAdapter_spec.js @@ -4,11 +4,19 @@ import adapterManager from 'src/adapterManager.js'; import * as utils from 'src/utils.js'; import { ajax } from 'src/ajax.js'; import { config } from 'src/config.js'; -import events from 'src/events.js'; +import * as events from 'src/events.js'; import CONSTANTS from 'src/constants.json'; import { server } from 'test/mocks/xhr.js'; import { createEidsArray } from 'modules/userId/eids.js'; import {deepAccess, deepClone} from 'src/utils.js'; +import 'modules/appnexusBidAdapter.js' // appnexus alias test +import 'modules/rubiconBidAdapter.js' // rubicon alias test +import 'src/prebid.js' // $$PREBID_GLOBAL$$.aliasBidder test +import 'modules/currency.js' // adServerCurrency test +import {hook} from '../../../src/hook.js'; +import {decorateAdUnitsWithNativeParams} from '../../../src/native.js'; +import {auctionManager} from '../../../src/auctionManager.js'; +import {stubAuctionIndex} from '../../helpers/indexStub.js'; let CONFIG = { accountId: '1', @@ -448,6 +456,16 @@ describe('S2S Adapter', function () { addBidResponse = sinon.spy(), done = sinon.spy(); + function prepRequest(req) { + req.ad_units.forEach((adUnit) => { delete adUnit.nativeParams }); + decorateAdUnitsWithNativeParams(req.ad_units); + } + + before(() => { + hook.ready(); + prepRequest(REQUEST); + }); + beforeEach(function () { config.resetConfig(); adapter = new Adapter(); @@ -1096,6 +1114,7 @@ describe('S2S Adapter', function () { it('should not include ext.aspectratios if adunit\'s aspect_ratios do not define radio_width and ratio_height', () => { const req = deepClone(REQUEST); req.ad_units[0].mediaTypes.native.icon.aspect_ratios[0] = {'min_width': 1, 'min_height': 2}; + prepRequest(req); adapter.callBids(req, BID_REQUESTS, addBidResponse, done, ajax); const nativeReq = JSON.parse(JSON.parse(server.requests[0].requestBody).imp[0].native.request); const icons = nativeReq.assets.map((a) => a.img).filter((img) => img && img.type === 1); @@ -2404,11 +2423,8 @@ describe('S2S Adapter', function () { }); it('handles OpenRTB native responses', function () { - sinon.stub(utils, 'getBidRequest').returns({ - adUnitCode: 'div-gpt-ad-1460505748561-0', - bidder: 'appnexus', - bidId: '123' - }); + const stub = sinon.stub(auctionManager, 'index'); + stub.get(() => stubAuctionIndex({adUnits: REQUEST.ad_units})); const s2sConfig = Object.assign({}, CONFIG, { endpoint: { p1Consent: 'https://prebidserverurl/openrtb2/auction?querystring=param' @@ -2431,7 +2447,51 @@ describe('S2S Adapter', function () { expect(response).to.have.property('requestId', '123'); expect(response).to.have.property('cpm', 10); - utils.getBidRequest.restore(); + stub.restore(); + }); + + it('does not (by default) allow bids that were not requested', function () { + config.setConfig({ s2sConfig: CONFIG }); + adapter.callBids(REQUEST, BID_REQUESTS, addBidResponse, done, ajax); + const response = deepClone(RESPONSE_OPENRTB); + response.seatbid[0].seat = 'unknown'; + server.requests[0].respond(200, {}, JSON.stringify(response)); + + expect(addBidResponse.called).to.be.false; + }); + + it('allows unrequested bids if config.allowUnknownBidderCodes', function () { + const cfg = {...CONFIG, allowUnknownBidderCodes: true}; + config.setConfig({s2sConfig: cfg}); + adapter.callBids({...REQUEST, s2sConfig: cfg}, BID_REQUESTS, addBidResponse, done, ajax); + const response = deepClone(RESPONSE_OPENRTB); + response.seatbid[0].seat = 'unknown'; + server.requests[0].respond(200, {}, JSON.stringify(response)); + + expect(addBidResponse.calledWith(sinon.match.any, sinon.match({bidderCode: 'unknown'}))).to.be.true; + }); + + it('uses "null" request\'s ID for all responses, when a null request is present', function () { + const cfg = {...CONFIG, allowUnknownBidderCodes: true}; + config.setConfig({s2sConfig: cfg}); + const req = {...REQUEST, s2sConfig: cfg, ad_units: [{...REQUEST.ad_units[0], bids: [{bidder: null, bid_id: 'testId'}]}]}; + const bidReq = {...BID_REQUESTS[0], bidderCode: null, bids: [{...BID_REQUESTS[0].bids[0], bidder: null, bidId: 'testId'}]} + adapter.callBids(req, [bidReq], addBidResponse, done, ajax); + const response = deepClone(RESPONSE_OPENRTB); + response.seatbid[0].seat = 'storedImpression'; + server.requests[0].respond(200, {}, JSON.stringify(response)); + sinon.assert.calledWith(addBidResponse, sinon.match.any, sinon.match({bidderCode: 'storedImpression', requestId: 'testId'})) + }); + + it('copies ortb2Imp to response when there is only a null bid', () => { + const cfg = {...CONFIG}; + config.setConfig({s2sConfig: cfg}); + const ortb2Imp = {ext: {prebid: {storedrequest: 'value'}}}; + const req = {...REQUEST, s2sConfig: cfg, ad_units: [{...REQUEST.ad_units[0], bids: [{bidder: null, bid_id: 'testId'}], ortb2Imp}]}; + const bidReq = {...BID_REQUESTS[0], bidderCode: null, bids: [{...BID_REQUESTS[0].bids[0], bidder: null, bidId: 'testId'}]} + adapter.callBids(req, [bidReq], addBidResponse, done, ajax); + const actual = JSON.parse(server.requests[0].requestBody); + sinon.assert.match(actual.imp[0], sinon.match(ortb2Imp)); }); describe('on sync requested with no cookie', () => { @@ -2586,21 +2646,6 @@ describe('S2S Adapter', function () { sinon.assert.calledOnce(logErrorSpy); }); - it('should log an error when bidders is missing', function () { - const options = { - accountId: '1', - enabled: true, - timeout: 1000, - adapter: 's2s', - endpoint: { - p1Consent: 'https://prebid.adnxs.com/pbs/v1/openrtb2/auction' - } - }; - - config.setConfig({ s2sConfig: options }); - sinon.assert.calledOnce(logErrorSpy); - }); - it('should log an error when endpoint is missing', function () { const options = { accountId: '1', @@ -2873,6 +2918,24 @@ describe('S2S Adapter', function () { expect(requestBid.coopSync).to.be.undefined; }); + it('should set imp banner if ortb2Imp.banner is present', function() { + const consentConfig = { s2sConfig: CONFIG }; + config.setConfig(consentConfig); + const bidRequest = utils.deepClone(REQUEST); + bidRequest.ad_units[0].ortb2Imp = { + banner: { + api: 7 + }, + instl: 1 + }; + + adapter.callBids(bidRequest, BID_REQUESTS, addBidResponse, done, ajax); + const parsedRequestBody = JSON.parse(server.requests[0].requestBody); + + expect(parsedRequestBody.imp[0].banner.api).to.equal(7); + expect(parsedRequestBody.imp[0].instl).to.equal(1); + }); + it('adds debug flag', function () { config.setConfig({debug: true}); diff --git a/test/spec/modules/priceFloors_spec.js b/test/spec/modules/priceFloors_spec.js index b3105dafc39..6ea58e8c47a 100644 --- a/test/spec/modules/priceFloors_spec.js +++ b/test/spec/modules/priceFloors_spec.js @@ -14,8 +14,12 @@ import { fieldMatchingFunctions, allowedFields } from 'modules/priceFloors.js'; -import events from 'src/events.js'; +import * as events from 'src/events.js'; import * as mockGpt from '../integration/faker/googletag.js'; +import 'src/prebid.js'; +import {createBid} from '../../../src/bidfactory.js'; +import {auctionManager} from '../../../src/auctionManager.js'; +import {stubAuctionIndex} from '../../helpers/indexStub.js'; describe('the price floors module', function () { let logErrorSpy; @@ -109,6 +113,7 @@ describe('the price floors module', function () { bidder: 'rubicon', adUnitCode: 'test_div_1', auctionId: '1234-56-789', + transactionId: 'tr_test_div_1' }; function getAdUnitMock(code = 'adUnit-code') { @@ -225,6 +230,44 @@ describe('the price floors module', function () { }); describe('getFirstMatchingFloor', function () { + it('uses a 0 floor as overrite', function () { + let inputFloorData = { + currency: 'USD', + schema: { + delimiter: '|', + fields: ['adUnitCode'] + }, + values: { + 'test_div_1': 0, + 'test_div_2': 2 + }, + default: 0.5 + }; + + expect(getFirstMatchingFloor(inputFloorData, basicBidRequest, {mediaType: 'banner', size: '*'})).to.deep.equal({ + floorMin: 0, + floorRuleValue: 0, + matchingFloor: 0, + matchingData: 'test_div_1', + matchingRule: 'test_div_1' + }); + + expect(getFirstMatchingFloor(inputFloorData, {...basicBidRequest, adUnitCode: 'test_div_2'}, {mediaType: 'banner', size: '*'})).to.deep.equal({ + floorMin: 0, + floorRuleValue: 2, + matchingFloor: 2, + matchingData: 'test_div_2', + matchingRule: 'test_div_2' + }); + + expect(getFirstMatchingFloor(inputFloorData, {...basicBidRequest, adUnitCode: 'test_div_3'}, {mediaType: 'banner', size: '*'})).to.deep.equal({ + floorMin: 0, + floorRuleValue: 0.5, + matchingFloor: 0.5, + matchingData: 'test_div_3', + matchingRule: undefined + }); + }); it('selects the right floor for different mediaTypes', function () { // banner with * size (not in rule file so does not do anything) expect(getFirstMatchingFloor({...basicFloorData}, basicBidRequest, {mediaType: 'banner', size: '*'})).to.deep.equal({ @@ -401,6 +444,7 @@ describe('the price floors module', function () { }); describe('with gpt enabled', function () { let gptFloorData; + let indexStub, adUnits; beforeEach(function () { gptFloorData = { currency: 'USD', @@ -426,10 +470,13 @@ describe('the price floors module', function () { code: '/12345/sports/basketball', divId: 'test_div_2' }); + indexStub = sinon.stub(auctionManager, 'index'); + indexStub.get(() => stubAuctionIndex({adUnits})) }); afterEach(function () { // reset it so no lingering stuff from other test specs mockGpt.reset(); + indexStub.restore(); }); it('picks the right rule when looking for gptSlot', function () { expect(getFirstMatchingFloor(gptFloorData, basicBidRequest)).to.deep.equal({ @@ -449,9 +496,10 @@ describe('the price floors module', function () { matchingRule: '/12345/sports/basketball' }); }); - it('picks the gptSlot from the bidRequest and does not call the slotMatching', function () { - const newBidRequest1 = { ...basicBidRequest }; - utils.deepSetValue(newBidRequest1, 'ortb2Imp.ext.data.adserver', { + it('picks the gptSlot from the adUnit and does not call the slotMatching', function () { + const newBidRequest1 = { ...basicBidRequest, transactionId: 'au1' }; + adUnits = [{code: newBidRequest1.code, transactionId: 'au1'}]; + utils.deepSetValue(adUnits[0], 'ortb2Imp.ext.data.adserver', { name: 'gam', adslot: '/12345/news/politics' }) @@ -463,8 +511,9 @@ describe('the price floors module', function () { matchingRule: '/12345/news/politics' }); - const newBidRequest2 = { ...basicBidRequest, adUnitCode: 'test_div_2' }; - utils.deepSetValue(newBidRequest2, 'ortb2Imp.ext.data.adserver', { + const newBidRequest2 = { ...basicBidRequest, adUnitCode: 'test_div_2', transactionId: 'au2' }; + adUnits = [{code: newBidRequest2.adUnitCode, transactionId: newBidRequest2.transactionId}]; + utils.deepSetValue(adUnits[0], 'ortb2Imp.ext.data.adserver', { name: 'gam', adslot: '/12345/news/weather' }) @@ -1565,17 +1614,12 @@ describe('the price floors module', function () { }); }); describe('bidResponseHook tests', function () { - let returnedBidResponse; - let bidderRequest = { - bidderCode: 'appnexus', - auctionId: '123456', - bids: [{ - bidder: 'appnexus', - adUnitCode: 'test_div_1', - auctionId: '123456', - bidId: '1111' - }] - }; + const AUCTION_ID = '123456'; + let returnedBidResponse, indexStub; + let adUnit = { + transactionId: 'au', + code: 'test_div_1' + } let basicBidResponse = { bidderCode: 'appnexus', width: 300, @@ -1583,38 +1627,46 @@ describe('the price floors module', function () { cpm: 0.5, mediaType: 'banner', requestId: '1111', + transactionId: 'au', }; beforeEach(function () { returnedBidResponse = {}; + indexStub = sinon.stub(auctionManager, 'index'); + indexStub.get(() => stubAuctionIndex({adUnits: [adUnit]})); + }); + + afterEach(() => { + indexStub.restore(); }); + function runBidResponse(bidResp = basicBidResponse) { let next = (adUnitCode, bid) => { returnedBidResponse = bid; }; - addBidResponseHook.bind({ bidderRequest })(next, bidResp.adUnitCode, bidResp); + addBidResponseHook(next, bidResp.adUnitCode, Object.assign(createBid(CONSTANTS.STATUS.GOOD, {auctionId: AUCTION_ID}), bidResp)); }; it('continues with the auction if not floors data is present without any flooring', function () { runBidResponse(); expect(returnedBidResponse).to.not.haveOwnProperty('floorData'); }); it('if no matching rule it should not floor and should call log warn', function () { - _floorDataForAuction[bidderRequest.auctionId] = utils.deepClone(basicFloorConfig); - _floorDataForAuction[bidderRequest.auctionId].data.values = { 'video': 1.0 }; + _floorDataForAuction[AUCTION_ID] = utils.deepClone(basicFloorConfig); + _floorDataForAuction[AUCTION_ID].data.values = { 'video': 1.0 }; runBidResponse(); expect(returnedBidResponse).to.not.haveOwnProperty('floorData'); expect(logWarnSpy.calledOnce).to.equal(true); }); it('if it finds a rule and floors should update the bid accordingly', function () { - _floorDataForAuction[bidderRequest.auctionId] = utils.deepClone(basicFloorConfig); - _floorDataForAuction[bidderRequest.auctionId].data.values = { 'banner': 1.0 }; + _floorDataForAuction[AUCTION_ID] = utils.deepClone(basicFloorConfig); + _floorDataForAuction[AUCTION_ID].data.values = { 'banner': 1.0 }; runBidResponse(); expect(returnedBidResponse).to.haveOwnProperty('floorData'); expect(returnedBidResponse.status).to.equal(CONSTANTS.BID_STATUS.BID_REJECTED); expect(returnedBidResponse.cpm).to.equal(0); }); it('if it finds a rule and does not floor should update the bid accordingly', function () { - _floorDataForAuction[bidderRequest.auctionId] = utils.deepClone(basicFloorConfig); - _floorDataForAuction[bidderRequest.auctionId].data.values = { 'banner': 0.3 }; + _floorDataForAuction[AUCTION_ID] = utils.deepClone(basicFloorConfig); + _floorDataForAuction[AUCTION_ID].data.values = { 'banner': 0.3 }; runBidResponse(); expect(returnedBidResponse).to.haveOwnProperty('floorData'); expect(returnedBidResponse.floorData).to.deep.equal({ @@ -1636,7 +1688,7 @@ describe('the price floors module', function () { expect(returnedBidResponse.cpm).to.equal(0.5); }); it('if should work with more complex rules and update accordingly', function () { - _floorDataForAuction[bidderRequest.auctionId] = { + _floorDataForAuction[AUCTION_ID] = { ...basicFloorConfig, data: { currency: 'USD', @@ -1722,4 +1774,49 @@ describe('the price floors module', function () { expect(_floorDataForAuction[AUCTION_END_EVENT.auctionId]).to.be.undefined; }); }); + + describe('fieldMatchingFunctions', () => { + let sandbox; + + const req = { + ...basicBidRequest, + } + + const resp = { + transactionId: req.transactionId, + size: [100, 100], + mediaType: 'banner', + } + + beforeEach(() => { + sandbox = sinon.sandbox.create(); + sandbox.stub(auctionManager, 'index').get(() => stubAuctionIndex({ + adUnits: [ + { + code: req.adUnitCode, + transactionId: req.transactionId, + ortb2Imp: {ext: {data: {adserver: {name: 'gam', adslot: 'slot'}}}} + } + ] + })); + }); + + afterEach(() => { + sandbox.restore(); + }) + + Object.entries({ + size: '100x100', + mediaType: resp.mediaType, + gptSlot: 'slot', + domain: 'localhost', + adUnitCode: req.adUnitCode, + }).forEach(([test, expected]) => { + describe(`${test}`, () => { + it('should work with only bidResponse', () => { + expect(fieldMatchingFunctions[test](undefined, resp)).to.eql(expected) + }) + }); + }) + }); }); diff --git a/test/spec/modules/publinkIdSystem_spec.js b/test/spec/modules/publinkIdSystem_spec.js index cfb5f8ed135..4656afe1585 100644 --- a/test/spec/modules/publinkIdSystem_spec.js +++ b/test/spec/modules/publinkIdSystem_spec.js @@ -5,7 +5,7 @@ import sinon from 'sinon'; import {uspDataHandler} from '../../../src/adapterManager'; import {parseUrl} from '../../../src/utils'; -export const storage = getStorageManager(24); +export const storage = getStorageManager({gvlid: 24}); const TEST_COOKIE_VALUE = 'cookievalue'; describe('PublinkIdSystem', () => { describe('decode', () => { diff --git a/test/spec/modules/pubmaticAnalyticsAdapter_spec.js b/test/spec/modules/pubmaticAnalyticsAdapter_spec.js index b6268548ccc..c60b08ae972 100755 --- a/test/spec/modules/pubmaticAnalyticsAdapter_spec.js +++ b/test/spec/modules/pubmaticAnalyticsAdapter_spec.js @@ -7,7 +7,6 @@ import { addBidResponseHook, } from 'modules/currency.js'; -// using es6 "import * as events from 'src/events'" causes the events.getEvents stub not to work... let events = require('src/events'); let ajax = require('src/ajax'); let utils = require('src/utils'); diff --git a/test/spec/modules/pubmaticBidAdapter_spec.js b/test/spec/modules/pubmaticBidAdapter_spec.js index 9696501437b..64e95460321 100644 --- a/test/spec/modules/pubmaticBidAdapter_spec.js +++ b/test/spec/modules/pubmaticBidAdapter_spec.js @@ -3861,4 +3861,80 @@ describe('PubMatic adapter', function () { }) }); }); + + describe('Video request params', function() { + let sandbox, utilsMock, newVideoRequest; + beforeEach(() => { + utilsMock = sinon.mock(utils); + sandbox = sinon.sandbox.create(); + sandbox.spy(utils, 'logWarn'); + newVideoRequest = utils.deepClone(videoBidRequests) + }); + + afterEach(() => { + utilsMock.restore(); + sandbox.restore(); + }) + + it('Should log warning if video params from mediaTypes and params obj of bid are not present', function () { + delete newVideoRequest[0].mediaTypes.video; + delete newVideoRequest[0].params.video; + + let request = spec.buildRequests(newVideoRequest, { + auctionId: 'new-auction-id' + }); + + sinon.assert.calledOnce(utils.logWarn); + expect(request).to.equal(undefined); + }); + + it('Should consider video params from mediaType object of bid', function () { + delete newVideoRequest[0].params.video; + + let request = spec.buildRequests(newVideoRequest, { + auctionId: 'new-auction-id' + }); + let data = JSON.parse(request.data); + expect(data.imp[0].video).to.exist; + expect(data.imp[0]['video']['w']).to.equal(videoBidRequests[0].mediaTypes.video.playerSize[0]); + expect(data.imp[0]['video']['h']).to.equal(videoBidRequests[0].mediaTypes.video.playerSize[1]); + expect(data.imp[0]['video']['battr']).to.equal(undefined); + }); + }); + + describe('GroupM params', function() { + let sandbox, utilsMock, newBidRequests, newBidResponses; + beforeEach(() => { + utilsMock = sinon.mock(utils); + sandbox = sinon.sandbox.create(); + sandbox.spy(utils, 'logInfo'); + newBidRequests = utils.deepClone(bidRequests) + newBidRequests[0].bidder = 'groupm'; + newBidResponses = utils.deepClone(bidResponses); + newBidResponses.body.seatbid[0].bid[0].ext.marketplace = 'groupm' + }); + + afterEach(() => { + utilsMock.restore(); + sandbox.restore(); + }) + + it('Should log info when bidder is groupm and return', function () { + let request = spec.buildRequests(newBidRequests, {bidderCode: 'groupm', + auctionId: 'new-auction-id' + }); + sinon.assert.calledOnce(utils.logInfo); + expect(request).to.equal(undefined); + }); + + it('Should add bidder code & bidder as groupm for marketplace groupm response', function () { + let request = spec.buildRequests(newBidRequests, { + auctionId: 'new-auction-id' + }); + let response = spec.interpretResponse(newBidResponses, request); + expect(response).to.be.an('array').with.length.above(0); + expect(response[0].bidderCode).to.equal('groupm'); + expect(response[0].bidder).to.equal('groupm'); + }); + }); }); diff --git a/test/spec/modules/pubstackAnalyticsAdapter_spec.js b/test/spec/modules/pubstackAnalyticsAdapter_spec.js index e3db334c888..4df25f1665b 100644 --- a/test/spec/modules/pubstackAnalyticsAdapter_spec.js +++ b/test/spec/modules/pubstackAnalyticsAdapter_spec.js @@ -1,7 +1,7 @@ import * as utils from 'src/utils.js'; import pubstackAnalytics from '../../../modules/pubstackAnalyticsAdapter.js'; import adapterManager from 'src/adapterManager'; -import events from 'src/events'; +import * as events from 'src/events'; import constants from 'src/constants.json' describe('Pubstack Analytics Adapter', () => { diff --git a/test/spec/modules/pubxaiAnalyticsAdapter_spec.js b/test/spec/modules/pubxaiAnalyticsAdapter_spec.js index e6a1c063a86..63364b867be 100644 --- a/test/spec/modules/pubxaiAnalyticsAdapter_spec.js +++ b/test/spec/modules/pubxaiAnalyticsAdapter_spec.js @@ -636,6 +636,11 @@ describe('pubxai analytics adapter', function() { 'statusMessage': 'Bid available', 'timeToRespond': 267 }, + 'pageDetail': { + 'host': location.host, + 'path': location.pathname, + 'search': location.search + }, 'deviceDetail': { 'platform': navigator.platform, 'deviceType': getDeviceType(), diff --git a/test/spec/modules/readpeakBidAdapter_spec.js b/test/spec/modules/readpeakBidAdapter_spec.js index eefd7792a7c..04358fad52b 100644 --- a/test/spec/modules/readpeakBidAdapter_spec.js +++ b/test/spec/modules/readpeakBidAdapter_spec.js @@ -4,9 +4,13 @@ import { config } from 'src/config.js'; import { parseUrl } from 'src/utils.js'; describe('ReadPeakAdapter', function() { - let bidRequest; - let serverResponse; - let serverRequest; + let baseBidRequest; + let bannerBidRequest; + let nativeBidRequest; + let nativeServerResponse; + let nativeServerRequest; + let bannerServerResponse; + let bannerServerRequest; let bidderRequest; beforeEach(function() { @@ -16,15 +20,8 @@ describe('ReadPeakAdapter', function() { } }; - bidRequest = { + baseBidRequest = { bidder: 'readpeak', - nativeParams: { - title: { required: true, len: 200 }, - image: { wmin: 100 }, - sponsoredBy: {}, - body: { required: false }, - cta: { required: false } - }, params: { bidfloor: 5.0, publisherId: '11bc5dd5-7421-4dd8-c926-40fa653bec76', @@ -34,17 +31,46 @@ describe('ReadPeakAdapter', function() { bidId: '2ffb201a808da7', bidderRequestId: '178e34bad3658f', auctionId: 'c45dd708-a418-42ec-b8a7-b70a6c6fab0a', - transactionId: 'd45dd707-a418-42ec-b8a7-b70a6c6fab0b' + transactionId: 'd45dd707-a418-42ec-b8a7-b70a6c6fab0b', + }; + + nativeBidRequest = { + ...baseBidRequest, + nativeParams: { + title: { required: true, len: 200 }, + image: { wmin: 100 }, + sponsoredBy: {}, + body: { required: false }, + cta: { required: false } + }, + mediaTypes: { + native: { + title: { required: true, len: 200 }, + image: { wmin: 100 }, + sponsoredBy: {}, + body: { required: false }, + cta: { required: false } + }, + } }; - serverResponse = { - id: bidRequest.bidderRequestId, + bannerBidRequest = { + ...baseBidRequest, + mediaTypes: { + banner: { + sizes: [[640, 320], [300, 600]], + } + }, + sizes: [[640, 320], [300, 600]], + } + nativeServerResponse = { + id: baseBidRequest.bidderRequestId, cur: 'USD', seatbid: [ { bid: [ { - id: 'bidRequest.bidId', - impid: bidRequest.bidId, + id: 'baseBidRequest.bidId', + impid: baseBidRequest.bidId, price: 0.12, cid: '12', crid: '123', @@ -91,7 +117,30 @@ describe('ReadPeakAdapter', function() { } ] }; - serverRequest = { + bannerServerResponse = { + id: baseBidRequest.bidderRequestId, + cur: 'USD', + seatbid: [ + { + bid: [ + { + id: 'baseBidRequest.bidId', + impid: baseBidRequest.bidId, + price: 0.12, + cid: '12', + crid: '123', + adomain: ['readpeak.com'], + adm: '', + burl: 'https://localhost:8081/url/b?d=0O95O4326I528Ie4d39f94-533d-4577-a579-585fd4c02b0aI0I352e303232363639333139393939393939&c=USD&p=${AUCTION_PRICE}&bad=0-0-95O0O0OdO640360&gc=0', + nurl: 'https://localhost:8081/url/n?d=0O95O4326I528Ie4d39f94-533d-4577-a579-585fd4c02b0aI0I352e303232363639333139393939393939&gc=0', + w: 640, + h: 360, + } + ] + } + ] + }; + nativeServerRequest = { method: 'POST', url: 'http://localhost:60080/header/prebid', data: JSON.stringify({ @@ -101,7 +150,7 @@ describe('ReadPeakAdapter', function() { id: '2ffb201a808da7', native: { request: - '{"assets":[{"id":1,"required":1,"title":{"len":200}},{"id":2,"required":0,"data":{"type":1,"len":50}},{"id":3,"required":0,"img":{"type":3,"wmin":100,"hmin":150}}]}', + '{\"assets\":[{\"id\":1,\"required\":1,\"title\":{\"len\":70}},{\"id\":2,\"required\":1,\"img\":{\"type\":3,\"wmin\":150,\"hmin\":150}},{\"id\":4,\"required\":1,\"data\":{\"type\":2,\"len\":120}}]}', ver: '1.1' }, bidfloor: 5, @@ -127,175 +176,362 @@ describe('ReadPeakAdapter', function() { isPrebid: true }) }; + bannerServerRequest = { + method: 'POST', + url: 'http://localhost:60080/header/prebid', + data: JSON.stringify({ + id: '178e34bad3658f', + imp: [ + { + id: '2ffb201a808da7', + bidfloor: 5, + bidfloorcur: 'USD', + tagId: 'test-tag-1', + banner: { + w: 640, + h: 360, + format: [ + { w: 640, h: 360 }, + { w: 320, h: 320 }, + ] + } + } + ], + site: { + publisher: { + id: '11bc5dd5-7421-4dd8-c926-40fa653bec76' + }, + id: '11bc5dd5-7421-4dd8-c926-40fa653bec77', + ref: '', + page: 'http://localhost', + domain: 'localhost' + }, + app: null, + device: { + ua: + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/61.0.3163.100 Safari/537.36', + language: 'en-US' + }, + isPrebid: true + }) + }; }); - describe('spec.isBidRequestValid', function() { - it('should return true when the required params are passed', function() { - expect(spec.isBidRequestValid(bidRequest)).to.equal(true); - }); + describe('Native', function() { + describe('spec.isBidRequestValid', function() { + it('should return true when the required params are passed', function() { + expect(spec.isBidRequestValid(nativeBidRequest)).to.equal(true); + }); - it('should return false when the native params are missing', function() { - bidRequest.nativeParams = undefined; - expect(spec.isBidRequestValid(bidRequest)).to.equal(false); - }); + it('should return false when the "publisherId" param is missing', function() { + nativeBidRequest.params = { + bidfloor: 5.0 + }; + expect(spec.isBidRequestValid(nativeBidRequest)).to.equal(false); + }); + + it('should return false when no bid params are passed', function() { + nativeBidRequest.params = {}; + expect(spec.isBidRequestValid(nativeBidRequest)).to.equal(false); + }); - it('should return false when the "publisherId" param is missing', function() { - bidRequest.params = { - bidfloor: 5.0 - }; - expect(spec.isBidRequestValid(bidRequest)).to.equal(false); + it('should return false when a bid request is not passed', function() { + expect(spec.isBidRequestValid()).to.equal(false); + expect(spec.isBidRequestValid({})).to.equal(false); + }); }); - it('should return false when no bid params are passed', function() { - bidRequest.params = {}; - expect(spec.isBidRequestValid(bidRequest)).to.equal(false); + describe('spec.buildRequests', function() { + it('should create a POST request for every bid', function() { + const request = spec.buildRequests([nativeBidRequest], bidderRequest); + expect(request.method).to.equal('POST'); + expect(request.url).to.equal(ENDPOINT); + }); + + it('should attach request data', function() { + config.setConfig({ + currency: { + adServerCurrency: 'EUR' + } + }); + + const request = spec.buildRequests([nativeBidRequest], bidderRequest); + + const data = JSON.parse(request.data); + + expect(data.source.ext.prebid).to.equal('$prebid.version$'); + expect(data.id).to.equal(nativeBidRequest.bidderRequestId); + expect(data.imp[0].bidfloor).to.equal(nativeBidRequest.params.bidfloor); + expect(data.imp[0].bidfloorcur).to.equal('USD'); + expect(data.imp[0].tagId).to.equal('test-tag-1'); + expect(data.site.publisher.id).to.equal(nativeBidRequest.params.publisherId); + expect(data.site.id).to.equal(nativeBidRequest.params.siteId); + expect(data.site.page).to.equal(bidderRequest.refererInfo.referer); + expect(data.site.domain).to.equal(parseUrl(bidderRequest.refererInfo.referer).hostname); + expect(data.device).to.deep.contain({ + ua: navigator.userAgent, + language: navigator.language + }); + expect(data.cur).to.deep.equal(['EUR']); + expect(data.user).to.be.undefined; + expect(data.regs).to.be.undefined; + }); + + it('should get bid floor from module', function() { + const floorModuleData = { + currency: 'USD', + floor: 3.2, + } + nativeBidRequest.getFloor = function () { + return floorModuleData + } + const request = spec.buildRequests([nativeBidRequest], bidderRequest); + + const data = JSON.parse(request.data); + + expect(data.source.ext.prebid).to.equal('$prebid.version$'); + expect(data.id).to.equal(nativeBidRequest.bidderRequestId); + expect(data.imp[0].bidfloor).to.equal(floorModuleData.floor); + expect(data.imp[0].bidfloorcur).to.equal(floorModuleData.currency); + }); + + it('should send gdpr data when gdpr does not apply', function() { + const gdprData = { + gdprConsent: { + gdprApplies: false, + consentString: undefined, + } + } + const request = spec.buildRequests([nativeBidRequest], {...bidderRequest, ...gdprData}); + + const data = JSON.parse(request.data); + + expect(data.user).to.deep.equal({ + ext: { + consent: '' + } + }); + expect(data.regs).to.deep.equal({ + ext: { + gdpr: false + } + }); + }); + + it('should send gdpr data when gdpr applies', function() { + const tcString = 'sometcstring'; + const gdprData = { + gdprConsent: { + gdprApplies: true, + consentString: tcString + } + } + const request = spec.buildRequests([nativeBidRequest], {...bidderRequest, ...gdprData}); + + const data = JSON.parse(request.data); + + expect(data.user).to.deep.equal({ + ext: { + consent: tcString + } + }); + expect(data.regs).to.deep.equal({ + ext: { + gdpr: true + } + }); + }); }); - it('should return false when a bid request is not passed', function() { - expect(spec.isBidRequestValid()).to.equal(false); - expect(spec.isBidRequestValid({})).to.equal(false); + describe('spec.interpretResponse', function() { + it('should return no bids if the response is not valid', function() { + const bidResponse = spec.interpretResponse({ body: null }, nativeServerRequest); + expect(bidResponse.length).to.equal(0); + }); + + it('should return a valid bid response', function() { + const bidResponse = spec.interpretResponse( + { body: nativeServerResponse }, + nativeServerRequest + )[0]; + expect(bidResponse).to.contain({ + requestId: nativeBidRequest.bidId, + cpm: nativeServerResponse.seatbid[0].bid[0].price, + creativeId: nativeServerResponse.seatbid[0].bid[0].crid, + ttl: 300, + netRevenue: true, + mediaType: 'native', + currency: nativeServerResponse.cur + }); + + expect(bidResponse.meta).to.deep.equal({ + advertiserDomains: ['readpeak.com'], + }) + expect(bidResponse.native.title).to.equal('Title'); + expect(bidResponse.native.body).to.equal('Description'); + expect(bidResponse.native.image).to.deep.equal({ + url: 'http://url.to/image', + width: 750, + height: 500 + }); + expect(bidResponse.native.clickUrl).to.equal( + 'http%3A%2F%2Furl.to%2Ftarget' + ); + expect(bidResponse.native.impressionTrackers).to.contain( + 'http://url.to/pixeltracker' + ); + }); }); }); - describe('spec.buildRequests', function() { - it('should create a POST request for every bid', function() { - const request = spec.buildRequests([bidRequest], bidderRequest); - expect(request.method).to.equal('POST'); - expect(request.url).to.equal(ENDPOINT); - }); + describe('Banner', function() { + describe('spec.isBidRequestValid', function() { + it('should return true when the required params are passed', function() { + expect(spec.isBidRequestValid(bannerBidRequest)).to.equal(true); + }); - it('should attach request data', function() { - config.setConfig({ - currency: { - adServerCurrency: 'EUR' - } + it('should return false when the "publisherId" param is missing', function() { + bannerBidRequest.params = { + bidfloor: 5.0 + }; + expect(spec.isBidRequestValid(bannerBidRequest)).to.equal(false); }); - const request = spec.buildRequests([bidRequest], bidderRequest); - - const data = JSON.parse(request.data); - - expect(data.source.ext.prebid).to.equal('$prebid.version$'); - expect(data.id).to.equal(bidRequest.bidderRequestId); - expect(data.imp[0].bidfloor).to.equal(bidRequest.params.bidfloor); - expect(data.imp[0].bidfloorcur).to.equal('USD'); - expect(data.imp[0].tagId).to.equal('test-tag-1'); - expect(data.site.publisher.id).to.equal(bidRequest.params.publisherId); - expect(data.site.id).to.equal(bidRequest.params.siteId); - expect(data.site.page).to.equal(bidderRequest.refererInfo.referer); - expect(data.site.domain).to.equal(parseUrl(bidderRequest.refererInfo.referer).hostname); - expect(data.device).to.deep.contain({ - ua: navigator.userAgent, - language: navigator.language + it('should return false when no bid params are passed', function() { + bannerBidRequest.params = {}; + expect(spec.isBidRequestValid(bannerBidRequest)).to.equal(false); }); - expect(data.cur).to.deep.equal(['EUR']); - expect(data.user).to.be.undefined; - expect(data.regs).to.be.undefined; }); - it('should get bid floor from module', function() { - const floorModuleData = { - currency: 'USD', - floor: 3.2, - } - bidRequest.getFloor = function () { - return floorModuleData - } - const request = spec.buildRequests([bidRequest], bidderRequest); + describe('spec.buildRequests', function() { + it('should create a POST request for every bid', function() { + const request = spec.buildRequests([bannerBidRequest], bidderRequest); + expect(request.method).to.equal('POST'); + expect(request.url).to.equal(ENDPOINT); + }); - const data = JSON.parse(request.data); + it('should attach request data', function() { + config.setConfig({ + currency: { + adServerCurrency: 'EUR' + } + }); - expect(data.source.ext.prebid).to.equal('$prebid.version$'); - expect(data.id).to.equal(bidRequest.bidderRequestId); - expect(data.imp[0].bidfloor).to.equal(floorModuleData.floor); - expect(data.imp[0].bidfloorcur).to.equal(floorModuleData.currency); - }); + const request = spec.buildRequests([bannerBidRequest], bidderRequest); - it('should send gdpr data when gdpr does not apply', function() { - const gdprData = { - gdprConsent: { - gdprApplies: false, - consentString: undefined, - } - } - const request = spec.buildRequests([bidRequest], {...bidderRequest, ...gdprData}); + const data = JSON.parse(request.data); - const data = JSON.parse(request.data); + expect(data.source.ext.prebid).to.equal('$prebid.version$'); + expect(data.id).to.equal(bannerBidRequest.bidderRequestId); + expect(data.imp[0].bidfloor).to.equal(bannerBidRequest.params.bidfloor); + expect(data.imp[0].bidfloorcur).to.equal('USD'); + expect(data.imp[0].tagId).to.equal('test-tag-1'); + expect(data.site.publisher.id).to.equal(bannerBidRequest.params.publisherId); + expect(data.site.id).to.equal(bannerBidRequest.params.siteId); + expect(data.site.page).to.equal(bidderRequest.refererInfo.referer); + expect(data.site.domain).to.equal(parseUrl(bidderRequest.refererInfo.referer).hostname); + expect(data.device).to.deep.contain({ + ua: navigator.userAgent, + language: navigator.language + }); + expect(data.cur).to.deep.equal(['EUR']); + expect(data.user).to.be.undefined; + expect(data.regs).to.be.undefined; + }); - expect(data.user).to.deep.equal({ - ext: { - consent: '' + it('should get bid floor from module', function() { + const floorModuleData = { + currency: 'USD', + floor: 3.2, } - }); - expect(data.regs).to.deep.equal({ - ext: { - gdpr: false + bannerBidRequest.getFloor = function () { + return floorModuleData } + const request = spec.buildRequests([bannerBidRequest], bidderRequest); + + const data = JSON.parse(request.data); + + expect(data.source.ext.prebid).to.equal('$prebid.version$'); + expect(data.id).to.equal(bannerBidRequest.bidderRequestId); + expect(data.imp[0].bidfloor).to.equal(floorModuleData.floor); + expect(data.imp[0].bidfloorcur).to.equal(floorModuleData.currency); }); - }); - it('should send gdpr data when gdpr applies', function() { - const tcString = 'sometcstring'; - const gdprData = { - gdprConsent: { - gdprApplies: true, - consentString: tcString + it('should send gdpr data when gdpr does not apply', function() { + const gdprData = { + gdprConsent: { + gdprApplies: false, + consentString: undefined, + } } - } - const request = spec.buildRequests([bidRequest], {...bidderRequest, ...gdprData}); + const request = spec.buildRequests([bannerBidRequest], {...bidderRequest, ...gdprData}); - const data = JSON.parse(request.data); + const data = JSON.parse(request.data); - expect(data.user).to.deep.equal({ - ext: { - consent: tcString - } + expect(data.user).to.deep.equal({ + ext: { + consent: '' + } + }); + expect(data.regs).to.deep.equal({ + ext: { + gdpr: false + } + }); }); - expect(data.regs).to.deep.equal({ - ext: { - gdpr: true + + it('should send gdpr data when gdpr applies', function() { + const tcString = 'sometcstring'; + const gdprData = { + gdprConsent: { + gdprApplies: true, + consentString: tcString + } } - }); - }); - }); + const request = spec.buildRequests([bannerBidRequest], {...bidderRequest, ...gdprData}); + + const data = JSON.parse(request.data); - describe('spec.interpretResponse', function() { - it('should return no bids if the response is not valid', function() { - const bidResponse = spec.interpretResponse({ body: null }, serverRequest); - expect(bidResponse.length).to.equal(0); + expect(data.user).to.deep.equal({ + ext: { + consent: tcString + } + }); + expect(data.regs).to.deep.equal({ + ext: { + gdpr: true + } + }); + }); }); - it('should return a valid bid response', function() { - const bidResponse = spec.interpretResponse( - { body: serverResponse }, - serverRequest - )[0]; - expect(bidResponse).to.contain({ - requestId: bidRequest.bidId, - cpm: serverResponse.seatbid[0].bid[0].price, - creativeId: serverResponse.seatbid[0].bid[0].crid, - ttl: 300, - netRevenue: true, - mediaType: 'native', - currency: serverResponse.cur + describe('spec.interpretResponse', function() { + it('should return no bids if the response is not valid', function() { + const bidResponse = spec.interpretResponse({ body: null }, bannerServerRequest); + expect(bidResponse.length).to.equal(0); }); - expect(bidResponse.meta).to.deep.equal({ - advertiserDomains: ['readpeak.com'], - }) - expect(bidResponse.native.title).to.equal('Title'); - expect(bidResponse.native.body).to.equal('Description'); - expect(bidResponse.native.image).to.deep.equal({ - url: 'http://url.to/image', - width: 750, - height: 500 + it('should return a valid bid response', function() { + const bidResponse = spec.interpretResponse( + { body: bannerServerResponse }, + bannerServerRequest + )[0]; + expect(bidResponse).to.contain({ + requestId: bannerBidRequest.bidId, + cpm: bannerServerResponse.seatbid[0].bid[0].price, + creativeId: bannerServerResponse.seatbid[0].bid[0].crid, + ttl: 300, + netRevenue: true, + mediaType: 'banner', + currency: bannerServerResponse.cur, + ad: bannerServerResponse.seatbid[0].bid[0].adm, + width: bannerServerResponse.seatbid[0].bid[0].w, + height: bannerServerResponse.seatbid[0].bid[0].h, + }); + expect(bidResponse.meta).to.deep.equal({ + advertiserDomains: ['readpeak.com'], + }); }); - expect(bidResponse.native.clickUrl).to.equal( - 'http%3A%2F%2Furl.to%2Ftarget' - ); - expect(bidResponse.native.impressionTrackers).to.contain( - 'http://url.to/pixeltracker' - ); }); }); }); diff --git a/test/spec/modules/realTimeDataModule_spec.js b/test/spec/modules/realTimeDataModule_spec.js index 6538dec37c4..daeeb9bc47c 100644 --- a/test/spec/modules/realTimeDataModule_spec.js +++ b/test/spec/modules/realTimeDataModule_spec.js @@ -2,7 +2,7 @@ import * as rtdModule from 'modules/rtdModule/index.js'; import {config} from 'src/config.js'; import * as sinon from 'sinon'; import {default as CONSTANTS} from '../../../src/constants.json'; -import {default as events} from '../../../src/events.js'; +import * as events from '../../../src/events.js'; import 'src/prebid.js'; const getBidRequestDataSpy = sinon.spy(); @@ -141,7 +141,56 @@ describe('Real time module', function () { const adUnits = rtdModule.getAdUnitTargeting(auction); assert.deepEqual(expectedAdUnits, adUnits) done(); - }) + }); + + describe('setBidRequestData', () => { + let withWait, withoutWait; + + function runSetBidRequestData() { + return new Promise((resolve) => { + rtdModule.setBidRequestsData(resolve, {bidRequest: {}}); + }); + } + + beforeEach(() => { + withWait = { + submod: validSMWait, + cbTime: 0, + cbRan: false + }; + withoutWait = { + submod: validSM, + cbTime: 0, + cbRan: false + }; + + [withWait, withoutWait].forEach((c) => { + c.submod.getBidRequestData = sinon.stub().callsFake((_, cb) => { + setTimeout(() => { + c.cbRan = true; + cb(); + }, c.cbTime); + }); + }); + }); + + it('should allow non-priority submodules to run synchronously', () => { + withWait.cbTime = withoutWait.cbTime = 0; + return runSetBidRequestData().then(() => { + expect(withWait.cbRan).to.be.true; + expect(withoutWait.cbRan).to.be.true; + }) + }); + + it('should not wait for non-priority submodules if priority ones complete first', () => { + withWait.cbTime = 10; + withoutWait.cbTime = 100; + return runSetBidRequestData().then(() => { + expect(withWait.cbRan).to.be.true; + expect(withoutWait.cbRan).to.be.false; + }); + }); + }); }); it('deep merge object', function () { @@ -255,5 +304,5 @@ describe('Real time module', function () { expect(providers[1][hook].called).to.be.true; }); }); - }) + }); }); diff --git a/test/spec/modules/relaidoBidAdapter_spec.js b/test/spec/modules/relaidoBidAdapter_spec.js index 51850e5f357..f0d381ee3ed 100644 --- a/test/spec/modules/relaidoBidAdapter_spec.js +++ b/test/spec/modules/relaidoBidAdapter_spec.js @@ -303,6 +303,7 @@ describe('RelaidoAdapter', function () { expect(response.currency).to.equal(serverResponse.body.ads[0].currency); expect(response.creativeId).to.equal(serverResponse.body.ads[0].creativeId); expect(response.vastXml).to.equal(serverResponse.body.ads[0].vast); + expect(response.playerUrl).to.equal(serverResponse.body.playerUrl); expect(response.meta.advertiserDomains).to.equal(serverResponse.body.ads[0].adomain); expect(response.meta.mediaType).to.equal(VIDEO); expect(response.ad).to.be.undefined; @@ -320,11 +321,29 @@ describe('RelaidoAdapter', function () { expect(response.currency).to.equal(serverResponse.body.ads[0].currency); expect(response.creativeId).to.equal(serverResponse.body.ads[0].creativeId); expect(response.vastXml).to.be.undefined; + expect(response.playerUrl).to.equal(serverResponse.body.playerUrl); expect(response.ad).to.include(`
`); expect(response.ad).to.include(``); expect(response.ad).to.include(`window.RelaidoPlayer.renderAd`); }); + it('should build bid response by video and playerUrl in ads', function () { + serverResponse.body.ads[0].playerUrl = 'https://relaido/player-customized.js'; + const bidResponses = spec.interpretResponse(serverResponse, serverRequest); + expect(bidResponses).to.have.lengthOf(1); + const response = bidResponses[0]; + expect(response.playerUrl).to.equal(serverResponse.body.ads[0].playerUrl); + }); + + it('should build bid response by banner and playerUrl in ads', function () { + serverResponse.body.ads[0].playerUrl = 'https://relaido/player-customized.js'; + serverResponse.body.ads[0].mediaType = 'banner'; + const bidResponses = spec.interpretResponse(serverResponse, serverRequest); + expect(bidResponses).to.have.lengthOf(1); + const response = bidResponses[0]; + expect(response.playerUrl).to.equal(serverResponse.body.ads[0].playerUrl); + }); + it('should not build bid response', function () { serverResponse = {}; const bidResponses = spec.interpretResponse(serverResponse, serverRequest); diff --git a/test/spec/modules/relevantAnalyticsAdapter_spec.js b/test/spec/modules/relevantAnalyticsAdapter_spec.js index 3e31db2d7dc..5c818fe01d4 100644 --- a/test/spec/modules/relevantAnalyticsAdapter_spec.js +++ b/test/spec/modules/relevantAnalyticsAdapter_spec.js @@ -1,6 +1,6 @@ import relevantAnalytics from '../../../modules/relevantAnalyticsAdapter.js'; import adapterManager from 'src/adapterManager'; -import events from 'src/events'; +import * as events from 'src/events'; import constants from 'src/constants.json' import { expect } from 'chai'; diff --git a/test/spec/modules/riseBidAdapter_spec.js b/test/spec/modules/riseBidAdapter_spec.js index 211c36a254a..596c4d0f58c 100644 --- a/test/spec/modules/riseBidAdapter_spec.js +++ b/test/spec/modules/riseBidAdapter_spec.js @@ -2,12 +2,13 @@ import { expect } from 'chai'; import { spec } from 'modules/riseBidAdapter.js'; import { newBidder } from 'src/adapters/bidderFactory.js'; import { config } from 'src/config.js'; -import { VIDEO } from '../../../src/mediaTypes.js'; -import { deepClone } from 'src/utils.js'; +import { BANNER, VIDEO } from '../../../src/mediaTypes.js'; +import * as utils from 'src/utils.js'; -const ENDPOINT = 'https://hb.yellowblue.io/hb'; -const TEST_ENDPOINT = 'https://hb.yellowblue.io/hb-test'; +const ENDPOINT = 'https://hb.yellowblue.io/hb-multi'; +const TEST_ENDPOINT = 'https://hb.yellowblue.io/hb-multi-test'; const TTL = 360; +/* eslint no-console: ["error", { allow: ["log", "warn", "error"] }] */ describe('riseAdapter', function () { const adapter = newBidder(spec); @@ -54,6 +55,29 @@ describe('riseAdapter', function () { 'bidId': '299ffc8cca0b87', 'bidderRequestId': '1144f487e563f9', 'auctionId': 'bfc420c3-8577-4568-9766-a8a935fb620d', + 'mediaTypes': { + 'video': { + 'playerSize': [[640, 480]], + 'context': 'instream' + } + }, + 'vastXml': '"..."' + }, + { + 'bidder': spec.code, + 'adUnitCode': 'adunit-code', + 'sizes': [[300, 250]], + 'params': { + 'org': 'jdye8weeyirk00000001' + }, + 'bidId': '299ffc8cca0b87', + 'bidderRequestId': '1144f487e563f9', + 'auctionId': 'bfc420c3-8577-4568-9766-a8a935fb620d', + 'mediaTypes': { + 'banner': { + } + }, + 'ad': '""' } ]; @@ -77,44 +101,41 @@ describe('riseAdapter', function () { } const placementId = '12345678'; - it('sends the placementId as a query param', function () { + it('sends the placementId to ENDPOINT via POST', function () { bidRequests[0].params.placementId = placementId; - const requests = spec.buildRequests(bidRequests, bidderRequest); - for (const request of requests) { - expect(request.data.placement_id).to.equal(placementId); - } + const request = spec.buildRequests(bidRequests, bidderRequest); + expect(request.data.bids[0].placementId).to.equal(placementId); }); - it('sends bid request to ENDPOINT via GET', function () { - const requests = spec.buildRequests(bidRequests, bidderRequest); - for (const request of requests) { - expect(request.url).to.equal(ENDPOINT); - expect(request.method).to.equal('GET'); - } + it('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 GET', function () { - const requests = spec.buildRequests(testModeBidRequests, bidderRequest); - for (const request of requests) { - expect(request.url).to.equal(TEST_ENDPOINT); - expect(request.method).to.equal('GET'); - } + it('sends bid request to TEST ENDPOINT via POST', function () { + const request = spec.buildRequests(testModeBidRequests, bidderRequest); + expect(request.url).to.equal(TEST_ENDPOINT); + expect(request.method).to.equal('POST'); }); it('should send the correct bid Id', function () { - const requests = spec.buildRequests(bidRequests, bidderRequest); - for (const request of requests) { - expect(request.data.bid_id).to.equal('299ffc8cca0b87'); - } + const request = spec.buildRequests(bidRequests, bidderRequest); + expect(request.data.bids[0].bidId).to.equal('299ffc8cca0b87'); }); - it('should send the correct width and height', function () { - const requests = spec.buildRequests(bidRequests, bidderRequest); - for (const request of requests) { - expect(request.data).to.be.an('object'); - expect(request.data).to.have.property('width', 640); - expect(request.data).to.have.property('height', 480); - } + it('should send the correct sizes array', function () { + const request = spec.buildRequests(bidRequests, bidderRequest); + expect(request.data.bids[0].sizes).to.be.an('array'); + expect(request.data.bids[0].sizes).to.equal(bidRequests[0].sizes) + expect(request.data.bids[1].sizes).to.be.an('array'); + expect(request.data.bids[1].sizes).to.equal(bidRequests[1].sizes) + }); + + it('should send the correct media type', function () { + const request = spec.buildRequests(bidRequests, bidderRequest); + expect(request.data.bids[0].mediaType).to.equal(VIDEO) + expect(request.data.bids[1].mediaType).to.equal(BANNER) }); it('should respect syncEnabled option', function() { @@ -129,11 +150,9 @@ describe('riseAdapter', function () { } } }); - const requests = spec.buildRequests(bidRequests, bidderRequest); - for (const request of requests) { - expect(request.data).to.be.an('object'); - expect(request.data).to.not.have.property('cs_method'); - } + const request = spec.buildRequests(bidRequests, bidderRequest); + expect(request.data.params).to.be.an('object'); + expect(request.data.params).to.not.have.property('cs_method'); }); it('should respect "iframe" filter settings', function () { @@ -148,11 +167,9 @@ describe('riseAdapter', function () { } } }); - const requests = spec.buildRequests(bidRequests, bidderRequest); - for (const request of requests) { - expect(request.data).to.be.an('object'); - expect(request.data).to.have.property('cs_method', 'iframe'); - } + const request = spec.buildRequests(bidRequests, bidderRequest); + expect(request.data.params).to.be.an('object'); + expect(request.data.params).to.have.property('cs_method', 'iframe'); }); it('should respect "all" filter settings', function () { @@ -167,24 +184,21 @@ describe('riseAdapter', function () { } } }); - const requests = spec.buildRequests(bidRequests, bidderRequest); - for (const request of requests) { - expect(request.data).to.be.an('object'); - expect(request.data).to.have.property('cs_method', 'iframe'); - } + const request = spec.buildRequests(bidRequests, bidderRequest); + expect(request.data.params).to.be.an('object'); + expect(request.data.params).to.have.property('cs_method', 'iframe'); }); it('should send the pixel user sync param if userSync is enabled and no "iframe" or "all" configs are present', function () { + config.resetConfig(); config.setConfig({ userSync: { - syncEnabled: true + syncEnabled: true, } }); - const requests = spec.buildRequests(bidRequests, bidderRequest); - for (const request of requests) { - expect(request.data).to.be.an('object'); - expect(request.data).to.have.property('cs_method', 'pixel'); - } + const request = spec.buildRequests(bidRequests, bidderRequest); + expect(request.data.params).to.be.an('object'); + expect(request.data.params).to.have.property('cs_method', 'pixel'); }); it('should respect total exclusion', function() { @@ -203,48 +217,38 @@ describe('riseAdapter', function () { } } }); - const requests = spec.buildRequests(bidRequests, bidderRequest); - for (const request of requests) { - expect(request.data).to.be.an('object'); - expect(request.data).to.not.have.property('cs_method'); - } + const request = spec.buildRequests(bidRequests, bidderRequest); + expect(request.data.params).to.be.an('object'); + expect(request.data.params).to.not.have.property('cs_method'); }); it('should have us_privacy param if usPrivacy is available in the bidRequest', function () { const bidderRequestWithUSP = Object.assign({uspConsent: '1YNN'}, bidderRequest); - const requests = spec.buildRequests(bidRequests, bidderRequestWithUSP); - for (const request of requests) { - expect(request.data).to.be.an('object'); - expect(request.data).to.have.property('us_privacy', '1YNN'); - } + const request = spec.buildRequests(bidRequests, bidderRequestWithUSP); + expect(request.data.params).to.be.an('object'); + expect(request.data.params).to.have.property('us_privacy', '1YNN'); }); it('should have an empty us_privacy param if usPrivacy is missing in the bidRequest', function () { - const requests = spec.buildRequests(bidRequests, bidderRequest); - for (const request of requests) { - expect(request.data).to.be.an('object'); - expect(request.data).to.not.have.property('us_privacy'); - } + const request = spec.buildRequests(bidRequests, bidderRequest); + expect(request.data.params).to.be.an('object'); + expect(request.data.params).to.not.have.property('us_privacy'); }); it('should not send the gdpr param if gdprApplies is false in the bidRequest', function () { const bidderRequestWithGDPR = Object.assign({gdprConsent: {gdprApplies: false}}, bidderRequest); - const requests = spec.buildRequests(bidRequests, bidderRequestWithGDPR); - for (const request of requests) { - expect(request.data).to.be.an('object'); - expect(request.data).to.not.have.property('gdpr'); - expect(request.data).to.not.have.property('gdpr_consent'); - } + const request = spec.buildRequests(bidRequests, bidderRequestWithGDPR); + expect(request.data.params).to.be.an('object'); + expect(request.data.params).to.not.have.property('gdpr'); + expect(request.data.params).to.not.have.property('gdpr_consent'); }); it('should send the gdpr param if gdprApplies is true in the bidRequest', function () { const bidderRequestWithGDPR = Object.assign({gdprConsent: {gdprApplies: true, consentString: 'test-consent-string'}}, bidderRequest); - const requests = spec.buildRequests(bidRequests, bidderRequestWithGDPR); - for (const request of requests) { - expect(request.data).to.be.an('object'); - expect(request.data).to.have.property('gdpr', true); - expect(request.data).to.have.property('gdpr_consent', 'test-consent-string'); - } + const request = spec.buildRequests(bidRequests, bidderRequestWithGDPR); + expect(request.data.params).to.be.an('object'); + expect(request.data.params).to.have.property('gdpr', true); + expect(request.data.params).to.have.property('gdpr_consent', 'test-consent-string'); }); it('should have schain param if it is available in the bidRequest', () => { @@ -254,15 +258,13 @@ describe('riseAdapter', function () { nodes: [{ asi: 'indirectseller.com', sid: '00001', hp: 1 }], }; bidRequests[0].schain = schain; - const requests = spec.buildRequests(bidRequests, bidderRequest); - for (const request of requests) { - expect(request.data).to.be.an('object'); - expect(request.data).to.have.property('schain', '1.0,1!indirectseller.com,00001,1,,,'); - } + const request = spec.buildRequests(bidRequests, bidderRequest); + expect(request.data.params).to.be.an('object'); + expect(request.data.params).to.have.property('schain', '1.0,1!indirectseller.com,00001,1,,,'); }); - it('should set floor_price to getFloor.floor value if it is greater than params.floorPrice', function() { - const bid = deepClone(bidRequests[0]); + it('should set flooPrice to getFloor.floor value if it is greater than params.floorPrice', function() { + const bid = utils.deepClone(bidRequests[0]); bid.getFloor = () => { return { currency: 'USD', @@ -270,13 +272,13 @@ describe('riseAdapter', function () { } } bid.params.floorPrice = 0.64; - const request = spec.buildRequests([bid], bidderRequest)[0]; - expect(request.data).to.be.an('object'); - expect(request.data).to.have.property('floor_price', 3.32); + const request = spec.buildRequests([bid], bidderRequest); + expect(request.data.bids[0]).to.be.an('object'); + expect(request.data.bids[0]).to.have.property('floorPrice', 3.32); }); - it('should set floor_price to params.floorPrice value if it is greater than getFloor.floor', function() { - const bid = deepClone(bidRequests[0]); + it('should set floorPrice to params.floorPrice value if it is greater than getFloor.floor', function() { + const bid = utils.deepClone(bidRequests[0]); bid.getFloor = () => { return { currency: 'USD', @@ -284,61 +286,109 @@ describe('riseAdapter', function () { } } bid.params.floorPrice = 1.5; - const request = spec.buildRequests([bid], bidderRequest)[0]; - expect(request.data).to.be.an('object'); - expect(request.data).to.have.property('floor_price', 1.5); + const request = spec.buildRequests([bid], bidderRequest); + expect(request.data.bids[0]).to.be.an('object'); + expect(request.data.bids[0]).to.have.property('floorPrice', 1.5); }); }); describe('interpretResponse', function () { const response = { + params: { + currency: 'USD', + netRevenue: true, + }, + bids: [{ + cpm: 12.5, + vastXml: '', + width: 640, + height: 480, + requestId: '21e12606d47ba7', + adomain: ['abc.com'], + mediaType: VIDEO + }, + { + cpm: 12.5, + ad: '""', + width: 300, + height: 250, + requestId: '21e12606d47ba7', + adomain: ['abc.com'], + mediaType: BANNER + }] + }; + + const expectedVideoResponse = { + requestId: '21e12606d47ba7', cpm: 12.5, - vastXml: '', + currency: 'USD', width: 640, height: 480, - requestId: '21e12606d47ba7', + ttl: TTL, + creativeId: '21e12606d47ba7', netRevenue: true, + nurl: 'http://example.com/win/1234', + mediaType: VIDEO, + meta: { + mediaType: VIDEO, + advertiserDomains: ['abc.com'] + }, + vastXml: '', + }; + + const expectedBannerResponse = { + requestId: '21e12606d47ba7', + cpm: 12.5, currency: 'USD', - adomain: ['abc.com'] + width: 640, + height: 480, + ttl: TTL, + creativeId: '21e12606d47ba7', + netRevenue: true, + nurl: 'http://example.com/win/1234', + mediaType: BANNER, + meta: { + mediaType: BANNER, + advertiserDomains: ['abc.com'] + }, + ad: '""' }; it('should get correct bid response', function () { - let expectedResponse = [ - { - requestId: '21e12606d47ba7', - cpm: 12.5, - width: 640, - height: 480, - creativeId: '21e12606d47ba7', - currency: 'USD', - netRevenue: true, - ttl: TTL, - vastXml: '', - mediaType: VIDEO, - meta: { - advertiserDomains: ['abc.com'] - } - } - ]; const result = spec.interpretResponse({ body: response }); - expect(Object.keys(result[0])).to.have.members(Object.keys(expectedResponse[0])); + expect(Object.keys(result[0])).to.deep.equal(Object.keys(expectedVideoResponse)); + expect(Object.keys(result[1])).to.deep.equal(Object.keys(expectedBannerResponse)); + }); + + it('video type should have vastXml key', function () { + const result = spec.interpretResponse({ body: response }); + expect(result[0].vastXml).to.equal(expectedVideoResponse.vastXml) + }); + + it('banner type should have ad key', function () { + const result = spec.interpretResponse({ body: response }); + expect(result[1].ad).to.equal(expectedBannerResponse.ad) }); }) describe('getUserSyncs', function() { const imageSyncResponse = { body: { - userSyncPixels: [ - 'https://image-sync-url.test/1', - 'https://image-sync-url.test/2', - 'https://image-sync-url.test/3' - ] + params: { + userSyncPixels: [ + 'https://image-sync-url.test/1', + 'https://image-sync-url.test/2', + 'https://image-sync-url.test/3' + ] + } } }; const iframeSyncResponse = { body: { - userSyncURL: 'https://iframe-sync-url.test' + params: { + userSyncURL: 'https://iframe-sync-url.test' + } } }; @@ -402,4 +452,28 @@ describe('riseAdapter', function () { expect(syncs).to.deep.equal([]); }); }) + + describe('onBidWon', function() { + beforeEach(function() { + sinon.stub(utils, 'triggerPixel'); + }); + afterEach(function() { + utils.triggerPixel.restore(); + }); + + it('Should trigger pixel if bid nurl', function() { + const bid = { + 'bidder': spec.code, + 'adUnitCode': 'adunit-code', + 'sizes': [['640', '480']], + 'nurl': 'http://example.com/win/1234', + 'params': { + 'org': 'jdye8weeyirk00000001' + } + }; + + spec.onBidWon(bid); + expect(utils.triggerPixel.callCount).to.equal(1) + }) + }) }); diff --git a/test/spec/modules/rubiconAnalyticsAdapter_spec.js b/test/spec/modules/rubiconAnalyticsAdapter_spec.js index 798e98bb97d..f365d416876 100644 --- a/test/spec/modules/rubiconAnalyticsAdapter_spec.js +++ b/test/spec/modules/rubiconAnalyticsAdapter_spec.js @@ -13,6 +13,7 @@ import { setConfig, addBidResponseHook, } from 'modules/currency.js'; +import truncate from 'lodash.truncate'; let Ajv = require('ajv'); let schema = require('./rubiconAnalyticsSchema.json'); let ajv = new Ajv({ @@ -26,7 +27,6 @@ function validate(message) { expect(validator.errors).to.deep.equal(null); } -// using es6 "import * as events from 'src/events.js'" causes the events.getEvents stub not to work... let events = require('src/events.js'); let utils = require('src/utils.js'); @@ -39,7 +39,8 @@ const { BIDDER_DONE, BID_WON, BID_TIMEOUT, - SET_TARGETING + SET_TARGETING, + BILLABLE_EVENT } } = CONSTANTS; @@ -246,7 +247,7 @@ const MOCK = { } }, BID_REQUESTED: { - 'bidder': 'rubicon', + 'bidderCode': 'rubicon', 'auctionId': '25c6d7f5-699a-4bfc-87c9-996f915341fa', 'bidderRequestId': '1be65d7958826a', 'bids': [ @@ -384,6 +385,10 @@ const ANALYTICS_MESSAGE = { 'referrerHostname': 'www.test.com', 'auctions': [ { + + 'auctionEnd': 1519767013781, + 'auctionStart': 1519767010567, + 'bidderOrder': ['rubicon'], 'requestId': '25c6d7f5-699a-4bfc-87c9-996f915341fa', 'clientTimeoutMillis': 3000, 'serverTimeoutMillis': 1000, @@ -573,21 +578,21 @@ const ANALYTICS_MESSAGE = { } }; -function performStandardAuction(gptEvents) { - events.emit(AUCTION_INIT, MOCK.AUCTION_INIT); - events.emit(BID_REQUESTED, MOCK.BID_REQUESTED); - events.emit(BID_RESPONSE, MOCK.BID_RESPONSE[0]); - events.emit(BID_RESPONSE, MOCK.BID_RESPONSE[1]); - events.emit(BIDDER_DONE, MOCK.BIDDER_DONE); - events.emit(AUCTION_END, MOCK.AUCTION_END); +function performStandardAuction(gptEvents, auctionId = MOCK.AUCTION_INIT.auctionId) { + events.emit(AUCTION_INIT, { ...MOCK.AUCTION_INIT, auctionId }); + events.emit(BID_REQUESTED, { ...MOCK.BID_REQUESTED, auctionId }); + events.emit(BID_RESPONSE, { ...MOCK.BID_RESPONSE[0], auctionId }); + events.emit(BID_RESPONSE, { ...MOCK.BID_RESPONSE[1], auctionId }); + events.emit(BIDDER_DONE, { ...MOCK.BIDDER_DONE, auctionId }); + events.emit(AUCTION_END, { ...MOCK.AUCTION_END, auctionId }); if (gptEvents && gptEvents.length) { gptEvents.forEach(gptEvent => mockGpt.emitEvent(gptEvent.eventName, gptEvent.params)); } - events.emit(SET_TARGETING, MOCK.SET_TARGETING); - events.emit(BID_WON, MOCK.BID_WON[0]); - events.emit(BID_WON, MOCK.BID_WON[1]); + events.emit(SET_TARGETING, { ...MOCK.SET_TARGETING, auctionId }); + events.emit(BID_WON, { ...MOCK.BID_WON[0], auctionId }); + events.emit(BID_WON, { ...MOCK.BID_WON[1], auctionId }); } describe('rubicon analytics adapter', function () { @@ -673,6 +678,11 @@ describe('rubicon analytics adapter', function () { }); expect(rubiConf).to.deep.equal({ analyticsEventDelay: 0, + dmBilling: { + enabled: false, + vendors: [], + waitForAuction: true + }, pvid: '12345678', wrapperName: '1001_general', int_type: 'dmpbjs', @@ -692,6 +702,11 @@ describe('rubicon analytics adapter', function () { }); expect(rubiConf).to.deep.equal({ analyticsEventDelay: 0, + dmBilling: { + enabled: false, + vendors: [], + waitForAuction: true + }, pvid: '12345678', wrapperName: '1001_general', int_type: 'dmpbjs', @@ -713,6 +728,11 @@ describe('rubicon analytics adapter', function () { }); expect(rubiConf).to.deep.equal({ analyticsEventDelay: 0, + dmBilling: { + enabled: false, + vendors: [], + waitForAuction: true + }, pvid: '12345678', wrapperName: '1001_general', int_type: 'dmpbjs', @@ -853,6 +873,32 @@ describe('rubicon analytics adapter', function () { expect(message).to.deep.equal(ANALYTICS_MESSAGE); }); + it('should pass along bidderOrder correctly', function () { + const appnexusBid = utils.deepClone(MOCK.BID_REQUESTED); + appnexusBid.bidderCode = 'appnexus'; + const pubmaticBid = utils.deepClone(MOCK.BID_REQUESTED); + pubmaticBid.bidderCode = 'pubmatic'; + const indexBid = utils.deepClone(MOCK.BID_REQUESTED); + indexBid.bidderCode = 'ix'; + events.emit(AUCTION_INIT, MOCK.AUCTION_INIT); + events.emit(BID_REQUESTED, pubmaticBid); + events.emit(BID_REQUESTED, MOCK.BID_REQUESTED); + events.emit(BID_REQUESTED, indexBid); + events.emit(BID_REQUESTED, appnexusBid); + events.emit(BIDDER_DONE, MOCK.BIDDER_DONE); + events.emit(AUCTION_END, MOCK.AUCTION_END); + events.emit(SET_TARGETING, MOCK.SET_TARGETING); + clock.tick(SEND_TIMEOUT + 1000); + + let message = JSON.parse(server.requests[0].requestBody); + expect(message.auctions[0].bidderOrder).to.deep.equal([ + 'pubmatic', + 'rubicon', + 'ix', + 'appnexus' + ]); + }); + it('should pass along user ids', function () { let auctionInit = utils.deepClone(MOCK.AUCTION_INIT); auctionInit.bidderRequests[0].bids[0].userId = { @@ -987,6 +1033,36 @@ describe('rubicon analytics adapter', function () { expect(message.auctions[0].adUnits[1].bids[0].bidResponse.adomains).to.be.undefined; }); + it('should NOT pass along adomians with other edge cases', function () { + events.emit(AUCTION_INIT, MOCK.AUCTION_INIT); + events.emit(BID_REQUESTED, MOCK.BID_REQUESTED); + + // should filter out non string values and pass valid ones + let bidResponse1 = utils.deepClone(MOCK.BID_RESPONSE[0]); + bidResponse1.meta = { + advertiserDomains: [123, 'prebid.org', false, true, [], 'magnite.com', {}] + } + + // array of arrays (as seen when passed by kargo bid adapter) + let bidResponse2 = utils.deepClone(MOCK.BID_RESPONSE[1]); + bidResponse2.meta = { + advertiserDomains: [['prebid.org']] + } + + events.emit(BID_RESPONSE, bidResponse1); + events.emit(BID_RESPONSE, bidResponse2); + 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]); + + let message = JSON.parse(server.requests[0].requestBody); + validate(message); + expect(message.auctions[0].adUnits[0].bids[0].bidResponse.adomains).to.deep.equal(['prebid.org', 'magnite.com']); + expect(message.auctions[0].adUnits[1].bids[0].bidResponse.adomains).to.be.undefined; + }); + it('should not pass empty adServerTargeting values', function () { events.emit(AUCTION_INIT, MOCK.AUCTION_INIT); events.emit(BID_REQUESTED, MOCK.BID_REQUESTED); @@ -1703,6 +1779,50 @@ describe('rubicon analytics adapter', function () { expect(message).to.deep.equal(expectedMessage); }); + it('should only mark the first gam data not all matches', function () { + config.setConfig({ + rubicon: { + waitForGamSlots: true + } + }); + performStandardAuction(); + performStandardAuction([gptSlotRenderEnded0, gptSlotRenderEnded1], '32d332de-123a-32dg-2345-cefef3423324'); + + // tick the clock and both should fire + clock.tick(3000); + + expect(server.requests.length).to.equal(2); + + // first one should have GAM data + let request = server.requests[0]; + let message = JSON.parse(request.requestBody); + + // trigger should be gam since all adunits had associated gam render + expect(message.trigger).to.be.equal('gam'); + expect(message.auctions[0].adUnits[0].gam).to.deep.equal({ + advertiserId: 1111, + creativeId: 2222, + lineItemId: 3333, + adSlot: '/19968336/header-bid-tag-0' + }); + expect(message.auctions[0].adUnits[1].gam).to.deep.equal({ + advertiserId: 4444, + creativeId: 5555, + lineItemId: 6666, + adSlot: '/19968336/header-bid-tag1' + }); + + // second one should NOT have gam data + request = server.requests[1]; + message = JSON.parse(request.requestBody); + validate(message); + + // trigger should be auctionEnd + expect(message.trigger).to.be.equal('auctionEnd'); + expect(message.auctions[0].adUnits[0].gam).to.be.undefined; + expect(message.auctions[0].adUnits[1].gam).to.be.undefined; + }); + it('should send request when waitForGamSlots is present but no bidWons are sent', function () { config.setConfig({ rubicon: { @@ -2022,6 +2142,35 @@ describe('rubicon analytics adapter', function () { expect(message.auctions[0].adUnits[1].pattern).to.equal('1234/mycoolsite/*&gpt_skyscraper&deviceType=mobile'); }); + it('should pass gpid if defined', function () { + let bidRequest = utils.deepClone(MOCK.BID_REQUESTED); + bidRequest.bids[0].ortb2Imp = { + ext: { + gpid: '1234/mycoolsite/lowerbox' + } + }; + bidRequest.bids[1].ortb2Imp = { + ext: { + gpid: '1234/mycoolsite/leaderboard' + } + }; + events.emit(AUCTION_INIT, MOCK.AUCTION_INIT); + events.emit(BID_REQUESTED, bidRequest); + 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); + + clock.tick(SEND_TIMEOUT + 1000); + + expect(server.requests.length).to.equal(1); + + let message = JSON.parse(server.requests[0].requestBody); + validate(message); + expect(message.auctions[0].adUnits[0].gpid).to.equal('1234/mycoolsite/lowerbox'); + expect(message.auctions[0].adUnits[1].gpid).to.equal('1234/mycoolsite/leaderboard'); + }); + it('should pass bidderDetail for multibid auctions', function () { let bidResponse = utils.deepClone(MOCK.BID_RESPONSE[1]); bidResponse.targetingBidder = 'rubi2'; @@ -2103,6 +2252,246 @@ describe('rubicon analytics adapter', function () { }); }); + describe('billing events integration', () => { + beforeEach(function () { + rubiconAnalyticsAdapter.enableAnalytics({ + options: { + endpoint: '//localhost:9999/event', + accountId: 1001 + } + }); + // default dmBilling + config.setConfig({ + rubicon: { + dmBilling: { + enabled: false, + vendors: [], + waitForAuction: true + } + } + }) + }); + afterEach(function () { + rubiconAnalyticsAdapter.disableAnalytics(); + }); + const basicBillingAuction = (billingEvents = []) => { + events.emit(AUCTION_INIT, MOCK.AUCTION_INIT); + events.emit(BID_REQUESTED, MOCK.BID_REQUESTED); + + // emit billing events + billingEvents.forEach(ev => events.emit(BILLABLE_EVENT, ev)); + 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]); + } + it('should ignore billing events when not enabled', () => { + basicBillingAuction([{ + vendor: 'vendorName', + type: 'auction', + billingId: 'f8558d41-62de-4349-bc7b-2dbee1e69965' + }]); + expect(server.requests.length).to.equal(1); + const request = server.requests[0]; + const message = JSON.parse(request.requestBody); + expect(message.billableEvents).to.be.undefined; + }); + it('should ignore billing events when enabled but vendor is not whitelisted', () => { + // off by default + config.setConfig({ + rubicon: { + dmBilling: { + enabled: true + } + } + }); + basicBillingAuction([{ + vendor: 'vendorName', + type: 'auction', + billingId: 'f8558d41-62de-4349-bc7b-2dbee1e69965' + }]); + expect(server.requests.length).to.equal(1); + const request = server.requests[0]; + const message = JSON.parse(request.requestBody); + expect(message.billableEvents).to.be.undefined; + }); + it('should ignore billing events if billingId is not defined or billingId is not a string', () => { + // off by default + config.setConfig({ + rubicon: { + dmBilling: { + enabled: true, + vendors: ['vendorName'] + } + } + }); + basicBillingAuction([ + { + vendor: 'vendorName', + type: 'auction', + }, + { + vendor: 'vendorName', + type: 'auction', + billingId: true + }, + { + vendor: 'vendorName', + type: 'auction', + billingId: 1233434 + }, + { + vendor: 'vendorName', + type: 'auction', + billingId: null + } + ]); + expect(server.requests.length).to.equal(1); + const request = server.requests[0]; + const message = JSON.parse(request.requestBody); + expect(message.billableEvents).to.be.undefined; + }); + it('should pass along billing event in same payload', () => { + // off by default + config.setConfig({ + rubicon: { + dmBilling: { + enabled: true, + vendors: ['vendorName'] + } + } + }); + basicBillingAuction([{ + vendor: 'vendorName', + type: 'auction', + billingId: 'f8558d41-62de-4349-bc7b-2dbee1e69965' + }]); + expect(server.requests.length).to.equal(1); + const request = server.requests[0]; + const message = JSON.parse(request.requestBody); + expect(message).to.haveOwnProperty('auctions'); + expect(message.billableEvents).to.deep.equal([{ + accountId: 1001, + vendor: 'vendorName', + type: 'general', // mapping all events to endpoint as 'general' for now + billingId: 'f8558d41-62de-4349-bc7b-2dbee1e69965' + }]); + }); + it('should pass along multiple billing events but filter out duplicates', () => { + // off by default + config.setConfig({ + rubicon: { + dmBilling: { + enabled: true, + vendors: ['vendorName'] + } + } + }); + basicBillingAuction([ + { + vendor: 'vendorName', + type: 'auction', + billingId: 'f8558d41-62de-4349-bc7b-2dbee1e69965' + }, + { + vendor: 'vendorName', + type: 'auction', + billingId: '743db6e3-21f2-44d4-917f-cb3488c6076f' + }, + { + vendor: 'vendorName', + type: 'auction', + billingId: 'f8558d41-62de-4349-bc7b-2dbee1e69965' + } + ]); + expect(server.requests.length).to.equal(1); + const request = server.requests[0]; + const message = JSON.parse(request.requestBody); + expect(message).to.haveOwnProperty('auctions'); + expect(message.billableEvents).to.deep.equal([ + { + accountId: 1001, + vendor: 'vendorName', + type: 'general', + billingId: 'f8558d41-62de-4349-bc7b-2dbee1e69965' + }, + { + accountId: 1001, + vendor: 'vendorName', + type: 'general', + billingId: '743db6e3-21f2-44d4-917f-cb3488c6076f' + } + ]); + }); + it('should pass along event right away if no pending auction', () => { + // off by default + config.setConfig({ + rubicon: { + dmBilling: { + enabled: true, + vendors: ['vendorName'] + } + } + }); + + events.emit(BILLABLE_EVENT, { + vendor: 'vendorName', + type: 'auction', + billingId: 'f8558d41-62de-4349-bc7b-2dbee1e69965' + }); + expect(server.requests.length).to.equal(1); + const request = server.requests[0]; + const message = JSON.parse(request.requestBody); + expect(message).to.not.haveOwnProperty('auctions'); + expect(message.billableEvents).to.deep.equal([ + { + accountId: 1001, + vendor: 'vendorName', + type: 'general', + billingId: 'f8558d41-62de-4349-bc7b-2dbee1e69965' + } + ]); + }); + it('should pass along event right away if pending auction but not waiting', () => { + // off by default + config.setConfig({ + rubicon: { + dmBilling: { + enabled: true, + vendors: ['vendorName'], + waitForAuction: false + } + } + }); + // should fire right away, and then auction later + basicBillingAuction([{ + vendor: 'vendorName', + type: 'auction', + billingId: 'f8558d41-62de-4349-bc7b-2dbee1e69965' + }]); + expect(server.requests.length).to.equal(2); + const billingRequest = server.requests[0]; + const billingMessage = JSON.parse(billingRequest.requestBody); + expect(billingMessage).to.not.haveOwnProperty('auctions'); + expect(billingMessage.billableEvents).to.deep.equal([ + { + accountId: 1001, + vendor: 'vendorName', + type: 'general', + billingId: 'f8558d41-62de-4349-bc7b-2dbee1e69965' + } + ]); + // auction event after + const auctionRequest = server.requests[1]; + const auctionMessage = JSON.parse(auctionRequest.requestBody); + // should not double pass events! + expect(auctionMessage).to.not.haveOwnProperty('billableEvents'); + }); + }); + describe('wrapper details passed in', () => { it('should correctly pass in the wrapper details if provided', () => { config.setConfig({ diff --git a/test/spec/modules/rubiconAnalyticsSchema.json b/test/spec/modules/rubiconAnalyticsSchema.json index 39a33867edd..07239ca06ed 100644 --- a/test/spec/modules/rubiconAnalyticsSchema.json +++ b/test/spec/modules/rubiconAnalyticsSchema.json @@ -17,6 +17,11 @@ "required": [ "bidsWon" ] + }, + { + "required": [ + "billableEvents" + ] } ], "properties": { @@ -237,9 +242,44 @@ ] } }, - "wrapperName": { - "type": "string" - } + "billableEvents":{ + "type":"array", + "minItems":1, + "items":{ + "type":"object", + "required":[ + "accountId", + "vendor", + "type", + "billingId" + ], + "properties":{ + "vendor":{ + "type":"string", + "description":"The name of the vendor who emitted the billable event" + }, + "type":{ + "type":"string", + "description":"The type of billable event", + "enum":[ + "impression", + "pageLoad", + "auction", + "request", + "general" + ] + }, + "billingId":{ + "type":"string", + "description":"A UUID which is responsible more mapping this event to" + }, + "accountId": { + "type": "number", + "description": "The account id for the rubicon publisher" + } + } + } + } }, "definitions": { "gam": { @@ -451,4 +491,4 @@ } } } -} \ No newline at end of file +} diff --git a/test/spec/modules/rubiconBidAdapter_spec.js b/test/spec/modules/rubiconBidAdapter_spec.js index ea839bc7ae3..c281c195dd2 100644 --- a/test/spec/modules/rubiconBidAdapter_spec.js +++ b/test/spec/modules/rubiconBidAdapter_spec.js @@ -10,7 +10,7 @@ import { import {parse as parseQuery} from 'querystring'; import {config} from 'src/config.js'; import * as utils from 'src/utils.js'; -import find from 'core-js-pure/features/array/find.js'; +import {find} from 'src/polyfill.js'; import {createEidsArray} from 'modules/userId/eids.js'; const INTEGRATION = `pbjs_lite_v$prebid.version$`; // $prebid.version$ will be substituted in by gulp in built prebid @@ -597,7 +597,7 @@ describe('the rubicon adapter', function () { sandbox.stub(Math, 'random').callsFake(() => 0.1); let [request] = spec.buildRequests(bidderRequest.bids, bidderRequest); - const referenceOrdering = ['account_id', 'site_id', 'zone_id', 'size_id', 'alt_size_ids', 'p_pos', 'rf', 'p_geo.latitude', 'p_geo.longitude', 'kw', 'tg_v.ucat', 'tg_v.lastsearch', 'tg_v.likes', 'tg_i.rating', 'tg_i.prodtype', 'tk_flint', 'x_source.tid', 'x_source.pchain', 'p_screen_res', 'rp_secure', 'tk_user_key', 'tg_fl.eid', 'rp_maxbids', 'slots', 'rand']; + const referenceOrdering = ['account_id', 'site_id', 'zone_id', 'size_id', 'alt_size_ids', 'p_pos', 'rf', 'p_geo.latitude', 'p_geo.longitude', 'kw', 'tg_v.ucat', 'tg_v.lastsearch', 'tg_v.likes', 'tg_i.rating', 'tg_i.prodtype', 'tk_flint', 'x_source.tid', 'l_pb_bid_id', 'x_source.pchain', 'p_screen_res', 'rp_secure', 'tk_user_key', 'tg_fl.eid', 'rp_maxbids', 'slots', 'rand']; request.data.split('&').forEach((item, i) => { expect(item.split('=')[0]).to.equal(referenceOrdering[i]); @@ -3262,7 +3262,7 @@ describe('the rubicon adapter', function () { label: undefined, placement: { align: 'left', - attachTo: '#outstream_video1_placement', + attachTo: adUnit, position: 'append', }, vastUrl: 'https://test.com/vast.xml', diff --git a/test/spec/modules/scaleableAnalyticsAdapter_spec.js b/test/spec/modules/scaleableAnalyticsAdapter_spec.js index 70b94a2b807..c65740252d2 100644 --- a/test/spec/modules/scaleableAnalyticsAdapter_spec.js +++ b/test/spec/modules/scaleableAnalyticsAdapter_spec.js @@ -1,6 +1,6 @@ import scaleableAnalytics from 'modules/scaleableAnalyticsAdapter.js'; import { expect } from 'chai'; -import events from 'src/events.js'; +import * as events from 'src/events.js'; import CONSTANTS from 'src/constants.json'; import { server } from 'test/mocks/xhr.js'; diff --git a/test/spec/modules/seedtagBidAdapter_spec.js b/test/spec/modules/seedtagBidAdapter_spec.js index 159645ff6d6..1e0dca68d00 100644 --- a/test/spec/modules/seedtagBidAdapter_spec.js +++ b/test/spec/modules/seedtagBidAdapter_spec.js @@ -1,14 +1,17 @@ -import { expect } from 'chai' -import { spec, getTimeoutUrl } from 'modules/seedtagBidAdapter.js' -import * as utils from 'src/utils.js' +import { expect } from 'chai'; +import { spec, getTimeoutUrl } from 'modules/seedtagBidAdapter.js'; +import * as utils from 'src/utils.js'; -const PUBLISHER_ID = '0000-0000-01' -const ADUNIT_ID = '000000' +const PUBLISHER_ID = '0000-0000-01'; +const ADUNIT_ID = '000000'; function getSlotConfigs(mediaTypes, params) { return { params: params, - sizes: [[300, 250], [300, 600]], + sizes: [ + [300, 250], + [300, 600], + ], bidId: '30b31c1838de1e', bidderRequestId: '22edbae2733bf6', auctionId: '1d1a030790a475', @@ -17,328 +20,336 @@ function getSlotConfigs(mediaTypes, params) { mediaTypes: mediaTypes, src: 'client', transactionId: 'd704d006-0d6e-4a09-ad6c-179e7e758096', - adUnitCode: 'adunit-code' - } + adUnitCode: 'adunit-code', + }; } function createVideoSlotConfig(mediaType) { return getSlotConfigs(mediaType, { publisherId: PUBLISHER_ID, adUnitId: ADUNIT_ID, - placement: 'video' - }) + placement: 'video', + }); } -describe('Seedtag Adapter', function() { - describe('isBidRequestValid method', function() { - describe('returns true', function() { +describe('Seedtag Adapter', function () { + describe('isBidRequestValid method', function () { + describe('returns true', function () { describe('when banner slot config has all mandatory params', () => { - describe('and placement has the correct value', function() { - const createBannerSlotConfig = placement => { + describe('and placement has the correct value', function () { + const createBannerSlotConfig = (placement) => { return getSlotConfigs( { banner: {} }, { publisherId: PUBLISHER_ID, adUnitId: ADUNIT_ID, - placement + placement, } - ) - } - const placements = ['banner', 'video', 'inImage', 'inScreen', 'inArticle'] - placements.forEach(placement => { - it('should be ' + placement, function() { + ); + }; + const placements = [ + 'banner', + 'video', + 'inImage', + 'inScreen', + 'inArticle', + ]; + placements.forEach((placement) => { + it('should be ' + placement, function () { const isBidRequestValid = spec.isBidRequestValid( createBannerSlotConfig(placement) - ) - expect(isBidRequestValid).to.equal(true) - }) - }) - }) - }) - describe('when video slot has all mandatory params', function() { + ); + expect(isBidRequestValid).to.equal(true); + }); + }); + }); + }); + describe('when video slot has all mandatory params', function () { it('should return true, when video context is instream', function () { const slotConfig = getSlotConfigs( { video: { context: 'instream', - playerSize: [[600, 200]] - } + playerSize: [[600, 200]], + }, }, { publisherId: PUBLISHER_ID, adUnitId: ADUNIT_ID, - placement: 'video' + placement: 'video', } - ) - const isBidRequestValid = spec.isBidRequestValid(slotConfig) - expect(isBidRequestValid).to.equal(true) - }) + ); + const isBidRequestValid = spec.isBidRequestValid(slotConfig); + expect(isBidRequestValid).to.equal(true); + }); it('should return true, when video context is outstream', function () { const slotConfig = getSlotConfigs( { video: { context: 'outstream', - playerSize: [[600, 200]] - } + playerSize: [[600, 200]], + }, }, { publisherId: PUBLISHER_ID, adUnitId: ADUNIT_ID, - placement: 'video' + placement: 'video', } - ) - const isBidRequestValid = spec.isBidRequestValid(slotConfig) - expect(isBidRequestValid).to.equal(true) - }) - }) - }) - describe('returns false', function() { - describe('when params are not correct', function() { + ); + const isBidRequestValid = spec.isBidRequestValid(slotConfig); + expect(isBidRequestValid).to.equal(true); + }); + }); + }); + describe('returns false', function () { + describe('when params are not correct', function () { function createSlotConfig(params) { - return getSlotConfigs({ banner: {} }, params) + return getSlotConfigs({ banner: {} }, params); } - it('does not have the PublisherToken.', function() { + it('does not have the PublisherToken.', function () { const isBidRequestValid = spec.isBidRequestValid( createSlotConfig({ adUnitId: ADUNIT_ID, - placement: 'banner' + placement: 'banner', }) - ) - expect(isBidRequestValid).to.equal(false) - }) - it('does not have the AdUnitId.', function() { + ); + expect(isBidRequestValid).to.equal(false); + }); + it('does not have the AdUnitId.', function () { const isBidRequestValid = spec.isBidRequestValid( createSlotConfig({ publisherId: PUBLISHER_ID, - placement: 'banner' + placement: 'banner', }) - ) - expect(isBidRequestValid).to.equal(false) - }) - it('does not have the placement.', function() { + ); + expect(isBidRequestValid).to.equal(false); + }); + it('does not have the placement.', function () { const isBidRequestValid = spec.isBidRequestValid( createSlotConfig({ publisherId: PUBLISHER_ID, - adUnitId: ADUNIT_ID + adUnitId: ADUNIT_ID, }) - ) - expect(isBidRequestValid).to.equal(false) - }) - it('does not have a the correct placement.', function() { + ); + expect(isBidRequestValid).to.equal(false); + }); + it('does not have a the correct placement.', function () { const isBidRequestValid = spec.isBidRequestValid( createSlotConfig({ publisherId: PUBLISHER_ID, adUnitId: ADUNIT_ID, - placement: 'another_thing' + placement: 'another_thing', }) - ) - expect(isBidRequestValid).to.equal(false) - }) - }) - describe('when video mediaType object is not correct', function() { + ); + expect(isBidRequestValid).to.equal(false); + }); + }); + describe('when video mediaType object is not correct', function () { function createVideoSlotconfig(mediaType) { return getSlotConfigs(mediaType, { publisherId: PUBLISHER_ID, adUnitId: ADUNIT_ID, - placement: 'video' - }) + placement: 'video', + }); } - it('is a void object', function() { + it('is a void object', function () { const isBidRequestValid = spec.isBidRequestValid( createVideoSlotConfig({ video: {} }) - ) - expect(isBidRequestValid).to.equal(false) - }) - it('does not have playerSize.', function() { + ); + expect(isBidRequestValid).to.equal(false); + }); + it('does not have playerSize.', function () { const isBidRequestValid = spec.isBidRequestValid( createVideoSlotConfig({ video: { context: 'instream' } }) - ) - expect(isBidRequestValid).to.equal(false) - }) + ); + expect(isBidRequestValid).to.equal(false); + }); it('is outstream ', function () { const isBidRequestValid = spec.isBidRequestValid( createVideoSlotConfig({ video: { context: 'outstream', - playerSize: [[600, 200]] - } + playerSize: [[600, 200]], + }, }) - ) - expect(isBidRequestValid).to.equal(true) - }) - describe('order does not matter', function() { - it('when video is not the first slot', function() { + ); + expect(isBidRequestValid).to.equal(true); + }); + describe('order does not matter', function () { + it('when video is not the first slot', function () { const isBidRequestValid = spec.isBidRequestValid( createVideoSlotConfig({ banner: {}, video: {} }) - ) - expect(isBidRequestValid).to.equal(false) - }) - it('when video is the first slot', function() { + ); + expect(isBidRequestValid).to.equal(false); + }); + it('when video is the first slot', function () { const isBidRequestValid = spec.isBidRequestValid( createVideoSlotConfig({ video: {}, banner: {} }) - ) - expect(isBidRequestValid).to.equal(false) - }) - }) - }) - }) - }) + ); + expect(isBidRequestValid).to.equal(false); + }); + }); + }); + }); + }); - describe('buildRequests method', function() { + describe('buildRequests method', function () { const bidderRequest = { refererInfo: { referer: 'referer' }, - timeout: 1000 - } + timeout: 1000, + }; const mandatoryParams = { publisherId: PUBLISHER_ID, adUnitId: ADUNIT_ID, - placement: 'banner' - } + placement: 'banner', + }; const inStreamParams = Object.assign({}, mandatoryParams, { video: { - mimes: 'mp4' - } - }) + mimes: 'mp4', + }, + }); const validBidRequests = [ getSlotConfigs({ banner: {} }, mandatoryParams), getSlotConfigs( { video: { context: 'instream', playerSize: [[300, 200]] } }, inStreamParams - ) - ] - it('Url params should be correct ', function() { - const request = spec.buildRequests(validBidRequests, bidderRequest) - expect(request.method).to.equal('POST') - expect(request.url).to.equal('https://s.seedtag.com/c/hb/bid') - }) + ), + ]; + it('Url params should be correct ', function () { + const request = spec.buildRequests(validBidRequests, bidderRequest); + expect(request.method).to.equal('POST'); + expect(request.url).to.equal('https://s.seedtag.com/c/hb/bid'); + }); - it('Common data request should be correct', function() { - const request = spec.buildRequests(validBidRequests, bidderRequest) - const data = JSON.parse(request.data) - expect(data.url).to.equal('referer') - expect(data.publisherToken).to.equal('0000-0000-01') - expect(typeof data.version).to.equal('string') - expect(['fixed', 'mobile', 'unknown'].indexOf(data.connectionType)).to.be.above(-1) - expect(data.bidRequests[0].adUnitCode).to.equal('adunit-code') - }) + it('Common data request should be correct', function () { + const request = spec.buildRequests(validBidRequests, bidderRequest); + const data = JSON.parse(request.data); + expect(data.url).to.equal('referer'); + expect(data.publisherToken).to.equal('0000-0000-01'); + expect(typeof data.version).to.equal('string'); + expect( + ['fixed', 'mobile', 'unknown'].indexOf(data.connectionType) + ).to.be.above(-1); + expect(data.bidRequests[0].adUnitCode).to.equal('adunit-code'); + }); - describe('adPosition param', function() { - it('should sended when publisher set adPosition param', function() { + describe('adPosition param', function () { + it('should sended when publisher set adPosition param', function () { const params = Object.assign({}, mandatoryParams, { - adPosition: 1 - }) - const validBidRequests = [getSlotConfigs({ banner: {} }, params)] - const request = spec.buildRequests(validBidRequests, bidderRequest) - const data = JSON.parse(request.data) - expect(data.bidRequests[0].adPosition).to.equal(1) - }) - it('should not sended when publisher has not set adPosition param', function() { + adPosition: 1, + }); + const validBidRequests = [getSlotConfigs({ banner: {} }, params)]; + const request = spec.buildRequests(validBidRequests, bidderRequest); + const data = JSON.parse(request.data); + expect(data.bidRequests[0].adPosition).to.equal(1); + }); + it('should not sended when publisher has not set adPosition param', function () { const validBidRequests = [ - getSlotConfigs({ banner: {} }, mandatoryParams) - ] - const request = spec.buildRequests(validBidRequests, bidderRequest) - const data = JSON.parse(request.data) - expect(data.bidRequests[0].adPosition).to.equal(undefined) - }) - }) + getSlotConfigs({ banner: {} }, mandatoryParams), + ]; + const request = spec.buildRequests(validBidRequests, bidderRequest); + const data = JSON.parse(request.data); + expect(data.bidRequests[0].adPosition).to.equal(undefined); + }); + }); - describe('GDPR params', function() { - describe('when there arent consent management platform', function() { - it('cmp should be false', function() { - const request = spec.buildRequests(validBidRequests, bidderRequest) - const data = JSON.parse(request.data) - expect(data.cmp).to.equal(false) - }) - }) - describe('when there are consent management platform', function() { - it('cmps should be true and ga should not sended, when gdprApplies is undefined', function() { + describe('GDPR params', function () { + describe('when there arent consent management platform', function () { + it('cmp should be false', function () { + const request = spec.buildRequests(validBidRequests, bidderRequest); + const data = JSON.parse(request.data); + expect(data.cmp).to.equal(false); + }); + }); + describe('when there are consent management platform', function () { + it('cmps should be true and ga should not sended, when gdprApplies is undefined', function () { bidderRequest['gdprConsent'] = { gdprApplies: undefined, - consentString: 'consentString' - } - const request = spec.buildRequests(validBidRequests, bidderRequest) - const data = JSON.parse(request.data) - expect(data.cmp).to.equal(true) - expect(Object.keys(data).indexOf('data')).to.equal(-1) - expect(data.cd).to.equal('consentString') - }) - it('cmps should be true and all gdpr parameters should be sended, when there are gdprApplies', function() { + consentString: 'consentString', + }; + const request = spec.buildRequests(validBidRequests, bidderRequest); + const data = JSON.parse(request.data); + expect(data.cmp).to.equal(true); + expect(Object.keys(data).indexOf('data')).to.equal(-1); + expect(data.cd).to.equal('consentString'); + }); + it('cmps should be true and all gdpr parameters should be sended, when there are gdprApplies', function () { bidderRequest['gdprConsent'] = { gdprApplies: true, - consentString: 'consentString' - } - const request = spec.buildRequests(validBidRequests, bidderRequest) - const data = JSON.parse(request.data) - expect(data.cmp).to.equal(true) - expect(data.ga).to.equal(true) - expect(data.cd).to.equal('consentString') - }) - it('should expose gvlid', function() { - expect(spec.gvlid).to.equal(157) - }) - }) - }) + consentString: 'consentString', + }; + const request = spec.buildRequests(validBidRequests, bidderRequest); + const data = JSON.parse(request.data); + expect(data.cmp).to.equal(true); + expect(data.ga).to.equal(true); + expect(data.cd).to.equal('consentString'); + }); + it('should expose gvlid', function () { + expect(spec.gvlid).to.equal(157); + }); + }); + }); - describe('BidRequests params', function() { - const request = spec.buildRequests(validBidRequests, bidderRequest) - const data = JSON.parse(request.data) - const bidRequests = data.bidRequests - it('should request a Banner', function() { - const bannerBid = bidRequests[0] - expect(bannerBid.id).to.equal('30b31c1838de1e') + describe('BidRequests params', function () { + const request = spec.buildRequests(validBidRequests, bidderRequest); + const data = JSON.parse(request.data); + const bidRequests = data.bidRequests; + it('should request a Banner', function () { + const bannerBid = bidRequests[0]; + expect(bannerBid.id).to.equal('30b31c1838de1e'); expect(bannerBid.transactionId).to.equal( 'd704d006-0d6e-4a09-ad6c-179e7e758096' - ) - expect(bannerBid.supplyTypes[0]).to.equal('display') - expect(bannerBid.adUnitId).to.equal('000000') - expect(bannerBid.sizes[0][0]).to.equal(300) - expect(bannerBid.sizes[0][1]).to.equal(250) - expect(bannerBid.sizes[1][0]).to.equal(300) - expect(bannerBid.sizes[1][1]).to.equal(600) - expect(bannerBid.requestCount).to.equal(1) - }) - it('should request an InStream Video', function() { - const videoBid = bidRequests[1] - expect(videoBid.id).to.equal('30b31c1838de1e') + ); + expect(bannerBid.supplyTypes[0]).to.equal('display'); + expect(bannerBid.adUnitId).to.equal('000000'); + expect(bannerBid.sizes[0][0]).to.equal(300); + expect(bannerBid.sizes[0][1]).to.equal(250); + expect(bannerBid.sizes[1][0]).to.equal(300); + expect(bannerBid.sizes[1][1]).to.equal(600); + expect(bannerBid.requestCount).to.equal(1); + }); + it('should request an InStream Video', function () { + const videoBid = bidRequests[1]; + expect(videoBid.id).to.equal('30b31c1838de1e'); expect(videoBid.transactionId).to.equal( 'd704d006-0d6e-4a09-ad6c-179e7e758096' - ) - expect(videoBid.supplyTypes[0]).to.equal('video') - expect(videoBid.adUnitId).to.equal('000000') - expect(videoBid.videoParams.mimes).to.equal('mp4') - expect(videoBid.videoParams.w).to.equal(300) - expect(videoBid.videoParams.h).to.equal(200) - expect(videoBid.sizes[0][0]).to.equal(300) - expect(videoBid.sizes[0][1]).to.equal(250) - expect(videoBid.sizes[1][0]).to.equal(300) - expect(videoBid.sizes[1][1]).to.equal(600) - expect(videoBid.requestCount).to.equal(1) - }) - }) - }) + ); + expect(videoBid.supplyTypes[0]).to.equal('video'); + expect(videoBid.adUnitId).to.equal('000000'); + expect(videoBid.videoParams.mimes).to.equal('mp4'); + expect(videoBid.videoParams.w).to.equal(300); + expect(videoBid.videoParams.h).to.equal(200); + expect(videoBid.sizes[0][0]).to.equal(300); + expect(videoBid.sizes[0][1]).to.equal(250); + expect(videoBid.sizes[1][0]).to.equal(300); + expect(videoBid.sizes[1][1]).to.equal(600); + expect(videoBid.requestCount).to.equal(1); + }); + }); + }); - describe('interpret response method', function() { - it('should return a void array, when the server response are not correct.', function() { - const request = { data: JSON.stringify({}) } + describe('interpret response method', function () { + it('should return a void array, when the server response are not correct.', function () { + const request = { data: JSON.stringify({}) }; const serverResponse = { - body: {} - } - const bids = spec.interpretResponse(serverResponse, request) - expect(typeof bids).to.equal('object') - expect(bids.length).to.equal(0) - }) - it('should return a void array, when the server response have no bids.', function() { - const request = { data: JSON.stringify({}) } - const serverResponse = { body: { bids: [] } } - const bids = spec.interpretResponse(serverResponse, request) - expect(typeof bids).to.equal('object') - expect(bids.length).to.equal(0) - }) - describe('when the server response return a bid', function() { - describe('the bid is a banner', function() { - it('should return a banner bid', function() { - const request = { data: JSON.stringify({}) } + body: {}, + }; + const bids = spec.interpretResponse(serverResponse, request); + expect(typeof bids).to.equal('object'); + expect(bids.length).to.equal(0); + }); + it('should return a void array, when the server response have no bids.', function () { + const request = { data: JSON.stringify({}) }; + const serverResponse = { body: { bids: [] } }; + const bids = spec.interpretResponse(serverResponse, request); + expect(typeof bids).to.equal('object'); + expect(bids.length).to.equal(0); + }); + describe('when the server response return a bid', function () { + describe('the bid is a banner', function () { + it('should return a banner bid', function () { + const request = { data: JSON.stringify({}) }; const serverResponse = { body: { bids: [ @@ -352,28 +363,30 @@ describe('Seedtag Adapter', function() { mediaType: 'display', ttl: 360, nurl: 'testurl.com/nurl', - adomain: ['advertiserdomain.com'] - } + adomain: ['advertiserdomain.com'], + }, ], - cookieSync: { url: '' } - } - } - const bids = spec.interpretResponse(serverResponse, request) - expect(bids.length).to.equal(1) - expect(bids[0].requestId).to.equal('2159a54dc2566f') - expect(bids[0].cpm).to.equal(0.5) - expect(bids[0].width).to.equal(728) - expect(bids[0].height).to.equal(90) - expect(bids[0].currency).to.equal('USD') - expect(bids[0].netRevenue).to.equal(true) - expect(bids[0].ad).to.equal('content') - expect(bids[0].nurl).to.equal('testurl.com/nurl') - expect(bids[0].meta.advertiserDomains).to.deep.equal(['advertiserdomain.com']) - }) - }) - describe('the bid is a video', function() { - it('should return a instream bid', function() { - const request = { data: JSON.stringify({}) } + cookieSync: { url: '' }, + }, + }; + const bids = spec.interpretResponse(serverResponse, request); + expect(bids.length).to.equal(1); + expect(bids[0].requestId).to.equal('2159a54dc2566f'); + expect(bids[0].cpm).to.equal(0.5); + expect(bids[0].width).to.equal(728); + expect(bids[0].height).to.equal(90); + expect(bids[0].currency).to.equal('USD'); + expect(bids[0].netRevenue).to.equal(true); + expect(bids[0].ad).to.equal('content'); + expect(bids[0].nurl).to.equal('testurl.com/nurl'); + expect(bids[0].meta.advertiserDomains).to.deep.equal([ + 'advertiserdomain.com', + ]); + }); + }); + describe('the bid is a video', function () { + it('should return a instream bid', function () { + const request = { data: JSON.stringify({}) }; const serverResponse = { body: { bids: [ @@ -386,114 +399,124 @@ describe('Seedtag Adapter', function() { height: 90, mediaType: 'video', ttl: 360, - nurl: undefined - } + nurl: undefined, + }, ], - cookieSync: { url: '' } - } - } - const bids = spec.interpretResponse(serverResponse, request) - expect(bids.length).to.equal(1) - expect(bids[0].requestId).to.equal('2159a54dc2566f') - expect(bids[0].cpm).to.equal(0.5) - expect(bids[0].width).to.equal(728) - expect(bids[0].height).to.equal(90) - expect(bids[0].currency).to.equal('USD') - expect(bids[0].netRevenue).to.equal(true) - expect(bids[0].vastXml).to.equal('content') - expect(bids[0].meta.advertiserDomains).to.deep.equal([]) - }) - }) - }) - }) + cookieSync: { url: '' }, + }, + }; + const bids = spec.interpretResponse(serverResponse, request); + expect(bids.length).to.equal(1); + expect(bids[0].requestId).to.equal('2159a54dc2566f'); + expect(bids[0].cpm).to.equal(0.5); + expect(bids[0].width).to.equal(728); + expect(bids[0].height).to.equal(90); + expect(bids[0].currency).to.equal('USD'); + expect(bids[0].netRevenue).to.equal(true); + expect(bids[0].vastXml).to.equal('content'); + expect(bids[0].meta.advertiserDomains).to.deep.equal([]); + }); + }); + }); + }); - describe('user syncs method', function() { - it('should return empty array, when iframe sync option are disabled.', function() { - const syncOption = { iframeEnabled: false } - const serverResponses = [{ body: { cookieSync: 'someUrl' } }] - const cookieSyncArray = spec.getUserSyncs(syncOption, serverResponses) - expect(cookieSyncArray.length).to.equal(0) - }) - it('should return empty array, when the server response are wrong.', function() { - const syncOption = { iframeEnabled: true } - const serverResponses = [{ body: {} }] - const cookieSyncArray = spec.getUserSyncs(syncOption, serverResponses) - expect(cookieSyncArray.length).to.equal(0) - }) - it('should return empty array, when the server response are void.', function() { - const syncOption = { iframeEnabled: true } - const serverResponses = [{ body: { cookieSync: '' } }] - const cookieSyncArray = spec.getUserSyncs(syncOption, serverResponses) - expect(cookieSyncArray.length).to.equal(0) - }) - it('should return a array with the cookie sync, when the server response with a cookie sync.', function() { - const syncOption = { iframeEnabled: true } - const serverResponses = [{ body: { cookieSync: 'someUrl' } }] - const cookieSyncArray = spec.getUserSyncs(syncOption, serverResponses) - expect(cookieSyncArray.length).to.equal(1) - expect(cookieSyncArray[0].type).to.equal('iframe') - expect(cookieSyncArray[0].url).to.equal('someUrl') - }) - }) + describe('user syncs method', function () { + it('should return empty array, when iframe sync option are disabled.', function () { + const syncOption = { iframeEnabled: false }; + const serverResponses = [{ body: { cookieSync: 'someUrl' } }]; + const cookieSyncArray = spec.getUserSyncs(syncOption, serverResponses); + expect(cookieSyncArray.length).to.equal(0); + }); + it('should return empty array, when the server response are wrong.', function () { + const syncOption = { iframeEnabled: true }; + const serverResponses = [{ body: {} }]; + const cookieSyncArray = spec.getUserSyncs(syncOption, serverResponses); + expect(cookieSyncArray.length).to.equal(0); + }); + it('should return empty array, when the server response are void.', function () { + const syncOption = { iframeEnabled: true }; + const serverResponses = [{ body: { cookieSync: '' } }]; + const cookieSyncArray = spec.getUserSyncs(syncOption, serverResponses); + expect(cookieSyncArray.length).to.equal(0); + }); + it('should return a array with the cookie sync, when the server response with a cookie sync.', function () { + const syncOption = { iframeEnabled: true }; + const serverResponses = [{ body: { cookieSync: 'someUrl' } }]; + const cookieSyncArray = spec.getUserSyncs(syncOption, serverResponses); + expect(cookieSyncArray.length).to.equal(1); + expect(cookieSyncArray[0].type).to.equal('iframe'); + expect(cookieSyncArray[0].url).to.equal('someUrl'); + }); + }); describe('onTimeout', function () { - beforeEach(function() { - sinon.stub(utils, 'triggerPixel') - }) + beforeEach(function () { + sinon.stub(utils, 'triggerPixel'); + }); - afterEach(function() { - utils.triggerPixel.restore() - }) + afterEach(function () { + utils.triggerPixel.restore(); + }); it('should return the correct endpoint', function () { - const params = { publisherId: '0000', adUnitId: '11111' } - const timeoutData = [{ params: [ params ] }]; + const params = { publisherId: '0000', adUnitId: '11111' }; + const timeout = 3000; + const timeoutData = [{ params: [params], timeout }]; const timeoutUrl = getTimeoutUrl(timeoutData); expect(timeoutUrl).to.equal( 'https://s.seedtag.com/se/hb/timeout?publisherToken=' + - params.publisherId + - '&adUnitId=' + - params.adUnitId - ) - }) + params.publisherId + + '&adUnitId=' + + params.adUnitId + + '&timeout=' + + timeout + ); + }); - it('should set the timeout pixel', function() { - const params = { publisherId: '0000', adUnitId: '11111' } - const timeoutData = [{ params: [ params ] }]; - spec.onTimeout(timeoutData) - expect(utils.triggerPixel.calledWith('https://s.seedtag.com/se/hb/timeout?publisherToken=' + - params.publisherId + - '&adUnitId=' + - params.adUnitId)).to.equal(true); - }) - }) + it('should set the timeout pixel', function () { + const params = { publisherId: '0000', adUnitId: '11111' }; + const timeout = 3000; + const timeoutData = [{ params: [params], timeout }]; + spec.onTimeout(timeoutData); + expect( + utils.triggerPixel.calledWith( + 'https://s.seedtag.com/se/hb/timeout?publisherToken=' + + params.publisherId + + '&adUnitId=' + + params.adUnitId + + '&timeout=' + + timeout + ) + ).to.equal(true); + }); + }); describe('onBidWon', function () { - beforeEach(function() { - sinon.stub(utils, 'triggerPixel') - }) + beforeEach(function () { + sinon.stub(utils, 'triggerPixel'); + }); - afterEach(function() { - utils.triggerPixel.restore() - }) + afterEach(function () { + utils.triggerPixel.restore(); + }); - describe('without nurl', function() { - const bid = {} + describe('without nurl', function () { + const bid = {}; - it('does not create pixel ', function() { - spec.onBidWon(bid) + it('does not create pixel ', function () { + spec.onBidWon(bid); expect(utils.triggerPixel.called).to.equal(false); - }) - }) + }); + }); describe('with nurl', function () { - const nurl = 'http://seedtag_domain/won' - const bid = { nurl } + const nurl = 'http://seedtag_domain/won'; + const bid = { nurl }; - it('creates nurl pixel if bid nurl', function() { - spec.onBidWon({ nurl }) + it('creates nurl pixel if bid nurl', function () { + spec.onBidWon({ nurl }); expect(utils.triggerPixel.calledWith(nurl)).to.equal(true); - }) - }) - }) -}) + }); + }); + }); +}); diff --git a/test/spec/modules/sizeMappingV2_spec.js b/test/spec/modules/sizeMappingV2_spec.js index ab06ddc8f51..9bbd472c7e0 100644 --- a/test/spec/modules/sizeMappingV2_spec.js +++ b/test/spec/modules/sizeMappingV2_spec.js @@ -13,10 +13,11 @@ import { getAdUnitDetail, getFilteredMediaTypes, getBids, - internal + internal, setupAdUnitMediaTypes } from '../../../modules/sizeMappingV2.js'; import { adUnitSetupChecks } from '../../../src/prebid.js'; +import {deepClone} from '../../../src/utils.js'; const AD_UNITS = [{ code: 'div-gpt-ad-1460505748561-0', @@ -193,49 +194,23 @@ describe('sizeMappingV2', function () { utils.logError.restore(); }); - it('should filter out adUnit if it does not contain the required property mediaTypes', function () { - let adUnits = utils.deepClone(AD_UNITS); - delete adUnits[0].mediaTypes; - // before checkAdUnitSetupHook is called, the length of adUnits should be '2' - expect(adUnits.length).to.equal(2); - adUnits = checkAdUnitSetupHook(adUnits); - - // after checkAdUnitSetupHook is called, the length of adUnits should be '1' - expect(adUnits.length).to.equal(1); - expect(adUnits[0].code).to.equal('div-gpt-ad-1460505748561-1'); - }); + describe('basic validation', () => { + let validateAdUnit; - it('should filter out adUnit if it does not contain the required property "bids"', function() { - let adUnits = utils.deepClone(AD_UNITS); - delete adUnits[0].mediaTypes; - // before checkAdUnitSetupHook is called, the length of the adUnits should equal '2' - expect(adUnits.length).to.equal(2); - adUnits = checkAdUnitSetupHook(adUnits); - - // after checkAdUnitSetupHook is called, the length of the adUnits should be '1' - expect(adUnits.length).to.equal(1); - expect(adUnits[0].code).to.equal('div-gpt-ad-1460505748561-1'); - }); - - it('should filter out adUnit if it has declared property mediaTypes with an empty object', function () { - let adUnits = utils.deepClone(AD_UNITS); - adUnits[0].mediaTypes = {}; - // before checkAdUnitSetupHook is called, the length of adUnits should be '2' - expect(adUnits.length).to.equal(2); - adUnits = checkAdUnitSetupHook(adUnits); - - // after checkAdUnitSetupHook is called, the length of adUnits should be '1' - expect(adUnits.length).to.equal(1); - expect(adUnits[0].code).to.equal('div-gpt-ad-1460505748561-1'); - }); + beforeEach(() => { + validateAdUnit = sinon.stub(adUnitSetupChecks, 'validateAdUnit'); + }); - it('should log an error message if Ad Unit does not contain the required property "mediaTypes"', function () { - let adUnits = utils.deepClone(AD_UNITS); - delete adUnits[0].mediaTypes; + afterEach(() => { + validateAdUnit.restore(); + }); - checkAdUnitSetupHook(adUnits); - sinon.assert.callCount(utils.logError, 1); - sinon.assert.calledWith(utils.logError, 'Detected adUnit.code \'div-gpt-ad-1460505748561-0\' did not have a \'mediaTypes\' object defined. This is a required field for the auction, so this adUnit has been removed.'); + it('should filter out adUnits that do not pass adUnitSetupChecks.validateAdUnit', () => { + validateAdUnit.returns(null); + const adUnits = checkAdUnitSetupHook(utils.deepClone(AD_UNITS)); + AD_UNITS.forEach((u) => sinon.assert.calledWith(validateAdUnit, u)); + expect(adUnits.length).to.equal(0); + }); }); describe('banner mediaTypes checks', function () { @@ -1106,14 +1081,14 @@ describe('sizeMappingV2', function () { internal.checkBidderSizeConfigFormat.restore(); internal.getActiveSizeBucket.restore(); }); - it('should return an empty array if the bidder sizeConfig object is not formatted correctly', function () { + it('should return an empty set if the bidder sizeConfig object is not formatted correctly', function () { const sizeConfig = [ { minViewPort: [], relevantMediaTypes: ['none'] }, { minViewPort: [700, 0], relevantMediaTypes: ['banner', 'video'] } ]; const activeViewport = [720, 600]; const relevantMediaTypes = getRelevantMediaTypesForBidder(sizeConfig, activeViewport); - expect(relevantMediaTypes).to.deep.equal([]); + expect(relevantMediaTypes.size).to.equal(0) }); it('should call function checkBidderSizeConfigFormat() once', function () { @@ -1140,220 +1115,51 @@ describe('sizeMappingV2', function () { sinon.assert.calledWith(internal.getActiveSizeBucket, sizeConfig, activeViewport); }); - it('should return the array contained in "relevantMediaTypes" property whose sizeBucket matches with the current viewport', function () { + it('should return the types contained in "relevantMediaTypes" property whose sizeBucket matches with the current viewport', function () { const sizeConfig = [ { minViewPort: [0, 0], relevantMediaTypes: ['none'] }, { minViewPort: [700, 0], relevantMediaTypes: ['banner', 'video'] } ]; const activeVewport = [720, 600]; const relevantMediaTypes = getRelevantMediaTypesForBidder(sizeConfig, activeVewport); - expect(relevantMediaTypes).to.deep.equal(['banner', 'video']); + expect([...relevantMediaTypes]).to.deep.equal(['banner', 'video']); }); }); - describe('getAdUnitDetail(auctionId, adUnit, labels)', function () { + describe('getAdUnitDetail', function () { const adUnitDetailFixture_1 = { - adUnitCode: 'div-gpt-ad-1460505748561-0', - mediaTypes: { - banner: { - sizeConfig: [ - { minViewPort: [0, 0], sizes: [] }, // remove if < 750px - { minViewPort: [750, 0], sizes: [[300, 250], [300, 600]] }, // between 750px and 1199px - { minViewPort: [1200, 0], sizes: [[970, 90], [728, 90], [300, 250]] }, // between 1200px and 1599px - { minViewPort: [1600, 0], sizes: [[1000, 300], [970, 90], [728, 90], [300, 250]] } // greater than 1600px - ] - }, - video: { - context: 'instream', - sizeConfig: [ - { minViewPort: [0, 0], playerSize: [] }, - { minViewPort: [800, 0], playerSize: [[640, 400]] }, - { minViewPort: [1200, 0], playerSize: [] } - ] - }, - native: { - image: { - required: true, - sizes: [150, 50] - }, - title: { - required: true, - len: 80 - }, - sponsoredBy: { - required: true - }, - clickUrl: { - required: true - }, - privacyLink: { - required: false - }, - body: { - required: true - }, - icon: { - required: true, - sizes: [50, 50] - }, - sizeConfig: [ - { minViewPort: [0, 0], active: false }, - { minViewPort: [600, 0], active: true }, - { minViewPort: [1000, 0], active: false } - ] - } - }, sizeBucketToSizeMap: {}, activeViewport: {}, - transformedMediaTypes: {}, - cacheHits: 0, - instance: 1, - isLabelActivated: true, - }; - const adUnitDetailFixture_2 = { - adUnitCode: 'div-gpt-ad-1460505748561-1', - mediaTypes: { - banner: { - sizes: [[300, 250], [300, 600]] - }, - video: { - context: 'instream', - playerSize: [300, 460] - } - }, - sizeBucketToSizeMap: {}, - activeViewport: {}, - cacheHits: 0, - instance: 1, - isLabelActivated: true, transformedMediaTypes: { banner: {}, video: {} } } - // adunit with same code at adUnitDetailFixture_1 but differnet mediaTypes object - const adUnitDetailFixture_3 = { - adUnitCode: 'div-gpt-ad-1460505748561-0', - mediaTypes: { - banner: { - sizeConfig: [ - { minViewPort: [0, 0], sizes: [] }, - { minViewPort: [1000, 0], sizes: [[1000, 300], [1000, 90], [970, 250], [970, 90], [728, 90]] } - ] - } - }, - sizeBucketToSizeMap: {}, - activeViewport: {}, - transformedMediaTypes: {}, - cacheHits: 0, - instance: 1, - isLabelActivated: true, - } const labels = ['mobile']; beforeEach(function () { - sinon - .stub(sizeMappingInternalStore, 'getAuctionDetail') - .withArgs('a1b2c3') - .returns({ - usingSizeMappingV2: true, - adUnits: [adUnitDetailFixture_1] - }); - - sinon - .stub(sizeMappingInternalStore, 'setAuctionDetail') - .withArgs('a1b2c3', adUnitDetailFixture_2); - const getFilteredMediaTypesStub = sinon.stub(internal, 'getFilteredMediaTypes'); getFilteredMediaTypesStub .withArgs(AD_UNITS[1].mediaTypes) - .returns(adUnitDetailFixture_2); - - getFilteredMediaTypesStub - .withArgs(adUnitDetailFixture_3.mediaTypes) - .returns(adUnitDetailFixture_3); - + .returns(adUnitDetailFixture_1); sinon.spy(utils, 'logInfo'); sinon.spy(utils, 'deepEqual'); }); afterEach(function () { - sizeMappingInternalStore.getAuctionDetail.restore(); - sizeMappingInternalStore.setAuctionDetail.restore(); internal.getFilteredMediaTypes.restore(); utils.logInfo.restore(); utils.deepEqual.restore(); }); - it('should return adUnit detail object from "sizeMappingInternalStore" if adUnit is already present in the store', function () { - const [adUnit] = utils.deepClone(AD_UNITS); - const adUnitDetail = getAdUnitDetail('a1b2c3', adUnit, labels); - sinon.assert.callCount(sizeMappingInternalStore.getAuctionDetail, 1); - sinon.assert.callCount(utils.deepEqual, 1); - sinon.assert.callCount(internal.getFilteredMediaTypes, 0); - expect(adUnitDetail.cacheHits).to.equal(1); - expect(adUnitDetail).to.deep.equal(adUnitDetailFixture_1); - }); - - it('should NOT return adunit detail object from "sizeMappingInternalStore" if adUnit with the SAME CODE BUT DIFFERENT MEDIATYPES OBJECT is present in the store', function () { - const [adUnit] = utils.deepClone(AD_UNITS); - adUnit.mediaTypes = { - banner: { - sizeConfig: [ - { minViewPort: [0, 0], sizes: [] }, - { minViewPort: [1000, 0], sizes: [[1000, 300], [1000, 90], [970, 250], [970, 90], [728, 90]] } - ] - } - }; - const adUnitDetail = getAdUnitDetail('a1b2c3', adUnit, labels); - sinon.assert.callCount(sizeMappingInternalStore.getAuctionDetail, 1); - sinon.assert.callCount(utils.deepEqual, 1); - expect(adUnitDetail).to.not.deep.equal(adUnitDetailFixture_1); - sinon.assert.callCount(internal.getFilteredMediaTypes, 1); - }); - - it('should store value in "sizeMappingInterStore" object if adUnit is NOT preset in this object', function () { - const [, adUnit] = utils.deepClone(AD_UNITS); - const adUnitDetail = getAdUnitDetail('a1b2c3', adUnit, labels); - sinon.assert.callCount(sizeMappingInternalStore.setAuctionDetail, 1); - sinon.assert.callCount(internal.getFilteredMediaTypes, 1); - expect(adUnitDetail).to.deep.equal(adUnitDetailFixture_2); - }); - it('should log info message to show the details for activeSizeBucket', function () { const [, adUnit] = utils.deepClone(AD_UNITS); - getAdUnitDetail('a1b2c3', adUnit, labels); + getAdUnitDetail(adUnit, labels, 1); sinon.assert.callCount(utils.logInfo, 1); - sinon.assert.calledWith(utils.logInfo, `Size Mapping V2:: Ad Unit: div-gpt-ad-1460505748561-1(1) => Active size buckets after filtration: `, adUnitDetailFixture_2.sizeBucketToSizeMap); - }); - - it('should increment "instance" count if presence of "Identical ad units" is detected', function () { - const adUnit = { - code: 'div-gpt-ad-1460505748561-0', - mediaTypes: { - banner: { - sizeConfig: [{ minViewPort: [0, 0], sizes: [[300, 300]] }] - } - }, - bids: [{ - bidder: 'appnexus', - params: 12 - }] - }; - - internal.getFilteredMediaTypes.restore(); - - sinon.stub(internal, 'getFilteredMediaTypes') - .withArgs(adUnit.mediaTypes) - .returns({ mediaTypes: {}, sizeBucketToSizeMap: {}, activeViewPort: [], transformedMediaTypes: {} }); - - const adUnitDetail = getAdUnitDetail('a1b2c3', adUnit, labels); - sinon.assert.callCount(sizeMappingInternalStore.setAuctionDetail, 1); - sinon.assert.callCount(internal.getFilteredMediaTypes, 1); - expect(adUnitDetail.instance).to.equal(2); + sinon.assert.calledWith(utils.logInfo, `Size Mapping V2:: Ad Unit: div-gpt-ad-1460505748561-1(1) => Active size buckets after filtration: `, adUnitDetailFixture_1.sizeBucketToSizeMap); }); it('should not execute "getFilteredMediaTypes" function if label is not activated on the ad unit', function () { const [adUnit] = utils.deepClone(AD_UNITS); adUnit.labelAny = ['tablet']; - getAdUnitDetail('a1b2c3', adUnit, labels); + getAdUnitDetail(adUnit, labels, 1); // assertions sinon.assert.callCount(internal.getFilteredMediaTypes, 0); @@ -1376,57 +1182,8 @@ describe('sizeMappingV2', function () { utils.getWindowTop.restore(); utils.logWarn.restore(); }); - it('should return filteredMediaTypes object with all four properties (mediaTypes, transformedMediaTypes, activeViewport, sizeBucketToSizeMap) evaluated correctly', function () { + it('should return filteredMediaTypes object with all properties (transformedMediaTypes, activeViewport, sizeBucketToSizeMap) evaluated correctly', function () { const [adUnit] = utils.deepClone(AD_UNITS); - const expectedMediaTypes = { - banner: { - sizeConfig: [ - { minViewPort: [0, 0], sizes: [] }, // remove if < 750px - { minViewPort: [750, 0], sizes: [[300, 250], [300, 600]] }, // between 750px and 1199px - { minViewPort: [1200, 0], sizes: [[970, 90], [728, 90], [300, 250]] }, // between 1200px and 1599px - { minViewPort: [1600, 0], sizes: [[1000, 300], [970, 90], [728, 90], [300, 250]] } // greater than 1600px - ] - }, - video: { - context: 'instream', - sizeConfig: [ - { minViewPort: [0, 0], playerSize: [] }, - { minViewPort: [800, 0], playerSize: [[640, 400]] }, - { minViewPort: [1200, 0], playerSize: [] } - ] - }, - native: { - image: { - required: true, - sizes: [150, 50] - }, - title: { - required: true, - len: 80 - }, - sponsoredBy: { - required: true - }, - clickUrl: { - required: true - }, - privacyLink: { - required: false - }, - body: { - required: true - }, - icon: { - required: true, - sizes: [50, 50] - }, - sizeConfig: [ - { minViewPort: [0, 0], active: false }, - { minViewPort: [600, 0], active: true }, - { minViewPort: [1000, 0], active: false } - ] - } - }; const expectedSizeBucketToSizeMap = { banner: { activeSizeBucket: [1600, 0], @@ -1459,8 +1216,7 @@ describe('sizeMappingV2', function () { ] } }; - const { mediaTypes, sizeBucketToSizeMap, activeViewport, transformedMediaTypes } = getFilteredMediaTypes(adUnit.mediaTypes); - expect(mediaTypes).to.deep.equal(expectedMediaTypes); + const { sizeBucketToSizeMap, activeViewport, transformedMediaTypes } = getFilteredMediaTypes(adUnit.mediaTypes); expect(activeViewport).to.deep.equal(expectedActiveViewport); expect(sizeBucketToSizeMap).to.deep.equal(expectedSizeBucketToSizeMap); expect(transformedMediaTypes).to.deep.equal(expectedTransformedMediaTypes); @@ -1478,7 +1234,8 @@ describe('sizeMappingV2', function () { }); }); - describe('getBids({ bidderCode, auctionId, bidderRequestId, adUnits, labels, src })', function () { + describe('setupAdUnitsForLabels', function () { + let adUnits, adUnitDetail; const basic_AdUnit = [{ code: 'adUnit1', mediaTypes: { @@ -1508,46 +1265,31 @@ describe('sizeMappingV2', function () { }], transactionId: '123456' }]; - const adUnitDetailFixture = { - adUnitCode: 'adUnit1', - transactionId: '123456', - sizes: [[300, 200], [400, 600]], - mediaTypes: { - banner: { - sizeConfig: [ - { minViewPort: [0, 0], sizes: [] }, - { minViewPort: [600, 0], sizes: [[300, 200], [400, 600]] } - ] - } - }, - sizeBucketToSizeMap: { - banner: { - activeSizeBucket: [[500, 0]], - activeSizeDimensions: [[300, 200], [400, 600]] - } - }, - activeViewport: [560, 260], - transformedMediaTypes: { - banner: { - filteredSizeConfig: [ - { minViewPort: [500, 0], sizes: [[300, 200], [400, 600]] } - ], - sizeConfig: [ - { minViewPort: [0, 0], sizes: [[]] }, - { minViewPort: [500, 0], sizes: [[300, 200], [400, 600]] } - ], - sizes: [[300, 200], [400, 600]] - } - }, - isLabelActivated: true, - instance: 1, - cacheHits: 0 - }; + + const bidderMap = (adUnit) => Object.fromEntries(adUnit.bids.map((bid) => [bid.bidder, bid])); + beforeEach(function () { + adUnits = deepClone(basic_AdUnit); + adUnitDetail = { + activeViewport: [560, 260], + transformedMediaTypes: { + banner: { + filteredSizeConfig: [ + { minViewPort: [500, 0], sizes: [[300, 200], [400, 600]] } + ], + sizeConfig: [ + { minViewPort: [0, 0], sizes: [[]] }, + { minViewPort: [500, 0], sizes: [[300, 200], [400, 600]] } + ], + sizes: [[300, 200], [400, 600]] + } + }, + isLabelActivated: true, + }; sinon .stub(internal, 'getAdUnitDetail') - .withArgs('6d51e2d7-1447-4242-b6af-aaa5525a2c6e', basic_AdUnit[0], []) - .returns(adUnitDetailFixture); + .withArgs(adUnits[0], []) + .callsFake(() => adUnitDetail); sinon.spy(internal, 'getRelevantMediaTypesForBidder'); @@ -1564,153 +1306,82 @@ describe('sizeMappingV2', function () { utils.logWarn.restore(); }); - it('should return an array of bids specific to the bidder', function () { - const expectedMediaTypes = { - banner: { - filteredSizeConfig: [ - { minViewPort: [500, 0], sizes: [[300, 200], [400, 600]] } - ], - sizeConfig: [ - { minViewPort: [0, 0], sizes: [[]] }, - { minViewPort: [500, 0], sizes: [[300, 200], [400, 600]] } - ], - sizes: [[300, 200], [400, 600]] - } + it('should update adUnit mediaTypes', function () { + adUnitDetail = { + activeViewport: [560, 260], + transformedMediaTypes: { + banner: { + filteredSizeConfig: [ + { minViewPort: [500, 0], sizes: [[300, 200], [400, 600]] } + ], + sizeConfig: [ + { minViewPort: [0, 0], sizes: [[]] }, + { minViewPort: [500, 0], sizes: [[300, 200], [400, 600]] } + ], + sizes: [[300, 200], [400, 600]] + } + }, + isLabelActivated: true, }; - const bidRequests_1 = getBids({ - bidderCode: 'appnexus', - auctionId: '6d51e2d7-1447-4242-b6af-aaa5525a2c6e', - bidderRequestId: '393a43193a0ac', - adUnits: basic_AdUnit, - labels: [], - src: 'client' - }); - expect(bidRequests_1[0].mediaTypes).to.deep.equal(expectedMediaTypes); - expect(bidRequests_1[0].bidder).to.equal('appnexus'); - - const bidRequests_2 = getBids({ - bidderCode: 'rubicon', - auctionId: '6d51e2d7-1447-4242-b6af-aaa5525a2c6e', - bidderRequestId: '393a43193a0aa', - adUnits: basic_AdUnit, - labels: [], - src: 'client' - }); - expect(bidRequests_2[0]).to.be.undefined; + const actual = setupAdUnitMediaTypes(adUnits, [])[0]; + + expect(actual.mediaTypes).to.deep.equal(adUnitDetail.transformedMediaTypes); + const bids = bidderMap(actual); + expect(bids.appnexus).to.not.be.undefined; + expect(bids.appnexus.mediaTypes).to.be.undefined; + expect(bids.rubicon).to.be.undefined; sinon.assert.callCount(internal.getRelevantMediaTypesForBidder, 1); }); it('should log an error message if ad unit is disabled because there are no active media types left after size config filtration', function () { - internal.getAdUnitDetail.restore(); - - const adUnit = utils.deepClone(basic_AdUnit); - adUnit[0].mediaTypes.banner.sizeConfig = [ + adUnits[0].mediaTypes.banner.sizeConfig = [ { minViewPort: [0, 0], sizes: [] }, { minViewPort: [600, 0], sizes: [[300, 200], [400, 600]] } ]; - const adUnitDetailFixture = { - adUnitCode: 'adUnit1', - mediaTypes: { - banner: { - sizeConfig: [ - { minViewPort: [0, 0], sizes: [] }, - { minViewPort: [600, 0], sizes: [[300, 200], [400, 600]] } - ] - } - }, - sizeBucketToSizeMap: { - banner: { - activeSizeBucket: [0, 0], - activeSizeDimensions: [[]] - } - }, + adUnitDetail = { activeViewport: [560, 260], transformedMediaTypes: {}, isLabelActivated: true, - instance: 1, - cacheHits: 0 }; - sinon - .stub(internal, 'getAdUnitDetail') - .withArgs('6d51e2d7-1447-4242-b6af-aaa5525a2c6e', adUnit[0]) - .returns(adUnitDetailFixture); - - const bidRequests = getBids({ - bidderCode: 'appnexus', - auctionId: '6d51e2d7-1447-4242-b6af-aaa5525a2c6e', - bidderRequestId: '393a43193a0ac', - adUnits: adUnit, - labels: [], - src: 'client' - }); - expect(bidRequests[0]).to.be.undefined; + const actual = setupAdUnitMediaTypes(adUnits, [])[0]; + expect(actual).to.be.undefined; sinon.assert.callCount(utils.logInfo, 1); sinon.assert.calledWith(utils.logInfo, `Size Mapping V2:: Ad Unit: adUnit1(1) => Ad unit disabled since there are no active media types after sizeConfig filtration.`); }); it('should throw an error if bidder level sizeConfig is not configured properly', function () { - internal.getAdUnitDetail.restore(); - - const adUnit = utils.deepClone(basic_AdUnit); - adUnit[0].bids[1].sizeConfig = [ + adUnits[0].bids[1].sizeConfig = [ { minViewPort: [], relevantMediaTypes: ['none'] }, { minViewPort: [700, 0], relevantMediaTypes: ['banner'] } ]; - - sinon - .stub(internal, 'getAdUnitDetail') - .withArgs('6d51e2d7-1447-4242-b6af-aaa5525a2c6e', adUnit[0]) - .returns(adUnitDetailFixture); - - const bidRequests = getBids({ - bidderCode: 'rubicon', - auctionId: '6d51e2d7-1447-4242-b6af-aaa5525a2c6e', - bidderRequestId: '393a43193a0ac', - adUnits: adUnit, - labels: [], - src: 'client' - }); - - expect(bidRequests[0]).to.not.be.undefined; + const actual = setupAdUnitMediaTypes(adUnits, [])[0]; + expect(actual).to.not.be.undefined; + const bids = bidderMap(actual); + expect(bids.rubicon.mediaTypes).to.be.undefined; sinon.assert.callCount(utils.logError, 1); - sinon.assert.calledWith(utils.logError, `Size Mapping V2:: Ad Unit: adUnit1(1), Bidder: rubicon => 'sizeConfig' is not configured properly. This bidder won't be eligible for sizeConfig checks and will remail active.`); + sinon.assert.calledWith(utils.logError, `Size Mapping V2:: Ad Unit: adUnit1(1), Bidder: rubicon => 'sizeConfig' is not configured properly. This bidder won't be eligible for sizeConfig checks and will remain active.`); }); - it('should ensure bidder relevantMediaTypes is a subset of active media types at the ad unit level', function () { - internal.getAdUnitDetail.restore(); - - const adUnit = utils.deepClone(basic_AdUnit); - adUnit[0].bids[1].sizeConfig = [ + it('should ensure only relevant sizes are in adUnit.mediaTypes', function () { + adUnits[0].bids[1].sizeConfig = [ { minViewPort: [0, 0], relevantMediaTypes: ['none'] }, { minViewPort: [400, 0], relevantMediaTypes: ['banner'] } ]; - sinon - .stub(internal, 'getAdUnitDetail') - .withArgs('6d51e2d7-1447-4242-b6af-aaa5525a2c6e', adUnit[0]) - .returns(adUnitDetailFixture); - - const bidRequests = getBids({ - bidderCode: 'rubicon', - auctionId: '6d51e2d7-1447-4242-b6af-aaa5525a2c6e', - bidderRequestId: '393a43193a0ac', - adUnits: adUnit, - labels: [], - src: 'client' - }); - expect(bidRequests[0]).to.not.be.undefined; - expect(bidRequests[0].mediaTypes.banner).to.not.be.undefined; - expect(bidRequests[0].mediaTypes.banner.sizes).to.deep.equal([[300, 200], [400, 600]]); + const actual = setupAdUnitMediaTypes(adUnits, [])[0]; + expect(actual).to.not.be.undefined; + const bids = bidderMap(actual); + expect(bids.rubicon.mediaTypes).to.be.undefined; + expect(bids.appnexus.mediaTypes).to.be.undefined; + expect(actual.mediaTypes.banner).to.not.be.undefined; + expect(actual.mediaTypes.banner.sizes).to.deep.equal([[300, 200], [400, 600]]); }); - it('should logInfo if bidder relevantMediaTypes contains media type that is not active at the ad unit level', function () { - internal.getAdUnitDetail.restore(); - - const adUnit = utils.deepClone(basic_AdUnit); - adUnit[0].mediaTypes = { + it('should remove bidder if its relevantMediaTypes contains media type that is not active at the ad unit level', function () { + adUnits[0].mediaTypes = { banner: { sizeConfig: [ { minViewPort: [0, 0], sizes: [] }, @@ -1725,60 +1396,22 @@ describe('sizeMappingV2', function () { } }; - adUnit[0].bids[1].sizeConfig = [ + adUnits[0].bids[1].sizeConfig = [ { minViewPort: [0, 0], relevantMediaTypes: ['none'] }, { minViewPort: [200, 0], relevantMediaTypes: ['banner'] } ]; - const adUnitDetailFixture = { - adUnitCode: 'adUnit1', - mediaTypes: { - banner: { - sizeConfig: [ - { minViewPort: [0, 0], sizes: [] }, - { minViewPort: [700, 0], sizes: [[300, 200], [400, 600]] } - ] - }, - native: { - sizeConfig: [ - { minViewPort: [0, 0], active: false }, - { minViewPort: [400, 0], active: true } - ] - } - }, - sizeBucketToSizeMap: { - banner: { - activeSizeBucket: [0, 0], - activeSizeDimensions: [[]] - }, - native: { - activeSizeBucket: [400, 0], - activeSizeDimensions: 'NA' - } - }, + adUnitDetail = { activeViewport: [560, 260], transformedMediaTypes: { native: {} }, isLabelActivated: true, - instance: 1, - cacheHits: 0 }; - sinon - .stub(internal, 'getAdUnitDetail') - .withArgs('6d51e2d7-1447-4242-b6af-aaa5525a2c6e', adUnit[0], []) - .returns(adUnitDetailFixture); - - const bidRequests = getBids({ - bidderCode: 'rubicon', - auctionId: '6d51e2d7-1447-4242-b6af-aaa5525a2c6e', - bidderRequestId: '393a43193a0ac', - adUnits: adUnit, - labels: [], - src: 'client' - }); - expect(bidRequests[0]).to.be.undefined; + const actual = setupAdUnitMediaTypes(adUnits, [])[0]; + const bids = bidderMap(actual); + expect(bids.rubicon).to.be.undefined; sinon.assert.callCount(utils.logInfo, 1); sinon.assert.calledWith(utils.logInfo, `Size Mapping V2:: Ad Unit: adUnit1(1), Bidder: rubicon => 'relevantMediaTypes' does not match with any of the active mediaTypes at the Ad Unit level. This bidder is disabled.`); }); @@ -1788,38 +1421,19 @@ describe('sizeMappingV2', function () { .stub(utils, 'isValidMediaTypes') .returns(false); - getBids({ - bidderCode: 'appnexus', - auctionId: '6d51e2d7-1447-4242-b6af-aaa5525a2c6e', - bidderRequestId: '393a43193a0ac', - adUnits: basic_AdUnit, - labels: [], - src: 'client' - }); + try { + setupAdUnitMediaTypes(adUnits, []); + } finally { + utils.isValidMediaTypes.restore(); + } + sinon.assert.callCount(utils.logWarn, 1); sinon.assert.calledWith(utils.logWarn, `Size Mapping V2:: Ad Unit: adUnit1 => Ad unit has declared invalid 'mediaTypes' or has not declared a 'mediaTypes' property`); - - utils.isValidMediaTypes.restore(); }); it('should log a message if ad unit is disabled due to a failing label check', function () { - internal.getAdUnitDetail.restore(); - const adUnitDetail = Object.assign({}, adUnitDetailFixture); adUnitDetail.isLabelActivated = false; - sinon - .stub(internal, 'getAdUnitDetail') - .withArgs('6d51e2d7-1447-4242-b6af-aaa5525a2c6e', basic_AdUnit[0], []) - .returns(adUnitDetail); - - getBids({ - bidderCode: 'appnexus', - auctionId: '6d51e2d7-1447-4242-b6af-aaa5525a2c6e', - bidderRequestId: '393a43193a0ac', - adUnits: basic_AdUnit, - labels: [], - src: 'client' - }); - + setupAdUnitMediaTypes(adUnits, []); sinon.assert.callCount(utils.logInfo, 1); sinon.assert.calledWith(utils.logInfo, `Size Mapping V2:: Ad Unit: adUnit1(1) => Ad unit is disabled due to failing label check.`); }); @@ -1827,19 +1441,25 @@ describe('sizeMappingV2', function () { it('should log a message if bidder is disabled due to a failing label check', function () { const stub = sinon.stub(internal, 'isLabelActivated').returns(false); - getBids({ - bidderCode: 'appnexus', - auctionId: '6d51e2d7-1447-4242-b6af-aaa5525a2c6e', - bidderRequestId: '393a43193a0ac', - adUnits: basic_AdUnit, - labels: [], - src: 'client' - }); + try { + setupAdUnitMediaTypes(adUnits, []); + } finally { + stub.restore(); + } - sinon.assert.callCount(utils.logInfo, 1); + sinon.assert.callCount(utils.logInfo, 2); // called once for each bidder sinon.assert.calledWith(utils.logInfo, `Size Mapping V2:: Ad Unit: adUnit1(1), Bidder: appnexus => Label check for this bidder has failed. This bidder is disabled.`); + }); - internal.isLabelActivated.restore(); - }) + it('should set adUnit.bids[].mediaTypes if the bid mediaTypes should differ from the adUnit', () => { + adUnits[0].mediaTypes.native = {}; + adUnits[0].bids[1].sizeConfig = [ + { minViewPort: [0, 0], relevantMediaTypes: ['banner'] } + ]; + adUnitDetail.transformedMediaTypes.native = {}; + const actual = setupAdUnitMediaTypes(adUnits, [])[0]; + const bids = bidderMap(actual); + expect(bids.rubicon.mediaTypes).to.deep.equal({banner: adUnitDetail.transformedMediaTypes.banner}); + }); }); }); diff --git a/test/spec/modules/smaatoBidAdapter_spec.js b/test/spec/modules/smaatoBidAdapter_spec.js index 9803d1df103..38df03652b1 100644 --- a/test/spec/modules/smaatoBidAdapter_spec.js +++ b/test/spec/modules/smaatoBidAdapter_spec.js @@ -127,6 +127,15 @@ describe('smaatoBidAdapterTest', () => { } }; + let sandbox; + beforeEach(() => { + sandbox = sinon.sandbox.create(); + }); + + afterEach(() => { + sandbox.restore(); + }) + it('auction type is 1 (first price auction)', () => { const reqs = spec.buildRequests([singleBannerBidRequest], defaultBidderRequest); @@ -319,12 +328,15 @@ describe('smaatoBidAdapterTest', () => { }); it('sends first party data', () => { - this.sandbox = sinon.sandbox.create() - this.sandbox.stub(config, 'getConfig').callsFake(key => { + sandbox.stub(config, 'getConfig').callsFake(key => { const config = { ortb2: { site: { - keywords: 'power tools,drills' + keywords: 'power tools,drills', + publisher: { + id: 'otherpublisherid', + name: 'publishername' + } }, user: { keywords: 'a,b', @@ -345,7 +357,6 @@ describe('smaatoBidAdapterTest', () => { expect(req.user.ext.consent).to.equal(CONSENT_STRING); expect(req.site.keywords).to.eql('power tools,drills'); expect(req.site.publisher.id).to.equal('publisherId'); - this.sandbox.restore(); }); it('has no user ids', () => { diff --git a/test/spec/modules/smarthubBidAdapter_spec.js b/test/spec/modules/smarthubBidAdapter_spec.js new file mode 100644 index 00000000000..05fb1424dca --- /dev/null +++ b/test/spec/modules/smarthubBidAdapter_spec.js @@ -0,0 +1,392 @@ +import { expect } from 'chai'; +import { spec } from '../../../modules/smarthubBidAdapter'; +import { BANNER, VIDEO, NATIVE } from '../../../src/mediaTypes.js'; +import { getUniqueIdentifierStr } from '../../../src/utils.js'; + +const bidder = 'smarthub' + +describe('SmartHubBidAdapter', function () { + const bids = [ + { + bidId: getUniqueIdentifierStr(), + bidder: bidder, + mediaTypes: { + [BANNER]: { + sizes: [[300, 250]] + } + }, + params: { + partnerName: 'testname', + seat: 'testSeat', + token: 'testBanner', + iabCat: ['IAB1-1', 'IAB3-1', 'IAB4-3'], + minBidfloor: 10, + pos: 1, + } + }, + { + bidId: getUniqueIdentifierStr(), + bidder: bidder, + mediaTypes: { + [VIDEO]: { + playerSize: [[300, 300]], + minduration: 5, + maxduration: 60, + } + }, + params: { + partnerName: 'testname', + seat: 'testSeat', + token: 'testVideo', + iabCat: ['IAB1-1', 'IAB3-1', 'IAB4-3'], + minBidfloor: 10, + pos: 1, + } + }, + { + bidId: getUniqueIdentifierStr(), + bidder: bidder, + mediaTypes: { + [NATIVE]: { + native: { + title: { + required: true + }, + body: { + required: true + }, + icon: { + required: true, + size: [64, 64] + } + } + } + }, + params: { + partnerName: 'testname', + seat: 'testSeat', + token: 'testToken', + iabCat: ['IAB1-1', 'IAB3-1', 'IAB4-3'], + minBidfloor: 10, + pos: 1, + } + } + ]; + + const invalidBid = { + bidId: getUniqueIdentifierStr(), + bidder: bidder, + mediaTypes: { + [BANNER]: { + sizes: [[300, 250]] + } + }, + params: { + + } + } + + const bidderRequest = { + uspConsent: '1---', + gdprConsent: 'COvFyGBOvFyGBAbAAAENAPCAAOAAAAAAAAAAAEEUACCKAAA.IFoEUQQgAIQwgIwQABAEAAAAOIAACAIAAAAQAIAgEAACEAAAAAgAQBAAAAAAAGBAAgAAAAAAAFAAECAAAgAAQARAEQAAAAAJAAIAAgAAAYQEAAAQmAgBC3ZAYzUw', + refererInfo: { + referer: 'https://test.com' + } + }; + + 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://testname-prebid.smart-hub.io/pbjs'); + }); + + it('Returns general data valid', function () { + let data = serverRequest.data; + expect(data).to.be.an('object'); + expect(data).to.have.all.keys('deviceWidth', + 'deviceHeight', + 'language', + 'secure', + 'host', + 'page', + 'placements', + 'coppa', + 'ccpa', + 'gdpr', + 'tmax' + ); + expect(data.deviceWidth).to.be.a('number'); + expect(data.deviceHeight).to.be.a('number'); + expect(data.language).to.be.a('string'); + expect(data.secure).to.be.within(0, 1); + expect(data.host).to.be.a('string'); + expect(data.page).to.be.a('string'); + expect(data.coppa).to.be.a('number'); + expect(data.gdpr).to.be.a('string'); + expect(data.ccpa).to.be.a('string'); + expect(data.tmax).to.be.a('number'); + expect(data.placements).to.have.lengthOf(3); + }); + + it('Returns valid placements', function () { + const { placements } = serverRequest.data; + for (let i = 0, len = placements.length; i < len; i++) { + const placement = placements[i]; + expect(placement.adFormat).to.be.oneOf([BANNER, VIDEO, NATIVE]); + expect(placement.bidId).to.be.a('string'); + expect(placement.partnerName).to.be.a('string'); + expect(placement.seat).to.be.a('string'); + expect(placement.token).to.be.a('string'); + expect(placement.iabCat).to.be.an('array'); + expect(placement.minBidfloor).to.be.a('number'); + expect(placement.pos).to.be.within(0, 7); + 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'); + 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[0].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[0].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); + expect(serverRequest).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; + }); + }); +}); diff --git a/test/spec/modules/sortableAnalyticsAdapter_spec.js b/test/spec/modules/sortableAnalyticsAdapter_spec.js index 258c4b8f74d..9300756eae2 100644 --- a/test/spec/modules/sortableAnalyticsAdapter_spec.js +++ b/test/spec/modules/sortableAnalyticsAdapter_spec.js @@ -1,6 +1,6 @@ import {expect} from 'chai'; import sortableAnalyticsAdapter, {TIMEOUT_FOR_REGISTRY, DEFAULT_PBID_TIMEOUT} from 'modules/sortableAnalyticsAdapter.js'; -import events from 'src/events.js'; +import * as events from 'src/events.js'; import CONSTANTS from 'src/constants.json'; import * as prebidGlobal from 'src/prebidGlobal.js'; import {server} from 'test/mocks/xhr.js'; @@ -148,7 +148,6 @@ describe('Sortable Analytics Adapter', function() { } } }); - sortableAnalyticsAdapter.enableAnalytics(initialConfig); }); @@ -203,11 +202,11 @@ describe('Sortable Analytics Adapter', function() { brc: 1, brid: ['10141593b1d84a', '37a8760be6db23'], rs: ['300x250', '728x90'], - btcp: [0.70, 0.50], + btcp: [0.70, 0.50].map(n => n * 0.95), btcc: 'USD', btin: true, btsrc: 'sortable', - c: [0.70, 0.50], + c: [0.70, 0.50].map(n => n * 0.95), cc: 'USD', did: null, inr: true, diff --git a/test/spec/modules/sovrnBidAdapter_spec.js b/test/spec/modules/sovrnBidAdapter_spec.js index 729c48c28f4..09a61c82b6c 100644 --- a/test/spec/modules/sovrnBidAdapter_spec.js +++ b/test/spec/modules/sovrnBidAdapter_spec.js @@ -5,7 +5,7 @@ import * as utils from 'src/utils.js' const ENDPOINT = `https://ap.lijit.com/rtb/bid?src=$$REPO_AND_VERSION$$`; -const adUnitBidRequest = { +const baseBidRequest = { 'bidder': 'sovrn', 'params': { 'tagid': 403370 @@ -19,7 +19,7 @@ const adUnitBidRequest = { 'bidderRequestId': '22edbae2733bf6', 'auctionId': '1d1a030790a475', } -const bidderRequest = { +const baseBidderRequest = { refererInfo: { referer: 'http://example.com/page.html', } @@ -28,28 +28,35 @@ const bidderRequest = { describe('sovrnBidAdapter', function() { describe('isBidRequestValid', function () { it('should return true when required params found', function () { - expect(spec.isBidRequestValid(adUnitBidRequest)).to.equal(true); + expect(spec.isBidRequestValid(baseBidRequest)).to.equal(true); }); it('should return false when tagid not passed correctly', function () { - const bid = {...adUnitBidRequest} - const params = adUnitBidRequest.params - bid.params = {...params} - bid.params.tagid = 'ABCD' - expect(spec.isBidRequestValid(bid)).to.equal(false) + const bidRequest = { + ...baseBidRequest, + 'params': { + ...baseBidRequest.params, + 'tagid': 'ABCD' + }, + }; + + expect(spec.isBidRequestValid(bidRequest)).to.equal(false); }); it('should return false when require params are not passed', function () { - const bid = {...adUnitBidRequest} - bid.params = {}; - expect(spec.isBidRequestValid(bid)).to.equal(false); + const bidRequest = { + ...baseBidRequest, + 'params': {} + } + + expect(spec.isBidRequestValid(bidRequest)).to.equal(false); }); }); describe('buildRequests', function () { describe('basic bid parameters', function() { - const bidRequests = [adUnitBidRequest]; - const request = spec.buildRequests(bidRequests, bidderRequest); + const request = spec.buildRequests([baseBidRequest], baseBidderRequest); + const payload = JSON.parse(request.data); it('sends bid request to our endpoint via POST', function () { expect(request.method).to.equal('POST'); @@ -60,15 +67,65 @@ describe('sovrnBidAdapter', function() { }); it('sets the proper banner object', function() { + const bannerBidRequest = { + ...baseBidRequest, + 'mediaTypes': { + banner: {} + } + } + const request = spec.buildRequests([bannerBidRequest], baseBidderRequest) + + const payload = JSON.parse(request.data) + const impression = payload.imp[0] + + expect(impression.banner.format).to.deep.equal([{w: 300, h: 250}, {w: 300, h: 600}]) + expect(impression.banner.w).to.equal(1) + expect(impression.banner.h).to.equal(1) + }) + + it('sets the proper video object', function() { + const width = 640 + const height = 480 + const mimes = ['video/mp4', 'application/javascript'] + const protocols = [2, 5] + const minduration = 5 + const maxduration = 60 + const startdelay = 0 + const videoBidRequest = { + ...baseBidRequest, + 'mediaTypes': { + video: { + mimes, + protocols, + playerSize: [[width, height], [360, 240]], + minduration, + maxduration, + startdelay + } + } + } + const request = spec.buildRequests([videoBidRequest], baseBidderRequest) + const payload = JSON.parse(request.data) - expect(payload.imp[0].banner.format).to.deep.equal([{w: 300, h: 250}, {w: 300, h: 600}]) - expect(payload.imp[0].banner.w).to.equal(1) - expect(payload.imp[0].banner.h).to.equal(1) + const impression = payload.imp[0] + + expect(impression.video.w).to.equal(width) + expect(impression.video.h).to.equal(height) + expect(impression.video.mimes).to.have.same.members(mimes) + expect(impression.video.protocols).to.have.same.members(protocols) + expect(impression.video.minduration).to.equal(minduration) + expect(impression.video.maxduration).to.equal(maxduration) + expect(impression.video.startdelay).to.equal(startdelay) }) - it('includes the ad unit code int the request', function() { - const payload = JSON.parse(request.data); - expect(payload.imp[0].adunitcode).to.equal('adunit-code') + it('gets correct site info', function() { + expect(payload.site.page).to.equal('http://example.com/page.html'); + expect(payload.site.domain).to.equal('example.com'); + }); + + it('includes the ad unit code in the request', function() { + const impression = payload.imp[0] + expect(impression.adunitcode).to.equal('adunit-code') }) it('converts tagid to string', function () { @@ -77,109 +134,79 @@ describe('sovrnBidAdapter', function() { }) it('accepts a single array as a size', function() { - const singleSize = [{ - 'bidder': 'sovrn', + const singleSizeBidRequest = { + ...baseBidRequest, 'params': { - 'tagid': '403370', 'iv': 'vet' }, - 'adUnitCode': 'adunit-code', 'sizes': [300, 250], - 'bidId': '30b31c1838de1e', - 'bidderRequestId': '22edbae2733bf6', - 'auctionId': '1d1a030790a475' - }] - const request = spec.buildRequests(singleSize, bidderRequest) + 'mediaTypes': { + banner: {} + }, + } + const request = spec.buildRequests([singleSizeBidRequest], baseBidderRequest) + const payload = JSON.parse(request.data) - expect(payload.imp[0].banner.format).to.deep.equal([{w: 300, h: 250}]) - expect(payload.imp[0].banner.w).to.equal(1) - expect(payload.imp[0].banner.h).to.equal(1) + const impression = payload.imp[0] + + expect(impression.banner.format).to.deep.equal([{w: 300, h: 250}]) + expect(impression.banner.w).to.equal(1) + expect(impression.banner.h).to.equal(1) }) it('sends \'iv\' as query param if present', function () { - const ivBidRequests = [{ - 'bidder': 'sovrn', - 'params': { - 'tagid': '403370', - 'iv': 'vet' - }, - 'adUnitCode': 'adunit-code', - 'sizes': [ - [300, 250], - [300, 600] - ], - 'bidId': '30b31c1838de1e', - 'bidderRequestId': '22edbae2733bf6', - 'auctionId': '1d1a030790a475' - }]; - const bidderRequest = { - refererInfo: { - referer: 'http://example.com/page.html', + const ivBidRequest = { + ...baseBidRequest, + params: { + iv: 'vet' } - }; - const request = spec.buildRequests(ivBidRequests, bidderRequest); + } + const request = spec.buildRequests([ivBidRequest], baseBidderRequest) expect(request.url).to.contain('iv=vet') }); it('sends gdpr info if exists', function () { - let consentString = 'BOJ8RZsOJ8RZsABAB8AAAAAZ+A=='; const bidderRequest = { + ...baseBidderRequest, 'bidderCode': 'sovrn', 'auctionId': '1d1a030790a475', 'bidderRequestId': '22edbae2733bf6', 'timeout': 3000, - gdprConsent: { - consentString: consentString, - gdprApplies: true + 'gdprConsent': { + 'consentString': 'BOJ8RZsOJ8RZsABAB8AAAAAZ+A==', + 'gdprApplies': true }, - refererInfo: { - referer: 'http://example.com/page.html', - } + 'bids': [baseBidRequest] }; - bidderRequest.bids = [adUnitBidRequest]; - const data = JSON.parse(spec.buildRequests([adUnitBidRequest], bidderRequest).data); + const { regs, user } = JSON.parse(spec.buildRequests([baseBidRequest], bidderRequest).data) - expect(data.regs.ext.gdpr).to.exist.and.to.be.a('number'); - expect(data.regs.ext.gdpr).to.equal(1); - expect(data.user.ext.consent).to.exist.and.to.be.a('string'); - expect(data.user.ext.consent).to.equal(consentString); - }); + expect(regs.ext.gdpr).to.exist.and.to.be.a('number') + expect(regs.ext.gdpr).to.equal(1) + expect(user.ext.consent).to.exist.and.to.be.a('string') + expect(user.ext.consent).to.equal(bidderRequest.gdprConsent.consentString) + }) it('should send us_privacy if bidderRequest has a value for uspConsent', function () { - const uspString = '1NYN'; const bidderRequest = { + ...baseBidderRequest, 'bidderCode': 'sovrn', 'auctionId': '1d1a030790a475', 'bidderRequestId': '22edbae2733bf6', 'timeout': 3000, - uspConsent: uspString, - refererInfo: { - referer: 'http://example.com/page.html', - } - }; - bidderRequest.bids = [adUnitBidRequest]; + 'uspConsent': '1NYN', + 'bids': [baseBidRequest] + } - const data = JSON.parse(spec.buildRequests([adUnitBidRequest], bidderRequest).data); + const data = JSON.parse(spec.buildRequests([baseBidRequest], bidderRequest).data) - expect(data.regs.ext['us_privacy']).to.equal(uspString); - }); + expect(data.regs.ext['us_privacy']).to.equal(bidderRequest.uspConsent) + }) it('should add schain if present', function() { - const schainRequests = [{ - 'bidder': 'sovrn', - 'params': { - 'tagid': 403370 - }, - 'adUnitCode': 'adunit-code', - 'sizes': [ - [300, 250], - [300, 600] - ], - 'bidId': '30b31c1838de1e', - 'bidderRequestId': '22edbae2733bf6', - 'auctionId': '1d1a030790a475', + const schainRequest = { + ...baseBidRequest, 'schain': { 'ver': '1.0', 'complete': 1, @@ -192,100 +219,89 @@ describe('sovrnBidAdapter', function() { } ] } - }].concat(adUnitBidRequest); - const bidderRequest = { - refererInfo: { - referer: 'http://example.com/page.html', - } - }; - const data = JSON.parse(spec.buildRequests(schainRequests, bidderRequest).data); + } + const schainRequests = [schainRequest, baseBidRequest] + + const data = JSON.parse(spec.buildRequests(schainRequests, baseBidderRequest).data) expect(data.source.ext.schain.nodes.length).to.equal(1) - }); + }) - it('should add ids to the bid request', function() { - const criteoIdRequest = [{ - 'bidder': 'sovrn', - 'params': { - 'tagid': 403370 - }, - 'adUnitCode': 'adunit-code', - 'sizes': [ - [300, 250], - [300, 600] - ], - 'bidId': '30b31c1838de1e', - 'bidderRequestId': '22edbae2733bf6', - 'auctionId': '1d1a030790a475', - 'userId': { - 'criteoId': 'A_CRITEO_ID', - 'tdid': 'SOMESORTOFID', - } - }].concat(adUnitBidRequest); - const bidderRequest = { - refererInfo: { - referer: 'http://example.com/page.html', + it('should add eds to the bid request', function() { + const criteoIdRequest = { + ...baseBidRequest, + userId: { + criteoId: 'A_CRITEO_ID', + tdid: 'SOMESORTOFID', } - }; - - const data = JSON.parse(spec.buildRequests(criteoIdRequest, bidderRequest).data); - expect(data.user.ext.eids[0].source).to.equal('criteo.com') - expect(data.user.ext.eids[0].uids[0].id).to.equal('A_CRITEO_ID') - expect(data.user.ext.eids[0].uids[0].atype).to.equal(1) - expect(data.user.ext.eids[1].source).to.equal('adserver.org') - expect(data.user.ext.eids[1].uids[0].id).to.equal('SOMESORTOFID') - expect(data.user.ext.eids[1].uids[0].ext.rtiPartner).to.equal('TDID') - expect(data.user.ext.eids[1].uids[0].atype).to.equal(1) - expect(data.user.ext.tpid[0].source).to.equal('criteo.com') - expect(data.user.ext.tpid[0].uid).to.equal('A_CRITEO_ID') - expect(data.user.ext.prebid_criteoid).to.equal('A_CRITEO_ID') - }); + } + const criteoIdRequests = [criteoIdRequest, baseBidRequest] + + const ext = JSON.parse(spec.buildRequests(criteoIdRequests, baseBidderRequest).data).user.ext + const firstEID = ext.eids[0] + const secondEID = ext.eids[1] + + expect(firstEID.source).to.equal('criteo.com') + expect(firstEID.uids[0].id).to.equal('A_CRITEO_ID') + expect(firstEID.uids[0].atype).to.equal(1) + expect(secondEID.source).to.equal('adserver.org') + expect(secondEID.uids[0].id).to.equal('SOMESORTOFID') + expect(secondEID.uids[0].ext.rtiPartner).to.equal('TDID') + expect(secondEID.uids[0].atype).to.equal(1) + expect(ext.tpid[0].source).to.equal('criteo.com') + expect(ext.tpid[0].uid).to.equal('A_CRITEO_ID') + expect(ext.prebid_criteoid).to.equal('A_CRITEO_ID') + }) it('should ignore empty segments', function() { - const request = spec.buildRequests([adUnitBidRequest], bidderRequest) + const request = spec.buildRequests([baseBidRequest], baseBidderRequest) const payload = JSON.parse(request.data) + expect(payload.imp[0].ext).to.be.undefined }) it('should pass the segments param value as trimmed deal ids array', function() { - const segmentsRequests = [{ - 'bidder': 'sovrn', - 'params': { - 'segments': ' test1,test2 ' - }, - 'adUnitCode': 'adunit-code', - 'sizes': [ - [300, 250], - [300, 600] - ], - 'bidId': '30b31c1838de1e', - 'bidderRequestId': '22edbae2733bf6', - 'auctionId': '1d1a030790a475' - }] - const request = spec.buildRequests(segmentsRequests, bidderRequest) - const payload = JSON.parse(request.data) - expect(payload.imp[0].ext.deals[0]).to.equal('test1') - expect(payload.imp[0].ext.deals[1]).to.equal('test2') + const segmentsRequest = { + ...baseBidRequest, + params: { + segments: ' test1,test2 ' + } + } + const request = spec.buildRequests([segmentsRequest], baseBidderRequest) + const deals = JSON.parse(request.data).imp[0].ext.deals + + expect(deals[0]).to.equal('test1') + expect(deals[1]).to.equal('test2') }) it('should use the floor provided from the floor module if present', function() { - const floorBid = {...adUnitBidRequest, getFloor: () => ({currency: 'USD', floor: 1.10})} - floorBid.params = { - tagid: 1234, - bidfloor: 2.00 + const floorBid = { + ...baseBidRequest, + getFloor: () => ({currency: 'USD', floor: 1.10}), + params: { + tagid: 1234, + bidfloor: 2.00 + } } - const request = spec.buildRequests([floorBid], bidderRequest) + + const request = spec.buildRequests([floorBid], baseBidderRequest) const payload = JSON.parse(request.data) + expect(payload.imp[0].bidfloor).to.equal(1.10) }) it('should use the floor from the param if there is no floor from the floor module', function() { - const floorBid = {...adUnitBidRequest, getFloor: () => ({})} + const floorBid = { + ...baseBidRequest, + getFloor: () => ({}) + } floorBid.params = { tagid: 1234, bidfloor: 2.00 } - const request = spec.buildRequests([floorBid], bidderRequest) - const payload = JSON.parse(request.data) - expect(payload.imp[0].bidfloor).to.equal(2.00) + + const request = spec.buildRequests([floorBid], baseBidderRequest) + const impression = JSON.parse(request.data).imp[0] + + expect(impression.bidfloor).to.equal(2.00) }) describe('First Party Data', function () { let sandbox @@ -307,54 +323,79 @@ describe('sovrnBidAdapter', function() { data: 'some user data' } } - }; - return utils.deepAccess(cfg, key); - }); - const request = spec.buildRequests([adUnitBidRequest], bidderRequest) - const payload = JSON.parse(request.data) - expect(payload.user.data).to.equal('some user data') - expect(payload.site.keywords).to.equal('test keyword') - expect(payload.site.page).to.equal('http://example.com/page.html') - expect(payload.site.domain).to.equal('example.com') + } + return utils.deepAccess(cfg, key) + }) + + const request = spec.buildRequests([baseBidRequest], baseBidderRequest) + const { user, site } = JSON.parse(request.data) + + expect(user.data).to.equal('some user data') + expect(site.keywords).to.equal('test keyword') + expect(site.page).to.equal('http://example.com/page.html') + expect(site.domain).to.equal('example.com') }) it('should append impression first party data', function () { - const fpdBid = {...adUnitBidRequest} - fpdBid.ortb2Imp = { - ext: { - data: { - pbadslot: 'homepage-top-rect', - adUnitSpecificAttribute: '123' + const fpdBidRequest = { + ...baseBidRequest, + ortb2Imp: { + ext: { + data: { + pbadslot: 'homepage-top-rect', + adUnitSpecificAttribute: '123' + } } } } - const request = spec.buildRequests([fpdBid], bidderRequest) + + const request = spec.buildRequests([fpdBidRequest], baseBidderRequest) const payload = JSON.parse(request.data) + expect(payload.imp[0].ext.data.pbadslot).to.equal('homepage-top-rect') expect(payload.imp[0].ext.data.adUnitSpecificAttribute).to.equal('123') }) it('should not overwrite deals when impression fpd is present', function() { - const fpdBid = {...adUnitBidRequest} - fpdBid.params = {...adUnitBidRequest.params} - fpdBid.params.segments = 'seg1, seg2' - fpdBid.ortb2Imp = { - ext: { - data: { - pbadslot: 'homepage-top-rect', - adUnitSpecificAttribute: '123' + const fpdBid = { + ...baseBidRequest, + params: { + segments: 'seg1, seg2' + }, + ortb2Imp: { + ext: { + data: { + pbadslot: 'homepage-top-rect', + adUnitSpecificAttribute: '123' + } } } } - const request = spec.buildRequests([fpdBid], bidderRequest) - const payload = JSON.parse(request.data) - expect(payload.imp[0].ext.data.pbadslot).to.equal('homepage-top-rect') - expect(payload.imp[0].ext.data.adUnitSpecificAttribute).to.equal('123') - expect(payload.imp[0].ext.deals).to.deep.equal(['seg1', 'seg2']) + + const request = spec.buildRequests([fpdBid], baseBidderRequest) + const impression = JSON.parse(request.data).imp[0] + + expect(impression.ext.data.pbadslot).to.equal('homepage-top-rect') + expect(impression.ext.data.adUnitSpecificAttribute).to.equal('123') + expect(impression.ext.deals).to.deep.equal(['seg1', 'seg2']) }) }) }); describe('interpretResponse', function () { let response; + const baseResponse = { + 'requestId': '263c448586f5a1', + 'cpm': 0.45882675, + 'width': 728, + 'height': 90, + 'creativeId': 'creativelycreatedcreativecreative', + 'dealId': null, + 'currency': 'USD', + 'netRevenue': true, + 'mediaType': 'banner', + 'ad': decodeURIComponent(``), + 'ttl': 90, + 'meta': { advertiserDomains: [] } + } beforeEach(function () { response = { body: { @@ -376,106 +417,175 @@ describe('sovrnBidAdapter', function() { }); it('should get the correct bid response', function () { - let expectedResponse = [{ - 'requestId': '263c448586f5a1', - 'cpm': 0.45882675, - 'width': 728, - 'height': 90, - 'creativeId': 'creativelycreatedcreativecreative', - 'dealId': null, - 'currency': 'USD', - 'netRevenue': true, - 'mediaType': 'banner', + const expectedResponse = { + ...baseResponse, 'ad': decodeURIComponent(`>`), 'ttl': 60000, - 'meta': { advertiserDomains: [] } - }]; + }; + + const result = spec.interpretResponse(response); - let result = spec.interpretResponse(response); - expect(Object.keys(result[0])).to.deep.equal(Object.keys(expectedResponse[0])); + expect(result[0]).to.have.deep.keys(expectedResponse) }); it('crid should default to the bid id if not on the response', function () { delete response.body.seatbid[0].bid[0].crid; - let expectedResponse = [{ - 'requestId': '263c448586f5a1', - 'cpm': 0.45882675, - 'width': 728, - 'height': 90, + + const expectedResponse = { + ...baseResponse, 'creativeId': response.body.seatbid[0].bid[0].id, - 'dealId': null, - 'currency': 'USD', - 'netRevenue': true, - 'mediaType': 'banner', 'ad': decodeURIComponent(``), - 'ttl': 90, - 'meta': { advertiserDomains: [] } - }]; + } - let result = spec.interpretResponse(response); - expect(result[0]).to.deep.equal(expectedResponse[0]); + const result = spec.interpretResponse(response); + + expect(result[0]).to.deep.equal(expectedResponse); }); it('should get correct bid response when dealId is passed', function () { response.body.seatbid[0].bid[0].dealid = 'baking'; - - let expectedResponse = [{ - 'requestId': '263c448586f5a1', - 'cpm': 0.45882675, - 'width': 728, - 'height': 90, - 'creativeId': 'creativelycreatedcreativecreative', + const expectedResponse = { + ...baseResponse, 'dealId': 'baking', - 'currency': 'USD', - 'netRevenue': true, - 'mediaType': 'banner', - 'ad': decodeURIComponent(``), - 'ttl': 90, - 'meta': { advertiserDomains: [] } - }]; + } + + const result = spec.interpretResponse(response) - let result = spec.interpretResponse(response); - expect(result[0]).to.deep.equal(expectedResponse[0]); + expect(result[0]).to.deep.equal(expectedResponse); }); it('should get correct bid response when ttl is set', function () { - response.body.seatbid[0].bid[0].ext = { 'ttl': 480 }; - - let expectedResponse = [{ - 'requestId': '263c448586f5a1', - 'cpm': 0.45882675, - 'width': 728, - 'height': 90, - 'creativeId': 'creativelycreatedcreativecreative', - 'dealId': null, - 'currency': 'USD', - 'netRevenue': true, - 'mediaType': 'banner', - 'ad': decodeURIComponent(``), + response.body.seatbid[0].bid[0].ext = { 'ttl': 480 } + + const expectedResponse = { + ...baseResponse, 'ttl': 480, - 'meta': { advertiserDomains: [] } - }]; + } + + const result = spec.interpretResponse(response) + + expect(result[0]).to.deep.equal(expectedResponse) + }) - let result = spec.interpretResponse(response); - expect(result[0]).to.deep.equal(expectedResponse[0]); + it('handles empty bid response', function () { + const response = { + body: { + 'id': '37386aade21a71', + 'seatbid': [] + } + }; + + const result = spec.interpretResponse(response) + + expect(result.length).to.equal(0); + }); + }); + + describe('interpretResponse video', function () { + let videoResponse; + const bidAdm = 'key%3Dvalue'; + const decodedBidAdm = decodeURIComponent(bidAdm); + const baseVideoResponse = { + 'requestId': '263c448586f5a1', + 'cpm': 0.45882675, + 'width': 640, + 'height': 480, + 'creativeId': 'creativelycreatedcreativecreative', + 'dealId': null, + 'currency': 'USD', + 'netRevenue': true, + 'mediaType': 'video', + 'ttl': 90, + 'meta': { advertiserDomains: [] }, + 'vastXml': decodedBidAdm + } + beforeEach(function () { + videoResponse = { + body: { + 'id': '37386aade21a71', + 'seatbid': [{ + 'bid': [{ + 'id': 'a_403370_332fdb9b064040ddbec05891bd13ab28', + 'crid': 'creativelycreatedcreativecreative', + 'impid': '263c448586f5a1', + 'price': 0.45882675, + 'nurl': '', + 'adm': bidAdm, + 'h': 480, + 'w': 640 + }] + }] + } + }; }); + it('should get the correct bid response', function () { + const expectedResponse = { + ...baseVideoResponse, + 'ttl': 60000, + }; + + const result = spec.interpretResponse(videoResponse); + + expect(result[0]).to.have.deep.keys(expectedResponse) + }); + + it('crid should default to the bid id if not on the response', function () { + delete videoResponse.body.seatbid[0].bid[0].crid; + + const expectedResponse = { + ...baseVideoResponse, + 'creativeId': videoResponse.body.seatbid[0].bid[0].id, + } + + const result = spec.interpretResponse(videoResponse); + + expect(result[0]).to.deep.equal(expectedResponse); + }); + + it('should get correct bid response when dealId is passed', function () { + videoResponse.body.seatbid[0].bid[0].dealid = 'baking'; + const expectedResponse = { + ...baseVideoResponse, + 'dealId': 'baking', + } + + const result = spec.interpretResponse(videoResponse) + + expect(result[0]).to.deep.equal(expectedResponse); + }); + + it('should get correct bid response when ttl is set', function () { + videoResponse.body.seatbid[0].bid[0].ext = { 'ttl': 480 } + + const expectedResponse = { + ...baseVideoResponse, + 'ttl': 480, + } + + const result = spec.interpretResponse(videoResponse) + + expect(result[0]).to.deep.equal(expectedResponse) + }) + it('handles empty bid response', function () { - let response = { + const response = { body: { 'id': '37386aade21a71', 'seatbid': [] } }; - let result = spec.interpretResponse(response); + + const result = spec.interpretResponse(response) + expect(result.length).to.equal(0); }); }); describe('getUserSyncs ', function() { - let syncOptions = { iframeEnabled: true, pixelEnabled: false }; - let iframeDisabledSyncOptions = { iframeEnabled: false, pixelEnabled: false }; - let serverResponse = [ + const syncOptions = { iframeEnabled: true, pixelEnabled: false }; + const iframeDisabledSyncOptions = { iframeEnabled: false, pixelEnabled: false }; + const serverResponse = [ { 'body': { 'id': '546956d68c757f', @@ -524,14 +634,14 @@ describe('sovrnBidAdapter', function() { ]; it('should return if iid present on server response & iframe syncs enabled', function() { - const expectedReturnStatement = [ - { - 'type': 'iframe', - 'url': 'https://ap.lijit.com/beacon?informer=13487408', - } - ]; + const expectedReturnStatement = { + 'type': 'iframe', + 'url': 'https://ap.lijit.com/beacon?informer=13487408', + } + const returnStatement = spec.getUserSyncs(syncOptions, serverResponse); - expect(returnStatement[0]).to.deep.equal(expectedReturnStatement[0]); + + expect(returnStatement[0]).to.deep.equal(expectedReturnStatement); }); it('should include gdpr consent string if present', function() { @@ -539,26 +649,26 @@ describe('sovrnBidAdapter', function() { gdprApplies: 1, consentString: 'BOJ8RZsOJ8RZsABAB8AAAAAZ+A==' } - const expectedReturnStatement = [ - { - 'type': 'iframe', - 'url': `https://ap.lijit.com/beacon?gdpr_consent=${gdprConsent.consentString}&informer=13487408`, - } - ]; + const expectedReturnStatement = { + 'type': 'iframe', + 'url': `https://ap.lijit.com/beacon?gdpr_consent=${gdprConsent.consentString}&informer=13487408`, + } + const returnStatement = spec.getUserSyncs(syncOptions, serverResponse, gdprConsent, ''); - expect(returnStatement[0]).to.deep.equal(expectedReturnStatement[0]); + + expect(returnStatement[0]).to.deep.equal(expectedReturnStatement); }); it('should include us privacy string if present', function() { const uspString = '1NYN'; - const expectedReturnStatement = [ - { - 'type': 'iframe', - 'url': `https://ap.lijit.com/beacon?us_privacy=${uspString}&informer=13487408`, - } - ]; + const expectedReturnStatement = { + 'type': 'iframe', + 'url': `https://ap.lijit.com/beacon?us_privacy=${uspString}&informer=13487408`, + } + const returnStatement = spec.getUserSyncs(syncOptions, serverResponse, null, uspString); - expect(returnStatement[0]).to.deep.equal(expectedReturnStatement[0]); + + expect(returnStatement[0]).to.deep.equal(expectedReturnStatement); }); it('should include all privacy strings if present', function() { @@ -567,57 +677,35 @@ describe('sovrnBidAdapter', function() { consentString: 'BOJ8RZsOJ8RZsABAB8AAAAAZ+A==' } const uspString = '1NYN'; - const expectedReturnStatement = [ - { - 'type': 'iframe', - 'url': `https://ap.lijit.com/beacon?gdpr_consent=${gdprConsent.consentString}&us_privacy=${uspString}&informer=13487408`, - } - ]; - const returnStatement = spec.getUserSyncs(syncOptions, serverResponse, gdprConsent, uspString); - expect(returnStatement[0]).to.deep.equal(expectedReturnStatement[0]); + const expectedReturnStatement = { + 'type': 'iframe', + 'url': `https://ap.lijit.com/beacon?gdpr_consent=${gdprConsent.consentString}&us_privacy=${uspString}&informer=13487408`, + } + + const returnStatement = spec.getUserSyncs(syncOptions, serverResponse, gdprConsent, uspString) + + expect(returnStatement[0]).to.deep.equal(expectedReturnStatement) }); it('should not return if iid missing on server response', function() { const returnStatement = spec.getUserSyncs(syncOptions, []); + expect(returnStatement).to.be.empty; }); it('should not return if iframe syncs disabled', function() { const returnStatement = spec.getUserSyncs(iframeDisabledSyncOptions, serverResponse); + expect(returnStatement).to.be.empty; }); it('should include pixel syncs', function() { - let pixelEnabledOptions = { iframeEnabled: false, pixelEnabled: true }; - const resp2 = { + const pixelEnabledOptions = { iframeEnabled: false, pixelEnabled: true } + + const otherResponce = { + ...serverResponse, 'body': { - 'id': '546956d68c757f-2', - 'seatbid': [ - { - 'bid': [ - { - 'id': 'a_448326_16c2ada014224bee815a90d2248322f5-2', - 'impid': '2a3826aae345f4', - 'price': 1.0099999904632568, - 'nurl': 'http://localhost/rtb/impression?bannerid=220958&campaignid=3890&rtb_tid=15588614-75d2-40ab-b27e-13d2127b3c2e&rpid=1295&seatid=seat1&zoneid=448326&cb=26900712&tid=a_448326_16c2ada014224bee815a90d2248322f5', - 'adm': 'yo a creative', - 'crid': 'cridprebidrtb', - 'w': 160, - 'h': 600 - }, - { - 'id': 'a_430392_beac4c1515da4576acf6cb9c5340b40c-2', - 'impid': '3cf96fd26ed4c5', - 'price': 1.0099999904632568, - 'nurl': 'http://localhost/rtb/impression?bannerid=220957&campaignid=3890&rtb_tid=5bc0e68b-3492-448d-a6f9-26fa3fd0b646&rpid=1295&seatid=seat1&zoneid=430392&cb=62735099&tid=a_430392_beac4c1515da4576acf6cb9c5340b40c', - 'adm': 'yo a creative', - 'crid': 'cridprebidrtb', - 'w': 300, - 'h': 250 - }, - ] - } - ], + ...serverResponse.body, 'ext': { 'iid': 13487408, sync: { @@ -631,10 +719,11 @@ describe('sovrnBidAdapter', function() { ] } } - }, - 'headers': {} + } } - const returnStatement = spec.getUserSyncs(pixelEnabledOptions, [...serverResponse, resp2]); + + const returnStatement = spec.getUserSyncs(pixelEnabledOptions, [...serverResponse, otherResponce]) + expect(returnStatement.length).to.equal(4); expect(returnStatement).to.deep.include.members([ { type: 'image', url: 'http://idprovider1.com' }, @@ -642,34 +731,25 @@ describe('sovrnBidAdapter', function() { { type: 'image', url: 'http://idprovider3.com' }, { type: 'image', url: 'http://idprovider4.com' } ]); - }); - }); + }) + }) describe('prebid 3 upgrade', function() { - const bidRequests = [{ - 'bidder': 'sovrn', + const bidRequest = { + ...baseBidRequest, 'params': { 'tagid': '403370' }, - 'adUnitCode': 'adunit-code', - mediaTypes: { - banner: { - sizes: [ + 'mediaTypes': { + 'banner': { + 'sizes': [ [300, 250], [300, 600] ] } }, - 'bidId': '30b31c1838de1e', - 'bidderRequestId': '22edbae2733bf6', - 'auctionId': '1d1a030790a475' - }]; - const bidderRequest = { - refererInfo: { - referer: 'http://example.com/page.html', - } }; - const request = spec.buildRequests(bidRequests, bidderRequest); + const request = spec.buildRequests([bidRequest], baseBidderRequest); const payload = JSON.parse(request.data); it('gets sizes from mediaTypes.banner', function() { diff --git a/test/spec/modules/sspBCBidAdapter_spec.js b/test/spec/modules/sspBCBidAdapter_spec.js index d9f3ca84a3a..a0c4837bb51 100644 --- a/test/spec/modules/sspBCBidAdapter_spec.js +++ b/test/spec/modules/sspBCBidAdapter_spec.js @@ -514,8 +514,8 @@ describe('SSPBC adapter', function () { }); it('should send gdpr data', function () { - expect(payload.regs).to.be.an('object').and.to.have.property('[ortb_extensions.gdpr]', 1); - expect(payload.user).to.be.an('object').and.to.have.property('[ortb_extensions.consent]', bidRequest.gdprConsent.consentString); + expect(payload.regs).to.be.an('object').and.to.have.property('gdpr', 1); + expect(payload.user).to.be.an('object').and.to.have.property('consent', bidRequest.gdprConsent.consentString); }); it('should send net info and pvid', function () { @@ -653,7 +653,7 @@ describe('SSPBC adapter', function () { let nativeBid = resultNative[0]; expect(nativeBid).to.have.keys('bidderCode', 'cpm', 'creativeId', 'currency', 'width', 'height', 'meta', 'mediaType', 'netRevenue', 'requestId', 'ttl', 'native'); - expect(nativeBid.native).to.have.keys('image', 'icon', 'title', 'sponsoredBy', 'body', 'clickUrl', 'impressionTrackers'); + expect(nativeBid.native).to.have.keys('image', 'icon', 'title', 'sponsoredBy', 'body', 'clickUrl', 'impressionTrackers', 'javascriptTrackers'); }); }); @@ -668,8 +668,8 @@ describe('SSPBC adapter', function () { }); it('should send no syncs, if frame sync is not allowed', function () { - expect(syncResultImage).to.be.undefined; - expect(syncResultNone).to.be.undefined; + expect(syncResultImage).to.have.length(0); ; + expect(syncResultNone).to.have.length(0); ; }); }); @@ -686,7 +686,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('adUnit').that.deep.equals([bid.adUnitCode]); + 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'); }); @@ -707,7 +707,7 @@ describe('SSPBC adapter', function () { 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('adUnit').that.deep.equals([bids_timeouted[0].adUnitCode, bids_timeouted[1].adUnitCode]); + 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 6f24da85cfe..e723523de31 100644 --- a/test/spec/modules/stroeerCoreBidAdapter_spec.js +++ b/test/spec/modules/stroeerCoreBidAdapter_spec.js @@ -2,7 +2,7 @@ import {assert} from 'chai'; import {spec} from 'modules/stroeerCoreBidAdapter.js'; import * as utils from 'src/utils.js'; import {BANNER, VIDEO} from '../../../src/mediaTypes.js'; -import find from 'core-js-pure/features/array/find.js'; +import {find} from 'src/polyfill.js'; describe('stroeerCore bid adapter', function () { let sandbox; diff --git a/test/spec/modules/synacormediaBidAdapter_spec.js b/test/spec/modules/synacormediaBidAdapter_spec.js index 5f3633ec311..b9a02799219 100644 --- a/test/spec/modules/synacormediaBidAdapter_spec.js +++ b/test/spec/modules/synacormediaBidAdapter_spec.js @@ -244,6 +244,13 @@ describe('synacormediaBidAdapter ', function () { rtiPartner: 'TDID' } }] + }, + { + source: 'neustar.biz', + uids: [{ + id: 'neustar809-044-23njhwer3', + atype: 1 + }] } ]; @@ -989,7 +996,7 @@ describe('synacormediaBidAdapter ', function () { netRevenue: true, mediaType: 'video', ad: '\n\n\n\nSynacor Media Ad Server - 9999\nhttps://uat-net.technoratimedia.com/openrtb/tags?ID=QVVDVElPTl9JRD1lOTBhYWU1My1hZDkwLTRkNDEtYTQxMC1lZDY1MjIxMDc0ZGMmQVVDVElPTl9CSURfSUQ9MTEzMzkxMjgwMDE2OTIzMzd-OTk5OX4wJkFVQ1RJT05fU0VBVF9JRD05OTk5JkFVQ1RJT05fSU1QX0lEPXYyZGE3MzIyYjJkZjYxZi02NDB4NDgwJkFDVE9SX1JFRj1ha2thLnRjcDovL2F3cy1lYXN0MUBhZHMxMy5jYXAtdXNlMS5zeW5hY29yLmNvbToyNTUxL3VzZXIvJGNMYmZiIy0xOTk4NTIzNTk3JlNFQVRfSUQ9cHJlYmlk&AUCTION_PRICE=0.45\n\n\n', - ttl: 60, + ttl: 420, meta: { advertiserDomains: ['psacentral.org'] }, videoCacheKey: 'QVVDVElPTl9JRD1lOTBhYWU1My1hZDkwLTRkNDEtYTQxMC1lZDY1MjIxMDc0ZGMmQVVDVElPTl9CSURfSUQ9MTEzMzkxMjgwMDE2OTIzMzd-OTk5OX4wJkFVQ1RJT05fU0VBVF9JRD05OTk5JkFVQ1RJT05fSU1QX0lEPXYyZGE3MzIyYjJkZjYxZi02NDB4NDgwJkFDVE9SX1JFRj1ha2thLnRjcDovL2F3cy1lYXN0MUBhZHMxMy5jYXAtdXNlMS5zeW5hY29yLmNvbToyNTUxL3VzZXIvJGNMYmZiIy0xOTk4NTIzNTk3JlNFQVRfSUQ9cHJlYmlk', vastUrl: 'https://uat-net.technoratimedia.com/openrtb/tags?ID=QVVDVElPTl9JRD1lOTBhYWU1My1hZDkwLTRkNDEtYTQxMC1lZDY1MjIxMDc0ZGMmQVVDVElPTl9CSURfSUQ9MTEzMzkxMjgwMDE2OTIzMzd-OTk5OX4wJkFVQ1RJT05fU0VBVF9JRD05OTk5JkFVQ1RJT05fSU1QX0lEPXYyZGE3MzIyYjJkZjYxZi02NDB4NDgwJkFDVE9SX1JFRj1ha2thLnRjcDovL2F3cy1lYXN0MUBhZHMxMy5jYXAtdXNlMS5zeW5hY29yLmNvbToyNTUxL3VzZXIvJGNMYmZiIy0xOTk4NTIzNTk3JlNFQVRfSUQ9cHJlYmlk&AUCTION_PRICE=0.45' @@ -1010,7 +1017,7 @@ describe('synacormediaBidAdapter ', function () { netRevenue: true, mediaType: BANNER, ad: '', - ttl: 60 + ttl: 420 }); }); @@ -1032,7 +1039,7 @@ describe('synacormediaBidAdapter ', function () { netRevenue: true, mediaType: BANNER, ad: '', - ttl: 60 + ttl: 420 }); expect(resp[1]).to.eql({ @@ -1045,7 +1052,7 @@ describe('synacormediaBidAdapter ', function () { netRevenue: true, mediaType: BANNER, ad: '', - ttl: 60 + ttl: 420 }); }); @@ -1156,7 +1163,7 @@ describe('synacormediaBidAdapter ', function () { netRevenue: true, mediaType: 'video', ad: '\n\n\n\nSynacor Media Ad Server - 9999\nhttps://uat-net.technoratimedia.com/openrtb/tags?ID=QVVDVElPTl9JRD1lOTBhYWU1My1hZDkwLTRkNDEtYTQxMC1lZDY1MjIxMDc0ZGMmQVVDVElPTl9CSURfSUQ9MTEzMzkxMjgwMDE2OTIzMzd-OTk5OX4wJkFVQ1RJT05fU0VBVF9JRD05OTk5JkFVQ1RJT05fSU1QX0lEPXYyZGE3MzIyYjJkZjYxZi02NDB4NDgwJkFDVE9SX1JFRj1ha2thLnRjcDovL2F3cy1lYXN0MUBhZHMxMy5jYXAtdXNlMS5zeW5hY29yLmNvbToyNTUxL3VzZXIvJGNMYmZiIy0xOTk4NTIzNTk3JlNFQVRfSUQ9cHJlYmlk&AUCTION_PRICE=0.45\n\n\n', - ttl: 60, + ttl: 420, meta: { advertiserDomains: ['psacentral.org'] }, videoCacheKey: 'QVVDVElPTl9JRD1lOTBhYWU1My1hZDkwLTRkNDEtYTQxMC1lZDY1MjIxMDc0ZGMmQVVDVElPTl9CSURfSUQ9MTEzMzkxMjgwMDE2OTIzMzd-OTk5OX4wJkFVQ1RJT05fU0VBVF9JRD05OTk5JkFVQ1RJT05fSU1QX0lEPXYyZGE3MzIyYjJkZjYxZi02NDB4NDgwJkFDVE9SX1JFRj1ha2thLnRjcDovL2F3cy1lYXN0MUBhZHMxMy5jYXAtdXNlMS5zeW5hY29yLmNvbToyNTUxL3VzZXIvJGNMYmZiIy0xOTk4NTIzNTk3JlNFQVRfSUQ9cHJlYmlk', vastUrl: 'https://uat-net.technoratimedia.com/openrtb/tags?ID=QVVDVElPTl9JRD1lOTBhYWU1My1hZDkwLTRkNDEtYTQxMC1lZDY1MjIxMDc0ZGMmQVVDVElPTl9CSURfSUQ9MTEzMzkxMjgwMDE2OTIzMzd-OTk5OX4wJkFVQ1RJT05fU0VBVF9JRD05OTk5JkFVQ1RJT05fSU1QX0lEPXYyZGE3MzIyYjJkZjYxZi02NDB4NDgwJkFDVE9SX1JFRj1ha2thLnRjcDovL2F3cy1lYXN0MUBhZHMxMy5jYXAtdXNlMS5zeW5hY29yLmNvbToyNTUxL3VzZXIvJGNMYmZiIy0xOTk4NTIzNTk3JlNFQVRfSUQ9cHJlYmlk&AUCTION_PRICE=0.45' @@ -1209,9 +1216,63 @@ describe('synacormediaBidAdapter ', function () { netRevenue: true, mediaType: BANNER, ad: '', - ttl: 60 + ttl: 420 }); }); + + it('should return ttl equal to DEFAULT_TTL_MAX if bid.exp and bid.ext["imds.tv"].ttl are both undefined', function() { + const br = { ...bidResponse }; + serverResponse.body.seatbid[0].bid.push(br); + const resp = spec.interpretResponse(serverResponse, bidRequest); + expect(resp).to.be.an('array').to.have.lengthOf(1); + expect(resp[0]).to.have.property('ttl'); + expect(resp[0].ttl).to.equal(420); + }); + + it('should return ttl equal to bid.ext["imds.tv"].ttl if it is defined but bid.exp is undefined', function() { + let br = { ext: { 'imds.tv': { ttl: 4321 } }, ...bidResponse }; + serverResponse.body.seatbid[0].bid.push(br); + let resp = spec.interpretResponse(serverResponse, bidRequest); + expect(resp).to.be.an('array').to.have.lengthOf(1); + expect(resp[0]).to.have.property('ttl'); + expect(resp[0].ttl).to.equal(4321); + }); + + it('should return ttl equal to bid.exp if bid.exp is less than or equal to DEFAULT_TTL_MAX and bid.ext["imds.tv"].ttl is undefined', function() { + const br = { exp: 123, ...bidResponse }; + serverResponse.body.seatbid[0].bid.push(br); + const resp = spec.interpretResponse(serverResponse, bidRequest); + expect(resp).to.be.an('array').to.have.lengthOf(1); + expect(resp[0]).to.have.property('ttl'); + expect(resp[0].ttl).to.equal(123); + }); + + it('should return ttl equal to DEFAULT_TTL_MAX if bid.exp is greater than DEFAULT_TTL_MAX and bid.ext["imds.tv"].ttl is undefined', function() { + const br = { exp: 4321, ...bidResponse }; + serverResponse.body.seatbid[0].bid.push(br); + const resp = spec.interpretResponse(serverResponse, bidRequest); + expect(resp).to.be.an('array').to.have.lengthOf(1); + expect(resp[0]).to.have.property('ttl'); + expect(resp[0].ttl).to.equal(420); + }); + + it('should return ttl equal to bid.exp if bid.exp is less than or equal to bid.ext["imds.tv"].ttl', function() { + const br = { exp: 1234, ext: { 'imds.tv': { ttl: 4321 } }, ...bidResponse }; + serverResponse.body.seatbid[0].bid.push(br); + const resp = spec.interpretResponse(serverResponse, bidRequest); + expect(resp).to.be.an('array').to.have.lengthOf(1); + expect(resp[0]).to.have.property('ttl'); + expect(resp[0].ttl).to.equal(1234); + }); + + it('should return ttl equal to bid.ext["imds.tv"].ttl if bid.exp is greater than bid.ext["imds.tv"].ttl', function() { + const br = { exp: 4321, ext: { 'imds.tv': { ttl: 1234 } }, ...bidResponse }; + serverResponse.body.seatbid[0].bid.push(br); + const resp = spec.interpretResponse(serverResponse, bidRequest); + expect(resp).to.be.an('array').to.have.lengthOf(1); + expect(resp[0]).to.have.property('ttl'); + expect(resp[0].ttl).to.equal(1234); + }); }); describe('getUserSyncs', function () { it('should return a usersync when iframes is enabled', function () { @@ -1231,4 +1292,74 @@ describe('synacormediaBidAdapter ', function () { expect(usersyncs).to.be.an('array').that.is.empty; }); }); + + describe('Bid Requests with price module should use if available', function () { + let validVideoBidRequest = { + bidder: 'synacormedia', + params: { + bidfloor: '0.50', + seatId: 'prebid', + placementId: 'demo1', + pos: 1, + video: {} + }, + renderer: { + url: '../syncOutstreamPlayer.js' + }, + mediaTypes: { + video: { + playerSize: [[300, 250]], + context: 'outstream' + } + }, + adUnitCode: 'div-1', + transactionId: '0869f34e-090b-4b20-84ee-46ff41405a39', + sizes: [[300, 250]], + bidId: '22b3a2268d9f0e', + bidderRequestId: '1d195910597e13', + auctionId: '3375d336-2aea-4ee7-804c-6d26b621ad20', + src: 'client', + bidRequestsCount: 1, + bidderRequestsCount: 1, + bidderWinsCount: 0 + }; + + let validBannerBidRequest = { + bidId: '9876abcd', + sizes: [[300, 250]], + params: { + bidfloor: '0.50', + seatId: 'prebid', + placementId: '1234', + } + }; + + let bidderRequest = { + refererInfo: { + referer: 'http://localhost:9999/' + }, + bidderCode: 'synacormedia', + auctionId: 'f8a75621-d672-4cbb-9275-3db7d74fb110' + }; + + it('should return valid bidfloor using price module for banner/video impression', function () { + let bannerRequest = spec.buildRequests([validBannerBidRequest], bidderRequest); + let videoRequest = spec.buildRequests([validVideoBidRequest], bidderRequest); + + expect(bannerRequest.data.imp[0].bidfloor).to.equal(0.5); + expect(videoRequest.data.imp[0].bidfloor).to.equal(0.5); + + let priceModuleFloor = 3; + let floorResponse = { currency: 'USD', floor: priceModuleFloor }; + + validBannerBidRequest.getFloor = () => { return floorResponse; }; + validVideoBidRequest.getFloor = () => { return floorResponse; }; + + bannerRequest = spec.buildRequests([validBannerBidRequest], bidderRequest); + videoRequest = spec.buildRequests([validVideoBidRequest], bidderRequest); + + expect(bannerRequest.data.imp[0].bidfloor).to.equal(priceModuleFloor); + expect(videoRequest.data.imp[0].bidfloor).to.equal(priceModuleFloor); + }); + }); }); diff --git a/test/spec/modules/telariaBidAdapter_spec.js b/test/spec/modules/telariaBidAdapter_spec.js index 25649115cc1..457dd568764 100644 --- a/test/spec/modules/telariaBidAdapter_spec.js +++ b/test/spec/modules/telariaBidAdapter_spec.js @@ -234,9 +234,7 @@ describe('TelariaAdapter', () => { }]; it('should get correct bid response', () => { - let expectedResponseKeys = ['bidderCode', 'width', 'height', 'statusMessage', 'adId', 'mediaType', 'source', - 'getStatusCode', 'getSize', 'requestId', 'cpm', 'creativeId', 'vastXml', - 'vastUrl', 'currency', 'netRevenue', 'ttl', 'ad', 'meta']; + let expectedResponseKeys = ['requestId', 'cpm', 'creativeId', 'vastXml', 'vastUrl', 'mediaType', 'width', 'height', 'currency', 'netRevenue', 'ttl', 'ad', 'meta']; let bidRequest = spec.buildRequests(stub, BIDDER_REQUEST)[0]; bidRequest.bidId = '1234'; diff --git a/test/spec/modules/trustpidSystem_spec.js b/test/spec/modules/trustpidSystem_spec.js new file mode 100644 index 00000000000..97e87e3a303 --- /dev/null +++ b/test/spec/modules/trustpidSystem_spec.js @@ -0,0 +1,232 @@ +import { expect } from 'chai'; +import { trustpidSubmodule } from 'modules/trustpidSystem.js'; +import { storage } from 'modules/trustpidSystem.js'; + +describe('trustpid System', () => { + const connectDataKey = 'fcIdConnectData'; + const connectDomainKey = 'fcIdConnectDomain'; + + const getStorageData = (idGraph) => { + if (!idGraph) { + idGraph = {id: 501, domain: ''}; + } + return { + 'connectId': { + 'idGraph': [idGraph], + } + } + }; + + it('should have the correct module name declared', () => { + expect(trustpidSubmodule.name).to.equal('trustpid'); + }); + + describe('trustpid getId()', () => { + afterEach(() => { + storage.removeDataFromLocalStorage(connectDataKey); + storage.removeDataFromLocalStorage(connectDomainKey); + }); + + it('it should return object with key callback', () => { + expect(trustpidSubmodule.getId()).to.have.property('callback'); + }); + + it('should return object with key callback with value type - function', () => { + storage.setDataInLocalStorage(connectDataKey, JSON.stringify(getStorageData())); + expect(trustpidSubmodule.getId()).to.have.property('callback'); + expect(typeof trustpidSubmodule.getId().callback).to.be.equal('function'); + }); + + it('tests if localstorage & JSON works properly ', () => { + const idGraph = { + 'domain': 'domainValue', + 'umid': 'umidValue', + }; + storage.setDataInLocalStorage(connectDataKey, JSON.stringify(getStorageData(idGraph))); + expect(JSON.parse(storage.getDataFromLocalStorage(connectDataKey))).to.have.property('connectId'); + }); + + it('returns {callback: func} if domains don\'t match', () => { + const idGraph = { + 'domain': 'domainValue', + 'umid': 'umidValue', + }; + storage.setDataInLocalStorage(connectDomainKey, JSON.stringify('differentDomainValue')); + storage.setDataInLocalStorage(connectDataKey, JSON.stringify(getStorageData(idGraph))); + expect(trustpidSubmodule.getId()).to.have.property('callback'); + }); + + it('returns {id: {trustpid: data.trustpid}} if we have the right data stored in the localstorage ', () => { + const idGraph = { + 'domain': 'uat.mno.link', + 'umid': 'umidValue', + }; + storage.setDataInLocalStorage(connectDomainKey, JSON.stringify('uat.mno.link')); + storage.setDataInLocalStorage(connectDataKey, JSON.stringify(getStorageData(idGraph))); + const response = trustpidSubmodule.getId(); + expect(response).to.have.property('id'); + expect(response.id).to.have.property('trustpid'); + expect(response.id.trustpid).to.be.equal('umidValue-xxxx'); + }); + + it('returns {trustpid: data.trustpid} if we have the right data stored in the localstorage right after the callback is called', (done) => { + const idGraph = { + 'domain': 'uat.mno.link', + 'umid': 'umidValue', + }; + const response = trustpidSubmodule.getId(); + expect(response).to.have.property('callback'); + expect(response.callback.toString()).contain('result(callback)'); + + if (typeof response.callback === 'function') { + storage.setDataInLocalStorage(connectDomainKey, JSON.stringify('uat.mno.link')); + storage.setDataInLocalStorage(connectDataKey, JSON.stringify(getStorageData(idGraph))); + response.callback(function (result) { + expect(result).to.not.be.null; + expect(result).to.have.property('trustpid'); + expect(result.trustpid).to.be.equal('umidValue-xxxx'); + done() + }) + } + }); + + it('returns null if domains don\'t match', (done) => { + const idGraph = { + 'domain': 'uat.mno.link', + 'umid': 'umidValue', + }; + storage.setDataInLocalStorage(connectDomainKey, JSON.stringify('differentDomainValue')); + storage.setDataInLocalStorage(connectDataKey, JSON.stringify(getStorageData(idGraph))); + + const response = trustpidSubmodule.getId(); + expect(response).to.have.property('callback'); + expect(response.callback.toString()).contain('result(callback)'); + + if (typeof response.callback === 'function') { + setTimeout(() => { + expect(JSON.parse(storage.getDataFromLocalStorage(connectDomainKey))).to.be.equal('differentDomainValue'); + }, 100) + response.callback(function (result) { + expect(result).to.be.null; + done() + }) + } + }); + + it('returns {trustpid: data.trustpid} if we have the right data stored in the localstorage right after 500ms delay', (done) => { + const idGraph = { + 'domain': 'uat.mno.link', + 'umid': 'umidValue', + }; + + const response = trustpidSubmodule.getId(); + expect(response).to.have.property('callback'); + expect(response.callback.toString()).contain('result(callback)'); + + if (typeof response.callback === 'function') { + setTimeout(() => { + storage.setDataInLocalStorage(connectDomainKey, JSON.stringify('uat.mno.link')); + storage.setDataInLocalStorage(connectDataKey, JSON.stringify(getStorageData(idGraph))); + }, 500); + response.callback(function (result) { + expect(result).to.not.be.null; + expect(result).to.have.property('trustpid'); + expect(result.trustpid).to.be.equal('umidValue-xxxx'); + done() + }) + } + }); + + it('returns null if we have the data stored in the localstorage after 500ms delay and the max (waiting) delay is only 200ms ', (done) => { + const idGraph = { + 'domain': 'uat.mno.link', + 'umid': 'umidValue', + }; + + const response = trustpidSubmodule.getId({params: {maxDelayTime: 200}}); + expect(response).to.have.property('callback'); + expect(response.callback.toString()).contain('result(callback)'); + + if (typeof response.callback === 'function') { + setTimeout(() => { + storage.setDataInLocalStorage(connectDomainKey, JSON.stringify('uat.mno.link')); + storage.setDataInLocalStorage(connectDataKey, JSON.stringify(getStorageData(idGraph))); + }, 500); + response.callback(function (result) { + expect(result).to.be.null; + done() + }) + } + }); + }); + + describe('trustpid decode()', () => { + const VALID_API_RESPONSES = [ + { + expected: '32a97f612', + payload: { + trustpid: '32a97f612' + } + }, + { + expected: '32a97f61', + payload: { + trustpid: '32a97f61', + } + }, + ]; + VALID_API_RESPONSES.forEach(responseData => { + it('should return a newly constructed object with the trustpid for a payload with {trustpid: value}', () => { + expect(trustpidSubmodule.decode(responseData.payload)).to.deep.equal( + {trustpid: responseData.expected} + ); + }); + }); + + [{}, '', {foo: 'bar'}].forEach((response) => { + it(`should return null for an invalid response "${JSON.stringify(response)}"`, () => { + expect(trustpidSubmodule.decode(response)).to.be.null; + }); + }); + }); + + describe('trustpid messageHandler for acronyms', () => { + afterEach(() => { + storage.removeDataFromLocalStorage(connectDataKey); + storage.removeDataFromLocalStorage(connectDomainKey); + }); + + const domains = [ + {domain: 'tmi.mno.link', acronym: 'ndye'}, + {domain: 'tmi.vodafone.de', acronym: 'pqnx'}, + {domain: 'tmi.telekom.de', acronym: 'avgw'}, + {domain: 'tmi.tmid.es', acronym: 'kjws'}, + {domain: 'uat.mno.link', acronym: 'xxxx'}, + {domain: 'es.tmiservice.orange.com', acronym: 'aplw'}, + ]; + + domains.forEach(({domain, acronym}) => { + it(`correctly sets trustpid value and acronym to ${acronym} for ${domain} domain`, (done) => { + const idGraph = { + 'domain': domain, + 'umid': 'umidValue', + }; + + storage.setDataInLocalStorage(connectDomainKey, JSON.stringify(domain)); + storage.setDataInLocalStorage(connectDataKey, JSON.stringify(getStorageData(idGraph))); + + const eventData = { + data: `{\"msgType\":\"MNOSELECTOR\",\"body\":{\"url\":\"https://${domain}/some/path\"}}` + }; + + window.dispatchEvent(new MessageEvent('message', eventData)); + + const response = trustpidSubmodule.getId(); + expect(response).to.have.property('id'); + expect(response.id).to.have.property('trustpid'); + expect(response.id.trustpid).to.be.equal(`umidValue-${acronym}`); + done(); + }); + }); + }); +}); diff --git a/test/spec/modules/trustxBidAdapter_spec.js b/test/spec/modules/trustxBidAdapter_spec.js index e710bd6d00f..b34813948fc 100644 --- a/test/spec/modules/trustxBidAdapter_spec.js +++ b/test/spec/modules/trustxBidAdapter_spec.js @@ -401,30 +401,68 @@ describe('TrustXAdapter', function () { expect(payload.site.content).to.deep.equal(jsContent); }); - it('if segment is present in permutive targeting, payload must have right params', function () { - const permSegments = [{id: 'test_perm_1'}, {id: 'test_perm_2'}]; - const bidRequestsWithPermutiveTargeting = bidRequests.map((bid) => { - return Object.assign({ - rtd: { - p_standard: { - targeting: { - segments: permSegments - } + it('should have user.data filled from config ortb2.user.data', function () { + const userData = [ + { + name: 'someName', + segment: [1, 2, { anyKey: 'anyVal' }, 'segVal', { id: 'segId' }, { value: 'segValue' }, { id: 'segId2', name: 'segName' }] + }, + { + name: 'permutive.com', + segment: [1, 2, 'segVal', { id: 'segId' }, { anyKey: 'anyVal' }, { value: 'segValue' }, { id: 'segId2', name: 'segName' }] + }, + { + someKey: 'another data' + } + ]; + + const getConfigStub = sinon.stub(config, 'getConfig').callsFake( + arg => arg === 'ortb2.user.data' ? userData : null); + const request = spec.buildRequests([bidRequests[0]], bidderRequest); + const payload = parseRequest(request.data); + expect(payload.user.data).to.deep.equal(userData); + getConfigStub.restore(); + }); + + it('should have right value in user.data when jwpsegments are present', function () { + const userData = [ + { + name: 'someName', + segment: [1, 2, { anyKey: 'anyVal' }, 'segVal', { id: 'segId' }, { value: 'segValue' }, { id: 'segId2', name: 'segName' }] + }, + { + name: 'permutive.com', + segment: [1, 2, 'segVal', { id: 'segId' }, { anyKey: 'anyVal' }, { value: 'segValue' }, { id: 'segId2', name: 'segName' }] + }, + { + someKey: 'another data' + } + ]; + const getConfigStub = sinon.stub(config, 'getConfig').callsFake( + arg => arg === 'ortb2.user.data' ? userData : null); + + const jsContent = {id: 'test_jw_content_id'}; + const jsSegments = ['test_seg_1', 'test_seg_2']; + const bidRequestsWithJwTargeting = Object.assign({}, bidRequests[0], { + rtd: { + jwplayer: { + targeting: { + segments: jsSegments, + content: jsContent } } - }, bid); + } }); - const request = spec.buildRequests(bidRequestsWithPermutiveTargeting, bidderRequest); - expect(request.data).to.be.an('string'); + const request = spec.buildRequests([bidRequestsWithJwTargeting], bidderRequest); const payload = parseRequest(request.data); - expect(payload).to.have.property('user'); expect(payload.user.data).to.deep.equal([{ - name: 'permutive', + name: 'iow_labs_pub_data', segment: [ - {name: 'p_standard', value: permSegments[0].id}, - {name: 'p_standard', value: permSegments[1].id} + {name: 'jwpseg', value: jsSegments[0]}, + {name: 'jwpseg', value: jsSegments[1]} ] - }]); + }, ...userData]); + getConfigStub.restore(); }); it('should contain the keyword values if it present in ortb2.(site/user)', function () { @@ -501,7 +539,7 @@ describe('TrustXAdapter', function () { getConfigStub.restore(); }); - it('shold be right tmax when timeout in config is less then timeout in bidderRequest', function() { + it('should be right tmax when timeout in config is less then timeout in bidderRequest', function() { const getConfigStub = sinon.stub(config, 'getConfig').callsFake( arg => arg === 'bidderTimeout' ? 2000 : null); const request = spec.buildRequests([bidRequests[0]], bidderRequest); @@ -510,7 +548,7 @@ describe('TrustXAdapter', function () { expect(payload.tmax).to.equal(2000); getConfigStub.restore(); }); - it('shold be right tmax when timeout in bidderRequest is less then timeout in config', function() { + it('should be right tmax when timeout in bidderRequest is less then timeout in config', function() { const getConfigStub = sinon.stub(config, 'getConfig').callsFake( arg => arg === 'bidderTimeout' ? 5000 : null); const request = spec.buildRequests([bidRequests[0]], bidderRequest); @@ -561,6 +599,39 @@ describe('TrustXAdapter', function () { divid: bidRequests[2].adUnitCode }); }); + it('should contain imp[].instl if available', function() { + const ortb2Imp = [{ + instl: 1 + }, { + instl: 2, + ext: { + data: { + adserver: { + name: 'ad_server_name', + adslot: '/222222/slot' + }, + pbadslot: '/222222/slot' + } + } + }]; + const bidRequestsWithOrtb2Imp = bidRequests.slice(0, 3).map((bid, ind) => { + return Object.assign(ortb2Imp[ind] ? { ortb2Imp: ortb2Imp[ind] } : {}, bid); + }); + const request = spec.buildRequests(bidRequestsWithOrtb2Imp, bidderRequest); + expect(request.data).to.be.an('string'); + const payload = parseRequest(request.data); + expect(payload.imp[0].instl).to.equal(1); + expect(payload.imp[1].ext).to.deep.equal({ + divid: bidRequests[1].adUnitCode, + data: ortb2Imp[1].ext.data, + gpid: ortb2Imp[1].ext.data.adserver.adslot + }); + expect(payload.imp[1].instl).to.equal(2); + expect(payload.imp[2].ext).to.deep.equal({ + divid: bidRequests[2].adUnitCode + }); + expect(payload.imp[2].instl).to.be.undefined; + }); it('all id like request fields must be a string', function () { const bidderRequestWithNumId = Object.assign({}, bidderRequest, { bidderRequestId: 123123, auctionId: 345345543 }); diff --git a/test/spec/modules/ttdBidAdapter_spec.js b/test/spec/modules/ttdBidAdapter_spec.js new file mode 100644 index 00000000000..099e5e56c33 --- /dev/null +++ b/test/spec/modules/ttdBidAdapter_spec.js @@ -0,0 +1,1313 @@ +import { expect } from 'chai'; +import { spec } from 'modules/ttdBidAdapter'; +import { deepClone } from 'src/utils.js'; +import { config } from 'src/config'; + +describe('ttdBidAdapter', function () { + function testBuildRequests(bidRequests, bidderRequestBase) { + let clonedBidderRequest = deepClone(bidderRequestBase); + clonedBidderRequest.bids = bidRequests; + return spec.buildRequests(bidRequests, clonedBidderRequest); + } + + describe('isBidRequestValid', function() { + function makeBid() { + return { + 'bidder': 'ttd', + 'params': { + 'supplySourceId': 'supplier', + 'publisherId': '22222222', + 'placementId': 'some-PlacementId_1', + 'siteId': 'testSiteId' + }, + 'mediaTypes': { + 'banner': { + 'sizes': [ + [300, 250] + ] + } + }, + '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 false when supplySourceId not passed', function () { + let bid = makeBid(); + delete bid.params.supplySourceId; + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + + it('should return false when publisherId is longer than 64 characters', function () { + let bid = makeBid(); + bid.params.publisherId = '1111111111111111111111111111111111111111111111111111111111111111111111'; + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + + it('should return false when siteId not passed', function () { + let bid = makeBid(); + delete bid.params.siteId; + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + + it('should return false when siteId is longer than 50 characters', function () { + let bid = makeBid(); + bid.params.siteId = '1111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111' + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + + it('should return false when placementId not passed', function () { + let bid = makeBid(); + delete bid.params.placementId; + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + + it('should return false when the placementId is longer than 128 characters', function () { + let bid = makeBid(); + bid.params.placementId = '1111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111'; // 130 characters + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + + it('should return false if neither mediaTypes.banner nor mediaTypes.video is 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 () { + function makeBid() { + return { + 'bidder': 'ttd', + 'params': { + 'supplySourceId': 'supplier', + 'publisherId': '22222222', + 'siteId': 'testSiteId123', + 'placementId': 'somePlacementId' + }, + 'mediaTypes': { + 'video': { + 'minduration': 5, + 'maxduration': 30, + 'playerSize': [640, 480], + 'api': [1, 3], + 'mimes': ['video/mp4'], + 'protocols': [2, 3, 5, 6] + } + }, + 'adUnitCode': 'adunit-code', + 'sizes': [ + [300, 250] + ], + 'bidId': '30b31c1838de1e', + 'bidderRequestId': '22edbae2733bf6', + 'auctionId': '1d1a030790a475', + }; + } + + it('should return true if required parameters are passed', function () { + let bid = makeBid(); + expect(spec.isBidRequestValid(bid)).to.equal(true); + }); + + it('should return false if maxduration is missing', function () { + let bid = makeBid(); + delete bid.mediaTypes.video.maxduration; + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + + it('should return false if api is missing', function () { + let bid = makeBid(); + delete bid.mediaTypes.video.api; + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + + it('should return false if mimes is missing', function () { + let bid = makeBid(); + delete bid.mediaTypes.video.mimes; + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + + it('should return false if protocols is missing', function () { + let bid = makeBid(); + delete bid.mediaTypes.video.protocols; + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + }); + }); + + describe('getUserSyncs', function () { + it('to check the user sync iframe', function () { + const syncOptions = { + pixelEnabled: true + }; + const gdprConsentString = 'BON3G4EON3G4EAAABAENAA____ABl____A'; + const gdprConsent = { + consentString: gdprConsentString, + gdprApplies: true + }; + const uspConsent = '1YYY'; + + let syncs = spec.getUserSyncs(syncOptions, [], gdprConsent, uspConsent); + expect(syncs).to.have.lengthOf(1); + expect(syncs[0].type).to.equal('image'); + + let params = new URLSearchParams(new URL(syncs[0].url).search); + expect(params.get('us_privacy')).to.equal(uspConsent); + expect(params.get('ust')).to.equal('image'); + expect(params.get('gdpr')).to.equal('1'); + expect(params.get('gdpr_consent')).to.equal(gdprConsentString); + }); + }); + + describe('buildRequests-banner', function () { + const baseBannerBidRequests = [{ + 'bidder': 'ttd', + 'params': { + 'supplySourceId': 'supplier', + 'publisherId': '13144370', + 'placementId': '1gaa015', + 'siteId': 'testSiteId123' + }, + 'mediaTypes': { + 'banner': { + 'sizes': [[300, 250], [300, 600]] + } + }, + 'sizes': [[300, 250], [300, 600]], + 'adUnitCode': 'div-gpt-ad-1460505748561-0', + 'transactionId': '8651474f-58b1-4368-b812-84f8c937a099', + 'bidId': '243310435309b5', + 'bidderRequestId': '18084284054531', + 'auctionId': 'e7b34fa3-8654-424e-8c49-03e509e53d8c', + 'src': 'client', + 'bidRequestsCount': 1 + }]; + + const baseBidderRequest = { + 'bidderCode': 'ttd', + 'auctionId': 'e7b34fa3-8654-424e-8c49-03e509e53d8c', + 'bidderRequestId': '18084284054531', + 'auctionStart': 1540945362095, + 'timeout': 3000, + 'refererInfo': { + 'referer': 'https://www.example.com/test', + 'reachedTop': true, + 'numIframes': 0, + 'stack': [ + 'https://www.example.com/test' + ] + }, + 'start': 1540945362099, + 'doneCbCallCount': 0 + }; + + it('sends bid request to our endpoint that makes sense', function () { + const request = testBuildRequests(baseBannerBidRequests, baseBidderRequest); + expect(request.method).to.equal('POST'); + expect(request.url).to.be.not.empty; + expect(request.data).to.be.not.null; + }); + + 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'); + }); + + it('sends bid requests to the correct endpoint', function () { + const url = testBuildRequests(baseBannerBidRequests, baseBidderRequest).url; + expect(url).to.equal('https://direct.adsrvr.org/bid/bidder/supplier'); + }); + + it('sends publisher id, site id, and placement id', function () { + const requestBody = testBuildRequests(baseBannerBidRequests, baseBidderRequest).data; + expect(requestBody.site).to.be.not.null; + expect(requestBody.site.publisher).to.be.not.null; + expect(requestBody.imp[0].tagid).to.be.not.null; + expect(requestBody.site.publisher.id).to.equal(baseBannerBidRequests[0].params.publisherId); + expect(requestBody.site.id).to.equal(baseBannerBidRequests[0].params.siteId); + expect(requestBody.imp[0].tagid).to.equal(baseBannerBidRequests[0].params.placementId); + }); + + it('includes the ad size in the bid request', function () { + const requestBody = testBuildRequests(baseBannerBidRequests, baseBidderRequest).data; + expect(requestBody.imp[0].banner.format[0].w).to.equal(300); + expect(requestBody.imp[0].banner.format[0].h).to.equal(250); + expect(requestBody.imp[0].banner.format[1].w).to.equal(300); + expect(requestBody.imp[0].banner.format[1].h).to.equal(600); + }); + + it('includes the detected referer in the bid request', function () { + const requestBody = testBuildRequests(baseBannerBidRequests, baseBidderRequest).data; + expect(requestBody.site.page).to.equal('https://www.example.com/test'); + }); + + it('sets the banner pos correctly if sent', function () { + let clonedBannerRequests = deepClone(baseBannerBidRequests); + clonedBannerRequests[0].mediaTypes.banner.pos = 1; + + const requestBody = testBuildRequests(clonedBannerRequests, baseBidderRequest).data; + expect(requestBody.imp[0].banner.pos).to.equal(1); + }); + + it('sets the banner expansion direction correctly if sent', function () { + let clonedBannerRequests = deepClone(baseBannerBidRequests); + const expdir = [1, 3] + clonedBannerRequests[0].params.banner = { + expdir: expdir + }; + + const requestBody = testBuildRequests(clonedBannerRequests, baseBidderRequest).data; + expect(requestBody.imp[0].banner.expdir).to.equal(expdir); + }); + + it('sets keywords properly if sent', function () { + let clonedBannerRequests = deepClone(baseBannerBidRequests); + + config.setConfig({ortb2: { + site: { + keywords: 'highViewability, clothing, holiday shopping' + } + }}); + const requestBody = testBuildRequests(clonedBannerRequests, baseBidderRequest).data; + config.resetConfig(); + expect(requestBody.ext.ttdprebid.keywords).to.deep.equal(['highViewability', 'clothing', 'holiday shopping']); + }); + + it('sets ext properly', function () { + let clonedBannerRequests = deepClone(baseBannerBidRequests); + + const requestBody = testBuildRequests(clonedBannerRequests, baseBidderRequest).data; + expect(requestBody.ext.ttdprebid.pbjs).to.equal('$prebid.version$'); + }); + + it('adds gdpr consent info to the request', function () { + let consentString = 'BON3G4EON3G4EAAABAENAA____ABl____A'; + let clonedBidderRequest = deepClone(baseBidderRequest); + clonedBidderRequest.gdprConsent = { + consentString: consentString, + gdprApplies: true + }; + + const requestBody = testBuildRequests(baseBannerBidRequests, clonedBidderRequest).data; + expect(requestBody.user.ext.consent).to.equal(consentString); + expect(requestBody.regs.ext.gdpr).to.equal(1); + }); + + it('adds usp consent info to the request', function () { + let consentString = 'BON3G4EON3G4EAAABAENAA____ABl____A'; + let clonedBidderRequest = deepClone(baseBidderRequest); + clonedBidderRequest.uspConsent = consentString; + + const requestBody = testBuildRequests(baseBannerBidRequests, clonedBidderRequest).data; + expect(requestBody.regs.ext.us_privacy).to.equal(consentString); + }); + + it('adds coppa consent info to the request', function () { + let clonedBidderRequest = deepClone(baseBidderRequest); + + config.setConfig({coppa: true}); + const requestBody = testBuildRequests(baseBannerBidRequests, clonedBidderRequest).data; + config.resetConfig(); + expect(requestBody.regs.coppa).to.equal(1); + }); + + it('adds schain info to the request', function () { + const schain = { + 'ver': '1.0', + 'complete': 1, + 'nodes': [{ + 'asi': 'indirectseller.com', + 'sid': '00001', + 'hp': 1 + }, { + 'asi': 'indirectseller-2.com', + 'sid': '00002', + 'hp': 1 + }] + }; + let clonedBannerBidRequests = deepClone(baseBannerBidRequests); + clonedBannerBidRequests[0].schain = schain; + + const requestBody = testBuildRequests(clonedBannerBidRequests, baseBidderRequest).data; + expect(requestBody.source.ext.schain).to.deep.equal(schain); + }); + + it('adds unified ID info to the request', function () { + const TDID = '00000000-0000-0000-0000-000000000000'; + let clonedBannerRequests = deepClone(baseBannerBidRequests); + clonedBannerRequests[0].userId = { + tdid: TDID + }; + + const requestBody = testBuildRequests(clonedBannerRequests, baseBidderRequest).data; + expect(requestBody.user.buyeruid).to.equal(TDID); + }); + + it('adds unified ID and UID2 info to user.ext.eids in the request', function () { + const TDID = '00000000-0000-0000-0000-000000000000'; + const UID2 = '99999999-9999-9999-9999-999999999999'; + let clonedBannerRequests = deepClone(baseBannerBidRequests); + clonedBannerRequests[0].userId = { + tdid: TDID, + uid2: { + id: UID2 + } + }; + const expectedEids = [ + { + source: 'adserver.org', + uids: [ + { + atype: 1, + ext: { + rtiPartner: 'TDID' + }, + id: TDID + } + ] + }, + { + source: 'uidapi.com', + uids: [ + { + atype: 3, + id: UID2 + } + ] + } + ]; + + const requestBody = testBuildRequests(clonedBannerRequests, baseBidderRequest).data; + expect(requestBody.user.ext.eids).to.deep.equal(expectedEids); + }); + + it('adds first party site data to the request', function () { + let clonedBidderRequest = deepClone(baseBidderRequest); + + config.setConfig({ortb2: { + site: { + name: 'example', + domain: 'page.example.com', + cat: ['IAB2'], + sectioncat: ['IAB2-2'], + pagecat: ['IAB2-2'], + page: 'https://page.example.com/here.html', + ref: 'https://ref.example.com', + keywords: 'power tools, drills' + } + }}); + const requestBody = testBuildRequests(baseBannerBidRequests, clonedBidderRequest).data; + config.resetConfig(); + expect(requestBody.site.name).to.equal('example'); + expect(requestBody.site.domain).to.equal('page.example.com'); + expect(requestBody.site.cat[0]).to.equal('IAB2'); + expect(requestBody.site.sectioncat[0]).to.equal('IAB2-2'); + expect(requestBody.site.pagecat[0]).to.equal('IAB2-2'); + expect(requestBody.site.page).to.equal('https://page.example.com/here.html'); + expect(requestBody.site.ref).to.equal('https://ref.example.com'); + expect(requestBody.site.keywords).to.equal('power tools, drills'); + }); + }); + + describe('buildRequests-banner-multiple', function () { + const baseBannerMultipleBidRequests = [{ + 'bidder': 'ttd', + 'params': { + 'supplySourceId': 'supplier', + 'publisherId': '13144370', + 'placementId': 'bottom' + }, + 'mediaTypes': { + 'banner': { + 'sizes': [[300, 250], [300, 600]] + } + }, + 'sizes': [[300, 250], [300, 600]], + 'adUnitCode': 'div-gpt-ad-1460505748561-0', + 'transactionId': '8651474f-58b1-4368-b812-84f8c937a099', + 'bidId': 'small', + 'bidderRequestId': '18084284054531', + 'auctionId': 'e7b34fa3-8654-424e-8c49-03e509e53d8c', + 'src': 'client', + 'bidRequestsCount': 1 + }, { + 'bidder': 'ttd', + 'params': { + 'publisherId': '13144370', + 'placementId': 'top', + 'siteId': 'testSite123' + }, + 'mediaTypes': { + 'banner': { + 'sizes': [[728, 90]] + } + }, + 'sizes': [[728, 90]], + 'adUnitCode': 'div-gpt-ad-91515710-0', + 'transactionId': '825c1228-ca8c-4657-b40f-2df500621527', + 'bidId': 'large', + 'bidderRequestId': '18084284054531', + 'auctionId': 'e7b34fa3-8654-424e-8c49-03e509e53d8c', + 'src': 'client', + 'bidRequestsCount': 1 + }]; + + const baseBidderRequest = { + 'bidderCode': 'ttd', + 'auctionId': 'e7b34fa3-8654-424e-8c49-03e509e53d8c', + 'bidderRequestId': '18084284054531', + 'auctionStart': 1540945362095, + 'timeout': 3000, + 'refererInfo': { + 'referer': 'https://www.test.com', + 'reachedTop': true, + 'numIframes': 0, + 'stack': [ + 'https://www.test.com' + ] + }, + 'start': 1540945362099, + 'doneCbCallCount': 0 + }; + + it('sends multiple impressions', function () { + const requestBody = testBuildRequests(baseBannerMultipleBidRequests, baseBidderRequest).data; + expect(requestBody.imp.length).to.equal(2); + }); + + it('sends the right tag ids for each ad unit', function () { + const requestBody = testBuildRequests(baseBannerMultipleBidRequests, baseBidderRequest).data; + requestBody.imp.forEach(imp => { + if (imp.id === 'small') { + expect(imp.tagid).to.equal('bottom'); + } else if (imp.id === 'large') { + expect(imp.tagid).to.equal('top'); + } else { + assert.fail('no matching impression id found'); + } + }); + }); + + it('sends the sizes for each ad unit', function () { + const requestBody = testBuildRequests(baseBannerMultipleBidRequests, baseBidderRequest).data; + requestBody.imp.forEach(imp => { + if (imp.id === 'small') { + expect(imp.banner.format[0].w).to.equal(300); + expect(imp.banner.format[0].h).to.equal(250); + expect(imp.banner.format[1].w).to.equal(300); + expect(imp.banner.format[1].h).to.equal(600); + } else if (imp.id === 'large') { + expect(imp.banner.format[0].w).to.equal(728); + expect(imp.banner.format[0].h).to.equal(90); + } else { + assert.fail('no matching impression id found'); + } + }); + }); + }); + + describe('buildRequests-display-video-multiformat', function () { + const baseMultiformatBidRequests = [{ + 'bidder': 'ttd', + 'params': { + 'supplySourceId': 'supplier', + 'publisherId': '13144370', + 'placementId': '1gaa015' + }, + 'mediaTypes': { + 'video': { + 'playerSize': [640, 480], + 'api': [1, 3], + 'mimes': ['video/mp4'], + 'protocols': [2, 3, 5, 6], + 'minduration': 5, + 'maxduration': 30 + }, + 'banner': { + 'sizes': [[300, 250], [300, 600]] + } + }, + 'adUnitCode': 'div-gpt-ad-1460505748561-0', + 'transactionId': '8651474f-58b1-4368-b812-84f8c937a099', + 'bidId': '243310435309b5', + 'bidderRequestId': '18084284054531', + 'auctionId': 'e7b34fa3-8654-424e-8c49-03e509e53d8c', + 'src': 'client', + 'bidRequestsCount': 1 + }]; + + const baseBidderRequest = { + 'bidderCode': 'ttd', + 'auctionId': 'e7b34fa3-8654-424e-8c49-03e509e53d8c', + 'bidderRequestId': '18084284054531', + 'auctionStart': 1540945362095, + 'timeout': 3000, + 'refererInfo': { + 'referer': 'https://www.example.com/test', + 'reachedTop': true, + 'numIframes': 0, + 'stack': [ + 'https://www.example.com/test' + ] + }, + 'start': 1540945362099, + 'doneCbCallCount': 0 + }; + + it('includes the video ad size in the bid request', function () { + const requestBody = testBuildRequests(baseMultiformatBidRequests, baseBidderRequest).data; + expect(requestBody.imp[0].video.w).to.equal(640); + expect(requestBody.imp[0].video.h).to.equal(480); + }); + + it('includes the banner ad size in the bid request', function () { + const requestBody = testBuildRequests(baseMultiformatBidRequests, baseBidderRequest).data; + expect(requestBody.imp[0].banner.format[0].w).to.equal(300); + expect(requestBody.imp[0].banner.format[0].h).to.equal(250); + expect(requestBody.imp[0].banner.format[1].w).to.equal(300); + expect(requestBody.imp[0].banner.format[1].h).to.equal(600); + }); + }); + + describe('buildRequests-video', function () { + const baseVideoBidRequests = [{ + 'bidder': 'ttd', + 'params': { + 'supplySourceId': 'supplier', + 'publisherId': '13144370', + 'placementId': '1gaa015' + }, + 'mediaTypes': { + 'video': { + 'playerSize': [640, 480], + 'api': [1, 3], + 'mimes': ['video/mp4'], + 'protocols': [2, 3, 5, 6], + 'minduration': 5, + 'maxduration': 30 + } + }, + 'adUnitCode': 'div-gpt-ad-1460505748561-0', + 'transactionId': '8651474f-58b1-4368-b812-84f8c937a099', + 'bidId': '243310435309b5', + 'bidderRequestId': '18084284054531', + 'auctionId': 'e7b34fa3-8654-424e-8c49-03e509e53d8c', + 'src': 'client', + 'bidRequestsCount': 1 + }]; + + const baseBidderRequest = { + 'bidderCode': 'ttd', + 'auctionId': 'e7b34fa3-8654-424e-8c49-03e509e53d8c', + 'bidderRequestId': '18084284054531', + 'auctionStart': 1540945362095, + 'timeout': 3000, + 'refererInfo': { + 'referer': 'https://www.example.com/test', + 'reachedTop': true, + 'numIframes': 0, + 'stack': [ + 'https://www.example.com/test' + ] + }, + 'start': 1540945362099, + 'doneCbCallCount': 0 + }; + + it('includes the ad size in the bid request', function () { + const requestBody = testBuildRequests(baseVideoBidRequests, baseBidderRequest).data; + expect(requestBody.imp[0].video.w).to.equal(640); + expect(requestBody.imp[0].video.h).to.equal(480); + }); + + it('includes the mimes in the bid request', function () { + const requestBody = testBuildRequests(baseVideoBidRequests, baseBidderRequest).data; + expect(requestBody.imp[0].video.mimes[0]).to.equal('video/mp4'); + }); + + it('includes the min and max duration in the bid request', function () { + const requestBody = testBuildRequests(baseVideoBidRequests, baseBidderRequest).data; + expect(requestBody.imp[0].video.minduration).to.equal(5); + expect(requestBody.imp[0].video.maxduration).to.equal(30); + }); + + it('sets the minduration to 0 if missing', function () { + let clonedVideoRequests = deepClone(baseVideoBidRequests); + delete clonedVideoRequests[0].mediaTypes.video.minduration + + const requestBody = testBuildRequests(clonedVideoRequests, baseBidderRequest).data; + expect(requestBody.imp[0].video.minduration).to.equal(0); + }); + + it('includes the api frameworks in the bid request', function () { + const requestBody = testBuildRequests(baseVideoBidRequests, baseBidderRequest).data; + expect(requestBody.imp[0].video.api[0]).to.equal(1); + expect(requestBody.imp[0].video.api[1]).to.equal(3); + }); + + it('includes the protocols in the bid request', function () { + const requestBody = testBuildRequests(baseVideoBidRequests, baseBidderRequest).data; + expect(requestBody.imp[0].video.protocols[0]).to.equal(2); + expect(requestBody.imp[0].video.protocols[1]).to.equal(3); + expect(requestBody.imp[0].video.protocols[2]).to.equal(5); + expect(requestBody.imp[0].video.protocols[3]).to.equal(6); + }); + + it('sets skip correctly if sent', function () { + let clonedVideoRequests = deepClone(baseVideoBidRequests); + clonedVideoRequests[0].mediaTypes.video.skip = 1; + clonedVideoRequests[0].mediaTypes.video.skipmin = 5; + clonedVideoRequests[0].mediaTypes.video.skipafter = 10; + + const requestBody = testBuildRequests(clonedVideoRequests, baseBidderRequest).data; + expect(requestBody.imp[0].video.skip).to.equal(1); + expect(requestBody.imp[0].video.skipmin).to.equal(5); + expect(requestBody.imp[0].video.skipafter).to.equal(10); + }); + + it('sets bitrate correctly if sent', function () { + let clonedVideoRequests = deepClone(baseVideoBidRequests); + clonedVideoRequests[0].mediaTypes.video.minbitrate = 100; + clonedVideoRequests[0].mediaTypes.video.maxbitrate = 500; + + const requestBody = testBuildRequests(clonedVideoRequests, baseBidderRequest).data; + expect(requestBody.imp[0].video.minbitrate).to.equal(100); + expect(requestBody.imp[0].video.maxbitrate).to.equal(500); + }); + + it('sets pos correctly if sent', function () { + let clonedVideoRequests = deepClone(baseVideoBidRequests); + clonedVideoRequests[0].mediaTypes.video.pos = 1; + + const requestBody = testBuildRequests(clonedVideoRequests, baseBidderRequest).data; + expect(requestBody.imp[0].video.pos).to.equal(1); + }); + + it('sets playbackmethod correctly if sent', function () { + let clonedVideoRequests = deepClone(baseVideoBidRequests); + clonedVideoRequests[0].mediaTypes.video.playbackmethod = [1]; + + const requestBody = testBuildRequests(clonedVideoRequests, baseBidderRequest).data; + expect(requestBody.imp[0].video.playbackmethod[0]).to.equal(1); + }); + + it('sets startdelay correctly if sent', function () { + let clonedVideoRequests = deepClone(baseVideoBidRequests); + clonedVideoRequests[0].mediaTypes.video.startdelay = -1; + + const requestBody = testBuildRequests(clonedVideoRequests, baseBidderRequest).data; + expect(requestBody.imp[0].video.startdelay).to.equal(-1); + }); + + it('sets placement correctly if sent', function () { + let clonedVideoRequests = deepClone(baseVideoBidRequests); + clonedVideoRequests[0].mediaTypes.video.placement = 3; + + const requestBody = testBuildRequests(clonedVideoRequests, baseBidderRequest).data; + expect(requestBody.imp[0].video.placement).to.equal(3); + }); + }); + + describe('interpretResponse-empty', function () { + it('should handle empty response', function () { + let result = spec.interpretResponse({}); + expect(result.length).to.equal(0); + }); + + it('should handle empty seatbid response', function () { + let response = { + body: { + 'id': '5e5c23a5ba71e78', + 'seatbid': [] + } + }; + let result = spec.interpretResponse(response); + expect(result.length).to.equal(0); + }); + }); + + describe('interpretResponse-simple-display', function () { + const incoming = { + body: { + 'id': '5e5c23a5ba71e78', + 'seatbid': [ + { + 'bid': [ + { + 'id': '6vmb3isptf', + 'crid': 'ttdscreative', + 'impid': '322add653672f68', + 'price': 1.22, + 'adm': '', + 'cat': [], + 'h': 90, + 'w': 728, + 'ttl': 60, + 'dealid': 'ttd-dealid-1', + 'adomain': ['advertiser.com'], + 'ext': { + 'mediatype': 1 + } + } + ], + 'seat': 'MOCK' + } + ], + 'cur': 'EUR', + 'bidid': '5e5c23a5ba71e78' + } + }; + + const serverRequest = { + 'method': 'POST', + 'url': 'https://direct.adsrvr.org/bid/bidder/supplier', + 'data': { + 'id': 'c47237df-c108-419f-9c2b-da513dc3c133', + 'imp': [ + { + 'id': '322add653672f68', + 'tagid': 'simple', + 'banner': { + 'w': 728, + 'h': 90, + 'format': [ + { + 'w': 728, + 'h': 90 + } + ] + } + } + ], + 'site': { + 'page': 'http://www.test.com', + 'publisher': { + 'id': '111' + } + }, + 'device': { + 'ua': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.122 Safari/537.36', + 'dnt': 0, + 'language': 'en-US', + 'connectiontype': 0 + }, + 'user': {}, + 'at': 1, + 'cur': [ + 'USD' + ], + 'regs': {}, + 'ext': { + 'ttdprebid': { + 'ver': 'TTD-PREBID-2019.11.12', + 'pbjs': '2.31.0' + } + } + }, + 'options': { + 'withCredentials': true + } + }; + + const expectedBid = { + 'requestId': '322add653672f68', + 'cpm': 1.22, + 'width': 728, + 'height': 90, + 'creativeId': 'ttdscreative', + 'dealId': 'ttd-dealid-1', + 'currency': 'EUR', + 'netRevenue': true, + 'ttl': 60, + 'ad': '', + 'mediaType': 'banner', + 'meta': { + 'advertiserDomains': ['advertiser.com'] + } + }; + + it('should get the correct bid response', function () { + let result = spec.interpretResponse(incoming, serverRequest); + expect(result.length).to.equal(1); + expect(result[0]).to.deep.equal(expectedBid); + }); + }); + + describe('interpretResponse-multiple-display', function () { + const incoming = { + 'body': { + 'id': 'e7b34fa3-8654-424e-8c49-03e509e53d8c', + 'seatbid': [ + { + 'bid': [ + { + 'id': 'small', + 'impid': 'small', + 'price': 4.25, + 'adm': 'Default Test Ad Tag', + 'cid': 'campaignId132', + 'crid': 'creativeId999', + 'adomain': [ + 'http://foo' + ], + 'dealid': null, + 'w': 300, + 'h': 600, + 'cat': [], + 'ext': { + 'mediatype': 1 + } + }, + { + 'id': 'large', + 'impid': 'large', + 'price': 5.25, + 'adm': 'Default Test Ad Tag', + 'cid': 'campaignId132', + 'crid': 'creativeId222', + 'adomain': [ + 'http://foo2' + ], + 'dealid': null, + 'w': 728, + 'h': 90, + 'cat': [], + 'ext': { + 'mediatype': 1 + } + } + ], + 'seat': 'supplyVendorBuyerId132' + } + ], + 'cur': 'USD' + } + }; + + const expectedBids = [ + { + 'requestId': 'small', + 'cpm': 4.25, + 'width': 300, + 'height': 600, + 'creativeId': 'creativeId999', + 'currency': 'USD', + 'dealId': null, + 'netRevenue': true, + 'ttl': 360, + 'ad': 'Default Test Ad Tag', + 'mediaType': 'banner', + 'meta': { + 'advertiserDomains': ['http://foo'] + } + }, + { + 'requestId': 'large', + 'cpm': 5.25, + 'width': 728, + 'height': 90, + 'creativeId': 'creativeId222', + 'currency': 'USD', + 'dealId': null, + 'netRevenue': true, + 'ttl': 360, + 'ad': 'Default Test Ad Tag', + 'mediaType': 'banner', + 'meta': { + 'advertiserDomains': ['http://foo2'] + } + } + ]; + + const serverRequest = { + 'method': 'POST', + 'url': 'https://direct.adsrvr.org/bid/bidder/supplier', + 'data': { + 'id': 'c47237df-c108-419f-9c2b-da513dc3c133', + 'imp': [ + { + 'id': 'small', + 'tagid': 'test1', + 'banner': { + 'w': 300, + 'h': 600, + 'format': [ + { + 'w': 300, + 'h': 600 + } + ] + } + }, + { + 'id': 'large', + 'tagid': 'test2', + 'banner': { + 'w': 728, + 'h': 90, + 'format': [ + { + 'w': 728, + 'h': 90 + } + ] + } + } + ], + 'site': { + 'page': 'http://www.test.com', + 'publisher': { + 'id': '111' + } + }, + 'device': { + 'ua': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.122 Safari/537.36', + 'dnt': 0, + 'language': 'en-US', + 'connectiontype': 0 + }, + 'user': {}, + 'at': 1, + 'cur': [ + 'USD' + ], + 'regs': {}, + 'ext': { + 'ttdprebid': { + 'ver': 'TTD-PREBID-2019.11.12', + 'pbjs': '2.31.0' + } + } + }, + 'options': { + 'withCredentials': true + } + }; + + it('should get the correct bid response', function () { + let result = spec.interpretResponse(incoming, serverRequest); + expect(result.length).to.equal(2); + expect(result).to.deep.equal(expectedBids); + }); + }); + + describe('interpretResponse-simple-video', function () { + const incoming = { + 'body': { + 'cur': 'USD', + 'seatbid': [ + { + 'bid': [ + { + 'crid': 'mokivv6m', + 'ext': { + 'advid': '7ieo6xk', + 'agid': '7q9n3s2', + 'deal': { + 'dealid': '7013542' + }, + 'imptrackers': [], + 'viewabilityvendors': [], + 'mediatype': 2 + }, + 'h': 480, + 'impid': '2eabb87dfbcae4', + 'nurl': 'https://insight.adsrvr.org/enduser/vast?iid=00000000-0000-0000-0000-000000000000&crid=v3pek2eh&wp=${AUCTION_PRICE}&aid=&wpc=&sfe=0&puid=&tdid=00000000-0000-0000-0000-000000000000&pid=&ag=&adv=&sig=AAAAAAAAAAAAAA.&cf=&fq=0&td_s=&rcats=&mcat=&mste=&mfld=4&mssi=&mfsi=&uhow=&agsa=&rgco=&rgre=&rgme=&rgci=&rgz=&svbttd=0&dt=&osf=&os=&br=&rlangs=en&mlang=en&svpid=&did=&rcxt=&lat=&lon=&tmpc=&daid=&vp=0&osi=&osv=&dc=0&vcc=QAFIAVABiAECwAEDyAED0AED6AEG8AEBgAIDigIMCAIIBQgDCAYICwgMmgIECAEIAqACA6gCAsACAA..&sv=noop&pidi=&advi=&cmpi=&agi=&cridi=&svi=&cmp=&skip=1&c=&dur=&crrelr=', + 'price': 13.6, + 'ttl': 500, + 'w': 600 + } + ], + 'seat': 'supplyVendorBuyerId132' + } + ] + }, + 'headers': {} + }; + + const serverRequest = { + 'method': 'POST', + 'url': 'https://direct.bid.adsrvr.org/bid/bidder/supplier', + 'data': { + 'id': 'e94ec12d-ae1d-4ed7-abd1-eb3198ce3b63', + 'imp': [ + { + 'id': '2eabb87dfbcae4', + 'tagid': 'video', + 'video': { + 'api': [ + 1, + 3 + ], + 'mimes': [ + 'video/mp4' + ], + 'minduration': 5, + 'maxduration': 30, + 'w': 640, + 'h': 480, + 'protocols': [ + 2, + 3, + 5, + 6 + ] + } + } + ], + 'site': { + 'page': 'http://www.test.com', + 'publisher': { + 'id': '111' + } + }, + 'device': { + 'ua': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.132 Safari/537.36', + 'dnt': 0, + 'language': 'en-US', + 'connectiontype': 0 + }, + 'user': {}, + 'at': 1, + 'cur': [ + 'USD' + ], + 'regs': {}, + 'ext': { + 'ttdprebid': { + 'ver': 'TTD-PREBID-2019.11.12', + 'pbjs': '3.10.0' + } + } + }, + 'options': { + 'withCredentials': true + } + }; + + const expectedBid = + { + 'requestId': '2eabb87dfbcae4', + 'cpm': 13.6, + 'creativeId': 'mokivv6m', + 'dealId': null, + 'currency': 'USD', + 'netRevenue': true, + 'ttl': 500, + 'width': 640, + 'height': 480, + 'mediaType': 'video', + 'vastUrl': 'https://insight.adsrvr.org/enduser/vast?iid=00000000-0000-0000-0000-000000000000&crid=v3pek2eh&wp=13.6&aid=&wpc=&sfe=0&puid=&tdid=00000000-0000-0000-0000-000000000000&pid=&ag=&adv=&sig=AAAAAAAAAAAAAA.&cf=&fq=0&td_s=&rcats=&mcat=&mste=&mfld=4&mssi=&mfsi=&uhow=&agsa=&rgco=&rgre=&rgme=&rgci=&rgz=&svbttd=0&dt=&osf=&os=&br=&rlangs=en&mlang=en&svpid=&did=&rcxt=&lat=&lon=&tmpc=&daid=&vp=0&osi=&osv=&dc=0&vcc=QAFIAVABiAECwAEDyAED0AED6AEG8AEBgAIDigIMCAIIBQgDCAYICwgMmgIECAEIAqACA6gCAsACAA..&sv=noop&pidi=&advi=&cmpi=&agi=&cridi=&svi=&cmp=&skip=1&c=&dur=&crrelr=', + 'meta': {} + }; + + it('should get the correct bid response if nurl is returned', function () { + let result = spec.interpretResponse(incoming, serverRequest); + expect(result.length).to.equal(1); + expect(result[0]).to.deep.equal(expectedBid); + }); + + it('should get the correct bid response if adm is returned', function () { + const vastXml = "2.0574840600:00:30 \"Click ]]>"; + let admIncoming = deepClone(incoming); + delete admIncoming.body.seatbid[0].bid[0].nurl; + admIncoming.body.seatbid[0].bid[0].adm = vastXml; + + let vastXmlExpectedBid = deepClone(expectedBid); + delete vastXmlExpectedBid.vastUrl; + vastXmlExpectedBid.vastXml = vastXml; + + let result = spec.interpretResponse(admIncoming, serverRequest); + expect(result.length).to.equal(1); + expect(result[0]).to.deep.equal(vastXmlExpectedBid); + }); + }); + + describe('interpretResponse-display-and-video', function () { + const incoming = { + 'body': { + 'id': 'e7b34fa3-8654-424e-8c49-03e509e53d8c', + 'seatbid': [ + { + 'bid': [ + { + 'id': 'small', + 'impid': 'small', + 'price': 4.25, + 'adm': 'Default Test Ad Tag', + 'cid': 'campaignId132', + 'crid': 'creativeId999', + 'adomain': [ + 'http://foo' + ], + 'dealid': null, + 'w': 300, + 'h': 600, + 'cat': [], + 'ext': { + 'mediatype': 1 + } + }, + { + 'crid': 'mokivv6m', + 'ext': { + 'advid': '7ieo6xk', + 'agid': '7q9n3s2', + 'deal': { + 'dealid': '7013542' + }, + 'imptrackers': [], + 'viewabilityvendors': [], + 'mediatype': 2 + }, + 'h': 480, + 'impid': '2eabb87dfbcae4', + 'nurl': 'https://insight.adsrvr.org/enduser/vast?iid=00000000-0000-0000-0000-000000000000&crid=v3pek2eh&wp=${AUCTION_PRICE}&aid=&wpc=&sfe=0&puid=&tdid=00000000-0000-0000-0000-000000000000&pid=&ag=&adv=&sig=AAAAAAAAAAAAAA.&cf=&fq=0&td_s=&rcats=&mcat=&mste=&mfld=4&mssi=&mfsi=&uhow=&agsa=&rgco=&rgre=&rgme=&rgci=&rgz=&svbttd=0&dt=&osf=&os=&br=&rlangs=en&mlang=en&svpid=&did=&rcxt=&lat=&lon=&tmpc=&daid=&vp=0&osi=&osv=&dc=0&vcc=QAFIAVABiAECwAEDyAED0AED6AEG8AEBgAIDigIMCAIIBQgDCAYICwgMmgIECAEIAqACA6gCAsACAA..&sv=noop&pidi=&advi=&cmpi=&agi=&cridi=&svi=&cmp=&skip=1&c=&dur=&crrelr=', + 'price': 13.6, + 'ttl': 500, + 'w': 600 + } + ], + 'seat': 'supplyVendorBuyerId132' + } + ], + 'cur': 'USD' + } + }; + + const expectedBids = [ + { + 'requestId': 'small', + 'cpm': 4.25, + 'width': 300, + 'height': 600, + 'creativeId': 'creativeId999', + 'currency': 'USD', + 'dealId': null, + 'netRevenue': true, + 'ttl': 360, + 'ad': 'Default Test Ad Tag', + 'mediaType': 'banner', + 'meta': { + 'advertiserDomains': ['http://foo'] + } + }, + { + 'requestId': '2eabb87dfbcae4', + 'cpm': 13.6, + 'creativeId': 'mokivv6m', + 'dealId': null, + 'currency': 'USD', + 'netRevenue': true, + 'ttl': 500, + 'width': 640, + 'height': 480, + 'mediaType': 'video', + 'vastUrl': 'https://insight.adsrvr.org/enduser/vast?iid=00000000-0000-0000-0000-000000000000&crid=v3pek2eh&wp=13.6&aid=&wpc=&sfe=0&puid=&tdid=00000000-0000-0000-0000-000000000000&pid=&ag=&adv=&sig=AAAAAAAAAAAAAA.&cf=&fq=0&td_s=&rcats=&mcat=&mste=&mfld=4&mssi=&mfsi=&uhow=&agsa=&rgco=&rgre=&rgme=&rgci=&rgz=&svbttd=0&dt=&osf=&os=&br=&rlangs=en&mlang=en&svpid=&did=&rcxt=&lat=&lon=&tmpc=&daid=&vp=0&osi=&osv=&dc=0&vcc=QAFIAVABiAECwAEDyAED0AED6AEG8AEBgAIDigIMCAIIBQgDCAYICwgMmgIECAEIAqACA6gCAsACAA..&sv=noop&pidi=&advi=&cmpi=&agi=&cridi=&svi=&cmp=&skip=1&c=&dur=&crrelr=', + 'meta': {} + } + ]; + + const serverRequest = { + 'method': 'POST', + 'url': 'https://direct.adsrvr.org/bid/bidder/supplier', + 'data': { + 'id': 'c47237df-c108-419f-9c2b-da513dc3c133', + 'imp': [ + { + 'id': 'small', + 'tagid': 'test1', + 'banner': { + 'w': 300, + 'h': 600, + 'format': [ + { + 'w': 300, + 'h': 600 + } + ] + }, + }, + { + 'id': '2eabb87dfbcae4', + 'tagid': 'video', + 'video': { + 'api': [ + 1, + 3 + ], + 'mimes': [ + 'video/mp4' + ], + 'minduration': 5, + 'maxduration': 30, + 'w': 640, + 'h': 480, + 'protocols': [ + 2, + 3, + 5, + 6 + ] + } + } + ], + 'site': { + 'page': 'http://www.test.com', + 'publisher': { + 'id': '111' + } + }, + 'device': { + 'ua': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.122 Safari/537.36', + 'dnt': 0, + 'language': 'en-US', + 'connectiontype': 0 + }, + 'user': {}, + 'at': 1, + 'cur': [ + 'USD' + ], + 'regs': {}, + 'ext': { + 'ttdprebid': { + 'ver': 'TTD-PREBID-2019.11.12', + 'pbjs': '2.31.0' + } + } + }, + 'options': { + 'withCredentials': true + } + }; + + it('should get the correct bid response', function () { + let result = spec.interpretResponse(incoming, serverRequest); + expect(result.length).to.equal(2); + expect(result).to.deep.equal(expectedBids); + }); + }); +}); diff --git a/test/spec/modules/undertoneBidAdapter_spec.js b/test/spec/modules/undertoneBidAdapter_spec.js index 56217fe3561..c24f63c0b99 100644 --- a/test/spec/modules/undertoneBidAdapter_spec.js +++ b/test/spec/modules/undertoneBidAdapter_spec.js @@ -1,5 +1,7 @@ -import { expect } from 'chai'; -import { spec } from 'modules/undertoneBidAdapter.js'; +import {expect} from 'chai'; +import {spec} from 'modules/undertoneBidAdapter.js'; +import {BANNER, VIDEO} from '../../../src/mediaTypes'; +import {deepClone} from '../../../src/utils'; const URL = 'https://hb.undertone.com/hb'; const BIDDER_CODE = 'undertone'; @@ -276,6 +278,57 @@ describe('Undertone Adapter', () => { sandbox.restore(); }); + describe('getFloor', function () { + it('should send 0 floor when getFloor is undefined', function() { + const request = spec.buildRequests(videoBidReq, bidderReq); + const bidReq = JSON.parse(request.data)['x-ut-hb-params'][0]; + expect(bidReq.mediaType).to.deep.equal(VIDEO); + expect(bidReq.bidfloor).to.deep.equal(0); + }); + it('should send mocked floor when defined on video media-type', function() { + const clonedVideoBidReqArr = deepClone(videoBidReq); + const mockedFloorResponse = { + currency: 'USD', + floor: 2.3 + }; + clonedVideoBidReqArr[1].getFloor = () => mockedFloorResponse; + + const request = spec.buildRequests(clonedVideoBidReqArr, bidderReq); + const bidReq1 = JSON.parse(request.data)['x-ut-hb-params'][0]; + const bidReq2 = JSON.parse(request.data)['x-ut-hb-params'][1]; + expect(bidReq1.mediaType).to.deep.equal(VIDEO); + expect(bidReq1.bidfloor).to.deep.equal(0); + + expect(bidReq2.mediaType).to.deep.equal(VIDEO); + expect(bidReq2.bidfloor).to.deep.equal(mockedFloorResponse.floor); + }); + it('should send mocked floor on banner media-type', function() { + const clonedValidBidReqArr = [deepClone(validBidReq)]; + const mockedFloorResponse = { + currency: 'USD', + floor: 2.3 + }; + clonedValidBidReqArr[0].getFloor = () => mockedFloorResponse; + + const request = spec.buildRequests(clonedValidBidReqArr, bidderReq); + const bidReq = JSON.parse(request.data)['x-ut-hb-params'][0]; + expect(bidReq.mediaType).to.deep.equal(BANNER); + expect(bidReq.bidfloor).to.deep.equal(mockedFloorResponse.floor); + }); + it('should send 0 floor on invalid currency', function() { + const clonedValidBidReqArr = [deepClone(validBidReq)]; + const mockedFloorResponse = { + currency: 'EUR', + floor: 2.3 + }; + clonedValidBidReqArr[0].getFloor = () => mockedFloorResponse; + + const request = spec.buildRequests(clonedValidBidReqArr, bidderReq); + const bidReq = JSON.parse(request.data)['x-ut-hb-params'][0]; + expect(bidReq.mediaType).to.deep.equal(BANNER); + expect(bidReq.bidfloor).to.deep.equal(0); + }); + }); describe('supply chain', function () { it('should send supply chain if found on first bid', function () { const request = spec.buildRequests(supplyChainedBidReqs, bidderReq); diff --git a/test/spec/modules/userId_spec.js b/test/spec/modules/userId_spec.js index 8ddb02138f5..ceb83b87ebb 100644 --- a/test/spec/modules/userId_spec.js +++ b/test/spec/modules/userId_spec.js @@ -14,7 +14,7 @@ import { import {createEidsArray} from 'modules/userId/eids.js'; import {config} from 'src/config.js'; import * as utils from 'src/utils.js'; -import events from 'src/events.js'; +import * as events from 'src/events.js'; import CONSTANTS from 'src/constants.json'; import {getGlobal} from 'src/prebidGlobal.js'; import { @@ -23,7 +23,7 @@ import { setConsentConfig } from 'modules/consentManagement.js'; import {server} from 'test/mocks/xhr.js'; -import find from 'core-js-pure/features/array/find.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'; @@ -36,7 +36,7 @@ import {nextrollIdSubmodule} from 'modules/nextrollIdSystem.js'; import {intentIqIdSubmodule} from 'modules/intentIqIdSystem.js'; import {zeotapIdPlusSubmodule} from 'modules/zeotapIdPlusIdSystem.js'; import {sharedIdSystemSubmodule} from 'modules/sharedIdSystem.js'; -import {haloIdSubmodule} from 'modules/haloIdSystem.js'; +import {hadronIdSubmodule} from 'modules/hadronIdSystem.js'; import {pubProvidedIdSubmodule} from 'modules/pubProvidedIdSystem.js'; import {criteoIdSubmodule} from 'modules/criteoIdSystem.js'; import {mwOpenLinkIdSubModule} from 'modules/mwOpenLinkIdSystem.js'; @@ -51,6 +51,8 @@ import {akamaiDAPIdSubmodule} from 'modules/akamaiDAPIdSystem.js' import {kinessoIdSubmodule} from 'modules/kinessoIdSystem.js' import {adqueryIdSubmodule} from 'modules/adqueryIdSystem.js'; import * as mockGpt from '../integration/faker/googletag.js'; +import 'src/prebid.js'; +import {hook} from '../../../src/hook.js'; let assert = require('chai').assert; let expect = require('chai').expect; @@ -107,10 +109,16 @@ describe('User ID', function () { } before(function () { + hook.ready(); localStorage.removeItem(PBJS_USER_ID_OPTOUT_NAME); }); beforeEach(function () { + // TODO: this whole suite needs to be redesigned; it is passing by accident + // some tests do not pass if consent data is available + // (there are functions here with signature `getId(config, storedId)`, but storedId is actually consentData) + // also, this file is ginormous; do we really need to test *all* id systems as one? + resetConsentData(); coreStorage.setCookie(CONSENT_LOCAL_STORAGE_NAME, '', EXPIRED_COOKIE_DATE); }); @@ -625,7 +633,7 @@ describe('User ID', function () { }); it('config with 24 configurations should result in 24 submodules add', function () { - setSubmoduleRegistry([sharedIdSystemSubmodule, unifiedIdSubmodule, id5IdSubmodule, identityLinkSubmodule, liveIntentIdSubmodule, britepoolIdSubmodule, netIdSubmodule, nextrollIdSubmodule, intentIqIdSubmodule, zeotapIdPlusSubmodule, haloIdSubmodule, pubProvidedIdSubmodule, criteoIdSubmodule, mwOpenLinkIdSubModule, tapadIdSubmodule, uid2IdSubmodule, admixerIdSubmodule, deepintentDpesSubmodule, dmdIdSubmodule, flocIdSubmodule, akamaiDAPIdSubmodule, amxIdSubmodule, kinessoIdSubmodule, adqueryIdSubmodule]); + setSubmoduleRegistry([sharedIdSystemSubmodule, unifiedIdSubmodule, id5IdSubmodule, identityLinkSubmodule, liveIntentIdSubmodule, britepoolIdSubmodule, netIdSubmodule, nextrollIdSubmodule, intentIqIdSubmodule, zeotapIdPlusSubmodule, hadronIdSubmodule, pubProvidedIdSubmodule, criteoIdSubmodule, mwOpenLinkIdSubModule, tapadIdSubmodule, uid2IdSubmodule, admixerIdSubmodule, deepintentDpesSubmodule, dmdIdSubmodule, flocIdSubmodule, akamaiDAPIdSubmodule, amxIdSubmodule, kinessoIdSubmodule, adqueryIdSubmodule]); init(config); config.setConfig({ userSync: { @@ -658,8 +666,8 @@ describe('User ID', function () { name: 'intentIqId', storage: {name: 'intentIqId', type: 'cookie'} }, { - name: 'haloId', - storage: {name: 'haloId', type: 'cookie'} + name: 'hadronId', + storage: {name: 'hadronId', type: 'cookie'} }, { name: 'zeotapIdPlus' }, { @@ -700,7 +708,7 @@ describe('User ID', function () { }); it('config syncDelay updates module correctly', function () { - setSubmoduleRegistry([sharedIdSystemSubmodule, unifiedIdSubmodule, id5IdSubmodule, identityLinkSubmodule, netIdSubmodule, nextrollIdSubmodule, intentIqIdSubmodule, zeotapIdPlusSubmodule, haloIdSubmodule, pubProvidedIdSubmodule, criteoIdSubmodule, mwOpenLinkIdSubModule, tapadIdSubmodule, uid2IdSubmodule, admixerIdSubmodule, deepintentDpesSubmodule, dmdIdSubmodule, flocIdSubmodule, akamaiDAPIdSubmodule, amxIdSubmodule, kinessoIdSubmodule, adqueryIdSubmodule]); + setSubmoduleRegistry([sharedIdSystemSubmodule, unifiedIdSubmodule, id5IdSubmodule, identityLinkSubmodule, netIdSubmodule, nextrollIdSubmodule, intentIqIdSubmodule, zeotapIdPlusSubmodule, hadronIdSubmodule, pubProvidedIdSubmodule, criteoIdSubmodule, mwOpenLinkIdSubModule, tapadIdSubmodule, uid2IdSubmodule, admixerIdSubmodule, deepintentDpesSubmodule, dmdIdSubmodule, flocIdSubmodule, akamaiDAPIdSubmodule, amxIdSubmodule, kinessoIdSubmodule, adqueryIdSubmodule]); init(config); config.setConfig({ @@ -716,7 +724,7 @@ describe('User ID', function () { }); it('config auctionDelay updates module correctly', function () { - setSubmoduleRegistry([sharedIdSystemSubmodule, unifiedIdSubmodule, id5IdSubmodule, identityLinkSubmodule, netIdSubmodule, nextrollIdSubmodule, intentIqIdSubmodule, zeotapIdPlusSubmodule, haloIdSubmodule, pubProvidedIdSubmodule, criteoIdSubmodule, mwOpenLinkIdSubModule, tapadIdSubmodule, uid2IdSubmodule, admixerIdSubmodule, deepintentDpesSubmodule, dmdIdSubmodule, flocIdSubmodule, akamaiDAPIdSubmodule, amxIdSubmodule, kinessoIdSubmodule, adqueryIdSubmodule]); + setSubmoduleRegistry([sharedIdSystemSubmodule, unifiedIdSubmodule, id5IdSubmodule, identityLinkSubmodule, netIdSubmodule, nextrollIdSubmodule, intentIqIdSubmodule, zeotapIdPlusSubmodule, hadronIdSubmodule, pubProvidedIdSubmodule, criteoIdSubmodule, mwOpenLinkIdSubModule, tapadIdSubmodule, uid2IdSubmodule, admixerIdSubmodule, deepintentDpesSubmodule, dmdIdSubmodule, flocIdSubmodule, akamaiDAPIdSubmodule, amxIdSubmodule, kinessoIdSubmodule, adqueryIdSubmodule]); init(config); config.setConfig({ userSync: { @@ -731,7 +739,7 @@ describe('User ID', function () { }); it('config auctionDelay defaults to 0 if not a number', function () { - setSubmoduleRegistry([sharedIdSystemSubmodule, unifiedIdSubmodule, id5IdSubmodule, identityLinkSubmodule, netIdSubmodule, nextrollIdSubmodule, intentIqIdSubmodule, zeotapIdPlusSubmodule, haloIdSubmodule, pubProvidedIdSubmodule, criteoIdSubmodule, mwOpenLinkIdSubModule, tapadIdSubmodule, uid2IdSubmodule, admixerIdSubmodule, deepintentDpesSubmodule, dmdIdSubmodule, flocIdSubmodule, akamaiDAPIdSubmodule, amxIdSubmodule, kinessoIdSubmodule, adqueryIdSubmodule]); + setSubmoduleRegistry([sharedIdSystemSubmodule, unifiedIdSubmodule, id5IdSubmodule, identityLinkSubmodule, netIdSubmodule, nextrollIdSubmodule, intentIqIdSubmodule, zeotapIdPlusSubmodule, hadronIdSubmodule, pubProvidedIdSubmodule, criteoIdSubmodule, mwOpenLinkIdSubModule, tapadIdSubmodule, uid2IdSubmodule, admixerIdSubmodule, deepintentDpesSubmodule, dmdIdSubmodule, flocIdSubmodule, akamaiDAPIdSubmodule, amxIdSubmodule, kinessoIdSubmodule, adqueryIdSubmodule]); init(config); config.setConfig({ userSync: { @@ -1639,28 +1647,28 @@ describe('User ID', function () { }, {adUnits}); }); - it('test hook from haloId html5', function (done) { + it('test hook from hadronId html5', function (done) { // simulate existing browser local storage values - localStorage.setItem('haloId', JSON.stringify({'haloId': 'random-ls-identifier'})); - localStorage.setItem('haloId_exp', ''); + localStorage.setItem('hadronId', JSON.stringify({'hadronId': 'random-ls-identifier'})); + localStorage.setItem('hadronId_exp', ''); - setSubmoduleRegistry([haloIdSubmodule]); + setSubmoduleRegistry([hadronIdSubmodule]); init(config); - config.setConfig(getConfigMock(['haloId', 'haloId', 'html5'])); + config.setConfig(getConfigMock(['hadronId', 'hadronId', 'html5'])); requestBidsHook(function () { adUnits.forEach(unit => { unit.bids.forEach(bid => { - expect(bid).to.have.deep.nested.property('userId.haloId'); - expect(bid.userId.haloId).to.equal('random-ls-identifier'); + expect(bid).to.have.deep.nested.property('userId.hadronId'); + expect(bid.userId.hadronId).to.equal('random-ls-identifier'); expect(bid.userIdAsEids[0]).to.deep.equal({ source: 'audigent.com', uids: [{id: 'random-ls-identifier', atype: 1}] }); }); }); - localStorage.removeItem('haloId'); - localStorage.removeItem('haloId_exp', ''); + localStorage.removeItem('hadronId'); + localStorage.removeItem('hadronId_exp', ''); done(); }, {adUnits}); }); @@ -1858,7 +1866,7 @@ describe('User ID', function () { }, {adUnits}); }); - it('test hook when pubCommonId, unifiedId, id5Id, identityLink, britepoolId, intentIqId, zeotapIdPlus, netId, haloId, Criteo, UID 2.0, admixerId, amxId, dmdId, kpuid, qid and mwOpenLinkId have data to pass', function (done) { + it('test hook when pubCommonId, unifiedId, id5Id, identityLink, britepoolId, intentIqId, zeotapIdPlus, netId, hadronId, Criteo, UID 2.0, admixerId, amxId, dmdId, kpuid, qid and mwOpenLinkId have data to pass', function (done) { coreStorage.setCookie('pubcid', 'testpubcid', (new Date(Date.now() + 5000).toUTCString())); coreStorage.setCookie('unifiedid', JSON.stringify({'TDID': 'testunifiedid'}), (new Date(Date.now() + 5000).toUTCString())); coreStorage.setCookie('id5id', JSON.stringify({'universal_uid': 'testid5id'}), (new Date(Date.now() + 5000).toUTCString())); @@ -1868,7 +1876,7 @@ describe('User ID', function () { coreStorage.setCookie('netId', JSON.stringify({'netId': 'testnetId'}), (new Date(Date.now() + 5000).toUTCString())); coreStorage.setCookie('intentIqId', 'testintentIqId', (new Date(Date.now() + 5000).toUTCString())); coreStorage.setCookie('IDP', btoa(JSON.stringify('zeotapId')), (new Date(Date.now() + 5000).toUTCString())); - coreStorage.setCookie('haloId', JSON.stringify({'haloId': 'testHaloId'}), (new Date(Date.now() + 5000).toUTCString())); + coreStorage.setCookie('hadronId', JSON.stringify({'hadronId': 'testHadronId'}), (new Date(Date.now() + 5000).toUTCString())); coreStorage.setCookie('storage_criteo', JSON.stringify({'criteoId': 'test_bidid'}), (new Date(Date.now() + 5000).toUTCString())); coreStorage.setCookie('mwol', JSON.stringify({eid: 'XX-YY-ZZ-123'}), (new Date(Date.now() + 5000).toUTCString())); coreStorage.setCookie('uid2id', 'Sample_AD_Token', (new Date(Date.now() + 5000).toUTCString())); @@ -1881,7 +1889,7 @@ describe('User ID', function () { // qid only supports localStorage localStorage.setItem('qid', 'testqid'); localStorage.setItem('qid_exp', (new Date(Date.now() + 5000)).toUTCString()); - setSubmoduleRegistry([sharedIdSystemSubmodule, unifiedIdSubmodule, id5IdSubmodule, identityLinkSubmodule, britepoolIdSubmodule, netIdSubmodule, intentIqIdSubmodule, zeotapIdPlusSubmodule, haloIdSubmodule, criteoIdSubmodule, mwOpenLinkIdSubModule, tapadIdSubmodule, uid2IdSubmodule, admixerIdSubmodule, deepintentDpesSubmodule, dmdIdSubmodule, amxIdSubmodule, kinessoIdSubmodule, adqueryIdSubmodule]); + setSubmoduleRegistry([sharedIdSystemSubmodule, unifiedIdSubmodule, id5IdSubmodule, identityLinkSubmodule, britepoolIdSubmodule, netIdSubmodule, intentIqIdSubmodule, zeotapIdPlusSubmodule, hadronIdSubmodule, criteoIdSubmodule, mwOpenLinkIdSubModule, tapadIdSubmodule, uid2IdSubmodule, admixerIdSubmodule, deepintentDpesSubmodule, dmdIdSubmodule, amxIdSubmodule, kinessoIdSubmodule, adqueryIdSubmodule]); init(config); config.setConfig(getConfigMock(['pubCommonId', 'pubcid', 'cookie'], ['unifiedId', 'unifiedid', 'cookie'], @@ -1892,7 +1900,7 @@ describe('User ID', function () { ['netId', 'netId', 'cookie'], ['intentIqId', 'intentIqId', 'cookie'], ['zeotapIdPlus', 'IDP', 'cookie'], - ['haloId', 'haloId', 'cookie'], + ['hadronId', 'hadronId', 'cookie'], ['criteo', 'storage_criteo', 'cookie'], ['mwOpenLinkId', 'mwol', 'cookie'], ['tapadId', 'tapad_id', 'cookie'], @@ -1933,9 +1941,9 @@ describe('User ID', function () { // also check that zeotapIdPlus id data was copied to bid expect(bid).to.have.deep.nested.property('userId.IDP'); expect(bid.userId.IDP).to.equal('zeotapId'); - // also check that haloId id was copied to bid - expect(bid).to.have.deep.nested.property('userId.haloId'); - expect(bid.userId.haloId).to.equal('testHaloId'); + // also check that hadronId id was copied to bid + expect(bid).to.have.deep.nested.property('userId.hadronId'); + expect(bid.userId.hadronId).to.equal('testHadronId'); // also check that criteo id was copied to bid expect(bid).to.have.deep.nested.property('userId.criteoId'); expect(bid.userId.criteoId).to.equal('test_bidid'); @@ -1974,7 +1982,7 @@ describe('User ID', function () { coreStorage.setCookie('netId', '', EXPIRED_COOKIE_DATE); coreStorage.setCookie('intentIqId', '', EXPIRED_COOKIE_DATE); coreStorage.setCookie('IDP', '', EXPIRED_COOKIE_DATE); - coreStorage.setCookie('haloId', '', EXPIRED_COOKIE_DATE); + coreStorage.setCookie('hadronId', '', EXPIRED_COOKIE_DATE); coreStorage.setCookie('storage_criteo', '', EXPIRED_COOKIE_DATE); coreStorage.setCookie('mwol', '', EXPIRED_COOKIE_DATE); coreStorage.setCookie('uid2id', '', EXPIRED_COOKIE_DATE); @@ -1989,7 +1997,7 @@ describe('User ID', function () { }, {adUnits}); }); - it('test hook when pubCommonId, unifiedId, id5Id, britepoolId, dmdId, intentIqId, zeotapIdPlus, criteo, netId, haloId, UID 2.0, admixerId, kpuid and mwOpenLinkId have their modules added before and after init', function (done) { + it('test hook when pubCommonId, unifiedId, id5Id, britepoolId, dmdId, intentIqId, zeotapIdPlus, criteo, netId, hadronId, UID 2.0, admixerId, kpuid and mwOpenLinkId have their modules added before and after init', function (done) { coreStorage.setCookie('pubcid', 'testpubcid', (new Date(Date.now() + 5000).toUTCString())); coreStorage.setCookie('unifiedid', JSON.stringify({'TDID': 'cookie-value-add-module-variations'}), new Date(Date.now() + 5000).toUTCString()); coreStorage.setCookie('id5id', JSON.stringify({'universal_uid': 'testid5id'}), (new Date(Date.now() + 5000).toUTCString())); @@ -1998,7 +2006,7 @@ describe('User ID', function () { coreStorage.setCookie('netId', JSON.stringify({'netId': 'testnetId'}), (new Date(Date.now() + 5000).toUTCString())); coreStorage.setCookie('intentIqId', 'testintentIqId', (new Date(Date.now() + 5000).toUTCString())); coreStorage.setCookie('IDP', btoa(JSON.stringify('zeotapId')), (new Date(Date.now() + 5000).toUTCString())); - coreStorage.setCookie('haloId', JSON.stringify({'haloId': 'testHaloId'}), (new Date(Date.now() + 5000).toUTCString())); + coreStorage.setCookie('hadronId', JSON.stringify({'hadronId': 'testHadronId'}), (new Date(Date.now() + 5000).toUTCString())); coreStorage.setCookie('dmdId', 'testdmdId', (new Date(Date.now() + 5000).toUTCString())); coreStorage.setCookie('storage_criteo', JSON.stringify({'criteoId': 'test_bidid'}), (new Date(Date.now() + 5000).toUTCString())); coreStorage.setCookie('mwol', JSON.stringify({eid: 'XX-YY-ZZ-123'}), (new Date(Date.now() + 5000).toUTCString())); @@ -2022,7 +2030,7 @@ describe('User ID', function () { attachIdSystem(netIdSubmodule); attachIdSystem(intentIqIdSubmodule); attachIdSystem(zeotapIdPlusSubmodule); - attachIdSystem(haloIdSubmodule); + attachIdSystem(hadronIdSubmodule); attachIdSystem(dmdIdSubmodule); attachIdSystem(criteoIdSubmodule); attachIdSystem(mwOpenLinkIdSubModule); @@ -2040,7 +2048,7 @@ describe('User ID', function () { ['netId', 'netId', 'cookie'], ['intentIqId', 'intentIqId', 'cookie'], ['zeotapIdPlus', 'IDP', 'cookie'], - ['haloId', 'haloId', 'cookie'], + ['hadronId', 'hadronId', 'cookie'], ['dmdId', 'dmdId', 'cookie'], ['criteo', 'storage_criteo', 'cookie'], ['mwOpenLinkId', 'mwol', 'cookie'], @@ -2078,9 +2086,9 @@ describe('User ID', function () { // also check that zeotapIdPlus id data was copied to bid expect(bid).to.have.deep.nested.property('userId.IDP'); expect(bid.userId.IDP).to.equal('zeotapId'); - // also check that haloId id data was copied to bid - expect(bid).to.have.deep.nested.property('userId.haloId'); - expect(bid.userId.haloId).to.equal('testHaloId'); + // also check that hadronId id data was copied to bid + expect(bid).to.have.deep.nested.property('userId.hadronId'); + expect(bid.userId.hadronId).to.equal('testHadronId'); // also check that dmdId id data was copied to bid expect(bid).to.have.deep.nested.property('userId.dmdId'); expect(bid.userId.dmdId).to.equal('testdmdId'); @@ -2116,7 +2124,7 @@ describe('User ID', function () { coreStorage.setCookie('netId', '', EXPIRED_COOKIE_DATE); coreStorage.setCookie('intentIqId', '', EXPIRED_COOKIE_DATE); coreStorage.setCookie('IDP', '', EXPIRED_COOKIE_DATE); - coreStorage.setCookie('haloId', '', EXPIRED_COOKIE_DATE); + coreStorage.setCookie('hadronId', '', EXPIRED_COOKIE_DATE); coreStorage.setCookie('dmdId', '', EXPIRED_COOKIE_DATE); coreStorage.setCookie('storage_criteo', '', EXPIRED_COOKIE_DATE); coreStorage.setCookie('mwol', '', EXPIRED_COOKIE_DATE); @@ -2166,7 +2174,7 @@ describe('User ID', function () { coreStorage.setCookie('netId', JSON.stringify({'netId': 'testnetId'}), new Date(Date.now() + 5000).toUTCString()); coreStorage.setCookie('intentIqId', 'testintentIqId', (new Date(Date.now() + 5000).toUTCString())); coreStorage.setCookie('IDP', btoa(JSON.stringify('zeotapId')), (new Date(Date.now() + 5000).toUTCString())); - coreStorage.setCookie('haloId', JSON.stringify({'haloId': 'testHaloId'}), (new Date(Date.now() + 5000).toUTCString())); + coreStorage.setCookie('hadronId', JSON.stringify({'hadronId': 'testHadronId'}), (new Date(Date.now() + 5000).toUTCString())); coreStorage.setCookie('admixerId', 'testadmixerId', new Date(Date.now() + 5000).toUTCString()); coreStorage.setCookie('deepintentId', 'testdeepintentId', new Date(Date.now() + 5000).toUTCString()); coreStorage.setCookie('MOCKID', JSON.stringify({'MOCKID': '123456778'}), new Date(Date.now() + 5000).toUTCString()); @@ -2177,7 +2185,7 @@ describe('User ID', function () { localStorage.setItem('qid', 'testqid'); localStorage.setItem('qid_exp', new Date(Date.now() + 5000).toUTCString()) - setSubmoduleRegistry([sharedIdSystemSubmodule, unifiedIdSubmodule, id5IdSubmodule, identityLinkSubmodule, britepoolIdSubmodule, netIdSubmodule, intentIqIdSubmodule, zeotapIdPlusSubmodule, haloIdSubmodule, uid2IdSubmodule, admixerIdSubmodule, deepintentDpesSubmodule, dmdIdSubmodule, akamaiDAPIdSubmodule, amxIdSubmodule, kinessoIdSubmodule, adqueryIdSubmodule]); + setSubmoduleRegistry([sharedIdSystemSubmodule, unifiedIdSubmodule, id5IdSubmodule, identityLinkSubmodule, britepoolIdSubmodule, netIdSubmodule, intentIqIdSubmodule, zeotapIdPlusSubmodule, hadronIdSubmodule, uid2IdSubmodule, admixerIdSubmodule, deepintentDpesSubmodule, dmdIdSubmodule, akamaiDAPIdSubmodule, amxIdSubmodule, kinessoIdSubmodule, adqueryIdSubmodule]); init(config); config.setConfig({ @@ -2202,7 +2210,7 @@ describe('User ID', function () { }, { name: 'zeotapIdPlus' }, { - name: 'haloId', storage: {name: 'haloId', type: 'cookie'} + name: 'hadronId', storage: {name: 'hadronId', type: 'cookie'} }, { name: 'admixerId', storage: {name: 'admixerId', type: 'cookie'} }, { @@ -2268,9 +2276,9 @@ describe('User ID', function () { // also check that zeotapIdPlus id data was copied to bid expect(bid).to.have.deep.nested.property('userId.IDP'); expect(bid.userId.IDP).to.equal('zeotapId'); - // also check that haloId id data was copied to bid - expect(bid).to.have.deep.nested.property('userId.haloId'); - expect(bid.userId.haloId).to.equal('testHaloId'); + // also check that hadronId id data was copied to bid + expect(bid).to.have.deep.nested.property('userId.hadronId'); + expect(bid.userId.hadronId).to.equal('testHadronId'); expect(bid.userId.uid2).to.deep.equal({ id: 'Sample_AD_Token' }); @@ -2302,7 +2310,7 @@ describe('User ID', function () { coreStorage.setCookie('netId', '', EXPIRED_COOKIE_DATE); coreStorage.setCookie('intentIqId', '', EXPIRED_COOKIE_DATE); coreStorage.setCookie('IDP', '', EXPIRED_COOKIE_DATE); - coreStorage.setCookie('haloId', '', EXPIRED_COOKIE_DATE); + coreStorage.setCookie('hadronId', '', EXPIRED_COOKIE_DATE); coreStorage.setCookie('dmdId', '', EXPIRED_COOKIE_DATE); coreStorage.setCookie('admixerId', '', EXPIRED_COOKIE_DATE); coreStorage.setCookie('deepintentId', '', EXPIRED_COOKIE_DATE); @@ -2653,5 +2661,97 @@ describe('User ID', function () { }); }); }); + + describe('handles config with ESP configuration in user sync object', function() { + describe('Call registerSignalSources to register signal sources with gtag', function () { + it('pbjs.registerSignalSources should be defined', () => { + expect(typeof (getGlobal()).registerSignalSources).to.equal('function'); + }); + }) + + describe('Call getEncryptedEidsForSource to get encrypted Eids for source', function() { + const signalSources = ['pubcid.org']; + + it('pbjs.getEncryptedEidsForSource should be defined', () => { + expect(typeof (getGlobal()).getEncryptedEidsForSource).to.equal('function'); + }); + + it('pbjs.getEncryptedEidsForSource should return the string without encryption if encryption is false', (done) => { + setSubmoduleRegistry([sharedIdSystemSubmodule]); + init(config); + config.setConfig({ + userSync: { + syncDelay: 0, + userIds: [ + { + 'name': 'sharedId', + 'storage': { + 'type': 'cookie', + 'name': '_pubcid', + 'expires': 365 + } + }, + { + 'name': 'pubcid.org' + } + ] + }, + }); + const encrypt = false; + (getGlobal()).getEncryptedEidsForSource(signalSources[0], encrypt).then((data) => { + let users = (getGlobal()).getUserIdsAsEids(); + expect(data).to.equal(users[0].uids[0].id); + done(); + }).catch(done); + }); + + it('pbjs.getEncryptedEidsForSource should return the string base64 encryption if encryption is true', (done) => { + const encrypt = true; + (getGlobal()).getEncryptedEidsForSource(signalSources[0], encrypt).then((result) => { + expect(result.startsWith('1||')).to.true; + done(); + }).catch(done); + }); + + it('pbjs.getEncryptedEidsForSource should return string if custom function is defined', (done) => { + const getCustomSignal = () => { + return '{"keywords":["tech","auto"]}'; + } + const expectedString = '1||eyJrZXl3b3JkcyI6WyJ0ZWNoIiwiYXV0byJdfQ=='; + const encrypt = false; + const source = 'pubmatic.com'; + (getGlobal()).getEncryptedEidsForSource(source, encrypt, getCustomSignal).then((result) => { + expect(result).to.equal(expectedString); + done(); + }).catch(done); + }); + + it('pbjs.getUserIdsAsEidBySource', () => { + const users = { + 'source': 'pubcid.org', + 'uids': [ + { + 'id': '11111', + 'atype': 1 + } + ] + } + setSubmoduleRegistry([sharedIdSystemSubmodule, amxIdSubmodule]); + init(config); + config.setConfig({ + userSync: { + syncDelay: 0, + userIds: [{ + name: 'pubCommonId', value: {'pubcid': '11111'} + }, { + name: 'amxId', value: {'amxId': 'amx-id-value-amx-id-value-amx-id-value'} + }] + } + }); + expect(typeof (getGlobal()).getUserIdsAsEidBySource).to.equal('function'); + expect((getGlobal()).getUserIdsAsEidBySource(signalSources[0])).to.deep.equal(users); + }); + }) + }); }) }); diff --git a/test/spec/modules/vibrantmediaBidAdapter_spec.js b/test/spec/modules/vibrantmediaBidAdapter_spec.js new file mode 100644 index 00000000000..c6ce7d52fb3 --- /dev/null +++ b/test/spec/modules/vibrantmediaBidAdapter_spec.js @@ -0,0 +1,1237 @@ +import {expect} from 'chai'; +import {spec} from 'modules/vibrantmediaBidAdapter.js'; +import {newBidder} from 'src/adapters/bidderFactory.js'; +import {BANNER, NATIVE, VIDEO} from 'src/mediaTypes.js'; +import {INSTREAM, OUTSTREAM} from 'src/video.js'; + +const EXPECTED_PREBID_SERVER_URL = 'https://prebid.intellitxt.com/prebid'; + +const BANNER_AD = + 'Test Banner Ad UnitHello!'; +const VIDEO_AD = 'Test Video Ad Unit' + + ''; + +const VALID_BANNER_BID_PARAMS = Object.freeze({ + member: '1234', + invCode: 'ABCD', + placementId: '10433394' +}); + +const VALID_VIDEO_BID_PARAMS = Object.freeze({ + member: '1234', + invCode: 'ABCD', + placementId: '10433394', + video: { + skippable: false, + playback_method: 'auto_play_sound_off' + } +}); + +const VALID_NATIVE_BID_PARAMS = VALID_BANNER_BID_PARAMS; + +const DEFAULT_BID_SIZES = [[300, 250], [600, 240]]; + +const VALID_CONSENT_STRING = 'BOJ8RZsOJ8RZsABAB8AAAAAZ+A=='; + +const getValidBidderRequest = (bidRequests) => { + return Object.freeze({ + bidderCode: 'vibrantmedia', + auctionId: '1d1a030790a475', + bidderRequestId: '22edbae2733bf6', + timeout: 3000, + gdprConsent: { + consentString: VALID_CONSENT_STRING, + vendorData: {}, + gdprApplies: true, + }, + bids: bidRequests, + }); +}; + +describe('VibrantMediaBidAdapter', function () { + const adapter = newBidder(spec); + + describe('constants', function () { + expect(spec.code).to.equal('vibrantmedia'); + expect(spec.supportedMediaTypes).to.deep.equal([BANNER, NATIVE, VIDEO]); + }); + + describe('inherited functions', function () { + it('exists and is a function', function () { + expect(adapter.callBids).to.exist.and.to.be.a('function'); + }); + }); + + describe('transformBidParams', function () { + it('transforms bid params correctly', function () { + expect(spec.transformBidParams(VALID_VIDEO_BID_PARAMS)).to.deep.equal(VALID_VIDEO_BID_PARAMS); + }); + }) + + let bidRequest; + + beforeEach(function () { + bidRequest = { + bidder: 'vibrantmedia', + params: { + // Filled in by individual tests + }, + mediaTypes: { + // Filled in by individual tests + }, + adUnitCode: 'test-div', + transactionId: '13579acef87623', + placementId: '7623587623857', + bidId: '30b31c1838de1e', + bidderRequestId: '22edbae2733bf6', + auctionId: '1d1a030790a475' + }; + }); + + describe('isBidRequestValid', function () { + describe('with banner bid requests', function () { + beforeEach(function () { + bidRequest.mediaTypes.banner = { + sizes: DEFAULT_BID_SIZES, + }; + }); + + it('should return true for a valid banner bid request', function () { + bidRequest.params = VALID_BANNER_BID_PARAMS; + expect(spec.isBidRequestValid(bidRequest)).to.equal(true); + }); + + it('should return true for a valid banner bid request with a member id and inventory code', function () { + bidRequest.params = { + member: '1234', + invCode: 'ABCD', + }; + expect(spec.isBidRequestValid(bidRequest)).to.equal(true); + }); + + it('should return true for a valid banner bid request with a placement id', function () { + bidRequest.params = { + placementId: '10433394', + }; + expect(spec.isBidRequestValid(bidRequest)).to.equal(true); + }); + + it('should return false for a valid banner bid request but with a member id and no inventory code', function () { + bidRequest.params = { + member: '1234', + }; + expect(spec.isBidRequestValid(bidRequest)).to.equal(false); + }); + + it('should return false for a valid banner bid request but with no member id and an inventory code', function () { + bidRequest.params = { + invCode: 'ABCD', + }; + expect(spec.isBidRequestValid(bidRequest)).to.equal(false); + }); + + it('should return false for a valid banner bid request but with no supported media types', function () { + bidRequest.params = { + placementId: '10433394', + }; + delete bidRequest.mediaTypes.banner; + bidRequest.mediaTypes.unsupported = { + sizes: DEFAULT_BID_SIZES, + } + expect(spec.isBidRequestValid(bidRequest)).to.equal(false); + }); + + it('should return false for a valid banner bid request but with no params', function () { + bidRequest.params = {}; + expect(spec.isBidRequestValid(bidRequest)).to.equal(false); + }); + }); + + describe('with video bid requests', function () { + describe('with sizes attribute', function () { + const validVideoMediaTypes = { + context: OUTSTREAM, + sizes: DEFAULT_BID_SIZES, + minduration: 1, + maxduration: 60, + skip: 0, + skipafter: 5, + playbackmethod: [2], + protocols: [1, 2, 3] + }; + + it('should return true for a valid video bid request', function () { + bidRequest.params = VALID_VIDEO_BID_PARAMS; + bidRequest.mediaTypes.video = validVideoMediaTypes; + + expect(spec.isBidRequestValid(bidRequest)).to.equal(true); + }); + + it('should return false for an instream video bid request', function () { + bidRequest.params = VALID_VIDEO_BID_PARAMS; + bidRequest.mediaTypes.video = { + context: INSTREAM, + sizes: DEFAULT_BID_SIZES, + }; + + expect(spec.isBidRequestValid(bidRequest)).to.equal(false); + }); + + it('should return false for a video bid request with an unknown context', function () { + bidRequest.params = VALID_VIDEO_BID_PARAMS; + bidRequest.mediaTypes.video = { + context: 'fake', + sizes: DEFAULT_BID_SIZES, + }; + + expect(spec.isBidRequestValid(bidRequest)).to.equal(false); + }); + + it('should return false for a video bid request with no context', function () { + bidRequest.params = VALID_VIDEO_BID_PARAMS; + bidRequest.mediaTypes.video = { + sizes: DEFAULT_BID_SIZES, + }; + + expect(spec.isBidRequestValid(bidRequest)).to.equal(false); + }); + + it('should return true for a valid video bid request with a member id and inventory code', function () { + bidRequest.params = { + member: '1234', + invCode: 'ABCD', + }; + bidRequest.mediaTypes.video = validVideoMediaTypes; + + expect(spec.isBidRequestValid(bidRequest)).to.equal(true); + }); + + it('should return true for a valid video bid request with a placement id', function () { + bidRequest.params = { + placementId: '10433394', + }; + bidRequest.mediaTypes.video = validVideoMediaTypes; + + expect(spec.isBidRequestValid(bidRequest)).to.equal(true); + }); + + it('should return false for a valid video bid request but with a member id and no inventory code', function () { + bidRequest.params = { + member: '1234', + }; + bidRequest.mediaTypes.video = validVideoMediaTypes; + + expect(spec.isBidRequestValid(bidRequest)).to.equal(false); + }); + + it('should return false for a valid video bid request but with no member id and an inventory code', function () { + bidRequest.params = { + invCode: 'ABCD', + }; + bidRequest.mediaTypes.video = validVideoMediaTypes; + + expect(spec.isBidRequestValid(bidRequest)).to.equal(false); + }); + + it('should return false for a valid video bid request but with no params', function () { + bidRequest.params = {}; + bidRequest.mediaTypes.video = validVideoMediaTypes; + + expect(spec.isBidRequestValid(bidRequest)).to.equal(false); + }); + }); + + describe('with playerSize attribute', function () { + const validVideoMediaTypes = { + context: OUTSTREAM, + playerSize: DEFAULT_BID_SIZES, + minduration: 1, + maxduration: 60, + skip: 0, + skipafter: 5, + playbackmethod: [2], + protocols: [1, 2, 3] + }; + + beforeEach(function () { + bidRequest.mediaTypes.video = { + // Filled in by individual tests + }; + }); + + it('should return true for a valid video bid request', function () { + bidRequest.params = VALID_VIDEO_BID_PARAMS; + bidRequest.mediaTypes.video = validVideoMediaTypes; + + expect(spec.isBidRequestValid(bidRequest)).to.equal(true); + }); + + it('should return false for an instream video bid request', function () { + bidRequest.params = VALID_VIDEO_BID_PARAMS; + bidRequest.mediaTypes.video = { + context: INSTREAM, + playerSize: DEFAULT_BID_SIZES, + }; + + expect(spec.isBidRequestValid(bidRequest)).to.equal(false); + }); + + it('should return false for a video bid request with an unknown context', function () { + bidRequest.params = VALID_VIDEO_BID_PARAMS; + bidRequest.mediaTypes.video = { + context: 'fake', + playerSize: DEFAULT_BID_SIZES, + }; + + expect(spec.isBidRequestValid(bidRequest)).to.equal(false); + }); + + it('should return false for a video bid request with no context', function () { + bidRequest.params = VALID_VIDEO_BID_PARAMS; + bidRequest.mediaTypes.video = { + playerSize: DEFAULT_BID_SIZES, + }; + + expect(spec.isBidRequestValid(bidRequest)).to.equal(false); + }); + + it('should return true for a valid video bid request with a member id and inventory code', function () { + bidRequest.params = { + member: '1234', + invCode: 'ABCD', + }; + bidRequest.mediaTypes.video = validVideoMediaTypes; + + expect(spec.isBidRequestValid(bidRequest)).to.equal(true); + }); + + it('should return true for a valid video bid request with a placement id', function () { + bidRequest.params = { + placementId: '10433394', + }; + bidRequest.mediaTypes.video = validVideoMediaTypes; + + expect(spec.isBidRequestValid(bidRequest)).to.equal(true); + }); + + it('should return false for a valid video bid request but with a member id and no inventory code', function () { + bidRequest.params = { + member: '1234', + }; + bidRequest.mediaTypes.video = validVideoMediaTypes; + + expect(spec.isBidRequestValid(bidRequest)).to.equal(false); + }); + + it('should return false for a valid video bid request but with no member id and an inventory code', function () { + bidRequest.params = { + invCode: 'ABCD', + }; + bidRequest.mediaTypes.video = validVideoMediaTypes; + + expect(spec.isBidRequestValid(bidRequest)).to.equal(false); + }); + + it('should return false for a valid video bid request but with no params', function () { + bidRequest.params = {}; + bidRequest.mediaTypes.video = validVideoMediaTypes; + + expect(spec.isBidRequestValid(bidRequest)).to.equal(false); + }); + }); + }); + + describe('with native bid requests', function () { + beforeEach(function () { + bidRequest.mediaTypes.native = { + image: { + required: true, + // Sizes is filled in by individual tests + }, + title: { + required: true + }, + sponsoredBy: { + required: true + }, + clickUrl: { + required: true + } + }; + }); + + it('should return true for a valid native bid request with a single size', function () { + bidRequest.params = VALID_NATIVE_BID_PARAMS; + bidRequest.mediaTypes.native.image.sizes = [300, 250]; + + expect(spec.isBidRequestValid(bidRequest)).to.equal(true); + }); + + it('should return true for a valid native bid request with multiple sizes', function () { + bidRequest.params = VALID_NATIVE_BID_PARAMS; + bidRequest.mediaTypes.native.image.sizes = [[300, 250], [300, 600]]; + + expect(spec.isBidRequestValid(bidRequest)).to.equal(true); + }); + + it('should return true for a valid native bid request with a member id and inventory code', function () { + bidRequest.params = { + member: '1234', + invCode: 'ABCD', + }; + bidRequest.mediaTypes.native.image.sizes = [300, 250]; + + expect(spec.isBidRequestValid(bidRequest)).to.equal(true); + }); + + it('should return true for a valid native bid request with a placement id', function () { + bidRequest.params = { + placementId: '10433394', + }; + bidRequest.mediaTypes.native.image.sizes = [300, 250]; + + expect(spec.isBidRequestValid(bidRequest)).to.equal(true); + }); + + it('should return false for a valid native bid request but with a member id and no inventory code', function () { + bidRequest.params = { + member: '1234', + }; + bidRequest.mediaTypes.native.image.sizes = [300, 250]; + + expect(spec.isBidRequestValid(bidRequest)).to.equal(false); + }); + + it('should return false for a valid native bid request but with no member id and an inventory code', function () { + bidRequest.params = { + invCode: 'ABCD', + }; + bidRequest.mediaTypes.native.image.sizes = [300, 250]; + + expect(spec.isBidRequestValid(bidRequest)).to.equal(false); + }); + + it('should return false for a valid native bid request but with no params', function () { + bidRequest.params = {}; + bidRequest.mediaTypes.native.image.sizes = [300, 250]; + + expect(spec.isBidRequestValid(bidRequest)).to.equal(false); + }); + + it('should return false for a native bid request with no image property', function () { + bidRequest.params = VALID_NATIVE_BID_PARAMS; + delete bidRequest.mediaTypes.native.image; + + expect(spec.isBidRequestValid(bidRequest)).to.equal(false); + }); + }); + }); + + describe('buildRequests', function () { + let bidRequests; + + beforeEach(function () { + bidRequests = [bidRequest]; + + bidRequests[0].params = VALID_BANNER_BID_PARAMS; + bidRequests[0].mediaTypes.banner = { + sizes: DEFAULT_BID_SIZES, + }; + }); + + it('should use HTTP POST', function () { + const request = spec.buildRequests(bidRequests, {}); + expect(request.method).to.equal('POST'); + }); + + it('should use the correct prebid server URL', function () { + const request = spec.buildRequests(bidRequests, {}); + expect(request.url).to.equal(EXPECTED_PREBID_SERVER_URL); + }); + + it('should add the page URL to the server request', function () { + const request = spec.buildRequests(bidRequests, {}); + const payload = JSON.parse(request.data); + + expect(payload.url).to.exist; + expect(payload.url).to.be.a('string'); + }); + + it('should add GDPR consent to the server request, where present', function () { + const bidderRequest = { + bidderCode: 'vibrantmedia', + auctionId: '1d1a030790a475', + bidderRequestId: '22edbae2733bf6', + timeout: 3000, + gdprConsent: { + consentString: VALID_CONSENT_STRING, + gdprApplies: true, + }, + bids: bidRequests, + }; + + const request = spec.buildRequests(bidRequests, bidderRequest); + // TODO: Check that we should not be implementing withCredentials + // expect(request.options).to.deep.equal({withCredentials: true}); + + const payload = JSON.parse(request.data); + expect(payload.gdpr).to.exist; + expect(payload.gdpr.consentString).to.exist.and.to.equal(VALID_CONSENT_STRING); + expect(payload.gdpr.gdprApplies).to.exist.and.to.be.true; + }); + + it('should add USP consent to the server request, where present', function () { + const bidderRequest = { + bidderCode: 'vibrantmedia', + auctionId: '1d1a030790a475', + bidderRequestId: '22edbae2733bf6', + timeout: 3000, + uspConsent: { + cmpApi: 'iab', + timeout: 10000, + consentData: { + testDatum: true + } + }, + bids: bidRequests + }; + + const request = spec.buildRequests(bidRequests, bidderRequest); + // TODO: Check that we should not be implementing withCredentials + // expect(request.options).to.deep.equal({withCredentials: true}); + + const payload = JSON.parse(request.data); + expect(payload.usp).to.exist; + expect(payload.usp.cmpApi).to.exist.and.to.equal('iab'); + expect(payload.usp.timeout).to.exist.and.to.equal(10000); + expect(payload.usp.consentData).to.exist.and.to.deep.equal({ + testDatum: true + }); + }); + + it('should add GDPR and USP consent to the server request, where both present', function () { + const bidderRequest = { + bidderCode: 'vibrantmedia', + auctionId: '1d1a030790a475', + bidderRequestId: '22edbae2733bf6', + timeout: 3000, + gdprConsent: { + consentString: VALID_CONSENT_STRING, + gdprApplies: true, + }, + uspConsent: { + cmpApi: 'iab', + timeout: 10000, + consentData: { + testDatum: true + } + }, + bids: bidRequests, + }; + + const request = spec.buildRequests(bidRequests, bidderRequest); + // TODO: Check that we should not be implementing withCredentials + // expect(request.options).to.deep.equal({withCredentials: true}); + + const payload = JSON.parse(request.data); + + expect(payload.gdpr).to.exist; + expect(payload.gdpr.consentString).to.exist.and.to.equal(VALID_CONSENT_STRING); + expect(payload.gdpr.gdprApplies).to.exist.and.to.be.true; + + expect(payload.usp).to.exist; + expect(payload.usp.cmpApi).to.exist.and.to.equal('iab'); + expect(payload.usp.timeout).to.exist.and.to.equal(10000); + expect(payload.usp.consentData).to.exist.and.to.deep.equal({ + testDatum: true + }); + }); + + it('should add window dimensions to the server request', function () { + const request = spec.buildRequests(bidRequests, {}); + const payload = JSON.parse(request.data); + + expect(payload.window).to.exist; + expect(payload.window.width).to.equal(window.innerWidth); + expect(payload.window.height).to.equal(window.innerHeight); + }); + + it('should add the top-level sizes to the bid request, if present', function () { + bidRequest.params = VALID_BANNER_BID_PARAMS; + bidRequest.sizes = DEFAULT_BID_SIZES; + bidRequest.mediaTypes = { + banner: {}, + }; + + const request = spec.buildRequests(bidRequests, {}, true); + const payload = JSON.parse(request.data); + + expect(payload.biddata).to.exist; + expect(payload.biddata.length).to.equal(1); + expect(payload.biddata[0]).to.exist; + expect(payload.biddata[0].code).to.equal(bidRequest.adUnitCode); + expect(payload.biddata[0].id).to.equal(bidRequest.placementId); + expect(payload.biddata[0].bidder).to.equal(bidRequest.bidder); + expect(payload.biddata[0].sizes).to.deep.equal(DEFAULT_BID_SIZES); + expect(payload.biddata[0].mediaTypes).to.exist; + expect(payload.biddata[0].mediaTypes[BANNER]).to.exist; + expect(payload.biddata[0].mediaTypes[BANNER]).to.deep.equal({}); + }); + + it('should add the list of bids to the bid request, if present', function () { + const testBid = { + bidder: 'testBidder', + params: { + placement: '12345' + } + }; + + bidRequest.params = VALID_BANNER_BID_PARAMS; + bidRequest.bids = [testBid]; + bidRequest.mediaTypes = { + banner: {}, + }; + + // These will be present in the list of bids instead + delete bidRequest.bidId; + delete bidRequest.transactionId; + delete bidRequest.bidder; + + const request = spec.buildRequests(bidRequests, {}, true); + const payload = JSON.parse(request.data); + + expect(payload.biddata).to.exist; + expect(payload.biddata.length).to.equal(1); + expect(payload.biddata[0]).to.exist; + expect(payload.biddata[0].code).to.equal(bidRequest.adUnitCode); + expect(payload.biddata[0].id).to.equal(bidRequest.placementId); + expect(payload.biddata[0].bidder).to.equal(bidRequest.bidder); + expect(payload.biddata[0].bids.length).to.equal(1); + expect(payload.biddata[0].bids[0]).to.deep.equal(testBid); + expect(payload.biddata[0].mediaTypes).to.exist; + expect(payload.biddata[0].mediaTypes[BANNER]).to.exist; + expect(payload.biddata[0].mediaTypes[BANNER]).to.deep.equal({}); + }); + + it('should add the correct bid data to the server request for one bid request', function () { + bidRequest.params = VALID_BANNER_BID_PARAMS; + bidRequest.mediaTypes = { + banner: { + sizes: DEFAULT_BID_SIZES, + }, + }; + + const request = spec.buildRequests(bidRequests, {}, true); + const payload = JSON.parse(request.data); + + expect(payload.biddata).to.exist; + expect(payload.biddata.length).to.equal(1); + expect(payload.biddata[0]).to.exist; + expect(payload.biddata[0].code).to.equal(bidRequest.adUnitCode); + expect(payload.biddata[0].id).to.equal(bidRequest.placementId); + expect(payload.biddata[0].bidder).to.equal(bidRequest.bidder); + expect(payload.biddata[0].sizes).to.be.undefined; + expect(payload.biddata[0].mediaTypes).to.exist; + expect(payload.biddata[0].mediaTypes[BANNER]).to.exist; + expect(payload.biddata[0].mediaTypes[BANNER]).to.deep.equal({ + sizes: DEFAULT_BID_SIZES, + }); + }); + + it('should add the correct bid data to the server request for multiple bid requests', function () { + bidRequest.params = VALID_BANNER_BID_PARAMS; + bidRequest.mediaTypes = { + banner: { + sizes: DEFAULT_BID_SIZES, + }, + }; + const bid2 = { + bidder: 'vibrantmedia', + params: VALID_VIDEO_BID_PARAMS, + mediaTypes: { + video: { + context: OUTSTREAM, + sizes: DEFAULT_BID_SIZES, + } + }, + adUnitCode: 'video-div', + bidId: '30b31c1838de1f', + placementId: '135797531abcdef', + bidderRequestId: '22edbae2733bf6', + auctionId: '1d1a030790a475', + }; + const bid3 = { + bidder: 'vibrantmedia', + params: VALID_NATIVE_BID_PARAMS, + mediaTypes: { + native: { + image: { + required: true, + sizes: [300, 250] + }, + title: { + required: true + }, + sponsoredBy: { + required: true + }, + clickUrl: { + required: true + } + } + }, + adUnitCode: 'native-div', + bidId: '30b31c1838de14', + placementId: '918273645abcdef', + bidderRequestId: '22edbae2733bf6', + auctionId: '1d1a030790a475', + }; + + bidRequests.push(bid2, bid3); + + const request = spec.buildRequests(bidRequests, {}, true); + const payload = JSON.parse(request.data); + + expect(payload.biddata).to.exist; + expect(payload.biddata.length).to.equal(3); + expect(payload.biddata[0]).to.exist; + expect(payload.biddata[0].code).to.equal(bidRequest.adUnitCode); + expect(payload.biddata[0].id).to.equal(bidRequest.placementId); + expect(payload.biddata[0].bidder).to.equal(bidRequest.bidder); + expect(payload.biddata[0].mediaTypes).to.exist; + expect(payload.biddata[0].mediaTypes[BANNER]).to.exist; + expect(payload.biddata[0].mediaTypes[BANNER]).to.deep.equal({ + sizes: DEFAULT_BID_SIZES, + }); + expect(payload.biddata[1]).to.exist; + expect(payload.biddata[1].code).to.equal(bid2.adUnitCode); + expect(payload.biddata[1].id).to.equal(bid2.placementId); + expect(payload.biddata[1].bidder).to.equal(bid2.bidder); + expect(payload.biddata[1].mediaTypes).to.exist; + expect(payload.biddata[1].mediaTypes[VIDEO]).to.exist; + expect(payload.biddata[1].mediaTypes[VIDEO]).to.deep.equal({ + context: OUTSTREAM, + sizes: DEFAULT_BID_SIZES, + }); + expect(payload.biddata[2]).to.exist; + expect(payload.biddata[2].code).to.equal(bid3.adUnitCode); + expect(payload.biddata[2].id).to.equal(bid3.placementId); + expect(payload.biddata[2].bidder).to.equal(bid3.bidder); + expect(payload.biddata[2].mediaTypes[NATIVE]).to.exist; + expect(payload.biddata[2].mediaTypes[NATIVE]).to.deep.equal(bid3.mediaTypes.native); + }); + + it('should add the correct bid data to the bid request where a bid has multiple media types', function () { + bidRequest.params = VALID_VIDEO_BID_PARAMS; + bidRequest.mediaTypes = { + banner: { + sizes: DEFAULT_BID_SIZES, + }, + video: { + context: OUTSTREAM, + sizes: DEFAULT_BID_SIZES, + }, + native: { + image: { + required: true, + sizes: [300, 250] + }, + title: { + required: true + }, + sponsoredBy: { + required: true + }, + clickUrl: { + required: true + } + } + }; + bidRequest.adUnitCode = 'mixed-div'; + + const bid2 = { + bidder: 'vibrantmedia', + params: VALID_VIDEO_BID_PARAMS, + mediaTypes: { + video: { + context: OUTSTREAM, + sizes: DEFAULT_BID_SIZES, + } + }, + adUnitCode: 'video-div', + bidId: '30b31c1838de1a', + bidderRequestId: '22edbae2733bf6', + placementId: '293857832abfef', + auctionId: '1d1a030790a475', + }; + + bidRequests.push(bid2); + + const request = spec.buildRequests(bidRequests, {}, true); + const payload = JSON.parse(request.data); + + expect(payload.biddata).to.exist; + expect(payload.biddata.length).to.equal(2); + expect(payload.biddata[0]).to.exist; + expect(payload.biddata[0].code).to.equal(bidRequest.adUnitCode); + expect(payload.biddata[0].id).to.equal(bidRequest.placementId); + expect(payload.biddata[0].bidder).to.equal(bidRequest.bidder); + expect(payload.biddata[0].mediaTypes).to.exist; + expect(Object.keys(payload.biddata[0].mediaTypes).length).to.equal(3); + expect(payload.biddata[0].mediaTypes[BANNER]).to.exist; + expect(payload.biddata[0].mediaTypes[BANNER]).to.deep.equal({ + sizes: DEFAULT_BID_SIZES, + }); + expect(payload.biddata[0].mediaTypes[VIDEO]).to.exist; + expect(payload.biddata[0].mediaTypes[VIDEO]).to.deep.equal({ + context: OUTSTREAM, + sizes: DEFAULT_BID_SIZES, + }); + expect(payload.biddata[0].code).to.equal(bidRequest.adUnitCode); + expect(payload.biddata[0].id).to.equal(bidRequest.placementId); + expect(payload.biddata[0].bidder).to.equal(bidRequest.bidder); + expect(payload.biddata[0].mediaTypes[NATIVE]).to.exist; + expect(payload.biddata[0].mediaTypes[NATIVE]).to.deep.equal(bidRequest.mediaTypes.native); + expect(payload.biddata[1]).to.exist; + expect(payload.biddata[1].code).to.equal(bid2.adUnitCode); + expect(payload.biddata[1].id).to.equal(bid2.placementId); + expect(payload.biddata[1].bidder).to.equal(bid2.bidder); + expect(payload.biddata[1].mediaTypes).to.exist; + expect(Object.keys(payload.biddata[1].mediaTypes).length).to.equal(1); + expect(payload.biddata[1].mediaTypes[VIDEO]).to.exist; + expect(payload.biddata[1].mediaTypes[VIDEO]).to.deep.equal({ + context: OUTSTREAM, + sizes: DEFAULT_BID_SIZES, + }); + }); + }); + + describe('interpretResponse', function () { + it('returns a valid Prebid API response object for a banner Prebid Server response', function () { + const prebidServerResponse = { + body: [{ + mediaType: 'banner', + requestId: '12345', + cpm: 1, + currency: 'USD', + width: 640, + height: 240, + ad: BANNER_AD, + ttl: 300, + creativeId: '86f4aef9-2f17-421d-84db-5c9814bf4a79', + netRevenue: false, + meta: Object.freeze({ + advertiser: '105600', + width: 300, + height: 250, + isCustom: '1', + progressBar: false, + mpuSrc: '//images.intellitxt.com/a/105600/Genpact/genpact.jpg', + clickURL: '{{click}}' + }) + }] + }; + + const interpretedResponse = spec.interpretResponse(prebidServerResponse, {}); + + expect(interpretedResponse).to.be.a('array'); + expect(interpretedResponse.length).to.equal(1); + + const interpretedBid = interpretedResponse[0]; + + expect(interpretedBid.mediaType).to.equal('banner'); + expect(interpretedBid.requestId).to.equal('12345'); + expect(interpretedBid.cpm).to.equal(1); + expect(interpretedBid.currency).to.equal('USD'); + expect(interpretedBid.width).to.equal(640); + expect(interpretedBid.height).to.equal(240); + expect(interpretedBid.ad).to.equal(BANNER_AD); + expect(interpretedBid.ttl).to.equal(300); + expect(interpretedBid.creativeId).to.equal('86f4aef9-2f17-421d-84db-5c9814bf4a79'); + expect(interpretedBid.netRevenue).to.be.false; + expect(interpretedBid.meta).to.deep.equal(prebidServerResponse.body[0].meta); + expect(interpretedBid.renderer).to.be.undefined; + expect(interpretedBid.adResponse).to.deep.equal(prebidServerResponse); + }); + + it('returns a valid Prebid API response object for a video Prebid Server response', function () { + const prebidServerResponse = Object.freeze({ + body: [{ + mediaType: 'video', + requestId: '67890', + cpm: 2, + currency: 'USD', + width: 600, + height: 300, + ad: VIDEO_AD, + ttl: 300, + creativeId: '248e8e0c-2f17-421d-84db-5c9814bf4a79', + netRevenue: false, + meta: { + advertiser: '105600', + width: 300, + height: 250, + isCustom: '1', + progressBar: false, + mpuSrc: '//images.intellitxt.com/a/105600/Genpact/genpact.jpg', + clickURL: '{{click}}' + }, + vastUrl: 'https://www.example.com/myVastVideo' + }] + }); + + const interpretedResponse = spec.interpretResponse(prebidServerResponse, {}); + + expect(interpretedResponse).to.be.a('array'); + expect(interpretedResponse.length).to.equal(1); + + const interpretedBid = interpretedResponse[0]; + + expect(interpretedBid.mediaType).to.equal('video'); + expect(interpretedBid.requestId).to.equal('67890'); + expect(interpretedBid.cpm).to.equal(2); + expect(interpretedBid.currency).to.equal('USD'); + expect(interpretedBid.width).to.equal(600); + expect(interpretedBid.height).to.equal(300); + expect(interpretedBid.ad).to.equal(VIDEO_AD); + expect(interpretedBid.ttl).to.equal(300); + expect(interpretedBid.creativeId).to.equal('248e8e0c-2f17-421d-84db-5c9814bf4a79'); + expect(interpretedBid.netRevenue).to.be.false; + expect(interpretedBid.meta).to.deep.equal(prebidServerResponse.body[0].meta); + expect(interpretedBid.renderer).to.be.undefined; + expect(interpretedBid.adResponse).to.deep.equal(prebidServerResponse); + }); + + it('returns a valid Prebid API response object for a native Prebid Server response', function () { + const prebidServerResponse = Object.freeze({ + body: [{ + mediaType: 'native', + requestId: '13579', + cpm: 3, + currency: 'USD', + width: 240, + height: 300, + ad: 'https://www.example.com/native-display.html', + ttl: 300, + creativeId: 'd28e8e0c-2f17-421d-84db-5c9814bf4a81', + netRevenue: false, + meta: {}, + title: 'Test native ad bid for 13579', + sponsoredBy: 'Vibrant Media Ltd', + clickUrl: 'https://www.example.com/native-ct.html', + image: { + url: 'https://www.example.com/native-display.html', + width: 240, + height: 300 + } + }] + }); + + const interpretedResponse = spec.interpretResponse(prebidServerResponse, {}); + + expect(interpretedResponse).to.be.a('array'); + expect(interpretedResponse.length).to.equal(1); + + const interpretedBid = interpretedResponse[0]; + + expect(interpretedBid.mediaType).to.equal('native'); + expect(interpretedBid.requestId).to.equal('13579'); + expect(interpretedBid.cpm).to.equal(3); + expect(interpretedBid.currency).to.equal('USD'); + expect(interpretedBid.width).to.equal(240); + expect(interpretedBid.height).to.equal(300); + expect(interpretedBid.ad).to.equal('https://www.example.com/native-display.html'); + expect(interpretedBid.ttl).to.equal(300); + expect(interpretedBid.creativeId).to.equal('d28e8e0c-2f17-421d-84db-5c9814bf4a81'); + expect(interpretedBid.netRevenue).to.be.false; + expect(interpretedBid.meta).to.deep.equal(prebidServerResponse.body[0].meta); + expect(interpretedBid.renderer).to.be.undefined; + expect(interpretedBid.adResponse).to.deep.equal(prebidServerResponse); + }); + + it('returns a valid Prebid API response object for a multi-bid Prebid Server response', function () { + const prebidServerResponse = Object.freeze({ + body: [ + { + mediaType: 'banner', + requestId: '12345', + cpm: 3, + currency: 'USD', + width: 640, + height: 240, + ad: BANNER_AD, + ttl: 300, + creativeId: '86f4aef9-2f17-421d-84db-5c9814bf4a79', + netRevenue: false, + meta: { + advertiser: '105600', + width: 300, + height: 250, + isCustom: '1', + progressBar: false, + mpuSrc: '//images.intellitxt.com/a/105600/Genpact/genpact.jpg', + clickURL: '{{click}}' + } + }, + { + mediaType: 'video', + requestId: '67890', + cpm: 4, + currency: 'USD', + width: 300, + height: 300, + ad: VIDEO_AD, + ttl: 300, + creativeId: 'd28e8e0c-2f17-421d-84db-5c9814bf4a79', + netRevenue: false, + meta: { + advertiser: '105600', + width: 300, + height: 250, + isCustom: '1', + progressBar: false, + mpuSrc: '//images.intellitxt.com/a/105600/Genpact/genpact.jpg', + clickURL: '{{click}}' + }, + vastUrl: 'https://www.example.com/myVastVideo' + }, + { + mediaType: 'native', + requestId: '13579', + cpm: 5, + currency: 'USD', + width: 640, + height: 240, + ad: 'https://www.example.com/native-display.html', + ttl: 300, + creativeId: '888e8e0c-2f17-421d-84db-5c9814bf4a81', + netRevenue: false, + meta: {}, + title: 'Test native ad bid for 13579', + sponsoredBy: 'Vibrant Media Ltd', + clickUrl: 'https://www.example.com/native-ct.html', + image: { + url: 'https://www.example.com/native-display.html', + width: 640, + height: 240 + } + } + ] + }); + + const interpretedResponse = spec.interpretResponse(prebidServerResponse, {}); + + expect(interpretedResponse).to.be.a('array'); + expect(interpretedResponse.length).to.equal(3); + + const interpretedBannerBid = interpretedResponse[0]; + + expect(interpretedBannerBid.mediaType).to.equal('banner'); + expect(interpretedBannerBid.requestId).to.equal('12345'); + expect(interpretedBannerBid.cpm).to.equal(3); + expect(interpretedBannerBid.currency).to.equal('USD'); + expect(interpretedBannerBid.width).to.equal(640); + expect(interpretedBannerBid.height).to.equal(240); + expect(interpretedBannerBid.ad).to.equal(BANNER_AD); + expect(interpretedBannerBid.ttl).to.equal(300); + expect(interpretedBannerBid.creativeId).to.equal('86f4aef9-2f17-421d-84db-5c9814bf4a79'); + expect(interpretedBannerBid.netRevenue).to.be.false; + expect(interpretedBannerBid.meta).to.deep.equal(prebidServerResponse.body[0].meta); + expect(interpretedBannerBid.renderer).to.be.undefined; + expect(interpretedBannerBid.adResponse).to.deep.equal(prebidServerResponse); + + const interpretedVideoBid = interpretedResponse[1]; + + expect(interpretedVideoBid.mediaType).to.equal('video'); + expect(interpretedVideoBid.requestId).to.equal('67890'); + expect(interpretedVideoBid.cpm).to.equal(4); + expect(interpretedVideoBid.currency).to.equal('USD'); + expect(interpretedVideoBid.width).to.equal(300); + expect(interpretedVideoBid.height).to.equal(300); + expect(interpretedVideoBid.ad).to.equal(VIDEO_AD); + expect(interpretedVideoBid.ttl).to.equal(300); + expect(interpretedVideoBid.creativeId).to.equal('d28e8e0c-2f17-421d-84db-5c9814bf4a79'); + expect(interpretedVideoBid.netRevenue).to.be.false; + expect(interpretedVideoBid.meta).to.deep.equal(prebidServerResponse.body[1].meta); + expect(interpretedVideoBid.renderer).to.be.undefined; + expect(interpretedVideoBid.adResponse).to.deep.equal(prebidServerResponse); + + const interpretedNativeBid = interpretedResponse[2]; + + expect(interpretedNativeBid.mediaType).to.equal('native'); + expect(interpretedNativeBid.requestId).to.equal('13579'); + expect(interpretedNativeBid.cpm).to.equal(5); + expect(interpretedNativeBid.currency).to.equal('USD'); + expect(interpretedNativeBid.width).to.equal(640); + expect(interpretedNativeBid.height).to.equal(240); + expect(interpretedNativeBid.ad).to.equal('https://www.example.com/native-display.html'); + expect(interpretedNativeBid.ttl).to.equal(300); + expect(interpretedNativeBid.creativeId).to.equal('888e8e0c-2f17-421d-84db-5c9814bf4a81'); + expect(interpretedNativeBid.netRevenue).to.be.false; + expect(interpretedNativeBid.meta).to.deep.equal(prebidServerResponse.body[2].meta); + expect(interpretedNativeBid.renderer).to.be.undefined; + expect(interpretedNativeBid.adResponse).to.deep.equal(prebidServerResponse); + }); + }); + + describe('Flow tests', function () { + describe('For successive API calls to the public functions', function () { + it('should succeed with one media type per bid', function () { + const transformedBannerBidParams = spec.transformBidParams(VALID_BANNER_BID_PARAMS); + const transformedVideoBidParams = spec.transformBidParams(VALID_VIDEO_BID_PARAMS); + const transformedNativeBidParams = spec.transformBidParams(VALID_NATIVE_BID_PARAMS); + + const bannerBid = { + bidder: 'vibrantmedia', + params: transformedBannerBidParams, + mediaTypes: { + banner: { + sizes: DEFAULT_BID_SIZES, + }, + }, + adUnitCode: 'banner-div', + bidId: '30b31c1838de11', + bidderRequestId: '22edbae2733bf6', + placementId: '293857832abfef', + auctionId: '1d1a030790a475', + }; + const videoBid = { + bidder: 'vibrantmedia', + params: transformedVideoBidParams, + mediaTypes: { + video: { + context: OUTSTREAM, + sizes: DEFAULT_BID_SIZES, + }, + }, + adUnitCode: 'video-div', + bidId: '30b31c1838de15', + bidderRequestId: '22edbae2733bf6', + placementId: '293857832abfef', + auctionId: '1d1a030790a475', + }; + const nativeBid = { + bidder: 'vibrantmedia', + params: transformedNativeBidParams, + mediaTypes: { + native: { + image: { + required: true, + sizes: [300, 250] + }, + title: { + required: true + }, + sponsoredBy: { + required: true + }, + clickUrl: { + required: true + } + } + }, + adUnitCode: 'native-div', + bidId: '30b31c1838de12', + bidderRequestId: '22edbae2733bf6', + placementId: '293857832abfef', + auctionId: '1d1a030790a475', + }; + + expect(spec.isBidRequestValid(bannerBid)).to.be.true; + expect(spec.isBidRequestValid(videoBid)).to.be.true; + expect(spec.isBidRequestValid(nativeBid)).to.be.true; + + const bidRequests = [bannerBid, videoBid, nativeBid]; + const validBidderRequest = getValidBidderRequest(bidRequests); + const serverRequest = spec.buildRequests(bidRequests, validBidderRequest); + expect(serverRequest.method).to.equal('POST'); + expect(serverRequest.url).to.equal(EXPECTED_PREBID_SERVER_URL); + + const payload = JSON.parse(serverRequest.data); + expect(payload.biddata).to.exist; + expect(payload.biddata.length).to.equal(3); + expect(payload.biddata[0]).to.exist; + expect(payload.biddata[0].code).to.equal(bannerBid.adUnitCode); + expect(payload.biddata[0].id).to.equal(bannerBid.placementId); + expect(payload.biddata[0].bidder).to.equal(bannerBid.bidder); + expect(payload.biddata[0].mediaTypes[BANNER]).to.exist; + expect(payload.biddata[0].mediaTypes[BANNER]).to.deep.equal({ + sizes: DEFAULT_BID_SIZES, + }); + expect(payload.biddata[1]).to.exist; + expect(payload.biddata[1].code).to.equal(videoBid.adUnitCode); + expect(payload.biddata[1].id).to.equal(videoBid.placementId); + expect(payload.biddata[1].bidder).to.equal(videoBid.bidder); + expect(payload.biddata[1].mediaTypes[VIDEO]).to.exist; + expect(payload.biddata[1].mediaTypes[VIDEO]).to.deep.equal({ + context: OUTSTREAM, + sizes: DEFAULT_BID_SIZES + }); + expect(payload.biddata[2]).to.exist; + expect(payload.biddata[2].code).to.equal(nativeBid.adUnitCode); + expect(payload.biddata[2].id).to.equal(nativeBid.placementId); + expect(payload.biddata[2].bidder).to.equal(nativeBid.bidder); + expect(payload.biddata[2].mediaTypes[NATIVE]).to.exist; + expect(payload.biddata[2].mediaTypes[NATIVE]).to.deep.equal(nativeBid.mediaTypes.native); + + // From here, the API would call the Prebid Server and call interpretResponse, which is covered by tests elsewhere + }); + + it('should succeed with multiple media types for a single bid', function () { + const bidParams = spec.transformBidParams(VALID_VIDEO_BID_PARAMS); + const bid = { + bidder: 'vibrantmedia', + params: bidParams, + mediaTypes: { + banner: { + sizes: DEFAULT_BID_SIZES + }, + video: { + context: OUTSTREAM, + sizes: DEFAULT_BID_SIZES + }, + native: { + sizes: DEFAULT_BID_SIZES + } + }, + adUnitCode: 'test-div', + bidId: '30b31c1838de13', + bidderRequestId: '22edbae2733bf6', + placementId: '293857832abfef', + auctionId: '1d1a030790a475', + }; + + expect(spec.isBidRequestValid(bid)).to.be.true; + + const bidRequests = [bid]; + const validBidderRequest = getValidBidderRequest(bidRequests); + const serverRequest = spec.buildRequests(bidRequests, validBidderRequest); + expect(serverRequest.method).to.equal('POST'); + expect(serverRequest.url).to.equal(EXPECTED_PREBID_SERVER_URL); + + const payload = JSON.parse(serverRequest.data); + expect(payload.biddata).to.exist; + expect(payload.biddata.length).to.equal(1); + expect(payload.biddata[0]).to.exist; + expect(payload.biddata[0].code).to.equal(bid.adUnitCode); + expect(payload.biddata[0].id).to.equal(bid.placementId); + expect(payload.biddata[0].bidder).to.equal(bid.bidder); + expect(payload.biddata[0].mediaTypes[BANNER]).to.exist; + expect(payload.biddata[0].mediaTypes[BANNER]).to.deep.equal({ + sizes: DEFAULT_BID_SIZES, + }); + expect(payload.biddata[0].mediaTypes[VIDEO]).to.exist; + expect(payload.biddata[0].mediaTypes[VIDEO]).to.deep.equal({ + context: OUTSTREAM, + sizes: DEFAULT_BID_SIZES, + }); + expect(payload.biddata[0].mediaTypes[NATIVE]).to.exist; + expect(payload.biddata[0].mediaTypes[NATIVE]).to.deep.equal({ + sizes: DEFAULT_BID_SIZES, + }); + + // From here, the API would call the Prebid Server and call interpretResponse, which is covered by tests elsewhere + }); + }); + }); +}); diff --git a/test/spec/modules/vidazooBidAdapter_spec.js b/test/spec/modules/vidazooBidAdapter_spec.js index 35f510fd6ee..0b5dadce09f 100644 --- a/test/spec/modules/vidazooBidAdapter_spec.js +++ b/test/spec/modules/vidazooBidAdapter_spec.js @@ -37,7 +37,8 @@ const BID = { 'transactionId': 'c881914b-a3b5-4ecf-ad9c-1c2f37c6aabf', 'sizes': [[300, 250], [300, 600]], 'bidderRequestId': '1fdb5ff1b6eaa7', - 'requestId': 'b0777d85-d061-450e-9bc7-260dd54bbb7a' + 'requestId': 'b0777d85-d061-450e-9bc7-260dd54bbb7a', + 'schain': 'a0819c69-005b-41ed-af06-1be1e0aefefc' }; const BIDDER_REQUEST = { @@ -163,6 +164,7 @@ describe('VidazooBidAdapter', function () { uniqueDealId: `${hashUrl}_${Date.now().toString()}`, bidderVersion: adapter.version, prebidVersion: version, + schain: BID.schain, res: `${window.top.screen.width}x${window.top.screen.height}`, 'ext.param1': 'loremipsum', 'ext.param2': 'dolorsitamet', diff --git a/test/spec/modules/vidoomyBidAdapter_spec.js b/test/spec/modules/vidoomyBidAdapter_spec.js index 52f522bdcfc..8aa127faef2 100644 --- a/test/spec/modules/vidoomyBidAdapter_spec.js +++ b/test/spec/modules/vidoomyBidAdapter_spec.js @@ -16,7 +16,8 @@ describe('vidoomyBidAdapter', function() { 'bidder': 'vidoomy', 'params': { pid: '123123', - id: '123123' + id: '123123', + bidfloor: 0.5 }, 'adUnitCode': 'code', 'sizes': [[300, 250]] @@ -32,6 +33,11 @@ describe('vidoomyBidAdapter', function() { expect(spec.isBidRequestValid(bid)).to.equal(false); }); + it('should return false when bidfloor is invalid', function () { + bid.params.bidfloor = 'not a number'; + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + it('should return false when id is empty', function () { bid.params.id = ''; expect(spec.isBidRequestValid(bid)).to.equal(false); diff --git a/test/spec/modules/viewability_spec.js b/test/spec/modules/viewability_spec.js new file mode 100644 index 00000000000..ab2753daf53 --- /dev/null +++ b/test/spec/modules/viewability_spec.js @@ -0,0 +1,280 @@ +import { expect } from 'chai'; +import * as sinon from 'sinon'; +import * as utils from 'src/utils.js'; +import * as viewability from 'modules/viewability.js'; + +describe('viewability test', () => { + describe('start measurement', () => { + let sandbox; + let intersectionObserverStub; + let setTimeoutStub; + let observeCalled; + let unobserveCalled; + let ti = 1; + beforeEach(() => { + observeCalled = false; + unobserveCalled = false; + sandbox = sinon.sandbox.create(); + + let fakeIntersectionObserver = (stateChange, options) => { + return { + observe: (element) => { + observeCalled = true; + stateChange([{ isIntersecting: true }]); + }, + unobserve: (element) => { + unobserveCalled = true; + }, + }; + }; + + intersectionObserverStub = sandbox.stub(window, 'IntersectionObserver').callsFake(fakeIntersectionObserver); + setTimeoutStub = sandbox.stub(window, 'setTimeout').callsFake((callback, timeout) => { + callback(); + return ti++; + }); + }); + + afterEach(() => { + sandbox.restore(); + }); + + it('should trigger appropriate callbacks', () => { + viewability.startMeasurement('0', {}, { method: 'img', value: 'http://my.tracker/123' }, { inViewThreshold: 0.5, timeInView: 1000 }); + + sinon.assert.called(intersectionObserverStub); + sinon.assert.called(setTimeoutStub); + expect(observeCalled).to.equal(true); + expect(unobserveCalled).to.equal(true); + }); + + it('should trigger img tracker', () => { + let triggerPixelSpy = sandbox.spy(utils, ['triggerPixel']); + viewability.startMeasurement('1', {}, { method: 'img', value: 'http://my.tracker/123' }, { inViewThreshold: 0.5, timeInView: 1000 }); + expect(triggerPixelSpy.callCount).to.equal(1); + }); + + it('should trigger js tracker', () => { + let insertHtmlIntoIframeSpy = sandbox.spy(utils, ['insertHtmlIntoIframe']); + viewability.startMeasurement('2', {}, { method: 'js', value: 'http://my.tracker/123.js' }, { inViewThreshold: 0.5, timeInView: 1000 }); + expect(insertHtmlIntoIframeSpy.callCount).to.equal(1); + }); + + it('should trigger callback tracker', () => { + let callbackFired = false; + viewability.startMeasurement('3', {}, { method: 'callback', value: () => { callbackFired = true; } }, { inViewThreshold: 0.5, timeInView: 1000 }); + expect(callbackFired).to.equal(true); + }); + + it('should check for vid uniqueness', () => { + let logWarnSpy = sandbox.spy(utils, 'logWarn'); + viewability.startMeasurement('4', {}, { method: 'js', value: 'http://my.tracker/123.js' }, { inViewThreshold: 0.5, timeInView: 1000 }); + expect(logWarnSpy.callCount).to.equal(0); + + viewability.startMeasurement('4', {}, { method: 'js', value: 'http://my.tracker/123.js' }, { inViewThreshold: 0.5, timeInView: 1000 }); + expect(logWarnSpy.callCount).to.equal(1); + expect(logWarnSpy.calledWith(`${viewability.MODULE_NAME}: must provide an unregistered vid`, '4')).to.equal(true); + }); + + it('should check for valid criteria', () => { + let logWarnSpy = sandbox.spy(utils, 'logWarn'); + viewability.startMeasurement('5', {}, { method: 'js', value: 'http://my.tracker/123.js' }, { timeInView: 1000 }); + expect(logWarnSpy.callCount).to.equal(1); + expect(logWarnSpy.calledWith(`${viewability.MODULE_NAME}: missing criteria`, { timeInView: 1000 })).to.equal(true); + }); + + it('should check for valid tracker', () => { + let logWarnSpy = sandbox.spy(utils, 'logWarn'); + viewability.startMeasurement('6', {}, { method: 'callback', value: 'string' }, { inViewThreshold: 0.5, timeInView: 1000 }); + expect(logWarnSpy.callCount).to.equal(1); + expect(logWarnSpy.calledWith(`${viewability.MODULE_NAME}: invalid tracker`, { method: 'callback', value: 'string' })).to.equal(true); + }); + + it('should check if element provided', () => { + let logWarnSpy = sandbox.spy(utils, 'logWarn'); + viewability.startMeasurement('7', undefined, { method: 'js', value: 'http://my.tracker/123.js' }, { timeInView: 1000 }); + expect(logWarnSpy.callCount).to.equal(1); + expect(logWarnSpy.calledWith(`${viewability.MODULE_NAME}: no html element provided`)).to.equal(true); + }); + }); + + describe('stop measurement', () => { + let sandbox; + let intersectionObserverStub; + let setTimeoutStub; + let clearTimeoutStub; + let observeCalled; + let unobserveCalled; + let stateChangeBackup; + let ti = 1; + beforeEach(() => { + observeCalled = false; + unobserveCalled = false; + sandbox = sinon.sandbox.create(); + + let fakeIntersectionObserver = (stateChange, options) => { + return { + observe: (element) => { + stateChangeBackup = stateChange; + observeCalled = true; + stateChange([{ isIntersecting: true }]); + }, + unobserve: (element) => { + unobserveCalled = true; + }, + }; + }; + + intersectionObserverStub = sandbox.stub(window, 'IntersectionObserver').callsFake(fakeIntersectionObserver); + setTimeoutStub = sandbox.stub(window, 'setTimeout').callsFake((callback, timeout) => { + // skipping the callback + return ti++; + }); + clearTimeoutStub = sandbox.stub(window, 'clearTimeout').callsFake((timeoutId) => { }); + }); + + afterEach(() => { + sandbox.restore(); + }); + + it('should clear the timeout', () => { + viewability.startMeasurement('10', {}, { method: 'img', value: 'http://my.tracker/123' }, { inViewThreshold: 0.5, timeInView: 1000 }); + stateChangeBackup([{ isIntersecting: false }]); + sinon.assert.called(intersectionObserverStub); + sinon.assert.called(setTimeoutStub); + sinon.assert.called(clearTimeoutStub); + expect(observeCalled).to.equal(true); + }); + + it('should unobserve', () => { + viewability.startMeasurement('11', {}, { method: 'img', value: 'http://my.tracker/123' }, { inViewThreshold: 0.5, timeInView: 1000 }); + sinon.assert.called(intersectionObserverStub); + sinon.assert.called(setTimeoutStub); + expect(observeCalled).to.equal(true); + expect(unobserveCalled).to.equal(false); + + viewability.stopMeasurement('11'); + expect(unobserveCalled).to.equal(true); + sinon.assert.called(clearTimeoutStub); + }); + + it('should check for vid existence', () => { + let logWarnSpy = sandbox.spy(utils, 'logWarn'); + viewability.stopMeasurement('100'); + expect(logWarnSpy.callCount).to.equal(1); + expect(logWarnSpy.calledWith(`${viewability.MODULE_NAME}: must provide a registered vid`, '100')).to.equal(true); + }); + }); + + describe('handle creative messages', () => { + let sandbox; + let intersectionObserverStub; + let setTimeoutStub; + let observeCalled; + let unobserveCalled; + let ti = 1; + let getElementsByTagStub; + let getElementByIdStub; + + let fakeContentWindow = {}; + beforeEach(() => { + observeCalled = false; + unobserveCalled = false; + sandbox = sinon.sandbox.create(); + + let fakeIntersectionObserver = (stateChange, options) => { + return { + observe: (element) => { + observeCalled = true; + stateChange([{ isIntersecting: true }]); + }, + unobserve: (element) => { + unobserveCalled = true; + }, + }; + }; + + intersectionObserverStub = sandbox.stub(window, 'IntersectionObserver').callsFake(fakeIntersectionObserver); + setTimeoutStub = sandbox.stub(window, 'setTimeout').callsFake((callback, timeout) => { + callback(); + return ti++; + }); + + getElementsByTagStub = sandbox.stub(document, 'getElementsByTagName').callsFake((tagName) => { + return [{ + contentWindow: fakeContentWindow, + }]; + }); + getElementByIdStub = sandbox.stub(document, 'getElementById').callsFake((id) => { + return {}; + }); + }); + + afterEach(() => { + sandbox.restore(); + }); + + it('should find element by contentWindow', () => { + let viewabilityRecord = { + vid: 1000, + tracker: { + value: 'http://my.tracker/123', + method: 'img', + }, + criteria: { inViewThreshold: 0.5, timeInView: 1000 }, + message: 'Prebid Viewability', + action: 'startMeasurement', + }; + let data = JSON.stringify(viewabilityRecord); + + viewability.receiveMessage({ + data: data, + source: fakeContentWindow, + }); + + sinon.assert.called(getElementsByTagStub); + sinon.assert.called(intersectionObserverStub); + sinon.assert.called(setTimeoutStub); + expect(observeCalled).to.equal(true); + expect(unobserveCalled).to.equal(true); + }); + + it('should find element by id', () => { + let viewabilityRecord = { + vid: 1001, + tracker: { + value: 'http://my.tracker/123', + method: 'img', + }, + criteria: { inViewThreshold: 0.5, timeInView: 1000 }, + message: 'Prebid Viewability', + action: 'startMeasurement', + elementId: '1', + }; + let data = JSON.stringify(viewabilityRecord); + viewability.receiveMessage({ + data: data, + }); + + sinon.assert.called(getElementByIdStub); + sinon.assert.called(intersectionObserverStub); + sinon.assert.called(setTimeoutStub); + expect(observeCalled).to.equal(true); + expect(unobserveCalled).to.equal(true); + }); + + it('should stop measurement', () => { + let viewabilityRecord = { + vid: 1001, + message: 'Prebid Viewability', + action: 'stopMeasurement', + }; + let data = JSON.stringify(viewabilityRecord); + viewability.receiveMessage({ + data: data, + }); + + expect(unobserveCalled).to.equal(true); + }); + }); +}); diff --git a/test/spec/modules/visxBidAdapter_spec.js b/test/spec/modules/visxBidAdapter_spec.js index d29eb6c25dc..4aaaf996f58 100755 --- a/test/spec/modules/visxBidAdapter_spec.js +++ b/test/spec/modules/visxBidAdapter_spec.js @@ -18,7 +18,7 @@ describe('VisxAdapter', function () { let bid = { 'bidder': 'visx', 'params': { - 'uid': '903536' + 'uid': 903536 }, 'adUnitCode': 'adunit-code', 'sizes': [[300, 250], [300, 600]], @@ -40,6 +40,15 @@ describe('VisxAdapter', function () { expect(spec.isBidRequestValid(bid)).to.equal(false); }); + it('should return false when uid can not be parsed as number', function () { + let bid = Object.assign({}, bid); + delete bid.params; + bid.params = { + 'uid': 'sdvsdv' + }; + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + it('it should fail on invalid video bid', function () { let videoBid = Object.assign({}, bid); videoBid.mediaTypes = { @@ -102,7 +111,7 @@ describe('VisxAdapter', function () { { 'bidder': 'visx', 'params': { - 'uid': 903535 + 'uid': '903535' }, 'adUnitCode': 'adunit-code-2', 'sizes': [[728, 90], [300, 250]], @@ -1263,9 +1272,10 @@ describe('VisxAdapter', function () { }); it('onTimeout', function () { - const data = { timeout: 3000, bidId: '23423', params: { uid: 1 } }; + const data = [{ timeout: 3000, adUnitCode: 'adunit-code-1', auctionId: '1cbd2feafe5e8b', bidder: 'visx', bidId: '23423', params: [{ uid: '1' }] }]; + const expectedData = [{ ...data[0], params: [{ uid: 1 }] }]; spec.onTimeout(data); - expect(utils.triggerPixel.calledOnceWith('https://t.visx.net/track/bid_timeout//' + JSON.stringify(data))).to.equal(true); + expect(utils.triggerPixel.calledOnceWith('https://t.visx.net/track/bid_timeout//' + JSON.stringify(expectedData))).to.equal(true); }); }); diff --git a/test/spec/modules/weboramaRtdProvider_spec.js b/test/spec/modules/weboramaRtdProvider_spec.js index 081a6f06876..0f0af4efe2f 100644 --- a/test/spec/modules/weboramaRtdProvider_spec.js +++ b/test/spec/modules/weboramaRtdProvider_spec.js @@ -8,14 +8,21 @@ import { storage, DEFAULT_LOCAL_STORAGE_USER_PROFILE_KEY } from '../../../modules/weboramaRtdProvider.js'; +import { + config +} from 'src/config.js'; +import { + getGlobal +} from 'src/prebidGlobal.js'; +import 'src/prebid.js'; const responseHeader = { 'Content-Type': 'application/json' }; -describe('weboramaRtdProvider', function() { - describe('weboramaSubmodule', function() { - it('successfully instantiates and call contextual api', function() { +describe('weboramaRtdProvider', function () { + describe('weboramaSubmodule', function () { + it('successfully instantiates and call contextual api', function () { const moduleConfig = { params: { weboCtxConf: { @@ -28,7 +35,7 @@ describe('weboramaRtdProvider', function() { expect(weboramaSubmodule.init(moduleConfig)).to.equal(true); }); - it('instantiate without contextual token should fail', function() { + it('instantiate without contextual token should fail', function () { const moduleConfig = { params: { weboCtxConf: {} @@ -37,7 +44,7 @@ describe('weboramaRtdProvider', function() { expect(weboramaSubmodule.init(moduleConfig)).to.equal(false); }); - it('instantiate with empty weboUserData conf should return true', function() { + it('instantiate with empty weboUserData conf should return true', function () { const moduleConfig = { params: { weboUserDataConf: {} @@ -47,26 +54,37 @@ describe('weboramaRtdProvider', function() { }); }); - describe('Handle Set Targeting', function() { + describe('Handle Set Targeting', function () { let sandbox; - beforeEach(function() { + beforeEach(function () { sandbox = sinon.sandbox.create(); storage.removeDataFromLocalStorage('webo_wam2gam_entry'); + + getGlobal().setConfig({ + ortb2: undefined + }); }); - afterEach(function() { + afterEach(function () { sandbox.restore(); }); - describe('Add Contextual Data', function() { - it('should set gam targeting and send to bidders by default', function() { + describe('Add Contextual Data', function () { + it('should set gam targeting and send to bidders by default', function () { + let onDataResponse = {}; const moduleConfig = { params: { weboCtxConf: { token: 'foo', targetURL: 'https://prebid.org', + onData: (data, site) => { + onDataResponse = { + data: data, + site: site, + }; + }, } } }; @@ -79,9 +97,18 @@ describe('weboramaRtdProvider', function() { adUnits: [{ bids: [{ bidder: 'smartadserver' + }, { + bidder: 'pubmatic' + }, { + bidder: 'appnexus' + }, { + bidder: 'rubicon' + }, { + bidder: 'other' }] }] }; + const onDoneSpy = sinon.spy(); expect(weboramaSubmodule.init(moduleConfig)).to.be.true; @@ -104,10 +131,34 @@ describe('weboramaRtdProvider', function() { 'adunit2': data, }); + expect(reqBidsConfigObj.adUnits[0].bids.length).to.equal(5); expect(reqBidsConfigObj.adUnits[0].bids[0].params.target).to.equal('webo_ctx=foo;webo_ctx=bar;webo_ds=baz'); + expect(reqBidsConfigObj.adUnits[0].bids[1].params.dctr).to.equal('webo_ctx=foo,bar|webo_ds=baz'); + expect(reqBidsConfigObj.adUnits[0].bids[2].params.keywords).to.deep.equal(data); + expect(reqBidsConfigObj.adUnits[0].bids[3].params).to.deep.equal({ + inventory: data + }); + expect(reqBidsConfigObj.adUnits[0].bids[4].ortb2).to.deep.equal({ + site: { + ext: { + data: data + }, + } + }); + expect(getGlobal().getConfig('ortb2')).to.deep.equal({ + site: { + ext: { + data: data + }, + } + }); + expect(onDataResponse).to.deep.equal({ + data: data, + site: true, + }); }); - it('should set gam targeting but not send to bidders with setPrebidTargeting=true/sendToBidders=false', function() { + it('should set gam targeting but not send to bidders with setPrebidTargeting=true/sendToBidders=false', function () { const moduleConfig = { params: { weboCtxConf: { @@ -130,6 +181,30 @@ describe('weboramaRtdProvider', function() { params: { target: 'foo=bar' } + }, { + bidder: 'pubmatic', + params: { + dctr: 'foo=bar' + } + }, { + bidder: 'appnexus', + params: { + keywords: { + foo: ['bar'] + } + } + }, { + bidder: 'rubicon', + params: { + inventory: { + foo: 'bar', + }, + visitor: { + baz: 'bam', + } + } + }, { + bidder: 'other', }] }] }; @@ -155,10 +230,90 @@ describe('weboramaRtdProvider', function() { 'adunit2': data, }); + expect(reqBidsConfigObj.adUnits[0].bids.length).to.equal(5); expect(reqBidsConfigObj.adUnits[0].bids[0].params.target).to.equal('foo=bar'); + expect(reqBidsConfigObj.adUnits[0].bids[1].params.dctr).to.equal('foo=bar'); + expect(reqBidsConfigObj.adUnits[0].bids[2].params.keywords).to.deep.equal({ + foo: ['bar'] + }); + expect(reqBidsConfigObj.adUnits[0].bids[3].params).to.deep.equal({ + inventory: { + foo: 'bar', + }, + visitor: { + baz: 'bam', + } + }); + expect(reqBidsConfigObj.adUnits[0].bids[4].ortb2).to.be.undefined; + expect(getGlobal().getConfig('ortb2')).to.be.undefined; }); - it('should not set gam targeting with setPrebidTargeting=false but send to bidders', function() { + it('should set gam targeting but not send to bidders with (submodule override) setPrebidTargeting=true/(global) sendToBidders=false', function () { + let onDataResponse = {}; + const moduleConfig = { + params: { + setPrebidTargeting: false, + sendToBidders: false, + onData: (data, site) => { + onDataResponse = { + data: data, + site: site, + }; + }, + weboCtxConf: { + token: 'foo', + targetURL: 'https://prebid.org', + setPrebidTargeting: true, // submodule parameter will override module parameter + } + } + }; + const data = { + webo_ctx: ['foo', 'bar'], + webo_ds: ['baz'], + }; + const adUnitsCodes = ['adunit1', 'adunit2']; + const reqBidsConfigObj = { + adUnits: [{ + bids: [{ + bidder: 'smartadserver', + params: { + target: 'foo=bar' + } + }] + }] + }; + const onDoneSpy = sinon.spy(); + + expect(weboramaSubmodule.init(moduleConfig)).to.be.true; + weboramaSubmodule.getBidRequestData(reqBidsConfigObj, onDoneSpy, moduleConfig); + + let request = server.requests[0]; + + expect(request.method).to.equal('GET'); + expect(request.url).to.equal('https://ctx.weborama.com/api/profile?token=foo&url=https%3A%2F%2Fprebid.org&'); + expect(request.withCredentials).to.be.false; + + request.respond(200, responseHeader, JSON.stringify(data)); + + expect(onDoneSpy.calledOnce).to.be.true; + + const targeting = weboramaSubmodule.getTargetingData(adUnitsCodes, moduleConfig); + + expect(targeting).to.deep.equal({ + 'adunit1': data, + 'adunit2': data, + }); + + expect(reqBidsConfigObj.adUnits[0].bids.length).to.equal(1); + expect(getGlobal().getConfig('ortb2')).to.be.undefined; + + expect(onDataResponse).to.deep.equal({ + data: data, + site: true, + }); + }); + + it('should not set gam targeting with setPrebidTargeting=false but send to bidders', function () { const moduleConfig = { params: { weboCtxConf: { @@ -180,6 +335,30 @@ describe('weboramaRtdProvider', function() { params: { target: 'foo=bar' } + }, { + bidder: 'pubmatic', + params: { + dctr: 'foo=bar' + } + }, { + bidder: 'appnexus', + params: { + keywords: { + foo: ['bar'] + } + } + }, { + bidder: 'rubicon', + params: { + inventory: { + foo: 'bar', + }, + visitor: { + baz: 'bam', + } + } + }, { + bidder: 'other', }] }] } @@ -202,13 +381,45 @@ describe('weboramaRtdProvider', function() { expect(targeting).to.deep.equal({}); + expect(reqBidsConfigObj.adUnits[0].bids.length).to.equal(5); expect(reqBidsConfigObj.adUnits[0].bids[0].params.target).to.equal('foo=bar;webo_ctx=foo;webo_ctx=bar;webo_ds=baz'); + expect(reqBidsConfigObj.adUnits[0].bids[1].params.dctr).to.equal('foo=bar|webo_ctx=foo,bar|webo_ds=baz'); + expect(reqBidsConfigObj.adUnits[0].bids[2].params.keywords).to.deep.equal({ + foo: ['bar'], + webo_ctx: ['foo', 'bar'], + webo_ds: ['baz'], + }); + expect(reqBidsConfigObj.adUnits[0].bids[3].params).to.deep.equal({ + inventory: { + foo: 'bar', + webo_ctx: ['foo', 'bar'], + webo_ds: ['baz'], + }, + visitor: { + baz: 'bam', + } + }); + expect(reqBidsConfigObj.adUnits[0].bids[4].ortb2).to.deep.equal({ + site: { + ext: { + data: data + }, + } + }); + expect(getGlobal().getConfig('ortb2')).to.deep.equal({ + site: { + ext: { + data: data + }, + } + }); }); - it('should use default profile in case of api error', function() { + it('should use default profile in case of api error', function () { const defaultProfile = { webo_ctx: ['baz'], }; + let onDataResponse = {}; const moduleConfig = { params: { weboCtxConf: { @@ -216,6 +427,12 @@ describe('weboramaRtdProvider', function() { targetURL: 'https://prebid.org', setPrebidTargeting: true, defaultProfile: defaultProfile, + onData: (data, site) => { + onDataResponse = { + data: data, + site: site, + }; + }, } } }; @@ -225,6 +442,14 @@ describe('weboramaRtdProvider', function() { adUnits: [{ bids: [{ bidder: 'smartadserver' + }, { + bidder: 'pubmatic' + }, { + bidder: 'appnexus' + }, { + bidder: 'rubicon' + }, { + bidder: 'other' }] }] }; @@ -250,15 +475,48 @@ describe('weboramaRtdProvider', function() { 'adunit2': defaultProfile, }); + expect(reqBidsConfigObj.adUnits[0].bids.length).to.equal(5); expect(reqBidsConfigObj.adUnits[0].bids[0].params.target).to.equal('webo_ctx=baz'); + expect(reqBidsConfigObj.adUnits[0].bids[1].params.dctr).to.equal('webo_ctx=baz'); + expect(reqBidsConfigObj.adUnits[0].bids[2].params.keywords).to.deep.equal(defaultProfile); + expect(reqBidsConfigObj.adUnits[0].bids[3].params).to.deep.equal({ + inventory: defaultProfile + }); + expect(reqBidsConfigObj.adUnits[0].bids[4].ortb2).to.deep.equal({ + site: { + ext: { + data: defaultProfile + }, + } + }); + expect(getGlobal().getConfig('ortb2')).to.deep.equal({ + site: { + ext: { + data: defaultProfile + }, + } + }); + expect(onDataResponse).to.deep.equal({ + data: defaultProfile, + site: true, + }); }); }); - describe('Add WAM2GAM Data', function() { - it('should set gam targeting from local storage and send to bidders by default', function() { + describe('Add user-centric data (WAM2GAM)', function () { + it('should set gam targeting from local storage and send to bidders by default', function () { + let onDataResponse = {}; const moduleConfig = { params: { - weboUserDataConf: {} + weboUserDataConf: { + accountID: 12345, + onData: (data, site) => { + onDataResponse = { + data: data, + site: site, + }; + }, + } } }; const data = { @@ -280,6 +538,14 @@ describe('weboramaRtdProvider', function() { adUnits: [{ bids: [{ bidder: 'smartadserver' + }, { + bidder: 'pubmatic' + }, { + bidder: 'appnexus' + }, { + bidder: 'rubicon' + }, { + bidder: 'other' }] }] }; @@ -297,10 +563,34 @@ describe('weboramaRtdProvider', function() { 'adunit2': data, }); + expect(reqBidsConfigObj.adUnits[0].bids.length).to.equal(5); expect(reqBidsConfigObj.adUnits[0].bids[0].params.target).to.equal('webo_cs=foo;webo_cs=bar;webo_audiences=baz'); + expect(reqBidsConfigObj.adUnits[0].bids[1].params.dctr).to.equal('webo_cs=foo,bar|webo_audiences=baz'); + expect(reqBidsConfigObj.adUnits[0].bids[2].params.keywords).to.deep.equal(data); + expect(reqBidsConfigObj.adUnits[0].bids[3].params).to.deep.equal({ + visitor: data + }); + expect(reqBidsConfigObj.adUnits[0].bids[4].ortb2).to.deep.equal({ + user: { + ext: { + data: data + }, + } + }); + expect(getGlobal().getConfig('ortb2')).to.deep.equal({ + user: { + ext: { + data: data + }, + } + }); + expect(onDataResponse).to.deep.equal({ + data: data, + site: false, + }); }); - it('should set gam targeting but not send to bidders with setPrebidTargeting=true/sendToBidders=false', function() { + it('should set gam targeting but not send to bidders with setPrebidTargeting=true/sendToBidders=false', function () { const moduleConfig = { params: { weboUserDataConf: { @@ -331,6 +621,30 @@ describe('weboramaRtdProvider', function() { params: { target: 'foo=bar' } + }, { + bidder: 'pubmatic', + params: { + dctr: 'foo=bar' + } + }, { + bidder: 'appnexus', + params: { + keywords: { + foo: ['bar'] + } + } + }, { + bidder: 'rubicon', + params: { + inventory: { + foo: 'bar' + }, + visitor: { + baz: 'bam' + } + } + }, { + bidder: 'other' }] }] }; @@ -348,10 +662,90 @@ describe('weboramaRtdProvider', function() { 'adunit2': data, }); + expect(reqBidsConfigObj.adUnits[0].bids.length).to.equal(5); expect(reqBidsConfigObj.adUnits[0].bids[0].params.target).to.equal('foo=bar'); + expect(reqBidsConfigObj.adUnits[0].bids[1].params.dctr).to.equal('foo=bar'); + expect(reqBidsConfigObj.adUnits[0].bids[2].params.keywords).to.deep.equal({ + foo: ['bar'] + }); + expect(reqBidsConfigObj.adUnits[0].bids[3].params).to.deep.equal({ + inventory: { + foo: 'bar' + }, + visitor: { + baz: 'bam' + } + }); + expect(reqBidsConfigObj.adUnits[0].bids[4].ortb2).to.be.undefined; + expect(getGlobal().getConfig('ortb2')).to.be.undefined; }); - it('should not set gam targeting with setPrebidTargeting=false but send to bidders', function() { + it('should set gam targeting but not send to bidders with (submodule override) setPrebidTargeting=true/(global) sendToBidders=false', function () { + let onDataResponse = {}; + const moduleConfig = { + params: { + setPrebidTargeting: false, + sendToBidders: false, + onData: (data, site) => { + onDataResponse = { + data: data, + site: site, + }; + }, + weboUserDataConf: { + setPrebidTargeting: true, // submodule parameter will override module parameter + } + } + }; + const data = { + webo_cs: ['foo', 'bar'], + webo_audiences: ['baz'], + }; + + const entry = { + targeting: data, + }; + + sandbox.stub(storage, 'localStorageIsEnabled').returns(true); + sandbox.stub(storage, 'getDataFromLocalStorage') + .withArgs(DEFAULT_LOCAL_STORAGE_USER_PROFILE_KEY) + .returns(JSON.stringify(entry)); + + const adUnitsCodes = ['adunit1', 'adunit2']; + const reqBidsConfigObj = { + adUnits: [{ + bids: [{ + bidder: 'smartadserver', + params: { + target: 'foo=bar' + } + }] + }] + }; + const onDoneSpy = sinon.spy(); + + expect(weboramaSubmodule.init(moduleConfig)).to.be.true; + weboramaSubmodule.getBidRequestData(reqBidsConfigObj, onDoneSpy, moduleConfig); + + expect(onDoneSpy.calledOnce).to.be.true; + + const targeting = weboramaSubmodule.getTargetingData(adUnitsCodes, moduleConfig); + + expect(targeting).to.deep.equal({ + 'adunit1': data, + 'adunit2': data, + }); + + expect(reqBidsConfigObj.adUnits[0].bids.length).to.equal(1); + expect(reqBidsConfigObj.adUnits[0].bids[0].params.target).to.equal('foo=bar'); + expect(getGlobal().getConfig('ortb2')).to.be.undefined; + expect(onDataResponse).to.deep.equal({ + data: data, + site: false, + }); + }); + + it('should not set gam targeting with setPrebidTargeting=false but send to bidders', function () { const moduleConfig = { params: { weboUserDataConf: { @@ -381,6 +775,30 @@ describe('weboramaRtdProvider', function() { params: { target: 'foo=bar' } + }, { + bidder: 'pubmatic', + params: { + dctr: 'foo=bar' + } + }, { + bidder: 'appnexus', + params: { + keywords: { + foo: ['bar'] + } + } + }, { + bidder: 'rubicon', + params: { + inventory: { + foo: 'bar', + }, + visitor: { + baz: 'bam', + } + } + }, { + bidder: 'other' }] }] }; @@ -395,10 +813,41 @@ describe('weboramaRtdProvider', function() { expect(targeting).to.deep.equal({}); + expect(reqBidsConfigObj.adUnits[0].bids.length).to.equal(5); expect(reqBidsConfigObj.adUnits[0].bids[0].params.target).to.equal('foo=bar;webo_cs=foo;webo_cs=bar;webo_audiences=baz'); + expect(reqBidsConfigObj.adUnits[0].bids[1].params.dctr).to.equal('foo=bar|webo_cs=foo,bar|webo_audiences=baz'); + expect(reqBidsConfigObj.adUnits[0].bids[2].params.keywords).to.deep.equal({ + foo: ['bar'], + webo_cs: ['foo', 'bar'], + webo_audiences: ['baz'], + }); + expect(reqBidsConfigObj.adUnits[0].bids[3].params).to.deep.equal({ + inventory: { + foo: 'bar', + }, + visitor: { + baz: 'bam', + webo_cs: ['foo', 'bar'], + webo_audiences: ['baz'], + } + }); + expect(reqBidsConfigObj.adUnits[0].bids[4].ortb2).to.deep.equal({ + user: { + ext: { + data: data + }, + } + }); + expect(getGlobal().getConfig('ortb2')).to.deep.equal({ + user: { + ext: { + data: data + }, + } + }); }); - it('should use default profile in case of nothing on local storage', function() { + it('should use default profile in case of nothing on local storage', function () { const defaultProfile = { webo_audiences: ['baz'] }; @@ -418,6 +867,14 @@ describe('weboramaRtdProvider', function() { adUnits: [{ bids: [{ bidder: 'smartadserver' + }, { + bidder: 'pubmatic' + }, { + bidder: 'appnexus' + }, { + bidder: 'rubicon' + }, { + bidder: 'other' }] }] }; @@ -435,18 +892,45 @@ describe('weboramaRtdProvider', function() { 'adunit2': defaultProfile, }); + expect(reqBidsConfigObj.adUnits[0].bids.length).to.equal(5); expect(reqBidsConfigObj.adUnits[0].bids[0].params.target).to.equal('webo_audiences=baz'); + expect(reqBidsConfigObj.adUnits[0].bids[1].params.dctr).to.equal('webo_audiences=baz'); + expect(reqBidsConfigObj.adUnits[0].bids[2].params.keywords).to.deep.equal(defaultProfile); + expect(reqBidsConfigObj.adUnits[0].bids[3].params).to.deep.equal({ + visitor: defaultProfile + }); + expect(reqBidsConfigObj.adUnits[0].bids[4].ortb2).to.deep.equal({ + user: { + ext: { + data: defaultProfile + }, + } + }); + expect(getGlobal().getConfig('ortb2')).to.deep.equal({ + user: { + ext: { + data: defaultProfile + }, + } + }); }); - it('should use default profile if cant read from local storage', function() { + it('should use default profile if cant read from local storage', function () { const defaultProfile = { webo_audiences: ['baz'] }; + let onDataResponse = {}; const moduleConfig = { params: { weboUserDataConf: { setPrebidTargeting: true, defaultProfile: defaultProfile, + onData: (data, site) => { + onDataResponse = { + data: data, + site: site, + }; + }, } } }; @@ -458,6 +942,14 @@ describe('weboramaRtdProvider', function() { adUnits: [{ bids: [{ bidder: 'smartadserver' + }, { + bidder: 'pubmatic' + }, { + bidder: 'appnexus' + }, { + bidder: 'rubicon' + }, { + bidder: 'other' }] }] }; @@ -475,7 +967,31 @@ describe('weboramaRtdProvider', function() { 'adunit2': defaultProfile, }); + expect(reqBidsConfigObj.adUnits[0].bids.length).to.equal(5); expect(reqBidsConfigObj.adUnits[0].bids[0].params.target).to.equal('webo_audiences=baz'); + expect(reqBidsConfigObj.adUnits[0].bids[1].params.dctr).to.equal('webo_audiences=baz'); + expect(reqBidsConfigObj.adUnits[0].bids[2].params.keywords).to.deep.equal(defaultProfile); + expect(reqBidsConfigObj.adUnits[0].bids[3].params).to.deep.equal({ + visitor: defaultProfile + }); + expect(reqBidsConfigObj.adUnits[0].bids[4].ortb2).to.deep.equal({ + user: { + ext: { + data: defaultProfile + }, + } + }); + expect(getGlobal().getConfig('ortb2')).to.deep.equal({ + user: { + ext: { + data: defaultProfile + }, + } + }); + expect(onDataResponse).to.deep.equal({ + data: defaultProfile, + site: false, + }); }); }); }); diff --git a/test/spec/modules/widespaceBidAdapter_spec.js b/test/spec/modules/widespaceBidAdapter_spec.js index af8d505b4f4..0a0af1f1229 100644 --- a/test/spec/modules/widespaceBidAdapter_spec.js +++ b/test/spec/modules/widespaceBidAdapter_spec.js @@ -1,6 +1,6 @@ import {expect} from 'chai'; import {spec, storage} from 'modules/widespaceBidAdapter.js'; -import includes from 'core-js-pure/features/array/includes.js'; +import {includes} from 'src/polyfill.js'; describe('+widespaceAdatperTest', function () { // Dummy bid request diff --git a/test/spec/modules/yahoosspBidAdapter_spec.js b/test/spec/modules/yahoosspBidAdapter_spec.js index e0af8784605..e301218741c 100644 --- a/test/spec/modules/yahoosspBidAdapter_spec.js +++ b/test/spec/modules/yahoosspBidAdapter_spec.js @@ -177,6 +177,16 @@ describe('YahooSSP Bid Adapter:', () => { expect(obj).to.be.an('object'); }); + describe('Validate basic properties', () => { + it('should define the correct bidder code', () => { + expect(spec.code).to.equal('yahoossp') + }); + + it('should define the correct vendor ID', () => { + expect(spec.gvlid).to.equal(25) + }); + }); + describe('getUserSyncs()', () => { const IMAGE_PIXEL_URL = 'http://image-pixel.com/foo/bar?1234&baz=true'; const IFRAME_ONE_URL = 'http://image-iframe.com/foo/bar?1234&baz=true'; @@ -1355,5 +1365,13 @@ describe('YahooSSP Bid Adapter:', () => { expect(response[0].ttl).to.equal(500); }); }); + + describe('Aliasing support', () => { + it('should return undefined as the bidder code value', () => { + const { serverResponse, bidderRequest } = generateResponseMock('banner'); + const response = spec.interpretResponse(serverResponse, {bidderRequest}); + expect(response[0].bidderCode).to.be.undefined; + }); + }); }); }); diff --git a/test/spec/modules/yandexBidAdapter_spec.js b/test/spec/modules/yandexBidAdapter_spec.js new file mode 100644 index 00000000000..833f883fb7c --- /dev/null +++ b/test/spec/modules/yandexBidAdapter_spec.js @@ -0,0 +1,166 @@ +import {assert, expect} from 'chai'; +import {spec} from 'modules/yandexBidAdapter.js'; +import {parseUrl} from 'src/utils.js'; +import {BANNER} from '../../../src/mediaTypes'; + +describe('Yandex adapter', function () { + function getBidConfig() { + return { + bidder: 'yandex', + params: { + pageId: 123, + impId: 1, + }, + }; + } + + function getBidRequest() { + return { + ...getBidConfig(), + bidId: 'bidid-1', + adUnitCode: 'adUnit-123', + mediaTypes: { + banner: { + sizes: [ + [300, 250], + [300, 600] + ], + }, + }, + }; + } + + describe('isBidRequestValid', function () { + it('should return true when required params found', function () { + const bid = getBidConfig(); + assert(spec.isBidRequestValid(bid)); + }); + + it('should return false when required params not found', function () { + expect(spec.isBidRequestValid({})).to.be.false; + }); + + it('should return false when required params.pageId are not passed', function () { + const bid = getBidConfig(); + delete bid.params.pageId; + + expect(spec.isBidRequestValid(bid)).to.be.false + }); + + it('should return false when required params.impId are not passed', function () { + const bid = getBidConfig(); + delete bid.params.impId; + + expect(spec.isBidRequestValid(bid)).to.be.false + }); + }); + + describe('buildRequests', function () { + const refererUrl = 'https://yandex.ru/secure-ads'; + + const gdprConsent = { + gdprApplies: 1, + consentString: 'concent-string', + apiVersion: 1, + }; + + const bidderRequest = { + refererInfo: { + referer: refererUrl + }, + gdprConsent + }; + + it('creates a valid banner request', function () { + const bannerRequest = getBidRequest(); + bannerRequest.getFloor = () => ({ + currency: 'USD', + // floor: 0.5 + }); + + const requests = spec.buildRequests([bannerRequest], bidderRequest); + + expect(requests).to.have.lengthOf(1); + const request = requests[0]; + + expect(request).to.exist; + const { method, url, data } = request; + + expect(method).to.equal('POST'); + + const parsedRequestUrl = parseUrl(url); + const { search: query } = parsedRequestUrl + + expect(parsedRequestUrl.hostname).to.equal('bs-metadsp.yandex.ru'); + expect(parsedRequestUrl.pathname).to.equal('/metadsp/123'); + + expect(query['imp-id']).to.equal('1'); + expect(query['target-ref']).to.equal('yandex.ru'); + expect(query['ssp-id']).to.equal('10500'); + + expect(query['gdpr']).to.equal('1'); + expect(query['tcf-consent']).to.equal('concent-string'); + + expect(request.data).to.exist; + expect(data.site).to.not.equal(null); + expect(data.site.page).to.equal('yandex.ru'); + + // expect(data.device).to.not.equal(null); + // expect(data.device.w).to.equal(window.innerWidth); + // expect(data.device.h).to.equal(window.innerHeight); + + expect(data.imp).to.have.lengthOf(1); + expect(data.imp[0].banner).to.not.equal(null); + expect(data.imp[0].banner.w).to.equal(300); + expect(data.imp[0].banner.h).to.equal(250); + }); + }); + + describe('response handler', function () { + const bannerRequest = getBidRequest(); + + const bannerResponse = { + body: { + seatbid: [{ + bid: [ + { + impid: '1', + price: 0.3, + crid: 321, + adm: '', + w: 300, + h: 250, + adomain: [ + 'example.com' + ], + adid: 'yabs.123=', + } + ] + }], + cur: 'USD', + }, + }; + + it('handles banner responses', function () { + bannerRequest.bidRequest = { + mediaType: BANNER, + bidId: 'bidid-1', + }; + const result = spec.interpretResponse(bannerResponse, bannerRequest); + + expect(result).to.have.lengthOf(1); + expect(result[0]).to.exist; + + 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.ad).to.equal(''); + expect(rtbBid.currency).to.equal('USD'); + expect(rtbBid.netRevenue).to.equal(true); + expect(rtbBid.ttl).to.equal(180); + + expect(rtbBid.meta.advertiserDomains).to.deep.equal(['example.com']); + }); + }); +}); diff --git a/test/spec/modules/yieldlabBidAdapter_spec.js b/test/spec/modules/yieldlabBidAdapter_spec.js index f80cad46d50..e4d258ecdea 100644 --- a/test/spec/modules/yieldlabBidAdapter_spec.js +++ b/test/spec/modules/yieldlabBidAdapter_spec.js @@ -414,4 +414,40 @@ describe('yieldlabBidAdapter', function () { expect(result[0].vastUrl).to.include('&iab_content=id%3Afoo_id%2Cepisode%3A99%2Ctitle%3Afoo_title%252Cbar_title%2Cseries%3Afoo_series%2Cseason%3As1%2Cartist%3Afoo%2520bar%2Cgenre%3Abaz%2Cisrc%3ACC-XXX-YY-NNNNN%2Curl%3Ahttp%253A%252F%252Ffoo_url.de%2Ccat%3Acat1%7Ccat2%252Cppp%7Ccat3%257C%257C%257C%252F%252F%2Ccontext%3A7%2Ckeywords%3Ak1%252C%7Ck2..%2Clive%3A0') }) }) + + describe('getUserSyncs', function () { + const syncOptions = { + iframeEnabled: true, + pixelEnabled: false + }; + const expectedUrlSnippets = ['https://ad.yieldlab.net/d/6846326/766/2x2?', 'ts=', 'type=h']; + + it('should return user sync as expected', function () { + const bidRequest = { + gdprConsent: { + consentString: 'BN5lERiOMYEdiAKAWXEND1AAAAE6DABACMA', + gdprApplies: true + }, + uspConsent: '1YYY' + }; + const sync = spec.getUserSyncs(syncOptions, [], bidRequest.gdprConsent, bidRequest.uspConsent); + expect(expectedUrlSnippets.every(urlSnippet => sync[0].url.includes(urlSnippet))); + expect(sync[0].url).to.have.string('gdpr=' + Number(bidRequest.gdprConsent.gdprApplies)); + expect(sync[0].url).to.have.string('gdpr_consent=' + bidRequest.gdprConsent.consentString); + // USP consent should be ignored + expect(sync[0].url).not.have.string('usp_consent='); + expect(sync[0].type).to.have.string('iframe'); + }); + + it('should return user sync even without gdprApplies in gdprConsent', function () { + const gdprConsent = { + consentString: 'BN5lERiOMYEdiAKAWXEND1AAAAE6DABACMA' + } + const sync = spec.getUserSyncs(syncOptions, [], gdprConsent, undefined); + expect(expectedUrlSnippets.every(urlSnippet => sync[0].url.includes(urlSnippet))); + expect(sync[0].url).to.have.string('gdpr_consent=' + gdprConsent.consentString); + expect(sync[0].url).not.have.string('gdpr='); + expect(sync[0].type).to.have.string('iframe'); + }); + }); }) diff --git a/test/spec/modules/yieldmoBidAdapter_spec.js b/test/spec/modules/yieldmoBidAdapter_spec.js index 3eee9e44453..f72705a79ac 100644 --- a/test/spec/modules/yieldmoBidAdapter_spec.js +++ b/test/spec/modules/yieldmoBidAdapter_spec.js @@ -511,6 +511,7 @@ describe('YieldmoAdapter', function () { body: [{ callback_id: '21989fdbef550a', cpm: 3.45455, + publisherDealId: 'YMO_123', width: 300, height: 250, ad: '' + @@ -525,6 +526,7 @@ describe('YieldmoAdapter', function () { const newResponse = spec.interpretResponse(mockServerResponse()); expect(newResponse.length).to.be.equal(1); expect(newResponse[0]).to.deep.equal({ + dealId: 'YMO_123', requestId: '21989fdbef550a', cpm: 3.45455, width: 300, @@ -552,6 +554,7 @@ describe('YieldmoAdapter', function () { crid: 'dd65c0a7536aff', impid: '91ea8bba1', price: 1.5, + dealid: 'YMO_456' }, }, ]; @@ -574,6 +577,7 @@ describe('YieldmoAdapter', function () { const newResponse = spec.interpretResponse(response, bidRequest); expect(newResponse.length).to.be.equal(2); expect(newResponse[1]).to.deep.equal({ + dealId: 'YMO_456', cpm: 1.5, creativeId: 'dd65c0a7536aff', currency: 'USD', diff --git a/test/spec/modules/yieldoneBidAdapter_spec.js b/test/spec/modules/yieldoneBidAdapter_spec.js index 1b38bb94a8c..d452d78e147 100644 --- a/test/spec/modules/yieldoneBidAdapter_spec.js +++ b/test/spec/modules/yieldoneBidAdapter_spec.js @@ -366,6 +366,39 @@ describe('yieldoneBidAdapter', function() { expect(request[0].data.lr_env).to.equal('idl_env_sample'); }); }); + + describe('IMID', function () { + it('dont send IMID if undefined', function () { + const bidRequests = [ + { + params: {placementId: '0'}, + }, + { + params: {placementId: '1'}, + userId: {}, + }, + { + params: {placementId: '2'}, + userId: undefined, + }, + ]; + const request = spec.buildRequests(bidRequests, bidderRequest); + expect(request[0].data).to.not.have.property('imuid'); + expect(request[1].data).to.not.have.property('imuid'); + expect(request[2].data).to.not.have.property('imuid'); + }); + + it('should send IMID if available', function () { + const bidRequests = [ + { + params: {placementId: '0'}, + userId: {imuid: 'imuid_sample'}, + }, + ]; + const request = spec.buildRequests(bidRequests, bidderRequest); + expect(request[0].data.imuid).to.equal('imuid_sample'); + }); + }); }); describe('interpretResponse', function () { diff --git a/test/spec/modules/zeotapIdPlusIdSystem_spec.js b/test/spec/modules/zeotapIdPlusIdSystem_spec.js index 9de6fa843bc..98459a36d45 100644 --- a/test/spec/modules/zeotapIdPlusIdSystem_spec.js +++ b/test/spec/modules/zeotapIdPlusIdSystem_spec.js @@ -1,5 +1,5 @@ import { expect } from 'chai'; -import find from 'core-js-pure/features/array/find.js'; +import {find} from 'src/polyfill.js'; import { config } from 'src/config.js'; import { init, requestBidsHook, setSubmoduleRegistry } from 'modules/userId/index.js'; import { storage, getStorage, zeotapIdPlusSubmodule } from 'modules/zeotapIdPlusIdSystem.js'; @@ -54,7 +54,7 @@ describe('Zeotap ID System', function() { it('when a stored Zeotap ID exists it is added to bids', function() { let store = getStorage(); expect(getStorageManagerSpy.calledOnce).to.be.true; - sinon.assert.calledWith(getStorageManagerSpy, 301, 'zeotapIdPlus'); + sinon.assert.calledWith(getStorageManagerSpy, {gvlid: 301, moduleName: 'zeotapIdPlus'}); }); }); diff --git a/test/spec/modules/zeta_global_sspBidAdapter_spec.js b/test/spec/modules/zeta_global_sspBidAdapter_spec.js index 7d4a115958e..20113a63994 100644 --- a/test/spec/modules/zeta_global_sspBidAdapter_spec.js +++ b/test/spec/modules/zeta_global_sspBidAdapter_spec.js @@ -100,6 +100,13 @@ describe('Zeta Ssp Bid Adapter', function () { expect(payload.user.ext.eids).to.eql(eids); }); + it('Test contains ua and language', function () { + const request = spec.buildRequests(bannerRequest, bannerRequest[0]); + const payload = JSON.parse(request.data); + expect(payload.device.ua).to.not.be.empty; + expect(payload.device.language).to.not.be.empty; + }); + it('Test page and domain in site', function () { const request = spec.buildRequests(bannerRequest, bannerRequest[0]); const payload = JSON.parse(request.data); diff --git a/test/spec/native_spec.js b/test/spec/native_spec.js index 0ffef30965b..66e11b9a472 100644 --- a/test/spec/native_spec.js +++ b/test/spec/native_spec.js @@ -1,10 +1,19 @@ import { expect } from 'chai'; -import { fireNativeTrackers, getNativeTargeting, nativeBidIsValid, getAssetMessage, getAllAssetsMessage } from 'src/native.js'; +import { + fireNativeTrackers, + getNativeTargeting, + nativeBidIsValid, + getAssetMessage, + getAllAssetsMessage, + decorateAdUnitsWithNativeParams +} from 'src/native.js'; import CONSTANTS from 'src/constants.json'; +import {stubAuctionIndex} from '../helpers/indexStub.js'; const utils = require('src/utils'); const bid = { adId: '123', + transactionId: 'au', native: { title: 'Native Creative', body: 'Cool description great stuff', @@ -32,6 +41,7 @@ const bid = { }; const bidWithUndefinedFields = { + transactionId: 'au', native: { title: 'Native Creative', body: undefined, @@ -52,6 +62,10 @@ describe('native.js', function () { let triggerPixelStub; let insertHtmlIntoIframeStub; + function deps(adUnit) { + return { index: stubAuctionIndex({ adUnits: [adUnit] }) }; + } + beforeEach(function () { triggerPixelStub = sinon.stub(utils, 'triggerPixel'); insertHtmlIntoIframeStub = sinon.stub(utils, 'insertHtmlIntoIframe'); @@ -71,7 +85,8 @@ describe('native.js', function () { }); it('sends placeholders for configured assets', function () { - const bidRequest = { + const adUnit = { + transactionId: 'au', nativeParams: { body: { sendId: true }, clickUrl: { sendId: true }, @@ -85,7 +100,7 @@ describe('native.js', function () { } } }; - const targeting = getNativeTargeting(bid, bidRequest); + const targeting = getNativeTargeting(bid, deps(adUnit)); expect(targeting[CONSTANTS.NATIVE_KEYS.title]).to.equal(bid.native.title); expect(targeting[CONSTANTS.NATIVE_KEYS.body]).to.equal('hb_native_body:123'); @@ -95,7 +110,8 @@ describe('native.js', function () { }); it('should only include native targeting keys with values', function () { - const bidRequest = { + const adUnit = { + transactionId: 'au', nativeParams: { body: { sendId: true }, clickUrl: { sendId: true }, @@ -110,7 +126,7 @@ describe('native.js', function () { } }; - const targeting = getNativeTargeting(bidWithUndefinedFields, bidRequest); + const targeting = getNativeTargeting(bidWithUndefinedFields, deps(adUnit)); expect(Object.keys(targeting)).to.deep.equal([ CONSTANTS.NATIVE_KEYS.title, @@ -121,7 +137,8 @@ describe('native.js', function () { }); it('should only include targeting that has sendTargetingKeys set to true', function () { - const bidRequest = { + const adUnit = { + transactionId: 'au', nativeParams: { image: { required: true, @@ -136,7 +153,7 @@ describe('native.js', function () { } }; - const targeting = getNativeTargeting(bid, bidRequest); + const targeting = getNativeTargeting(bid, deps(adUnit)); expect(Object.keys(targeting)).to.deep.equal([ CONSTANTS.NATIVE_KEYS.title @@ -144,7 +161,8 @@ describe('native.js', function () { }); it('should only include targeting if sendTargetingKeys not set to false', function () { - const bidRequest = { + const adUnit = { + transactionId: 'au', nativeParams: { image: { required: true, @@ -181,7 +199,7 @@ describe('native.js', function () { } }; - const targeting = getNativeTargeting(bid, bidRequest); + const targeting = getNativeTargeting(bid, deps(adUnit)); expect(Object.keys(targeting)).to.deep.equal([ CONSTANTS.NATIVE_KEYS.title, @@ -193,7 +211,8 @@ describe('native.js', function () { }); it('should copy over rendererUrl to bid object and include it in targeting', function () { - const bidRequest = { + const adUnit = { + transactionId: 'au', nativeParams: { image: { required: true, @@ -209,7 +228,7 @@ describe('native.js', function () { } }; - const targeting = getNativeTargeting(bid, bidRequest); + const targeting = getNativeTargeting(bid, deps(adUnit)); expect(Object.keys(targeting)).to.deep.equal([ CONSTANTS.NATIVE_KEYS.title, @@ -227,7 +246,8 @@ describe('native.js', function () { }); it('should copy over adTemplate to bid object and include it in targeting', function () { - const bidRequest = { + const adUnit = { + transactionId: 'au', nativeParams: { image: { required: true, @@ -241,7 +261,7 @@ describe('native.js', function () { } }; - const targeting = getNativeTargeting(bid, bidRequest); + const targeting = getNativeTargeting(bid, deps(adUnit)); expect(Object.keys(targeting)).to.deep.equal([ CONSTANTS.NATIVE_KEYS.title, @@ -374,35 +394,33 @@ describe('native.js', function () { }); describe('validate native', function () { - let bidReq = [{ - bids: [{ - bidderCode: 'test_bidder', - bidId: 'test_bid_id', - mediaTypes: { - native: { - title: { - required: true, - }, - body: { - required: true, - }, - image: { - required: true, - sizes: [150, 50], - aspect_ratios: [150, 50] - }, - icon: { - required: true, - sizes: [50, 50] - }, - } + const adUnit = { + transactionId: 'test_adunit', + mediaTypes: { + native: { + title: { + required: true, + }, + body: { + required: true, + }, + image: { + required: true, + sizes: [150, 50], + aspect_ratios: [150, 50] + }, + icon: { + required: true, + sizes: [50, 50] + }, } - }] - }]; + } + } let validBid = { adId: 'abc123', requestId: 'test_bid_id', + transactionId: 'test_adunit', adUnitCode: '123/prebid_native_adunit', bidder: 'test_bidder', native: { @@ -428,6 +446,7 @@ describe('validate native', function () { let noIconDimBid = { adId: 'abc234', requestId: 'test_bid_id', + transactionId: 'test_adunit', adUnitCode: '123/prebid_native_adunit', bidder: 'test_bidder', native: { @@ -449,6 +468,7 @@ describe('validate native', function () { let noImgDimBid = { adId: 'abc345', requestId: 'test_bid_id', + transactionId: 'test_adunit', adUnitCode: '123/prebid_native_adunit', bidder: 'test_bidder', native: { @@ -472,11 +492,13 @@ describe('validate native', function () { afterEach(function () {}); it('should accept bid if no image sizes are defined', function () { - let result = nativeBidIsValid(validBid, bidReq); + decorateAdUnitsWithNativeParams([adUnit]); + const index = stubAuctionIndex({adUnits: [adUnit]}) + let result = nativeBidIsValid(validBid, {index}); expect(result).to.be.true; - result = nativeBidIsValid(noIconDimBid, bidReq); + result = nativeBidIsValid(noIconDimBid, {index}); expect(result).to.be.true; - result = nativeBidIsValid(noImgDimBid, bidReq); + result = nativeBidIsValid(noImgDimBid, {index}); expect(result).to.be.true; }); }); diff --git a/test/spec/sizeMapping_spec.js b/test/spec/sizeMapping_spec.js index a3c39a52441..c4efbddad6d 100644 --- a/test/spec/sizeMapping_spec.js +++ b/test/spec/sizeMapping_spec.js @@ -1,6 +1,6 @@ import { expect } from 'chai'; import { resolveStatus, setSizeConfig, sizeSupported } from 'src/sizeMapping.js'; -import includes from 'core-js-pure/features/array/includes.js'; +import {includes} from 'src/polyfill.js' let utils = require('src/utils'); let deepClone = utils.deepClone; diff --git a/test/spec/unit/core/adapterManager_spec.js b/test/spec/unit/core/adapterManager_spec.js index f414febcebe..88beaa88a67 100644 --- a/test/spec/unit/core/adapterManager_spec.js +++ b/test/spec/unit/core/adapterManager_spec.js @@ -1,5 +1,11 @@ import { expect } from 'chai'; -import adapterManager, { allS2SBidders, clientTestAdapters, gdprDataHandler, coppaDataHandler } from 'src/adapterManager.js'; +import adapterManager, { + gdprDataHandler, + coppaDataHandler, + _partitionBidders, + PARTITIONS, + getS2SBidderSet, _filterBidsForAdUnit +} from 'src/adapterManager.js'; import { getAdUnits, getServerTestingConfig, @@ -11,9 +17,9 @@ import * as utils from 'src/utils.js'; import { config } from 'src/config.js'; import { registerBidder } from 'src/adapters/bidderFactory.js'; import { setSizeConfig } from 'src/sizeMapping.js'; -import find from 'core-js-pure/features/array/find.js'; -import includes from 'core-js-pure/features/array/includes.js'; +import {find, includes} from 'src/polyfill.js'; import s2sTesting from 'modules/s2sTesting.js'; +import {hook} from '../../../../src/hook.js'; var events = require('../../../../src/events'); const CONFIG = { @@ -87,9 +93,14 @@ describe('adapterManager tests', function () { config.setConfig({s2sConfig: { enabled: false }}); }); + afterEach(() => { + s2sTesting.clientTestBidders.clear(); + }); + describe('callBids', function () { before(function () { config.setConfig({s2sConfig: { enabled: false }}); + hook.ready(); }); beforeEach(function () { @@ -700,10 +711,6 @@ describe('adapterManager tests', function () { prebidServerAdapterMock.callBids.reset(); }); - afterEach(function () { - allS2SBidders.length = 0; - }); - const bidRequests = [{ 'bidderCode': 'appnexus', 'auctionId': '1863e370099523', @@ -1304,9 +1311,6 @@ describe('adapterManager tests', function () { } beforeEach(function () { - allS2SBidders.length = 0; - clientTestAdapters.length = 0 - adapterManager.bidderRegistry['prebidServer'] = prebidServerAdapterMock; adapterManager.bidderRegistry['adequant'] = adequantAdapterMock; adapterManager.bidderRegistry['appnexus'] = appnexusAdapterMock; @@ -1661,13 +1665,32 @@ describe('adapterManager tests', function () { describe('makeBidRequests', function () { let adUnits; beforeEach(function () { - allS2SBidders.length = 0 adUnits = utils.deepClone(getAdUnits()).map(adUnit => { adUnit.bids = adUnit.bids.filter(bid => includes(['appnexus', 'rubicon'], bid.bidder)); return adUnit; }) }); + it('should add nativeParams to adUnits after BEFORE_REQUEST_BIDS', () => { + function beforeReqBids(adUnits) { + adUnits.forEach(adUnit => { + adUnit.mediaTypes.native = { + type: 'image', + } + }) + } + events.on(CONSTANTS.EVENTS.BEFORE_REQUEST_BIDS, beforeReqBids); + adapterManager.makeBidRequests( + adUnits, + Date.now(), + utils.getUniqueIdentifierStr(), + function callback() {}, + [] + ); + events.off(CONSTANTS.EVENTS.BEFORE_REQUEST_BIDS, beforeReqBids); + expect(adUnits.map((u) => u.nativeParams).some(i => i == null)).to.be.false; + }); + it('should make separate bidder request objects for each bidder', () => { adUnits = [utils.deepClone(getAdUnits()[0])]; @@ -1715,8 +1738,6 @@ describe('adapterManager tests', function () { let sandbox; beforeEach(function () { sandbox = sinon.sandbox.create(); - allS2SBidders.length = 0; - clientTestAdapters.length = 0; // always have matchMedia return true for us sandbox.stub(utils.getWindowTop(), 'matchMedia').callsFake(() => ({matches: true})); }); @@ -1892,7 +1913,7 @@ describe('adapterManager tests', function () { ['visitor-uk', 'desktop'] ); - // only one adUnit and one bid from that adUnit should make it through the applied labels above + // only one adUnit and one bid from that adUnit should make it through the applied labels above expect(bidRequests.length).to.equal(1); expect(bidRequests[0].bidderCode).to.equal('rubicon'); expect(bidRequests[0].bids.length).to.equal(1); @@ -1976,7 +1997,6 @@ describe('adapterManager tests', function () { describe('s2sTesting - testServerOnly', () => { beforeEach(() => { config.setConfig({ s2sConfig: getServerTestingConfig(CONFIG) }); - allS2SBidders.length = 0 s2sTesting.bidSource = {}; }); @@ -2107,7 +2127,6 @@ describe('adapterManager tests', function () { afterEach(() => { config.resetConfig() - allS2SBidders.length = 0; s2sTesting.bidSource = {}; }); @@ -2287,4 +2306,102 @@ describe('adapterManager tests', function () { ); }); }); + + describe('getS2SBidderSet', () => { + it('should always return the "null" bidder', () => { + expect([...getS2SBidderSet({bidders: []})]).to.eql([null]); + }); + + it('should not consider disabled s2s adapters', () => { + const actual = getS2SBidderSet([{enabled: false, bidders: ['A', 'B']}, {enabled: true, bidders: ['C']}]); + expect([...actual]).to.include.members(['C']); + expect([...actual]).not.to.include.members(['A', 'B']); + }); + + it('should accept both single config objects and an array of them', () => { + const conf = {enabled: true, bidders: ['A', 'B']}; + expect(getS2SBidderSet(conf)).to.eql(getS2SBidderSet([conf])); + }); + }); + + describe('separation of client and server bidders', () => { + let s2sBidders, getS2SBidders; + beforeEach(() => { + s2sBidders = null; + getS2SBidders = sinon.stub(); + getS2SBidders.callsFake(() => s2sBidders); + }) + + describe('partitionBidders', () => { + let adUnits; + + beforeEach(() => { + adUnits = [{ + bids: [{ + bidder: 'A' + }, { + bidder: 'B' + }] + }, { + bids: [{ + bidder: 'A', + }, { + bidder: 'C' + }] + }]; + }); + + function partition(adUnits, s2sConfigs) { + return _partitionBidders(adUnits, s2sConfigs, {getS2SBidders}) + } + + Object.entries({ + 'all client': { + s2s: [], + expected: { + [PARTITIONS.CLIENT]: ['A', 'B', 'C'], + [PARTITIONS.SERVER]: [] + } + }, + 'all server': { + s2s: ['A', 'B', 'C'], + expected: { + [PARTITIONS.CLIENT]: [], + [PARTITIONS.SERVER]: ['A', 'B', 'C'] + } + }, + 'mixed': { + s2s: ['B', 'C'], + expected: { + [PARTITIONS.CLIENT]: ['A'], + [PARTITIONS.SERVER]: ['B', 'C'] + } + } + }).forEach(([test, {s2s, expected}]) => { + it(`should partition ${test} requests`, () => { + s2sBidders = new Set(s2s); + const s2sConfig = {}; + expect(partition(adUnits, s2sConfig)).to.eql(expected); + sinon.assert.calledWith(getS2SBidders, sinon.match.same(s2sConfig)); + }); + }); + }); + + describe('filterBidsForAdUnit', () => { + function filterBids(bids, s2sConfig) { + return _filterBidsForAdUnit(bids, s2sConfig, {getS2SBidders}); + } + it('should not filter any bids when s2sConfig == null', () => { + const bids = ['untouched', 'data']; + expect(filterBids(bids)).to.eql(bids); + }); + + it('should remove bids that have bidder not present in s2sConfig', () => { + s2sBidders = new Set('A', 'B'); + const s2sConfig = {}; + expect(filterBids(['A', 'C', 'D'].map((code) => ({bidder: code})), s2sConfig)).to.eql([{bidder: 'A'}]); + sinon.assert.calledWith(getS2SBidders, sinon.match.same(s2sConfig)); + }) + }); + }); }); diff --git a/test/spec/unit/core/auctionIndex_spec.js b/test/spec/unit/core/auctionIndex_spec.js new file mode 100644 index 00000000000..f00e2cd281f --- /dev/null +++ b/test/spec/unit/core/auctionIndex_spec.js @@ -0,0 +1,129 @@ +import {AuctionIndex} from '../../../../src/auctionIndex.js'; + +describe('auction index', () => { + let index, auctions; + + function mockAuction(id, adUnits, bidderRequests) { + return { + getAuctionId() { return id }, + getAdUnits() { return adUnits; }, + getBidRequests() { return bidderRequests; } + } + } + + beforeEach(() => { + auctions = []; + index = new AuctionIndex(() => auctions); + }) + + describe('getAuction', () => { + beforeEach(() => { + auctions = [mockAuction('a1'), mockAuction('a2')]; + }); + + it('should find auctions by auctionId', () => { + expect(index.getAuction({auctionId: 'a1'})).to.equal(auctions[0]); + }); + + it('should return undef if auction is missing', () => { + expect(index.getAuction({auctionId: 'missing'})).to.be.undefined; + }); + + it('should return undef if no auctionId is provided', () => { + expect(index.getAuction({})).to.be.undefined; + }); + }); + + describe('getAdUnit', () => { + let adUnits; + + beforeEach(() => { + adUnits = [{transactionId: 'au1'}, {transactionId: 'au2'}]; + auctions = [ + mockAuction('a1', [adUnits[0], {}]), + mockAuction('a2', [adUnits[1]]) + ]; + }); + + it('should find adUnits by transactionId', () => { + expect(index.getAdUnit({transactionId: 'au2'})).to.equal(adUnits[1]); + }); + + it('should return undefined if adunit is missing', () => { + expect(index.getAdUnit({transactionId: 'missing'})).to.be.undefined; + }); + + it('should return undefined if no transactionId is provided', () => { + expect(index.getAdUnit({})).to.be.undefined; + }); + }); + + describe('getBidRequest', () => { + let bidRequests; + beforeEach(() => { + bidRequests = [{bidId: 'b1'}, {bidId: 'b2'}]; + auctions = [ + mockAuction('a1', [], [{bids: [bidRequests[0], {}]}]), + mockAuction('a2', [], [{bids: [bidRequests[1]]}]) + ] + }); + + it('should find bidRequests by requestId', () => { + expect(index.getBidRequest({requestId: 'b2'})).to.equal(bidRequests[1]); + }); + + it('should return undef if bidRequest is missing', () => { + expect(index.getBidRequest({requestId: 'missing'})).to.be.undefined; + }); + + it('should return undef if no requestId is provided', () => { + expect(index.getBidRequest({})).to.be.undefined; + }); + }); + + describe('getMediaTypes', () => { + let bidderRequests, mediaTypes, adUnits; + + beforeEach(() => { + mediaTypes = [{mockMT: '1'}, {mockMT: '2'}, {mockMT: '3'}, {mockMT: '4'}] + adUnits = [ + {transactionId: 'au1', mediaTypes: mediaTypes[0]}, + {transactionId: 'au2', mediaTypes: mediaTypes[1]} + ] + bidderRequests = [ + {bidderRequestId: 'ber1', bids: [{bidId: 'b1', mediaTypes: mediaTypes[2], transactionId: 'au1'}, {}]}, + {bidderRequestId: 'ber2', bids: [{bidId: 'b2', mediaTypes: mediaTypes[3], transactionId: 'au2'}]} + ] + auctions = [ + mockAuction('a1', [adUnits[0]], [bidderRequests[0], {}]), + mockAuction('a2', [adUnits[1]], [bidderRequests[1]]) + ] + }); + + it('should find mediaTypes by transactionId', () => { + expect(index.getMediaTypes({transactionId: 'au2'})).to.equal(mediaTypes[1]); + }); + + it('should find mediaTypes by requestId', () => { + expect(index.getMediaTypes({requestId: 'b1'})).to.equal(mediaTypes[2]); + }); + + it('should give precedence to request.mediaTypes over adUnit.mediaTypes', () => { + expect(index.getMediaTypes({requestId: 'b2', transactionId: 'au2'})).to.equal(mediaTypes[3]); + }); + + it('should return undef if requestId and transactionId do not match', () => { + expect(index.getMediaTypes({requestId: 'b1', transactionId: 'au2'})).to.be.undefined; + }); + + it('should return undef if no params are provided', () => { + expect(index.getMediaTypes({})).to.be.undefined; + }); + + ['requestId', 'transactionId'].forEach(param => { + it(`should return undef if ${param} is missing`, () => { + expect(index.getMediaTypes({[param]: 'missing'})).to.be.undefined; + }); + }) + }); +}); diff --git a/test/spec/unit/core/bidderFactory_spec.js b/test/spec/unit/core/bidderFactory_spec.js index 4dc79deaf85..067f9abe424 100644 --- a/test/spec/unit/core/bidderFactory_spec.js +++ b/test/spec/unit/core/bidderFactory_spec.js @@ -7,7 +7,10 @@ import * as utils from 'src/utils.js'; import { config } from 'src/config.js'; import { server } from 'test/mocks/xhr.js'; import CONSTANTS from 'src/constants.json'; -import events from 'src/events.js'; +import * as events from 'src/events.js'; +import {hook} from '../../../../src/hook.js'; +import {auctionManager} from '../../../../src/auctionManager.js'; +import {stubAuctionIndex} from '../../../helpers/indexStub.js'; const CODE = 'sampleBidder'; const MOCK_BIDS_REQUEST = { @@ -43,6 +46,10 @@ describe('bidders created by newBidder', function () { let addBidResponseStub; let doneStub; + before(() => { + hook.ready(); + }); + beforeEach(function () { spec = { code: CODE, @@ -59,17 +66,22 @@ describe('bidders created by newBidder', function () { describe('when the ajax response is irrelevant', function () { let ajaxStub; let getConfigSpy; + let aliasRegistryStub, aliasRegistry; beforeEach(function () { ajaxStub = sinon.stub(ajax, 'ajax'); addBidResponseStub.reset(); getConfigSpy = sinon.spy(config, 'getConfig'); doneStub.reset(); + aliasRegistry = {}; + aliasRegistryStub = sinon.stub(adapterManager, 'aliasRegistry'); + aliasRegistryStub.get(() => aliasRegistry); }); afterEach(function () { ajaxStub.restore(); getConfigSpy.restore(); + aliasRegistryStub.restore(); }); it('should let registerSyncs run with invalid alias and aliasSync enabled', function () { @@ -116,6 +128,7 @@ describe('bidders created by newBidder', function () { }); 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); }); @@ -548,6 +561,30 @@ describe('bidders created by newBidder', function () { expect(logErrorSpy.calledOnce).to.equal(true); }); + + 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); + + bidder.callBids(MOCK_BIDS_REQUEST, addBidResponseStub, doneStub, ajaxStub, onTimelyResponseStub, wrappedCallback); + + expect(addBidResponseStub.called).to.be.false; + }); }); describe('when the ajax call fails', function () { @@ -783,7 +820,7 @@ describe('registerBidder', function () { describe('validate bid response: ', function () { let spec; - let bidder; + let indexStub, adUnits, bidderRequests; let addBidResponseStub; let doneStub; let ajaxStub; @@ -824,25 +861,34 @@ describe('validate bid response: ', function () { 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})) }); afterEach(function () { ajaxStub.restore(); logErrorSpy.restore(); + indexStub.restore; }); it('should add native bids that do have required assets', function () { + adUnits = [{ + transactionId: 'au', + nativeParams: { + title: {'required': true}, + } + }] let bidRequest = { bids: [{ bidId: '1', auctionId: 'first-bid-id', adUnitCode: 'mock/placement', + transactionId: 'au', params: { param: 5 }, - nativeParams: { - title: {'required': true}, - }, mediaType: 'native', }] }; @@ -869,21 +915,24 @@ describe('validate bid response: ', function () { }); it('should not add native bids that do not have required assets', function () { + adUnits = [{ + transactionId: 'au', + nativeParams: { + title: {'required': true}, + }, + }]; let bidRequest = { bids: [{ bidId: '1', auctionId: 'first-bid-id', adUnitCode: 'mock/placement', + transactionId: 'au', params: { param: 5 }, - nativeParams: { - title: {'required': true}, - }, mediaType: 'native', }] }; - let bids1 = Object.assign({}, bids[0], { @@ -905,17 +954,21 @@ describe('validate bid response: ', function () { }); 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 }, - mediaTypes: { - video: {context: 'outstream'} - } }] }; @@ -951,7 +1004,7 @@ describe('validate bid response: ', function () { sizes: [[300, 250]], }] }; - + bidderRequests = [bidRequest]; let bids1 = Object.assign({}, bids[0], { diff --git a/test/spec/unit/core/bidderSettings_spec.js b/test/spec/unit/core/bidderSettings_spec.js new file mode 100644 index 00000000000..ece18040d1e --- /dev/null +++ b/test/spec/unit/core/bidderSettings_spec.js @@ -0,0 +1,123 @@ +import {bidderSettings, ScopedSettings} from '../../../../src/bidderSettings.js'; +import {expect} from 'chai'; +import * as prebidGlobal from '../../../../src/prebidGlobal'; +import sinon from 'sinon'; + +describe('ScopedSettings', () => { + let data; + let settings; + + beforeEach(() => { + settings = new ScopedSettings(() => data, 'fallback'); + }); + + describe('get', () => { + it('should retrieve setting from scope', () => { + data = { + scope: {key: 'value'} + }; + expect(settings.get('scope', 'key')).to.equal('value'); + }); + + it('should fallback to fallback scope', () => { + data = { + fallback: { + key: 'value' + } + }; + expect(settings.get('scope', 'key')).to.equal('value'); + }); + + it('should retrieve from default scope if scope is null', () => { + data = { + fallback: { + key: 'value' + } + }; + + expect(settings.get(null, 'key')).to.equal('value'); + }); + + it('should not fall back if own setting has a falsy value', () => { + data = { + scope: { + key: false, + }, + fallback: { + key: true + } + } + expect(settings.get('scope', 'key')).to.equal(false); + }) + }); + + describe('getOwn', () => { + it('should not fall back to default scope', () => { + data = { + fallback: { + key: 'value' + } + }; + expect(settings.getOwn('missing', 'key')).to.be.undefined; + }); + + it('should use default if scope is null', () => { + data = { + fallback: { + key: 'value' + } + }; + expect(settings.getOwn(null, 'key')).to.equal('value'); + }); + }); + + describe('getScopes', () => { + it('should return all top-level keys except the default scope', () => { + data = { + fallback: {}, + scope1: {}, + scope2: {}, + }; + expect(settings.getScopes()).to.have.members(['scope1', 'scope2']); + }); + }); + + describe('settingsFor', () => { + it('should merge with default scope', () => { + data = { + fallback: { + dkey: 'value' + }, + scope: { + skey: 'value' + } + } + expect(settings.settingsFor('scope')).to.eql({ + dkey: 'value', + skey: 'value' + }) + }) + }); +}); + +describe('bidderSettings', () => { + let sandbox; + beforeEach(() => { + sandbox = sinon.sandbox.create(); + sandbox.stub(prebidGlobal, 'getGlobal').returns({ + bidderSettings: { + scope: { + key: 'value' + } + } + }); + }) + + afterEach(() => { + sandbox.restore(); + }) + + it('should fetch data from getGlobal().bidderSettings', () => { + expect(bidderSettings.get('scope', 'key')).to.equal('value'); + }) +}); diff --git a/test/spec/unit/core/consentHandler_spec.js b/test/spec/unit/core/consentHandler_spec.js new file mode 100644 index 00000000000..082ff34f90c --- /dev/null +++ b/test/spec/unit/core/consentHandler_spec.js @@ -0,0 +1,59 @@ +import {ConsentHandler} from '../../../../src/consentHandler.js'; + +describe('Consent data handler', () => { + let handler; + beforeEach(() => { + handler = new ConsentHandler(); + }) + + it('should be disabled, return null data on init', () => { + expect(handler.enabled).to.be.false; + expect(handler.getConsentData()).to.equal(null); + }) + + it('should resolve promise to null when disabled', () => { + return handler.promise.then((data) => { + expect(data).to.equal(null); + }); + }); + + it('should return data after setConsentData', () => { + const data = {consent: 'string'}; + handler.enable(); + handler.setConsentData(data); + expect(handler.getConsentData()).to.equal(data); + }); + + it('should resolve .promise to data after setConsentData', (done) => { + let actual = null; + const data = {consent: 'string'}; + handler.enable(); + handler.promise.then((d) => actual = d); + setTimeout(() => { + expect(actual).to.equal(null); + handler.setConsentData(data); + setTimeout(() => { + expect(actual).to.equal(data); + done(); + }) + }) + }); + + it('should resolve .promise to new data if setConsentData is called a second time', (done) => { + let actual = null; + const d1 = {data: '1'}; + const d2 = {data: '2'}; + handler.enable(); + handler.promise.then((d) => actual = d); + handler.setConsentData(d1); + setTimeout(() => { + expect(actual).to.equal(d1); + handler.setConsentData(d2); + handler.promise.then((d) => actual = d); + setTimeout(() => { + expect(actual).to.equal(d2); + done(); + }) + }) + }); +}) diff --git a/test/spec/unit/core/storageManager_spec.js b/test/spec/unit/core/storageManager_spec.js index 5bb766217f5..74a3b3b023f 100644 --- a/test/spec/unit/core/storageManager_spec.js +++ b/test/spec/unit/core/storageManager_spec.js @@ -1,8 +1,19 @@ -import { resetData, getCoreStorageManager, storageCallbacks, getStorageManager } from 'src/storageManager.js'; +import { + resetData, + getCoreStorageManager, + storageCallbacks, + getStorageManager, + newStorageManager +} from 'src/storageManager.js'; import { config } from 'src/config.js'; import * as utils from 'src/utils.js'; +import {hook} from '../../../../src/hook.js'; describe('storage manager', function() { + before(() => { + hook.ready(); + }); + beforeEach(function() { resetData(); }); @@ -95,4 +106,62 @@ describe('storage manager', function() { expect(localStorage.getItem('unrelated')).to.be.eq('dummy'); }); }); + + describe('when bidderSettings.allowStorage is defined', () => { + const DENIED_BIDDER = 'denied-bidder'; + const DENY_KEY = 'storageAllowed'; + + const COOKIE = 'test-cookie'; + const LS_KEY = 'test-localstorage'; + + function mockBidderSettings() { + return { + get(bidder, key) { + if (bidder === DENIED_BIDDER && key === DENY_KEY) { + return false; + } else { + return undefined; + } + } + } + } + + Object.entries({ + disallowed: [DENIED_BIDDER, false], + allowed: ['allowed-bidder', true] + }).forEach(([test, [bidderCode, shouldWork]]) => { + describe(`for ${test} bidders`, () => { + let mgr; + + beforeEach(() => { + mgr = newStorageManager({bidderCode: bidderCode}, {bidderSettings: mockBidderSettings()}); + }) + + afterEach(() => { + mgr.setCookie(COOKIE, 'delete', new Date().toUTCString()); + mgr.removeDataFromLocalStorage(LS_KEY); + }) + + const testDesc = (desc) => `should ${shouldWork ? '' : 'not'} ${desc}`; + + it(testDesc('allow cookies'), () => { + mgr.setCookie(COOKIE, 'value'); + expect(mgr.getCookie(COOKIE)).to.equal(shouldWork ? 'value' : null); + }); + + it(testDesc('allow localStorage'), () => { + mgr.setDataInLocalStorage(LS_KEY, 'value'); + expect(mgr.getDataFromLocalStorage(LS_KEY)).to.equal(shouldWork ? 'value' : null); + }); + + it(testDesc('report localStorage as available'), () => { + expect(mgr.hasLocalStorage()).to.equal(shouldWork); + }); + + it(testDesc('report cookies as available'), () => { + expect(mgr.cookiesAreEnabled()).to.equal(shouldWork); + }); + }); + }); + }) }); diff --git a/test/spec/unit/core/targeting_spec.js b/test/spec/unit/core/targeting_spec.js index 4eaf414bf85..53aa3b90fa8 100644 --- a/test/spec/unit/core/targeting_spec.js +++ b/test/spec/unit/core/targeting_spec.js @@ -228,6 +228,8 @@ describe('targeting tests', function () { let sandbox; let enableSendAllBids = false; let useBidCache; + let bidCacheFilterFunction; + let undef; beforeEach(function() { sandbox = sinon.sandbox.create(); @@ -242,12 +244,16 @@ describe('targeting tests', function () { if (key === 'useBidCache') { return useBidCache; } + if (key === 'bidCacheFilterFunction') { + return bidCacheFilterFunction; + } return origGetConfig.apply(config, arguments); }); }); afterEach(function () { sandbox.restore(); + bidCacheFilterFunction = undef; }); describe('getAllTargeting', function () { @@ -901,6 +907,93 @@ describe('targeting tests', function () { expect(bids[0].adId).to.equal('adid-2'); }); + it('should use bidCacheFilterFunction', function() { + auctionManagerStub.returns([ + createBidReceived({bidder: 'appnexus', cpm: 7, auctionId: 1, responseTimestamp: 100, adUnitCode: 'code-0', adId: 'adid-1', mediaType: 'banner'}), + createBidReceived({bidder: 'appnexus', cpm: 5, auctionId: 2, responseTimestamp: 102, adUnitCode: 'code-0', adId: 'adid-2', mediaType: 'banner'}), + createBidReceived({bidder: 'appnexus', cpm: 6, auctionId: 1, responseTimestamp: 101, adUnitCode: 'code-1', adId: 'adid-3', mediaType: 'banner'}), + createBidReceived({bidder: 'appnexus', cpm: 8, auctionId: 2, responseTimestamp: 103, adUnitCode: 'code-1', adId: 'adid-4', mediaType: 'banner'}), + createBidReceived({bidder: 'appnexus', cpm: 27, auctionId: 1, responseTimestamp: 100, adUnitCode: 'code-2', adId: 'adid-5', mediaType: 'video'}), + createBidReceived({bidder: 'appnexus', cpm: 25, auctionId: 2, responseTimestamp: 102, adUnitCode: 'code-2', adId: 'adid-6', mediaType: 'video'}), + createBidReceived({bidder: 'appnexus', cpm: 26, auctionId: 1, responseTimestamp: 101, adUnitCode: 'code-3', adId: 'adid-7', mediaType: 'video'}), + createBidReceived({bidder: 'appnexus', cpm: 28, auctionId: 2, responseTimestamp: 103, adUnitCode: 'code-3', adId: 'adid-8', mediaType: 'video'}), + ]); + + let adUnitCodes = ['code-0', 'code-1', 'code-2', 'code-3']; + targetingInstance.setLatestAuctionForAdUnit('code-0', 2); + targetingInstance.setLatestAuctionForAdUnit('code-1', 2); + targetingInstance.setLatestAuctionForAdUnit('code-2', 2); + targetingInstance.setLatestAuctionForAdUnit('code-3', 2); + + // Bid Caching On, No Filter Function + useBidCache = true; + bidCacheFilterFunction = undef; + let bids = targetingInstance.getWinningBids(adUnitCodes); + + expect(bids.length).to.equal(4); + expect(bids[0].adId).to.equal('adid-1'); + expect(bids[1].adId).to.equal('adid-4'); + expect(bids[2].adId).to.equal('adid-5'); + expect(bids[3].adId).to.equal('adid-8'); + + // Bid Caching Off, No Filter Function + useBidCache = false; + bidCacheFilterFunction = undef; + bids = targetingInstance.getWinningBids(adUnitCodes); + + expect(bids.length).to.equal(4); + expect(bids[0].adId).to.equal('adid-2'); + expect(bids[1].adId).to.equal('adid-4'); + expect(bids[2].adId).to.equal('adid-6'); + expect(bids[3].adId).to.equal('adid-8'); + + // Bid Caching On AGAIN, No Filter Function (should be same as first time) + useBidCache = true; + bidCacheFilterFunction = undef; + bids = targetingInstance.getWinningBids(adUnitCodes); + + expect(bids.length).to.equal(4); + expect(bids[0].adId).to.equal('adid-1'); + expect(bids[1].adId).to.equal('adid-4'); + expect(bids[2].adId).to.equal('adid-5'); + expect(bids[3].adId).to.equal('adid-8'); + + // Bid Caching On, with Filter Function to Exclude video + useBidCache = true; + let bcffCalled = 0; + bidCacheFilterFunction = bid => { + bcffCalled++; + return bid.mediaType !== 'video'; + } + bids = targetingInstance.getWinningBids(adUnitCodes); + + expect(bids.length).to.equal(4); + expect(bids[0].adId).to.equal('adid-1'); + expect(bids[1].adId).to.equal('adid-4'); + expect(bids[2].adId).to.equal('adid-6'); + expect(bids[3].adId).to.equal('adid-8'); + // filter function should have been called for each cached bid (4 times) + expect(bcffCalled).to.equal(4); + + // Bid Caching Off, with Filter Function to Exclude video + // - should not use cached bids or call the filter function + useBidCache = false; + bcffCalled = 0; + bidCacheFilterFunction = bid => { + bcffCalled++; + return bid.mediaType !== 'video'; + } + bids = targetingInstance.getWinningBids(adUnitCodes); + + expect(bids.length).to.equal(4); + expect(bids[0].adId).to.equal('adid-2'); + expect(bids[1].adId).to.equal('adid-4'); + expect(bids[2].adId).to.equal('adid-6'); + expect(bids[3].adId).to.equal('adid-8'); + // filter function should not have been called + expect(bcffCalled).to.equal(0); + }); + it('should not use rendered bid to get winning bid', function () { let bidsReceived = [ createBidReceived({bidder: 'appnexus', cpm: 8, auctionId: 1, responseTimestamp: 100, adUnitCode: 'code-0', adId: 'adid-1', status: 'rendered'}), diff --git a/test/spec/unit/pbjs_api_spec.js b/test/spec/unit/pbjs_api_spec.js index 2522887bb98..5302b3b8c74 100644 --- a/test/spec/unit/pbjs_api_spec.js +++ b/test/spec/unit/pbjs_api_spec.js @@ -15,7 +15,10 @@ 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 find from 'core-js-pure/features/array/find.js'; +import {find} from 'src/polyfill.js'; +import {synchronizePromise} from '../../helpers/syncPromise.js'; +import 'src/prebid.js'; +import {hook} from '../../../src/hook.js'; var assert = require('chai').assert; var expect = require('chai').expect; @@ -190,13 +193,21 @@ window.apntag = { } describe('Unit: Prebid Module', function () { - let bidExpiryStub; + let bidExpiryStub, promiseSandbox; + + before(() => { + hook.ready(); + }); + beforeEach(function () { + promiseSandbox = sinon.createSandbox(); + synchronizePromise(promiseSandbox); bidExpiryStub = sinon.stub(filters, 'isBidNotExpired').callsFake(() => true); configObj.setConfig({ useBidCache: true }); }); afterEach(function() { + promiseSandbox.restore(); $$PREBID_GLOBAL$$.adUnits = []; bidExpiryStub.restore(); configObj.setConfig({ useBidCache: false }); @@ -422,6 +433,7 @@ describe('Unit: Prebid Module', function () { let bid; let auction; let ajaxStub; + let indexStub; let cbTimeout = 3000; let targeting; @@ -487,7 +499,8 @@ describe('Unit: Prebid Module', function () { ], 'bidId': '4d0a6829338a07', 'bidderRequestId': '331f3cf3f1d9c8', - 'auctionId': '20882439e3238c' + 'auctionId': '20882439e3238c', + 'transactionId': 'trdiv-gpt-ad-1460505748561-0', } ], 'auctionStart': 1505250713622, @@ -505,6 +518,7 @@ describe('Unit: Prebid Module', function () { let auctionManagerInstance = newAuctionManager(); targeting = newTargeting(auctionManagerInstance); let adUnits = [{ + transactionId: 'trdiv-gpt-ad-1460505748561-0', code: 'div-gpt-ad-1460505748561-0', sizes: [[300, 250], [300, 600]], bids: [{ @@ -516,6 +530,8 @@ describe('Unit: Prebid Module', function () { }]; let adUnitCodes = ['div-gpt-ad-1460505748561-0']; auction = auctionManagerInstance.createAuction({adUnits, adUnitCodes}); + indexStub = sinon.stub(auctionManager, 'index'); + indexStub.get(() => auctionManagerInstance.index); ajaxStub = sinon.stub(ajaxLib, 'ajaxBuilder').callsFake(function() { return function(url, callback) { const fakeResponse = sinon.stub(); @@ -527,6 +543,7 @@ describe('Unit: Prebid Module', function () { afterEach(function () { ajaxStub.restore(); + indexStub.restore(); }); it('should get correct ' + CONSTANTS.TARGETING_KEYS.PRICE_BUCKET + ' when using bid.cpm is between 0 to 5', function() { @@ -566,6 +583,7 @@ describe('Unit: Prebid Module', function () { let cbTimeout = 3000; let auctionManagerInstance; let targeting; + let indexStub; const bannerResponse = { 'version': '0.0.1', @@ -644,6 +662,7 @@ describe('Unit: Prebid Module', function () { } const adUnit = { + transactionId: `tr${code}`, code: code, sizes: [[300, 250], [300, 600]], bids: [{ @@ -656,22 +675,22 @@ describe('Unit: Prebid Module', function () { let _mediaTypes = {}; if (mediaTypes.indexOf('banner') !== -1) { - _mediaTypes['banner'] = { + Object.assign(_mediaTypes, { 'banner': {} - }; + }); } if (mediaTypes.indexOf('video') !== -1) { - _mediaTypes['video'] = { + Object.assign(_mediaTypes, { 'video': { context: 'instream', playerSize: [300, 250] } - }; + }); } if (mediaTypes.indexOf('native') !== -1) { - _mediaTypes['native'] = { + Object.assign(_mediaTypes, { 'native': {} - }; + }); } if (Object.keys(_mediaTypes).length > 0) { @@ -733,35 +752,41 @@ describe('Unit: Prebid Module', function () { before(function () { currentPriceBucket = configObj.getConfig('priceGranularity'); - sinon.stub(adapterManager, 'makeBidRequests').callsFake(() => ([{ - 'bidderCode': 'appnexus', - 'auctionId': '20882439e3238c', - 'bidderRequestId': '331f3cf3f1d9c8', - 'bids': [ - { - 'bidder': 'appnexus', - 'params': { - 'placementId': '10433394' - }, - 'adUnitCode': 'div-gpt-ad-1460505748561-0', - 'sizes': [ - [ - 300, - 250 + sinon.stub(adapterManager, 'makeBidRequests').callsFake(() => { + const br = { + 'bidderCode': 'appnexus', + 'auctionId': '20882439e3238c', + 'bidderRequestId': '331f3cf3f1d9c8', + 'bids': [ + { + 'bidder': 'appnexus', + 'params': { + 'placementId': '10433394' + }, + 'adUnitCode': 'div-gpt-ad-1460505748561-0', + 'transactionId': 'trdiv-gpt-ad-1460505748561-0', + 'sizes': [ + [ + 300, + 250 + ], + [ + 300, + 600 + ] ], - [ - 300, - 600 - ] - ], - 'bidId': '4d0a6829338a07', - 'bidderRequestId': '331f3cf3f1d9c8', - 'auctionId': '20882439e3238c' - } - ], - 'auctionStart': 1505250713622, - 'timeout': 3000 - }])); + 'bidId': '4d0a6829338a07', + 'bidderRequestId': '331f3cf3f1d9c8', + 'auctionId': '20882439e3238c' + } + ], + 'auctionStart': 1505250713622, + 'timeout': 3000 + }; + const au = auction.getAdUnits().find((au) => au.transactionId === br.bids[0].transactionId); + br.bids[0].mediaTypes = Object.assign({}, au.mediaTypes); + return [br]; + }); }); after(function () { @@ -769,8 +794,14 @@ describe('Unit: Prebid Module', function () { adapterManager.makeBidRequests.restore(); }) + beforeEach(() => { + indexStub = sinon.stub(auctionManager, 'index'); + indexStub.get(() => auctionManagerInstance.index); + }); + afterEach(function () { ajaxStub.restore(); + indexStub.restore(); }); it('should get correct ' + CONSTANTS.TARGETING_KEYS.PRICE_BUCKET + ' with cpm between 0 - 5', function() { @@ -1002,12 +1033,7 @@ describe('Unit: Prebid Module', function () { adUnitCode: config.adUnitCodes[0], }; - const event = { - source: { postMessage: sinon.stub() }, - origin: 'origin.sf.com' - }; - - _sendAdToCreative(mockAdObject, event); + _sendAdToCreative(mockAdObject, sinon.stub()); expect(slots[0].spyGetSlotElementId.called).to.equal(false); expect(slots[1].spyGetSlotElementId.called).to.equal(true); @@ -1600,6 +1626,7 @@ describe('Unit: Prebid Module', function () { let auctionArgs; beforeEach(function () { + auctionArgs = null; adUnitsBackup = auction.getAdUnits auctionManagerStub = sinon.stub(auctionManager, 'createAuction').callsFake(function() { auctionArgs = arguments[0]; @@ -1854,11 +1881,39 @@ describe('Unit: Prebid Module', function () { pos: 2 } } + }, + { + code: 'test6', + bids: [], + sizes: [300, 250], + mediaTypes: { + banner: { + sizes: [300, 250], + pos: 0 + } + } }]; $$PREBID_GLOBAL$$.requestBids({ adUnits: adUnit }); expect(auctionArgs.adUnits[0].mediaTypes.banner.pos).to.equal(2); + expect(auctionArgs.adUnits[1].mediaTypes.banner.pos).to.equal(0); + }); + + it(`should allow no bids if 'ortb2Imp' is specified`, () => { + const adUnit = { + code: 'test', + mediaTypes: { + banner: { + sizes: [[300, 250]] + } + }, + ortb2Imp: {} + }; + $$PREBID_GLOBAL$$.requestBids({ + adUnits: [adUnit] + }); + sinon.assert.match(auctionArgs.adUnits[0], adUnit); }); }); @@ -1995,7 +2050,7 @@ describe('Unit: Prebid Module', function () { }); expect(auctionArgs.adUnits.length).to.equal(1); expect(auctionArgs.adUnits[1]).to.not.exist; - assert.ok(logErrorSpy.calledWith("Detected adUnit.code 'bad-ad-unit-2' did not have 'adUnit.bids' defined or 'adUnit.bids' is not an array. Removing adUnit from auction.")); + assert.ok(logErrorSpy.calledWith("adUnit.code 'bad-ad-unit-2' has no 'adUnit.bids' and no 'adUnit.ortb2Imp'. Removing adUnit from auction")); }); }); }); diff --git a/test/spec/unit/secureCreatives_spec.js b/test/spec/unit/secureCreatives_spec.js index cee416bd1be..e3dc21ffd92 100644 --- a/test/spec/unit/secureCreatives_spec.js +++ b/test/spec/unit/secureCreatives_spec.js @@ -1,20 +1,60 @@ import { - _sendAdToCreative, receiveMessage + _sendAdToCreative, getReplier, receiveMessage } from 'src/secureCreatives.js'; +import * as secureCreatives 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'; import * as auctionModule from 'src/auction.js'; import * as native from 'src/native.js'; import {fireNativeTrackers, getAllAssetsMessage} from 'src/native.js'; -import events from 'src/events.js'; +import * as events from 'src/events.js'; import { config as configObj } from 'src/config.js'; +import 'src/prebid.js'; import { expect } from 'chai'; var CONSTANTS = require('src/constants.json'); describe('secureCreatives', () => { + function makeEvent(ev) { + return Object.assign({origin: 'mock-origin', ports: []}, ev) + } + + describe('getReplier', () => { + it('should use source.postMessage if no MessagePort is available', () => { + const ev = { + ports: [], + source: { + postMessage: sinon.spy() + }, + origin: 'mock-origin' + }; + getReplier(ev)('test'); + sinon.assert.calledWith(ev.source.postMessage, JSON.stringify('test')); + }); + + it('should use MesagePort.postMessage if available', () => { + const ev = { + ports: [{ + postMessage: sinon.spy() + }] + } + getReplier(ev)('test'); + sinon.assert.calledWith(ev.ports[0].postMessage, JSON.stringify('test')); + }); + + it('should throw if origin is null and no MessagePort is available', () => { + const ev = { + origin: null, + ports: [], + postMessage: sinon.spy() + } + const reply = getReplier(ev); + expect(() => reply('test')).to.throw(); + }); + }); + describe('_sendAdToCreative', () => { beforeEach(function () { sinon.stub(utils, 'logError'); @@ -40,14 +80,10 @@ describe('secureCreatives', () => { cpm: '1.00', adUnitCode: 'some_dom_id' }; - const event = { - source: { postMessage: sinon.stub() }, - origin: 'origin.sf.com' - }; - - _sendAdToCreative(mockAdObject, event); - expect(JSON.parse(event.source.postMessage.args[0][0]).ad).to.equal(''); - expect(JSON.parse(event.source.postMessage.args[0][0]).adUrl).to.equal('http://creative.prebid.org/1.00'); + 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; }); @@ -141,9 +177,9 @@ describe('secureCreatives', () => { message: 'Prebid Request' }; - const ev = { - data: JSON.stringify(data) - }; + const ev = makeEvent({ + data: JSON.stringify(data), + }); receiveMessage(ev); @@ -168,9 +204,9 @@ describe('secureCreatives', () => { message: 'Prebid Request' }; - const ev = { + const ev = makeEvent({ data: JSON.stringify(data) - }; + }); receiveMessage(ev); @@ -209,9 +245,9 @@ describe('secureCreatives', () => { message: 'Prebid Request' }; - const ev = { + const ev = makeEvent({ data: JSON.stringify(data) - }; + }); receiveMessage(ev); @@ -237,6 +273,38 @@ describe('secureCreatives', () => { configObj.setConfig({'auctionOptions': {}}); }); + + it('should emit AD_RENDER_FAILED if requested missing adId', () => { + const ev = makeEvent({ + data: JSON.stringify({ + message: 'Prebid Request', + adId: 'missing' + }) + }); + receiveMessage(ev); + sinon.assert.calledWith(stubEmit, CONSTANTS.EVENTS.AD_RENDER_FAILED, sinon.match({ + reason: CONSTANTS.AD_RENDER_FAILED_REASON.CANNOT_FIND_AD, + adId: 'missing' + })); + }); + + it('should emit AD_RENDER_FAILED if creative can\'t be sent to rendering frame', () => { + pushBidResponseToAuction({}); + const ev = makeEvent({ + source: { + postMessage: sinon.stub().callsFake(() => { throw new Error(); }) + }, + data: JSON.stringify({ + message: 'Prebid Request', + adId: bidId + }) + }); + receiveMessage(ev) + sinon.assert.calledWith(stubEmit, CONSTANTS.EVENTS.AD_RENDER_FAILED, sinon.match({ + reason: CONSTANTS.AD_RENDER_FAILED_REASON.EXCEPTION, + adId: bidId + })); + }); }); describe('Prebid Native', function() { @@ -249,13 +317,13 @@ describe('secureCreatives', () => { action: 'allAssetRequest' }; - const ev = { + const ev = makeEvent({ data: JSON.stringify(data), source: { postMessage: sinon.stub() }, origin: 'any origin' - }; + }); receiveMessage(ev); @@ -278,13 +346,13 @@ describe('secureCreatives', () => { action: 'allAssetRequest' }; - const ev = { + const ev = makeEvent({ data: JSON.stringify(data), source: { postMessage: sinon.stub() }, origin: 'any origin' - }; + }); receiveMessage(ev); @@ -322,13 +390,13 @@ describe('secureCreatives', () => { action: 'allAssetRequest' }; - const ev = { + const ev = makeEvent({ data: JSON.stringify(data), source: { postMessage: sinon.stub() }, origin: 'any origin' - }; + }); receiveMessage(ev); @@ -366,13 +434,13 @@ describe('secureCreatives', () => { action: 'click', }; - const ev = { + const ev = makeEvent({ data: JSON.stringify(data), source: { postMessage: sinon.stub() }, origin: 'any origin' - }; + }); receiveMessage(ev); @@ -395,5 +463,56 @@ describe('secureCreatives', () => { expect(adResponse).to.have.property('status', CONSTANTS.BID_STATUS.RENDERED); }); }); + + describe('Prebid Event', () => { + Object.entries({ + 'unrendered': [false, (bid) => { delete bid.status; }], + 'rendered': [true, (bid) => { bid.status = CONSTANTS.BID_STATUS.RENDERED }] + }).forEach(([test, [shouldEmit, prepBid]]) => { + describe(`for ${test} bids`, () => { + beforeEach(() => { + prepBid(adResponse); + pushBidResponseToAuction(adResponse); + }); + + it(`should${shouldEmit ? ' ' : ' not '}emit AD_RENDER_FAILED`, () => { + const event = makeEvent({ + data: JSON.stringify({ + message: 'Prebid Event', + event: CONSTANTS.EVENTS.AD_RENDER_FAILED, + adId: bidId, + info: { + reason: 'Fail reason', + message: 'Fail message', + }, + }) + }); + receiveMessage(event); + expect(stubEmit.calledWith(CONSTANTS.EVENTS.AD_RENDER_FAILED, { + adId: bidId, + bid: adResponse, + reason: 'Fail reason', + message: 'Fail message' + })).to.equal(shouldEmit); + }); + + it(`should${shouldEmit ? ' ' : ' not '}emit AD_RENDER_SUCCEEDED`, () => { + const event = makeEvent({ + data: JSON.stringify({ + message: 'Prebid Event', + event: CONSTANTS.EVENTS.AD_RENDER_SUCCEEDED, + adId: bidId, + }) + }); + receiveMessage(event); + expect(stubEmit.calledWith(CONSTANTS.EVENTS.AD_RENDER_SUCCEEDED, { + adId: bidId, + bid: adResponse, + doc: null + })).to.equal(shouldEmit); + }); + }); + }); + }); }); }); diff --git a/test/spec/utils_spec.js b/test/spec/utils_spec.js index 898b79cdcb5..50a95b079c9 100644 --- a/test/spec/utils_spec.js +++ b/test/spec/utils_spec.js @@ -2,7 +2,7 @@ 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 {waitForElementToLoad} from 'src/utils.js'; +import {deepEqual, waitForElementToLoad} from 'src/utils.js'; var assert = require('assert'); @@ -1178,6 +1178,13 @@ describe('Utils', function () { } expect(utils.deepEqual(obj1, obj2)).to.equal(false); }); + it('should check types if {matchTypes: true}', () => { + function Typed(obj) { + Object.assign(this, obj); + } + const obj = {key: 'value'}; + expect(deepEqual({outer: obj}, {outer: new Typed(obj)}, {checkTypes: true})).to.be.false; + }); describe('cyrb53Hash', function() { it('should return the same hash for the same string', function() { diff --git a/test/spec/videoCache_spec.js b/test/spec/videoCache_spec.js index 554db3ebe4e..34e9bed04b6 100644 --- a/test/spec/videoCache_spec.js +++ b/test/spec/videoCache_spec.js @@ -2,6 +2,8 @@ import chai from 'chai'; import { getCacheUrl, store } from 'src/videoCache.js'; import { config } from 'src/config.js'; import { server } from 'test/mocks/xhr.js'; +import {auctionManager} from '../../src/auctionManager.js'; +import {AuctionIndex} from '../../src/auctionIndex.js'; const should = chai.should(); @@ -32,23 +34,6 @@ function getMockBid(bidder, auctionId, bidderRequestId) { }; } -function getMockBidRequest(bidder = 'appnexus', auctionId = '173afb6d132ba3', bidderRequestId = '3d1063078dfcc8') { - return { - 'bidderCode': bidder, - 'auctionId': auctionId, - 'bidderRequestId': bidderRequestId, - 'tid': '437fbbf5-33f5-487a-8e16-a7112903cfe5', - 'bids': [getMockBid(bidder, auctionId, bidderRequestId)], - 'auctionStart': 1510852447530, - 'timeout': 5000, - 'src': 's2s', - 'doneCbCallCount': 0, - 'refererInfo': { - 'referer': 'http://mytestpage.com' - } - } -} - describe('The video cache', function () { function assertError(callbackSpy) { callbackSpy.calledOnce.should.equal(true); @@ -240,7 +225,7 @@ describe('The video cache', function () { JSON.parse(request.requestBody).should.deep.equal(payload); }); - it('should include additional params in request payload should config.cache.vasttrack be true and bidderRequest argument was defined', () => { + it('should include additional params in request payload should config.cache.vasttrack be true - with timestamp', () => { config.setConfig({ cache: { url: 'https://prebid.adnxs.com/pbc/v1/cache', @@ -269,7 +254,21 @@ describe('The video cache', function () { auctionId: '1234-56789-abcde' }]; - store(bids, function () { }, getMockBidRequest()); + const stub = sinon.stub(auctionManager, 'index'); + stub.get(() => new AuctionIndex(() => [{ + getAuctionId() { + return '1234-56789-abcde'; + }, + getAuctionStart() { + return 1510852447530; + } + }])) + try { + store(bids, function () { }); + } finally { + stub.restore(); + } + const request = server.requests[0]; request.method.should.equal('POST'); request.url.should.equal('https://prebid.adnxs.com/pbc/v1/cache'); diff --git a/test/spec/video_spec.js b/test/spec/video_spec.js index 3ce8ba081da..61621c7ec42 100644 --- a/test/spec/video_spec.js +++ b/test/spec/video_spec.js @@ -1,113 +1,103 @@ import { isValidVideoBid } from 'src/video.js'; +import {hook} from '../../src/hook.js'; +import {stubAuctionIndex} from '../helpers/indexStub.js'; describe('video.js', function () { + before(() => { + hook.ready(); + }); + it('validates valid instream bids', function () { const bid = { adId: '456xyz', vastUrl: 'http://www.example.com/vastUrl', - requestId: '123abc' + transactionId: 'au' }; - const bidRequests = [{ - bids: [{ - bidId: '123abc', - bidder: 'appnexus', - mediaTypes: { - video: { context: 'instream' } - } - }] + const adUnits = [{ + transactionId: 'au', + mediaTypes: { + video: {context: 'instream'} + } }]; - const valid = isValidVideoBid(bid, bidRequests); + const valid = isValidVideoBid(bid, {index: stubAuctionIndex({adUnits})}); expect(valid).to.equal(true); }); it('catches invalid instream bids', function () { const bid = { - requestId: '123abc' + transactionId: 'au' }; - const bidRequests = [{ - bids: [{ - bidId: '123abc', - bidder: 'appnexus', - mediaTypes: { - video: { context: 'instream' } - } - }] + const adUnits = [{ + transactionId: 'au', + mediaTypes: { + video: {context: 'instream'} + } }]; - const valid = isValidVideoBid(bid, bidRequests); + const valid = isValidVideoBid(bid, {index: stubAuctionIndex({adUnits})}); expect(valid).to.equal(false); }); it('catches invalid bids when prebid-cache is disabled', function () { - const bidRequests = [{ - bids: [{ - bidder: 'vastOnlyVideoBidder', - mediaTypes: { video: {} }, - }] + const adUnits = [{ + transactionId: 'au', + bidder: 'vastOnlyVideoBidder', + mediaTypes: {video: {}}, }]; - const valid = isValidVideoBid({ vastXml: 'vast' }, bidRequests); + const valid = isValidVideoBid({ transactionId: 'au', vastXml: 'vast' }, {index: stubAuctionIndex({adUnits})}); expect(valid).to.equal(false); }); it('validates valid outstream bids', function () { const bid = { - requestId: '123abc', + transactionId: 'au', renderer: { url: 'render.url', render: () => true, } }; - const bidRequests = [{ - bids: [{ - bidId: '123abc', - bidder: 'appnexus', - mediaTypes: { - video: { context: 'outstream' } - } - }] + const adUnits = [{ + transactionId: 'au', + mediaTypes: { + video: {context: 'outstream'} + } }]; - const valid = isValidVideoBid(bid, bidRequests); + const valid = isValidVideoBid(bid, {index: stubAuctionIndex({adUnits})}); expect(valid).to.equal(true); }); it('validates valid outstream bids with a publisher defined renderer', function () { const bid = { - requestId: '123abc', + transactionId: 'au', }; - const bidRequests = [{ - bids: [{ - bidId: '123abc', - bidder: 'appnexus', - mediaTypes: { - video: { - context: 'outstream', - renderer: { - url: 'render.url', - render: () => true, - } - } + const adUnits = [{ + transactionId: 'au', + mediaTypes: { + video: { + context: 'outstream', } - }] + }, + renderer: { + url: 'render.url', + render: () => true, + } }]; - const valid = isValidVideoBid(bid, bidRequests); + const valid = isValidVideoBid(bid, {index: stubAuctionIndex({adUnits})}); expect(valid).to.equal(true); }); it('catches invalid outstream bids', function () { const bid = { - requestId: '123abc' + transactionId: 'au', }; - const bidRequests = [{ - bids: [{ - bidId: '123abc', - bidder: 'appnexus', - mediaTypes: { - video: { context: 'outstream' } - } - }] + const adUnits = [{ + transactionId: 'au', + mediaTypes: { + video: {context: 'outstream'} + } }]; - const valid = isValidVideoBid(bid, bidRequests); + 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 e535154b799..8b8c9fd3a0f 100644 --- a/test/test_deps.js +++ b/test/test_deps.js @@ -1,3 +1,9 @@ +window.process = { + env: { + NODE_ENV: 'production' + } +}; + require('test/helpers/prebidGlobal.js'); require('test/mocks/adloaderStub.js'); require('test/mocks/xhr.js'); diff --git a/webpack.conf.js b/webpack.conf.js index a738a2a0868..5269f5300f5 100644 --- a/webpack.conf.js +++ b/webpack.conf.js @@ -2,19 +2,11 @@ var prebid = require('./package.json'); var path = require('path'); var webpack = require('webpack'); var helpers = require('./gulpHelpers.js'); -var RequireEnsureWithoutJsonp = require('./plugins/RequireEnsureWithoutJsonp.js'); var { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer'); var argv = require('yargs').argv; -var allowedModules = require('./allowedModules.js'); - -// list of module names to never include in the common bundle chunk -var neverBundle = [ - 'AnalyticsAdapter.js' -]; var plugins = [ - new RequireEnsureWithoutJsonp(), - new webpack.EnvironmentPlugin(['LiveConnectMode']) + new webpack.EnvironmentPlugin({'LiveConnectMode': null}) ]; if (argv.analyze) { @@ -23,27 +15,8 @@ if (argv.analyze) { ) } -plugins.push( // this plugin must be last so it can be easily removed for karma unit tests - new webpack.optimize.CommonsChunkPlugin({ - name: 'prebid', - filename: 'prebid-core.js', - minChunks: function(module) { - return ( - ( - module.context && module.context.startsWith(path.resolve('./src')) && - !(module.resource && neverBundle.some(name => module.resource.includes(name))) - ) || - ( - module.resource && (allowedModules.src.concat(['core-js'])).some( - name => module.resource.includes(path.resolve('./node_modules/' + name)) - ) - ) - ); - } - }) -); - module.exports = { + mode: 'production', devtool: 'source-map', resolve: { modules: [ @@ -51,8 +24,26 @@ module.exports = { 'node_modules' ], }, + entry: (() => { + const entry = { + 'prebid-core': { + import: './src/prebid.js' + } + }; + const selectedModules = new Set(helpers.getArgModules()); + Object.entries(helpers.getModules()).forEach(([fn, mod]) => { + if (selectedModules.size === 0 || selectedModules.has(mod)) { + entry[mod] = { + import: fn, + dependOn: 'prebid-core' + } + } + }); + return entry; + })(), output: { - jsonpFunction: prebid.globalVarName + 'Chunk' + chunkLoadingGlobal: prebid.globalVarName + 'Chunk', + chunkLoading: 'jsonp', }, module: { rules: [ @@ -77,5 +68,9 @@ module.exports = { } ] }, + optimization: { + usedExports: true, + sideEffects: true, + }, plugins };